FastAPI-秘籍-全-

FastAPI 秘籍(全)

原文:zh.annas-archive.org/md5/bd742fc2af9d0b64a6f94d7b9da9035f

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

FastAPI 食谱》是希望掌握 FastAPI 框架以构建 API 的 Python 开发者的宝贵资源。由 Sebastián Ramírez Montaño 创建,FastAPI 首次发布于 2018 年 12 月。它迅速获得了人气,并成为构建 API 最广泛使用的 Python 框架之一。

本书首先介绍 FastAPI,展示其优势,并帮助你设置开发环境。然后,它转向数据处理,展示数据库集成和创建、读取、更新和删除CRUD)操作,以帮助你有效地在 API 中管理数据。

随着本书的深入,它探讨了如何创建RESTful API,涵盖了高级主题,如复杂查询、版本控制和广泛的文档。安全性同样重要,本书有一章专门介绍实现认证机制,例如OAuth2JWT令牌,以保护 FastAPI 应用程序的安全。

测试是开发的重要组成部分,本书提供了确保 FastAPI 应用程序质量和可靠性的策略。讨论了部署策略,强调了生产环境中的最佳实践。对于高流量应用程序,本书探讨了扩展技术以提高性能。

通过中间件扩展 FastAPI 的功能是可能的,本书还展示了如何通过将其与其他 Python 工具和框架集成来增强 FastAPI 的能力,以适应机器学习模型并公开LLM RAG应用程序。

实时通信通过WebSockets章节进行处理,并提供了高级数据处理技术来管理大量数据集和文件管理。

本书以使用 FastAPI 处理现实世界流量结束,强调部署策略和打包发货。每一章都精心设计,以构建你的专业知识,使《FastAPI 食谱》成为专业级 API 开发的宝贵指南。

本书面向的对象

本书针对对网络开发概念有基础理解的初级到高级 Python 开发者。对于那些寻求使用现代 FastAPI 框架构建高效、可扩展 API 的人来说,本书特别有益。对于希望提高 API 开发技能并将实际解决方案应用于现实编程挑战的开发者来说,本书是宝贵的资源。无论你是想保护 API、有效管理数据还是优化性能,本书都提供了知识和动手示例,以提升你在 FastAPI 方面的专业知识。

本书涵盖的内容

第一章FastAPI 的第一步,作为框架的介绍,强调其速度、易用性和全面的文档。这一章是你设置开发环境、创建第一个 FastAPI 项目并探索其基本概念的入门。

第二章, 处理数据,致力于掌握网络应用程序中数据处理的关键方面。它涵盖了使用 SQL 和 NoSQL 数据库集成、管理和优化数据存储的复杂性。

第三章, 使用 FastAPI 构建 RESTful API,深入探讨了构建 RESTful API 的基本要素,这对于网络服务至关重要,它使应用程序能够高效地通信和交换数据。

第四章, 身份验证和授权,深入探讨了保护您的网络应用程序免受未经授权访问的关键领域。它涵盖了用户注册和认证的基础、将 OAuth2 协议与 JWT 集成以增强安全性,以及创建 API 的基本组件。

第五章, 测试和调试 FastAPI 应用程序,转向软件开发的一个关键方面,确保您应用程序的可靠性、健壮性和质量——测试和调试。

第六章, 将 FastAPI 与 SQL 数据库集成,开始了一段在 FastAPI 应用程序中充分利用 SQL 数据库潜力的旅程。它精心设计,旨在指导您利用 SQLAlchemy(一个强大的 Python SQL 工具包和对象关系映射器(ORM))。

第七章, 将 FastAPI 与 NoSQL 数据库集成,通过指导您设置和使用 MongoDB(一个流行的 NoSQL 数据库)与 FastAPI 的过程,探讨了 FastAPI 与 NoSQL 数据库的集成。它涵盖了 CRUD 操作、使用索引进行性能优化以及处理 NoSQL 数据库中的关系。此外,本章还讨论了将 FastAPI 与 Elasticsearch 集成以实现强大的搜索功能,以及使用 Redis 实现缓存。

第八章, 高级特性和最佳实践,探讨了优化 FastAPI 应用程序功能、性能和可扩展性的高级技术和最佳实践。它涵盖了依赖注入、自定义中间件、国际化、性能优化、速率限制和后台任务执行等基本主题。

第九章, 使用 WebSockets,是一本全面指南,介绍了在 FastAPI 应用程序中使用 WebSockets 实现实时通信功能。它涵盖了设置 WebSocket 连接、发送和接收消息、处理连接和断开连接、错误处理以及实现聊天功能。

第十章, 将 FastAPI 与其他 Python 库集成,深入探讨了 FastAPI 与外部库结合时的潜力,增强了其核心功能之外的能力。它提供了一种基于食谱的方法,将 FastAPI 与各种技术(如 Cohere 和 LangChain)集成,以构建 LLM RAG 应用。

第十一章, 中间件和 Webhooks,深入探讨了 FastAPI 中中间件和 Webhooks 的先进和关键方面。中间件允许你全局处理请求和响应,而 Webhooks 则使你的 FastAPI 应用程序能够通过发送实时数据更新与其他服务进行通信。

第十二章, 部署和管理 FastAPI 应用程序,涵盖了无缝部署 FastAPI 应用程序所需的知识和工具,利用各种技术和最佳实践。你将学习如何利用 FastAPI CLI 高效运行服务器,启用 HTTPS 以保护你的应用程序,以及使用 Docker 容器化你的 FastAPI 项目。

为了充分利用本书

你应该对 Python 编程有基本的了解,因为本书假设你对 Python 语法和概念熟悉。此外,了解 Web 开发原则,包括 HTTP、RESTful API 和 JSON,将有所帮助。熟悉 SQL 和 NoSQL 数据库,以及使用 Git 等版本控制系统的经验,将帮助你完全理解内容。

本书涵盖的软件/硬件 操作系统要求
Python 3.9 或更高版本 Windows、macOS 或 Linux(任何)

如果你使用的是本书的数字版,我们建议你亲自输入代码或通过 GitHub 仓库(下一节中提供链接)访问代码。这样做将有助于避免与代码复制和粘贴相关的任何潜在错误

下载示例代码文件

你可以从 GitHub 下载本书的示例代码文件github.com/PacktPublishing/FastAPI-Cookbook。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有其他来自我们丰富的书籍和视频目录的代码包,可在github.com/PacktPublishing/找到。查看它们吧!

使用的约定

本书使用了多种文本约定。

文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“此外,你将在应用程序自动创建的新app.log文件中找到我们logger_client的消息。”

代码块设置如下:

from locust import HttpUser, task
class ProtoappUser(HttpUser):
    host = "http://localhost:8000"
    @task
    def hello_world(self):
        self.client.get("/home")

当我们希望将您的注意力引向代码块中的特定部分时,相关的行或项目将以粗体显示:

from pydantic import BaseModel, Field
class Book(BaseModel):
    title: str = Field(..., min_length=1, max_length=100)
    author: str = Field(..., min_length=1, max_length=50)
    year: int = Field(..., gt=1900, lt=2100)

任何命令行输入或输出都应按照以下方式编写:

$ pytest –-cov protoapp tests

在本书中,我们将一般使用类 Unix 终端命令。这可能会导致 Windows 系统在多行命令上出现兼容性问题。如果你使用的是 Windows 终端,请考虑将换行符\调整为以下形式:

$ python -m grpc_tools.protoc \ 
--proto_path=. ./grpcserver.proto \ 
--python_out=. \ 
--grpc_python_out=.

这是 CMD 中的相同行:

$ python -m grpc_tools.protoc ^
--proto_path=. ./grpcserver.proto ^
--python_out=. ^
--grpc_python_out=.

这是 PowerShell 中的相同行:

