目录
- 1. 项目结构
- 2. Pydantic数据验证
- 3. 数据验证与数据库的依赖关系
- 4. 链式依赖
- 5. 解耦和重用依赖关系,依赖项调用被缓存
- 6. Restful API 规则
- 7. 如果你只有阻塞 I/O 操作,不要让你的路由异步
- 8. 从0开始创建基础模型
- 9. 编写代码文档
- 10. 使用 Pydantic 的 BaseSettings 进行配置
- 11. SQLAlchemy:设置数据库键命名约定
- 12. Alembic的数据库迁移
- 13. 设置数据库命名约定
- 14. 从0开始设置异步测试客户端
- 15. BackgroundTasks处理I/O
- 16. 以块的形式保存文件
- 17. 小心动态 pydantic 字段 (Pydantic v1)
- 18. SQL第一,Pydantic第二
- 19. 如果用户可以发送公开可用的url,需要验证host
- 20. 如果 schema 直接面向客户端,则在自定义 pydantic 验证器中引发 ValueError
- 21. FastAPI将Pydantic对象转换为dict,然后转换为Pydantic对象,然后转换为JSON
- 22. 如果必须使用sync SDK,需要在线程池中运行它
- 23. 使用linters(black, ruff)
1. 项目结构
fastapi-project
├── alembic/
├── src
│ ├── auth
│ │ ├── router.py
│ │ ├── schemas.py # pydantic 模型
│ │ ├── models.py # 数据库模型
│ │ ├── dependencies.py
│ │ ├── config.py # 局部配置
│ │ ├── constants.py
│ │ ├── exceptions.py
│ │ ├── service.py
│ │ └── utils.py
│ ├── aws
│ │ ├── client.py # 外部服务通信的客户端模型
│ │ ├── schemas.py
│ │ ├── config.py
│ │ ├── constants.py
│ │ ├── exceptions.py
│ │ └── utils.py
│ └── posts
│ │ ├── router.py
│ │ ├── schemas.py
│ │ ├── models.py
│ │ ├── dependencies.py
│ │ ├── constants.py
│ │ ├── exceptions.py
│ │ ├── service.py
│ │ └── utils.py
│ ├── config.py # 全局配置
│ ├── models.py # 全局模型
│ ├── exceptions.py # 全局异常
│ ├── pagination.py # 全局模块,例如分页
│ ├── database.py # 数据库连接相关
│ └── main.py
├── tests/
│ ├── auth
│ ├── aws
│ └── posts
├── templates/
│ └── index.html
├── requirements
│ ├── base.txt
│ ├── dev.txt
│ └── prod.txt
├── .env
├── .gitignore
├── logging.ini
└── alembic.ini
- 将所有域目录存储在src文件夹 内
src/
- 应用程序的最高级别,包含常见模型、配置和常量等。src/main.py
- 项目的根目录,用于初始化 FastAPI 应用程序
- 每个包都有自己的路由器、模式、模型等。
router.py
- 是每个模块的核心,具有所有端点schemas.py
- 适用于 pydantic 模型models.py
- 对于数据库模型service.py
- 模块特定的业务逻辑dependencies.py
- 路由器依赖关系constants.py
- 模块特定常量和错误代码config.py
- 例如环境变量utils.py
- 非业务逻辑功能,例如响应规范化、数据丰富等。exceptions.py
- 模块特定的异常,例如PostNotFound,InvalidUserData
- 当包需要来自其他包的服务或依赖项或常量时 - 使用显式模块名称导入它们
from src.auth import constants as auth_constants
from src.notifications import service as notification_service
from src.posts.constants import ErrorCode as PostsErrorCode # in case we have Standard ErrorCode in constants module of each package
2. Pydantic数据验证
Pydantic 具有一组丰富的功能来验证和转换数据。
除了具有默认值的必填和非必填字段等常规功能之外,Pydantic 还内置了全面的数据处理工具,例如正则表达式、有限允许选项的枚举、长度验证、电子邮件验证等。
from enum import Enum
from pydantic import AnyUrl, BaseModel, EmailStr, Field, constr
class MusicBand(str, Enum):
AEROSMITH = "AEROSMITH"
QUEEN = "QUEEN"
ACDC = "AC/DC"
class UserBase(BaseModel):
first_name: str = Field(min_length=1, max_length=128)
username: constr(regex="^[A-Za-z0-9-_]+$", to_lower=True, strip_whitespace=True)
email: EmailStr
age: int = Field(ge=18, default=None) # 必须大于或等于 18
favorite_band: MusicBand = None # 只允许输入“AEROSMITH”、“QUEEN”、“AC/DC”值
website: AnyUrl = None
3. 数据验证与数据库的依赖关系
Pydantic 只能验证客户端输入的值。使用依赖项根据数据库约束(例如电子邮件已存在、用户未找到等)验证数据
# dependencies.py
async def valid_post_id(post_id: UUID4) -> Mapping:
post = await service.get_by_id(post_id)
if not post:
raise PostNotFound()
return post
# router.py
@router.get("/posts/{post_id}", response_model=PostResponse)
async def get_post_by_id(post: Mapping = Depends(valid_post_id)):
return post
@router.put("/posts/{post_id}", response_model=PostResponse)
async def update_post(
update_data: PostUpdate,
post: Mapping = Depends(valid_post_id),
):
updated_post: Mapping = await service.update(id=post["id"], data=update_data)
return updated_post
@router.get("/posts/{post_id}/reviews", response_model=list[ReviewsResponse])
async def get_post_reviews(post: Mapping = Depends(valid_post_id)):
post_reviews: list[Mapping] = await reviews_service.get_by_post_id(post["id"])
return post_reviews
如果我们不将数据验证置于依赖项中,则必须为每个端点添加 post_id 验证,并为每个端点编写相同的测试
4. 链式依赖
依赖关系可以使用其他依赖关系,避免类似逻辑的代码重复
# dependencies.py
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
async def valid_post_id(post_id: UUID4) -> Mapping:
post = await service.get_by_id(post_id)
if not post:
raise PostNotFound()
return post
async def parse_jwt_data(
token: str = Depends(OAuth2PasswordBearer(tokenUrl="/auth/token"))
) -> dict:
try:
payload = jwt.decode(token, "JWT_SECRET", algorithms=["HS256"])
except JWTError:
raise InvalidCredentials()
return {
"user_id": payload["id"]}
async def valid_owned_post(
post: Mapping = Depends(valid_post_id),
token_data: dict = Depends(parse_jwt_data),
) -> Mapping:
if post["creator_id"] != token_data["user_id"]:
raise UserNotOwner()
return post
# router.py
@router.get("/users/{user_id}/posts/{post_id}", response_model=PostResponse)
async def get_user_post(post: Mapping = Depends(valid_owned_post)):
return post
5. 解耦和重用依赖关系,依赖项调用被缓存
依赖项可以多次重用,并且不会重新计算 - FastAPI 默认情况下会在请求范围内缓存依赖项的结果,即如果我们有一个调用 service 的依赖项get_post_by_id
,则每次调用此依赖项时我们都不会访问数据库 -仅第一个函数调用。
知道了这一点,我们可以轻松地将依赖关系解耦到多个较小的函数,这些函数在较小的域上运行,并且更容易在其他路由中重用。例如,在下面的代码中我们使用了parse_jwt_data
三次:
valid_owned_post
valid_active_creator
get_user_post
但parse_jwt_data
仅在第一次调用时被调用一次。
# dependencies.py
from fastapi import BackgroundTasks
from fastapi.security import OAuth2Pa