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/main.py
- 项目的根目录,用于初始化 FastAPI 应用程序router.py
- 是每个模块的核心,具有所有端点schemas.py
- 适用于 pydantic 模型models.py
- 对于数据库模型service.py
- 模块特定的业务逻辑dependencies.py
- 路由器依赖关系constants.py
- 模块特定常量和错误代码config.py
- 例如环境变量utils.py
- 非业务逻辑功能,例如响应规范化、数据丰富等。exceptions.py
- 模块特定的异常,例如PostNotFound,InvalidUserDatafrom 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
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
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 验证,并为每个端点编写相同的测试
依赖关系可以使用其他依赖关系,避免类似逻辑的代码重复
# 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
依赖项可以多次重用,并且不会重新计算 - 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 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