$ python -m grpc_tools.protoc `
--proto_path=. ./grpcserver.proto `
--python_out=. `
--grpc_python_out=.

粗体:表示新术语、重要词汇或屏幕上看到的词汇。例如,菜单或对话框中的文字会以这种方式显示。以下是一个示例:“此限制可以在设置中调整(设置 | 高级设置 | 运行/调试 | 临时 配置限制)。”

小贴士或重要注意事项

显示如下。

部分

在本书中,你会发现一些频繁出现的标题(准备就绪如何操作…它是如何工作的…还有更多…,以及另请参阅)。

为了清楚地说明如何完成食谱,请按照以下方式使用这些部分。

准备就绪

本节告诉你可以在食谱中期待什么,并描述如何设置任何所需的软件或初步设置。

如何操作…

本节包含遵循食谱所需的步骤。

它是如何工作的…

本节通常包含对上一节发生事件的详细解释。

还有更多…

本节包含有关食谱的附加信息,以便让你对食谱有更深入的了解。

另请参阅

本节提供了对其他有用信息的链接,以帮助理解食谱。

联系我们

我们欢迎读者的反馈。

一般反馈:如果你对本书的任何方面有疑问,请在邮件主题中提及书名,并请通过 customercare@packtpub.com 给我们发送邮件。

勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果你在这本书中发现了错误,我们将非常感激你向我们报告。请访问www.packtpub.com/support/errata,选择你的书,点击勘误提交表单链接,并输入详细信息。

盗版:如果你在互联网上发现我们作品的任何非法副本,我们将非常感激你提供位置地址或网站名称。请通过 copyright@packt.com 与我们联系,并提供材料的链接。

如果你有兴趣成为作者:如果你在某个领域有专业知识,并且你感兴趣的是撰写或为本书做出贡献,请访问authors.packtpub.com

分享你的想法

一旦你阅读了FastAPI 食谱,我们很乐意听到你的想法!请点击此处直接进入此书的亚马逊评论页面并分享你的反馈。

您的评论对我们和科技社区非常重要,并将帮助我们确保我们提供高质量的内容。

下载此书的免费 PDF 副本

感谢您购买此书!

您喜欢在路上阅读,但无法随身携带您的印刷书籍吗?

您的电子书购买是否与您选择的设备不兼容?

别担心,现在每购买一本 Packt 书籍,您都可以免费获得该书的 DRM 免费 PDF 版本。

在任何地方、任何时间、任何设备上阅读。直接从您最喜欢的技术书籍中搜索、复制和粘贴代码到您的应用程序中。

优惠远不止这些,您还可以获得独家折扣、时事通讯和每日免费内容的访问权限。

按照以下简单步骤获取优惠:

  1. 扫描二维码或访问以下链接

二维码

packt.link/free-ebook/978-1-80512-785-7

  1. 提交您的购买证明

  2. 就这些!我们将直接将您的免费 PDF 和其他优惠发送到您的邮箱。

第一章:FastAPI 的第一步

欢迎来到激动人心的 FastAPI 世界,这是一个用于在 Python 中构建 API 和 Web 应用程序的现代、高性能框架。本章是您了解和利用 FastAPI 力量的入门。在这里,您将迈出第一步,设置您的开发环境,创建您的第一个 FastAPI 项目,并探索其基本概念。

FastAPI 以其速度、易用性和全面的文档而脱颖而出,成为希望构建可扩展和高效 Web 应用的开发者的首选选择。在本章中,您将实际参与设置 FastAPI,学习如何导航其架构,并了解其核心组件。通过定义简单的 API 端点、处理 HTTP 方法以及学习请求和响应处理,您将获得实践经验。这些基础技能对于任何进入使用 FastAPI 构建现代 Web 开发世界的开发者至关重要。

到本章结束时,您将对 FastAPI 的基本结构和功能有扎实的理解。您将能够设置新项目、定义 API 端点,并掌握使用 FastAPI 处理数据。这些基础知识为您在阅读本书的过程中遇到更高级主题和复杂应用奠定了基础。

在本章中,我们将介绍以下配方:

  • 设置您的开发环境

  • 创建新的 FastAPI 项目

  • 理解 FastAPI 基础

  • 定义您的第一个 API 端点

  • 使用路径和查询参数

  • 定义和使用请求和响应模型

  • 处理错误和异常

每个配方都是为了提供给您实用的知识和直接经验,确保在本章结束时,您将准备好开始构建自己的 FastAPI 应用程序。

技术要求

要开始您的 FastAPI 之旅,您需要设置一个支持 Python 开发和 FastAPI 功能的环境。以下是本章所需的技术要求和安装列表:

  • Python:FastAPI 是基于 Python 构建的,因此您需要一个与您的 FastAPI 版本兼容的 Python 版本。您可以从 python.org 下载最新版本。

  • pip,Python 的包管理器。您可以通过在命令行中运行 pip install fastapi 来完成此操作。

  • pip install uvicorn

  • 集成开发环境(IDE):一个支持 Python 开发的 IDE,例如 Visual Studio CodeVS Code)、PyCharm 或任何其他支持 Python 开发的 IDE,对于编写和测试您的代码将是必要的。

  • Postman 或 Swagger UI:用于测试 API 端点。FastAPI 自动生成并托管 Swagger UI,因此您可以直接使用它。

  • Git:版本控制是必不可少的,Git 是一个广泛使用的系统。如果尚未安装,您可以从 git-scm.com 获取它。

  • GitHub 账户:要访问代码仓库,需要 GitHub 账户。如果您还没有,请前往 github.com 注册。

本章中使用的代码可在以下地址的 GitHub 上找到:github.com/PacktPublishing/FastAPI-Cookbook/tree/main/Chapter01。您可以在本地机器上通过 github.com/PacktPublishing/FastAPI-Cookbook 克隆或下载存储库以进行跟随。

设置您的开发环境

这份食谱,致力于设置您的开发环境,是任何成功的 Web 开发项目的关键基础。在这里,您将学习如何安装和配置所有必需的工具,以便开始使用 FastAPI 进行构建。

我们首先将指导您安装 Python,这是 FastAPI 背后的核心语言。接下来,我们将继续安装 FastAPI 本身,以及 Uvicorn,一个闪电般的 ASGI 服务器,它是运行您的 FastAPI 应用程序的基础。

设置 IDE 是我们的下一个目标。无论您更喜欢 VS Code、PyCharm 还是其他任何 Python 友好的 IDE,我们都会提供一些提示,使您的开发过程更加顺畅和高效。

最后,我们将向您介绍 Git 和 GitHub – 这些是现代软件开发中版本控制和协作不可或缺的工具。了解如何使用这些工具不仅可以帮助您有效地管理代码,而且还能打开通往社区驱动开发和资源的广阔世界的大门。

准备工作

FastAPI 与 Python 兼容,因此在使用之前您需要检查您的 Python 版本。这是设置 FastAPI 的重要步骤。我们将指导您如何安装它。

Windows 安装

如果您在 Windows 上工作,请按照以下步骤安装 Python:

  1. 访问官方 Python 网站:python.org

  2. 下载 Python 的最新版本或任何高于 3.9 的版本。

  3. 运行安装程序。在点击“立即安装”之前,请确保勾选“将 Python 添加到 PATH”的复选框。

  4. 安装后,打开命令提示符并输入 python --version 以确认安装。

macOS/Linux 安装

macOS 通常预装了 Python,但可能不是最新版本。

您可以使用 Homebrew(macOS 的包管理器)。要安装它,打开终端并运行以下命令:

$ /bin/bash -c "$(curl –fsSL https://raw.githubusercontent.com/\Homebrew/install/HEAD/install.sh)"

然后,您可以使用以下命令在终端中安装 Python:

$ brew install python

在 Linux 上,您可以通过运行以下命令使用包管理器安装 Python:

$ sudo apt-get install python3

这就是您在 macOS 和 Linux 系统上安装 Python 所需要的一切。

检查安装

安装后,在终端中运行以下命令以检查 Python 是否正确安装:

$ python --version

如果您在 Linux 上安装了它,二进制命令是 python3,因此您可以通过运行以下命令来检查 Python 是否正确安装:

$ python3 --version

一旦安装了 Python,我们想要确保 Python 的包管理器已正确安装。它随 Python 的安装一起提供,被称为 pip

从终端窗口运行以下命令:

$ pip --version

在 Linux 上,运行以下命令:

$ pip3 --version

一旦您的计算机上安装了 Python,您现在可以考虑安装 FastAPI。

如何做到这一点...

当您已经准备好 Python 和 pip 后,我们可以继续安装 FastAPI 和 IDE。然后,我们将配置 Git。

我们将按照以下步骤进行:

  1. 安装 FastAPI 和 Uvicorn

  2. 设置您的 IDE(VS Code 或 PyCharm)

  3. 设置 Git 和 GitHub 以跟踪您的项目

安装 FastAPI 和 Uvicorn

在设置好 Python 后,下一步是安装 FastAPI 和 Uvicorn。FastAPI 是我们将用来构建应用程序的框架,而 Uvicorn 是一个 ASGI 服务器,用于运行和提供我们的 FastAPI 应用程序。

打开您的命令行界面,通过运行以下命令一起安装 FastAPI 和 Uvicorn:

$ pip install fastapi[all]

此命令将安装 FastAPI 以及其推荐的依赖项,包括 Uvicorn。

为了验证安装,您只需在终端中运行 uvicorn --version 即可。

设置您的 IDE

选择正确的 IDE 是您 FastAPI 之旅中的关键步骤。IDE 不仅仅是一个文本编辑器;它是一个您编写、调试和测试代码的空间。

一个好的 IDE 可以显著提高您的编码体验和生产力。对于 FastAPI 开发以及 Python 的一般使用,两个流行的选择是 VS Code 和 PyCharm。

VS Code

VS Code 是一个免费、开源、轻量级的 IDE,具有强大的功能。它提供了出色的 Python 支持,并且高度可定制。

您可以从官方网站(code.visualstudio.com)下载并安装 VS Code。安装过程相当简单。安装完成后,打开 VS Code,转到 python。安装微软版本,然后完成。

PyCharm

PyCharm 由 JetBrains 创建,专门针对 Python 开发。它为专业开发者提供了一系列工具,包括对 FastAPI 等网络开发框架的优秀支持。

您可以选择社区免费版和专业付费版。对于本书的范围,社区版已经足够,您可以在 JetBrains 网站上下载:www.jetbrains.com/pycharm/download/.

对于 PyCharm,安装过程也很简单。

提高您的开发体验

对于这两个 IDE - 以及如果您使用其他 IDE - 确保利用基本优势来提高您的开发体验并提高效率。以下是我接近新的 IDE 环境时使用的简短清单:

  • 代码补全和分析:好的 IDE 提供智能代码补全、错误突出显示和修复,这对于高效开发非常有价值。

  • 调试工具:利用 IDE 提供的调试功能来诊断和解决代码中的问题

  • 版本控制集成:一个好的 IDE 提供了对 Git 的支持,简化了代码更改跟踪和仓库管理

  • 自定义:通过调整主题、快捷键和设置来自定义你的 IDE,使你的开发体验尽可能舒适和高效

设置 Git 和 GitHub

版本控制是软件开发的一个基本方面。Git 与 GitHub 结合,形成了一套强大的工具集,用于跟踪更改、协作和维护项目的历史。你可以从官方网站 git-scm.com 下载 Git 安装程序并安装它。

安装完成后,使用以下命令在命令行中配置 Git 的用户名和电子邮件:

$ git config --global user.name "Your Name"
$ git config --global user.email "your.email@example.com"

GitHub 是本书中使用的代码示例存储的平台。如果你还没有,请在 github.com 上注册一个 GitHub 账户。

创建一个新的 FastAPI 项目

设置一个有组织的项目结构对于维护干净的代码库至关重要,尤其是在你的应用程序增长和演变时。这个食谱将指导你如何创建你的第一个基本的 FastAPI 项目。一个结构化的项目简化了导航、调试和协作。对于 FastAPI,遵循结构化的最佳实践可以显著提高可扩展性和可维护性。

准备工作

要遵循这个食谱,你需要确保你的开发环境已经设置好。

如何做到...

我们首先创建一个名为 fastapi_start 的项目文件夹,我们将使用它作为根项目文件夹。

  1. 在根项目文件夹级别的终端中,我们将通过运行以下命令设置我们的虚拟环境:

    .venv folder that will contain all packages required for the project within our project's root folder.
    
  2. 现在,你需要激活环境。如果你使用的是 Mac 或 Linux,请运行以下命令:

    (.venv) $. Alternatively, if you check the location of the python binary command, it should be located within the .venv folder. From now on, each time you install a module with pip, it will be installed in the .venv folder, and it will be activated only if the environment is active.
    
  3. 现在,你可以通过运行以下命令在你的环境中安装 fastapi 包和 uvicorn

    main.py.
    
  4. 此文件是你的 FastAPI 应用程序开始的地方。首先,编写 FastAPI 模块的导入。然后,创建 FastAPI 类的一个实例:

    from fastapi import FastAPI
    app = FastAPI()
    

    此实例存放着你的应用程序代码。

  5. 接下来,定义你的第一个路由。在 FastAPI 中,路由就像路标,将请求导向相应的函数。从一个简单的返回问候世界的路由开始:

    @app.get("/")
    def read_root():
        return {"Hello": "World"}
    

    你刚刚创建了你的第一个 FastAPI 应用程序的代码。

如果你想要跟踪项目,你可以按照以下方式设置 Git:

  1. 在你的项目根目录中,打开终端或命令提示符并运行以下命令:

    .gitignore file to specify untracked files to ignore (such as __pychache__, .venv, or IDE-specific folders). You can also have a look at the one on the GitHub repository of the project at the link: https://github.com/PacktPublishing/FastAPI-Cookbook/blob/main/.gitignore.
    
  2. 然后,使用以下命令添加你的文件:

    $ git add .
    
  3. 然后,使用以下命令提交它们:

    $ git commit –m "Initial commit"
    

就这样。你现在可以使用 Git 跟踪你的项目了。

还有更多...

一个结构良好的项目不仅仅是关于整洁;它关乎创建一个可持续和可扩展的环境,让你的应用程序可以增长和演变。在 FastAPI 中,这意味着以逻辑和高效的方式组织你的项目,以分离应用程序的不同方面。

对于 FastAPI 项目来说,没有独特和完美的结构;然而,一个常见的做法是将你的项目分为几个关键目录:

  • /src:这是你的主要应用程序代码所在的地方。在 /src 内,你可能会有不同模块的应用程序的子目录。例如,你可以有一个 models 目录用于你的数据库模型,一个 routes 目录用于你的 FastAPI 路由,以及一个 services 目录用于业务逻辑。

  • /tests:将你的测试代码与应用程序代码分开是一个好的做法。这使得管理它们变得更容易,并确保你的生产构建不包括测试代码。

  • /docs:对于任何项目来说,文档都是至关重要的。无论是 API 文档、安装指南还是使用说明,为文档保留一个专门的目录有助于保持清晰。

参见

你可以在以下链接中找到有关如何使用 venv 管理虚拟环境的详细信息:

为了用 Git 提升你的知识并熟悉添加、暂存和提交操作,请查看此指南:

理解 FastAPI 基础

在我们开始使用 FastAPI 的旅程时,建立一个坚实的基础至关重要。FastAPI 不仅仅是一个另一个网络框架;它是一个强大的工具,旨在使开发者的生活更轻松,使应用程序更快,使代码更健壮和易于维护。

在这个菜谱中,我们将揭示 FastAPI 的核心概念,深入探讨其独特的功能,如异步编程,并指导你创建和组织你的第一个端点。到菜谱结束时,你将拥有一个运行中的第一个 FastAPI 服务器——这是一个标志着现代网络开发激动人心旅程开始的里程碑。

FastAPI 是一个基于 Python 的现代、快速网络框架,用于构建基于标准 Python 类型提示的 API。

定义 FastAPI 的关键特性如下:

  • 速度:它是构建 Python API 中最快的框架之一,归功于其底层的 Starlette 框架和 Pydantic 数据处理

  • 易用性:FastAPI 被设计为易于使用,具有直观的编码,可以加速你的开发时间

  • 自动文档:使用 FastAPI,API 文档是自动生成的,这是一个既节省时间又对开发者有益的特性

如何做到这一点…

现在,我们将探讨如何有效地使用这些功能,并提供一些一般性的指导。

我们将按照以下步骤进行:

  • 将异步编程应用于现有端点以提高时间效率

  • 探索路由器和端点以更好地组织大型代码库

  • 使用基本配置运行你的第一个 FastAPI 服务器

  • 探索自动文档

应用异步编程

FastAPI 最强大的功能之一是其对异步编程的支持。这允许你的应用程序同时处理更多请求,使其更高效。异步编程是一种并发编程风格,其中任务在没有阻塞其他任务执行的情况下执行,从而提高了应用程序的整体性能。为了顺利集成异步编程,FastAPI 利用 async/await 语法 (fastapi.tiangolo.com/async/) 并自动集成异步函数。

因此,从 创建新的 FastAPI 项目 菜单中的前一个代码片段中,main.py 中的 read_root() 函数可以写成如下所示:

@app.get("/")
async def read_root():
    return {"Hello": "World"}

在这个例子中,代码的行为将与之前完全相同。

探索路由器和端点

在 FastAPI 中,将代码组织成路由器和端点是基本实践。这种组织有助于使代码更整洁、更模块化。

端点

端点是 API 交互发生的点。在 FastAPI 中,通过使用 HTTP 方法(如 @app.get("/"))装饰一个函数来创建端点。

这表示对应用程序根的 GET 请求。

考虑以下代码片段:

from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
    return {"Hello": "World"}

在这个片段中,我们定义了一个针对根 URL ("/") 的端点。当对 URL 发起 GET 请求时,read_root 函数被调用,返回一个 JSON 响应。

路由器

当我们需要处理位于不同文件中的多个端点时,我们可以从使用路由器中受益。路由器帮助我们将端点分组到不同的模块中,这使得我们的代码库更容易维护和理解。例如,我们可以为与用户相关的操作使用一个路由器,为与产品相关的操作使用另一个路由器。

要定义一个路由器,首先在 fastapi_start 文件夹中创建一个名为 router_example.py 的新文件。然后,创建路由器如下所示:

from fastapi import APIRouter
router = APIRouter()
@router.get("/items/{item_id}")
async def read_item(item_id: int):
    return {"item_id": item_id}

你现在可以重用它,并将路由器附加到 main.py 中的 FastAPI 服务器实例:

import router_example
from fastapi import FastAPI
app = FastAPI()
app.include_router(router_example.router)
@app.get("/")
async def read_root():
    return {"Hello": "World"}

现在你已经有了运行服务器的代码,其中包括来自另一个模块的 GET /items 端点的路由器导入。

运行你的第一个 FastAPI 服务器

要运行你的 FastAPI 应用程序,你需要将 Uvicorn 指向你的应用实例。如果你的文件名为 main.py,并且你的 FastAPI 实例名为 app,你可以在 fastapi_start 文件夹级别像这样启动你的服务器:

$ uvicorn main:app --reload

--reload 标志在代码更改后使服务器重新启动,这使得它非常适合开发。

服务器启动后,你可以在 http://127.0.0.1:8000 访问你的 API。如果你在浏览器中访问这个 URL,你会看到来自我们刚刚创建的 "/" 端点的 JSON 响应。

探索自动文档

FastAPI 最令人兴奋的特性之一是其自动文档。当你运行 FastAPI 应用程序时,会自动生成两个文档接口:Swagger UIRedoc

你可以通过 http://127.0.0.1:8000/docs 访问 Swagger UI,通过 http://127.0.0.1:8000/redoc 访问 Redoc。

这些接口提供了一种交互式的方式来探索你的 API 并测试其功能。

相关内容

你可以在以下链接中了解更多关于我们在菜谱中涵盖的内容:

定义你的第一个 API 端点

现在你已经对 FastAPI 有了一个基本的了解,你的开发环境也已经全部设置好,是时候迈出下一个激动人心的步骤:创建你的第一个 API 端点。

这就是 FastAPI 真正的魔力开始显现的地方。你会看到你可以多么轻松地构建一个功能性的 API 端点,准备好响应 HTTP 请求。

在这个菜谱中,你将创建一个书店后端服务的基本草案。

准备工作

确保你知道如何从 创建一个新的 FastAPI 项目 菜谱中启动一个基本的 FastAPI 项目。

如何做到这一点...

在 Web API 领域中,GET 请求可能是最常见的一种。它用于从服务器检索数据。在 FastAPI 中,处理 GET 请求既简单又直观。让我们创建一个基本的 GET 端点。

假设你正在构建一个书店的 API。你的第一个端点将在给定其 ID 的情况下提供关于一本书的信息。下面是如何做到这一点:

  1. 创建一个名为 bookstore 的新文件夹,它将包含你将要编写的代码。

  2. 在其中创建一个包含服务器实例的 main.py 文件:

    from fastapi import FastAPI
    app = FastAPI()
    @app.get("/books/{book_id}")
    async def read_book(book_id: int):
        return {
            "book_id": book_id,
            "title": "The Great Gatsby",
            "author": "F. Scott Fitzgerald"
        }
    

在前面的代码片段中,@app.get("/books/{book_id}") 装饰器告诉 FastAPI 这个函数将响应 /books/{book_id} 路径上的 GET 请求。路径中的 {book_id} 是一个路径参数,你可以用它来动态传递值。FastAPI 会自动提取 book_id 参数并将其传递给你的函数。

类型提示和自动数据验证

注意到使用了类型提示(book_id: int)。FastAPI 使用这些提示来进行数据验证。如果请求中带有非整数的 book_id 参数,FastAPI 会自动发送一个有用的错误响应。

它是如何工作的…

定义了 GET 端点后,使用 Uvicorn 运行你的 FastAPI 应用程序,就像你之前做的那样:

$ uvicorn main:app --reload

在终端上,您可以阅读描述服务器正在端口8000上运行的日志消息。

FastAPI 最受欢迎的特性之一是其使用 Swagger UI 自动生成交互式 API 文档。这个工具允许您直接从浏览器中测试您的 API 端点,而无需编写任何额外的代码,并且您可以直接检查其中新创建的端点是否存在。

使用 Swagger UI

要测试您的新GET端点,请在浏览器中导航到http://127.0.0.1:8000/docs。这个 URL 会显示您的 FastAPI 应用的 Swagger UI 文档。在这里,您会看到您的/books/{book_id}端点被列出。点击它,您将能够从界面直接执行测试请求。尝试输入一个书籍 ID,看看您的 API 生成的响应。

Postman – 一种多功能的替代方案

虽然 Swagger UI 对于快速测试来说很方便,但您可能希望使用像 Postman 这样的更健壮的工具来处理更复杂的场景。Postman 是一个 API 客户端,它允许您更广泛地构建、测试和记录您的 API。

要使用 Postman,请从 Postman 网站下载并安装它(www.postman.com/downloads/)。

安装完成后,创建一个新的请求。将方法设置为GET,并将请求 URL 设置为您的 FastAPI 端点,http://127.0.0.1:8000/books/1。点击发送,Postman 将显示您的 FastAPI 服务器的响应。

使用路径和查询参数进行工作

API 开发中最关键的一个方面是处理参数。参数允许您的 API 接受用户的输入,使您的端点变得动态和响应。

在这个食谱中,我们将探讨如何捕获和处理路径、查询参数,并高效地测试它们,从而增强 FastAPI 应用的灵活性和功能性。

准备工作

要遵循这个食谱,请确保您知道如何从上一个食谱中创建一个基本的端点。

如何操作...

路径参数是 URL 中预期会变化的组成部分。例如,在一个如/books/{book_id}的端点中,book_id是一个路径参数。FastAPI 允许您轻松捕获这些参数并在函数中使用它们。

  1. 让我们通过添加一个新的端点来扩展我们的书店 API,这个端点使用路径参数。这次,我们将创建一个获取特定作者信息的路由:

    @app.get("/authors/{author_id}")
    async def read_author(author_id: int):
        return {
            "author_id": author_id,
            "name": "Ernest Hemingway"
        }
    

    名称将不会改变;然而,author_id的值将是查询请求提供的那个。

    查询参数用于细化或自定义 API 端点的响应。它们可以包含在 URL 中的问号(?)之后。例如,/books?genre=fiction&year=2010可能会返回只有 2010 年发布的属于小说类别的书籍。

  2. 让我们在现有的端点中添加查询参数。假设我们想允许用户通过出版年份过滤书籍:

    @app.get("/books")
    async def read_books(year: int = None):
        if year:
            return {
                "year": year,
                "books": ["Book 1", "Book 2"]
            }
        return {"books": ["All Books"]}
    

在这里,year 是一个可选的查询参数。通过将其默认值设置为 None,我们使其成为可选的。如果指定了年份,端点将返回该年的书籍;否则,它将返回所有书籍。

练习

使用 APIRouter 类,将每个端点重构到单独的文件中,并将路由添加到 FastAPI 服务器。

它是如何工作的…

现在,从命令行终端,通过运行以下命令启动服务器:

$ uvicorn main:app

使用 Swagger UI 或 Postman 测试带有路径参数的端点,类似于我们测试基本 GET 端点的方式。

在 Swagger UI 中,在 http://localhost:8000/docs,导航到您的 /authors/{author_id} 端点。您会注意到在您可以尝试之前,它会提示您输入 author_id 值。输入一个有效的整数并执行请求。您应该会看到一个包含作者信息的响应。

GET /books 端点现在将显示一个可选的 year 查询参数字段。您可以通过输入不同的年份来测试它,并观察不同的响应。

如果您使用 Postman,创建一个新的 GET 请求,URL 为 http://127.0.0.1:8000/authors/1。发送此请求应该会产生类似的响应。

在 Postman 中,将查询参数附加到 URL,如下所示:http://127.0.0.1:8000/books?year=2021。发送此请求应该返回 2021 年出版的书籍。

参见

您可以在 FastAPI 官方文档中找到更多关于路径和查询参数的信息,以下是一些链接:

定义和使用请求和响应模型

在 API 开发的世界里,数据处理是决定您应用程序健壮性和可靠性的关键方面。FastAPI 通过与 Pydantic 的无缝集成简化了这一过程,Pydantic 是一个使用 Python 类型注解进行数据验证和设置管理的库。这个菜谱将向您展示如何在 FastAPI 中定义和使用请求和响应模型,确保您的数据结构良好、经过验证且定义清晰。

Pydantic 模型是数据验证和转换的强大功能。它们允许您定义应用程序处理的数据的结构、类型和约束,无论是传入请求还是传出响应。

在这个菜谱中,我们将看到如何使用 Pydantic 确保您的数据符合指定的模式,提供一层自动的安全性和清晰性。

准备工作

这个菜谱要求您知道如何在 FastAPI 中设置基本端点。

如何做到这一点…

我们将把整个过程分解为以下步骤:

  1. 创建模型

  2. 定义请求体

  3. 验证请求数据

  4. 管理响应格式

创建模型

让我们在名为 models.py 的新文件中为我们的书店应用程序创建一个 Pydantic BaseModel 类。

假设我们想要一个包含标题、作者和出版年份的书的模型:

from pydantic import BaseModel
class Book(BaseModel):
    title: str
    author: str
    year: int

在这里,Book 是一个具有三个字段:titleauthoryear 的 Pydantic BaseModel 类。每个字段都有类型,确保任何符合此模型的数据都将具有这些属性和指定的数据类型。

定义请求体

在 FastAPI 中,Pydantic 模型不仅用于验证,还作为请求体。让我们在我们的应用程序中添加一个端点,让用户可以添加新书:

from models import Book
@app.post("/book")
async def create_book(book: Book):
    return book

在此端点中,当用户向 /book 端点发送带有 JSON 数据的 POST 请求时,FastAPI 会自动解析并验证它是否与 Book 模型相匹配。如果数据无效,用户会收到自动的错误响应。

验证请求数据

Pydantic 提供了高级验证功能。例如,你可以添加正则表达式验证、默认值等:

from pydantic import BaseModel, Field
class Book(BaseModel):
    title: str = Field(..., min_length=1, max_length=100)
    author: str = Field(..., min_length=1, max_length=50)
    year: int = Field(..., gt=1900, lt=2100)

要查看完整的验证功能列表,请查看 Pydantic 的官方文档:docs.pydantic.dev/latest/concepts/fields/

接下来,你可以继续管理响应格式。

管理响应格式

FastAPI 允许你显式地定义响应模型,确保你的 API 返回的数据与特定的模式相匹配。这可以特别有用,用于过滤敏感数据或重新结构化响应。

例如,假设你想要 /allbooks GET 端点返回一本书的列表,但只包含它们的标题和作者,省略出版年份。在 main.py 中相应地添加以下内容:

from pydantic import BaseModel
class BookResponse(BaseModel):
    title: str
    author: str
@app.get("/allbooks")
async def read_all_books() -> list[BookResponse]:
    return [
        {
            "id": 1,
            "title": "1984",
            "author": "George Orwell"},
        {
            "id": 1,
            "title": "The Great Gatsby",
            "author": "F. Scott Fitzgerald",
        },
    ]

在这里,-> list[BookResponse] 函数类型提示告诉 FastAPI 使用 BookResponse 模型进行响应,确保响应 JSON 中只包含标题和作者字段。或者,你可以在端点装饰器的参数中指定响应类型,如下所示:

@app.get("/allbooks", response_model= list[BookResponse])
async def read_all_books() -> Any:
# rest of the endpoint content

response_model 参数具有优先级,可以用作替代类型提示来解决可能出现的类型检查问题。

请查阅文档,网址为 http://127.0.0.1:8000/docs。展开 /allbooks 端点详情,你会注意到基于以下模式的示例值响应:

[
  {
    "title": "string",
    "author": "string"
  }
]

通过掌握 FastAPI 中的 Pydantic 模型,你现在可以轻松且精确地处理复杂的数据结构。你已经学会了如何定义请求体和管理响应格式,确保在整个应用程序中保持数据的一致性和完整性。

参考也

Pydantic 是一个独立的项目,主要用于 Python 中的数据验证,具有比示例中展示的更多功能。请自由查看以下链接的官方文档:

你可以在 FastAPI 官方文档链接中了解更多关于响应模型的使用:fastapi.tiangolo.com/

处理错误和异常

错误处理是开发健壮和可靠的 Web 应用程序的一个基本方面。在 FastAPI 中,管理错误和异常不仅涉及捕获意外问题,还包括积极设计您的应用程序以优雅地应对各种错误场景。

本教程将指导您通过自定义错误处理、验证数据和处理异常,以及测试这些场景,以确保您的 FastAPI 应用程序具有弹性和用户友好性。

如何做到这一点...

FastAPI 提供了处理异常和错误的内置支持。

当发生错误时,FastAPI 会返回一个包含错误详细信息的 JSON 响应,这对于调试非常有用。然而,在某些情况下,您可能希望自定义这些错误响应以提供更好的用户体验或安全性。

让我们创建一个自定义错误处理器来捕获特定类型的错误并返回自定义响应。例如,如果请求的资源未找到,您可能希望返回一个更友好的错误消息。

要实现这一点,在main.py文件中相应地添加以下代码:

from fastapi import FastAPI, HTTPException
from starlette.responses import JSONResponse
@app.exception_handler(HTTPException)
async def http_exception_handler(request, exc):
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "message": "Oops! Something went wrong"
        },
    )

在本例中,将使用http_exception_handler函数来处理HTTPException错误。只要您的应用程序中任何地方引发HTTPException错误,FastAPI 就会使用此处理器来返回自定义响应。

您可以通过创建一个新的端点来引发 HTTP 异常来测试响应:

@app.get("/error_endpoint")
async def raise_exception():
    raise HTTPException(status_code=400)

该端点将明确抛出 HTTP 错误响应,以展示之前步骤中定义的自定义消息。

现在,使用以下命令从命令行启动服务器:

http://localhost:8000/error_endpoint, and you will have a JSON response like this:

{

"message": "Oops! Something went wrong"

}


 The response returns the default message we defined for any HTTP exception returned by the code.
There’s more…
As discussed in the previous recipe, *Defining and using request and response models*, FastAPI uses Pydantic models for data validation. When a request is made with data that does not conform to the defined model, FastAPI automatically raises an exception and returns an error response.
In some cases, you might want to customize the response for validation errors. FastAPI makes this quite straightforward:

import json

from fastapi import Request, status

from fastapi.exceptions import RequestValidationError

from fastapi.responses import PlainTextResponse

@app.exception_handler(RequestValidationError)

async def validation_exception_handler(

request: Request,

exc: RequestValidationError

):

return PlainTextResponse(

"这是一个纯文本响应:"

f" \n{json.dumps(exc.errors(), indent=2)}",

status_code=status.HTTP_400_BAD_REQUEST,

)


 This custom handler will catch any `RequestValidationError` error and return a plain text response with the details of the error.
If you try, for example, to call the `POST /book` endpoint with a number type of `title` instead of a string, you will get a response with a status code of `400` and body:

这是一个纯文本响应:

[

{

"type": "string_type",

"loc": [

"body",

"author"

],

"msg": "输入应是一个有效的字符串",

"input": 3,

"url": "https://errors.pydantic.dev/2.5/v/string_type"

},

{

"type": "greater_than",

"loc": [

"body",

"year"

],

"msg": "输入应大于 1900",

"input": 0,

"ctx": {

"gt": 1900

},

"url": "https://errors.pydantic.dev/2.5/v/greater_than"

}

]


 You can also, for example, mask the message to add a layer of security to protect from unwanted users using it incorrectly.
This is all you need to customize responses when a request validation error occurs.
You will use this basic knowledge as you move to the next chapter. *Chapter 2* will teach you more about data management in web applications, showing you how to set up and use SQL and NoSQL databases and stressing data security. This will not only improve your technical skills but also increase your awareness of creating scalable and reliable FastAPI applications.
See also
You can find more information about customizing errors and exceptions using FastAPI in the official documentation:

*   *Handling* *Errors*: [`fastapi.tiangolo.com/tutorial/handling-errors/`](https://fastapi.tiangolo.com/tutorial/handling-errors/)

第二章:与数据一起工作

数据处理是任何 Web 应用的骨架,本章致力于掌握这一关键方面。您将开始一段在 FastAPI 中处理数据的旅程,您将学习如何使用结构化查询语言SQL)和NoSQL数据库来集成、管理和优化数据存储的复杂性。我们将介绍 FastAPI 如何与强大的数据库工具结合,以创建高效和可扩展的数据管理解决方案。

从 SQL 数据库开始,您将获得实际操作经验,包括设置数据库、实现创建、读取、更新和删除CRUD)操作,以及理解与 SQLAlchemy(Python 中流行的对象关系映射ORM)选项)一起工作的细微差别。然后我们将转向 NoSQL 数据库,深入MongoDB的世界。您将学习如何将其与 FastAPI 集成,处理动态数据结构,并利用 NoSQL 解决方案的灵活性和可扩展性。

但这不仅仅是存储和检索数据。本章还关注保护敏感数据和管理数据库中的事务和并发性的最佳实践。您将探索如何保护您的数据免受漏洞的侵害,并确保应用程序数据操作的完整性和一致性。

到本章结束时,您不仅将深入了解如何在 FastAPI 中处理各种数据库系统,还将具备构建健壮和安全的 Web 应用数据模型所需的技能。无论是实现复杂查询、优化数据库性能还是确保数据安全,本章都提供了您管理应用程序数据所需的技术和知识。

在本章中,我们将介绍以下食谱:

  • 设置 SQL 数据库

  • 使用 SQLAlchemy 理解 CRUD 操作

  • 集成 MongoDB 用于 NoSQL 数据存储

  • 与数据验证和序列化一起工作

  • 与文件上传和下载一起工作

  • 处理异步数据操作

  • 保护敏感数据及最佳实践

每个主题都旨在为您提供处理 FastAPI 中数据的必要技能和知识,确保您的应用程序不仅功能齐全,而且安全且可扩展。

技术要求

为了有效地运行和理解本章中的代码,请确保您已设置以下内容。如果您已经跟随了第一章FastAPI 的第一步,您应该已经安装了一些这些内容:

  • Python:请确保您已在计算机上安装了 3.9 或更高版本的 Python。

  • pip install fastapi[all] 命令。正如我们在第一章FastAPI 的第一步中看到的,此命令还安装了Uvicorn,这是一个必要的 ASGI 服务器,用于运行您的 FastAPI 应用程序。

  • 集成开发环境IDE):应安装合适的 IDE,例如 VS CodePyCharm。这些 IDE 为 Python 和 FastAPI 开发提供了出色的支持,包括语法高亮、代码补全和易于调试等功能。

  • MongoDB:对于本章的 NoSQL 数据库部分,您需要在本地机器上安装 MongoDB。从 www.mongodb.com/try/download/community 下载并安装适合您操作系统的免费社区版服务器。

    通过命令行运行 Mongo Deamon 来确保 MongoDB 已正确安装:

    C:\Program>Files\MongoDB\Server\7.0\bin. You need to open the terminal in this location to run the daemon or run:
    
    

    $ C:\Program>Files\MongoDB\Server\7.0\bin\mongod -- version

    
    
  • MongoDB 工具:虽然不是必需的,但像 MongoDB Shell (www.mongodb.com/try/download/shell) 和 MongoDB Compass GUI (www.mongodb.com/try/download/compass) 这样的工具可以极大地增强您与 MongoDB 服务器的交互。它们提供了一个更用户友好的界面来管理数据库、运行查询和可视化数据结构。

本章中使用的所有代码和示例均可在 GitHub 上供参考和下载。请访问 github.com/PacktPublishing/FastAPI-Cookbook/tree/main/Chapter02 以访问存储库。

设置 SQL 数据库

在数据处理的世界里,Python 的力量与 SQL 数据库的效率相结合。本食谱旨在向您介绍如何在您的应用程序中集成 SQL 数据库,这对于任何希望构建健壮和可扩展的 Web 应用程序的开发者来说是一项关键技能。

SQL 是管理和管理关系型数据库的标准语言。当与 FastAPI 结合使用时,它解锁了数据存储和检索的无限可能。

FastAPI 与 SQL 数据库的兼容性是通过 ORM 实现的。其中最受欢迎的是 SQLAlchemy。在本食谱中,我们将重点关注它。

准备工作

首先,您需要确保 FastAPI 和 SQLAlchemy 已安装到您的虚拟环境中。如果您遵循了 第一章FastAPI 的第一步 中的步骤,那么您应该已经设置了 FastAPI。对于 SQLAlchemy,只需一个简单的 pip 命令即可:

$ pip install sqlalchemy

安装完成后,下一步是配置 SQLAlchemy,使其能够与 FastAPI 一起工作。这涉及到设置数据库连接——我们将一步步进行。

如何操作…

现在,让我们更深入地探讨如何为您的 FastAPI 应用程序配置 SQLAlchemy。SQLAlchemy 作为您 Python 代码和数据库之间的桥梁,允许您使用 Python 类和对象而不是编写原始 SQL 查询来与数据库交互。

在安装 SQLAlchemy 后,下一步是在你的 FastAPI 应用程序中配置它。这涉及到定义你的数据库模型——在 Python 代码中表示数据库表。在 SQLAlchemy 中,模型通常使用类来定义,每个类对应于数据库中的一个表,每个类的属性对应于表中的一个列。

按照以下步骤进行操作。

  1. 在当前目录下创建一个名为 sql_example 的新文件夹,进入该文件夹后,再创建一个名为 database.py 的文件。编写一个用作参考的 base 类:

    from sqlalchemy.orm import DeclarativeBase
    class Base(DeclarativeBase):
        pass
    

    要在 SQLAlchemy 中定义一个模型,你需要创建一个从 DeclarativeBase 类派生的基类。这个 Base 类维护了你定义的类和表的目录,并且是 SQLAlchemy ORM 功能的核心。

    你可以通过阅读官方文档来了解更多信息:docs.sqlalchemy.org/en/13/orm/extensions/declarative/index.xhtml

  2. 一旦你有了你的 Base 类,你就可以开始定义你的模型了。例如,如果你有一个用户表,你的模型可能看起来像这样:

    from sqlalchemy.orm import (
        Mapped,
        mapped_column
    )
    class User(Base):
        __tablename__ = "user"
        id: Mapped[int] = mapped_column(
            primary_key=True,
        )
        name: Mapped[str]
        email: Mapped[str]
    

    在这个模型中,User 类对应于数据库中名为 user 的表,包含 idnameemail 列。每个 class attribute 指定了列的数据类型。

  3. 一旦你的模型被定义,下一步就是连接到数据库并创建这些表。SQLAlchemy 使用连接字符串来定义它需要连接到的数据库的详细信息。这个连接字符串的格式取决于你使用的数据库系统。

    例如,SQLite 数据库的连接字符串可能看起来像这样:

    DATABASE_URL = "sqlite:///./test.db"
    

    第一次连接到 test.db 数据库文件时。

    你将使用 DATABASE_URL 连接字符串在 SQLAlchemy 中创建一个 Engine 对象,该对象代表与数据库的核心接口:

    from sqlalchemy import create_engine
    engine = create_engine(DATABASE_URL)
    
  4. 创建好引擎后,你可以继续在数据库中创建你的表。你可以通过传递你的 base 类和引擎到 SQLAlchemy 的 create_all 方法来完成此操作:

    Base.metadata.create_all(bind=engine)
    

现在你已经定义了代码中数据库的所有抽象,你可以继续设置数据库连接。

建立数据库连接

设置 SQL 数据库的最后一部分是建立数据库连接。这个连接允许你的应用程序与数据库通信,执行查询并检索数据。

数据库连接由会话管理。在 SQLAlchemy 中,会话代表了一个用于你的对象的 工作区,一个你可以添加新记录或检索现有记录的地方。每个会话都绑定到一个单独的数据库连接。

为了管理会话,我们需要创建一个 SessionLocal 类。这个类将被用来创建和管理与数据库交互的会话对象。以下是创建它的方法:

from sqlalchemy.orm import sessionmaker
SessionLocal = sessionmaker(
    autocommit=False, autoflush=False, bind=engine
)

sessionmaker 函数创建会话的工厂。autocommitautoflush 参数设置为 False,这意味着你必须手动提交事务并在更改刷新到数据库时管理它们。

SessionLocal 类就绪后,你可以创建一个函数,该函数将在你的 FastAPI 路由函数中使用,以获取一个新的数据库会话。我们可以在 main.py 模块中这样创建它:

from database import SessionLocal
def get_db()
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

在你的路由函数中,你可以使用此函数作为依赖项与数据库通信。

在 FastAPI 中,这可以通过 Depends 类来完成。在 main.py 文件中,你可以添加一个端点:

from fastapi import Depends, FastAPI
from sqlalchemy.orm import Session
from database import SessionLocal
app = FastAPI()
@app.get("/users/")
def read_users(db: Session = Depends(get_db)):
    users = db.query(User).all()
    return users

这种方法确保为每个请求创建一个新的会话,并在请求完成后关闭,这对于维护数据库事务的完整性至关重要。

你可以使用以下命令运行服务器:

$ uvicorn main:app –-reload

如果你尝试在 localhost:8000/users 上调用 GET 端点,你会得到一个空列表,因为没有添加任何用户。

参见

你可以在文档页面上了解更多关于如何在 SQLAlchemy 中设置会话的信息:

使用 SQLAlchemy 理解 CRUD 操作

在使用 FastAPI 设置好 SQL 数据库后,下一个关键步骤是创建数据库模型。这个过程对于你的应用程序如何与数据库交互至关重要。在 SQLAlchemy 中的 数据库模型 实质上是代表 SQL 数据库中表的 Python 类。它们提供了一个高级的面向对象接口,可以像处理常规 Python 对象一样操作数据库记录。

在这个菜谱中,我们将设置 创建、读取、更新和删除CRUD) 端点以与数据库交互。

准备工作

在设置好模型后,你现在可以实施 CRUD 操作。这些操作构成了大多数网络应用程序的骨架,允许你与数据库交互。

如何操作…

对于每个操作,我们将创建一个专门的端点,以实现与数据库的交互操作。

创建新用户

要添加新用户,我们将使用 POST 请求。在 main.py 文件中,我们必须定义一个端点,该端点接收用户数据,在请求体中创建一个新的 User 实例,并将其添加到数据库中:

class UserBody(BaseModel):
    name: str
    email: str
@app.post("/user")
def add_new_user(
    user: UserBody,
    db: Session = Depends(get_db)
):
    new_user = User(
        name=user.name,
        email=user.email
    )
    db.add(new_user)
    db.commit()
    db.refresh(new_user)
    return new_user

在几行代码中,你已经创建了添加新用户到数据库的端点。

读取特定用户

要获取单个用户,我们将使用 GET 端点:

from fastapi import HTTPException
@app.get("/user")
def get_user(
    user_id: int,
    db: Session = Depends(get_db)
    ):
    user = (
        db.query(User).filter(
            User.id == user_id
        ).first()
    )
    if user is None:
        raise HTTPException(
            status_code=404,
            detail="User not found"
        )
    return user

如果用户不存在,端点将返回 404 响应状态。

更新用户

通过 API 更新记录提供了各种方法,包括PUTPATCHPOST方法。尽管在理论上存在细微差别,但方法的选择通常取决于个人偏好。我倾向于使用POST请求,并通过添加user_id参数来增强/user端点。这简化了过程,最大限度地减少了需要大量记忆的需求。您可以在main.py模块中这样集成此端点:

@app.post("/user/{user_id}")
def update_user(
    user_id: int,
    user: UserBody,
    db: Session = Depends(get_db),
):
    db_user = (
        db.query(User).filter(
            User.id == user_id
        ).first()
    )
    if db_user is None:
        raise HTTPException(
            status_code=404,
            detail="User not found"
        )
    db_user.name = user.name
    db_user.email = user.email
    db.commit()
    db.refresh(db_user)
    return db_user

这就是创建数据库中更新用户记录端点所需做的所有事情。

删除用户

最后,要在同一main.py模块中删除用户,需要使用DELETE请求,如下所示:

@app.delete("/user")
def delete_user(
    user_id: int, db: Session = Depends(get_db)
):
    db_user = (
        db.query(User).filter(
            User.id == user_id
        ).first()
    )
    if db_user is None:
        raise HTTPException(
            status_code=404,
            detail="User not found"
        )
    db.delete(db_user)
    db.commit()
    return {"detail": "User deleted"}

这些端点涵盖了基本的 CRUD 操作,并展示了如何将 FastAPI 与 SQLAlchemy 集成以进行数据库操作。通过定义这些端点,您的应用程序可以创建、检索、更新和删除用户数据,为客户端交互提供一个完全功能的 API。

现在您已经实现了所有操作,您可以通过运行以下命令来启动服务器:

$ uvicorn main:app

然后,打开 inreactive 文档在http://localhost:8000/docs,并开始通过创建、读取、更新和删除用户来尝试这些端点。

在 FastAPI 中掌握这些 CRUD 操作是构建动态和以数据驱动的 Web 应用程序的重要一步。通过了解如何将 FastAPI 端点与 SQLAlchemy 模型集成,您已经具备了开发复杂和高效应用程序的能力。

参见

您可以在官方文档页面上找到如何使用 SQLAlchemy 设置 ORM 以进行 CRUD 操作的清晰快速入门指南:

集成 MongoDB 进行 NoSQL 数据存储

从 SQL 迁移到 NoSQL 数据库在数据存储和管理方面开辟了不同的范式。NoSQL 数据库,如 MongoDB,以其灵活性、可扩展性和处理大量非结构化数据的能力而闻名。在本食谱中,我们将探讨如何将流行的 NoSQL 数据库 MongoDB 与 FastAPI 集成。

NoSQL 数据库与传统 SQL 数据库的不同之处在于,它们通常允许更动态和灵活的数据模型。例如,MongoDB 以二进制 JSONBSON)格式存储数据,可以轻松适应数据结构的变化。这对于需要快速开发和频繁更新数据库模式的应用程序特别有用。

准备工作

确保您已在您的机器上安装了 MongoDB。如果您还没有安装,您可以从www.mongodb.com/try/download/community下载安装程序。

FastAPI 不提供用于 NoSQL 数据库的内置 ORM。然而,由于 Python 强大的库,将 MongoDB 集成到 FastAPI 中非常简单。

我们将使用pymongo,一个 Python 包驱动程序来与 MongoDB 交互。

首先,确保您已经在您的机器上安装并运行了 MongoDB。

然后,您可以使用 pip 安装 pymongo

$ pip install pymongo

在安装了 pymongo 之后,我们现在可以建立与 MongoDB 实例的连接并开始执行数据库操作。

如何做到这一点...

我们可以通过以下步骤快速将我们的应用程序连接到本地机器上运行的 Mongo DB 实例。

  1. 创建一个名为 nosql_example 的新项目文件夹。首先,在一个 database.py 文件中定义连接配置:

    From pymongo import MongoClient
    client = MongoClient()
    database = client.mydatabase
    

    在这个例子中,mydatabase 是您数据库的名称。您可以用您喜欢的名称替换它。在这里,MongoClient 通过连接到本地运行的 MongoDB 实例的 默认端口 27017 来建立连接。

  2. 一旦连接建立,您就可以定义您的集合(在 SQL 数据库中相当于表)并开始与之交互。MongoDB 将数据存储在文档集合中,其中每个文档都是一个类似 JSON 的结构:

    user_collection = database["users"]
    

    在这里,user_collection 是您 MongoDB 数据库中 users 集合的引用。

  3. 要测试连接,您可以在 main.py 文件中创建一个端点,该端点将检索所有用户,应该返回一个空列表:

    from database import user_collection
    from fastapi import FastAPI, HTTPException
    from pydantic import BaseModel
    app = FastAPI()
    class User(BaseModel):
        name: str
        email: str
    @app.get("/users")
    def read_users() -> list[User]:
        return [user for user in user_collection.find()]
    
  4. 现在,运行您的 mongod 实例。您可以从命令行执行此操作:

    $ mongod
    

    如果您在 Windows 上运行,命令将是:

    $ C:\Program>Files\MongoDB\Server\7.0\bin\mongod
    

就这样。为了测试它,在另一个终端窗口中,通过运行以下命令启动 FastAPI 服务器:

$ uvicorn main:app

然后,只需打开您的浏览器到 http://localhost:8000/users;您将看到一个空列表。这意味着您的数据库连接正在正确工作。

现在连接已经建立,我们将创建一个用于添加用户和用于通过 ID 获取特定用户的端点。我们将在 main.py 模块中创建这两个端点。

创建新用户

要向集合中添加新文档,请使用 insert_one 方法:

class UserResponse(User):
    id: str
@app.post("/user")
def create_user(user: User):
    result = user_collection.insert_one(
        user.model_dump(exclude_none=True)
    )
    user_response = UserResponse(
        id=str(result.inserted_id),
         *user.model_dump()
    )
    return user_response

我们刚刚创建的端点在响应中返回受影响的 id 号,用作其他端点的输入。

读取用户

要检索一个文档,您可以使用 find_one 方法:

from bson import ObjectId
@app.get("/user")
def get_user(user_id: str):
    db_user = user_collection.find_one(
        {
            "_id": ObjectId(user_id)
            if ObjectId.is_valid(user_id)
            else None
        }
    )
    if db_user is None:
        raise HTTPException(
            status_code=404,
            detail="User not found"
        )
    user_response = UserResponse(
        id=str(db_user["_id"]), **db_user
    )
    return user_response

如果指定的用户不存在,它将返回一个状态码为 404 的响应。

在 Mongo 中,文档的 ID 不会以纯文本形式存储,而是一个 12 字节的对象。这就是为什么在查询数据库时需要初始化一个专门的 bson.ObjectId,并在通过响应返回值时显式解码到 str

然后,您可以使用 uvicorn 启动服务器:

$ uvicorn main:app

您可以在交互式文档页面看到端点:localhost:8000/docs。确保您彻底测试每个端点及其之间的交互。

By integrating MongoDB with FastAPI, you gain the ability to handle dynamic, schemaless data structures, which is a significant advantage in many modern web applications. This recipe has equipped you with the knowledge to set up MongoDB, define models and collections, and perform CRUD operations, providing a solid foundation for building versatile and scalable applications with FastAPI and MongoDB.

See also

You can dig into how to use the PyMongo Python client by reading the official documentation:

Working with data validation and serialization

Effective data validation stands as a cornerstone of robust web applications, ensuring that incoming data meets predefined criteria and remains safe for processing.

FastAPI harnesses the power of Pydantic, a Python library dedicated to data validation and serialization. By integrating Pydantic models, FastAPI streamlines the process of validating and serializing data, offering an elegant and efficient solution. This recipe shows how to utilize Pydantic models within FastAPI applications, exploring how they enable precise validation and seamless data serialization.

Getting ready

Pydantic models are essentially Python classes that define the structure and validation rules of your data. They use Python’s type annotations to validate that incoming data matches the expected format. When you use a Pydantic model in your FastAPI endpoints, FastAPI automatically validates incoming request data against the model.

In this recipe, we’re going to use Pydantic’s email validator, which comes with the default pydantic package distribution. However, it needs to be installed in your environment. You can do this by running the following command:

$ pip install pydantic[email]

Once the package has been installed, you are ready to start this recipe.

How to do it…

Let’s use it in the previous project. In the main.py module, we’ll modify the UserCreate class, which is used to accept only valid email fields:

from typing import Optional
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
    name: str
name is a required string and email must be a valid email address. FastAPI will automatically use this model to validate incoming data for any endpoint that expects a UserCreate object.
Let’s say you try to add a user at the `POST /user` endpoint with an invalid user information body, as shown here:

{

"name": "John Doe",

"email": "invalidemail.com",

}


 You will get a `422` response with a message body specifying the invalid fields.
Serialization and deserialization concepts
**Serialization** is the process of converting complex data types, such as Pydantic models or database models, into simpler formats such as JSON, which can be easily transmitted over the network. **Deserialization** is the reverse process, converting incoming data into complex Python types.
FastAPI handles serialization and deserialization automatically using Pydantic models. When you return a Pydantic model from an endpoint, FastAPI serializes it to JSON. Conversely, when you accept a Pydantic model as an endpoint parameter, FastAPI deserializes the incoming JSON data into the model.
For example, the `get_user` endpoint from the NoSQL example can be improved further like so:

class UserResponse(User):

id: str

@app.get("/user")

def get_user(user_id: str) -> UserResponse:

db_user = user_collection.find_one(

{

"_id": ObjectId(user_id)

if ObjectId.is_valid(user_id)

else None

}

)

if db_user is None:

raise HTTPException(

status_code=404,

detail="User not found"

)

db_user["id"] = str(db_user["_id"])

User object and then serializes the returned UserResponse object back into JSON.

This automatic serialization and deserialization make working with JSON data in FastAPI straightforward and type-safe.

Advanced validation techniques

Pydantic offers a range of advanced validation techniques that you can leverage in FastAPI. These include custom validators and complex data types.

@field_validator.

For example, you could add a validator to ensure that a user’s age is within a certain range:

from pydantic import BaseModel, EmailStr, field_validator
class User(BaseModel):
    name: str
    email: EmailStr
    age: int
@field_validator("age")
    def validate_age(cls, value):
        if value < 18 or value > 100:
            raise ValueError(
                "Age must be between 18 and 100"
            )
age field of the User model is between 18 and 100.
If the validation fails, a descriptive error message is automatically returned to the client.
`list`, `dict`, and custom types, allowing you to define models that closely represent your data structures.
For instance, you can have a model with a list of items:

class Tweet(BaseModel):

content: str

hashtags: list[str]

class User(BaseModel):

name: str

email: EmailStr

age: Optional[int]

用户模型有一个可选的 tweets 字段,它是一个 Tweet 对象的列表。

通过利用 Pydantic 的高级验证功能,您可以确保 FastAPI 应用程序处理的数据不仅格式正确,而且符合您的特定业务逻辑和约束。这为在 FastAPI 应用程序中处理数据验证和序列化提供了一种强大且灵活的方法。

参见

您可以在文档页面了解更多关于 Pydantic 验证器的潜力:

处理文件上传和下载

在 Web 应用程序中处理文件是一个常见的需求,无论是上传用户头像、下载报告还是处理数据文件。FastAPI 提供了高效且易于实现的文件上传和下载方法。本食谱将指导您如何设置和实现 FastAPI 中的文件处理。

准备工作

让我们创建一个新的项目目录,名为uploads_and_downloads,其中包含一个名为main.py的模块和一个名为uploads的文件夹。这将包含应用程序侧的文件。目录结构将如下所示:

uploads_and_downloads/
|─ uploads/
|─ main.py

我们现在可以继续创建适当的端点。

如何操作...

要在 FastAPI 中处理文件上传,您必须使用 FastAPI 中的FileUploadFile类。UploadFile类特别有用,因为它提供了一个异步接口,并将大文件滚存到磁盘以避免内存耗尽。

main.py模块中,您可以定义如下上传文件的端点:

from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/uploadfile")
async def upload_file(
    file: UploadFile = File(...)):
    return {"filename": file.filename}

在此示例中,upload_file是一个端点,它接受一个上传的文件并返回其文件名。文件以UploadFile对象的形式接收,然后您可以将其保存到磁盘或进一步处理。

实现文件上传

在实现文件上传时,正确处理文件数据至关重要,以确保文件保存时不会损坏。以下是如何将上传的文件保存到服务器上目录的一个示例。

创建一个新的项目文件夹uploads_downloads

main.py模块中创建upload_file端点:

import shutil
from fastapi import FastAPI, File, UploadFile
app = FastAPI()
@app.post("/uploadfile")
async def upload_file(
    file: UploadFile = File(...),
):
    with open(
f"uploads/{file.filename}", "wb"
    ) as buffer:
        shutil.copyfileobj(file.file, buffer)
    return {"filename": file.filename}

此代码片段在uploads目录中以写二进制模式打开一个新文件,并使用shutil.copyfileobj将文件内容从UploadFile对象复制到新文件。

重要提示

在生产环境中,请记住适当地处理异常和错误,特别是对于较大的文件

创建一个包含一些文本的文本文件content.txt

通过运行 uvicorn main:app 命令来启动服务器。然后,访问交互式文档;你会观察到我们刚刚为文件上传创建的端点包含一个强制字段,提示用户上传文件。通过上传文件测试端点,你会发现上传的文件位于指定的 uploads 文件夹中。

管理文件下载和存储

下载文件是上传的逆过程。在 FastAPI 中,你可以轻松设置一个端点来提供文件下载。FileResponse 类对此特别有用。它从服务器流式传输文件到客户端,这使得为大型文件提供服务变得高效。

这里是一个简单的文件下载端点:

from fastapi.responses import FileResponse
@app.get(
    "/downloadfile/{filename}",
    response_class=FileResponse,
)
async def download_file(filename: str):
    if not Path(f"uploads/{filename}").exists():
        raise HTTPException(
            status_code=404,
            detail=f"file {filename} not found",
        )
    return FileResponse(
        path=f"uploads/{filename}", filename=filename
    )

在这个例子中,download_file 是一个端点,它从 uploads 目录提供文件供下载。在这里,FileResponse 会根据文件类型自动设置适当的内容类型头,并处理将文件流式传输到客户端。

文件内容将是端点的响应体。

处理文件存储是另一个关键方面,尤其是在处理大量文件或大文件大小时。通常建议将文件存储在专门的文件存储系统中,而不是直接存储在您的 Web 服务器上。可以将云存储解决方案如 Amazon S3Google Cloud StorageAzure Blob Storage 集成到您的 FastAPI 应用程序中,以实现可扩展和安全的文件存储。此外,考虑实施清理程序或归档策略来管理您存储的文件的生命周期。

参见

你可以在官方文档页面上了解更多关于如何管理上传文件的信息:

处理异步数据操作

异步编程 是 FastAPI 的一个核心特性,它允许你开发高度高效的 Web 应用程序。它允许你的应用程序同时处理多个任务,使其特别适合 I/O 密集型操作,如数据库交互、文件处理和网络通信。

让我们深入探讨在 FastAPI 中利用异步编程进行数据操作,以增强应用程序的性能和响应能力。

准备工作

FastAPI 是基于 Starlette 和 Pydantic 构建的,它们为使用 asyncio 库和 async/await 语法在 Python 中编写异步代码提供了一个强大的基础。

asyncio 库允许你编写非阻塞代码,在等待 I/O 操作完成时可以暂停其执行,然后从上次停止的地方继续执行,而无需阻塞主执行线程。

这个示例展示了在简单、实用的例子中使用 asyncio 和 FastAPI 的好处。

如何操作…

让我们创建一个包含两个端点的应用程序,一个运行睡眠操作,另一个也运行睡眠操作但以异步模式运行。创建一个新的项目文件夹async_example,包含main.py模块。按照以下内容填充模块。

  1. 让我们先创建 FastAPI 服务器对象类:

    from fastapi import FastAPI
    app = FastAPI()
    
    1. 现在,让我们创建一个睡眠 1 秒的端点:
    import time
    @app.get("/sync")
    def read_sync():
        time.sleep(2)
        return {
            "message": "Synchrounouns blocking endpoint"
        }
    

    睡眠操作代表在实际场景中从数据库获取响应的等待时间。

    1. 现在,让我们为async def版本创建相同的端点。睡眠操作将是来自asyncio模块的 sleep 函数:
    import asyncio
    @app.get("/async")
    async def read_async():
        await asyncio.sleep(2)
        return {
            "message": 
            "Asynchronous non-blocking endpoint"
        }
    

现在,我们有两个端点,GET /syncGET/async,它们除第二个包含非阻塞睡眠操作外,其他都相似。

一旦我们有了带有端点的应用程序,让我们创建一个单独的 Python 脚本来测量服务流量需求的时间。让我们称它为timing_api_calls.py,并通过以下步骤开始构建它。

  1. 让我们定义运行服务器的函数:

    import uvicorn
    from main import app
    def run_server():
        uvicorn.run(app, port=8000, log_level="error")
    
    1. 现在,让我们将服务器的开始定义为上下文管理器:
    from contextlib import contextmanager
    from multiprocessing import Process
    @contextmanager
    def run_server_in_process():
        p = Process(target=run_server)
        p.start()
        time.sleep(2)  # Give the server a second to start
        print("Server is running in a separate process")
        yield
        p.terminate()
    
    1. 现在,我们可以定义一个函数,该函数向指定的路径端点发送n个并发请求:
    async def make_requests_to_the_endpoint(
        n: int, path: str
    ):
        async with AsyncClient(
            base_url="http://localhost:8000"
        ) as client:
            tasks = (
                client.get(path, timeout=float("inf"))
                for _ in range(n)
            )
            await asyncio.gather(*tasks)
    
    1. 在这一点上,我们可以将操作组合到主函数中,为每个端点调用n次,并将服务所有调用的时间打印到终端:
    async def main(n: int = 10):
        with run_server_in_process():
            begin = time.time()
            await make_requests_to_the_endpoint(n,
                                                "/sync")
            end = time.time()
            print(
                f"Time taken to make {n} requests "
                f"to sync endpoint: {end - begin} seconds"
            )
            begin = time.time()
            await make_requests_to_the_endpoint(n,
                                                "/async")
            end = time.time()
            print(
                f"Time taken to make {n} requests "
                f"to async endpoint: {end - begin}
                seconds"
            )
    
    1. 最后,我们可以在asyncio事件循环中运行主函数:
    if __name__ == "__main__":
        asyncio.run(main())
    

现在我们已经构建了我们的计时脚本,让我们从命令终端按照以下方式运行它:

10, your output will likely resemble the one on my machine:

发送到同步端点的 10 次请求所需时间:2.3172452449798584 秒

发送到异步端点的 10 次请求所需时间:2.3033862113952637 秒


 It looks like there is no improvement at all with using asyncio programming.
Now, try to set the number of calls to `100`:

if name == "main":

asyncio.run(main(n=100))


 The output will likely be more like this:

发送到同步端点的 100 次请求所需时间:6.424988269805908 秒

发送到异步端点的 100 次请求所需时间:2.423431873321533 秒


 This improvement is certainly noteworthy, and it’s all thanks to the use of asynchronous functions.
There’s more…
Asynchronous data operations can significantly improve the performance of your application, particularly when dealing with high-latency operations such as database access. By not blocking the main thread while waiting for these operations to complete, your application remains responsive and capable of handling other incoming requests or tasks.
If you already wrote CRUD operations synchronously, as we did in the previous recipe, *Understanding CRUD operations with SQLAlchemy*, implementing asynchronous CRUD operations in FastAPI involves modifying your standard CRUD functions so that they’re asynchronous with the `sqlalchemy[asyncio]` library. Similarly to SQL, for NoSQL, you will need to use the `motor` package, which is the asynchronous MongoDB client built on top of `pymongo`.
However, it’s crucial to use asynchronous programming judiciously. Not all parts of your application will benefit from asynchrony, and in some cases, it can introduce complexity. Here are some best practices for using asynchronous programming in FastAPI:

*   **Use Async for I/O-bound operations**: Asynchronous programming is most beneficial for I/O-bound operations (such as database access, file operations, and network requests). CPU-bound tasks that require heavy computation might not benefit as much from asynchrony.
*   **Database transactions**: When working with databases asynchronously, be mindful of transactions. Ensure that your transactions are correctly managed to maintain the integrity of your data. This often involves using context managers (async with) to handle sessions and transactions.
*   **Error handling**: Asynchronous code can make error handling trickier, especially with multiple concurrent tasks. Use try-except blocks to catch and handle exceptions appropriately.
*   `async` and `await` in your test cases as needed.

By understanding and applying these concepts, you can build applications that are not only robust but also capable of performing optimally under various load conditions. This knowledge is a valuable addition to your skillset as a modern web developer working with FastAPI.
See also
An overview of the concurrency use of the `asyncio` library in FastAPI can be found on the documentation page:

*   *FastAPI* *C**oncurrency*: [`fastapi.tiangolo.com/async/`](https://fastapi.tiangolo.com/async/)

To integrate `async`/`await` syntax with **SQLAlchemy**, you can have a look at documentation support:

*   *SQLAlchemy* *Asyncio*: [`docs.sqlalchemy.org/en/20/orm/extensions/asyncio.xhtml`](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.xhtml)

*Chapter 6*, *Integrating FastAPI with SQL Databases*, will focus on SQL database interactions. Here, you can find examples of integrating `asyncio` with `sqlalchemy`.
To integrate `asyncio` with `motor`, which is built on top of `pymongo`:

*   *Motor asynchronous* *driver*: [`motor.readthedocs.io/en/stable/`](https://motor.readthedocs.io/en/stable/)

In *Chapter 7*, *Integrating FastAPI with NoSQL Databases*, you will find examples of motor integration with FastAPI.
Securing sensitive data and best practices
In the realm of web development, the security of sensitive data is paramount.
This recipe is a checklist of best practices for securing sensitive data in your FastAPI applications.
Getting ready
First and foremost, it’s crucial to understand the types of data that need protection. *Sensitive data* can include anything from passwords and tokens to personal user details. Handling such data requires careful consideration and adherence to security best practices.
Understanding the types of data that require protection sets the foundation for implementing robust security measures, such as leveraging environment variables for sensitive configurations, a key aspect of data security in app development.
Instead of hardcoding these values in your source code, they should be stored in environment variables, which can be accessed securely within your application. This approach not only enhances security but also makes your application more flexible and easier to configure across different environments.
Another important practice is encrypting sensitive data, particularly passwords. FastAPI doesn’t handle encryption directly, but you can use libraries such as `bcrypt` or `passlib` to hash and verify passwords securely.
This recipe will provide a checklist of good practices to apply to secure sensitive data.
How to do it…
Securely handling data in FastAPI involves more than just encryption; it encompasses a range of practices that are designed to protect data throughout its life cycle in your application.
Here is a list of good practices to apply when securing your application.

*   **Validation and sanitization**: Use the Pydantic model to validate and sanitize incoming data, as shown in the *Working with data validation and serialization* recipe. Ensure the data conforms to expected formats and values, reducing the risk of injection attacks or malformed data causing issues.

    Be cautious with data that will be output to users or logs. Sensitive information should be redacted or anonymized to prevent accidental disclosure.

*   **Access control**: Implement robust access control mechanisms to ensure that users can only access the data they are entitled to. This can involve **role-based access control** (**RBAC**), permission checks and properly managing user authentication. You will discover more about this in the *Setting up* *RBAC* recipe in *Chapter 4*, *Authentication* *and Authorization*.
*   **Secure communication**: Use HTTPS to encrypt data in transit. This prevents attackers from intercepting sensitive data that’s sent to or received from your application.
*   **Database security**: Ensure that your database is securely configured. Use secure connections, avoid exposing database ports publicly, and apply the principle of least privilege to database access.
*   **Regular updates**: Keep your dependencies, including FastAPI and its underlying libraries, up to date. This helps protect your application from vulnerabilities discovered in older versions of the software.

Some of them will be covered in detail throughout this book.
There’s more…
Managing sensitive data extends beyond immediate security practices and involves considerations for data storage, transmission, and even deletion.
Here’s a checklist of more general practices so that you can secure your data, regardless of whatever code you are writing:

*   **Data storage**: Store sensitive data only when necessary. If you don’t need to store data such as credit card numbers or personal identification numbers, then don’t. When storage is necessary, ensure it is encrypted and that access is tightly controlled.
*   **Data transmission**: Be cautious when transmitting sensitive data. Use secure APIs and ensure that any external services you interact with also follow security best practices.
*   **Data retention and deletion**: Have clear policies on data retention and deletion. When data is no longer needed, ensure it is deleted securely, leaving no trace in backups or logs.
*   **Monitoring and logging**: Implement monitoring to detect unusual access patterns or potential breaches. However, be careful with what you log. Avoid logging sensitive data and ensure that logs are stored securely and are only accessible to authorized personnel.

By applying these practices, you can significantly enhance the security posture of your applications, protecting both your users and your organization from potential data breaches and ensuring compliance with data protection regulations. As a developer, understanding and implementing data security is not just a skill but a responsibility in today’s digital landscape. In the next chapter, we will learn how to build an entire RESTful API with FastAPI.



第三章:使用 FastAPI 构建 RESTful API

在本章中,我们将深入探讨构建RESTful API的基本要素。RESTful API 是网络服务的骨架,它使得应用程序能够高效地进行通信和数据交换。

您将构建一个用于任务管理应用程序的 RESTful API。该应用程序将与 CSV 文件交互,尽管对于此类应用程序,典型的做法是使用数据库,如 SQL 或 NoSQL。这种方法是非传统的,并且由于可扩展性和性能限制,不建议在大多数场景中使用。然而,在某些情况下,特别是在遗留系统或处理大量结构化数据文件时,通过 CSV 管理数据可能是一个实用的解决方案。

我们的任务管理 API 将允许用户创建、读取、更新和删除CRUD)任务,每个任务都表示为 CSV 文件中的一个记录。本例将提供在 FastAPI 中处理非标准格式数据的见解。

我们将了解如何测试 API 的端点。随着 API 的增长,管理复杂查询和过滤变得至关重要。我们将探讨实现高级查询功能的技术,增强 API 的可用性和灵活性。

此外,我们将解决 API 版本化的重要问题。版本化是随着时间的推移演进 API 而不破坏现有客户端的关键。您将学习管理 API 版本的战略,确保向后兼容性和用户平滑过渡。

最后,我们将介绍使用 OAuth2 保护 API,这是一种行业标准的授权协议。安全性在 API 开发中至关重要,您将获得实施身份验证和保护端点的实践经验。

在本章中,我们将涵盖以下食谱:

  • 创建 CRUD 操作

  • 创建 RESTful 端点

  • 测试您的 RESTful API

  • 处理复杂查询和过滤

  • API 版本化

  • 使用 OAuth2 保护您的 API

  • 使用 Swagger 和 Redoc 记录您的 API

技术要求

为了在FastAPI 食谱集中充分参与本章的学习,并有效地构建 RESTful API,您需要安装和配置以下技术和工具:

  • Python:请确保您的环境中安装了高于 3.9 版本的 Python。

  • FastAPI:应安装所有必需的依赖项。如果您尚未从前面的章节中安装,您可以从终端简单地使用以下命令进行安装:

    $ pip install fastapi[all]
    
  • Pytest:您可以通过运行以下命令来安装此框架:

    $ pip install pytest
    

注意,已经对 Pytest 框架有所了解可能会非常有用,以便更好地遵循测试您的 RESTful API食谱。

本章中使用的代码可在 GitHub 上找到,地址为:github.com/PacktPublishing/FastAPI-Cookbook/tree/main/Chapter03

随时可以跟随或查阅,以防遇到困难。

创建 CRUD 操作

这个配方将向您展示如何使用作为数据库的 CSV 文件来实现基本的 CRUD 操作。

我们将开始为简单的任务列表草拟一个 CSV 格式的草案,并将操作放在一个单独的 Python 模块中。到配方结束时,您将拥有所有准备通过 API 端点使用的操作。

如何做到这一点…

让我们先创建一个名为 task_manager_app 的项目根目录,用于存放我们的应用程序代码库:

  1. 进入根项目文件夹,创建一个 tasks.csv 文件,我们将将其用作数据库,并在其中放入一些任务:

    id,title,description,status
    1,Task One,Description One,Incomplete
    2,Task Two,Description Two,Ongoing
    
  2. 然后,创建一个名为 models.py 的文件,其中包含我们将用于内部代码的 Pydantic 模型。它看起来如下所示:

    from pydantic import BaseModel
    class Task(BaseModel):
        title: str
        description: str
        status: str
    class TaskWithID(Task):
        id: int
    

    我们创建了两个独立的任务对象类,因为 id 在整个代码中都不会使用。

  3. 在一个名为 operations.py 的新文件中,我们将定义与我们的数据库交互的函数。

    我们可以开始创建 CRUD 操作

    创建一个从 .csv 文件中检索所有任务的函数:

    import csv
    from typing import Optional
    from models import Task, TaskWithID
    DATABASE_FILENAME = "tasks.csv"
    column_fields = [
        "id", "title", "description", "status"
    ]
    def read_all_tasks() -> list[TaskWithID]:
        with open(DATABASE_FILENAME) as csvfile:
            reader = csv.DictReader(
                csvfile,
            )
            return [TaskWithID(**row) for row in reader]
    
  4. 现在,我们需要创建一个基于 id 读取特定任务的函数:

    def read_task(task_id) -> Optional[TaskWithID]:
        with open(DATABASE_FILENAME) as csvfile:
            reader = csv.DictReader(
                csvfile,
            )
            for row in reader:
                if int(row["id"]) == task_id:
                    return TaskWithID(**row)
    
  5. 要编写一个任务,我们需要一个策略来为新写入数据库的任务分配一个新的 id

    一个好的策略是实施一个基于数据库中已存在的 ID 的逻辑,然后将任务写入我们的 CSV 文件,并将这两个操作组合到一个新的函数中。我们可以将创建任务操作拆分为三个函数。

    首先,让我们创建一个基于数据库中现有 ID 的函数来检索新 ID:

    def get_next_id():
        try:
            with open(DATABASE_FILENAME, "r") as csvfile:
                reader = csv.DictReader(csvfile)
                max_id = max(
                    int(row["id"]) for row in reader
                )
                return max_id + 1
        except (FileNotFoundError, ValueError):
            return 1
    

    然后,我们定义一个将任务写入 CSV 文件中具有 ID 的函数:

    def write_task_into_csv(
        task: TaskWithID
    ):
        with open(
            DATABASE_FILENAME, mode="a", newline=""
        ) as file:
            writer = csv.DictWriter(
                file,
                fieldnames=column_fields,
            )
            writer.writerow(task.model_dump())
    

    之后,我们可以利用这两个最后的功能来定义创建任务的函数:

    def create_task(
        task: Task
    ) -> TaskWithID:
        id = get_next_id()
        task_with_id = TaskWithID(
            id=id, **task.model_dump()
        )
        write_task_into_csv(task_with_id)
        return task_with_id
    
  6. 然后,让我们创建一个修改任务的函数:

    def modify_task(
        id: int, task: dict
    ) -> Optional[TaskWithID]:
        updated_task: Optional[TaskWithID] = None
        tasks = read_all_tasks()
        for number, task_ in enumerate(tasks):
            if task_.id == id:
                tasks[number] = (
                    updated_task
                ) = task_.model_copy(update=task)
        with open(
            DATABASE_FILENAME, mode="w", newline=""
        ) as csvfile:  # rewrite the file
            writer = csv.DictWriter(
                csvfile,
                fieldnames=column_fields,
            )
            writer.writeheader()
            for task in tasks:
                writer.writerow(task.model_dump())
        if updated_task:
            return updated_task
    
  7. 最后,让我们创建一个删除具有特定 id 的任务的函数:

    def remove_task(id: int) -> bool:
        deleted_task: Optional[Task] = None
        tasks = read_all_tasks()
        with open(
            DATABASE_FILENAME, mode="w", newline=""
        ) as csvfile:  # rewrite the file
            writer = csv.DictWriter(
                csvfile,
                fieldnames=column_fields,
            )
            writer.writeheader()
            for task in tasks:
                if task.id == id:
                    deleted_task = task
                    continue
                writer.writerow(task.model_dump())
        if deleted_task:
            dict_task_without_id = (
                deleted_task.model_dump()
            )
            del dict_task_without_id["id"]
            return Task(**dict_task_wihtout_id)
    

您刚刚创建了基本的 CRUD 操作。我们现在准备通过 API 端点公开这些操作。

它是如何工作的…

您的 API 结构在 RESTful 设计中至关重要。它涉及定义端点(URI)并将它们与 HTTP 方法关联以执行所需的操作。

在我们的任务管理系统(Task Management system)中,我们将创建处理任务的端点,以反映常见的 CRUD 操作。以下是概述:

  • 列出任务 (GET /tasks) 获取所有任务的列表

  • 检索任务 (GET /tasks/{task_id}) 获取特定任务的详细信息

  • 创建任务 (POST /task) 添加一个新任务

  • 更新任务 (PUT /tasks/{task_id}) 修改现有任务

  • 删除任务 (DELETE /tasks/{task_id}) 删除一个任务

每个端点代表 API 中的一个特定函数,定义明确且目的明确。FastAPI 的路由系统允许我们轻松地将这些操作映射到 Python 函数。

练习

尝试为每个 CRUD 操作编写单元测试。如果您跟随 GitHub 仓库,您可以在 Chapter03/task_manager_rest_api/test_operations.py 文件中找到测试。

创建 RESTful 端点

现在,我们将创建路由来通过特定的端点公开每个 CRUD 操作。在这个菜谱中,我们将看到 FastAPI 如何利用 Python 类型注解来定义预期的请求和响应数据类型,从而简化验证和序列化数据的过程。

准备工作…

在开始菜谱之前,请确保您知道如何设置本地环境并创建一个基本的 FastAPI 服务器。您可以在 第一章创建新的 FastAPI 项目理解 FastAPI 基础 菜谱中查看它。

此外,我们还将使用之前菜谱中创建的 CRUD 操作。

如何做到这一点…

让我们在项目根目录中创建一个 main.py 文件来编写带有端点的服务器。FastAPI 简化了不同 HTTP 方法的实现,使它们与相应的 CRUD 操作相匹配。

现在,让我们为每个操作编写端点:

  1. 使用 read_all_tasks 操作创建一个端点来列出所有任务的服务器:

    from fastapi import FastAPI, HTTPException
    from models import (
        Task,
        TaskWithID,
    )
    from operations import read_all_tasks
    app = FastAPI()
    @app.get("/tasks", response_model=list[TaskWithID])
    def get_tasks():
        tasks = read_all_tasks()
        return tasks
    
  2. 现在,让我们编写一个端点来根据 id 读取特定的任务:

    @app.get("/task/{task_id}")
    def get_task(task_id: int):
        task = read_task(task_id)
        if not task:
            raise HTTPException(
                status_code=404, detail="task not found"
            )
        return task
    
  3. 添加任务的端点如下:

    from operations import create_task
    @app.post("/task", response_model=TaskWithID)
    def add_task(task: Task):
        return create_task(task)
    
  4. 要更新任务,我们可以修改每个字段(descriptionstatustitle)。为此,我们创建一个用于正文的特定模型,称为 UpdateTask。端点将如下所示:

    from operations import modify_task
    class UpdateTask(BaseModel):
        title: str | None = None
        description: str | None = None
        status: str | None = None
    @app.put("/task/{task_id}", response_model=TaskWithID)
    def update_task(
        task_id: int, task_update: UpdateTask
    ):
        modified = modify_task(
            task_id,
            task_update.model_dump(exclude_unset=True),
        )
        if not modified:
            raise HTTPException(
                status_code=404, detail="task not found"
            )
        return modified
    
  5. 最后,这是删除任务的端点:

    from operations import remove_task
    @app.delete("/task/{task_id}", response_model=Task)
    def delete_task(task_id: int):
        removed_task = remove_task(task_id)
        if not removed_task:
            raise HTTPException(
                status_code=404, detail="task not found"
            )
        return removed_task
    

您刚刚实现了与用作数据库的 CSV 文件交互的操作。

在项目根目录级别的命令行中,使用 uvicorn 命令启动服务器:

$ uvicorn main:app

在浏览器中,访问 http://localhost:8000/docs,您将看到您刚刚创建的 RESTful API 的端点。

您可以通过创建一些任务,然后列出它们,更新它们,并直接通过交互式文档删除一些任务来实验。

测试您的 RESTful API

测试是 API 开发的一个关键部分。在 FastAPI 中,您可以使用各种测试框架,如 pytest,来编写 API 端点的测试。

在这个菜谱中,我们将为之前创建的每个端点编写单元测试。

准备工作…

如果尚未完成,请确保您已经通过运行以下命令在您的环境中安装了 pytest

$ pip install pytest

在测试中,使用一个专门的数据库来避免与生产数据库交互是一个好的实践。为了实现这一点,我们将创建一个测试固定装置,在每次测试之前生成数据库。

我们将在 conftest.py 模块中定义它,以便固定装置应用于项目根目录下的所有测试。让我们在项目根目录中创建该模块,并首先定义一个测试任务列表和用于测试的 CSV 文件名称:

TEST_DATABASE_FILE = "test_tasks.csv"
TEST_TASKS_CSV = [
    {
        "id": "1",
        "title": "Test Task One",
        "description": "Test Description One",
        "status": "Incomplete",
    },
    {
        "id": "2",
        "title": "Test Task Two",
        "description": "Test Description Two",
        "status": "Ongoing",
    },
]
TEST_TASKS = [
    {**task_json, "id": int(task_json["id"])}
    for task_json in TEST_TASKS_CSV
]

我们现在可以创建一个将用于所有测试的固定装置。这个固定装置将在每个测试函数执行之前设置测试数据库。

我们可以通过将autouse=True参数传递给pytest.fixture装饰器来实现这一点,这表示该功能将在每个测试之前运行:

import csv
import os
from pathlib import Path
from unittest.mock import patch
import pytest
@pytest.fixture(autouse=True)
def create_test_database():
    database_file_location = str(
        Path(__file__).parent / TEST_DATABASE_FILE
    )
    with patch(
        "operations.DATABASE_FILENAME",
        database_file_location,
    ) as csv_test:
        with open(
            database_file_location, mode="w", newline=""
        ) as csvfile:
            writer = csv.DictWriter(
                csvfile,
                fieldnames=[
                    "id",
                    "title",
                    "description",
                    "status",
                ],
            )
            writer.writeheader()
            writer.writerows(TEST_TASKS_CSV)
            print("")
        yield csv_test
        os.remove(database_file_location)

由于固定装置定义在conftest.py模块中,每个测试模块将自动导入它。

现在,我们可以继续创建之前配方中创建的端点的实际单元测试函数:

如何操作…

为了测试端点,FastAPI 提供了一个特定的TestClient类,允许在不运行服务器的情况下测试端点。

在一个名为test_main.py的新模块中,让我们定义我们的测试客户端:

from main import app
from fastapi.testclient import TestClient
client = TestClient(app)

我们可以像以下这样为每个端点创建测试。

  1. 让我们从GET /tasks端点开始,该端点列出数据库中的所有任务:

    from conftest import TEST_TASKS
    def test_endpoint_read_all_tasks():
        response = client.get("/tasks")
        assert response.status_code == 200
        assert response.json() == TEST_TASKS
    

    我们正在断言响应的状态码和json正文。

  2. 就这么简单,我们可以通过创建GET /tasks/{task_id}的测试来继续,以读取具有特定id的任务:

    def test_endpoint_get_task():
        response = client.get("/task/1")
        assert response.status_code == 200
        assert response.json() == TEST_TASKS[0]
        response = client.get("/task/5")
        assert response.status_code == 404
    

    除了现有任务的200状态码外,我们还断言当任务不存在于数据库中时,状态码等于404

  3. 以类似的方式,我们可以通过断言任务的新分配id来测试POST /task端点,以便将新任务添加到数据库中:

    from operations import read_all_tasks
    def test_endpoint_create_task():
        task = {
            "title": "To Define",
            "description": "will be done",
            "status": "Ready",
        }
        response = client.post("/task", json=task)
        assert response.status_code == 200
        assert response.json() == {**task, "id": 3}
        assert len(read_all_tasks()) == 3
    
  4. 修改任务的PUT /tasks/{task_id}端点的测试将是以下内容:

    from operations import read_task
    def test_endpoint_modify_task():
        updated_fields = {"status": "Finished"}
        response = client.put(
            "/task/2", json=updated_fields
        )
        assert response.status_code == 200
        assert response.json() == {
             *TEST_TASKS[1],
             *updated_fields,
        }
        response = client.put(
            "/task/3", json=updated_fields
        )
        assert response.status_code == 404
    
  5. 最后,我们测试DELETE /tasks/{task_id}端点以删除任务:

    def test_endpoint_delete_task():
        response = client.delete("/task/2")
        assert response.status_code == 200
        expected_response = TEST_TASKS[1]
        del expected_response["id"]
        assert response.json() == expected_response
        assert read_task(2) is None
    

你已经为每个 API 端点编写了所有单元测试。

你现在可以从项目根目录运行测试,在终端中运行或在您最喜欢的编辑器的 GUI 支持下运行:

$ pytest .

Pytest 将收集所有测试并运行它们。如果你正确编写了测试,你将在控制台输出中看到一条消息,表明你获得了 100%的分数。

参见

你可以在 Pytest 文档中检查测试固定装置:

你可以在官方文档中深入了解 FastAPI 测试工具和TestClient API:

处理复杂查询和过滤

在任何 RESTful API 中,提供基于某些标准过滤数据的功能是至关重要的。在这个配方中,我们将增强我们的任务管理 API,允许用户根据不同的参数过滤任务并创建一个搜索端点。

准备中…

过滤功能将在现有的GET /tasks端点中实现,以展示如何超载端点,而搜索功能将在全新的端点中展示。在继续之前,请确保您已经实现了至少 CRUD 操作。

如何操作…

我们将首先通过过滤器对GET /tasks端点进行过度充电。我们修改端点以接受两个查询参数:statustitle

该端点将看起来如下所示:

@app.get("/tasks", response_model=list[TaskWithID])
def get_tasks(
    status: Optional[str] = None,
    title: Optional[str] = None,
):
    tasks = read_all_tasks()
    if status:
        tasks = [
            task
            for task in tasks
            if task.status == status
        ]
    if title:
        tasks = [
            task for task in tasks if task.title == title
        ]
    return tasks

这两个参数可以可选地指定以过滤匹配其值的任务。

接下来,我们实现搜索功能。除了基本的过滤外,实现搜索功能可以显著提高 API 的可用性。我们将在新的端点中添加一个搜索功能,允许用户根据标题或描述中的关键词查找任务:

@app.get("/tasks/search", response_model=list[TaskWithID])
def search_tasks(keyword: str):
    tasks = read_all_tasks()
    filtered_tasks = [
        task
        for task in tasks
        if keyword.lower()
        in (task.title + task.description).lower()
    ]
    return filtered_tasks

search_tasks端点中,该函数会过滤任务,只包括标题或描述中包含关键词的任务。

要像往常一样启动服务器,请在命令行中运行此命令:

$ uvicorn main:app

然后,转到交互式文档地址http://localhost:8000/docs,您将看到我们刚刚创建的新端点。

通过指定可能出现在您任务标题或描述中的某些关键词来尝试一下。

API 版本控制

API 版本控制对于维护和演进网络服务而不中断现有用户至关重要。它允许开发者在提供向后兼容性的同时引入更改、改进或甚至破坏性更改。在这个食谱中,我们将实现我们的任务管理 API 的版本控制。

准备中…

要遵循食谱,您需要已经定义了端点。如果您还没有,可以先查看创建 RESTful 端点的食谱。

如何做到这一点...

对于 API 版本控制,有几种策略。我们将使用最常见的方法,即 URL 路径版本控制,来为我们的 API 使用。

让我们考虑我们想要通过添加一个名为priority的新str字段来改进任务信息,该字段默认设置为"lower"。让我们通过以下步骤来完成它。

  1. 让我们在models.py模块中创建一个名为TaskV2的对象类:

    from typing import Optional
    class TaskV2(BaseModel):
        title: str
        description: str
        status: str
        priority: str | None = "lower"
    class TaskV2WithID(TaskV2):
        id: int
    
  2. operations.py模块中,让我们创建一个名为read_all_tasks_v2的新函数,该函数读取所有任务,并添加priority字段:

    from models import TaskV2WIthID
    def read_all_tasks_v2() -> list[TaskV2WIthID]:
        with open(DATABASE_FILENAME) as csvfile:
            reader = csv.DictReader(
                csvfile,
            )
            return [TaskV2WIthID(**row) for row in reader]
    
  3. 我们现在已经拥有了创建read_all_tasks函数第二个版本所需的一切。我们将在main.py模块中完成这项工作:

    from models import TaskV2WithID
    @app.get(
        "/v2/tasks",
        response_model=list[TaskV2WithID]
    )
    def get_tasks_v2():
        tasks = read_all_tasks_v2()
        return tasks
    

您刚刚创建了端点的第二个版本。这样,您就可以通过端点的几个版本来开发和改进您的 API。

为了测试它,让我们通过手动将新字段添加到tasks.csv文件中来修改它,以测试新端点:

id,title,description,status,priority
1,Task One,Description One,Incomplete
2,Task Two,Description Two,Ongoing,higher

再次从命令行启动服务器:

$ uvicorn main:app

现在,交互式文档http://localhost:8000/docs将显示新的GET /v2/tasks端点,以列出所有以版本 2 模式运行的任务。

检查端点是否列出了带有新priority字段的任务,并且旧的GET /tasks是否仍然按预期工作。

练习

你可能已经注意到,使用 CSV 文件作为数据库可能不是最可靠的解决方案。如果在更新或删除过程中进程崩溃,你可能会丢失所有数据。因此,通过使用与 SQLite 数据库交互的操作函数的新版本端点来改进 API。

更多内容…

当你对 API 进行版本控制时,你实际上是在提供一个区分不同 API 发布或版本的方法,允许客户端选择他们想要交互的版本。

除了我们在配方中使用的基于 URL 的方法之外,还有其他常见的 API 版本控制方法,例如以下内容:

  • 查询参数版本控制:版本信息作为 API 请求中的查询参数传递。例如,参见以下内容:

    https://api.example.com/resource?version=1
    

    这种方法保持了不同版本之间的基础 URL 统一。

  • 头部版本控制:版本信息在 HTTP 请求的自定义头部中指定:

    GET /resource HTTP/1.1
    Host: api.example.com
    X-API-Version: 1
    

    这种方法保持了 URL 的简洁性,但要求客户端在他们的请求中显式设置版本。

  • 基于消费者的版本控制:这种策略允许客户选择他们需要的版本。他们在第一次交互时保存的版本将与他们的详细信息一起使用,并在所有未来的交互中使用,除非他们进行更改。

此外,可以使用MAJOR.MINOR.PATCH格式。MAJOR版本的更改表示不兼容的 API 更改,而MINORPATCH版本的更改表示向后兼容的更改。

版本控制允许 API 提供商在不破坏现有客户端集成的情况下引入更改(如添加新功能、修改现有行为或弃用端点和日落策略)。

它还让消费者控制何时以及如何采用新版本,最小化中断并保持 API 生态系统的稳定性。

参见

你可以查看 Postman 博客上关于 API 版本控制策略的一篇有趣的文章:

使用 OAuth2 保护您的 API

在 Web 应用程序中,保护端点免受未经授权的用户访问至关重要。OAuth2是一个常见的授权框架,它允许应用程序通过具有受限权限的用户账户访问。它是通过发行令牌而不是凭据来工作的。本配方将展示如何在我们的任务管理器 API 中使用 OAuth2 来保护端点。

准备中…

FastAPI 支持使用密码的 OAuth2,包括使用外部令牌。数据合规性法规要求密码不以明文形式存储。相反,通常的方法是存储散列操作的输出,这会将明文转换为人类无法读取的字符串,并且无法逆转。

重要提示

仅为了展示功能,我们将使用简单的机制来模拟散列机制以及令牌创建。出于明显的安全原因,请不要在生产环境中使用。

如何实现…

让我们在项目根目录中创建一个 security.py 模块,我们将在这里实现所有用于保护我们服务的工具。然后,让我们创建一个如下所示的安全端点。

  1. 首先,让我们创建一个包含用户名和密码的用户列表的字典:

    fake_users_db = {
        "johndoe": {
            "username": "johndoe",
            "hashed_password": "hashedsecret",
        },
        "janedoe": {
            "username": "janedoe",
            "hashed_password": "hashedsecret2",
        },
    }
    
  2. 密码不应该以纯文本形式存储,而应该加密或散列。为了演示这个特性,我们通过在密码字符串前插入 "hashed" 来模拟散列机制:

    def fakely_hash_password(password: str):
        return f"hashed{password}"
    
  3. 让我们创建处理用户和从我们创建的 dict 数据库中检索用户的函数的类:

    class User(BaseModel):
        username: str
    class UserInDB(User):
        hashed_password: str
    def get_user(db, username: str):
        if username in db:
            user_dict = db[username]
            return UserInDB(**user_dict)
    
  4. 使用与我们刚才用于散列的类似逻辑,让我们创建一个模拟令牌生成器和模拟令牌解析器:

    def fake_token_generator(user: UserInDB) -> str:
        # This doesn't provide any security at all
        return f"tokenized{user.username}"
    def fake_token_resolver(
        token: str
    ) -> UserInDB | None:
        if token.startswith("tokenized"):
            user_id = token.removeprefix("tokenized")
            user = get_user(fake_users_db, user_id)
            return user
    
  5. 现在,让我们创建一个函数来从令牌中检索用户。为此,我们将使用 Depends 类来利用 FastAPI 提供的依赖注入(见 fastapi.tiangolo.com/tutorial/dependencies/),使用 OAuthPasswordBearer 类来处理令牌:

    from fastapi import Depends, HTTPException, status
    oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
    def get_user_from_token(
        token: str = Depends(oauth2_scheme),
    ) -> UserInDB:
        user = fake_token_resolver(token)
        if not user:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail=(
                    "Invalid authentication credentials"
                ),
                headers={"WWW-Authenticate": "Bearer"},
            )
        return user
    

    oauth2scheme 包含了将被交互式文档用于认证浏览器的 /token URL 端点。

重要提示

我们已经使用依赖注入从 get_user_token 函数中检索令牌,使用了 fastapi.Depends 对象。依赖注入模式不是 Python 语言的原生特性,它与 FastAPI 框架紧密相关。在第八章 高级特性和最佳实践中,你可以找到一个专门的配方,称为 实现 依赖注入

  1. 让我们在 main.py 模块中创建端点:

    from fastapi import Depends, HTTPException
    from fastapi.security import OAuth2PasswordRequestForm
    from security import (
        UserInDB,
        fake_token_generator,
        fakely_hash_password,
        fake_users_db
    )
    @app.post("/token")
    async def login(
        form_data: OAuth2PasswordRequestForm = Depends(),
    ):
        user_dict = fake_users_db.get(form_data.username)
        if not user_dict:
            raise HTTPException(
                status_code=400,
                detail="Incorrect username or password",
            )
        user = UserInDB(**user_dict)
        hashed_password = fakely_hash_password(
            form_data.password
        )
        if not hashed_password == user.hashed_password:
            raise HTTPException(
                status_code=400,
                detail="Incorrect username or password",
            )
        token = fake_token_generator(user)
        return {
            "access_token": token,
            "token_type": "bearer"
        }
    

    现在我们已经拥有了创建带有 OAuth2 认证的安全端点所需的一切。

  2. 我们将要创建的端点将根据提供的令牌返回有关当前用户的信息。如果令牌没有授权,它将返回一个 400 异常:

    from security import get_user_from_token
    @app.get("/users/me", response_model=User)
    def read_users_me(
        current_user: User = Depends(get_user_from_token),
    ):
        return current_user
    

    我们刚刚创建的端点将只能被允许的用户访问。

现在我们来测试我们的安全端点。在项目根目录的命令行终端中,通过运行以下命令启动服务器:

$ uvicorn main:app

然后,打开浏览器,访问 http://localhost:8000/docs,你将注意到交互式文档中的新 tokenusers/me 端点。

你可能会在 users/me 端点注意到一个小锁形图标。如果你点击它,你会看到一个表单窗口,允许你获取令牌并将其直接存储在你的浏览器中,这样你就不必每次调用安全端点时都提供它。

练习

你刚刚学习了如何为你的 RESTful API 创建一个安全端点。现在,尝试在之前配方中创建的一些端点上实现安全性。

还有更多…

使用 OAuth2,我们可以定义一个作用域参数,该参数用于指定访问令牌在用于访问受保护资源时授予客户端应用的访问级别。作用域可以用来定义客户端应用代表用户可以执行或访问哪些操作或资源。

当客户端从资源所有者(用户)请求授权时,它会在授权请求中包含一个或多个作用域。在 FastAPI 中,这些作用域以dict的形式表示,其中键代表作用域的名称,值是描述。

授权服务器随后使用这些作用域来确定在颁发访问令牌时授予客户端应用的适当访问控制和权限。

本食谱的目的不是深入探讨在 FastAPI 中实现 OAuth2 作用域的细节。然而,您可以在官方文档页面找到实用示例,链接为:fastapi.tiangolo.com/advanced/security/oauth2-scopes/

参见

您可以在以下链接中查看 FastAPI 如何集成 OAuth2:

此外,您还可以在官方文档页面找到更多关于 FastAPI 中依赖注入的信息:

使用 Swagger 和 Redoc 记录 API

当启动服务器时,FastAPI 会自动使用Swagger UIRedoc为您的 API 生成文档。

此文档是从您的路由函数和 Pydantic 模型中派生出来的,对开发团队或 API 消费者来说非常有用。

在本食谱中,我们将看到如何根据特定需求自定义文档。

准备中…

默认情况下,FastAPI 提供了两个文档接口:

  • /docs 端点(例如,http://127.0.0.1:8000/docs

  • /redoc 端点(例如,http://127.0.0.1:8000/redoc

这些界面提供动态文档,用户可以查看和测试 API 端点和其详细信息。然而,这两份文档都可以进行修改。

如何实现...

FastAPI 允许自定义 Swagger UI。您可以通过FastAPI类的参数添加元数据、自定义外观和添加额外的文档。

您可以通过在main.py模块中的app对象提供额外的元数据,如titledescriptionversion来增强您的 API 文档。

app = FastAPI(
    title="Task Manager API",
    description="This is a task management API",
    version="0.1.0",
)

这些元数据将出现在 Swagger UI 和 Redoc 文档中。

如果您需要在某些条件下将 Swagger UI 暴露给第三方用户,您可以进一步自定义它。

让我们尝试隐藏文档中的/token端点。

在这种情况下,您可以使用 FastAPI 提供的utils模块,以以下方式在dict对象中检索 Swagger UI 的 OpenAPI 模式:

from fastapi.openapi.utils import get_openapi
def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema
    openapi_schema = get_openapi(
        title="Customized Title",
        version="2.0.0",
        description="This is a custom OpenAPI schema",
        routes=app.routes,
    )
    del openapi_schema["paths"]["/token"]
    app.openapi_schema = openapi_schema
    return app.openapi_schema
app = FastAPI(
    title="Task Manager API",
    description="This is a task management API",
    version="0.1.0",
)
app.openapi = custom_openapi

这就是您需要自定义 API 文档的所有内容。

如果您使用uvicorn main:app命令启动服务器并访问两个文档页面之一,/token端点将不再出现。

您现在可以自定义 API 文档,以提升您向客户展示的方式。

参见

您可以在官方文档页面上了解更多关于 FastAPI 生成元数据、特性和 OpenAPI 集成的信息:

第四章:身份验证和授权

在我们的FastAPI 食谱集这一章中,我们将深入研究身份验证和授权的关键领域,为您构建安全网络应用程序免受未经授权访问的基础。

在我们浏览本章内容的过程中,您将开始一段实际旅程,在 FastAPI 应用程序中实施一个全面的安全模型。从用户注册和身份验证的基础到将复杂的OAuth2协议与JSON Web TokenJWT)集成以提高安全性,本章涵盖了所有内容。

我们将创建软件即服务SaaS)的基本组件,帮助您学习如何实际建立用户注册系统、验证用户和高效处理会话。我们还将向您展示如何应用基于角色的访问控制RBAC)来调整用户权限,并使用 API 密钥身份验证保护 API 端点。通过使用 GitHub 等外部登录服务进行第三方身份验证,将展示如何利用现有平台进行用户身份验证,简化用户的登录过程。

此外,您将通过实施多因素身份验证MFA)添加一个额外的安全层,确保您的应用程序能够抵御各种攻击向量。

在本章中,我们将介绍以下食谱:

  • 设置用户注册

  • 使用 OAuth2 和 JWT 进行身份验证

  • 设置 RBAC

  • 使用第三方身份验证

  • 实施 MFA

  • 处理 API 密钥身份验证

  • 处理会话 cookie 和注销功能

技术要求

要深入了解本章并跟随身份验证和授权的食谱,请确保您的设置包括以下基本要素:

  • Python:在您的环境中安装一个高于 3.9 版本的 Python。

  • FastAPI:应与所有必需的依赖项一起安装。如果您在前几章中没有这样做,您可以从终端简单地完成它:

    $ pip install fastapi[all]
    

本章中使用的代码托管在 GitHub 上,地址为github.com/PacktPublishing/FastAPI-Cookbook/tree/main/Chapter04

在项目根目录内为项目设置虚拟环境也是推荐的,这样可以高效地管理依赖项并保持项目隔离。在您的虚拟环境中,您可以使用 GitHub 项目文件夹中提供的requirements.txt文件一次性安装所有依赖项:

pip install –r requirements.txt

由于编写时交互式 Swagger 文档有限,因此掌握Postman或其他测试 API 的基本技能对测试我们的 API 有益。

现在我们有了这个准备,我们可以开始准备我们的食谱。

设置用户注册

用户注册是保护你的 FastAPI 应用程序的第一步。它涉及收集用户详细信息并安全地存储它们。以下是你可以设置基本用户注册系统的方法。配方将向你展示如何设置 FastAPI 应用的注册系统。

准备工作

我们将首先在 SQL 数据库中存储用户。让我们创建一个名为 saas_app 的项目根文件夹,其中包含代码库。

为了存储用户密码,我们将使用一个外部包来使用 bcrypt 算法散列纯文本。散列函数将文本字符串转换为一个独特且不可逆的输出,允许安全地存储敏感数据,如密码。更多详情请参阅 en.wikipedia.org/wiki/Hash_function

如果你还没有在 saas_app 项目文件夹下安装来自 GitHub 仓库的 requirements.txt 中的包,你可以通过运行以下命令安装 passlib 包,其中包含 bcrypt

$ pip install passlib[bcrypt]

你还需要安装一个高于 2.0.0 版本的 sqlalchemy,以便跟随 GitHub 仓库中的代码:

$ pip install sqlalchemy>=2.0.0

我们的环境现在已准备好在我们的 SaaS 中实现用户注册。

如何操作…

在开始实施之前,我们需要设置数据库以存储我们的用户。

我们需要设置一个 sqlalchemy,以便应用程序存储用户凭据。

你需要做以下事情:

  • 设置一个 User 类来映射 SQL 数据库中的用户表。该表应包含 idusernameemailhashed_password 字段。

  • 建立应用程序与数据库之间的连接。

首先,让我们创建一个名为 saas_app 的项目根文件夹。然后你可以参考 第二章 中的 设置 SQL 数据库 配方,或者从 GitHub 仓库中复制 database.pydb_connection.py 模块到你的根文件夹下。

在设置好数据库会话后,让我们定义一个添加用户的函数。

让我们将其变成一个名为 operations.py 的专用模块,在其中我们将定义所有由 API 端点使用的支持函数。

该函数将使用来自 bcrypt 包的密码上下文对象来散列纯文本密码。我们可以如下定义它:

from passlib.context import CryptContext
pwd_context = CryptContext(
    schemes=["bcrypt"], deprecated="auto"
)

我们可以定义一个名为 add_user 的函数,根据大多数数据合规规定,将带有散列密码的新用户插入数据库中:

from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from models import User
def add_user(
    session: Session,
    username: str,
    password: str,
    email: str,
) -> User | None:
    hashed_password = pwd_context.hash(password)
    db_user = User(
        username=username,
        email=email,
        hashed_password=hashed_password,
    )
    session.add(db_user)
    try:
        session.commit()
        session.refresh(db_user)
    except IntegrityError:
        session.rollback()
        return
    return db_user

IntegrityError 将考虑尝试添加已存在的用户名或电子邮件的尝试。

现在,我们必须定义我们的端点,但首先,我们需要设置我们的服务器并初始化数据库连接。我们可以在 main.py 模块中这样做,如下所示:

from contextlib import (
    asynccontextmanager,
)
from fastapi import  FastAPI
from db_connection import get_engine
@asynccontextmanager
async def lifespan(app: FastAPI):
    Base.metadata.create_all(bind=get_engine())
    yield
app = FastAPI(
    title="Saas application", lifespan=lifespan
)

我们使用 FastAPI 对象的 lifespan 参数来指示服务器在启动时同步我们的数据库类 User 与数据库。

此外,我们还可以创建一个单独的模块,responses.py,以保存用于不同端点的响应类。请随意创建自己的或复制 GitHub 仓库中提供的那个。

我们现在可以编写合适的端点来在同一个 main.py 模块中注册用户:

from typing import Annotated
from sqlalchemy.orm import Session
from fastapi import Depends, HTTPException, status
from models import Base
from db_connection import get_session
from operations import add_user
@app.post(
    "/register/user",
    status_code=status.HTTP_201_CREATED,
    response_model=ResponseCreateUser,
    responses={
        status.HTTP_409_CONFLICT: {
            "description": "The user already exists"
        }
    },
)
def register(
    user: UserCreateBody,
    session: Session = Depends(get_session),
) -> dict[str, UserCreateResponse]:
    user = add_user(
        session=session, **user.model_dump()
    )
    if not user:
        raise HTTPException(
            status.HTTP_409_CONFLICT,
            "username or email already exists",
        )
    user_response = UserCreateResponse(
        username=user.username, email=user.email
    )
    return {
        "message": "user created",
        "user": user_response,
    }

我们刚刚实现了一个基本的机制来在我们的 SaaS 数据库中注册和存储用户。

它是如何工作的...

该端点将接受一个包含用户名、电子邮件和密码的 JSON 主体。

如果用户名或电子邮件已存在,将返回 409 响应,并且不允许创建用户。

要测试这个,在项目根目录下,运行以下命令启动服务器:

$ uvicorn main:app

然后,使用浏览器连接到 localhost:8000/docs 并检查我们在 Swagger 文档中刚刚创建的端点。请随意尝试。

练习

add_user 函数和 /register/user 端点创建适当的测试,例如以下内容:

def test_add_user_into_the_database(session):

user = add_user(…

# fill in the test

def test_endpoint_add_basic_user(client):

response = client.post(

"/``register/user",

json=

# continue the test

你可以以对你最有利的方式安排测试。

你可以在书的 GitHub 仓库的 Chapter04/saas_app 文件夹中找到一个可能的测试方法。

参见

bcrypt 库允许你为你的哈希函数添加多层安全性,例如盐和额外的密钥。请随意查看 GitHub 上的源代码:

此外,你还可以在以下位置找到一些有趣的示例,说明如何使用它:

使用 OAuth2 和 JWT 进行认证

在这个配方中,我们将集成 OAuth2 和 JWT 以在应用程序中进行安全的用户认证。这种方法通过利用令牌而不是凭据来提高安全性,符合现代认证标准。

准备工作

由于我们将使用特定的库来管理 JWT,请确保你已经安装了必要的依赖项。如果你还没有从 requirements.txt 安装包,请运行以下命令:

$ pip install python-jose[cryptography]

此外,我们还将使用之前配方中使用的用户表,设置用户注册。确保在开始配方之前已经设置好。

如何做到这一点...

我们可以通过以下步骤设置 JWT 令牌集成。

  1. 在一个名为 security.py 的新模块中,让我们定义用户的认证函数:

    from sqlalchemy.orm import Session
    from models import User
    from email_validator import (
        validate_email,
        EmailNotValidError,
    )
    from operations import pwd_context
    def authenticate_user(
        session: Session,
        username_or_email: str,
        password: str,
    ) -> User | None:
        try:
            validate_email(username_or_email)
            query_filter = User.email
        except EmailNotValidError:
            query_filter = User.username
        user = (
            session.query(User)
            .filter(query_filter == username_or_email)
            .first()
        )
        if not user or not pwd_context.verify(
            password, user.hashed_password
        ):
            return
        return user
    

    该函数可以根据用户名或电子邮件验证输入。

  2. 让我们在同一个模块(create_access_tokendecode_access_token)中定义创建和解码访问令牌的函数。

    要创建访问令牌,我们需要指定一个密钥、用于生成它的算法以及过期时间,如下所示:

    SECRET_KEY = "a_very_secret_key"
    ALGORITHM = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES = 30
    

    然后,create_access_token_function如下所示:

    from jose import jwt
    def create_access_token(data: dict) -> str:
        to_encode = data.copy()
        expire = datetime.utcnow() + timedelta(
            minutes=ACCESS_TOKEN_EXPIRE_MINUTES
        )
        to_encode.update({"exp": expire})
        encoded_jwt = jwt.encode(
            to_encode, SECRET_KEY, algorithm=ALGORITHM
        )
        return encoded_jwt
    

    要解码访问令牌,我们可以使用一个支持函数get_user,该函数通过用户名返回User对象。您可以在operations.py模块中自行实现,或者从 GitHub 仓库中获取。

    解码令牌的函数如下所示:

    from jose import JWTError
    def decode_access_token(
        token: str, session: Session
    ) -> User | None:
        try:
            payload = jwt.decode(
                token, SECRET_KEY, algorithms=[ALGORITHM]
            )
            username: str = payload.get("sub")
        except JWTError:
            return
        if not username:
            return
        user = get_user(session, username)
        return user
    
  3. 现在,我们可以继续在同一模块security.py中使用APIRouter类创建检索令牌的端点:

    from fastapi import (
        APIRouter,
        Depends,
        HTTPException,
        status,
    )
    from fastapi.security import (
        OAuth2PasswordRequestForm,
    )
    router = APIRouter()
    class Token(BaseModel):
        access_token: str
        token_type: str
    @router.post(
        "/token",
        response_model=Token,
        responses=..., # document the responses
    )
    def get_user_access_token(
        form_data: OAuth2PasswordRequestForm = Depends(),
        session: Session = Depends(get_session),
    ):
        user = authenticate_user(
            session,
            form_data.username,
            form_data.password
        )
        if not user:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Incorrect username or password",
            )
        access_token = create_access_token(
            data={"sub": user.username}
        )
        return {
            "access_token": access_token,
            "token_type": "bearer",
        }
    
  4. 然后,我们现在可以为POST /token端点创建一个OAuth2PasswordBearer对象以获取访问令牌:

    from fastapi.security import (
        OAuth2PasswordBearer,
    )
    oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
    
  5. 最后,我们可以创建一个返回基于令牌的凭据的/users/me端点:

    @router.get(
        "/users/me",
        responses=..., # document responses
    )
    def read_user_me(
        token: str = Depends(oauth2_scheme),
        session: Session = Depends(get_session),
    ):
        user = decode_access_token(token, session)
        if not user:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="User not authorized",
            )
        return {
            "description": f"{user.username} authorized",
        }
    
  6. 现在,让我们在main.py中将这些端点导入到 FastAPI 服务器中。在定义 FastAPI 对象后,让我们添加路由器,如下所示:

    import security
    # rest of the code
    app.include_router(security.router)
    

我们刚刚为我们的 SaaS 定义了认证机制。

它是如何工作的…

现在,从项目根目录的终端运行以下代码来启动服务器:

$ uvicorn main:app

在浏览器中转到 Swagger 文档地址(localhost:8000/docs),您将看到新的端点POST /tokenGET /users/me

您需要令牌来调用第二个端点,您可以通过点击锁形图标并填写凭据表单来自动在浏览器中存储它。

通过使用 JWT 和 OAuth2,您已经使您的 SaaS 应用程序更加安全,这有助于您保护敏感端点,并确保只有登录用户才能使用它们。这种安排为您提供了可靠且安全的方式来验证用户,这对于现代 Web 应用程序来说效果很好。

相关内容

您可以通过阅读这篇文章更好地理解 OAuth2 框架:

您还可以查看以下 JWT 协议定义:

设置 RBAC

基于组织内个体用户的角色来调节资源访问的 RBAC 是一种方法。在本食谱中,我们将实现 RBAC 在 FastAPI 应用程序中,以有效地管理用户权限。

准备工作

由于我们将扩展数据库以容纳角色定义,请确保在深入此之前已经完成了设置用户注册食谱。

要设置访问控制,我们首先需要定义一系列我们可以分配的角色。让我们按照以下步骤来做。

  1. module.py模块中,我们可以定义一个新的类Role,并将其作为User模型的新字段添加,该字段将存储在用户表中:

    from enum import Enum
    class Role(str, Enum):
        basic = "basic"
        premium = "premium"
    class User(Base):
        __tablename__ = "users"
    # existing fields
        role: Mapped[Role] = mapped_column(
            default=Role.basic
        )
    
  2. 然后,在operations.py模块中,我们将修改operations.py中的add_user函数,以接受一个参数来定义用户角色;默认值将是基本角色:

    from models import Role
    def add_user(
        session: Session,
        username: str,
        password: str,
        email: str,
        role: Role = Role.basic,
    ) -> User | None:
        hashed_password = pwd_context.hash(password)
        db_user = User(
            username=username,
            email=email,
            hashed_password=hashed_password,
            role=role,
        )
        # rest of the function
    
  3. 让我们创建一个新的模块premium_access.py,并通过一个新的路由器定义端点来注册高级用户,这将非常类似于注册基本用户的端点:

    @router.post(
        "/register/premium-user",
        status_code=status.HTTP_201_CREATED,
        response_model=ResponseCreateUser,
        responses=..., # document responses
    )
    def register_premium_user(
        user: UserCreateBody,
        session: Session = Depends(get_session),
    ):
        user = add_user(
            session=session,
             *user.model_dump(),
            role=Role.premium,
        )
        if not user:
            raise HTTPException(
                status.HTTP_409_CONFLICT,
                "username or email already exists",
            )
        user_response = UserCreate(
            username=user.username,
            email=user.email,
        )
        return {
            "message": "user created",
            "user": user_response,
        }
    similar to the ones used in other modules.
    
  4. 让我们在main.py模块中的app类中添加路由器:

    import security
    import premium_access
    # rest of the code
    app.include_router(security.router)
    app.include_router(premium_access.router)
    

现在我们已经拥有了在 SaaS 应用程序中实现 RBAC 的所有元素。

如何做到这一点...

让我们创建两个端点,一个对所有用户可访问,另一个仅对高级用户保留。让我们通过以下步骤创建端点。

  1. 首先,让我们创建两个辅助函数,get_current_userget_premium_user,分别用于检索每个案例,并作为端点的依赖项使用。

    我们可以定义一个单独的模块,称为rbac.py模块。让我们从导入开始:

    from typing import Annotated
    from fastapi import (
        APIRouter,
        Depends,
        HTTPException,
        Status
    )
    from sqlalchemy.orm import Session
    from db_connection import get_session
    from models import Role
    from security import (
        decode_access_token,
        oauth2_scheme
    )
    

    然后,我们创建了一个将用于端点的请求模型:

    class UserCreateResquestWithRole(BaseModel):
        username: str
        email: EmailStr
        role: Role
    

    然后,我们定义一个支持函数,根据令牌检索用户:

    def get_current_user(
        token: str = Depends(oauth2_scheme),
        session: Session = Depends(get_session),
    ) -> UserCreateRequestWithRole:
        user = decode_access_token(token, session)
        if not user:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="User not authorized",
            )
        return UserCreateRequestWithRole(
            username=user.username,
            email=user.email,
            role=user.role,
        )
    

    然后,我们可以利用这个函数仅筛选出高级用户:

    def get_premium_user(
        current_user: Annotated[
            get_current_user, Depends()
        ]
    ):
        if current_user.role != Role.premium:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="User not authorized",
            )
        return current_user
    
  2. 现在,我们可以使用这些函数在同一个模块中的路由器来创建相应的端点。首先,我们为所有用户定义一个欢迎页面:

    router = APIRouter()
    @router.get(
        "/welcome/all-users",
        responses=..., # document responses
    )
    def all_users_can_access(
        user: Annotated[get_current_user, Depends()]
    ):
        return {
            f"Hello {user.username}, "
            "welcome to your space"
        }
    

    然后,我们定义端点,仅允许高级用户访问:

    @router.get(
        "/welcome/premium-user",
        responses={
            status.HTTP_401_UNAUTHORIZED: {
                "description": "User not authorized"
            }
        },
    )
    def only_premium_users_can_access(
        user: UserCreateResponseWithRole = Depends(
            get_premium_user
        ),
    ):
        return {
            f"Hello {user.username}, "
            "Welcome to your premium space"
        }
    
  3. 让我们在main.py中添加我们创建的路由器:

    import security
    import premium_access
    import rbac
    # rest of the module
    app.include_router(premium_access.router)
    app.include_router(rbac.router)
    # rest of the module
    

我们已经实现了两个基于使用角色的权限端点。

要测试我们的端点,从命令行启动服务器:

$ uvicorn main:app

然后,从您的浏览器中,访问http://localhost:8000/docs上的 Swagger 页面,您可以看到刚刚创建的新端点。

一种实验方法是创建一个基本用户和一个高级用户,并使用相应的端点。在您创建了用户之后,您可以尝试使用GET welcome/all-usersGET /welcome/premium-user端点以及两个角色,并查看响应是否符合角色的预期。

在这个食谱中,您只是创建了基于用户角色的简单端点。您还可以尝试创建更多角色和端点。

还有更多...

应用 RBAC 的另一种方式是为令牌分配一个作用域。这个作用域可以是一个表示某些权限的字符串。因此,角色由令牌生成系统控制。在 FastAPI 中,您可以在令牌内定义作用域。您可以查看专门的文档页面以获取更多信息:fastapi.tiangolo.com/advanced/security/oauth2-scopes/.

使用第三方身份验证

将第三方身份验证集成到您的 FastAPI 应用程序中允许用户使用他们现有的社交媒体账户登录,例如 Google 或 Facebook。本食谱将指导您通过集成 GitHub 第三方登录的过程,通过简化登录过程来增强用户体验。

准备工作

我们将专注于集成 GitHub OAuth2 进行认证。GitHub 提供了全面的文档和一个支持良好的客户端库,简化了集成过程。

您的环境中需要httpx包,所以如果您还没有通过requirements.txt安装它,可以通过运行以下命令来完成:

$ pip install httpx

您还需要设置一个 GitHub 账户。如果您还没有,请创建一个;您可以在官方文档中找到全面的指南,网址为docs.github.com/en/get-started/start-your-journey/creating-an-account-on-github

然后,您需要按照以下步骤在您的账户中创建一个应用程序:

  1. 从您的个人页面,点击屏幕右上角的个人资料图标,然后导航到SaasFastAPIapp

  2. http://localhost:8000/home,这是我们稍后要创建的。

  3. http://localhost:8000/github/auth/token,我们稍后也将定义它。

  4. 点击注册应用程序,应用程序将被创建,并且您将被重定向到一个列出 OAuth2 应用程序必要数据的页面。

  5. 注意客户端 ID,然后点击生成新的****客户密钥

  6. 保存您刚刚创建的客户密钥。有了客户端 ID 和客户密钥,我们可以继续通过 GitHub 实现第三方认证。

现在,我们已经拥有了将 GitHub 第三方登录集成到我们应用程序所需的一切。

如何操作...

让我们从创建一个名为third_party_login.py的新模块开始,用于存储 GitHub 认证的辅助数据和函数。然后我们继续如下。

  1. third_party_login.py模块中,您可以定义用于认证的变量:

    GITHUB_CLIENT_ID = "your_github_client_id"
    GITHUB_CLIENT_SECRET = (
        "your_github_client_secret"
    )
    GITHUB_REDIRECT_URI = (
        "http://localhost:8000/github/auth/token"
    )
    GITHUB_AUTHORIZATION_URL = (
        "https://github.com/login/oauth/authorize"
    )
    

    对于GITHUB_CLIENT_IDGITHUB_CLIENT_SECRET,请使用 OAuth 应用的值。

警告

在生产环境中,请确保不要在代码库中硬编码任何用户名或客户端 ID。

  1. 然后,仍然在third_party_login.py模块中,让我们定义一个辅助函数resolve_github_token,该函数解析 GitHub 令牌并返回有关用户的信息:

    import httpx
    from fastapi import Depends, HTTPException
    from fastapi.security import OAuth2
    from sqlalchemy.orm import Session
    from models import User, get_session
    from operations import get_user
    def resolve_github_token(
        access_token: str = Depends(OAuth2()),
        session: Session = Depends(get_session),
    ) -> User:
        user_response = httpx.get(
            "https://api.github.com/user",
            headers={"Authorization": access_token},
        ).json()
        username = user_response.get("login", " ")
        user = get_user(session, username)
        if not user:
            email = user_response.get("email", " ")
            user = get_user(session, email)
        # Process user_response
        # to log the user in or create a new account
        if not user:
            raise HTTPException(
                status_code=403, detail="Token not valid"
            )
        return user
    
  2. 在一个名为github_login.py的新模块中,我们可以开始创建用于 GitHub 认证的端点。让我们创建一个新的路由器和github_login端点,该端点将返回前端用于将用户重定向到 GitHub 登录页面的 URL:

    import httpx
    from fastapi import APIRouter, HTTPException, status
    from security import Token
    from third_party_login import (
        GITHUB_AUTHORIZATION_URL,
        GITHUB_CLIENT_ID,
        GITHUB_CLIENT_SECRET,
        GITHUB_REDIRECT_URI,
    )
    router = APIRouter()
    @router.get("/auth/url")
    def github_login():
        return {
            "auth_url": GITHUB_AUTHORIZATION_URL
            + f"?client_id={GITHUB_CLIENT_ID}"
        }
    
  3. 现在,让我们在main.py模块中将路由器添加到服务器:

    import github_login
    # rest of the module
    app.include_router(github_login.router)
    # rest of the module
    
  4. 使用相同的命令uvicorn main:app启动服务器,并调用我们刚刚创建的端点GET /auth/url。您将在响应中看到一个类似的链接:github.com/login/oauth/authorize?client_id=your_github_client_id

    此链接由 GitHub 用于认证。重定向由前端管理,不在此书的范围之内。

  5. 验证登录后,您将被重定向到一个 404 页面。这是因为我们还没有在我们的应用程序中创建回调端点。让我们在 github_login.py 模块中这样做:

    @router.get(
        "/github/auth/token",
        response_model=Token,
        responses=..., # add responses documentation
    )
    async def github_callback(code: str):
        token_response = httpx.post(
            "https://github.com/login/oauth/access_token",
            data={
                "client_id": GITHUB_CLIENT_ID,
                "client_secret": GITHUB_CLIENT_SECRET,
                "code": code,
                "redirect_uri": GITHUB_REDIRECT_URI,
            },
            headers={"Accept": "application/json"},
        ).json()
        access_token = token_response.get("access_token")
        if not access_token:
            raise HTTPException(
                status_code=401,
                detail="User not registered",
            )
        token_type = token_response.get(
            "token_type", "bearer"
        )
        return {
            "access_token": access_token,
            "token_type": token_type,
        }
    

    我们刚刚创建的端点返回实际的访问令牌。

  6. 如果您重新启动服务器并尝试使用由 GET /auth/url 端点提供的链接再次验证 GitHub 登录,您将收到包含类似以下内容的响应:

    {
        "access_token": "gho_EnHbcmHdCHD1Bf2QzJ2B6gyt",
        "token_type": "bearer"
    }
    
  7. 最后一部分是创建一个可以通过 GitHub 令牌访问的主页端点,并且可以通过解析令牌来识别用户。我们可以在 main.py 模块中定义它:

    from third_party_login import resolve_github_token
    @router.get(
        "/home",
        responses=…, # add responses documentation
    )
    def homepage(
        user: UserCreateResponse = Depends(
            resolve_github_token
        ),
    ):
        return {
            "message" : f"logged in {user.username} !"
        }
    

您刚刚实现了一个通过 GitHub 第三方认证器进行认证的端点。

它是如何工作的…

首先,通过使用注册端点 POST /register/user,添加一个具有与您要测试的 GitHub 账户相同的用户名或电子邮件的用户。

然后,从 GET /``auth/url 端点提供的 GitHub URL 中检索令牌。

您将使用您的 favorite 工具中的令牌来查询 GET /home 端点,该端点使用 GitHub 令牌来验证权限。

在撰写本文时,我们无法使用交互式文档测试需要外部承载令牌的端点,因此请随意使用您喜欢的工具通过在头部授权中提供承载令牌来查询端点。

您也可以使用 shell 中的 curl 请求来完成,如下所示:

$ curl --location 'http://localhost:8000/home' \
--header 'Authorization: Bearer <github-token>'

如果一切设置正确,您将收到以下响应:

{"message":"logged in <your-username> !"}

您刚刚使用第三方应用程序(如 GitHub)实现了并测试了认证。其他提供者,如 Google 或 Twitter,遵循类似的程序,但有细微差别。您可以自由地实现它们。

参考信息

查看 GitHub 文档,它提供了如何设置 OAuth2 身份验证的指南:

您可以使用第三方授权登录,这些第三方提供类似配置。例如,您可以检查 Google 和 Twitter:

实现多因素认证(MFA)

多因素认证(MFA)通过要求用户提供两个或更多验证因素来访问资源,从而增加了一层安全性。本指南将指导您如何在 FastAPI 应用程序中添加 MFA,通过结合用户知道的东西(他们的密码)和他们拥有的东西(设备)来增强安全性。

准备工作

对于我们的 FastAPI 应用程序,我们将使用基于时间的一次性密码TOTP)作为我们的多因素认证方法。TOTP 提供的是一个六到八位的数字,通常有效期为 30 秒。

首先,确保您已安装必要的软件包:

$ pip install pyotp

Pyotp是一个 Python 库,实现了包括 TOTP 在内的一次性密码算法。

要使用 TOTP 认证,我们需要修改我们数据库中的用户表,以考虑用于验证密钥数的 TOTP 密钥。

让我们通过在models.py模块中的User类中添加totp_secret字段来修改它:

class User(Base):
    # existing fields
    totp_secret: Mapped[str] = mapped_column(
        nullable=True
)

现在我们已经准备好实现多因素认证(MFA)。

如何操作...

让我们先创建两个辅助函数来生成认证器使用的 TOTP 密钥和 TOTP URI,步骤如下。

  1. 我们在名为mfa.py的新模块中定义了这些函数:

    import pyotp
    def generate_totp_secret():
        return pyotp.random_base32()
    def generate_totp_uri(secret, user_email):
        return pyotp.totp.TOTP(secret).provisioning_uri(
            name=user_email, issuer_name="YourAppName"
        )
    

    TOTP URI 也可以是二维码或链接的形式。

    我们将使用generate_totp_secretgenerate_totp_uri函数来创建请求多因素认证的端点。

  2. 端点将返回一个用于认证器的TOTP URI。为了展示机制,我们还将返回密钥数,在现实场景中,这是由认证器生成的数字:

    from fastapi import (
        APIRouter,
        Depends,
        HTTPException,
        status,
    )
    from sqlalchemy.orm import Session
    from db_connection import get_session
    from operations import get_user
    from rbac import get_current_user
    from responses import UserCreateResponse
    router = APIRouter()
    @router.post("/user/enable-mfa")
    def enable_mfa(
        user: UserCreateResponse = Depends(
            get_current_user
        ),
        db_session: Session = Depends(get_session),
    ):
        secret = generate_totp_secret()
        db_user = get_user(db_session, user.username)
        db_user.totp_secret = secret
        db_session.add(db_user)
        db_session.commit()
        totp_uri = generate_totp_uri(secret, user.email)
        # Return the TOTP URI
        # for QR code generation in the frontend
        return {
            "totp_uri": totp_uri,
            "secret_numbers": pyotp.TOTP(secret).now(),
        }
    
  3. 现在,我们可以创建验证密钥数的端点:

    @app.post("/verify-totp")
    def verify_totp(
        code: str,
        username: str,
        session: Session = Depends(get_session),
    ):
        user = get_user(session, username)
        if not user.totp_secret:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="MFA not activated",
            )
        totp = pyotp.TOTP(user.totp_secret)
        if not totp.verify(code):
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid TOTP token",
            )
        # Proceed with granting access
        # or performing the sensitive operation
        return {
            "message": "TOTP token verified successfully"
        }
    

如前所述,您需要在main.py中的FastAPI对象类中包含路由器,以用于所有之前的端点。

为了测试它,像往常一样,从终端启动服务器,运行以下命令:

$ uvicorn main:app

确保您的数据库中有一个用户,转到交互式文档,并通过用户凭据调用/user/enable-mfa端点。您将获得包含 TOTP URI 和临时密钥数的响应,如下所示:

{
  "totp_uri":
  "otpauth://totp/YourAppName:giunio%40example.com?secret=
  NBSUC4CFDUT5IEYX4IR7WKBTDTU7LN25&issuer=YourAppName",
  "secret_numbers": "853567"
}

记下用作/verify-totp端点参数的密钥数,您将获得以下响应:

{
  "message": "TOTP token verified successfully"
}

您已经在 FastAPI 应用程序中实现了多因素认证,并通过确保即使用户的密码被泄露,攻击者仍然需要访问用户的第二个因素(运行 MFA 应用程序的设备)才能获得访问权限来增强了安全性。

参见

在官方文档中查看 Python One-Time Password 库:

处理 API 密钥认证

API 密钥认证是一种简单而有效的方法来控制对应用程序的访问。此方法涉及为需要访问您的 API 的每个用户或服务生成一个唯一的密钥,并要求在请求头中包含该密钥。

API 密钥可以通过多种方式生成,具体取决于所需的保密级别。

FastAPI 没有内置对 API 密钥认证的支持,但您可以使用依赖项或中间件轻松实现它。对于大多数用例,依赖项更灵活,因此我们将采用这种方法。

这个配方将向您展示一种基本但不够安全的方法来实现它。

准备工作

我们将继续开发我们的应用程序。然而,您也可以将此配方应用于从头开始的一个简单应用程序。

如何实现...

让我们创建一个api_key.py模块来存储处理 API 密钥的逻辑。该包将包含 API 列表和验证方法:

from fastapi import HTTPException
from typing import Optional
VALID_API_KEYS = [
    "verysecureapikey",
    "anothersecureapi",
    "onemoresecureapi",
]
async def get_api_key(
    api_key: Optional[str]
):
    if (
        api_key not in VALID_API_KEYS
    ):
        raise HTTPException(
            status_code=403, detail="Invalid API Key"
        )
    return api_key

在示例中,密钥被硬编码到VALID_API_KEYS列表中。然而,在实际的生产场景中,密钥的管理和验证通常由专门的库或服务完成。

让我们创建一个使用 API 密钥的端点:

from fastatpi import APIrouter
router = APIRouter()
@router.get("/secure-data")
async def get_secure_data(
    api_key: str = Depends(get_api_key),
):
    return {"message": "Access to secure data granted"}

现在,将路由器添加到main.py中的FastAPI对象类中,然后端点就准备好测试了。

通过运行以下命令启动服务器:

$ uvicorn main:app

前往交互式文档http://localhost:8000/docs,并通过提供 API 密钥测试您刚刚创建的端点。

如您所见,通过向端点添加一个简单的依赖项,您可以使用 API 密钥保护您应用程序的任何端点。

还有更多...

我们已经开发了一个简单的模块来管理我们应用程序的 API。在生产环境中,这通常由托管平台提供的外部服务处理。然而,如果您打算实现自己的 API 管理系统,请记住 API 密钥认证的最佳实践:

  • 传输安全:始终使用 HTTPS 以防止 API 密钥在传输过程中被拦截

  • 密钥轮换:定期轮换 API 密钥以最小化密钥泄露的风险

  • 限制权限:根据最小权限原则,为每个 API 密钥分配所需的最低权限

  • 监控和撤销:监控 API 密钥的使用情况,并在检测到可疑活动时建立撤销机制

处理会话 cookie 和注销功能

管理用户会话并实现注销功能对于维护 Web 应用程序的安全性和用户体验至关重要。本配方展示了如何在 FastAPI 中处理会话 cookie,从用户登录时创建 cookie 到安全地终止注销会话。

准备工作

会话提供了一种在请求之间持久化用户数据的方式。当用户登录时,应用程序在服务器端创建一个会话,并将会话标识符发送到客户端,通常在一个cookie中。客户端将此标识符随每个请求发送回服务器,允许服务器检索用户的会话数据。

该配方将展示如何管理具有登录和注销功能的会话的 cookie。

如何实现...

FastAPI 中的 cookie 可以通过RequestResponse对象类轻松管理。让我们创建一个登录和注销端点,将会话 cookie 附加到响应中,并从请求中忽略它。

让我们创建一个名为user_session.py的专用模块,并添加/login端点:

from fastapi import APIRouter, Depends, Response
from sqlalchemy.orm import Session
from db_connection import get_session
from operations import get_user
from rbac import get_current_user
from responses import UserCreateResponse
router = APIRouter()
@router.post("/login")
async def login(
    response: Response,
    user: UserCreateResponse = Depends(
        get_current_user
    ),
    session: Session = Depends(get_session),
):
    user = get_user(session, user.username)
    response.set_cookie(
        key="fakesession", value=f"{user.id}"
    )
    return {"message": "User logged in successfully"}

由于我们需要验证fakesession cookie 已被创建,因此使用 Swagger 文档测试登录端点将不可行。

使用uvicorn main:app启动服务器,并使用 Postman 通过提供要登录的用户身份验证令牌来创建对/login端点的Post请求。

通过从响应部分的下拉菜单中选择Cookies,验证响应中是否包含fakesession cookie。

因此,我们可以定义一个不会在响应中返回任何会话 cookie 的注销端点:

@router.post("/logout")
async def logout(
    response: Response,
    user: UserCreateResponse = Depends(
         get_current_user
    ),
):
    response.delete_cookie(
        "fakesession"
    )  # Clear session data
    return {"message": "User logged out successfully"}

这就是你需要管理会话的所有内容。

要测试POST /logout端点,使用uvicorn重新启动服务器。然后,在调用端点时,确保你在 HTTP 请求中提供了fakesession cookie 和用户的身份验证令牌。如果你之前调用了登录端点,它应该被自动存储;否则,你可以在请求的Cookies部分中设置它。

检查响应并确认响应中不再存在fakesession cookie。

还有更多...

除了基本配方之外,还有很多关于 cookie 可以学习。在实际环境中,你可以使用专门的库或外部服务。

无论你的选择是什么,都要将安全放在首位,并遵循以下实践来确保你的会话既安全又高效:

  • SecureHttpOnlySameSite用于防止跨站请求伪造CSRF)和跨站脚本XSS)攻击

  • 会话过期:在会话存储中实现会话过期,并在 cookie 上设置最大年龄

  • 重新生成会话 ID:在登录时重新生成会话 ID,以防止会话固定攻击

  • 监控会话:实现机制来监控活动会话并检测异常

通过将会话管理和注销功能集成到你的 FastAPI 应用程序中,你确保了用户状态在请求之间得到安全且高效的管理。这增强了你应用程序的安全性和用户体验。请记住,遵循会话安全的最佳实践,以有效地保护用户及其数据。

在下一章中,我们将看到如何高效地调试你的 FastAPI 应用程序。

另请参阅

你可以在文档页面了解更多关于在 Fast 中管理 cookie 的信息:

第五章:测试和调试 FastAPI 应用程序

在我们掌握 FastAPI 的旅程中,本章我们将转向软件开发的一个关键方面,确保您应用程序的可靠性、健壮性和质量:测试和调试。随着我们深入本章,您将具备创建有效测试环境、编写和执行全面测试以及高效精确地调试 FastAPI 应用程序所需的知识和工具。

理解如何正确地进行测试和调试,不仅仅是找到错误;它关乎确保您的应用程序能够承受实际使用,在高流量下不会崩溃,并提供无缝的用户体验。通过掌握这些技能,您将能够自信地增强您的应用程序,知道每一行代码都经过仔细审查,每个潜在的瓶颈都已解决。

我们将创建一个具有最小设置的 proto 应用程序来测试食谱。

到本章结束时,您不仅将深入理解适合 FastAPI 的测试框架和调试策略,还将具备将这些技术应用于构建更健壮应用程序的实际经验。这种知识是无价的,因为它直接影响到软件的质量、维护和可扩展性。

在本章中,我们将涵盖以下食谱:

  • 设置测试环境

  • 编写和运行单元测试

  • 测试 API 端点

  • 处理日志消息

  • 调试技术

  • 高流量应用程序的性能测试

技术要求

为了深入本章内容并跟随食谱进行操作,请确保您的设置包括以下基本要素:

  • Python:请确保您的计算机上已安装 Python 3.7 或更高版本。

  • 在您的工作环境中安装fastapi包。

  • pytest 框架,这是一个广泛用于测试 Python 代码的测试框架。

本章中使用的代码托管在 GitHub 上,地址为:github.com/PacktPublishing/FastAPI-Cookbook/tree/main/Chapter05

建议在项目根目录内为项目设置一个虚拟环境,以高效管理依赖项并保持项目隔离。在您的虚拟环境中,您可以使用项目文件夹中 GitHub 仓库提供的requirements.txt一次性安装所有依赖项:

$ pip install –r requirements.txt

虽然不是必需的,但具备基本的 HTTP 协议知识可能会有所帮助。

设置测试环境

本食谱将向您展示如何设置一个针对 FastAPI 应用程序高效且有效的测试环境。到食谱结束时,您将拥有编写、运行和管理测试的坚实基础。

准备工作

确保您有一个正在运行的应用程序。如果没有,您可以从创建一个名为 proto_app 的项目文件夹开始。

如果你还没有使用 GitHub 仓库上提供的 requirements.txt 文件安装包,那么请在你的环境中使用以下命令安装测试库 pytesthttpx

$ pip install pytest pytest-asyncio httpx

在项目根目录中创建一个新的文件夹 proto_app,其中包含一个 main.py 模块,该模块包含 app 对象实例:

from fastapi import FastAPI
app = FastAPI()
@app.get("/home")
async def read_main():
    return {"message": "Hello World"}

使用最小化应用程序设置,我们可以通过构建项目来容纳测试。

如何操作…

首先,让我们开始构建我们的项目文件夹树以容纳测试。

  1. 在根目录下,让我们创建一个 pytest.ini 文件和一个包含测试模块 test_main.pytests 文件夹。项目结构应该如下所示:

    protoapp/
    |─ protoapp/
    │  |─ main.py
    |─ tests/
    │  |─ test_main.py
    pytest.ini contains instructions for pytest. You can write in it:
    
    

    [pytest]

    pythonpath = . protoapp

    
    This will add the project root and the folder `protoapp`, containing the code, to the `PYTHONPATH` when running `pytest`.
    
  2. 现在,在 test_main.py 模块中,让我们为之前创建的 /home 端点编写一个测试:

    import pytest
    from httpx import ASGITransport, AsyncClient
    from protoapp.main import app
    @pytest.mark.asyncio
    async def test_read_main():
        client = AsyncClient(
            transport=ASGITransport(app=app),
            base_url="http://test",
        )
        response = await client.get("/home")
        assert response.status_code == 200
        assert response.json() == {
            "message": "Hello World"
        }
    $ pytest –-collect-only
    

    你应该得到如下输出:

    configfile: pytest.ini
    plugins: anyio-4.2.0, asyncio-0.23.5, cov-4.1.0
    asyncio: mode=Mode.STRICT
    collected 1 item
    <Dir protoapp>
      <Dir tests>
    <Module test_main.py>
    pytest.ini
    
  3. 使用的 pytest 插件

  4. 目录 tests,模块 test_main.py 和测试 test_read_main,它是一个协程

  5. 现在,在项目根目录的命令行终端中,运行 pytest 命令:

    $ pytest
    

你已经设置了测试我们的原型应用程序的环境。

参见

该食谱展示了如何在 FastAPI 项目中配置 pytest 并使用一些良好实践。请随意深入了解 Pytest 的官方文档,链接如下:

编写和运行单元测试

一旦我们设置了测试环境,我们就可以专注于编写和执行 FastAPI 应用程序的测试过程。单元测试对于验证应用程序各个部分在隔离状态下的行为至关重要,确保它们按预期执行。在本食谱中,你将学习如何测试应用程序的端点。

准备工作

我们将使用 pytest 来测试 FastAPI 客户端在单元测试中的表现。由于食谱将利用大多数 Python 标准代码中使用的公共测试 固定装置,在深入食谱之前,请确保熟悉测试固定装置。如果不是这样,你始终可以参考链接中的专用文档页面:docs.pytest.org/en/7.1.x/reference/fixtures.xhtml

如何操作…

我们将首先为相同的 GET /home 端点创建一个单元测试,但与之前的食谱不同。我们将使用 FastAPI 提供的 TestClient 类。

让我们为它创建一个测试夹具。由于它可能被多个测试使用,让我们在tests文件夹下创建一个新的conftest.py模块。conftest.pypytest用来存储在测试模块间共享的公共元素的默认文件。

conftest.py中,让我们编写:

import pytest
from fastapi.testclient import TestClient
from protoapp.main import app
@pytest.fixture(scope="function")
def test_client(db_session_test):
    client = TestClient(app)
    yield client

我们现在可以利用test_client测试夹具为我们的端点创建一个适当的单元测试。

我们将在test_main.py模块中编写我们的测试:

def test_read_main_client(test_client):
    response = test_client.get("/home")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello World"}

就这样。与之前的测试相比,这个测试更紧凑,编写起来更快,归功于 FastAPI 包提供的TestClient类。

现在运行pytest

$ pytest

你将在终端上看到一条消息,显示已成功收集并运行了两个测试。

参见

你可以在官方文档中了解更多关于 FastAPI 测试客户端的信息:

测试 API 端点

集成测试验证你的应用程序的不同部分是否按预期协同工作。它们对于确保你的系统组件正确交互至关重要,尤其是在处理外部服务、数据库或其他 API 时。

在这个配方中,我们将测试两个与 SQL 数据库交互的端点。一个将项目添加到数据库,另一个将根据 ID 读取项目。

准备工作

要应用这个配方,你需要你的测试环境已经为pytest设置好了。如果不是这种情况,请检查同一章节的配方设置 测试环境

此外,这个配方将向你展示如何使用现有端点进行集成测试。你可以用它来测试你的应用程序,或者你可以按照以下方式为我们的protoapp构建端点。

如果你正在使用这个配方来测试你的端点,你可以直接跳到如何进行…部分,并将规则应用到你的端点上。

否则,如果你还没有从requirements.txt中安装包,请在你的环境中安装sqlalchemy包:

$ pip install "sqlalchemy>=2.0.0"

现在让我们通过以下步骤设置数据库连接。

  1. protoapp文件夹下,与main.py模块处于同一级别,让我们创建一个包含数据库设置的database.py模块。让我们先创建Base类:

    from sqlalchemy.orm import DeclarativeBase,
    class Base(DeclarativeBase):
        pass
    

    我们将使用Base类来定义Item映射类。

  2. 然后,数据库Item映射类将如下所示:

    from sqlalchemy.orm import (
        Mapped,
        mapped_column,
    )
    class Item(Base):
        __tablename__ = "items"
        id: Mapped[int] = mapped_column(
            primary_key=True, index=True
        )
        name: Mapped[str] = mapped_column(index=True)
        color: Mapped[str]
    
  3. 然后,我们定义将处理会话的数据库引擎:

    DATABASE_URL = "sqlite:///./production.db"
    engine = create_engine(DATABASE_URL)
    

    引擎对象将用于处理会话。

  4. 然后,让我们将引擎绑定到Base映射类:

    Base.metadata.create_all(bind=engine)
    

    现在,引擎可以将数据库表映射到我们的 Python 类。

  5. 最后,在database.py模块中,让我们创建一个SessionLocal类,它将生成会话,如下所示:

    SessionLocal = sessionmaker(
        autocommit=False, autoflush=False, bind=engine
    )
    

    SessionLocal是一个类,它将初始化数据库会话对象。

  6. 最后,在创建端点之前,让我们创建一个数据库会话。

    由于应用程序相对较小,我们可以在同一个main.py中完成它:

    from protoapp.database import SessionLocal
    def get_db_session():
        db = SessionLocal()
        try:
            yield db
        finally:
            db.close()
    

    我们将使用会话与数据库进行交互。

现在我们已经设置了数据库连接,在main.py模块中,我们可以创建一个端点来添加项目到数据库,以及一个端点来读取它。让我们这样做。

  1. 让我们首先为端点创建请求体::

    from pydantic import BaseModel
    class ItemSchema(BaseModel):
        name: str
        color: str
    
  2. 用于添加项目的端点将是:

    from fastapi import (
        Depends,
        Request,
        HTTPException,
        status
    )
    from sqlalchemy.orm import Session
    @app.post(
    "/item",
    response_model=int,
    status_code=status.HTTP_201_CREATED
    )
    def add_item(
        item: ItemSchema,
        db_session: Session = Depends(get_db_session),
    ):
        db_item = Item(name=item.name, color=item.color)
        db_session.add(db_item)
        db_session.commit()
        db_session.refresh(db_item)
        return db_item.id
    

    当项目存储在数据库中时,端点将返回受影响的项 ID。

  3. 现在我们有了添加项目的端点,我们可以通过创建基于 ID 检索项目的端点来继续:

    @app.get("/item/{item_id}", response_model=ItemSchema)
    def get_item(
        item_id: int,
        db_session: Session = Depends(get_db_session),
    ):
        item_db = (
            db_session.query(Item)
            .filter(Item.id == item_id)
            .first()
        )
        if item_db is None:
            raise HTTPException(
                status_code=404, detail="Item not found"
            )
        return item_db
    

    如果 ID 不对应于数据库中的任何项目,端点将返回 404 状态码。

我们刚刚创建了允许我们创建集成测试的端点。

如何做到这一点…

一旦我们有了端点,在tests文件夹中,我们应该适配我们的test_client固定装置以使用与生产中不同的会话。

我们将把整个过程分解为两个主要动作:

  • 将测试客户端适配以适应测试数据库会话

  • 创建测试以模拟端点之间的交互

让我们按照以下步骤进行。

  1. 首先,在之前在配方“编写和运行单元测试”中创建的conftest.py文件中,让我们定义一个新的引擎,该引擎将使用内存中的 SQLite 数据库并将其绑定到Base类映射:

    from sqlalchemy.pool import StaticPool
    from sqlalchemy import create_engine
    engine = create_engine(
        "sqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(bind=engine)  # Bind the engine
    
  2. 让我们为测试会话创建一个专门的会话创建器,如下所示:

    from sqlalchemy.orm import sessionmaker
    TestingSessionLocal = sessionmaker(
        autocommit=False, autoflush=False, bind=engine
    )
    
  3. 类似于main.py模块中的get_db_session函数,我们可以在conftest.py模块中创建一个固定装置来检索测试会话:

    @pytest.fixture
    def test_db_session():
        db = TestingSessionLocal()
        try:
            yield db
        finally:
            db.close()
    
  4. 然后,我们应该修改test_client以使用这个会话而不是生产会话。我们可以通过覆盖返回会话的依赖项来实现,FastAPI 允许你通过调用测试客户端的方法dependency_overrides来轻松实现,如下所示:

    from protoapp.main import app, get_db_session
    @pytest.fixture(scope="function")
    def test_client(test_db_session):
        client = TestClient(app)
        app.dependency_overrides[get_db_session] = (
            lambda: test_db_session
    )
        return client
    

    每次测试客户端需要调用会话时,固定装置将用使用内存数据库的测试会话替换它。

  5. 然后,为了验证我们的应用程序与数据库的交互,我们创建了一个测试:

    • 通过POST /item端点将项目创建到数据库中

    • 通过使用测试会话验证项目是否正确创建在测试数据库中

    • 通过GET /item端点检索项目

    你可以将测试放入test_main.py,以下是它的样子:

    def test_client_can_add_read_the_item_from_database(
        test_client, test_db_session
    ):
        response = test_client.get("/item/1")
        assert response.status_code == 404
        response = test_client.post(
            "/item", json={"name": "ball", "color": "red"}
        )
        assert response.status_code == 201
        # Verify the user was added to the database
        item_id = response.json()
        item = (
            test_db_session.query(Item)
            .filter(Item.id == item_id)
            .first()
        )
        assert item is not None
        response = test_client.get(f"item/{item_id}")
        assert response.status_code == 200
        assert response.json() == {
            "name": "ball",
            "color": "red",
        }
    

你刚刚为我们的原型应用创建了一个集成测试,请随意丰富你的应用并相应地创建更多测试。

参见

我们已经为测试设置了一个内存中的 SQLite 数据库。由于每个会话都与线程绑定,因此需要相应地配置引擎以避免刷新数据。

配置策略已在以下文档页面找到:

运行测试技术

通过系统地覆盖所有端点和场景,你确保了你的 API 在各种条件下表现正确,从而为你的应用程序的功能提供信心。彻底测试 API 端点是构建可靠和健壮应用程序的基本要求。

这个配方将解释如何单独或按组运行测试以及如何检查代码的测试覆盖率。

准备工作

要运行这个配方,确保你已经放置了一些测试,或者你已经遵循了本章的所有前一个配方。此外,确保你在pytest.ini中定义了测试的 PYTHONPATH。查看配方设置 测试环境了解如何操作。

如何做到这一点...

我们将首先查看如何通过默认分组(单独或按模块)运行测试,然后我们将介绍一种基于标记自定义测试分组的技术。

如你所知,所有单元测试都可以通过终端使用以下命令运行:

$ pytest

然而,可以根据测试调用语法单独运行测试:

$ pytest <test_module>.py::<test_name>

例如,如果我们想运行测试函数test_read_main_client,运行:

$ pytest tests/test_main.py::test_read_main

有时测试名称变得过于复杂难以记住,或者我们有特定的需求只想运行一组特定的测试。这就是测试标记发挥作用的地方。

让我们假设我们只想运行集成测试。在我们的应用程序中,唯一的集成测试由函数tests_client_can_add_read_the_item_from_database表示。

我们可以通过添加特定的装饰器到函数中来应用标记:

@pytest.mark.integration
def test_client_can_add_read_the_item_from_database(
    test_client, test_db_session
):
    # test content

然后,在pytest.ini配置文件中,在专用部分添加integration标记以注册标记:

[pytest]
pythonpath = protoapp .
markers =
    integration: marks tests as integration

现在,你可以通过以下方式运行目标测试:

$ pytest –m integration -vv

在输出信息中,你会看到只有标记的测试被选中并运行。你可以使用标记根据逻辑标准对应用程序的测试进行分组,例如,一个组用于创建、读取、更新和删除CRUD)操作,一个组用于安全操作,等等。

检查测试覆盖率

为了确保你的端点以及代码的文本行都经过了测试,了解测试覆盖率可能很有用。

测试覆盖率是软件测试中用来衡量在特定测试套件运行时程序源代码执行程度的指标。

要与pytest一起使用,如果你没有使用requirements.txt安装包,你需要安装pytest-cov包:

$ pip install pytest-cov

它的工作方式非常直接。你需要将源代码根目录(在我们的例子中是protoapp目录)传递给pytest--cov参数和测试根目录(在我们的例子中是测试),如下所示:

$ pytest –-cov protoapp tests

您将在输出中看到一个表格,列出每个模块的覆盖率百分比:

Name                   Stmts   Miss  Cover
------------------------------------------
protoapp\database.py      16      0   100%
protoapp\main.py          37      4    89%
protoapp\schemas.py        8      8     0%
------------------------------------------
TOTAL                     61     12    80%

此外,还创建了一个名为.coverage的文件。这是一个包含测试覆盖率数据的二进制文件,可以使用其他工具从中生成报告。

例如,如果你运行:

$ coverage html

它将创建一个名为htmlcov的文件夹,其中包含一个index.xhtml页面,包含覆盖率页面,您可以通过用浏览器打开它来可视化它。

参见

您可以在官方文档链接中了解更多有关使用 Pytest 调用单元测试的选项以及如何评估测试覆盖率。

处理日志消息

在应用开发中有效地管理日志不仅有助于及时识别错误,还能提供有关用户交互、系统性能和潜在安全威胁的宝贵见解。它作为审计、合规和优化资源利用的关键工具,最终增强了软件的可靠性和可扩展性。

这个配方将展示如何高效地将日志记录系统集成到我们的 FastAPI 应用程序中,以监控 API 的调用。

准备工作

我们将使用 Python 日志生态系统的一些基本功能。

虽然这个例子很简单,但您可以参考官方文档,了解相关术语,如日志记录器处理程序格式化器日志级别。请点击以下链接:

docs.python.org/3/howto/logging-cookbook.xhtml

要将日志记录集成到 FastAPI 中,请确保您有一个运行中的应用程序或使用本章中一直开发的protoapp

如何操作...

我们希望创建一个日志记录器,将客户端的调用信息打印到终端并记录到文件中。

让我们在protoapp文件夹下的logging.py模块中创建日志记录器,按照以下步骤进行。

  1. 让我们首先定义一个具有INFO级别值的日志记录器:

    import logging
    client_logger = logging.getLogger("client.logger")
    logger.setLevel(logging.INFO)
    

    由于我们希望将消息流式传输到控制台并存储到文件中,我们需要定义两个单独的处理程序。

  2. 现在,让我们定义一个处理程序,将日志消息打印到控制台。我们将使用logging内置包中的StreamHandler对象:

    console_handler = logging.StreamHandler()
    

    这会将消息流式传输到控制台。

  3. 让我们创建一个彩色格式化器并将其添加到我们刚刚创建的处理程序中:

    from uvicorn.logging import ColourizedFormatter
    console_formatter = ColourizedFormatter(
        "%(levelprefix)s CLIENT CALL - %(message)s",
        use_colors=True,
    )
    console_handler.setFormatter(console_formatter)
    

    格式化器将以 FastAPI 使用的默认日志记录器 uvicorn 日志记录器的格式格式化日志消息。

  4. 然后让我们将处理程序添加到日志记录器中:

    client_logger.addHandler(console_handler)
    

    我们刚刚设置了日志记录器,以便将消息打印到控制台。

  5. 让我们重复之前的步骤 1 到 4来创建一个将消息存储到文件并添加到我们的client_logger的处理程序:

    from logging.handlers import TimedRotatingFileHandler
    file_handler = TimedRotatingFileHandler("app.log")
    file_formatter = logging.Formatter(
        "time %(asctime)s, %(levelname)s: %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    )
    file_handler.setFormatter(file_formatter)
    client_logger.addHandler(file_handler)
    

    现在我们已经设置了日志记录器。每条消息都将被输出到控制台并存储在app.log文件中。

  6. 一旦我们构建了我们的client_logger,我们就在代码中使用它来获取客户端调用的信息。

    你可以通过在main.py模块中添加日志记录器和专用中间件来实现这一点:

    from protoapp.logging import client_logger
    # ... module content
    @app.middleware("http")
    async def log_requests(request: Request, call_next):
        client_logger.info(
            f"method: {request.method}, "
            f"call: {request.url.path}, "
            f"ip: {request.client.host}"
        )
        response = await call_next(request)
        return response
    
  7. 现在启动服务器:

    $ uvicorn protoapp.main:app
    

尝试调用我们定义的任何端点,你将在终端上看到我们为请求和响应定义的日志。此外,你将在由应用程序自动创建的新app.log文件中找到来自我们的logger_client的消息。

更多内容

定义合适的日志策略需要单独的食谱,但这超出了本书的范围。然而,当将日志记录器集成到应用程序中时,遵循一些指南是很重要的:

  • 适当使用标准日志级别。一个经典的级别系统由 4 个级别组成:INFOWARNINGERRORCRITICAL。根据应用程序的需要,你可能需要更多或更少的级别。无论如何,将每条消息放置在适当的级别。

  • 保持日志格式一致。在整个应用程序中保持一致的日志格式。这包括一致的日期时间格式、严重级别,以及清楚地描述事件。一致的格式有助于解析日志和自动化日志分析。

  • 包含上下文信息。在你的日志中包含相关的上下文信息(例如,用户 ID,事务 ID),以帮助追踪和调试应用程序工作流程中的问题。

  • 避免敏感信息。永远不要记录敏感信息,如密码、API 密钥或个人可识别信息PII)。如果必要,可以对这些细节进行掩码或哈希处理。

  • 高效日志记录。注意日志记录对性能的影响。过度记录可能会减慢应用程序的速度,并导致日志噪声,使得找到有用的信息变得困难。在信息需求与性能影响之间取得平衡。

当然,这并不是一个详尽的列表。

参考内容

Python 发行版自带一个强大的内置日志记录包,您可以查看官方文档:

此外,在Sentry博客上了解更多关于日志记录最佳实践和指南:

Sentry是一个用于监控 Python 代码的工具。

调试技术

掌握调试应用程序开发对于高效识别和修复问题至关重要。这个食谱深入探讨了调试器的实际应用,利用工具和策略来定位 FastAPI 代码中的问题。

准备工作

要应用这个食谱,你只需要有一个正在运行的应用程序。我们可以继续使用我们的protoapp进行工作。

如何操作...

Python 发行版已经自带了一个默认的调试器,称为pdb。如果你使用的是集成开发环境IDE),它通常包含一个编辑器分布调试器。无论你使用什么来调试你的代码,你必须熟悉断点的概念。

断点是代码中的一个点,它暂停执行并显示代码变量的状态和调用。它可以附加一个条件,如果满足条件,则激活它,否则跳过。

无论你使用的是 Python 发行版调试器pdb还是你的 IDE 提供的调试器,定义一个启动脚本来启动服务器可能很有用。

在项目根目录下创建一个名为run_server.py的文件,包含以下代码:

import uvicorn
from protoapp.main import app
if __name__ == "__main__":
    uvicorn.run(app)

脚本导入uvicorn包和我们的应用app,并在uvicorn服务器上运行应用。这相当于启动命令:

$ uvicorn protoapp.main:app

有一个脚本可以让我们有更大的灵活性来运行服务器,并在需要时将其包含到一个更广泛的 Python 程序中。

要检查是否正确设置,像运行正常的 Python 脚本一样运行脚本:

$ python run_server.py

使用你喜欢的浏览器访问localhost:8000/docs并检查文档是否已正确生成。

使用 PDB 进行调试

PDB 调试器默认包含在任何 Python 发行版中。从 Python 3.7 以上的版本开始,你可以通过在想要暂停的代码行添加函数调用breakpoint()来定义一个断点,然后像平常一样运行代码。

如果你运行代码,当它到达断点行时,执行将自动切换到调试模式,你可以从终端运行调试命令。你可以通过输入 help 来找到你可以运行的命令列表:

(Pdb) help

你可以运行列出变量、显示堆栈跟踪以检查最近的帧或定义带有条件的新断点等命令。

在这里你可以找到所有可用命令的列表:docs.python.org/3/library/pdb.xhtml#debugger-commands

你也可以将pdb作为模块调用。在这种情况下,如果程序异常退出,pdb将自动进入事后调试:

$ python –m pdb run_server.py

这意味着pdb将自动重启程序,同时保留pdb模块的执行状态,包括断点。

当通过调用pytest作为模块进行测试调试时,也可以这样做:

$ python –m pdb -m pytest tests

另一种调试策略是利用uvicorn服务器的重新加载功能。为此,你需要修改run_server.py文件,如下所示:

import uvicorn
if __name__ == "__main__":
    uvicorn.run("protoapp.main:app", reload=True)

然后,不使用pdb模块运行服务器:

$ python run_server.py

这样,你就可以在重新加载服务器功能下轻松地使用断点。

在撰写本文时,unvicorn

使用 VS Code 进行调试

VS Code Python 扩展自带其分布式的调试器,称为 debugpy。运行环境的配置可以在 .vscode/launch.json 文件中管理。调试我们服务器的配置文件示例如下:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python Debugger FastAPI server",
            "type": "debugpy",
            "request": "launch",
            "program": "run_server.py",
            "console": "integratedTerminal",
        },
}

配置指定了要使用的调试器类型(debugpy)、要运行的程序(我们的启动脚本 run_server.py),并且可以在 GUI 选项中找到。

request 字段指定了运行调试器的模式,可以是 launch(用于运行程序),或 attach(用于连接到已运行的实例),这对于调试运行在远程实例上的程序特别有用。

调试远程实例超出了本食谱的范围,但您可以在官方文档中找到详细说明:code.visualstudio.com/docs/python/debugging#_debugging-by-attaching-over-a-network-connection

可以通过利用 Test Explorer 扩展来设置调试配置以运行单元测试。该扩展将在 launch.json 中查找包含 "type": "python""purpose": ["debug-test"](或 "request": "test")的配置。调试测试的配置示例如下:

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug test",
            "type": "python",
            "request": "launch",
            "console": "integratedTerminal",
            "justMyCode": false,
            "stopOnEntry": true,
            "envFile": "${workspaceFolder}/.env.test",
            "purpose": ["debug-test"]
        }
    ]
}

您可以在 VS Code 市场扩展页面找到详细的解释:marketplace.visualstudio.com/items?itemName=LittleFoxTeam.vscode-python-test-adapter

使用 PyCharm 进行调试

PyCharm 通过运行/调试配置管理代码执行,这些配置是一组命名的启动属性集,详细说明了执行参数和环境。这些配置允许使用不同的设置运行脚本,例如使用不同的 Python 解释器、环境变量和输入源。

运行/调试配置有两种类型:

  • 临时:自动为每次运行或调试会话生成。

  • 永久:手动从模板创建或由临时配置转换而来,并保存在您的项目中,直到删除。

PyCharm 默认使用现有的永久配置或为每个会话创建一个临时配置。临时配置限制为五个,最旧的配置将被删除以为新配置腾出空间。此限制可以在设置中调整(设置 | 高级设置 | 运行/调试 | 临时配置限制)。图标区分永久(不透明)和临时(半透明)配置。

每个配置都可以存储在一个单独的 xml 文件中,该文件由 GUI 自动检测。

我们 FastAPI protoapp 的配置示例如下:

<component name="ProjectRunConfigurationManager">
  <configuration default="false" name="run_server"
    type="PythonConfigurationType" factoryName="Python"
    nameIsGenerated="true">
    <module name="protoapp" />
    <option name="INTERPRETER_OPTIONS" value="" />
    <option name="PARENT_ENVS" value="true" />
    <envs>
      <env name="PYTHONUNBUFFERED" value="1" />
    </envs>
    <option name="WORKING_DIRECTORY"
      value="$PROJECT_DIR$" />
    <option name="IS_MODULE_SDK" value="true" />
    <option name="ADD_CONTENT_ROOTS" value="true" />
    <option name="ADD_SOURCE_ROOTS" value="true" />
    <option name="SCRIPT_NAME"
      value="$PROJECT_DIR$/run_server.py" />
    <option name="SHOW_COMMAND_LINE" value="false" />
    <option name="MODULE_MODE" value="false" />
    <option name="REDIRECT_INPUT" value="false" />
    <option name="INPUT_FILE" value="" />
    <method v="2" />
  </configuration>
</component>

您可以在专门的 PyCharm 文档页面找到如何设置的详细指南:www.jetbrains.com/help/pycharm/run-debug-configuration.xhtml

参见

你可以自由地深入研究我们刚刚在链接中解释的每个调试解决方案和概念:

高流量应用程序的性能测试

性能测试对于确保你的应用程序能够处理现实世界的使用场景至关重要,尤其是在高负载下。通过系统地实施和运行性能测试,分析结果,并根据发现进行优化,你可以显著提高应用程序的响应性、稳定性和可扩展性。

该食谱将展示如何使用Locust框架基准测试你的应用程序的基础知识。

准备工作

要运行性能测试,你需要一个运行中的应用程序,我们将使用我们的protoapp和一个测试框架。我们将使用基于 Python 语法的Locust框架,它是一个测试框架。

你可以在官方文档中找到详细的解释:docs.locust.io/en/stable/

在开始之前,请确保你已经通过运行以下命令在你的虚拟环境中安装了它:

$ pip install locust

现在我们已经准备好设置配置文件并运行 locust 实例。

如何做到这一点...

当应用程序正在运行且已安装locust包时,我们将通过指定我们的配置来运行性能测试。

在你的项目根目录中创建一个locustfile.py文件。此文件将定义与测试中的应用程序交互的用户的行为。

locustfile.py的最小示例可以是:

from locust import HttpUser, task
class ProtoappUser(HttpUser):
    host = "http://localhost:8000"
    @task
    def hello_world(self):
        self.client.get("/home")

配置定义了一个客户端类,其中包含服务地址和我们要测试的端点。

使用以下命令启动你的 FastAPI 服务器:

$ uvicorn protoapp.main:app

然后在另一个终端窗口中运行 locust:

$ locust

打开你的浏览器并导航到http://localhost:8089以访问应用程序的 Web 界面。

Web 界面设计直观,使得以下操作变得简单:

  • 设置并发用户:指定在高峰使用期间同时访问服务的最大用户数。

  • 配置爬坡速率:确定每秒添加的新用户数量以模拟增加的流量。

配置好这些参数后,点击locustfile.py中定义的/home端点。

或者,你可以使用命令行模拟流量。以下是方法:

$ locust --headless --users 10 --spawn-rate 1

此命令以无头模式运行 Locust 以模拟:

  • 10 个用户同时访问您的应用程序。

  • 每秒产生 1 个用户。

在部署之前,您可以通过将其包含在 持续集成/持续交付CI/CD)管道中,或者甚至将其纳入更大的测试流程中,来进一步扩展您的测试体验。

深入文档以测试您应用程序流量的各个方面。

您拥有所有调试和全面测试您应用程序的工具。

在下一章中,我们将构建一个与 SQL 数据库交互的综合 RESTful 应用程序。

参见

您可以在官方文档页面上找到更多关于 Locust 的信息:

第六章:将 FastAPI 与 SQL 数据库集成

现在,我们将开始一段旅程,充分利用 SQL 数据库在 FastAPI 应用程序中的全部潜力。本章精心设计,旨在指导你深入了解利用 SQLAlchemy 的细微差别,这是一个强大的 SQL 工具包和 对象关系映射ORM)库,适用于 Python。从设置你的数据库环境到实现复杂的 创建、读取、更新和删除CRUD)操作,以及管理复杂的关系,本章提供了一个全面蓝图,以无缝集成 SQL 数据库与 FastAPI。

通过创建一个基本的票务平台,你将实际参与配置 SQLAlchemy 与 FastAPI,创建反映你的应用程序数据结构的数据库模型,并构建高效、安全的 CRUD 操作。

此外,你还将探索使用 Alembic 管理数据库迁移,确保你的数据库模式与你的应用程序同步发展而无需麻烦。本章不仅涉及数据处理,还深入到优化 SQL 查询以提升性能、在数据库中保护敏感信息以及管理事务和并发,以确保数据完整性和可靠性。

到本章结束时,你将熟练地集成和管理 SQL 数据库在你的 FastAPI 应用程序中,并具备确保你的应用程序不仅高效和可扩展,而且安全的技能。无论你是从头开始构建新应用程序还是将数据库集成到现有项目中,这里涵盖的见解和技术将赋予你利用 SQL 数据库在 FastAPI 项目中全部力量的能力。

在本章中,我们将介绍以下食谱:

  • 设置 SQLAlchemy

  • 实现 CRUD 操作

  • 与迁移一起工作

  • 处理 SQL 数据库中的关系

  • 优化 SQL 查询以提升性能

  • 在 SQL 数据库中保护敏感数据

  • 处理事务和并发

技术要求

为了跟随本章的所有食谱,请确保你的设置中包含以下基本要素:

  • Python:你的环境应安装有高于 3.9 的 Python 版本。

  • FastAPI:它应该安装在你的虚拟环境中,并包含所有需要的依赖项。如果你在前几章中没有这样做,你可以很容易地从你的终端完成它:

    $ pip install fastapi[all]
    

本章的代码可在 GitHub 上的以下链接找到:github.com/PacktPublishing/FastAPI-Cookbook/tree/main/Chapter06

还建议在项目根目录内为项目创建一个虚拟环境,以更好地处理依赖关系并保持项目独立。在你的虚拟环境中,你可以通过使用项目文件夹中 GitHub 仓库的 requirements.txt 文件一次性安装所有依赖项:

$ pip install –r requirements.txt

由于本章的代码将使用来自 asyncio Python 库的 async/await 语法,你应该已经熟悉它。请随意阅读以下链接了解更多关于 asyncioasync/await 语法的信息:

现在我们已经准备好了 一旦一切准备就绪,我们就可以开始准备我们的食谱。

设置 SQLAlchemy

要开始任何数据应用,你需要建立数据库连接。本食谱将帮助你设置和配置 sqlalchemy 包与 SQLite 数据库,以便你可以在应用中使用 SQL 数据库的优势。

准备就绪

项目将会相当大,因此我们将把应用的工作模块放在一个名为 app 的文件夹中,该文件夹位于我们称之为 ticketing_system 的根项目文件夹下。

你需要在你的环境中安装 fastapisqlalchemyaiosqlite 以使用这个食谱。这个食谱旨在与版本高于 2.0.0 的 sqlalchemy 一起工作。你仍然可以使用版本 1;然而,需要一些适配。你可以在以下链接找到迁移指南:SQLAlchemy 2.0 迁移指南

如果你还没有使用存储库中的 requirements.txt 文件安装包,你可以通过运行以下命令来完成:

$ pip install fastapi[all] "sqlalchemy>=2.0.0" aiosqlite

一旦包正确安装,你就可以按照食谱进行操作。

如何操作...

使用 sqlalchemy 设置通用 SQL 数据库连接将经过以下步骤:

  1. 创建映射对象类,以匹配数据库表

  2. 创建抽象层、引擎和会话以与数据库通信

  3. 在服务器启动时初始化数据库连接

创建映射对象类

app 文件夹中,让我们创建一个名为 database.py 的模块,然后创建一个类对象来跟踪票务,如下所示:

from sqlalchemy import Column, Float, ForeignKey, Table
from sqlalchemy.orm import (
    DeclarativeBase,
    Mapped,
    mapped_column,
)
class Base(DeclarativeBase):
    pass
class Ticket(Base):
    __tablename__ = "tickets"
    id: Mapped[int] = mapped_column(primary_key=True)
    price: Mapped[float] = mapped_column(nullable=True)
    show: Mapped[str | None]
    user: Mapped[str | None]

我们刚刚创建了一个 Ticket 类,它将被用来将我们的 SQL 数据库中的 tickets 表进行匹配。

创建抽象层

在 SQLAlchemy 中,引擎 管理数据库连接并执行 SQL 语句,而 会话 允许在事务性上下文中查询、插入、更新和删除数据,确保一致性和原子性。会话绑定到引擎以与数据库通信。

我们将首先创建一个返回引擎的函数。在一个名为 db_connection.py 的新模块中,位于 app 文件夹下,让我们按照以下方式编写函数:

from sqlalchemy.ext.asyncio import (
    create_async_engine,
)
from sqlalchemy.orm import sessionmaker
SQLALCHEMY_DATABASE_URL = (
    "sqlite+aiosqlite:///.database.db"
)
def get_engine():
    return create_async_engine(
        SQLALCHEMY_DATABASE_URL, echo=True
    )

你可能已经注意到,SQLALCHEMY_DATABASE_URL 数据库 URL 使用了 sqliteaiosqlite 模块。

这意味着我们将使用 SQLite 数据库,操作将通过支持 asyncio 库的 aiosqlite 异步库来完成。

然后,我们将使用会话创建器来指定会话将是异步的,如下所示:

from sqlalchemy.ext.asyncio import (
    AsyncSession,
)
AsyncSessionLocal = sessionmaker(
    autocommit=False,
    autoflush=False,
    bind=get_engine(),
    class_=AsyncSession,
)
async def get_db_session():
    async with AsyncSessionLocal() as session:
        yield session

get_db_session函数将被用作与数据库交互的每个端点的依赖项。

初始化数据库连接

一旦我们有了抽象层,我们需要在服务器运行时创建我们的 FastAPI 服务器对象并启动数据库类。我们可以在app文件夹下的main.py模块中这样做:

from contextlib import asynccontextmanager
from fastapi import FastAPI
from app.database import Base
from app.db_connection import (
    AsynSessionLocal,
    get_db_session
)
@asynccontextmanager
async def lifespan(app: FastAPI):
    engine = get_engine()
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
        yield
    await engine.dispose()ispose()
app = FastAPI(lifespan=lifespan)

为了在启动事件中指定服务器操作,我们使用了lifespan参数。

我们已经准备好将我们的应用程序与数据库连接。

它是如何工作的…

Ticket数据库映射类的创建告诉我们应用程序数据库的结构,并且会话将管理事务。然后,引擎不仅会执行操作,还会将映射类与数据库进行比较,如果缺少任何表,它将创建这些表。

为了检查我们的应用程序是否与数据库通信,让我们从项目根目录的命令行启动服务器:

$ uvicorn app.main:app

你应该在命令输出中看到消息日志,表明已创建表 tickets。此外,使用你偏好的数据库阅读器打开.database.db文件,你应该会看到在database.py模块中定义的模式下的表。

参见

你可以在官方文档页面上了解更多关于如何使用 SQLAlchemy 设置数据库以及如何使其与asyncio模块兼容的信息:

在这个例子中,我们通过指定以下内容使用了 SQLite 数据库:

SQLALCHEMY_DATABASE_URL = "sqlite+aiosqlite:///.database.db"

然而,你可以使用 SQLAlchemy 与多个 SQL 数据库交互,例如asyncio支持的驱动程序和数据库地址。

例如,对于 MySQL,连接字符串看起来会是这样:

mysql+aiomysql://user:password@host:port/dbname[?key=value&key=value...]

在这种情况下,你需要在你的环境中安装aiomysql包。

你可以在官方文档页面查看更多信息:

实现 CRUD 操作

使用 RESTful API 的 CRUD 操作可以通过 HTTP 方法(POSTGETPUTDELETE)实现,用于网络服务。这个配方演示了如何使用 SQLAlchemy 和asyncio在 SQL 数据库上异步构建 CRUD 操作以及相应的端点。

准备工作

在开始食谱之前,您需要有一个数据库连接、数据集中的表以及代码库中匹配的类。如果您完成了前面的食谱,应该已经准备好了。

如何做…

我们将首先在app文件夹下创建一个operations.py模块,按照以下步骤包含我们的数据库操作。

  1. 首先,我们可以设置操作以将新票添加到数据库,如下所示:

    from sqlalchemy.ext.asyncio import AsyncSession
    from sqlalchemy.future import select
    from app.database import Ticket
    async def create_ticket(
        db_session: AsyncSession,
        show_name: str,
        user: str = None,
        price: float = None,
    ) -> int:
        ticket = Ticket(
            show=show_name,
            user=user,
            price=price,
        )
        async with db_session.begin():
            db_session.add(ticket)
            await db_session.flush()
            ticket_id = ticket.id
            await db_session.commit()
        return ticket_id
    

    函数在保存时会返回附加到票上的 ID。

  2. 然后,让我们创建一个获取票的功能:

    async def get_ticket(
        db_session: AsyncSession, ticket_id: int
    ) -> Ticket | None:
        query = (
            select(Ticket)
            .where(Ticket.id == ticket_id)
        )
        async with db_session as session:
            tickets = await session.execute(query)
            return tickets.scalars().first()
    

    如果找不到票,函数将返回一个None对象。

  3. 然后,我们构建一个仅更新票价的操作:

    async def update_ticket_price(
        db_session: AsyncSession,
        ticket_id: int,
        new_price: float,
    ) -> bool:
        query = (
            update(Ticket)
            .where(Ticket.id == ticket_id)
            .values(price=new_price)
        )
        async with db_session as session:
            ticket_updated = await session.execute(query)
            await session.commit()
            if ticket_updated.rowcount == 0:
                return False
            return True
    

    如果操作无法删除任何票,函数将返回False

  4. 为了完成 CRUD 操作,我们定义了一个delete_ticket操作:

    async def delete_ticket(
        db_session: AsyncSession, ticket_id
    ) -> bool:
        async with db_session as session:
            tickets_removed = await session.execute(
                delete(
                    Ticket
                ).where(Ticket.id == ticket_id)
            )
            await session.commit()
            if tickets_removed.rowcount == 0:
                return False
            return True
    

    与更新操作类似,如果找不到要删除的票,函数将返回False

  5. 在定义操作后,我们可以在main.py模块中创建相应的端点来公开它们。

    在定义应用服务器后,让我们立即为创建操作做这件事:

    from typing import Annotated
    from sqlalchemy.ext.asyncio import AsyncSession
    from app.db_connection import (
        AsyncSessionLocal,
        get_engine,
        get_session
    )
    from app.operations import create_ticket
    # rest of the code 
    class TicketRequest(BaseModel):
        price: float | None
        show: str | None
        user: str | None = None
    @app.post("/ticket", response_model=dict[str, int])
    async def create_ticket_route(
        ticket: TicketRequest,
        db_session: Annotated[
            AsyncSession,
            Depends(get_db_session)
        ]
    ):
        ticket_id = await create_ticket(
            db_session,
            ticket.show,
            ticket.user,
            ticket.price,
        )
        return {"ticket_id": ticket_id}
    

    剩余的操作也可以以相同的方式公开。

练习

create_ticket操作类似,使用相应的端点公开其他操作(获取、更新和删除)。

它是如何工作的…

用于与数据库交互的函数通过端点公开。这意味着外部用户将通过调用相应的端点来执行操作。

让我们验证端点是否正确工作。

按照惯例,从命令行启动服务器,运行以下命令:

$ uvicorn app.main:app

然后,转到交互式文档链接http://localhost:8000/docs,您将看到您刚刚创建的端点。尝试不同的组合并查看.database.db数据库文件中的结果。

您刚刚使用sqlalchemyasyncio库创建了对 SQL 数据库的 CRUD 操作。

练习

在根项目文件夹中创建一个tests文件夹,并编写操作函数和端点的所有单元测试。您可以参考第五章测试和调试 FastAPI 应用程序,了解如何对 FastAPI 应用程序进行单元测试。

与迁移一起工作

数据库迁移让您可以版本控制数据库模式,并使其在各个环境中保持一致。它们还有助于自动化数据库更改的部署,并跟踪模式演化的历史。

食谱向您展示了如何使用Alembic,这是一个流行的 Python 数据库迁移管理工具。您将学习如何创建、运行和回滚迁移,以及如何将它们与您的票务系统集成。

准备工作

要使用这个配方,你需要在你的环境中安装alembic。如果你没有通过 GitHub 仓库中的requirements.txt文件安装它,可以在命令行中输入以下内容来安装:

$ pip install alembic

你还需要确保你至少有一个与你要创建的数据库中的表相对应的类。如果你没有,请回到设置 SQLAlchemy配方并创建一个。如果你已经在运行应用程序,请删除应用程序创建的.database.db文件。

如何做到这一点…

要配置 Alembic 并管理数据库迁移,请按照以下步骤进行。

  1. 第一步是设置alembic。在项目根目录中,在命令行中运行以下命令:

    alembic.ini file and an alembic folder with some files inside it. The alembic.ini file is a configuration file for alembic.If you copy the project from the GitHub repository make sure to delete the existing `alembic` folder before running the `alembic` `init` command.
    
  2. 找到sqlalchemy.url变量,并将数据库 URL 设置为以下:

    sqlalchemy.url = sqlite:///.database.db
    

    这指定了我们正在使用 SQLite 数据库。如果你使用的是不同的数据库,请相应地调整。

  3. alembic目录包含一个版本文件夹和一个包含创建我们数据库迁移变量的env.py文件。

    打开env.py文件,找到target_metadata变量。将其值设置为我们的应用程序的元数据,如下所示:

    from app.database import Base
    target_metadata = Base.metadata
    

    我们现在可以创建我们的第一个数据库迁移脚本并应用迁移。

  4. 从命令行执行以下命令以创建初始迁移:

    alembic/versions folder.
    
  5. 确保你已经删除了现有的.database.db文件,然后使用以下命令执行我们的第一个迁移:

    .database.db file with the tickets table in it.
    

它是如何工作的…

一旦我们有了我们数据库的第一个版本,让我们看看迁移是如何工作的。

假设我们想在应用程序已经部署在生产环境中时更改database.py模块中的表,以便在更新时不能删除任何记录。

向数据库添加一些票据,然后在代码中,让我们添加一个名为sold的新字段,以指示票据是否已售出:

class Ticket(Base):
    __tablename__ = "tickets"
    id: Mapped[int] = mapped_column(primary_key=True)
    price: Mapped[float] = mapped_column(nullable=True)
    show: Mapped[str | None]
    user: Mapped[str | None]
    sold: Mapped[bool] = mapped_column(default=False)

要创建一个新的迁移,请运行以下命令:

$ alembic revision –-autogenerate -m "Add sold field"

你将在alembic/versions文件夹中找到一个新脚本。

再次运行迁移命令:

$ alembic upgrade head

打开数据库,你会看到tickets表模式已添加了sold字段,并且没有记录被删除。

你刚刚创建了一个迁移策略,它将在运行时无缝更改我们的数据库,而不会丢失任何数据。从现在开始,请记住使用迁移来跟踪数据库模式的变化。

参见

你可以在官方文档链接中了解更多关于如何使用 Alembic 管理数据库迁移的信息:

在 SQL 数据库中处理关系

数据库关系是两个或多个表之间的关联,允许您建模复杂的数据结构并在多个表之间执行查询。在本教程中,您将学习如何为现有的票务系统应用程序实现一对一、多对一和多对多关系。您还将了解如何使用 SQLAlchemy 定义数据库模式关系并查询数据库。

准备工作

为了遵循本教程,您需要已经实现应用程序的核心,其中至少包含一个表。如果您已经做到了这一点,您也将准备好必要的包。我们将继续在我们的票务系统平台应用程序上工作。

如何操作…

我们现在将继续设置关系。我们将为每种类型的 SQL 表关系提供一个示例。

一对一

我们将通过创建一个新的表格来展示一对一关系,该表格包含有关票的详细信息。

一对一关系用于将特定记录的信息分组到单独的逻辑中。

话虽如此,让我们在database.py模块中创建表格。记录将包含有关与票关联的座位、票类型等信息,我们将使用票类型作为可能信息的标签。让我们分两步创建表格。

  1. 首先,我们将向现有的Ticket类添加票详情引用:

    class Ticket(Base):
        __tablename__ = "tickets"
        id: Mapped[int] = mapped_column(primary_key=True)
        price: Mapped[float] = mapped_column(
            nullable=True
        )
        show: Mapped[str | None]
        user: Mapped[str | None]
        sold: Mapped[bool] = mapped_column(default=False)
        details: Mapped["TicketDetails"] = relationship(
            back_populates="ticket"
        )
    
  2. 然后,我们创建表格以映射票的详细信息,如下所示:

    from sqlalchemy import ForeignKey
    class TicketDetails(Base):
        __tablename__ = "ticket_details"
        id: Mapped[int] = mapped_column(primary_key=True)
        ticket_id: Mapped[int] = mapped_column(
            ForeignKey("tickets.id")
    )
        ticket: Mapped["Ticket"] = relationship(
            back_populates="details"
        )
        seat: Mapped[str | None]
        ticket_type: Mapped[str | None]
    

一旦数据库类被设置以容纳新表,我们就可以按照以下步骤更新 CRUD 操作。

  1. 要更新票务详情,让我们在operations.py模块中创建一个专用函数:

    async def update_ticket_details(
        db_session: AsyncSession,
        ticket_id: int,
        updating_ticket_details: dict,
    ) -> bool:
        ticket_query = update(TicketDetails).where(
            TicketDetails.ticket_id == ticket_id
        )
        if updating_ticket_details != {}:
            ticket_query = ticket_query.values(
                 *updating_ticket_details
            )
            result = await db_session.execute(
                    ticket_query
                )
            await db_session.commit()
            if result.rowcount == 0:
                    return False
        return True
    

    如果没有记录被更新,该函数将返回False

  2. 接下来,修改create_ticket函数以考虑票的详细信息,并创建一个端点来公开我们刚刚创建的更新操作,如下所示:

    async def create_ticket(
        db_session: AsyncSession,
        show_name: str,
        user: str = None,
        price: float = None,
    ) -> int:
        ticket = Ticket(
            show=show_name,
            user=user,
            price=price,
            details=TicketDetails(),
        )
        async with db_session.begin():
            db_session.add(ticket)
            await db_session.flush()
            ticket_id = ticket.id
            await db_session.commit()
        return ticket_id
    

    在本例中,每次创建票时,也会创建一个空的票详情记录,以保持数据库的一致性。

这是处理一对一关系的最小配置。我们将继续设置多对一关系。

多对一

票可以与活动相关联,活动可以有多个票。为了展示多对一关系,我们将创建一个events表,该表将与tickets表相关联。让我们按以下步骤进行:

让我们先在tickets表中创建一个列,该列将容纳database.py模块中events表的引用,如下所示:

class Ticket(Base):
    __tablename__ = "tickets"
    # skip existing columns
    event_id: Mapped[int | None] = mapped_column(
        ForeignKey("events.id")
    )
    event: Mapped["Event | None"] = relationship(
        back_populates="tickets"
Event class to map the events table into the database:

class Event(Base):

tablename = "events"

id: Mapped[int] = mapped_column(primary_key=True)

name: Mapped[str]

tickets: Mapped[list["Ticket"]] = relationship(

back_populates="event"

)


 `ForeignKey`, in this case, is defined only in the `Ticket` class since the event associated can be only one.
This is all you need to create a many-to-one relationship.
Exercise
You can add to the application the operations to create an event and specify the number of tickets to create with it. Once you’ve done this, expose the operation with the corresponding endpoint.
Many to many
Let’s imagine that we have a list of sponsors that can sponsor our events. Since we can have multiple sponsors that can sponsor multiple events, this situation is best representative of a many-to-many relationship.
To work with many-to-many relationships, we need to define a class for the concerned tables and another class to track the so-called *association table*.
Let’s start by defining a column to accommodate relationships in the `Event` class:

class Event(Base):

tablename = "events"

现有列

sponsors: Mapped[list["Sponsor"]] = relationship(

secondary="sponsorships",

back_populates="events",

赞助商表格:

class Sponsor(Base):
    __tablename__ = "sponsors"
    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str] = mapped_column(unique=True)
    events: Mapped[list["Event"]] = relationship(
        secondary="sponsorships",
        back_populates="sponsors",
    )

如您所注意到的,该类包含用于容纳events引用的列。

最后,我们可以定义一个关联表,该表将是sponsorships表:

class Sponsorship(Base):
    __tablename__ = "sponsorships"
    event_id: Mapped[int] = mapped_column(
        ForeignKey("events.id"), primary_key=True
    )
    sponsor_id: Mapped[int] = mapped_column(
        ForeignKey("sponsors.id"), primary_key=True
    )
    amount: Mapped[float] = mapped_column(
        nullable=False, default=10
    )

关联表可以包含关于关系本身的信息。例如,在我们的案例中,一条有用的信息是赞助商为活动提供的金额。

这就是您为您的票务系统平台创建多对多关系所需的所有内容。

练习

为了完成您的申请,创建一个包含相关端点的操作函数,以执行以下操作:

  • 向数据库添加赞助商。

  • 添加带有金额的赞助。如果赞助已经存在,则用新金额替换赞助。

参见

您可以在以下官方文档页面深入了解使用 SQLAlchemy 处理关系的操作:

优化 SQL 查询以提升性能

在数据库管理中,优化 SQL 查询是关键,因为它提高了效率、可扩展性、成本效益、用户满意度、数据完整性、合规性和安全性。

此配方展示了如何通过改进 SQL 查询来提高应用程序的运行速度。使用更少资源和时间的查询可以提升用户满意度和应用程序容量。改进 SQL 查询是一个反复的过程,但您可以采用一些有助于您的技巧。

准备工作

确保您有一个正在运行的应用程序,该应用程序使用 SQLAlchemy 进行数据库交互,或者在整个章节中继续开发票务系统应用程序。此外,对 SQL 和数据库模式设计的基本了解可能有益。

如何操作...

改进 SQL 查询是一个涉及多个步骤的过程。与大多数优化过程一样,许多步骤都是特定于用例的,但有一些通用规则可以帮助整体优化 SQL 查询,例如以下内容:

  • 避免使用N+1 查询

  • 适度使用JOIN语句

  • 最小化要获取的数据

我们将应用每个具有显著示例。

避免 N+1 查询

当您的应用程序执行一个查询以获取项目列表,然后遍历这些项目以获取相关数据,从而产生 N 个更多查询时,就会发生 N+1 查询问题。

假设我们想要一个端点来显示所有与相关赞助商相关的事件。第一次尝试可能是获取events表并遍历事件以获取sponsors表。这个解决方案意味着一个查询来获取事件,以及为每个事件获取赞助商的 N 个更多查询,这正是我们想要避免的。

解决方案是在查询中加载所有相关记录以检索相关赞助商。这在技术上称为预加载

在 SQLAlchemy 中,这是通过使用joinedload选项来完成的,以便函数操作看起来像这样:

async def get_events_with_sponsors(
    db_session: AsyncSession
) -> list[Event]:
    query = (
        select(Event)
        .options(
joinedload(Event.sponsors)
        )
    )
    async with db_session as session:
        result = await session.execute(query)
        events = result.scalars().all()
    return events

joinedload 方法将在查询中包含一个 JOIN 操作,因此不再需要执行 N 次查询来获取赞助商。

节省使用连接语句

连接表可以使查询更容易阅读。但请注意,只连接您查询所需的表。

假设我们想要获取一个按金额从高到低排序的赞助商名称列表,以获取某个活动的金额。

由于我们需要获取三个表,我们可以使用多个连接。函数看起来会是这样:

async def get_event_sponsorships_with_amount(
    db_session: AsyncSession, event_id: int
):
    query = (
        select(Sponsor.name, Sponsorship.amount)
        .join(
            Sponsorship,
            Sponsorship.sponsor_id == Sponsor.id,
        )
        .join(
            Event,
            Sponsorship.event_id == Event.id
)
        .order_by(Sponsorship.amount.desc())
    )
    async with db_session as session:
        result = await session.execute(query)
        sponsor_contributions = result.fetchall()
    return sponsor_contributions

双重连接意味着调用我们不会使用的 events 表,因此将查询组织如下会更有效率:

async def get_event_sponsorships_with_amount(
    db_session: AsyncSession, event_id: int
):
    query = (
select(Sponsor.name, Sponsorship.amount)
        .join(
            Sponsorship,
            Sponsorship.sponsor_id == Sponsor.id,
        )
        .where(Sponsorship.event_id == event_id)
        .order_by(Sponsorship.amount.desc())
    )
    async with db_session as session:
        result = await session.execute(query)
        sponsor_contributions = result.fetchall()
    return sponsor_contributions

这将返回我们所需的内容,而无需选择 events 表。

最小化要获取的数据

获取比所需更多的数据可能会减慢您的查询和应用程序。

使用 SQLAlchemy 的 load_only 函数只从数据库中加载特定的列。

想象一下,为了进行市场分析,我们被要求编写一个函数,该函数只获取具有票务 ID、用户和价格的票务列表:

async def get_events_tickets_with_user_price(
    db_session: AsyncSession, event_id: int
) -> list[Ticket]:
    query = (
        select(Ticket)
        .where(Ticket.event_id == event_id)
        .options(
            load_only(
                Ticket.id, Ticket.user, Ticket.price
            )
        )
    )
    async with db_session as session:
        result = await session.execute(query)
        tickets = result.scalars().all()
    return tickets

我们现在尝试从该函数检索票务,如下所示:

tickets = await get_events_tickets_with_user_price(
    session, event_id
)

您会注意到每个元素只包含 iduserprice 字段,如果您尝试访问 show 字段,例如,将会出现错误。在大型应用程序中,这可以减少内存使用并使响应更快。

还有更多...

SQL 查询优化不仅涉及配方中显示的内容。通常,选择特定的 SQL 数据库取决于特定的优化需求。

不同的 SQL 数据库在处理这些因素时可能具有不同的优势和劣势,这取决于它们的架构和功能。例如,一些 SQL 数据库可能支持分区、分片、复制或分布式处理,这可以提高数据的可扩展性和可用性。一些 SQL 数据库可能提供更高级的查询优化技术,如基于成本的优化、查询重写或查询缓存,这可以减少查询的执行时间和资源消耗。一些 SQL 数据库可能实现不同的存储引擎、事务模型或索引类型,这可能会影响数据操作的性能和一致性。

因此,在选择特定应用程序的 SQL 数据库时,考虑数据和查询的特征和需求,以及比较可用 SQL 数据库的功能和限制,非常重要。一种好的方法是使用真实数据集和查询对 SQL 数据库的性能进行基准测试,并测量相关指标,如吞吐量、延迟、准确性和可靠性。通过这样做,可以找到给定场景的最佳 SQL 数据库,并确定数据库设计和查询制定中可能需要改进的潜在领域。

在 SQL 数据库中保护敏感数据

敏感数据,如个人信息、财务记录或机密文件,通常存储在 SQL 数据库中,用于各种应用程序和目的。然而,这也使数据面临未经授权访问、盗窃、泄露或损坏的风险。因此,在 SQL 数据库中保护敏感数据并防止恶意攻击或意外错误至关重要。

这个食谱将展示如何将敏感数据,如信用卡信息,存储在 SQL 数据库中。

准备工作

要遵循这个食谱,你需要有一个已经设置好数据库连接的应用程序。

此外,我们还将使用cryptography包。如果你还没有通过requirements.txt文件安装它,你可以在你的环境中运行以下命令来安装:

$ pip install cryptography

对密码学的深入了解可能有益,但并非必需。

如何做……

我们将从零开始创建一个新的表格来存储信用卡信息。其中一些信息,例如信用卡号码和卡验证值CVV),将不会以明文形式存储在我们的数据库中,而是加密存储。由于我们需要将其恢复,我们将使用需要密钥的对称加密。让我们通过以下步骤来完成这个过程。

  1. 让我们从在database.py模块中创建一个与数据库中的credit_card表相对应的类开始,如下所示:

    class CreditCard(Base):
        __tablename__ = "credit_cards"
        id: Mapped[int] = mapped_column(primary_key=True)
        number: Mapped[str]
        expiration_date: Mapped[str]
        cvv: Mapped[str]
        card_holder_name: Mapped[str]
    
    1. 接下来,在app文件夹中,我们创建一个名为security.py的模块,我们将在这个模块中编写使用Fernet 对称加密加密和解密数据的代码,如下所示:
    from cryptography.fernet import Fernet
    cypher_key = Fernet.generate_key()
    cypher_suite = Fernet(cypher_key)
    

    cypher_suite对象将用于定义加密和解密函数。

    值得注意的是,在生产环境中,cypher_key对象可以是外部提供轮换服务的一部分,也可以根据业务的安全需求在启动时创建。

    1. 在同一个模块中,我们可以创建一个加密信用卡信息的函数和一个解密它的函数,如下所示:
    def encrypt_credit_card_info(card_info: str) -> str:
        return cypher_suite.encrypt(
            card_info.encode()
        ).decode()
    def decrypt_credit_card_info(
        encrypted_card_info: str,
    ) -> str:
        return cypher_suite.decrypt(
            encrypted_card_info.encode()
        ).decode()
    

    这些函数将在从数据库写入和读取时使用。

    1. 然后,我们可以在同一个security.py模块中编写存储操作,如下所示:
    from sqlalchemy import select
    from sqlalchemy.ext.asyncio import AsyncSession
    from app.database import CreditCard
    async def store_credit_card_info(
        db_session: AsyncSession,
        card_number: str,
        card_holder_name: str,
        expiration_date: str,
        cvv: str,
    ):
        encrypted_card_number = encrypt_credit_card_info(
            card_number
        )
        encrypted_cvv = encrypt_credit_card_info(cvv)
        # Store encrypted credit card information
        # in the database
        credit_card = CreditCard(
            number=encrypted_card_number,
            card_holder_name=card_holder_name,
            expiration_date=expiration_date,
            cvv=encrypted_cvv,
        )
        async with db_session.begin():
            db_session.add(credit_card)
            await db_session.flush()
            credit_card_id = credit_card.id
            await db_session.commit()
        return credit_card_id
    

    每次函数被等待时,信用卡信息将加密后与机密数据一起存储。

    1. 类似地,我们可以定义一个从数据库中检索加密信用卡信息的函数,如下所示:
    async def retrieve_credit_card_info(
        db_session: AsyncSession, credit_card_id: int
    ):
        query = select(CreditCard).where(
            CreditCard.id == credit_card_id
        )
        async with db_session as session:
            result = await session.execute(query)
            credit_card = result.scalars().first()
        credit_card_number = decrypt_credit_card_info(
                credit_card.number
            ),
        cvv = decrypt_credit_card_info(credit_card.cvv)
        card_holder = credit_card.card_holder_name
        expiry = credit_card.expiration_date
        return {
            "card_number": credit_card_number,
            "card_holder_name": card_holder,
            "expiration_date": expiry,
            "cvv": cvv
        }
    

    我们已经开发出了代码,可以在我们的数据库中保存机密信息。

练习

我们刚刚看到了如何安全存储敏感数据的基本框架。你可以通过以下步骤自己完成这个功能:

  • 为我们的加密操作编写单元测试。在tests文件夹中,让我们创建一个新的测试模块,名为test_security.py。验证信用卡信息是否安全地保存在我们的数据库中,但信用卡号码和 CVV 字段是加密的。

  • 在数据库中创建端点以存储、检索和删除信用卡信息。

  • 将信用卡与赞助商关联并管理相关的 CRUD 操作。

参见

我们已经使用 Fernet 对称加密来加密信用卡信息。您可以在以下链接中深入了解:

处理事务和并发

在数据库管理的领域,两个关键方面决定了应用程序的可靠性和性能:处理事务和管理并发。

事务,封装一系列数据库操作,通过确保更改作为一个单一的工作单元发生,对于维护数据一致性是基本的。另一方面,并发解决多个用户或进程同时访问共享资源的挑战。

当考虑可能同时尝试访问或修改相同数据的多个事务的场景时,事务和并发之间的关系变得明显。如果没有适当的并发控制机制,如锁定,事务可能会相互干扰,可能导致数据损坏或不一致。

这个食谱将展示如何通过模拟我们从票务平台创建的销售过程来使用 FastAPI 和 SQLAlchemy 管理事务和并发。

准备工作

您需要一个 CRUD 应用程序作为食谱的基础,或者您可以使用我们本章中一直在使用的票务系统应用程序。

如何做到这一点...

事务和并发变得重要的最显著情况是在管理更新操作时,例如在我们的应用程序的销售票中。

我们将首先创建一个函数操作,将我们的票标记为已售出并给出客户的名字。然后,我们将模拟同时发生两个销售并观察结果。为此,请按照以下步骤操作。

  1. operations.py模块中,创建以下函数来出售票:

    async def sell_ticket_to_user(
        db_session: AsyncSession, ticket_id: int, user: str
    ) -> bool:
        ticket_query = (
            update(Ticket)
            .where(
                and_(
                    Ticket.id == ticket_id,
                    Ticket.sold == False,
                )
            )
            .values(user=user, sold=True)
        )
        async with db_session as session:
            result = (
               await db_session.execute(ticket_query)
            )
            await db_session.commit()
            if result.rowcount == 0:
                return False
        return True
    

    查询只有在票尚未售出时才会出售票;否则,函数将返回False

    1. 让我们尝试向我们的数据库添加一个票并尝试模拟两个用户同时购买同一张票。让我们将所有内容都写成单元测试的形式。

    我们首先在tests/conftest.py文件中定义一个固定装置,将我们的票写入数据库,如下所示:

    @pytest.fixture
    async def add_special_ticket(db_session_test):
        ticket = Ticket(
            id=1234,
            show="Special Show",
            details=TicketDetails(),
        )
        async with db_session_test.begin():
            db_session_test.add(ticket)
            await db_session_test.commit()
    
    1. 我们可以通过在tests/test_operations.py文件中执行两个并发销售并使用两个不同的数据库会话(定义另一个作为不同的固定装置)来创建一个测试,以同时进行:
    import asyncio
    async def test_concurrent_ticket_sales(
        add_special_ticket,
        db_session_test,
        second_session_test,
    ):
        result = await asyncio.gather(
            sell_ticket_to_user(
                db_session_test, 1234, "Jake Fake"
            ),
            sell_ticket_to_user(
                second_session_test, 1234, "John Doe"
            ),
        )
        assert result in (
            [True, False],
            [False, True],
        )  # only one of the sales should be successful
        ticket = await get_ticket(db_session_test, 1234)
        # assert that the user who bought the ticket
        # correspond to the successful sale
        if result[0]:
            assert ticket.user == "Jake Fake"
        else:
            assert ticket.user == "John Doe"
    

    在测试函数中,我们通过使用asyncio.gather函数同时运行两个协程。

    我们只是假设只有一个用户可以购买票,并且它们将匹配成功的交易。一旦我们创建了测试,我们就可以使用pytest执行如下:

    $ pytest tests/test_operations.py::test_concurrent_ticket_sales
    

测试将成功,这意味着异步会话处理了事务冲突。

练习

您刚刚创建了一个售票操作的草稿。作为一个练习,您可以通过以下方式改进草稿:

  • 在数据库中添加一个用户表

  • 在票上添加用户的外键引用以使其售出

  • 为数据库修改创建一个 alembic 迁移

  • 创建一个公开 sell_ticket_to_user 函数的 API 端点

还有更多...

数据库系统的一个基本挑战是在保持数据一致性和完整性的同时处理来自多个用户的并发事务。不同类型的交易可能对它们如何访问和修改数据以及如何处理可能与之冲突的其他交易有不同的要求。例如,管理并发的一种常见方法是使用 ,这是一种防止对数据进行未经授权或不兼容操作的机制。然而,锁也可能在性能、可用性和正确性之间引入权衡。

根据业务需求,某些事务可能需要更长时间或在不同粒度级别上获取锁,例如表级别或行级别。例如,SQLite 只允许在数据库级别上锁定,而 PostgreSQL 允许锁定到行表级别。

管理并发事务的另一个关键方面是 隔离级别 的概念,它定义了一个事务必须从其他并发事务的影响中隔离的程度。隔离级别确保尽管有多个用户同时访问和修改数据,事务仍能保持数据一致性。

SQL 标准定义了四个隔离级别,每个级别在并发性和数据一致性之间提供不同的权衡:

  1. 读取未提交:

    • 此级别的交易允许脏读,这意味着一个事务可以看到其他并发事务所做的未提交更改。

    • 可重复读和幻读是可能的。

    • 此隔离级别提供了最高的并发性但数据一致性最低。

  2. 读取提交:

    • 此级别的交易只能看到其他事务提交的更改。

    • 不允许脏读。

    • 可重复读是可能的,但幻读仍然可能发生。

    • 此级别在并发性和一致性之间取得了平衡。

  3. 可重复读:

    • 此级别的交易在整个交易过程中看到数据的一致快照。

    • 事务开始后其他事务提交的更改不可见。

    • 防止了可重复读,但可能会发生幻读。

    • 此级别在牺牲一些并发性的情况下提供了更强的一致性。

  4. 可序列化:

    • 此级别的交易表现得好像它们是顺序执行的——也就是说,一个接一个。

    • 它们提供了数据一致性的最高级别。

    • 防止了可重复读和幻读。

    • 此级别提供强一致性,但由于锁定增加,可能会导致并发性降低。

例如,SQLite 允许隔离,而 MySQL 和 PostgreSQL 提供所有四个事务级别。

当数据库支持时,在 SQLAlchemy 中,你可以通过在初始化时指定它作为参数来为每个引擎或连接设置隔离级别。

例如,如果你想在 PostgreSQL 的引擎级别指定隔离级别,引擎将初始化如下:

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
eng = create_engine(
    "postgresql+psycopg2://scott:tiger@localhost/test",
    isolation_level="REPEATABLE READ",
)
Session = sessionmaker(eng)

所有这些关于锁和隔离级别的选择都会影响数据库系统的架构和设计,因为并非所有 SQL 数据库都支持这些功能。因此,了解锁定策略的原则和最佳实践,以及它们与事务行为和业务逻辑的关系非常重要。

你刚刚完成了一个关于将 SQL 数据库与 FastAPI 集成的全面概述。在下一章中,我们将探讨如何将 FastAPI 应用程序与 NoSQL 数据库集成。

参见

你可以在以下链接中找到有关 SQLite 和 PostgreSQL 锁定策略的更多信息:

关于单个数据库的隔离级别信息可以在相应的文档页面上找到:

此外,关于如何使用 SQLAlchemy 管理隔离级别的全面指南可在以下链接找到:


第七章:将 FastAPI 与 NoSQL 数据库集成

在本章中,我们将探讨 FastAPI 与 NoSQL 数据库的集成。通过构建音乐流媒体平台应用程序的后端,您将学习如何使用 FastAPI 设置和使用流行的 NoSQL 数据库 MongoDB

您还将学习如何执行 创建、读取、更新和删除CRUD) 操作,使用索引进行性能优化,以及处理 NoSQL 数据库中的关系。此外,您还将学习如何将 FastAPI 与 Elasticsearch 集成以实现强大的搜索功能,保护敏感数据,并使用 Redis 实现缓存。

到本章结束时,您将深入理解如何有效地使用 FastAPI 与 NoSQL 数据库结合,以提升应用程序的性能和功能。

在本章中,我们将介绍以下食谱:

  • 使用 FastAPI 设置 MongoDB

  • MongoDB 中的 CRUD 操作

  • 处理 NoSQL 数据库中的关系

  • 在 MongoDB 中使用索引

  • 从 NoSQL 数据库中公开敏感数据

  • 将 FastAPI 与 Elasticsearch 集成

  • 在 FastAPI 中使用 Redis 进行缓存

技术要求

要跟随本章的食谱,请确保您的设置包括以下基本要素:

  • Python:应在您的计算机上安装版本 3.7 或更高版本

  • 您的工作环境中的 fastapi

  • asyncio:熟悉 asyncio 框架和 async/await 语法,因为我们将贯穿整个食谱使用它们

本章中使用的代码托管在 GitHub 上,地址为:github.com/PacktPublishing/FastAPI-Cookbook/tree/main/Chapter07

您可以在项目根目录内为项目创建一个虚拟环境,以高效管理依赖项并保持项目隔离。

在您的虚拟环境中,您可以使用 requirements.txt 一次性安装所有依赖项,该文件位于 GitHub 仓库的项目文件夹中:

$ pip install –r requirements.txt

对每个食谱中将要使用的工具的一般了解可能有益,尽管不是强制性的。每个食谱都将为您提供对所使用工具的最小解释。

使用 FastAPI 设置 MongoDB

在这个食谱中,您将学习如何使用 FastAPI 设置流行的文档型 NoSQL 数据库 MongoDB。您将学习如何管理 Python 包以与 MongoDB 交互,创建数据库,并将其连接到 FastAPI 应用程序。到食谱结束时,您将深入理解如何将 MongoDB 与 FastAPI 集成以存储和检索应用程序数据。

准备工作

要跟随这个食谱,您需要在您的环境中安装 Python 和 fastapi package

此外,对于这个配方,确保你有一个正在运行且可访问的 MongoDB 实例,如果没有,请设置一个本地的。根据你的操作系统和你的个人偏好,你可以通过以下几种方式设置本地的 MongoDB 实例。请自由查阅以下链接上的官方文档,了解如何在你的本地机器上安装 MongoDB 社区版:www.mongodb.com/try/download/community

在整个章节中,我们将考虑运行在 http://localhost:27017 的 MongoDB 的本地实例。如果你在远程机器上运行 MongoDB 实例,或者使用不同的端口,请相应地调整 URL 引用。

你还需要在你的环境中安装 motor 包。如果你还没有使用 requirements.txt 安装包,你可以从命令行在你的环境中安装 motor

$ pip install motor

asyncio 库。

一旦我们有了正在运行且可访问的 MongoDB 实例,并且你的环境中已安装了 motor 包,我们就可以继续进行配方。

如何做到这一点...

让我们先创建一个名为 streaming_platform 的项目根文件夹,其中包含一个 app 子文件夹。在 app 中,我们创建一个名为 db_connection.py 的模块,其中将包含与 MongoDB 的连接信息。

现在,我们将通过以下步骤设置连接:

  1. db_connecion.py 模块中,让我们定义 MongoDB 客户端:

    from motor.motor_asyncio import AsyncIOMotorClient
    mongo_client = AsyncIOMotorClient(
        "mongodb://localhost:27017"
    )
    

    我们将每次需要与运行在 http://localhost:27017 的 MongoDB 实例交互时使用 mongo_client 对象。

  2. db_connection.py 模块中,我们将创建一个函数来 ping MongoDB 实例以确保它正在运行。但首先,我们检索 FastAPI 服务器使用的 uvicorn 日志记录器,以便将消息打印到终端:

    import logging
    logger = logging.getLogger("uvicorn.error")
    
  3. 然后,让我们创建一个函数来 ping MongoDB,如下所示:

    async def ping_mongo_db_server():
        try:
            await mongo_client.admin.command("ping")
            logger.info("Connected to MongoDB")
        except Exception as e:
            logger.error(
                f"Error connecting to MongoDB: {e}"
            )
            raise e
    

    该函数将 ping 服务器,如果它没有收到任何响应,它将传播一个错误,这将停止代码的运行。

  4. 最后,我们需要在启动 FastAPI 服务器时运行 ping_mongo_db_server 函数。在 app 文件夹中,让我们创建一个 main.py 模块,其中包含用于启动和关闭我们的 FastAPI 服务器的上下文管理器:

    from contextlib import asynccontextmanager
    from app.db_connection import (
        ping_mongo_db_server,
    )
    @asynccontextmanager
    async def lifespan(app: FastAPI):
        await ping_mongo_db_server(),
        yield
    

    lifespan 上下文管理器必须作为参数传递给 FastAPI 对象:

    from fastapi import FastAPI
    app = FastAPI(lifespan=lifespan)
    

    服务器被包装在 lifespan 上下文管理器中,以在启动时执行数据库检查。

为了测试它,确保你的 MongoDB 实例已经运行,并且像往常一样,让我们从命令行启动服务器:

$ uvicorn app.main:app

你将在输出中看到以下日志消息:

INFO:    Started server process [1364]
INFO:    Waiting for application startup.
INFO:    Connected to MongoDB
INFO:    Application startup complete.

此消息确认我们的应用程序正确地与 MongoDB 实例进行了通信。

你刚刚设置了 FastAPI 应用程序和 MongoDB 实例之间的连接。

参见

你可以在 MongoDB 官方文档页面上了解更多关于 Motor 异步驱动程序的信息:

对于 FastAPI 服务器的启动和关闭事件,您可以在本页面上找到更多信息:

MongoDB 中的 CRUD 操作

CRUD 操作是数据库数据操作的基础,使用户能够以高效、灵活和可扩展的方式创建、读取、更新和删除数据实体。

这个食谱将演示如何在 FastAPI 中创建端点,用于从 MongoDB 数据库创建、读取、更新和删除文档,这是我们流平台的核心。

准备工作

要跟随这个食谱,您需要一个数据库连接,MongoDB 已经与您的应用程序一起设置好了,否则,请回到之前的食谱,使用 FastAPI 设置 MongoDB,它将详细展示如何进行设置。

如何做到这一点…

在创建 CRUD 操作的端点之前,我们必须在 MongoDB 实例上初始化一个数据库,用于我们的流应用程序。

让我们在app目录下的一个名为database.py的专用模块中这样做,如下所示:

from app.db_connection import mongo_client
database = mongo_client.beat_streaming

我们已经定义了一个名为beat_streaming的数据库,它将包含我们应用程序的所有集合。

在 MongoDB 服务器端,我们不需要采取任何行动,因为motor库将自动检查名为beat_streaming的数据库以及最终集合的存在性,如果它们不存在,它将创建它们。

在同一个模块中,我们可以创建一个函数来返回将作为端点依赖项使用的数据库,以提高代码的可维护性:

def mongo_database():
    return database

现在,我们可以在main.py中定义我们的端点,用于每个 CRUD 操作,步骤如下。

  1. 让我们从创建添加歌曲到songs集合的端点开始:

    from bson import ObjectId
    from fastapi import Body, Depends
    from app.database import mongo_database
    from fastapi.encoders import ENCODERS_BY_TYPE
    ENCODERS_BY_TYPE[ObjectId] = str
    @app.post("/song")
    async def add_song(
        song: dict = Body(
            example={
                "title": "My Song",
                "artist": "My Artist",
                "genre": "My Genre",
            },
        ),
        mongo_db=Depends(mongo_database),
    ):
        await mongo_db.songs.insert_one(song)
        return {
            "message": "Song added successfully",
            "id": song["_id"],
        }
    

    该端点在体中接受一个通用的 JSON,并从数据库返回受影响的 ID。ENCONDERS_BY_TYPE[ObjectID] = str这一行指定 FastAPI 服务器,song["_id"]文档 ID 必须解码为string

    选择 NoSQL 数据库的一个原因是不受 SQL 模式限制,这允许在管理数据时具有更大的灵活性。然而,在文档中提供一个示例可能会有所帮助。这是通过使用带有示例参数的Body对象类来实现的。

  2. 获取歌曲的端点将非常直接:

    @app.get("/song/{song_id}")
    async def get_song(
        song_id: str,
        db=Depends(mongo_database),
    ):
        song = await db.songs.find_one(
            {
                "_id": ObjectId(song_id)
                if ObjectId.is_valid(song_id)
                else None
            }
        )
        if not song:
            raise HTTPException(
                status_code=404,
                detail="Song not found"
            )
        return song
    

    应用程序将搜索具有指定 ID 的歌曲,如果找不到,则返回404错误。

  3. 要更新歌曲,端点将看起来像这样:

    @app.put("/song/{song_id}")
    async def update_song(
        song_id: str,
        updated_song: dict,
        db=Depends(mongo_database),
    ):
        result = await db.songs.update_one(
            {
                "_id": ObjectId(song_id)
                if ObjectId.is_valid(song_id)
                else None
            },
            {"$set": updated_song},
        )
        if result.modified_count == 1:
          return {
              "message": "Song updated successfully"
          }
        raise HTTPException(
            status_code=404, detail="Song not found"
        )
    

    如果歌曲 ID 不存在,端点将返回404错误,否则它将只更新请求体中指定的字段。

  4. 最后,delete操作端点可以如下完成:

    @app.delete("/song/{song_id}")
    async def delete_song(
        song_id: str,
        db=Depends(mongo_database),
    ):
        result = await db.songs.delete_one(
            {
                "_id": ObjectId(song_id)
                if ObjectId.is_valid(song_id)
                else None
            }
        )
        if result.deleted_count == 1:
            return {
                "message": "Song deleted successfully"
            }
        raise HTTPException(
            status_code=404, detail="Song not found"
        )
    

    您刚刚创建了与 MongoDB 数据库交互的端点。

现在,从命令行启动服务器并测试您刚刚在 http://localhost:8000/docs 的交互式文档中创建的端点。

如果您跟随 GitHub 存储库,您还可以使用链接中的脚本fill_mongo_db_database.py预先填充数据库:github.com/PacktPublishing/FastAPI-Cookbook/blob/main/Chapter07/streaming_platform/fill_mongo_db_database.py

确保您还下载了同一文件夹中的songs_info.py

您可以从终端按照以下方式运行脚本:

$ python fill_mongo_db_database.py

如果您调用端点GET /songs,您将有一个预先填充的长列表歌曲以测试您的 API。

参考以下内容

您可以在官方文档链接中进一步调查motor提供的操作:

在 NoSQL 数据库中处理关系

与关系型数据库不同,NoSQL 数据库不支持连接或外键来定义集合之间的关系。

无模式数据库,如 MongoDB,不强制执行像传统关系型数据库那样的关系。相反,可以使用两种主要方法来处理关系:嵌入引用

嵌入涉及在单个文档中存储相关数据。这种方法适用于所有类型的关联,前提是嵌入的数据与父文档紧密相关。这种技术对于频繁访问的数据和单个文档的原子更新来说,对读取性能很有好处。然而,如果嵌入的数据频繁更改,它很容易导致数据重复和潜在的不一致性,从而引发大小限制问题。

引用涉及使用它们的对象 ID 或其他唯一标识符存储相关文档的引用。这种方法适用于多对一和多对多关系,其中相关数据很大,并且跨多个文档共享。

这种技术减少了数据重复,提高了独立更新相关数据的灵活性,但另一方面,由于多个查询导致读取操作的复杂性增加,从而在检索相关数据时性能变慢。

在这个食谱中,我们将通过向我们的流平台添加新的集合并使它们交互,来探索在 MongoDB 中处理数据实体之间关系的技术。

准备工作

我们将继续构建我们的流平台。请确保您已经遵循了本章中所有之前的食谱,或者您可以将这些步骤应用于与 NoSQL 数据库交互的现有应用程序。

如何实现它...

让我们看看如何实现嵌入和引用技术的关系。

嵌入

展示歌曲嵌入关系的合适候选者是专辑集合。一旦发布,专辑信息很少改变,甚至从不改变。

album文档将嵌套字段嵌入到song文档中:

{
    "title": "Title of the Song",
    "artist": "Singer Name",
    "genre": "Music genre",
    "album": {
        "title": "Album Title",
        "release_year": 2017,
    },
}

当使用 MongoDB 时,我们可以使用相同的端点检索有关专辑和歌曲的信息。这意味着当我们创建一首新歌时,我们可以直接添加它所属专辑的信息。我们指定文档 song 的存储方式,MongoDB 负责其余部分。

启动服务器并测试POST /song端点。在 JSON 体中包含有关专辑的信息。注意检索到的 ID,并使用它来调用GET /song端点。由于我们尚未在响应模型中定义任何响应模式限制,端点将返回从数据库检索到的所有文档信息,包括专辑。

对于这个用例示例,没有必要担心,但对于某些应用程序,你可能不希望向最终用户披露一个字段。你可以定义一个响应模型(参见第一章使用 FastAPI 的初步步骤,在定义和使用请求和响应模型食谱中)或者在该字段从dict对象返回之前将其删除。

你刚刚定义了一个使用嵌入策略的多对一关系,将歌曲与专辑相关联。

引用

引用关系的典型用例可以是创建播放列表。播放列表包含多首歌曲,每首歌曲可以出现在不同的播放列表中。此外,播放列表通常会被更改或更新,因此需要一个引用策略来管理这些关系。

在数据库方面,我们不需要采取任何行动,因此我们将直接创建创建播放列表和检索包含所有歌曲信息的播放列表的端点。

  1. 你可以在main.py模块中定义创建播放列表的端点:

    class Playlist(BaseModel):
        name: str
        songs: list[str] = []
    @app.post("/playlist")
    async def create_playlist(
        playlist: Playlist = Body(
            example={
                "name": "My Playlist",
                "songs": ["song_id"],
            }
        ),
        db=Depends(mongo_database),
    ):
        result = await db.playlists.insert_one(
            playlist.model_dump()
        )
        return {
            "message": "Playlist created successfully",
            "id": str(result.inserted_id),
        }
    

    该端点需要一个 JSON 体,指定播放列表名称和要包含的歌曲 ID 列表,并返回播放列表 ID。

  2. 获取播放列表的端点将接受播放列表 ID 作为参数。你可以这样编写代码:

    @app.get("/playlist/{playlist_id}")
    async def get_playlist(
        playlist_id: str,
        db=Depends(mongo_database),
    ):
        playlist = await db.playlists.find_one(
            {
                "_id": ObjectId(playlist_id)
                if ObjectId.is_valid(playlist_id)
                else None
            }
        )
        if not playlist:
            raise HTTPException(
                status_code=404,
                detail="Playlist not found"
            )
        songs = await db.songs.find(
            {
                "_id": {
                    "$in": [
                        ObjectId(song_id)
                        for song_id in playlist["songs"]
                    ]
                }
            }
        ).to_list(None)
        return {
            "name": playlist["name"],
            "songs": songs
        }
    

    注意,播放列表集合中的歌曲 ID 存储为字符串,而不是ObjectId,这意味着在查询时必须进行转换。

    此外,为了接收播放列表的歌曲列表,我们不得不进行两次查询:一次用于播放列表,一次用于根据 ID 检索歌曲。

现在你已经构建了创建和检索播放列表的端点,启动服务器:

http://localhost:8000/docs and you will see the new endpoints: POST /playlist and GET /playlist.
To test the endpoints, create some songs and note their IDs. Then, create a playlist and retrieve the playlist with the `GET /playlist` endpoint. You will see that the response will contain the songs with all the information including the album.
At this point, you have all the tools to manage relationships between collections in MongoDB.
See also
We just saw how to manage relationships with MongoDB and create relative endpoints. Feel free to check the official MongoDB guidelines at this link:

*   *MongoDB Model* *Relationships*: [`www.mongodb.com/docs/manual/applications/data-models-relationships/`](https://www.mongodb.com/docs/manual/applications/data-models-relationships/)

Working with indexes in MongoDB
An **index** is a data structure that provides a quick lookup mechanism for locating specific pieces of data within a vast dataset. Indexes are crucial for enhancing query performance by enabling the database to quickly locate documents based on specific fields.
By creating appropriate indexes, you can significantly reduce the time taken to execute queries, especially for large collections. Indexes also facilitate the enforcement of uniqueness constraints and support the execution of sorted queries and text search queries.
In this recipe, we’ll explore the concept of indexes in MongoDB and we will create indexes to improve search performances for songs in our streaming platform.
Getting ready
To follow along with the recipe, you need to have a MongoDB instance already set up with at least a collection to apply indexes. If you are following along with the cookbook, make sure you went through the *Setting up MongoDB with FastAPI* and *CRUD operations in* *MongoDB* recipes.
How to do it…
Let’s imagine we need to search for songs released in a certain year. We can create a dedicated endpoint directly in the `main.py` module as follows:

@app.get("/songs/year")

async def get_songs_by_released_year(

year: int,

db=Depends(mongo_database),

):

query = db.songs.find({"album.release_year": year})

songs = await query.to_list(None)

返回 songs


 The query will fetch all documents and filter the one with a certain `release_year`. To speed up the query, we can create a dedicated index on the release year. We can do it at the server startup in the `lifespan` context manager in `main.py`. A text search in MongoDB won’t be possible without a text index.
First, at the startup server, let’s create a text index based on the `artist` field of the collection document. To do this, let’s modify the `lifespan` context manager in the `main.py` module:

@asynccontextmanager

async def lifespan(app: FastAPI):

等待 ping_mongo_db_server(),

db = mongo_database()

await db.songs.create_index({"album.release_year": -1})

yield


 The `create_index` method will create an index based on the `release_year` field sorted in descending mode because of the `-``1` value.
You’ve just created an index based on the `release_year` field.
How it works…
The index just created is automatically used by MongoDB when running the query.
Let’s check it by leveraging the explain query method. Let’s add the following log message to the endpoint to retrieve songs released in a certain year:

@app.get("/songs/year")

async def get_songs_by_released_year(

year: int,

db=Depends(mongo_database),

):

query = db.songs.find({"album.release_year": year})

explained_query = await query.explain()

logger.info(

"Index used: %s",

explained_query.get("queryPlanner", {})

.get("winningPlan", {})

.get("inputStage", {})

.get("indexName", "No index used"),

)

songs = await query.to_list(None)

return songs


 The `explained_query` variable holds information about the query such as the query execution or index used for the search.
If you run the server and call the `GET /songs/year` endpoint, you will see the following message log on the terminal output:

INFO:    Index used: album.release_year_-1


 This confirms that the query has correctly used the index we created to run.
There’s more…
Database indexes become necessary to run text search queries. Imagine we need to retrieve the songs of a certain artist.
To query and create the endpoint, we need to make a text index on the `artist` field. We can do it at the server startup like the previous index on `album.release_year`.
In the `lifespan` context manager, you can add the index creation:

@asynccontextmanager

async def lifespan(app: FastAPI):

await ping_mongodb_server(),

db = mongo_database()

await db.songs.drop_indexes()

await db.songs.create_index({"release_year": -1})

await db.songs.create_index({"artist": "text"})

yield


 Once we have created the index, we can proceed to create the endpoint to retrieve the song based on the artist’s name.
In the same `main.py` module, create the endpoint as follows:

@app.get("/songs/artist")

async def get_songs_by_artist(

artist: str,

db=Depends(mongo_database),

):

query = db.songs.find(

{"\(text": {"\)search": artist}}

)

explained_query = await query.explain()

logger.info(

"Index used: %s",

explained_query.get("queryPlanner", {})

.get("winningPlan", {})

.get("indexName", "No index used"),

)

songs = await query.to_list(None)

return songs


 Spin up the server from the command line with the following:

$ uvicorn app.main:app


 Go to the interactive documentation at `http:/localhost:8000/docs` and try to run the new `GET /``songs/artist` endpoint.
Text searching allow you to fetch records based on text matching. If you have filled the database with the `fill_mongo_db_database.py` script you can try searching for Bruno Mars’s songs by specifying the family name `"mars"`. The query will be:

http://localhost:8000/songs/artist?artist=mars


 This will return at the least the song:

[

{

"_id": "667038acde3a00e55e764cf7",

"title": "Uptown Funk",

"artist": "Mark Ronson ft. Bruno Mars",

"genre": "Funk/pop",

"album": {

"title": "Uptown Special",

"release_year": 2014

}

}

]


 Also, you will see a message on the terminal output like:

INFO:    Index used: artist_text


 That means that the database has used the correct index to fetch the data.
Important note
By using the `explanation_query` variable, you can also check the difference in the execution time. However, you need a huge number of documents in your collection to appreciate the improvement.
See also
We saw how to build a text index for the search over the artist and a numbered index for the year of release. MongoDB allows you to do more, such as defining 2D sphere index types or compound indexes. Have a look at the documentation to discover the potential of indexing your MongoDB database:

*   *Mongo* *Indexes*: https://www.mongodb.com/docs/v5.3/indexes/
*   *MongoDB Text* *Search*: [`www.mongodb.com/docs/manual/core/link-text-indexes/`](https://www.mongodb.com/docs/manual/core/link-text-indexes/)

Exposing sensitive data from NoSQL databases
The way to expose sensitive data in NoSQL databases is pivotal to protecting sensitive information and maintaining the integrity of your application.
In this recipe, we will demonstrate how to securely view our data through database aggregations with the intent to expose it to a third-party consumer of our API. This technique is known as **data masking**. Then, we will explore some strategies and best practices for securing sensitive data in MongoDB and NoSQL databases in general.
By following best practices and staying informed about the latest security updates, you can effectively safeguard your MongoDB databases against potential security threats.
Getting ready
To follow the recipe, you need to have a running FastAPI application with a MongoDB connection already set up. If don’t have it yet, have a look at the *Setting up MongoDB with FastAPI* recipe. In addition, you need a collection of sensitive data such as **Personal Identifiable Information** (**PII**) or other restricted information.
Alternatively, we can build a collection of users into our MongoDB database, `beat_streaming`. The document contains PIIs such as names and emails, as well as users actions on the platform. The document will look like this:

{

"name": "John Doe",

"email": "johndoe@email.com",

"year_of_birth": 1990,

"country": "USA",

"consent_to_share_data": True,

"actions": [

{

"action": "basic subscription",

"date": "2021-01-01",

"amount": 10,

},

{

"action": "unscription",

"date": "2021-05-01",

},

],

}


 The `consent_to_share_data` field stores the consent of the user to share behavioral data with third-party partners.
Let’s first fill the collection users in our database. You can do this with a user’s sample by running the script provided in the GitHub repository:

$ python fill_users_in_mongo.py


 If everything runs smoothly, you should have the collection users in your MongoDB instance.
How to do it…
Imagine we need to expose users data for marketing research to a third-party API consumer for commercial purposes. The third-party consumer does not need PII information such as names or emails, and they are also not allowed to have data from users who didn’t give their consent. This is a perfect use case to apply data masking.
In MongoDB, you can build aggregation pipelines in stages. We will do it step by step.

1.  Since the database scaffolding is an infrastructure operation rather than an application, let’s create the pipeline with the view in a separate script that we will run separately from the server.

    In a new file called `create_aggregation_and_user_data_view.py`, let’s start by defining the client:

    ```

    from pymongo import MongoClient

    client = MongoClient("mongodb://localhost:27017/")

    ```py

    Since we don’t have any need to manage high traffic, we will use the simple `pymongo` client instead of the asynchronous one. We will reserve the asynchronous to the sole use of the application interactions.

     2.  The pipeline stage follows a specific aggregations framework. The first step of the pipeline will be to filter out the users who didn’t approve the consent. This can be done with a `$``redact` stage:

    ```

    pipeline_redact = {

    "$redact": {

    "$cond": {

    "if": {

    "$eq": [

    "$consent_to_share_data", True

    ]

    },

    "then": "$$KEEP",

    "else": "$$PRUNE",

    }

    }

    }

    ```py

     3.  Then, we filter out the emails that shouldn’t be shared with a `$``unset` stage:

    ```

    pipeline_remove_email_and_name = {

    "$unset": ["email", "name"]

    }

    ```py

     4.  This part of the pipeline will prevent emails and names from appearing in the pipeline’s output. We will split stage definition into three dictionaries for a better understanding.

    First, we define the action to obfuscate the day for each date:

    ```

    obfuscate_day_of_date = {

    "$concat": [

    {

    "$substrCP": [

    "$$action.date",

    0,

    7,

    ]

    },

    "-XX",

    ]

    }

    ```py

     5.  Then, we map the new `date` field for each element of the actions list:

    ```

    rebuild_actions_elements = {

    "input": "$actions",

    "as": "action",

    "in": {

    "$mergeObjects": [

    "$$action",

    {"date": obfuscate_day_of_date},

    ]

    },

    }

    ```py

     6.  Then, we use a `$set` operation to apply the `rebuild_actions_element` operation to every record like that:

    ```

    pipeline_set_actions = {

    "$set": {

    "actions": {"$map": rebuild_actions_elements},

    }

    }

    ```py

     7.  Then, we gather the pipelines just created to define the entire pipeline stage:

    ```

    pipeline = [

    pipeline_redact,

    pipeline_remove_email_and_name,

    pipeline_set_actions,

    ]

    ```py

     8.  We can use the list of aggregation stages to retrieve results and create the view in the `__main__` section of the script:

    ```

    if __name__ == "__main__":

    client["beat_streaming"].drop_collection(

    "users_data_view"

    )

    client["beat_streaming"].create_collection(

    "users_data_view",

    viewOn="users",

    pipeline=pipeline,

    )

    users_data_view view will be created in our beat_streaming database.

    ```py

     9.  Once we have the view, we can create a dedicated endpoint to expose this view to a third-party customer without exposing any sensible data. We can create our endpoint in a separate module for clarity. In the `app` folder, let’s create the `third_party_endpoint.py` module. In the module, let’s create the module router as follows:

    ```

    from fastapi import APIRouter, Depends

    from app.database import mongo_database

    router = APIRouter(

    prefix="/thirdparty",

    tags=["third party"],

    )

    ```py

     10.  Then, we can define the endpoint:

    ```

    @router.get("/users/actions")

    async def get_users_with_actions(

    db=Depends(mongo_database),

    ):

    users = [

    user

    async for user in db.users_data_view.find(

    {}, {"_id": 0}

    )

    ]

    return users

    ```py

     11.  Once the endpoint function has been created, let’s include the new router in the `FastAPI` object in the `main.py` module:

    ```

    from app import third_party_endpoint

    ## rest of the main.py code

    app = FastAPI(lifespan=lifespan)

    app.include_router(third_party_endpoint.router)

    ## rest of the main.py code

    ```py

The endpoint is now implemented in our API. Let’s start the server by running the following command:

通过访问 http://localhost:8000/docs,您可以检查新创建的端点是否存在,并调用它以检索创建的视图中的所有用户,而无需任何敏感信息。

您刚刚创建了一个安全地公开用户数据的端点。可以通过在端点上实现基于角色的访问控制(RBAC),例如在配方设置 RBAC中的第四章 身份验证和授权中,添加一个额外的安全层。

更多内容...

除了数据脱敏之外,通常还会添加额外的层来保护您的数据应用。其中最重要的如下:

  • 静态加密

  • 传输加密

  • 基于角色的访问控制(RBAC)

MongoDB 的企业版本提供了三个现成的服务解决方案。是否使用它们由软件架构师自行决定。

静态加密涉及加密存储在 MongoDB 数据库中的数据,以防止未经授权访问敏感信息。MongoDB 的企业版本通过使用专用存储引擎提供内置的加密功能。通过启用静态加密,您可以确保数据在磁盘上加密,使得没有适当加密密钥的人无法读取。

传输加密确保在您的应用程序和 MongoDB 服务器之间传输的数据被加密,以防止窃听和篡改。MongoDB 支持使用传输层安全性TLS)进行传输加密,它加密在您的应用程序和 MongoDB 服务器之间通过网络发送的数据。

基于角色的访问控制(RBAC)对于限制 MongoDB 数据库中敏感数据的访问至关重要。MongoDB 提供了强大的身份验证和授权机制来控制对数据库、集合和文档的访问。您可以根据不同的角色和权限创建用户账户,以确保只有授权用户可以访问和操作敏感数据。

MongoDB 支持 RBAC,允许您根据用户的责任分配特定的角色,并相应地限制对敏感数据的访问。

参见

在配方中,我们简要地了解了如何在 MongoDB 中创建聚合和视图。您可以自由地查看官方文档页面上的更多内容:

在此链接中可以找到一个很好的例子,展示了如何在 MongoDB 中通过数据库聚合推进数据脱敏:

您可以在官方文档页面上了解更多关于聚合框架命令的信息:

此外,一本关于 MongoDB 聚合的全面书籍,免费查阅,可在以下链接找到:

将 FastAPI 与 Elasticsearch 集成

Elasticsearch 是一个功能强大的搜索引擎,提供快速高效的全文本搜索、实时分析和更多功能。通过将 Elasticsearch 与 FastAPI 集成,您可以启用高级搜索功能,包括关键字搜索、过滤和聚合。我们将逐步介绍在 FastAPI 应用程序中集成 Elasticsearch、索引数据、执行搜索查询和处理搜索结果的过程。

在本菜谱中,我们将为我们的流媒体平台创建一个特定的端点,以启用分析和增强您的 Web 应用程序的搜索功能。具体来说,我们将根据指定国家的观看次数检索前十个艺术家。

到本菜谱结束时,您将具备利用 Elasticsearch 在 FastAPI 项目中实现强大搜索功能的知识和工具。

准备工作

要跟随本菜谱,您需要一个正在运行的应用程序或继续在我们的流媒体平台上工作。

此外,您需要一个运行中的 Elasticsearch 实例,并且可以通过此地址访问:http://localhost:9200

您也可以通过遵循官方指南在您的机器上安装 Elasticsearch:www.elastic.co/guide/en/elasticsearch/reference/current/install-elasticsearch.xhtml

然后,如果您还没有使用 requirements.txt 安装包,您需要使用 pip 从命令行在您的环境中安装 Elasticsearch Python 客户端和 aiohttp 包。您可以使用以下命令完成此操作:

$ pip install "elasticsearch>=8,<9" aiohttp

对 Elasticsearch 的领域特定语言DSL)有基本了解将有助于更深入地理解我们将要实施的查询。

查看此链接的官方文档:www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.xhtml

一旦您安装并运行了 Elasticsearch,我们就可以将其集成到我们的应用程序中。

如何操作…

我们将整个过程分解为以下步骤:

  1. 在我们的 FastAPI 应用程序中设置 Elasticsearch,以便我们的 API 可以与 Elasticsearch 实例通信。

  2. 创建一个 Elasticsearch 索引,以便我们的歌曲可以被索引并由 Elasticsearch 查询。

  3. 构建查询以查询我们的歌曲索引。

  4. 创建 FastAPI 端点以向 API 用户公开我们的分析端点。

让我们详细查看这些步骤。

在我们的 FastAPI 应用程序中设置 Elasticsearch

要与 Elasticsearch 服务器交互,我们需要在我们的 Python 代码中定义客户端。在已经定义了 MongoDB 参数的db_connection.py模块中,让我们定义 Elasticsearch 异步客户端:

from elasticsearch import AsyncElasticsearch,
es_client = AsyncElasticsearch(
    "localhost:27017"
)

我们可以在同一模块中创建一个函数来检查与 Elasticsearch 的连接:

from elasticsearch import (
    TransportError,
)
async def ping_elasticsearch_server():
    try:
        await es_client.info()
        logger.info(
            "Elasticsearch connection successful"
        )
    except TransportError as e:
        logger.error(
            f"Elasticsearch connection failed: {e}"
        )
        raise e

如果 ping 失败,函数将 ping Elasticsearch 服务器并传播错误。

然后,我们可以在main.py模块的lifetime上下文管理器中调用该函数:

@asynccontextmanager
async def lifespan(app: FastAPI):
    await ping_mongo_db_server(),
    await ping_elasticsearch_server()
# rest of the code

这将确保应用程序在启动时检查与 Elasticsearch 服务器的连接,如果 Elasticsearch 服务器没有响应,它将传播错误。

创建 Elasticsearch 索引

首先,我们应该开始用歌曲文档集合填充我们的 Elasticsearch 实例。在 Elasticsearch 中,集合被称为索引

歌曲文档应包含一个额外的字段,用于跟踪每个国家的观看信息。例如,新的文档歌曲将如下所示:

{
    "title": "Song Title",
    "artist": "Singer Name",
    "album": {
    "title": "Album Title",
    "release_year": 2012,
    },
    "genre": "rock pop",
    "views_per_country": {
    "India": 50_000_000,
    "UK": 35_000_150_000,
    "Mexico": 60_000_000,
    "Spain": 40_000_000,
    },
}

你可以在项目 GitHub 仓库中的songs_info.py文件中找到一个采样歌曲列表。如果你使用该文件,你还可以定义一个函数来填充索引,如下所示:

from app.db_connection import es_client
async def fill_elastichsearch():
    for song in songs_info:
        await es_client.index(
            index="songs_index", body=song
        )
    await es_client.close()

要根据国家的观看次数分组我们的歌曲,我们需要根据views_per_country字段获取数据,而对于前十位艺术家,我们将根据artist字段进行分组。

应将这些信息提供给索引过程,以便 Elasticsearch 了解如何索引索引内的文档以运行查询。

在一个名为fill_elasticsearch_index.py的新模块中,我们可以将此信息存储在一个python字典中:

mapping = {
    "mappings": {
        "properties": {
            "artist": {"type": "keyword"},
            "views_per_country": {
                "type": "object",
                "dynamic": True,
            },
        }
    }
}

当创建索引时,mapping对象将作为参数传递给 Elasticsearch 客户端。我们可以定义一个函数来创建我们的songs_index

from app.db_connection import es_client
async def create_index():
    await es_client.options(
        ignore_status=[400, 404]
    ).indices.create(
        index="songs_index",
        body=mapping,
    )
    await es_client.close()

你可以将该函数运行在main()分组中,并使用模块的__main__部分如下运行:

async def main():
    await create_index()
    await fill_elastichsearch() # only if you use it
if __name__ == "__main__":
    import asyncio
    asyncio.run(create_index())

然后,你可以从终端运行脚本:

$ python fill_elasticsearch_index.py

现在索引已创建,我们只需将歌曲添加到索引中。你可以通过创建一个单独的脚本或运行 GitHub 仓库中提供的fill_elasticsearch_index.py来实现这一点。

我们刚刚在我们的 Elasticsearch 索引中设置了一个索引,填充了文档。让我们看看如何构建查询。

构建查询

我们将构建一个函数,根据指定的国家返回查询。

我们可以在app文件夹中的单独模块es_queries.py中这样做。查询应获取包含特定国家views_per_country映射索引的所有文档,并按降序排序:

def top_ten_songs_query(country) -> dict:
    views_field = f"views_per_country.{country}"
    query = {
        "bool": {
            "must": {"match_all": {}},
            "filter": [
                {"exists": {"field": views_field}}
            ],
        }
    }
    sort = {views_field: {"order": "desc"}}

然后,我们过滤出我们希望在响应中包含的字段,如下所示:

    source = [
        "title",
        views_field,
        "album.title",
        "artist",
    ]

最后,我们通过指定我们期望的列表大小来以字典的形式返回查询:

      return {
        "index": "songs_index",
        "query": query,
        "size": 10,
        "sort": sort,
        "source": source,
    }

现在我们已经有了构建查询以检索指定国家前十个艺术家的函数,我们将在我们的端点中使用它。

创建 FastAPI 端点

一旦我们设置了 Elasticsearch 连接并制定了查询,创建端点就是一个简单的过程。让我们在app文件夹下的一个新模块main_search.py中定义它。让我们首先定义路由器:

from fastapi import APIRouter
router = APIRouter(prefix="/search", tags=["search"])

然后,端点将是:

from fastapi import Depends, HTTPException
from app.db_connection import es_client
def get_elasticsearch_client():
    return es_client
@router.get("/top/ten/artists/{country}")
async def top_ten_artist_by_country(
    country: str,
    es_client=Depends(get_elasticsearch_client),
):
    try:
        response = await es_client.search(
         *top_ten_artists_query(country)
    )
    except BadRequestError as e:
        logger.error(e)
        raise HTTPException(
            status_code=400,
            detail="Invalid country",
        )
    return [
        {
            "artist": record.get("key"),
            "views": record.get("views", {}).get(
                "value"
            ),
        }
        for record in response["aggregations"][
            "top_ten_artists"
        ]["buckets"]
    ]

在返回之前,查询结果将进一步调整,以提取我们感兴趣的唯一值,即艺术家和观看次数。

最后一步是将路由器包含到我们的FastAPI对象中,以包含端点。

main.py模块中,我们可以添加路由器如下:

import main_search
## existing code in main.py
app = FastAPI(lifespan=lifespan)
app.include_router(third_party_endpoint.router)
app.include_router(main_search.router)
## rest of the code

现在,如果您使用uvicorn app.main:app命令启动服务器并转到http://localhost:8000/docs的交互式文档,您将看到根据歌曲观看次数检索一个国家前十个艺术家的新创建的端点。

您刚刚创建了一个与 Elasticsearch 实例交互的 FastAPI 端点。您可以自由地创建自己的新端点。例如,您可以创建一个返回某个国家前十个歌曲的端点。

参见

由于我们使用了 Elasticsearch Python 客户端,您可以自由地深入了解官方文档页面:

要了解更多关于 Elasticsearch 索引的信息,请查看 Elasticsearch 文档:

您可以在此链接中找到映射指南:

最后,您可以在以下链接中深入了解搜索查询语言:

在 FastAPI 中使用 Redis 进行缓存

Redis 是一个内存数据存储,可以用作缓存来提高 FastAPI 应用程序的性能和可伸缩性。通过在 Redis 中缓存频繁访问的数据,您可以减少对数据库的负载并加快 API 端点的响应时间。

在这个菜谱中,我们将探讨如何将 Redis 缓存集成到我们的流平台应用程序中,并将缓存一个端点作为示例。

准备工作

要跟随这个菜谱,您需要一个运行中的 Redis 实例,可通过 http://localhost:6379 地址访问。

根据您的机器和偏好,您有多种安装和运行它的方法。查看 Redis 文档以了解如何在您的操作系统上执行此操作:redis.io/docs/install/install-redis/

此外,您还需要一个具有耗时端点的 FastAPI 应用程序。

或者,如果您遵循流平台,请确保您已经从之前的菜谱中创建了前十个艺术家的端点,将 FastAPI 与 Elasticsearch集成。

您的环境还需要 Python 的 Redis 客户端。如果您还没有使用requirements.txt安装包,可以通过运行以下命令来完成:

$ pip install redis

安装完成后,我们可以继续进行菜谱。

如何做到这一点…

一旦 Redis 运行并可通过localhost:6379访问,我们就可以将 Redis 客户端集成到我们的代码中:

  1. db_connection.py模块中,我们已经为 Mongo 和 Elasticsearch 定义了客户端,现在让我们添加 Redis 的客户端:

    from redis import asyncio as aioredis
    redis_client = aioredis.from_url("redis://localhost")
    
    1. 类似于其他数据库,我们可以在应用程序启动时创建一个 ping Redis 服务器的函数。该函数可以定义为以下内容:
    async def ping_redis_server():
        try:
            await redis_client.ping()
            logger.info("Connected to Redis")
        except Exception as e:
            logger.error(
                f"Error connecting to Redis: {e}"
            )
            raise e
    
    1. 然后,将其包含在main.py中的lifespan上下文管理器中:
    @asynccontextmanager
    async def lifespan(app: FastAPI):
        await ping_mongo_db_server(),
        await ping_elasticsearch_server(),
        await ping_redis_server(),
        yield
    

    现在,我们可以使用redis_client对象来缓存我们的端点。我们将缓存用于查询 Elasticsearch 的GET /search/top/ten/artists端点。

    1. main_search.py中,我们可以定义一个函数来检索 Redis 客户端作为依赖项:
    def get_redis_client():
        return redis_client
    
    1. 然后,您可以按如下方式修改端点:
    @router.get("/top/ten/artists/{country}")
    async def top_ten_artist_by_country(
        country: str,
        es_client=Depends(get_elasticsearch_client),
        redis_client=Depends(get_redis_client),
    ):
    
    1. 在函数的开始处,我们检索存储值的键,并检查该值是否已经存储在 Redis 中:
        cache_key = f"top_ten_artists_{country}"
        cached_data = await redis_client.get(cache_key)
        if cached_data:
            logger.info(
                f"Returning cached data for {country}"
            )
            return json.loads(cached_data)
    
    1. 然后,当我们看到数据不存在时,我们继续从 Elasticsearch 获取数据:
        try:
            response = await es_client.search(
                 *top_ten_artists_query(country)
            )
        except BadRequestError as e:
            logger.error(e)
            raise HTTPException(
                status_code=400,
                detail="Invalid country",
            )
        artists = [
            {
                "artist": record.get("key"),
                "views": record.get("views", {}).get(
                    "value"
                ),
            }
            for record in response["aggregations"][
                "top_ten_artists"
            ]["buckets"]
        ]
    
    1. 一旦我们检索到列表,我们将其存储在 Redis 中,以便在后续调用中检索:
        await redis_client.set(
            cache_key, json.dumps(artists), ex=3600
        )
        return artists
    
    1. 我们指定了一个过期时间,即记录将在 Redis 中停留的秒数。在此时间之后,记录将不再可用,艺术家列表将从 Elasticsearch 中重新调用。

现在,如果您使用uvicorn app.main:app命令运行服务器并尝试调用意大利的端点,您将注意到第二次调用的响应时间将大大减少。

您已经使用 Redis 为我们的应用程序的一个端点实现了缓存。使用相同的策略,您可以自由地缓存所有其他端点。

还有更多…

在撰写本文时,有一个有希望的库,fastapi-cache,它使 FastAPI 中的缓存变得非常简单。请查看 GitHub 仓库:github.com/long2ice/fastapi-cache

该库支持多个缓存数据库,包括 Redis 和内存缓存。通过简单的端点装饰器,您可以指定缓存参数,如存活时间、编码器和缓存响应头。

参见

Redis 的 Python 客户端支持更多高级功能。您可以在官方文档中自由探索其潜力:


第八章:高级特性和最佳实践

欢迎来到 第八章,我们将探讨优化 FastAPI 应用程序功能、性能和可扩展性的高级技术和最佳实践。

在本章中,通过构建一个旅行社平台,您将深入了解依赖注入、自定义中间件、国际化、性能优化、速率限制和后台任务执行等基本主题。通过掌握这些高级特性,您将能够使用 FastAPI 构建强大、高效和高性能的 API。

到本章结束时,您将对高级 FastAPI 功能和最佳实践有一个全面的理解,这将使您能够构建高效、可扩展和安全的 API,以满足现代 Web 应用程序的需求。让我们深入探讨这些高级技术,以提升您的 FastAPI 开发技能。

在本章中,我们将涵盖以下配方:

  • 实现依赖注入

  • 创建自定义中间件

  • 国际化和本地化

  • 优化应用程序性能

  • 实现速率限制

  • 实现后台任务

技术要求

要能够跟随本章的配方,您必须对以下基本概念有很好的掌握:

  • Python:您应该对 Python 3.7 或更高版本有很好的理解。您应该了解注解的工作原理以及基本类继承。

  • fastapiasyncioasync/await 语法。

本章中使用的代码托管在 GitHub 上,网址为 github.com/PacktPublishing/FastAPI-Cookbook/tree/main/Chapter08

为了更高效地管理依赖项并保持项目隔离,考虑在 project 根目录下创建一个虚拟环境。您可以通过使用 GitHub 仓库中 project 文件夹提供的 requirements.txt 文件,轻松地同时安装所有依赖项:

$  pip install –r requirements.txt

您可以从第一个配方开始,高效地在您的 FastAPI 应用程序中实现依赖注入。

实现依赖注入

依赖注入是一种在软件开发中用于管理组件之间依赖关系的强大设计模式。在 FastAPI 的上下文中,依赖注入允许您高效地管理和注入依赖项,例如数据库连接、认证服务和配置设置,到您的应用程序的端点和中间件中。尽管我们已经在之前的配方中使用了依赖注入,例如在 第二章 设置 SQL 数据库处理数据 或在 第四章 设置用户注册认证和授权 中,这个配方将向您展示如何在 FastAPI 中实现依赖注入,以及如何处理具有嵌套依赖注入的更复杂的使用案例。

准备工作

要跟随这个配方,你只需要在你的环境中安装了 Python,并且安装了 fastapiuvicorn 包,以及 pytest。如果你还没有使用 GitHub 仓库中提供的 requirements.txt 文件安装这些包,你可以从命令行使用 pip 安装它们:

$ pip install fastapi uvicorn pytest

此外,了解如何在 FastAPI 中创建一个简单的服务器将很有帮助。你可以参考 第一章 中的 Creating a new FastAPI project 配方,了解更多详情。

如何做到这一点……

让我们从创建一个名为 trip_platform 的项目根文件夹开始,该文件夹包含 app 文件夹。然后按照以下步骤继续进行。

  1. app 文件夹中,创建一个包含服务器的 main.py 模块,如下所示:

    from fastapi import FastAPI
    app = FastAPI()
    

    我们将在 app 文件夹内创建一个名为 dependencies.py 的单独模块来编写依赖项。

  2. 让我们假设我们需要创建一个端点来检索从开始日期到结束日期之间的所有行程。我们需要处理两个参数,即开始日期和结束日期,并检查开始日期是否早于结束日期。这两个参数都可以是可选的;如果未提供开始日期,则默认为当前日期。

    app 文件夹的 dependencies.py 专用模块中,让我们定义一个条件函数,该函数检查开始日期是否早于结束日期:

    from fastapi import HTTPException
    def check_start_end_condition(start: date, end: date):
        if end and end < start:
            raise HTTPException(
                status_code=400,
                detail=(
                    "End date must be "
                    "greater than start date"
                ),
            )
    
  3. 我们使用 check_start_end_condition 函数来定义 dependable 函数——即用作依赖项的函数——如下所示:

    from datetime import date, timedelta
    from fastapi import Query
    def time_range(
        start: date | None = Query(
            default=date.today(),
            description=(
                "If not provided the current date is used"
            ),
            example=date.today().isoformat(),
        ),
        end: date | None = Query(
            None,
            example=date.today() + timedelta(days=7),
        ),
    ) -> Tuple[date, date | None]:
        check_start_end_condition(start, end)
        return start, end
    

    Query 对象用于管理查询参数的元数据,例如在生成文档时使用的默认值、描述和示例。

  4. 我们可以使用可信赖的 time_range 函数在 main.py 模块中创建端点。为了指定它是一个依赖项,我们使用 Depends 对象,如下所示:

    from fastapi import Depends
    @app.get("/v1/trips")
    def get_tours(
        time_range = Depends(time_range),
    ):
        start, end = time_range
        message = f"Request trips from {start}"
        if end:
            return f"{message} to {end}"
        return message
    

    你还可以使用 typing 包中的 Annotated 类来如下定义依赖项:

    from typing import Annotated
    from fastapi import Depends
    @app.get("/v1/trips")
    def get_tours(
        time_range: Annotated[time_range, Depends()]
    ):
    

重要提示

FastAPI 中 Annotated 的使用目前正在演变,以避免重复并提高可读性;请参阅专门的文档部分:fastapi.tiangolo.com/tutorial/dependencies/#share-annotated-dependencies

对于本章的其余部分,我们将使用最新的 Annotated 习惯用法。

现在,如果你在终端中运行 uvicorn app.main:app 来启动服务器,你将在交互式文档的 http://localhost:8000/docs 中找到端点。你会看到你刚刚创建的端点,参数得到了正确的文档说明。示例中用字符串构造替换了数据库逻辑,返回了一条重要信息。

你刚刚实现了一个依赖注入策略来定义端点的查询参数。你可以使用相同的策略来编写路径或体参数,以编写模块化和可读的代码。

使用依赖注入的一个优点是逻辑上分离可以替换为其他东西的代码片段,比如在测试中。让我们看看如何做到这一点。

在测试中覆盖依赖注入

让我们为GET /v1/trips端点创建一个测试。如果你环境中没有pytest,请使用pip install pytest进行安装。然后,在项目根目录下创建pytest.ini文件,包含pytestpythonpath,如下所示:

[pytest]
pythonpath=.

测试将在tests文件夹下的test_main.py测试模块中进行。让我们通过覆盖客户端的依赖来编写一个单元测试:

from datetime import date
from fastapi.testclient import TestClient
from app.dependencies import time_range
from app.main import app
def test_get_v1_trips_endpoint():
    client = TestClient(app)
    app.dependency_overrides[time_range] = lambda: (
        date.fromisoformat("2024-02-01"),
        None,
)
    response = client.get("/v1/trips")
    assert (
        response.json()
        == "Request trips from 2024-02-01"
    )

通过覆盖time_range依赖,我们不需要在调用端点时传递参数,响应将取决于定义的 lambda 函数。

然后,你可以从命令行运行测试:

$ pytest tests

当编写不应干扰生产数据库的测试时,这项技术非常有用。如果测试不关心,最终的重计算逻辑也可以被模拟。

依赖注入的使用可以通过提高模块化程度显著提高测试质量。

它是如何工作的…

Depends对象和依赖注入利用 Python 强大的函数注解和类型提示功能。

当你定义一个依赖函数并用Depends进行注解时,FastAPI 将其解释为在执行端点函数之前需要解决的依赖。当对依赖于一个或多个依赖项的端点发出请求时,FastAPI 会检查端点函数的签名,识别依赖项,并通过调用正确的顺序中的相应依赖函数来解析它们。

FastAPI 使用 Python 的类型提示机制来确定每个依赖参数的类型,并自动将解析后的依赖注入到端点函数中。这个过程确保了所需的数据或服务在运行时对端点函数可用,从而实现了外部服务、数据库连接、身份验证机制和其他依赖与 FastAPI 应用程序的无缝集成。

总体而言,Depends类和 FastAPI 中的依赖注入提供了一种干净且高效的方式来管理依赖项,并促进模块化和可维护的代码架构。一个优点是它们可以在测试中重写,以便轻松模拟或替换。

还有更多…

我们可以通过利用子依赖来进一步推进。

让我们创建一个端点,该端点返回三个类别(巡游、城市之旅和度假住宿)之一的行程,并同时检查该类别的优惠券有效性。

dependencies.py模块中,让我们为该类别创建一个dependable函数。

想象一下,我们可以将我们的旅行分为三类——邮轮之旅、城市短途游和度假村住宿。我们需要添加一个参数来检索特定类别的旅行。我们需要一个dependable函数,如下所示:

def select_category(
    category: Annotated[
        str,
        Path(
            description=(
                "Kind of travel "
                "you are interested in"
            ),
            enum=[
                "Cruises",
                "City Breaks",
                "Resort Stay",
            ],
        ),
    ],
) -> str:
    return category

现在,让我们想象我们需要验证一个优惠券以获取折扣。

dependable函数将作为另一个用于检查优惠券的dependable函数的依赖项。让我们定义它,如下所示:

def check_coupon_validity(
    category: Annotated[select_category, Depends()],
    code: str | None = Query(
        None, description="Coupon code"
    ),
) -> bool:
    coupon_dict = {
        "cruises": "CRUISE10",
        "city-breaks": "CITYBREAK15",
        "resort-stays": "RESORT20",
    }
    if (
        code is not None
        and coupon_dict.get(category, ...) == code
    ):
        return True
    return False

main.py模块中,让我们定义一个新的端点,GET /v2/trips/{category},它返回指定类别的旅行:

@app.get("/v2/trips/{category}")
def get_trips_by_category(
    category: Annotated[select_category, Depends()],
    discount_applicable: Annotated[
        bool, Depends(check_coupon_validity)
    ],
):
    category = category.replace("-", " ").title()
    message = f"You requested {category} trips."
    if discount_applicable:
        message += (
            "\n. The coupon code is valid! "
            "You will get a discount!"
        )
    return message

如果你使用uvicorn app.main:app命令运行服务器,并在http://localhost:8000/docs打开交互式文档,你会看到新的端点。接受的参数categorycode都来自依赖项,并且category参数在代码中不会重复。

重要提示

你可以使用defasync def关键字来声明依赖项,无论是同步函数还是异步函数。FastAPI 将自动处理它们。

你刚刚创建了一个使用嵌套依赖项的端点。通过使用嵌套依赖项和子依赖项,你将能够编写清晰且模块化的代码,这使得代码更容易阅读和维护。

练习

在 FastAPI 中,依赖项也可以作为一个类来创建。查看fastapi.tiangolo.com/tutorial/dependencies/classes-as-dependencies/#classes-as-dependencies的文档,并创建一个新的端点,该端点使用我们在配方中定义的所有参数(time_rangecategorycode)。

将所有参数组合到一个类中,并定义和使用它作为端点的依赖项。

参见

我们已经使用了QueryPath描述符对象来设置querypath参数的元数据和文档相关数据。你可以在这些文档链接中了解更多关于它们潜力的信息:

对于 FastAPI 中的依赖注入,你可以找到广泛的文档,涵盖了所有可能的用法,解释了这一强大功能的潜力:

创建自定义中间件

中间件 是一个 API 组件,允许您拦截和修改传入的请求和传出的响应,使其成为实现跨切面关注点(如身份验证、日志记录和错误处理)的强大工具。

在本菜谱中,我们将探讨如何在 FastAPI 应用程序中开发自定义中间件以处理请求和响应,并检索客户端信息。

准备中…

您只需要有一个运行的 FastAPI 应用程序。菜谱将使用我们在之前的菜谱中定义的旅行平台,实现依赖注入。然而,中间件适用于通用运行的应用程序。

如何实现…

我们将通过以下步骤向您展示如何创建一个自定义中间件对象类,我们将在我们的应用程序中使用它。

  1. 让我们在应用文件夹中创建一个名为 middleware.py 的专用模块。

    我们希望中间件能够拦截请求并打印客户端的主机和方法到输出终端。在实际的应用场景中,这些信息可以存储在数据库中进行分析或用于安全检查。

    让我们使用 FastAPI 默认使用的相同 uvicorn 日志记录器:

    import logging
    logger = logging.getLogger("uvicorn.error")
    
  2. 然后,让我们创建我们的 ClientInfoMiddleware 类,如下所示:

    from fastapi import Request
    from starlette.middleware.base import BaseHTTPMiddleware
    class ClientInfoMiddleware(BaseHTTPMiddleware):
        async def dispatch(
            self, request: Request, call_next
        ):
            host_client = request.client.host
            requested_path = request.url.path
            method = request.method
            logger.info(
                f"host client {host_client} "
                f"requested {method} {requested_path} "
                "endpoint"
            )
            return await call_next(request)
    
  3. 然后,我们需要在 main.py 中将我们的中间件添加到 FastAPI 服务器。在定义应用服务器后,我们可以使用 add_middleware 方法添加中间件:

    # main.py import modules
    from app.middleware import ClientInfoMiddleware
    app = FastAPI()
    app.add_middleware(ClientInfoMiddleware)
    # rest of the code
    

现在,使用 uvicorn app.main:app 命令启动服务器,并尝试连接到 http://localhost:8000/v1/trips 的子路径。您甚至不需要调用现有的端点。您将在应用程序输出终端中看到日志消息:

INFO:host client 127.0.0.1 requested GET /v1/trips endpoint

您刚刚实现了一个基本的自定义中间件来检索客户端信息。您可以通过添加更多操作来增加其复杂性,例如根据 IP 重定向请求或集成 IP 阻止或过滤。

工作原理…

FastAPI 使用来自 Starlette 库的 BasicHTTPMiddleware 类。菜谱中展示的策略创建了一个从 BasicHTTPMiddleware 派生的类,它具有一个特定的 dispatch 方法,用于实现拦截操作。

要在 FastAPI 中创建中间件,您可以将 FastAPI 类方法中的一个装饰器添加到一个简单的函数上。然而,建议创建一个类,因为它允许更好的模块化和代码组织。通过创建一个类,您最终可以创建自己的中间件集合模块。

参考信息

您可以查看以下链接的官方文档页面,了解如何创建自定义中间件:

Stack Overflow 网站上可以找到一个有趣的讨论,关于如何在 FastAPI 中创建中间件类:

国际化和本地化

国际化i18n)和本地化l10n)是软件开发中的基本概念,它使得应用程序能够适应不同的语言、地区和文化。

i18n指的是设计和开发能够适应不同语言和文化的软件或产品的过程。这个过程主要涉及提供特定语言的内容。相反,l10n涉及将产品或内容适应特定地区或市场,例如货币或度量单位。

在我们的旅行平台中实现 i18n 和 l10n 的Accept-Language头。这将使我们的平台能够向客户端提供有针对性的内容。

准备工作

了解Accept-Language头信息会有所帮助;请查看 Mozilla 文档中的这篇有趣文章:developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language.

你需要有一个正在运行的 FastAPI 应用程序来遵循这个配方。你可以跟随本章中使用的旅行平台应用程序。

我们将使用依赖注入,因此完成本章的实现依赖注入配方会有所帮助。

此外,我们还将使用babel包来解析语言代码引用,所以如果你还没有通过requirements.txt文件安装这些包,请确保通过运行以下命令在你的环境中安装babel

$ pip install babel

安装完成后,你将拥有启动所需的一切。

如何实现它...

首先,我们必须确定我们希望服务哪些地区和语言。在这个例子中,我们将关注两个——美国英语en_US)和来自法国的法语fr_FR)。所有与语言相关的内 容都将使用这两种语言之一。

在主机客户端侧管理Accept-Language头是必要的,它是一个带有偏好权重参数的语言列表。

该头的示例如下:

Accept-Language: en
Accept-Language: en, fr
Accept-Language: en-US
Accept-Language: en-US;q=0.8, fr;q=0.5
Accept-Language: en, *
Accept-Language: en-US, en-GB
Accept-Language: zh-Hans-CN

我们需要一个函数,它接受头信息和我们的应用程序中可用的语言列表作为参数,并返回最合适的一个。让我们通过以下步骤来实现它。

  1. app文件夹下创建一个专门的模块,internationalization.py

    首先,我们将支持的语言存储在一个变量中,如下所示:

    SUPPORTED_LOCALES = [
        "en_US",
        "fr_FR",
    ]
    
  2. 然后,我们开始定义resolve_accept_language函数,如下所示:

    from babel import Locale, negotiate_locale
    def resolve_accept_language(
        accept_language: str = Header("en-US"),
    ) -> Locale:
    
  3. 在函数内部,我们将字符串解析成一个列表:

        client_locales = []
        for language_q in accept_language.split(","):
            if ";q=" in language_q:
                language, q = language_q.split(";q=")
            else:
                language, q = (language_q, float("inf"))
            try:
                Locale.parse(language, sep="-")
                client_locales.append(
                    (language, float(q))
                )
            except ValueError:
                continue
    
  4. 然后我们根据偏好q参数对字符串进行排序:

        client_locales.sort(
            key=lambda x: x[1], reverse=True
        )
        locales = [locale for locale, _ in client_locales]
    
  5. 然后,我们使用babel包中的negotiate_locale来获取最合适的语言:

        locale = negotiate_locale(
            [str(locale) for locale in locales],
            SUPPORTED_LOCALES,
        )
    
  6. 如果没有匹配项,我们返回默认的 en_US

        if locale is None:
            locale = "en_US"
        return locale
    

    resolve_accept_language 函数将被用作根据语言返回内容的端点的依赖项。

  7. 在相同的 internationalization.py 模块中,让我们创建一个返回欢迎字符串的 GET /homepage 端点,这个字符串取决于语言。我们将在一个单独的 APIRouter 中完成它,因此路由器将如下所示:

    from fastapi import APIRouter
    router = APIRouter(
        tags=["Localizad Content Endpoints"]
    )
    

    tags 参数指定路由器的端点将在交互式文档中根据指定的标签名称单独分组。

    GET /home 端点将如下所示:

    home_page_content = {
        "en_US": "Welcome to Trip Platform",
        "fr_FR": "Bienvenue sur Trip Platform",
    }
    @router.get("/homepage")
    async def home(
        request: Request,
        language: Annotated[
            resolve_accept_language, Depends()
        ],
    ):
        return {"message": home_page_content[language]}
    

    在示例中,内容已被硬编码为一个以语言代码为字典键的 dict 对象。

    在实际场景中,内容应该存储在数据库中,每个语言一个。

    类似地,你可以定义一个本地化策略来检索货币。

  8. 让我们创建一个 GET /show/currency 端点作为示例,该端点使用依赖项从 Accept-Language 标头检索货币。dependency 函数可以定义如下:

    async def get_currency(
        language: Annotated[
            resolve_accept_language, Depends()
        ],
    ):
        currencies = {
            "en_US": "USD",
            "fr_FR": "EUR",
        }
        return currencies[language]
    

    端点将如下所示:

    from babel.numbers import get_currency_name
    @router.get("/show/currency")
    async def show_currency(
        currency: Annotated[get_currency, Depends()],
        language: Annotated[
            resolve_accept_language,
            Depends(use_cache=True)
        ],
    ):
        currency_name = get_currency_name(
            currency, locale=language
        )
        return {
            "currency": currency,
            "currency_name": currency_name,
        }
    
  9. 要使用这两个端点,我们需要在 main.py 中将路由器添加到 FastAPI 对象中:

    from app import internationalization
    # rest of the code
    app.include_router(internationalization.router)
    

这就是实现国际化本地化的全部内容。要测试它,请在命令行中运行以下命令来启动服务器:

http:localhost:8000/docs, you will find the GET /homepage and GET /show/currency endpoints. Both accept the Accept-Language header to provide the language choice; if you don’t, it will get the default language from the browser. To test the implementation, try experimenting with different values for the header.
You have successfully implemented internationalization and localization from scratch for your API. Using the recipe provided, you have integrated i18n and l10n into your applications, making them easily understandable worldwide.
See also
You can find out more about the potential of `Babel` package on the official documentation page: [`babel.pocoo.org/en/latest/`](https://babel.pocoo.org/en/latest/).
Optimizing application performance
Optimizing FastAPI applications is crucial for ensuring high performance and scalability, especially under heavy loads.
In this recipe, we’ll see a technique to profile our FastAPI application and explore actionable strategies to optimize performances. By the end of the recipe, you will be able to detect code bottlenecks and optimize your application.
Getting ready
Before starting the recipe, make sure to have a FastAPI application running with some endpoints already set up. You can follow along with our trip platform application.
We will be using the `pyinstrument` package to set up a profiler for the application. If you haven’t installed the packages with `requirements.txt`, you can install `pyinstrument` in your environment by running the following:

$ pip install pyinstrument


 Also, it can be useful to have a look at the *Creating* *custom middleware* recipe from earlier in the chapter.
How to do it…
Let's implement the profiler in simple steps.

1.  Under the app folder, create a `profiler.py` module as follows:

    ```

    from pyinstrument import Profiler

    profiler = Profiler(

    interval=0.001, async_mode="enabled"

    )

    ```py

    The `async_mode="enabled"` parameter specifies that the profiler logs the time each time it encounters an `await` statement in the function being awaited, rather than observing other coroutines or the event loop. The `interval` specifies the time between two samples.

     2.  Before using the profiler, we should plan what we want to profile. Let’s plan to profile only the code executed in the endpoints. To do this, we can create simple middleware in a separate module that starts and stops the profiler before and after each call, respectively. We can create the middleware in the same `profiler.py` module, as follows:

    ```

    from starlette.middleware.base import (

    BaseHTTPMiddleware

    )

    class ProfileEndpointsMiddleWare(

    BaseHTTPMiddleware

    ):

    async def dispatch(

    self, request: Request, call_next

    ):

    if not profiler.is_running:

    profiler.start()

    response = await call_next(request)

    if profiler.is_running:

    profiler.stop()

    profiler.write_html(

    os.getcwd() + "/profiler.xhtml"

    )

    profiler.start()

    return response

    ```py

    The profiler is initiated every time an endpoint is requested, and it is terminated after the request is complete. However, since the server operates asynchronously, there is a possibility that the profiler may already be running, due to another endpoint request. This can result in errors during the start and stop of the profiler. To prevent this, we verify before each request whether the profiler is not already running. After the request, we check whether the profiler is running before terminating it.

     3.  You can attach the profiler to the FastAPI server by adding the middleware in the `main.py` module, as we did in the *Creating custom* *middleware* recipe:

    ```

    app.add_middleware(ProfileEndpointsMiddleWare)

    ```py

To test the profiler, spin up the server by running `uvicorn app.main:app`. Once you start making some calls, you can do it from the interactive documentation at http://localhost:8000/docs. Then, a `profiler.xhtml` file will be created. You can open the file with a simple browser and check the status of the code.
You have just integrated a profiler into your FastAPI application.
There’s more...
Integrating a profiler is the first step that allows you to spot code bottlenecks and optimize the performance of your application.
Let’s explore some techniques to optimize the performance of your FastAPI performances:

*   `Starlette` library and supports asynchronous request handlers, using the `async` and `await` keywords. By leveraging asynchronous programming, you can maximize CPU and **input/output** (**I/O**) utilization, reducing response times and improving scalability.
*   **Scaling Uvicorn workers**: Increasing the number of Uvicorn workers distributes incoming requests across multiple processes. However, it might not be always the best solution. For purely I/O operations, asynchronous programming massively reduces CPU usage, and additional workers remain idle. Before adding additional workers, check the CPU usage of the main process.
*   **Caching**: Implement caching mechanisms to store and reuse frequently accessed data, reducing database queries and computation overhead. Use dedicated libraries l to integrate caching into your FastAPI applications.

Other techniques are related to external libraries or tools, and whatever strategy you use, make sure to properly validate it with proper profiling configuration.
Also, for high-traffic testing, take a look at the *Performance testing for high traffic applications* recipe in *Chapter 5*, *Testing and Debugging* *FastAPI Applications*.
Exercise
We learned how to configure middleware to profile applications; however, it is more common to create tests to profile specific use cases. We learned how to configure middleware to profile applications; however, it is more common to create test scripts to profile specific use cases. Try to create one by yourself that attaches the profiler to the server, runs the server, makes API calls that reproduce the use case, and finally, writes the profiler output. The solution is provided on the GitHub repository in the `profiling_application.py` file. You can find it at [`github.com/PacktPublishing/FastAPI-Cookbook/blob/main/Chapter08/trip_platform/profiling_application.py`](https://github.com/PacktPublishing/FastAPI-Cookbook/blob/main/Chapter08/trip_platform/profiling_application.py).
See also
You can discover more about the potential of **pyinstrument** profiler on the official documentation:

*   *pyinstrument* *documentation*: [`pyinstrument.readthedocs.io/en/latest/`](https://pyinstrument.readthedocs.io/en/latest/)

Also, you can find a different approach to profile FastAPI endpoints on the page:

*   *pyinstrument – profiling FastAPI* *requests*: [`pyinstrument.readthedocs.io/en/latest/guide.xhtml#profile-a-web-request-in-fastapi`](https://pyinstrument.readthedocs.io/en/latest/guide.xhtml#profile-a-web-request-in-fastapi)

