FastAPI-8:Web层
8 Web层
本章将进一步介绍FastAPI应用程序的顶层(也可称为接口层或路由器层)及其与服务层和数据层的集成。
一般来说,我们如何处理信息?与大多数网站一样,我们的网站将提供以下方法:
- 检索
- 创建
- 修改
- 替换
- 删除
8.1 插曲: 自顶向下、自底向上、中间向外?(Top-Down, Bottom-Up, Middle-Out)
在设计网站时,你可以从以下几个方面入手:
- 网络层,然后向下
- 数据层,然后向上
- 服务层,双向展开
您是否已经安装了一个数据库,并装载了大量数据,只是苦于没有办法与世界分享?如果是这样,您可能想先处理数据层的代码和测试,然后再处理服务层,最后再编写网络层。
如果你遵循领域驱动设计,你可能会从中间的服务层开始,定义你的核心实体和数据模型。或者,你可能想先开发网络接口,在知道对下层的期望之前,假造对下层的调用。
你可以在这些书中找到很好的设计讨论和建议:
• Clean Architectures in Python by Leonardo Giordani (Digital Cat Books)
• Architecture Patterns with Python by Harry J.W. Percival and Bob Gregory (O’Reilly)
• Microservice APIs by José Haro Peralta (Manning)
以上书籍均可以找ding钉或V信: pythontesting给包烟钱获取。
在这些资料和其他资料中,您会看到六边形架构、端口和适配器等术语。至于如何继续,您的选择在很大程度上取决于您已经掌握了哪些数据,以及您想如何开展网站建设工作。
在本书中,我将采取网络先行的方法,一步一步地从基本部分开始,并在过程中根据需要添加其他部分。有时实验有效,有时无效。我会避免一开始就把所有东西都塞进这个网络层的冲动。
这个网络层只是在用户和服务之间传递数据的一种方式。还有其他方式,如 CLI 或软件开发工具包(SDK)。在其他框架中,这种网络层可能被称为视图层或表现层。
8.2 RESTful API设计
HTTP是在网络客户端和服务器之间获取命令和数据的一种方式。RESTful 设计包含以下核心组件:
- 资源(Resources):应用程序管理的数据元素
- IDs:唯一的资源标识符
- URL:结构化资源和ID字符串
- 动词或操作:用于不同目的的URL术语:
- GET:检索资源。
- POST:创建新资源。
- PUT:完全替换一个资源。
- PATCH:部分替换一个资源。
- DELETE:删除资源。
关于PUT和PATCH的相对优缺点,大家会有不同看法。如果不需要区分部分修改和完全修改(替换),则可能不需要两者。
将动词与包含资源和ID的URL结合起来的一般RESTful规则使用这些路径参数模式(URL 中 / 之间的内容):
- verb /resource/:对所有资源类型的资源使用动词。
- verb /resource/id:对ID为id的资源使用动词。
使用本书的示例数据,对端点/thing的GET请求将返回所有探索者的数据,但对/thing/abc的GET请求将只提供ID为abc的事物资源的数据。
最后,网络请求通常会携带更多信息,用于执行以下操作:
- 对结果排序
- 将结果分页
- 执行其他功能
这些参数有时可以表示为路径参数(加在末尾,在另一个/之后),但通常也包含在查询参数中(在 URL 的 ? 之后的 var=val 东西)。由于 URL 有大小限制,大型请求通常在 HTTP 主体中传达。
大多数作者建议在命名资源以及相关命名空间(如 API 部分和数据库表)时使用复数。我曾长期遵循这一建议,但现在觉得单名更简单,原因有很多(包括英语语言的怪异性):
- 有些单词是自己的复数:series、fish
- 有些词有不规则的复数:children、people
- 很多地方都需要定制的单复数转换代码
出于这些原因,我在本书的很多地方都使用了单数命名方案。这有悖于 RESTful 的通常建议,所以如果你不同意,请忽略这一点。
8.3文件和目录网站布局
我们的数据主要涉及生物和探险者。我们在Python文件中定义所有URLs及其用于访问数据的FastAPI路径函数。
首先,在你的机器上选择一个目录。将其命名为fastapi,或者任何能帮助你记住在哪里使用本书代码的名字。在其中创建以下子目录:
- src:包含所有网站代码
- web: FastAPI 网络层
- service:业务逻辑层
- data: 存储接口层
- model:Pydantic 模型定义
- fake: 早期硬连接(stub)数据
每个目录有三个文件:
- init.py:需要将此目录视为软件包
- creature.py:本层的Creature代码
- explorer.py:本层的资源管理器代码
关于如何布局开发网站,众说纷纭。本设计旨在显示层与层之间的分隔,并为将来的添加预留空间。
8.4 第一个网站代码
本节将讨论如何使用FastAPI来编写 RESTful API 网站的请求和响应。然后,我们将开始把这些代码应用到我们实际的、越来越复杂的网站中。
新建一个顶级的main.py程序,用于启动Uvicorn程序和FastAPI软件包。
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def top():
return "top here"
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", reload=True)
再添加一个节点:
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def top():
return "top here"
@app.get("/echo/{thing}")
def echo(thing):
return f"echoing {thing}"
if __name__ == "__main__":
uvicorn.run("main:app", reload=True)
8.5 请求
HTTP请求由一个文本header和一个或多个body部分组成。
FastAPI的依赖注入在这方面尤其有用。数据可能来自HTTP消息的不同部分,您已经看到了如何指定一个或多个依赖项来说明数据的位置:
- Header:在HTTP头中
- Path:在URL中
- Query:URL中的?后面部分
- Body:HTTP 主体中
其他更间接的来源包括以下内容:
- 环境变量
- 配置设置
8.6 多路由
在web目录下(与目前一直在修改的main.py文件位于同一目录),创建explorer.py:。
from fastapi import APIRouter
router = APIRouter(prefix = "/explorer")
@router.get("/")
def top():
return "top explorer endpoint"
修改main.py:
import uvicorn
from fastapi import FastAPI
import explorer
app = FastAPI()
app.include_router(explorer.router)
@app.get("/")
def top():
return "top here"
@app.get("/echo/{thing}")
def echo(thing):
return f"echoing {thing}"
if __name__ == "__main__":
uvicorn.run("main:app", reload=True)
测试新的子路由器
$ http -b localhost:8000/explorer/
"top explorer endpoint"
8.7 定义数据模型
首先,定义我们要在各层之间传递的数据。我们的领域包含探险者和生物,因此让我们为它们定义最小的Pydantic初始模型。以后可能还会有其他想法,比如探险、日志或咖啡杯的电子商务销售。但现在,我们只需定义两个会呼吸的(通常是生物)模型。
model/explorer.py
from pydantic import BaseModel
class Explorer(BaseModel):
name: str
country: str
description: str
model/creature.py
from pydantic import BaseModel
class Creature(BaseModel):
name: str
country: str
area: str
description: str
aka: str
这些都是非常简单的初始模型。我们没有使用Pydantic的任何功能,例如必选与可选或约束值。这些简单的代码可以在以后进行增强,而无需进行大规模的逻辑调整。
对于国家值,您将使用ISO双字符国家代码;这样可以节省一些键入时间,但代价是需要查找不常用的国家代码。
8.8 Stub和Fake数据
Stub也称为模拟数据,是在不调用正常“实时”模块的情况下返回的预制结果。它们是测试路由和响应的一种快速方法。
假数据是真实数据源的替身,至少执行部分相同的功能。模拟数据库的内存类就是一个例子。在本章和接下来的几章中,当你填写定义层及其通信的代码时,你将制作一些假数据。在第10章中,你将定义一个实际的实时数据存储(数据库)来替代这些假数据。
参考资料
- 软件测试精品书籍文档下载持续更新 https://github.com/china-testing/python-testing-examples 请点赞,谢谢!
- 本文涉及的python测试开发库 谢谢点赞! https://github.com/china-testing/python_cn_resouce
- python精品书籍下载 https://github.com/china-testing/python_cn_resouce/blob/main/python_good_books.md
- Linux精品书籍下载 https://www.cnblogs.com/testing-/p/17438558.html
8.9 通过堆栈创建常用函数
与数据示例类似,构建网站的方法也是探索性的。通常情况下,我们并不清楚最终会需要哪些功能,因此,让我们从类似网站常见的一些功能开始。提供数据前端通常需要以下方法:
- 获取一个、一些、全部
- 创建
- 完全替换
- 部分修改
- 删除
从本质上讲,这些都是数据库的CRUD基本功能,不过我把U分成了部分(修改)和完全(替换)功能。也许这种区分被证明是不必要的!这取决于数据的走向。
8.10 创建Fake数据
在自上而下的工作中,你会在所有三个层次中重复一些函数。为了节省输入,例 8-12 引入了名为 fake 的顶级目录,其中的模块提供了关于探索者和生物的虚假数据。
fake/explorer.py
from model.explorer import Explorer
# fake data, replaced in Chapter 10 by a real database and SQL
_explorers = [
Explorer(name="Claude Hande",
country="FR",
description="Scarce during full moons"),
Explorer(name="Noah Weiser",
country="DE",
description="Myopic machete man"),
]
def get_all() -> list[Explorer]:
"""Return all explorers"""
return _explorers
def get_one(name: str) -> Explorer | None:
for _explorer in _explorers:
if _explorer.name == name:
return _explorer
return None
# The following are nonfunctional for now,
# so they just act like they work, without modifying
# the actual fake _explorers list:
def create(explorer: Explorer) -> Explorer:
"""Add an explorer"""
return explorer
def modify(explorer: Explorer) -> Explorer:
"""Partially modify an explorer"""
return explorer
def replace(explorer: Explorer) -> Explorer:
"""Completely replace an explorer"""
return explorer
def delete(name: str) -> bool:
"""Delete an explorer; return None if it existed"""
return None
fake/creature.py
from model.creature import Creature
# fake data, until we use a real database and SQL
_creatures = [
Creature(name="Yeti",
aka="Abominable Snowman",
country="CN",
area="Himalayas",
description="Hirsute Himalayan"),
Creature(name="Bigfoot",
description="Yeti's Cousin Eddie",
country="US",
area="*",
aka="Sasquatch"),
]
def get_all() -> list[Creature]:
"""Return all creatures"""
return _creatures
def get_one(name: str) -> Creature | None:
"""Return one creature"""
for _creature in _creatures:
if _creature.name == name:
return _creature
return None
# The following are nonfunctional for now,
# so they just act like they work, without modifying
# the actual fake _creatures list:
def create(creature: Creature) -> Creature:
"""Add a creature"""
return creature
def modify(creature: Creature) -> Creature:
"""Partially modify a creature"""
return creature
def replace(creature: Creature) -> Creature:
"""Completely replace a creature"""
return creature
def delete(name: str):
"""Delete a creature; return None if it existed"""
return None
注意是的,模块函数几乎完全相同。稍后,当真正的数据库到来并必须处理两个模型的不同字段时,它们会发生变化。另外,你在这里使用的是单独的函数,而不是定义一个Fake类或抽象类。模块有自己的命名空间,因此是一种捆绑数据和函数的等价方式。
在第10章中,你将在数据层中做同样的事情。所有这些都只是添加部件并将它们连接起来,尽可能减少代码返工。直到第10 章的后面部分,你才会打开电源(即实时数据库和持久数据)。
为web/explorer.py 添加新端点
from fastapi import APIRouter
from model.explorer import Explorer
import fake.explorer as service
router = APIRouter(prefix = "/explorer")
@router.get("/")
def get_all() -> list[Explorer]:
return service.get_all()
@router.get("/{name}")
def get_one(name) -> Explorer | None:
return service.get_one(name)
# all the remaining endpoints do nothing yet:
@router.post("/")
def create(explorer: Explorer) -> Explorer:
return service.create(explorer)
@router.patch("/")
def modify(explorer: Explorer) -> Explorer:
return service.modify(explorer)
@router.put("/")
def replace(explorer: Explorer) -> Explorer:
return service.replace(explorer)
@router.delete("/{name}")
def delete(name: str):
return None
现在,对 /creature 端点做同样的处理。是的,这只是类似的剪切粘贴代码,但这样做可以简化日后的更改--日后总会有更改的。
web/creature.py:
from fastapi import APIRouter
from model.creature import Creature
import fake.creature as service
router = APIRouter(prefix = "/creature")
@router.get("/")
def get_all() -> list[Creature]:
return service.get_all()
@router.get("/{name}")
def get_one(name) -> Creature:
return service.get_one(name)
# all the remaining endpoints do nothing yet:
@router.post("/")
def create(creature: Creature) -> Creature:
return service.create(creature)
@router.patch("/")
def modify(creature: Creature) -> Creature:
return service.modify(creature)
@router.put("/")
def replace(creature: Creature) -> Creature:
return service.replace(creature)
@router.delete("/{name}")
def delete(name: str):
return service.delete(name)
上次我们在main.py中添加了用于/explorer URL的子路由器。
import sys
sys.path.append("..")
import uvicorn
from fastapi import FastAPI
from web import explorer, creature
app = FastAPI()
app.include_router(explorer.router)
app.include_router(creature.router)
if __name__ == "__main__":
uvicorn.run("main:app", reload=True)
8.11 测试
$ http -b localhost:8000/explorer/
[
{
"country": "FR",
"description": "Scarce during full moons",
"name": "Claude Hande"
},
{
"country": "DE",
"description": "Myopic machete man",
"name": "Noah Weiser"
}
]
(base) andrew@andrew-YTF-XXX:~/code/fastapi/example/ch8/web$ http -b localhost:8000/explorer/"Noah Weiser"
{
"country": "DE",
"description": "Myopic machete man",
"name": "Noah Weiser"
}
有点奇怪,PUT等操作用http提示:"Method Not Allowed",但是测试网页是好的。
8.12 分页和排序
在网络接口中,当使用 GET /resource等URL模式返回许多或所有内容时,通常需要请求查找和返回以下内容:
- 只有一个内容
- 可能有很多东西
- 所有内容
如何让我们这台善意但却极其注重字面意思的计算机来做这些事情呢?对于第一种情况,我前面提到的 RESTful 模式就是在 URL 路径中包含资源的 ID。
- 排序
GET /explorer?sort=country:获取所有探索者,按国家代码排序。
- 分页
GET/explorer?offset=10&size=10:返回整个列表中第 10 到第 19 位的探险者(此处未排序)。
- 分页排序
GET /explorer?sort=country&offset=10&size=10
虽然您可以将这些参数指定为单独的查询参数,但 FastAPI 的依赖注入可以提供帮助:
- 将排序和分页参数定义为 Pydantic 模型。
- 在路径函数参数中使用 Depends 功能向 get_all() 路径函数提供参数模型。
排序和分页应在哪里进行?起初,最简单的做法可能是将数据库查询的全部结果传到网络层,然后使用 Python 在网络层分割数据。但这样做效率并不高。这些任务通常最适合数据层,因为数据库擅长这些事情。我最终会在第 17 章为这些任务编写一些代码,该章除了第 10 章的内容外,还有更多数据库方面的花絮。
8.13 小结
本章充实了第3章和其他章节的更多细节。本章开始了制作一个完整网站的过程,该网站提供关于想象中的生物及其探险者的信息。从Web层开始,您使用 FastAPI 路径装饰器和路径函数定义了端点。路径函数从 HTTP 请求字节中的任意位置收集请求数据。模型数据由Pydantic自动检查和验证。路径函数通常会将参数传递给相应的服务函数,这些服务函数将在下一章中介绍。