【问题标题】:FastAPI - Best practices for writing REST APIs with multiple conditionsFastAPI - 编写具有多个条件的 REST API 的最佳实践
【发布时间】:2022-07-22 06:22:31
【问题描述】:

假设我有两个实体,UsersCouncils,以及一个 M2M 关联表 UserCouncilsUsers 可以从 Councils 添加/删除,并且只有管理员可以这样做(在 UserCouncil 关系中的 role 属性中定义)。 现在,在为/councils/{council_id}/remove 创建端点时,我面临着在操作前检查多个约束的问题,例如:


@router.delete("/{council_id}/remove", response_model=responses.CouncilDetail)
def remove_user_from_council(
    council_id: int | UUID = Path(...),
    *,
    user_in: schemas.CouncilUser,
    db: Session = Depends(get_db),
    current_user: Users = Depends(get_current_user),
    council: Councils = Depends(council_id_dep),
) -> dict[str, Any]:
    """

    DELETE /councils/:id/remove (auth)

    remove user with `user_in` from council
    current user must be ADMIN of council
    """

    # check if input user exists
    if not Users.get(db=db, id=user_in.user_id):
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
        )

    if not UserCouncil.get(db=db, user_id=user_in.user_id, council_id=council.id):
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Cannot delete user who is not part of council",
        )

    # check if current user exists in council
    if not (
        relation := UserCouncil.get(
            db=db, user_id=current_user.id, council_id=council.id
        )
    ):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Current user not part of council",
        )

    # check if current user is Admin
    if relation.role != Roles.ADMIN:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN, detail="Unauthorized"
        )

    elif current_user.id == user_in.user_id:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Admin cannot delete themselves",
        )

    else:
        updated_users = council.remove_member(db=db, user_id=user_in.user_id)
        result = {"council": council, "users": updated_users}
        return result

这些检查是不言自明的。但是,这会在端点定义中添加大量代码。端点定义通常应该是简约的吗?我可以将所有这些检查包装在 Councils crud 方法中(即 council.remove_member()),但这意味着在 crud 类中添加 HTTPExceptions,我不想这样做。

解决此类情况的一般最佳做法是什么,我可以在哪里阅读更多相关信息?任何形式的帮助将不胜感激。

谢谢。

【问题讨论】:

  • 为什么必须使用获取当前用户的依赖项检查用户是否存在之后?该依赖项应验证用户是否存在并且是有效用户;获取委员会的代码也是如此;让它依赖于用户和委员会 ID,并在你的依赖中解决这个问题。您还可以将该依赖项设为council_with_current_user_as_admin,以便将其全部隐藏在依赖层后面。您的视图变得非常有效和简洁,并且您的依赖项可以很容易地被重用来组合不同的需求。
  • 检查用户是否存在是针对输入用户user_incurrent_user 只解析头部以获取当前登录的用户。但你是对的。关系依赖会清除混乱。

标签: python rest design-patterns sqlalchemy fastapi


【解决方案1】:

那么,我会告诉你我将如何用你的例子来做这件事。

一般来说,我喜欢将端点保持在最低限度。您要采用的是用于构建 API 的常见模式,即将您的业务逻辑捆绑到服务类中。此服务类允许您重用逻辑。假设您想从队列或 cron 作业中删除理事会成员。这带来了您强调的下一个问题,即在您的服务类中存在 HTTP 特定异常,这些异常可能不会在 HTTP 上下文中使用。幸运的是,这并不难解决,您可以定义自己的异常并要求 API 框架捕获它们,然后重新引发所需的 HTTP 异常。

定义一个自定义异常:

class UnauthorizedException(Exception):
    def __init__(self, message: str):
        super().__init__(message)
        self.message = message


class InvalidActionException(Exception):
    ...


class NotFoundException(Exception):
    ...

在 Fast API 中,您可以捕获应用程序抛出的特定异常

@app.exception_handler(UnauthorizedException)
async def unauthorized_exception_handler(request: Request, exc: UnauthorizedException):
    return JSONResponse(
            status_code=status.HTTP_403_FORBIDDEN,
            content={"message": exc.message},
    )

@app.exception_handler(InvalidActionException)
async def unauthorized_exception_handler(request: Request, exc: InvalidActionException):
    ...

使用合理的方法将您的业务逻辑封装到一个服务类中,并引发您为服务定义的异常

class CouncilService:
    def __init__(self, db: Session):
        self.db = db

    def ensure_admin_council_member(self, user_id: int, council_id: int):
        # check if current user exists in council
        if not (
                relation := UserCouncil.get(
                        db=self.db, user_id=user_id, council_id=council_id
                )
        ):
            raise UnauthorizedException("Current user not part of council")

        # check if current user is Admin
        if relation.role != Roles.ADMIN:
            raise UnauthorizedException("Unauthorized")

    def remove_council_member(self, user_in: schemas.CouncilUser, council: Councils):
        # check if input user exists
        if not Users.get(db=self.db, id=user_in.user_id):
            raise NotFoundException("User not found")

        if not UserCouncil.get(db=self.db, user_id=user_in.user_id, council_id=council.id):
            raise InvalidActionException("Cannot delete user who is not part of council")

        if current_user.id == user_in.user_id:
            raise InvalidActionException("Admin cannot delete themselves")

        updated_users = council.remove_member(db=self.db, user_id=user_in.user_id)
        result = {"council": council, "users": updated_users}
        return result

最后你的端点定义很精简

@router.delete("/{council_id}/remove", response_model=responses.CouncilDetail)
def remove_user_from_council(
    council_id: int | UUID = Path(...),
    *,
    user_in: schemas.CouncilUser,
    current_user: Users = Depends(get_current_user),
    council: Councils = Depends(council_id_dep),
    council_service: CouncilService = Depends(get_council_service),
) -> responses.CouncilDetail:
    """

    DELETE /councils/:id/remove (auth)

    remove user with `user_in` from council
    current user must be ADMIN of council
    """
    council_service.ensure_admin_council_member(current_user.id, council_id)
    return council_service.remove_council_member(user_in, council)

【讨论】:

    猜你喜欢
    • 2018-05-25
    • 2013-10-23
    • 1970-01-01
    • 1970-01-01
    • 2020-05-29
    • 1970-01-01
    • 2020-04-23
    • 1970-01-01
    相关资源
    最近更新 更多