Implementing rate limiting
**Rate limiting** is an essential technique used to control and manage the flow of traffic to web applications, ensuring optimal performance, resource utilization, and protection against abuse or overload. In this recipe, we’ll explore how to implement rate limiting in FastAPI applications to safeguard against potential abuse, mitigate security risks, and optimize application responsiveness. By the end of this recipe, you’ll have a solid understanding of how to leverage rate limiting to enhance the security, reliability, and scalability of your FastAPI applications, ensuring optimal performance under varying traffic conditions and usage patterns.
Getting ready
To follow the recipe, you need a running FastAPI application with some endpoints to use for rate limiting. To implement rate limiting, we will use the `slowapi` package; if you haven’t installed the packages with the `requirements.txt` file provided in the GitHub repository, you can install `slowapi` in your environment with `pip` by running the following:

$ pip install slowapi


 Once the installation is completed, you are ready to start the recipe.
How to do it…
We will start by applying a rate limiter to a single endpoint in simple steps.

1.  Let’s create the `rate_limiter.py` module under the `app` folder that contains our limiter object class defined as follows:

    ```

    from slowapi import Limiter

    from slowapi.util import get_remote_address

    limiter = Limiter(

    key_func=get_remote_address,

    )

    ```py

    The limiter is designed to restrict the number of requests from a client based on their IP address. It is possible to create a function that can detect a user’s credentials and limit their calls according to their specific user profile. However, for the purpose of this example, we will use the client’s IP address to implement the limiter.

     2.  Now, we need to configure the FastAPI server to implement the limiter. In `main.py`, we have to add the following configuration:

    ```

    from slowapi import _rate_limit_exceeded_handler

    from slowapi.errors import RateLimitExceeded

    # 代码的其余部分

    app.state.limiter = limiter

    app.add_exception_handler(

    RateLimitExceeded, _rate_limit_exceeded_handler

    )

    # 代码的其余部分

    ```py

     3.  Now, we will apply a rate limit of two requests per minute to the `GET /homepage` endpoint defined in the `internalization.py` module:

    ```

    from fastapi import Request

    from app.rate_limiter import limiter

    @router.get("/homepage")

    @limiter.limit("2/minute")

    async def home(

    request: Request,

    language: Annotated[

    resolve_accept_language, Depends()

    ],

    ):

    return {"message": home_page_content[language]}

    ```py

    The rate limit is applied as a decorator. Also, the request parameter needs to be added to make the limiter work.

Now, spin up the server from the command line by running the following:

http://localhost:8000/homepage; 你将获得主页内容,第三次调用时,你将获得一个包含以下内容的 429 响应:

{
    "error": "Rate limit exceeded: 2 per 1 minute"
}

你刚刚为 GET /homepage 端点添加了速率限制。使用相同的策略,你可以为每个端点添加特定的速率限制器。

还有更多...

你可以通过添加全局速率限制到整个应用程序来做更多,如下所示。

main.py 中,你需要添加一个专门的中间件,如下所示:

# rest of the code in main.py
from slowapi.middleware import SlowAPIMiddleware
# rest of the code
app.add_exception_handler(
    RateLimitExceeded, _rate_limit_exceeded_handler
)
app.add_middleware(SlowAPIMiddleware)

然后,你只需在 rate_limiter.py 模块的 Limiter 对象实例化中指定默认限制即可:

limiter = Limiter(
    key_func=get_remote_address,
default_limits=["5/minute"],
)

就这样。现在,如果你重新启动服务器并连续调用任何端点超过五次,你将收到 429 响应。

你已经成功为你的 FastAPI 应用程序设置了一个全局速率限制器。

相关内容

你可以在官方文档中找到更多关于 Slowapi 功能的信息,例如共享限制、限制策略等,链接如下:

你可以在 Limits 项目文档中找到更多关于速率限制表示法语法的详细信息,链接如下:

实现背景任务

背景任务是一个有用的功能,它允许你将资源密集型操作委托给单独的进程。使用背景任务,你的应用程序可以保持响应性并同时处理多个请求。这对于处理长时间运行的过程而不阻塞主请求-响应周期尤为重要。这提高了应用程序的整体效率和可扩展性。在本教程中,我们将探讨如何在 FastAPI 应用程序中执行背景任务。

准备工作

要遵循这个教程,你只需要一个运行着至少一个端点以应用背景任务 FastAPI 应用程序。然而,我们将把背景任务实现到我们的行程平台中的 GET /v2/trips/{category} 端点,该端点在 实现依赖注入 教程中定义。

如何操作...

让我们假设我们想要将 GET /v2/trips/{category} 端点的消息存储在外部数据库中,用于分析目的。让我们分两步简单完成。

  1. 首先,我们定义一个函数,在 app 文件夹中的专用模块 background_tasks.py 中模拟存储操作。该函数看起来如下:

    import asyncio
    import logging
    logger = logging.getLogger("uvicorn.error")
    async def store_query_to_external_db(message: str):
        logger.info(f"Storing message '{message}'.")
        await asyncio.sleep(2)
        logger.info(f"Message '{message}' stored!")
    

    存储操作通过 asyncio.sleep 非阻塞操作进行模拟。我们还添加了一些日志消息以跟踪执行情况。

    1. 现在,我们需要将 store_query_to_external_db 函数作为我们端点的背景任务执行。在 main.py 中,让我们修改 GET /v2/trips/cruises,如下所示:
    from fastapi import BackgroundTasks
    @app.get("/v2/trips/{category}")
    def get_trips_by_category(
        background_tasks: BackgroundTasks,
        category: Annotated[select_category, Depends()],
        discount_applicable: Annotated[
            bool, Depends(check_coupon_validity)
        ],
    ):
        category = category.replace("-", " ").title()
        message = f"You requested {category} trips."
        if discount_applicable:
            message += (
                "\n. The coupon code is valid! "
                "You will get a discount!"
            )
        background_tasks.add_task(
            store_query_to_external_db, message
        )
        logger.info(
            "Query sent to background task, "
            "end of request."
        )
        return message
    

现在,如果你使用 uvicorn app.main:app 启动服务器并尝试调用 GET /v2/trips/cruises 端点,你将在终端输出中看到 store_query_to_external_db 函数的日志。

INFO:  Query sent to background task, end of request.
INFO:  127.0.0.1:58544 - "GET /v2/trips/cruises
INFO:  Storing message 'You requested Cruises trips.'
INFO:  Message 'You requested Cruises trips.' Stored!

这就是你在 FastAPI 中实现后台任务所需的所有内容!然而,如果你必须执行大量的后台计算,你可能想要使用专门的工具来处理队列任务执行。这将允许你在单独的进程中运行任务,避免在相同进程中运行时可能出现的任何性能问题。

它是如何工作的…

当对端点发起请求时,后台任务会被排队到BackgroundTasks对象。所有任务都会传递给事件循环,以便它们可以并发执行,从而允许非阻塞 I/O 操作。

如果你有一个需要大量处理能力且不一定需要由相同过程完成的任务,你可能想要考虑使用像 Celery 这样的大型工具。

参见

你可以在官方文档页面上的此链接找到更多关于在 FastAPI 中创建后台任务的信息:


第九章:使用 WebSocket

在现代 Web 应用程序中,实时通信变得越来越重要,它使得聊天、通知和实时更新等交互式功能成为可能。在本章中,我们将探索令人兴奋的 WebSocket 世界,以及如何在 FastAPI 应用程序中有效地利用它们。从设置 WebSocket 连接到实现聊天功能和错误处理等高级功能,本章提供了构建响应式、实时通信功能的全面指南。到本章结束时,你将具备在 FastAPI 应用程序中创建 WebSocket 并促进实时通信的技能,从而实现交互式功能和动态用户体验。

在本章中,我们将涵盖以下食谱:

  • 在 FastAPI 中设置 WebSockets

  • 通过 WebSocket 发送和接收消息

  • 处理 WebSocket 连接和断开

  • 处理 WebSocket 错误和异常

  • 使用 WebSocket 实现聊天功能

  • 优化 WebSocket 性能

  • 使用 OAuth2 保护 WebSocket 连接

技术要求

为了跟随 WebSocket 食谱,确保你的设置中包含以下基本要素:

  • Python:在你的环境中安装一个高于 3.9 版本的 Python。

  • FastAPI:应安装所有必需的依赖项。如果你在前几章中没有这样做,你可以简单地从你的终端执行:

    $ pip install fastapi[all]
    

本章中使用的代码托管在 GitHub 上,网址为github.com/PacktPublishing/FastAPI-Cookbook/tree/main/Chapter09

建议在项目根目录中为项目设置一个虚拟环境,以有效地管理依赖项并保持项目隔离。

在你的虚拟环境中,你可以通过使用 GitHub 仓库中项目文件夹提供的requirements.txt文件一次性安装所有依赖项:

$ pip install –r requirements.txt

由于交互式 Swagger 文档在撰写时有限,因此基本掌握Postman或其他测试 API 对于测试我们的 API 是有益的。

了解WebSockets的工作原理可能会有所帮助,尽管这不是必需的,因为食谱会引导你完成。

对于使用 WebSockets 实现聊天功能食谱,我们将编写一些基本的HTML,包括一些JavaScript代码。

在 FastAPI 中设置 WebSocket

WebSockets 提供了一种强大的机制,可以在客户端和服务器之间建立全双工通信通道,允许实时数据交换。在本食谱中,你将学习如何在 FastAPI 应用程序中建立 WebSocket 功能连接,以实现交互式和响应式的通信。

准备工作

在深入研究示例之前,请确保你的环境中已安装所有必需的包。你可以从 GitHub 仓库中提供的requirements.txt文件安装它们,或者使用pip手动安装:

$ pip install fastapi[all] websockets

由于 swagger 文档不支持 WebSocket,我们将使用外部工具来测试 WebSocket 连接,例如 Postman。

你可以在网站上找到如何安装它的说明:www.postman.com/downloads/.

免费社区版就足以测试这些示例。

如何操作…

创建一个名为chat_platform的项目根文件夹。我们可以在其中创建包含main.py模块的app文件夹。让我们按照以下方式构建我们的简单应用程序,其中包含 WebSocket 端点。

  1. 我们可以从在main.py模块中创建服务器开始:

    from fastapi import FastAPI
    app = FastAPI()
    
  2. 然后我们可以创建 WebSocket 端点以连接客户端到聊天室:

    from fastapi import WebSocket
    @app.websocket("/ws")
    async def ws_endpoint(websocket: WebSocket):
        await websocket.accept()
        await websocket.send_text(
            "Welcome to the chat room!"
        )
        await websocket.close()
    

    端点与客户端建立连接,发送欢迎消息,并关闭连接。这是 WebSocket 端点最基本的配置。

就这样。要测试它,从命令行启动服务器:

ws://localhost:8000/ws and click on Connect.
In the **Response** panel, right below the URL form, you should see the list of events that happened during the connection. In particular, look for the message received by the server:

欢迎来到聊天室!12:37:19


 That means that the WebSocket endpoint has been created and works properly.
How it works…
The `websocket` parameter in the WebSocket endpoint represents an individual WebSocket connection. By awaiting `websocket.accept()`, the server establishes the connection with the client (technically called an `websocket.send_text()` sends a message to the client. Finally, `websocket.close()` closes the connection.
The three events are listed in the **Response** panel of Postman.
Although not very useful from a practical point of view, this configuration is the bare minimum setup for a WebSocket connection. In the next recipe, we will see how to exchange messages between the client and the server through a WebSocket endpoint.
See also
You can check how to create a WebSocket endpoint on the FastAPI official documentation page:

*   *FastAPI* *WebSockets*: [`fastapi.tiangolo.com/advanced/websockets/`](https://fastapi.tiangolo.com/advanced/websockets/)

At the time of writing, the Swagger documentation does not support WebSocket endpoints. If you spin up the server and open Swagger at `http://localhost:8000/docs`, you won’t see the endpoint we have just created. A discussion is ongoing on the FastAPI GitHub repository – you can follow it at the following URL:

*   *FastAPI WebSocket Endpoints Documentation* *Discussion*: [`github.com/tiangolo/fastapi/discussions/7713`](https://github.com/tiangolo/fastapi/discussions/7713)

Sending and receiving messages over WebSockets
WebSocket connections enable bidirectional communication between clients and servers, allowing the real-time exchange of messages. This recipe will bring us one step closer to creating our chat application by showing how to enable the FastAPI application to receive messages over WebSockets and print them to the terminal output.
Getting ready
Before starting the recipe, make sure you know how to set up a **WebSocket** connection in **FastAPI**, as explained in the previous recipe. Also, you will need a tool to test WebSockets, such as **Postman**, on your machine.
How to do it…
We will enable our chatroom endpoint to receive messages from the client to print them to the standard output.
Let’s start by defining the logger. We will use the logger from the `uvicorn` package (as we did in other recipes – see, for example, *Creating custom middlewares* in *Chapter 8*, *Advanced Features and Best Practices*), which is the one used by FastAPI as well. In `main.py`, let’s write the following:

导入 logging

logger = logging.getLogger("uvicorn")


 Now let’s modify the `ws_endpoint` function endpoint:

@app.websocket("/ws")

async def ws_endpoint(websocket: WebSocket):

await websocket.accept()

await websocket.send_text(

"欢迎来到聊天室!"

)

while True:

data = await websocket.receive_text()

logger.info(f"Message received: {data}")

在上一个示例中,使用了websocket.close()调用并使用了一个无限循环。这允许服务器端持续接收来自客户端的消息并将其打印到控制台,而不会关闭连接。在这种情况下,只有客户端可以关闭连接。

这就是你需要从客户端读取消息并将其发送到终端输出的所有内容。

当客户端调用端点时,服务器会发起连接请求。使用websocket.receive_text()函数,服务器打开连接并准备好接收来自客户端的消息。消息存储在data变量中,并打印到终端输出。然后服务器向客户端发送确认消息。

让我们来测试一下。通过命令行运行uvicorn app.main:app启动服务器,然后打开 Postman。然后按照以下步骤操作。

  1. 创建一个新的 WebSocket 请求,并连接到ws://localhost:8000/ws地址。

    一旦建立连接,你将在终端输出中看到以下消息:

    Hello FastAPI application.On the output terminal you will the following message:
    
    

    INFO: Message received: Hello FastAPI application

    
    While in the messages section of the client request you will see the new message:
    
    

    消息已接收!14:46:20

    
    
    1. 然后,你可以通过点击 WebSocket URL字段右侧的断开连接按钮从客户端关闭连接。

通过使服务器能够接收来自客户端的消息,你刚刚通过 WebSocket 在客户端和服务器之间实现了双向通信。

参见

实际上,Fastapi.WebSocket实例是来自send_jsonreceive_json方法的starlette.WebSocket类。

更多信息请查看官方 Starlette 文档页面:

处理 WebSocket 连接和断开连接

当客户端与FastAPI服务器建立 WebSocket 连接时,适当地处理这些连接的生命周期至关重要。这包括接受传入的连接、维护活跃的连接以及优雅地处理断开连接,以确保客户端和服务器之间的通信顺畅。在这个配方中,我们将探讨如何有效地管理 WebSocket 连接并优雅地处理断开连接。

准备工作

要遵循这个配方,你需要有Postman或其他任何工具来测试 WebSocket 连接。此外,你需要在你的应用程序中已经实现了一个 WebSocket 端点。如果还没有,请检查前面的两个配方。

如何做到这一点...

我们将看到如何管理以下两种情况:

  • 客户端端断开连接

  • 服务器端断开连接

让我们详细查看这些情况中的每一个。

客户端端断开连接

你可能在通过 WebSockets 发送和接收消息的配方中注意到,如果连接从客户端(例如,从 Postman)在服务器控制台关闭,则会抛出一个未被捕获的WebSocketDisconnect异常。

这是因为客户端的断开连接应该在try-except块中适当处理。

让我们调整端点以考虑这一点。在main.py模块中,我们按照以下方式修改/ws端点:

from fastapi.websockets import WebSocketDisconnect
@app.websocket("/ws")
async def ws_endpoint(websocket: WebSocket):
    await websocket.accept()
    await websocket.send_text(
        "Welcome to the chat room!"
    )
    try:
        while True:
            data = await websocket.receive_text()
            logger.info(f"Message received: {data}")
    except WebSocketDisconnect:
        logger.warning(
            "Connection closed by the client"
/ws, and then disconnect, you won’t see the error propagation anymore.
Server-side disconnection
In this situation, the connection is closed by the server. Suppose the server will close the connection based on a specific message such as the `"disconnect"` text string, for example.
Let’s implement it in the `/``ws` endpoint:

@app.websocket("/ws")

async def ws_endpoint(websocket: WebSocket):

await websocket.accept()

await websocket.send_text(

"欢迎来到聊天室!"

)

while True:

data = await websocket.receive_text()

logger.info(f"收到消息: {data}")

if data == "disconnect":

logger.warn("断开连接...")

await websocket.close()

将数据字符串内容传递给 websocket.close 方法并退出 while 循环。

如果你运行服务器,尝试连接到 WebSocket /ws 端点,并发送"disconnect"字符串作为消息,服务器将关闭连接。

你已经看到了如何管理 WebSocket 端点的断开和连接握手,然而,我们仍然需要为每个端点管理正确的状态码和消息。让我们在下面的配方中查看这一点。

处理 WebSocket 错误和异常

WebSocket 连接容易受到在连接生命周期中可能发生的各种错误和异常的影响。常见问题包括连接失败、消息解析错误和意外的断开连接。正确处理错误并与客户端正确通信对于维护响应和健壮的基于 WebSocket 的应用程序至关重要。在这个菜谱中,我们将探讨如何在 FastAPI 应用程序中有效地处理 WebSocket 错误和异常。

准备工作

该菜谱将展示如何管理特定端点可能发生的 WebSocket 错误。我们将展示如何改进在 处理 WebSocket 连接和断开连接 菜谱中定义的 /ws 端点。

如何实现...

在前一个示例中,/ws 端点编码的方式在服务器关闭连接时返回相同的响应代码和消息。就像 HTTP 响应一样,FastAPI 允许你个性化响应,向客户端返回更有意义的消息。

让我们看看如何实现。你可以使用以下类似解决方案:

from fastapi import status
@app.websocket("/ws")
async def chatroom(websocket: WebSocket):
    if not websocket.headers.get("Authorization"):
        return await websocket.close()
    await websocket.accept()
    await websocket.send_text(
        "Welcome to the chat room!"
    )
    try:
        while True:
            data = await websocket.receive_text()
            logger.info(f"Message received: {data}")
            if data == "disconnect":
                logger.warn("Disconnecting...")
                return await websocket.close(
                    code=status.WS_1000_NORMAL_CLOSURE,
                    reason="Disconnecting...",
                )
    except WebSocketDisconnect:
        logger.warn("Connection closed by the client")

我们已经指定了 websocket.close 方法的状态码和原因,这些将被传输给客户端。

如果我们现在启动服务器并从客户端发送断开连接的消息,你将在响应窗口中看到断开连接的日志消息,如下所示:

Disconnected from localhost:8000/ws 14:09:08
1000 Normal Closure:  Disconnecting...

这就是你优雅地断开 WebSocket 连接所需的所有内容。

替代方案

类似于如何为 HTTP 请求渲染 HTTPException 实例(参见 第一章使用 FastAPI 的第一步)中的 处理错误和异常 菜谱,FastAPI 还允许使用 WebSocketException 来处理 WebSocket 连接,它将自动渲染为响应。

为了更好地理解,假设我们想要在客户端写入不允许的内容时断开连接——例如,"bad message" 文本字符串。让我们修改聊天室端点:

@app.websocket("/ws")
async def ws_endpoint(websocket: WebSocket):
    await websocket.accept()
    await websocket.send_text(
        "Welcome to the chat room!"
    )
    try:
        while True:
            data = await websocket.receive_text()
            logger.info(f"Message received: {data}")
            if data == "disconnect":
                logger.warn("Disconnecting...")
                return await websocket.close(
                    code=status.WS_1000_NORMAL_CLOSURE,
                    reason="Disconnecting...",
                )
            if "bad message" in data:
                raise WebSocketException(
                    code=status.WS_1008_POLICY_VIOLATION,
                    reason="Inappropriate message"
                )
    except WebSocketDisconnect:
        logger.warn("Connection closed by the client")

如果你启动服务器并尝试发送包含 "bad message" 字符串的消息,客户端将会断开连接。此外,在你的 WebSocket 连接的 Postman 的 响应 面板部分,你将看到以下日志消息:

Disconnected from localhost:8000/ws 14:51:40
1008 Policy Violation: Inappropriate message

你刚刚看到了如何通过抛出适当的异常来将 WebSocket 错误传达给客户端。你可以使用这种策略来处理在运行应用程序时可能出现的各种错误,并将它们正确地传达给 API 用户。

参见

与 HTTP 相比,WebSocket 是一种相对较新的协议,因此它仍在随着时间的推移而发展。尽管状态码不像 HTTP 那样被广泛使用,但你可以在以下链接中找到 WebSocket 代码的定义:

您还可以在以下页面找到浏览器 WebSocket 事件的兼容性列表:

此外,FastAPI 中的 WebSocketException 类在官方文档链接中有文档说明:

使用 WebSockets 实现聊天功能

实时聊天功能是许多现代网络应用程序的常见功能,使用户能够即时相互沟通。在本配方中,我们将探讨如何在 FastAPI 应用程序中使用 WebSockets 实现聊天功能。

通过利用 WebSockets,我们将在服务器和多个客户端之间创建双向通信通道,允许实时发送和接收消息。

准备工作

要遵循配方,您需要对 WebSockets 有良好的理解,并知道如何使用 FastAPI 构建 WebSocket 端点。

此外,具备一些基本的 HTML 和 JavaScript 知识可以帮助创建简单的网页,用于应用程序。我们将使用的配方是聊天应用程序的基础。

还将使用 jinja2 包为 HTML 页面应用基本模板。请确保它在您的环境中。如果您没有使用 requirements.txt 安装包,请使用 pip 安装 jinja2

$ pip install jinja2

安装完成后,我们就可以开始配方了。

如何操作…

要构建应用程序,我们需要构建三个核心组件——WebSocket 连接管理器、WebSocket 端点和聊天 HTML 页面:

  1. 让我们从构建连接管理器开始。连接管理器的角色是跟踪打开的 WebSocket 连接并向活跃的连接广播消息。让我们在 app 文件夹下的一个专用 ws_manager.py 模块中定义 ConnectionManager 类:

    import asyncio
    from fastapi import WebSocket
    class ConnectionManager:
        def __init__(self):
            self.active_connections: list[WebSocket] = []
        async def connect(self, websocket: WebSocket):
            await websocket.accept()
            self.active_connections.append(websocket)
        def disconnect(self, websocket: WebSocket):
            self.active_connections.remove(websocket)
        async def send_personal_message(
            self, message: dict, websocket: WebSocket
        ):
            await websocket.send_json(message)
        async def broadcast(
            self, message: json, exclude: WebSocket = None
        ):
            tasks = [
                connection.send_json(message)
                for connection in self.active_connections
                if connection != exclude
            ]
            await asyncio.gather(*tasks)
    

    async def connect 方法将负责握手并将 WebSocket 添加到活跃列表中。def disconnect 方法将从活跃连接列表中删除 WebSocket。async def send_personal_message 方法将向特定的 WebSocket 发送消息。最后,async def broadcast 将向所有活跃连接发送消息,除非指定了排除的连接。

    连接管理器将在聊天 WebSocket 端点中使用。

    1. 在一个名为 chat.py 的单独模块中创建 WebSocket 端点。让我们初始化连接管理器:
    from app.ws_manager import ConnectionManager
    conn_manager = ConnectionManager()
    

    然后我们定义路由器:

    from fastapi import APIRouter
    router = APIRouter()
    

    最后,我们可以定义 WebSocket 端点:

    from fastapi import WebSocket, WebSocketDisconnect
    @router.websocket("/chatroom/{username}")
    async def chatroom_endpoint(
        websocket: WebSocket, username: str
    ):
        await conn_manager.connect(websocket)
        await conn_manager.broadcast(
            f"{username} joined the chat",
            exclude=websocket,
        )
        try:
            while True:
                data = await websocket.receive_text()
                await conn_manager.broadcast(
                    {"sender": username, "message": data},
                    exclude=websocket,
                )
                await conn_manager.send_personal_message(
                    {"sender": "You", "message": data},
                    websocket,
                )
        except WebSocketDisconnect:
            conn_manager.disconnect(websocket)
            await connection_manager.broadcast(
                {
                    "sender": "system",
                    "message": f"Client #{username} "
                    "left the chat",
                }
            )
    
    1. 当新客户端加入聊天后,连接管理器会向所有聊天参与者发送消息,通知他们新成员的到来。端点使用 username 路径参数检索客户端的名称。别忘了在 main.py 文件中将路由器添加到 FastAPI 对象中:
    from app.chat import router as chat_router
    # rest of the code
    app = FastAPI()
    chatroom.xhtml should be stored in a templates folder in the project root. We will keep the page simple with the JavaScript tag embedded.The HTML part will look like this:
    
    

    <!doctype html>

    <head> <title>聊天</title> </head> <body>

    WebSocket 聊天

    您的 ID:

    <input

    type="text"

    id="messageText"

    autocomplete="off"

    />

    posted @ 2025-09-18 14:34  绝不原创的飞龙  阅读(87)  评论(0)    收藏  举报