Python-REST-Web-服务实用指南第二版-全-

Python REST Web 服务实用指南第二版(全)

原文:zh.annas-archive.org/md5/1277e719d0a203337610c227a9f451b4

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

REST(代表表示状态转移)是推动现代 Web 开发和移动应用开发的架构风格。实际上,开发和使用 RESTful Web 服务是任何现代软件开发工作的一项必备技能。有时,你必须与现有的 API 交互,在其他情况下,你必须从头设计一个 RESTful API 并使其与 JSON(代表 JavaScript 对象表示法)兼容。

Python 是最受欢迎的编程语言之一。Python 3.6 和 3.7 是 Python 最现代的版本。Python 是开源和多平台的,你可以用它来开发任何类型的应用程序,从网站到极其复杂的科学计算应用程序。总有一个 Python 包可以使事情变得更容易,避免重复造轮子并更快地解决问题。最重要的和最受欢迎的云计算提供商使使用 Python 及其相关 Web 框架变得容易。因此,Python 是开发 RESTful Web 服务的理想选择。本书涵盖了你需要知道的所有内容,以选择最合适的 Python Web 框架并从头开始开发 RESTful API。

你将使用四个最受欢迎的 Python Web 框架的最新版本,这些框架使开发 RESTful Web 服务变得容易:Flask、Django、Pyramid 和 Tornado。每个 Web 框架都有其优点和缺点。你将使用代表每个这些 Web 框架适当案例的示例,结合额外的 Python 包,这些包将简化最常见任务。你将学习如何使用不同的工具来测试和开发高质量、一致性和可扩展的 RESTful Web 服务。

你将编写单元测试并提高你将在本书中开发的 RESTful Web 服务的测试覆盖率。你不仅会运行示例代码;你还将确保为你的 RESTful API 编写测试。你将始终编写现代 Python 代码,并利用最新 Python 版本引入的功能。

本书将帮助你学习如何利用许多简化与 RESTful Web 服务相关的最常见任务的软件包。你将能够开始为任何领域创建自己的 RESTful API,无论是在 Python 3.6、3.7 或更高版本中覆盖的任何 Web 框架中。

本书面向对象

本书面向那些对 Python 有实际了解并希望利用 Python 的各种框架构建令人惊叹的 Web 服务的 Web 开发者。你应该对 RESTful API 有所了解。

为了充分利用本书

为了使用 Python 3.6 和 Python 3.7 的不同示例,你需要一台具有 Intel Core i3 或更高 CPU 和至少 4GB RAM 的计算机。你可以使用以下任何一种操作系统:

  • Windows 7 或更高版本(Windows 8、Windows 8.1 或 Windows 10)

  • Windows Server 2012 或更高版本(Windows Server 2016 或 Windows Server 2019)

  • macOS Mountain Lion 或更高版本

  • 任何能够运行 Python 3.7.1 的 Linux 版本以及任何支持 JavaScript 的现代浏览器

您需要在计算机上安装 Python 3.7.1 或更高版本。

下载示例代码文件

您可以从 www.packt.com 的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packt.com/support 并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packt.com 登录或注册。

  2. 选择“支持”选项卡。

  3. 点击“代码下载与勘误”。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

下载完成后,请确保您使用最新版本的软件解压缩或提取文件夹。

  • Windows 的 WinRAR/7-Zip

  • Mac 的 Zipeg/iZip/UnRarX

  • Linux 的 7-Zip/PeaZip

本书代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/Hands-On-RESTful-Python-Web-Services-Second-Edition ...

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表的彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781789532227_ColorImages.pdf

使用的约定

在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

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

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个示例:“我们可以通过使用 include 指令来包含其他上下文。”

代码块设置如下:

html, body, #map { height: 100%;  margin: 0; padding: 0}

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

[default]

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大价值的书籍。

要向我们发送一般反馈,请简单地发送电子邮件到 feedback@packtpub.com,并在邮件主题中提及书籍的标题。

如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南 www.packtpub.com/authors

客户支持

现在您已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从购买中获得最大价值。

联系我们

我们始终欢迎读者的反馈。

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

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

侵权: 如果您在互联网上发现任何形式的我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过发送链接至 copyright@packt.com 与我们联系。

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

勘误

尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详细信息。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看...

评论

请留下评论。一旦您阅读并使用了这本书,为什么不在您购买它的网站上留下评论呢?潜在的读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

有关 Packt 的更多信息,请访问 packt.com

侵权

在互联网上对版权材料的侵权是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过发送链接至 copyright@packt.com 与我们联系,以提供疑似侵权材料的链接。

我们感谢您的帮助,以保护我们的作者和提供有价值内容的能力。

问题

如果您对本书的任何方面有问题,您可以通过 questions@packtpub.com 与我们联系,我们将尽力解决问题。

第一章:使用 Flask 1.0.2 开发 RESTful API 和微服务

在本章中,我们将使用 Python 3.7 和四个不同的网络框架开始我们的 RESTful Web API 之旅。Python 是最受欢迎和最通用的编程语言之一。有成千上万的 Python 包,这些包允许你将 Python 的能力扩展到任何你能想象到的领域,例如网络开发、物联网IoT)、人工智能、机器学习和科学计算。我们可以使用许多不同的网络框架和包,用 Python 轻松构建简单和复杂的 RESTful Web API,并且我们可以将这些框架与其他 Python 包结合起来。

我们可以利用我们对 Python 及其所有 ... 的现有知识。

设计一个与简单数据源交互的 RESTful API

假设我们必须配置要在连接到物联网设备的 OLED(有机发光二极管)显示器上显示的通知消息。该物联网设备能够运行 Python 3.7.1、Flask 1.0.2 和其他 Python 包。有一个团队正在编写代码,从字典中检索表示通知的字符串消息,并在连接到物联网设备的 OLED 显示器上显示它们。我们必须开始开发一个移动应用和网站,它们必须与 RESTful API 交互,以执行表示通知的字符串消息的 CRUD 操作。

我们不需要 ORM(对象关系映射的缩写),因为我们不会在数据库中持久化通知。我们只需使用内存字典作为我们的数据源。这是我们对该 RESTful API 的一项要求。在这种情况下,RESTful 网络服务将在物联网设备上运行;也就是说,我们将在物联网设备上运行 Flask 开发服务。

由于我们在服务中有一个内存中的数据源,因此我们将失去我们的 RESTful API 的可伸缩性。然而,我们将与另一个相关联的例子一起工作,该例子涉及更复杂的数据源,稍后将以 RESTful 方式进行扩展。第一个例子将使我们能够了解 Flask 和 Flask-RESTful 如何与一个非常简单的内存数据源一起工作。

我们选择 Flask 是因为它是一个非常轻量级的框架,我们不需要配置 ORM,并且我们希望尽快在物联网设备上运行 RESTful API,以便所有团队都能与之交互。我们认为还将有一个用 Flask 编码的网站,因此,我们希望使用相同的网络微框架来驱动网站和 RESTful 网络服务。此外,Flask 是创建一个可以在云上运行我们的 RESTful API 的微服务的合适选择。

可用于 Flask 的扩展有很多,这些扩展使得使用 Flask 微型框架执行特定任务变得更加容易。我们将利用 Flask-RESTful 扩展,这个扩展将允许我们在构建 RESTful API 的同时鼓励最佳实践。在这种情况下,我们将使用 Python 字典作为数据源。正如之前所解释的,在未来的示例中,我们将使用更复杂的数据源。

首先,我们必须指定我们主要资源——通知的要求。对于通知,我们需要以下属性或字段:

  • 一个整数标识符。

  • 一个字符串消息。

  • 一个 TTL(即 生存时间),表示通知消息在 OLED 显示上显示的时间长度(以秒为单位)。

  • 创建日期和时间。当将新通知添加到集合时,时间戳将自动添加。

  • 一个通知类别描述,例如 警告信息

  • 一个整数计数器,表示通知消息在 OLED 显示上显示的次数。

  • 一个布尔值,表示通知消息是否至少在 OLED 显示上显示过一次。

以下表格显示了我们的 API 第一版必须支持的 HTTP 动词、作用域和语义。每个方法由一个 HTTP 动词和一个作用域组成,并且所有方法对所有通知和集合都有一个明确定义的意义。在我们的 API 中,每个通知都有自己的唯一 URL:

HTTP 动词 作用域 语义
GET 通知集合 获取集合中存储的所有通知。
GET 通知 获取单个通知。
POST 通知集合 在集合中创建一个新的通知。
PATCH 通知 更新现有通知的一个或多个字段。
DELETE 通知 删除现有的通知。

理解每个 HTTP 方法执行的任务

让我们考虑 http://localhost:5000/service/notifications/ 是通知集合的 URL。如果我们向之前的 URL 添加一个数字,我们就可以识别一个特定的通知,其 ID 等于指定的数值。例如,http://localhost:5000/service/notifications/5 识别 ID 等于 5 的通知。

我们希望我们的 API 能够在 URL 中区分集合和集合的单个资源。当我们提到集合时,我们将使用斜杠(/)作为 URL 的最后一个字符,例如 http://localhost:5000/service/notifications/。当我们提到集合的单个资源时,我们不会使用斜杠(/)作为 URL 的最后一个字符 ...

理解微服务

在过去几年中,许多大型且复杂的应用程序开始从单体架构转向微服务架构。微服务架构建议开发一系列较小、松散耦合的服务,以实现复杂应用程序所需的所有功能,这种方式既支持又简化了持续交付,而不是与大型且极其复杂的 Web 服务一起工作。

RESTful API 是微服务架构的必要组成部分,Python 在转向这种架构时非常流行。每个微服务可以封装一个 RESTful API,以实现特定的和有限的目的。微服务是自包含的,易于维护,并有助于支持持续交付。

就像任何架构一样,实现微服务架构有几种方法。我们将学习如何将使用 Flask 和 Python 开发的 RESTful API 封装到微服务中。这样,我们将能够通过开发 RESTful API 并使用它们作为构建自包含且易于维护的微服务的必要组件来利用我们的技能。

使用轻量级虚拟环境

在整本书中,我们将使用不同的框架、包和库来创建 RESTful Web API 和微服务,因此,使用 Python 虚拟环境来隔离每个开发环境是方便的。Python 3.3 引入了轻量级虚拟环境,并在后续的 Python 版本中得到了改进。我们将使用这些虚拟环境,因此,你需要 Python 3.7.1 或更高版本。你可以在www.python.org/dev/peps/pep-0405上阅读更多关于 PEP 405 Python 虚拟环境的介绍,它引入了venv模块。本书的所有示例都在 Linux、macOS 和 Windows 上的 Python 3.7.1 上进行了测试。

如果您...

使用 Flask 和 Flask-RESTful 设置虚拟环境

我们已经遵循了创建和激活虚拟环境的必要步骤。现在,我们将创建一个requirements.txt文件来指定我们的应用程序在任何支持平台上需要安装的包集合。这样,在任意新的虚拟环境中重复安装指定包及其版本将变得极其容易。

使用您喜欢的编辑器在最近创建的虚拟环境的根目录下创建一个名为requirements.txt的新文本文件。以下行显示了声明我们的 API 所需的包及其版本的文件内容。示例的代码文件包含在restful_python_2_01_01文件夹中的Flask01/requirements.txt文件中:

Flask==1.0.2 
flask-restful==0.3.6 
httpie==1.0.0

requirements.txt 文件中的每一行都指示需要安装的包和版本。在这种情况下,我们通过使用 == 操作符使用精确版本,因为我们想确保安装了指定的版本。以下表格总结了我们所指定的作为要求的包和版本号:

包名 要安装的版本
Flask 1.0.2
flask-restful 0.3.6
httpie 1.0.0

现在,我们必须在 macOS、Linux 或 Windows 上运行以下命令,使用 pip 通过最近创建的 requirements.txt 文件安装上一表格中解释的包和版本。请注意,Flask 是 Flask-RESTful 的依赖项。在运行以下命令之前,请确保您位于包含 requirements.txt 文件的文件夹中:

pip install -r requirements.txt     

输出的最后几行将指示所有成功安装的包,包括 Flaskflask-restfulhttpie

Installing collected packages: itsdangerous, click, MarkupSafe, Jinja2, Werkzeug, Flask, aniso8601, six, pytz, flask-restful, chardet, certifi, idna, urllib3, requests, Pygments, httpie
      Running setup.py install for itsdangerous ... done
      Running setup.py install for MarkupSafe ... done
Successfully installed Flask-1.0.2 Jinja2-2.10 MarkupSafe-1.0
Pygments-2.2.0 Werkzeug-0.14.1 aniso8601-3.0.2 certifi-2018.8.24 chardet-3.0.4 click-7.0 flask-restful-0.3.6 httpie-1.0.0 idna-2.7 itsdangerous-0.24 pytz-2018.5 requests-2.19.1 six-1.11.0 urllib3-1.23

使用可枚举的声明响应的状态码

Flask 和 Flask-RESTful 都不包括不同 HTTP 状态码的变量声明。我们不希望返回数字作为状态码。我们希望我们的代码易于阅读和理解,因此我们将使用描述性的 HTTP 状态码。具体来说,我们将利用 Python 3.4 中添加的对枚举的支持来声明一个类,该类定义了代表不同 HTTP 状态码的唯一名称和值集合。

首先,在最近创建的虚拟环境的根文件夹内创建一个 service 文件夹。在 service 文件夹内创建一个新的 http_status.py 文件。以下行显示了声明 HttpStatus ... 的代码。

创建模型

现在,我们将创建一个简单的 NotificationModel 类,我们将使用它来表示通知。请记住,我们不会在数据库或文件中持久化模型,因此在这种情况下,我们的类将仅提供所需的属性,而不提供映射信息。在 service 文件夹中创建一个新的 models.py 文件。以下行显示了在 service/models.py 文件中创建 NotificationModel 类的代码。示例代码文件包含在 restful_python_2_01_01 文件夹中,位于 Flask01/service/models.py 文件中:

class NotificationModel: 
    def __init__(self, message, ttl, creation_date, notification_category): 
        # We will automatically generate the new id 
        self.id = 0 
        self.message = message 
        self.ttl = ttl 
        self.creation_date = creation_date 
        self.notification_category = notification_category 
        self.displayed_times = 0 
        self.displayed_once = False 

NotificationModel 类仅声明了一个构造函数,即 __init__ 方法。此方法接收许多参数,并使用它们来初始化具有相同名称的属性:messagettlcreation_datenotification_categoryid 属性设置为 0displayed_times 设置为 0displayed_once 设置为 False。我们将自动通过 API 调用为每个新生成的通知递增标识符。

使用字典作为存储库

现在,我们将创建一个NotificationManager类,我们将使用它来在内存字典中持久化NotificationModel实例。我们的 API 方法将调用NotificationManager类的相关方法来检索、插入、更新和删除NotificationModel实例。在service文件夹中创建一个新的service.py文件。以下行显示了在service/service.py文件中创建NotificationManager类的代码。此外,以下行声明了我们将需要用于此文件中所有代码的所有导入。示例代码文件包含在restful_python_2_01_01文件夹中,位于Flask01/service/service.py文件:

from flask import Flask from flask_restful import ...

配置输出字段

现在,我们将创建一个notification_fields字典,我们将使用它来控制我们想要 Flask-RESTful 在返回NotificationModel实例时渲染的数据。打开之前创建的service/service.py文件,并将以下行添加到现有代码中。示例代码文件包含在restful_python_2_01_01文件夹中,位于Flask01/service/service.py文件:

notification_fields = { 
    'id': fields.Integer, 
    'uri': fields.Url('notification_endpoint'), 
    'message': fields.String, 
    'ttl': fields.Integer, 
    'creation_date': fields.DateTime, 
    'notification_category': fields.String, 
    'displayed_times': fields.Integer, 
    'displayed_once': fields.Boolean 
} 

notification_manager = NotificationManager() 

我们声明了notification_fields字典(dict),其中包含字符串和类的键值对,这些类是在flask_restful.fields模块中声明的。键是我们想要从NotificationModel类中渲染的属性名称,而值是格式化和返回字段值的类。在之前的代码中,我们使用了以下类来格式化和返回键中指定字段的值:

  • fields.Integer: 输出一个整数值。

  • fields.Url: 生成一个 URL 的字符串表示形式。默认情况下,此类为请求的资源生成一个相对 URI。代码指定了'notification_endpoint'作为endpoint参数。这样,该类将使用指定的端点名称。我们将在service.py文件中稍后声明此端点。我们不希望在生成的 URI 中包含主机名,因此我们使用absolute布尔属性的默认值,即False

  • fields.DateTime: 以 UTC 格式输出格式化的日期和时间字符串,默认为 RFC 822 格式。

  • fields.Boolean: 生成一个布尔值的字符串表示形式。

'uri'字段使用fields.Url,与指定的端点相关联,而不是与NotificationModel类的属性相关联。这是唯一一种指定字段名称在NotificationModel类中没有属性的情况。其他指定的字符串键表示我们希望在输出中渲染的所有属性,当我们使用notification_fields字典来组成最终的序列化响应输出时。

在我们声明notification_fields字典之后,下一行代码创建了一个之前创建的NotificationManager类的实例,命名为notification_manager。我们将使用此实例来创建、检索和删除NotificationModel实例。

在 Flask 可插拔视图之上进行资源路由操作

Flask-RESTful 使用基于 Flask 的可插拔视图构建的资源作为 RESTful API 的主要构建块。我们只需创建flask_restful.Resource类的子类并声明每个支持的 HTTP 动词的方法。

flask_restful.Resource的子类代表一个 RESTful 资源,因此我们将不得不声明一个类来表示通知集合,另一个类来表示通知资源。

首先,我们将创建一个Notification类,我们将使用它来表示通知资源。打开之前创建的service/service.py文件并添加以下行。示例的代码文件包含在 ...

配置资源路由和端点

我们必须通过定义 URL 规则来配置必要的资源路由,以调用适当的方法,并通过传递所有必要的参数。以下行创建应用程序的主要入口点,使用 Flask 应用程序初始化它,并配置服务的资源路由。打开之前创建的service/service.py文件并添加以下行。示例的代码文件包含在restful_python_2_01_01文件夹中的Flask01/service/service.py文件:

app = Flask(__name__) 
service = Api(app) 
service.add_resource(NotificationList, '/service/notifications/') 
service.add_resource(Notification, '/service/notifications/<int:id>', endpoint='notification_endpoint') 

if __name__ == '__main__': 
    app.run(debug=True) 

代码创建了一个flask_restful.Api类的实例,并将其保存在service变量中。每次调用service.add_resource方法都会将一个 URL 路由到一个资源,具体到之前声明的flask_restful.Resource超类的一个子类。当有请求到服务并且 URL 与service.add_resource方法中指定的 URL 之一匹配时,Flask 将调用与请求中指定的类中的 HTTP 动词匹配的方法。该方法遵循标准的 Flask 路由规则。

例如,以下行将发出一个不带任何附加参数的 HTTP GET请求到/service/notifications/,以调用NotificationList.get方法:

service.add_resource(NotificationList, '/service/notifications/') 

Flask 会将 URL 变量作为参数传递给被调用的方法。例如,以下行将发出一个 HTTP GET请求到/service/notifications/26以调用Notification.get方法,其中26作为id参数的值传递:

service.add_resource(Notification, '/service/notifications/<int:id>', endpoint='notification_endpoint')

此外,我们可以为端点参数指定一个字符串值,以便在fields.Url字段中轻松引用指定的路由。我们将相同的端点名称'notification_endpoint'作为notification_fields字典中声明的fields.Url字段的参数传递,我们使用该字典来渲染每个NotificationModel实例。这样,fields.Url将生成考虑此路由的 URI。

我们只需几行代码即可配置资源路由和端点。最后一行只是调用app.run方法来启动 Flask 应用程序,将debug参数设置为True以启用调试。在这种情况下,我们通过调用run方法立即启动本地服务器。我们也可以通过使用flask命令行脚本来达到相同的目的。然而,这个选项需要我们配置环境变量,而且在这个书中我们涵盖的平台(macOS、Windows 和 Linux)的说明是不同的。

与任何其他 Web 框架一样,你永远不应该在生产环境中启用调试。

向 Flask API 发送 HTTP 请求

现在,我们可以运行service/service.py脚本,该脚本启动 Flask 的开发服务器,以组合和发送 HTTP 请求到我们的未加密和简单的 Web API(我们肯定会添加安全功能)。执行以下命令。确保你已经激活了虚拟环境:

    python service/service.py

以下行显示了执行上一条命令后的输出。开发服务器正在监听端口5000

     * Serving Flask app "service" (lazy loading)
     * Environment: production
       WARNING: Do not use the development server in a production environment.
       Use a production WSGI server instead.
     * Debug mode: on
     * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
     * Restarting ...

使用 curl 和 httpie 命令行工具

我们将从命令行工具开始。命令行工具的一个关键优势是,我们可以在第一次构建 HTTP 请求后轻松地再次运行它们,我们不需要使用鼠标或触摸屏幕来运行请求。我们还可以轻松地构建一个包含批量请求的脚本并运行它们。与任何命令行工具一样,与 GUI 工具相比,第一次执行请求可能需要更多时间,但一旦我们执行了许多请求,它就会变得更容易,我们可以轻松地重用我们以前编写的命令来组合新的请求。

Curl,也称为cURL,是一个非常流行的开源命令行工具和库,它允许我们轻松地传输数据。我们可以使用curl命令行工具轻松地组合和发送 HTTP 请求并检查它们的响应。

在 macOS 或 Linux 中,你可以打开一个终端并从命令行开始使用curl

在 Windows 中,你可以在命令提示符中处理curl,或者你可以将curl作为 Cygwin 包安装选项的一部分进行安装,并在 Cygwin 终端中执行它。如果你决定在命令提示符中使用curl命令,请从curl.haxx.se/download.html下载并解压最新版本。然后,确保将包含curl.exe文件的文件夹包含在你的路径中,以便更容易运行命令。

你可以在cygwin.com/install.html了解更多关于 Cygwin 终端及其安装过程的信息。如果你决定使用 Cygwin 终端,每次你需要运行curl命令而不是使用命令提示符时,都要使用它。

注意,Windows PowerShell 包含一个名为 curl 的别名,该别名调用 Invoke-WebRequest 命令。因此,如果你决定使用 Windows PowerShell,你必须删除 curl 别名才能使用本书中使用的 curl 工具。

我们使用 requirements.txt 文件安装虚拟环境中的包。在这个文件中,我们将 httpie 指定为一个必需的包。这样,我们安装了 HTTPie,这是一个用 Python 编写的命令行 HTTP 客户端,它使得发送 HTTP 请求变得容易,并使用比 curl 更简单的语法。HTTPie 的一个巨大优点是它显示彩色输出,并使用多行来显示响应细节。因此,HTTPie 使得理解响应比 curl 工具更容易。然而,非常重要的一点是要提到 HTTPie 比 curl 慢。

每当我们使用命令行编写 HTTP 请求时,我们将使用同一命令的两个版本:第一个使用 HTTPie,第二个使用 curl。这样,您将能够使用最方便的一种。

确保您让 Flask 开发服务器继续运行。不要关闭运行此开发服务器的终端或命令提示符。在 macOS 或 Linux 中打开一个新的终端,或在 Windows 中打开一个命令提示符,并运行以下命令。非常重要的一点是,当指定时,您必须输入结束斜杠(/),因为 /service/notifications 不会匹配任何配置的 URL 路由。因此,我们必须输入 /service/notifications/,包括结束斜杠(/)。我们将组合并发送一个 HTTP 请求来创建一个新的通知。示例代码文件包含在 restful_python_2_01_02 文件夹中,位于 Flask01/cmd01.txt 文件:

http POST ":5000/service/notifications/" message='eSports competition starts in 2 minutes' ttl=20 notification_category='Information'

以下是对应的 curl 命令。非常重要的一点是使用 -H "Content-Type: application/json" 选项告诉 curl-d 选项之后指定的数据作为 application/json 发送,而不是默认的 application/x-www-form-urlencoded 选项。

示例代码文件包含在 restful_python_2_01_02 文件夹中,位于 Flask01/cmd02.txt 文件:

curl -iX POST -H "Content-Type: application/json" -d '{"message":"eSports competition starts in 2 minutes", "ttl":20, "notification_category": "Information"}' "localhost:5000/service/notifications/"

之前的命令将组合并发送带有以下 JSON 键值对的 POST http://localhost:5000/service/notifications/ HTTP 请求:

{  
   "message": "eSports competition starts in 2 minutes",  
   "ttl": 20,  
   "notification_category": "Information" 
} 

请求指定了 /service/notifications/,因此它将匹配 '/service/notifications/' 并运行 NotificationList.post 方法。由于 URL 路由不包含任何参数,该方法不接收任何参数。由于请求的 HTTP 动词是 POST,Flask 调用 post 方法。如果新的 NotificationModel 成功保存在字典中,函数将返回 HTTP 201 Created 状态码,并将最近持久化的 NotificationModel 序列化为 JSON 格式放在响应体中。以下行显示了 HTTP 请求的示例响应,其中包含 JSON 响应中的新 NotificationModel 对象:

    HTTP/1.0 201 CREATED
    Content-Length: 283
    Content-Type: application/json
    Date: Wed, 10 Oct 2018 01:01:44 GMT
    Server: Werkzeug/0.14.1 Python/3.7.1

    {
        "creation_date": "Wed, 10 Oct 2018 01:01:44 -0000",
        "displayed_once": false,
        "displayed_times": 0,
        "id": 1,
        "message": "eSports competition starts in 2 minutes",
        "notification_category": "Information",
        "ttl": 20,
        "uri": "/service/notifications/1"
    }

我们将组合并发送一个 HTTP 请求以创建另一个通知。返回到 Windows 的命令提示符,或在 macOS 或 Linux 的终端中运行以下命令。示例代码文件包含在 restful_python_2_01_02 文件夹中,在 Flask01/cmd03.txt 文件中:

http POST ":5000/service/notifications/" message='Ambient temperature is above the valid range' ttl=15 notification_category='Warning'

以下是对应的 curl 命令。示例代码文件包含在 restful_python_2_01_02 文件夹中,在 Flask01/cmd04.txt 文件中:

curl -iX POST -H "Content-Type: application/json" -d '{"message":"Ambient temperature is above the valid range", "ttl":15, "notification_category": "Warning"}' "localhost:5000/service/notifications/"

之前的命令将组合并发送带有以下 JSON 键值对的 POST http://localhost:5000/service/notifications/ HTTP 请求:

{  
   "message": "Ambient temperature is above the valid range",  
   "ttl": 15,  
   "notification_category": "Warning" 
} 

以下行显示了 HTTP 请求的示例响应,其中包含 JSON 响应中的新 NotificationModel 对象:

    HTTP/1.0 201 CREATED
    Content-Length: 280
    Content-Type: application/json
    Date: Wed, 10 Oct 2018 21:07:40 GMT
    Server: Werkzeug/0.14.1 Python/3.7.1

    {
        "creation_date": "Wed, 10 Oct 2018 21:07:40 -0000",
        "displayed_once": false,
        "displayed_times": 0,
        "id": 2,
        "message": "Ambient temperature is above valid range",
        "notification_category": "Warning",
        "ttl": 15,
        "uri": "/service/notifications/2"
    }

我们将组合并发送一个 HTTP 请求以检索所有通知。返回到 Windows 的命令提示符,或在 macOS 或 Linux 的终端中运行以下命令。示例代码文件包含在 restful_python_2_01_02 文件夹中,在 Flask01/cmd05.txt 文件中:

    http ":5000/service/notifications/"

以下是对应的 curl 命令。示例代码文件包含在 restful_python_2_01_02 文件夹中,在 Flask01/cmd06.txt 文件中:

    curl -iX GET "localhost:5000/service/notifications/"

之前的命令将组合并发送 GET http://localhost:5000/service/notifications/ HTTP 请求。该请求指定了 /service/notifications/,因此它将匹配 '/service/notifications/' 并运行 NotificationList.get 方法。由于 URL 路由不包含任何参数,该方法不接收任何参数。由于请求的 HTTP 动词是 GET,Flask 调用 get 方法。该方法检索所有 NotificationModel 对象,并生成包含所有这些 NotificationModel 对象序列化的 JSON 响应。

以下行显示了 HTTP 请求的示例响应。前几行显示了 HTTP 响应头,包括状态(200 OK)和内容类型(application/json)。在 HTTP 响应头之后,我们可以看到 JSON 响应中两个 NotificationModel 对象的详细信息:

    HTTP/1.0 200 OK
    Content-Length: 648
    Content-Type: application/json
    Date: Wed, 10 Oct 2018 21:09:43 GMT
    Server: Werkzeug/0.14.1 Python/3.7.1

    [
        {
            "creation_date": "Wed, 10 Oct 2018 21:07:31 -0000",
            "displayed_once": false,
            "displayed_times": 0,
            "id": 1,
            "message": "eSports competition starts in 2 minutes",
            "notification_category": "Information",
            "ttl": 20,
            "uri": "/service/notifications/1"
        },
        {
            "creation_date": "Wed, 10 Oct 2018 21:07:40 -0000",
            "displayed_once": false,
            "displayed_times": 0,
            "id": 2,
            "message": "Ambient temperature is above valid range",
            "notification_category": "Warning",
            "ttl": 15,
            "uri": "/service/notifications/2"
        }
    ]

在我们运行了三个请求之后,我们将在运行 Flask 开发服务器的窗口中看到以下行。输出表明服务接收了三个 HTTP 请求,具体是两个 POST 请求和一个带有 /service/notifications/ 作为 URI 的 GET 请求。服务处理了这三个 HTTP 请求,并返回了前两个请求的 201 状态码和最后一个请求的 200 状态码:

127.0.0.1 - - [10/Oct/2018 18:07:31] "POST /service/notifications/ HTTP/1.1" 201 -
127.0.0.1 - - [10/Oct/2018 18:07:40] "POST /service/notifications/ HTTP/1.1" 201 -
127.0.0.1 - - [10/Oct/2018 18:09:43] "GET /service/notifications/ HTTP/1.1" 200 -

以下截图显示了 macOS 上并排的两个终端窗口。左侧的终端窗口正在运行 Flask 开发服务器并显示接收和处理的 HTTP 请求。右侧的终端窗口正在运行 http 命令以生成 HTTP 请求。在我们组合和发送 HTTP 请求时,使用类似的配置来检查输出是一个好主意:

图片

现在,我们将编写并发送一个 HTTP 请求来检索一个不存在的通知。例如,在上一个列表中,没有id值等于78的通知。运行以下命令尝试检索此通知。确保你使用一个不存在的id值。我们必须确保工具将标题作为响应的一部分显示,以查看返回的状态码。示例代码文件包含在restful_python_2_01_02文件夹中的Flask01/cmd07.txt文件中:

    http ":5000/service/notifications/78" 

以下是对应的curl命令。示例代码文件包含在restful_python_2_01_02文件夹中的Flask01/cmd08.txt文件中:

    curl -iX GET "localhost:5000/service/notifications/78"

之前的命令将编写并发送GET http://localhost:5000/service/notifications/78 HTTP 请求。该请求与之前分析的那个相同,只是id参数的数字不同。服务将运行Notification.get方法,将78作为id参数的值。该方法将执行检索与作为参数接收的id值匹配的NotificationModel对象的代码。然而,NotificationList.get方法中的第一行调用了abort_if_notification_not_found方法,它无法在字典键中找到 ID,并将调用flask_restful.abort函数,因为没有指定id值的该通知。因此,代码将返回 HTTP 404 Not Found状态码。以下行显示了 HTTP 请求的示例响应头和体中包含的消息。在这种情况下,我们只是保留默认消息。当然,我们可以根据我们的具体需求进行自定义:

    HTTP/1.0 404 NOT FOUND
    Content-Length: 155
    Content-Type: application/json
    Date: Wed, 10 Oct 2018 21:24:32 GMT
    Server: Werkzeug/0.14.1 Python/3.7.1

    {
        "message": "Notification 78 not found. You have requested this     
    URI [/service/notifications/78] but did you mean 
    /service/notifications/<int:id> ?"
    }

我们为PATCH方法提供了一个实现,以便我们的 API 能够更新现有资源的单个字段。例如,我们可以使用PATCH方法更新现有通知的两个字段,并将displayed_once字段的值设置为true,将displayed_times设置为1。我们不希望使用PUT方法,因为这个方法旨在替换整个通知。

PATCH方法旨在对现有通知应用一个增量,因此它是仅更改displayed_oncedisplayed_times字段值的适当方法。

现在,我们将编写并发送一个 HTTP 请求来更新现有的通知,具体来说,是更新两个字段的值。确保你将配置中的2替换为现有通知的 ID。示例代码文件包含在restful_python_2_01_02文件夹中的Flask01/cmd09.txt文件中:

http PATCH ":5000/service/notifications/2" displayed_once=true 
displayed_times=1

以下是对应的curl命令。示例代码文件包含在restful_python_2_01_02文件夹中的Flask01/cmd10.txt文件中:

curl -iX PATCH -H "Content-Type: application/json" -d '{"displayed_once":"true", "displayed_times":1}' "localhost:5000/service/notifications/2"

之前的命令将编写并发送一个带有指定 JSON 键值对的 PATCH HTTP 请求。请求在 /service/notifications/ 后面有一个数字,因此它将匹配 '/service/notifications/<int:id>' 并运行 Notification.patch 方法,即 Notification 类的 patch 方法。如果存在具有指定 ID 的 NotificationModel 实例并且已成功更新,该方法调用将返回 HTTP 200 OK 状态码,并且最近更新的 NotificationModel 实例序列化为 JSON 格式在响应体中。以下行显示了示例响应:

HTTP/1.0 200 OK 
Content-Length: 279 
Content-Type: application/json 
Date: Thu, 11 Oct 2018 02:15:13 GMT 
Server: Werkzeug/0.14.1 Python/3.7.1 

{ 
    "creation_date": "Thu, 11 Oct 2018 02:15:05 -0000", 
    "displayed_once": true, 
    "displayed_times": 1, 
    "id": 2, 
    "message": "Ambient temperature is above valid range", 
    "notification_category": "Warning", 
    "ttl": 15, 
    "uri": "/service/notifications/2" 
} 

当物联网设备首次显示通知时,它将执行之前解释的 HTTP 请求。然后,它将执行额外的 PATCH 请求来更新 displayed_times 字段的值。

现在,我们将编写并发送一个 HTTP 请求来删除一个现有的通知,具体来说,是我们最后添加的那个。就像我们之前的 HTTP 请求一样,我们必须检查前一个响应中分配给 id 的值,并将命令中的 2 替换为返回的值。示例的代码文件包含在 restful_python_2_01_02 文件夹中的 Flask01/cmd11.txt 文件里:

    http DELETE ":5000/service/notifications/2"

以下是对应的 curl 命令。示例的代码文件包含在 restful_python_2_01_02 文件夹中的 Flask01/cmd12.txt 文件里:

    curl -iX DELETE "localhost:5000/service/notifications/2"

之前的命令将编写并发送 DELETE http://localhost:5000/service/notifications/2 的 HTTP 请求。请求在 /service/notifications/ 后面有一个数字,因此它将匹配 '/service/notifications/<int:id>' 并运行 Notification.delete 方法,即 Notification 类的 delete 方法。如果存在具有指定 ID 的 NotificationModel 实例并且已成功删除,该方法调用将返回 HTTP 204 No Content 状态码。以下行显示了示例响应:

    HTTP/1.0 204 NO CONTENT
    Content-Length: 3
    Content-Type: application/json
    Date: Thu, 11 Oct 2018 02:22:09 GMT
    Server: Werkzeug/0.14.1 Python/3.7.1

使用 GUI 工具 – Postman 及其他

到目前为止,我们一直在使用两个基于终端的或命令行工具来编写并发送 HTTP 请求到我们的 Flask 开发服务器:cURL 和 HTTPie。现在,我们将使用一个 GUI(代表 图形用户界面)工具。

Postman 是一个非常流行的 API 测试套件 GUI 工具,它允许我们轻松地编写并发送 HTTP 请求,以及其他功能。Postman 可作为 Chrome App 和 Macintosh App 提供。我们可以在 Windows、Linux 和 macOS 上作为原生应用执行它。您可以在 www.getpostman.com/apps 下载 Postman 应用程序的版本。

您可以免费下载并安装 Postman 来编写并发送 HTTP 请求到我们的 RESTful API。您只需注册 ...

使用其他编程语言消费 API

我们已经构建了我们第一个 RESTful Web Service,它能够使用 Flask 和 Python 作为微服务运行。我们可以使用任何现代编程语言来消费 API,这些语言能够组合并发送 HTTP 请求到 API 支持的资源和动词,并且可以轻松地处理 JSON 内容。

确保我们在使用curlhttp命令行工具时设置 HTTP 请求的内容类型非常重要。我们只需要检查在我们要使用的编程语言中,哪种方式最为方便。

我们可以轻松运行 Flask 开发服务器并检查其控制台输出,每当处理新的请求时,这使得检查到达服务器的请求变得容易。在这种情况下,我们正在处理一个基本且未加密的 API。然而,在接下来的章节中,我们将处理安全且更高级的 API。

测试你的知识

让我们看看你是否能正确回答以下问题:

  1. HTTPie 是一个:

    1. 用 Python 编写的命令行 HTTP 服务器,使得创建 RESTful Web Server 变得容易

    2. 命令行实用工具,允许我们对 SQLite 数据库运行查询

    3. 用 Python 编写的命令行 HTTP 客户端,使得组合和发送 HTTP 请求变得容易

  2. Flask-RESTful 使用以下哪个作为 RESTful API 的主要构建块:

    1. 建立在 Flask 可插拔视图之上的资源

    2. 建立在 Flask 资源视图之上的状态

    3. 建立在 Flask 可插拔控制器之上的资源

  3. 要处理资源上的 HTTP PATCH请求,我们应该在flask_restful.Resource的子类中声明哪个方法?

    1. patch_restful ...

摘要

在本章中,我们设计了一个 RESTful API 来与一个简单的字典交互,该字典充当数据存储库并执行与通知相关的 CRUD 操作,作为微服务的基线。我们定义了 API 的要求并理解了每个 HTTP 方法执行的任务。我们使用 Flask 和 Flask-RESTful 设置了一个虚拟环境。我们遵循最佳实践来生成可重复的虚拟环境。

我们创建了一个模型来表示和持久化通知。我们学习了如何使用 Flask-RESTful 中的功能配置通知的序列化为 JSON 表示。我们编写了代表资源并处理不同 HTTP 请求的类,并配置了 URL 模式将 URL 路由到类。

最后,我们启动了 Flask 开发服务器,并使用命令行工具来组合和发送 HTTP 请求到我们的 RESTful API,并分析了我们的代码中如何处理每个 HTTP 请求。我们还使用了许多有用的 GUI 工具来组合和发送 HTTP 请求。

现在我们已经了解了如何结合 Flask 和 Flask-RESTful 创建可以封装在微服务中的 RESTful API 的基础知识,在下一章中,我们将通过利用 Flask-RESTful 和相关 ORM 包含的高级功能来扩展 RESTful Web API 的功能。

第二章:在 Flask 中使用模型、SQLAlchemy 和超链接 API 进行工作

在本章中,我们将扩展上一章开始构建的 RESTful API 的功能。我们将使用 SQLAlchemy 作为我们的 ORM 来与 PostgreSQL 数据库交互,并且我们将利用 Flask 和 Flask-RESTful 中包含的先进功能,这将使我们能够轻松组织代码以处理复杂的 API,如模型和蓝图。

在本章中,我们将讨论以下主题:

  • 设计一个与 PostgreSQL 10.5 数据库交互的 RESTful API

  • 理解每个 HTTP 方法执行的任务

  • 使用 requirements.txt 文件安装包以简化我们的常见任务

  • 创建数据库

  • 配置数据库

  • 为模型编写代码,包括它们的 ...

设计一个与 PostgreSQL 10.5 数据库交互的 RESTful API

到目前为止,我们的 RESTful API 已经在充当数据存储库的简单内存字典上执行了 CRUD 操作。该字典永远不会持久化,因此,每次我们重新启动 Flask 开发服务器时,数据都会丢失。

现在,我们想要使用 Flask RESTful 创建一个更复杂的 RESTful API,以便与一个数据库模型交互,该模型允许我们处理分组到通知类别中的通知。在我们的上一个 RESTful API 中,我们使用一个字符串属性来指定通知的通知类别。在这种情况下,我们希望能够轻松检索属于特定通知类别的所有通知,因此,我们将有一个通知与通知类别之间的关系。

我们必须能够对不同的相关资源和资源集合执行 CRUD 操作。以下表格列出了我们将创建以表示模型的资源和类名:

资源 代表模型的类名
通知类别 NotificationCategory
通知 Notification

通知类别(NotificationCategory)只需要以下数据:

  • 一个整数标识符

  • 一个字符串名称

我们需要一个通知(Notification)的以下数据:

  • 一个整数标识符

  • 一个指向通知类别(NotificationCategory)的外键

  • 一个字符串消息

  • 一个 TTL(即 Time to Live,表示 生存时间),即指示通知消息在 OLED 显示上显示的秒数

  • 一个创建日期和时间。时间戳将在将新通知添加到集合时自动添加

  • 一个整数计数器,表示通知消息在 OLED 显示上显示的次数

  • 一个布尔值,表示通知消息是否至少在 OLED 显示上显示过一次

我们将利用许多与 Flask RESTful 和 SQLAlchemy 相关的包,这些包使得序列化和反序列化数据、执行验证以及将 SQLAlchemy 与 Flask 和 Flask RESTful 集成变得更加容易。这样,我们将减少样板代码。

理解每个 HTTP 方法执行的任务

以下表格显示了我们的新 API 必须支持的方法的 HTTP 动词、作用域和语义。每个方法由一个 HTTP 动词和一个作用域组成,并且所有方法对所有资源和集合都有明确的含义:

HTTP 动词 作用域 语义
GET 通知类别集合 获取集合中存储的所有通知类别,按名称升序排序。每个通知类别必须包含资源的完整 URL。此外,每个通知类别必须包含一个列表,其中包含属于该类别的所有通知的详细信息。通知不必包含 ...

使用 requirements.txt 文件安装包以简化我们的常见任务

确保您已退出 Flask 的开发服务器。您只需在运行它的终端或命令提示符窗口中按 Ctrl + C 即可。

现在,我们将安装一些额外的包。请确保您已激活我们在上一章中创建并命名为 Flask01 的虚拟环境。激活虚拟环境后,就是运行大量命令的时候了,这些命令对 macOS、Linux 或 Windows 都是一样的。

现在,我们将编辑现有的 requirements.txt 文件,以指定我们的应用程序在任何支持平台上需要安装的额外包集。这样,在任意新的虚拟环境中重复安装指定包及其版本将变得极其容易。

使用您喜欢的编辑器编辑虚拟环境根目录下名为 requirements.txt 的现有文本文件。在最后一行之后添加以下行,以声明 API 新版本所需的额外包及其版本。示例代码文件包含在 restful_python_2_02_01 文件夹中,位于 Flask01/requirements.txt 文件中:

Flask-SQLAlchemy==2.3.2 
Flask-Migrate==2.2.1 
marshmallow==2.16.0 
marshmallow-sqlalchemy==0.14.1 
flask-marshmallow==0.9.0 
psycopg2==2.7.5

requirements.txt 文件中添加的每一行都表示需要安装的包及其版本。以下表格总结了我们作为对先前包含的包的额外要求指定的包及其版本号:

包名 要安装的版本
Flask-SQLAlchemy 2.3.2
Flask-Migrate 2.2.1
marshmallow 2.16.0
marshmallow-sqlalchemy 0.14.1
flask-marshmallow 0.9.0
psycopg2 2.7.5

Flask-SQLAlchemy 为 Flask 应用程序添加了对 SQLAlchemy ORM 的支持。这个扩展简化了在 Flask 应用程序中执行常见的 SQLAlchemy 任务。SQLAlchemy 是 Flask-SQLAlchemy 的依赖项。

Flask-Migrate 使用 Alembic 包来处理 Flask 应用程序的 SQLAlchemy 数据库迁移。我们将使用 Flask-Migrate 来设置我们的 PostgreSQL 数据库。

如果您之前使用过 Flask-Migrate 的先前版本,请注意,Flask-Script 已不再是 Flask-Migrate 的依赖项。Flask-Script 是一个流行的包,它为 Flask 添加了编写外部脚本的支持,包括设置数据库的脚本。最新的 Flask 版本在虚拟环境中安装了 flask 脚本和基于 Click 包的命令行界面。因此,不再需要将 Flask-Migrate 与 Flask-Script 结合使用。

Marshmallow 是一个轻量级库,用于将复杂的数据类型转换为原生 Python 数据类型,反之亦然。Marshmallow 提供了模式,我们可以使用它们来验证输入数据,将输入数据反序列化为应用级别的对象,以及将应用级别的对象序列化为 Python 原始类型。

marshmallow-sqlalchemy 提供了与之前安装的 marshmallow 验证、序列化和反序列化轻量级库的 SQLAlchemy 集成。

Flask-Marshmallow 将之前安装的 marshmallow 库与 Flask 应用程序集成,使得生成 URL 和超链接字段变得简单易行。

Psycopg 2 (psycopg2) 是一个 Python-PostgreSQL 数据库适配器,SQLAlchemy 将使用它来与我们的最近创建的 PostgreSQL 数据库交互。同样,在运行此包的安装之前,确保 PostgreSQL 的 bin 文件夹包含在 PATH 环境变量中是非常重要的。

现在,我们必须在 macOS、Linux 或 Windows 上运行以下命令来安装先前表格中解释的附加包和版本,使用 pip 通过最近编辑的 requirements 文件。在运行命令之前,请确保您位于包含 requirements.txt 文件的文件夹中:

    pip install -r requirements.txt

输出的最后几行将指示所有新安装的包及其依赖项已成功安装。如果您下载了示例的源代码,并且您没有使用 API 的先前版本,pip 还将安装 requirements.txt 文件中包含的其他包:

Installing collected packages: SQLAlchemy, Flask-SQLAlchemy, Mako, python-editor, python-dateutil, alembic, Flask-Migrate, marshmallow, marshmallow-sqlalchemy, flask-marshmallow, psycopg2
      Running setup.py install for SQLAlchemy ... done
      Running setup.py install for Mako ... done
      Running setup.py install for python-editor ... done
Successfully installed Flask-Migrate-2.2.1 Flask-SQLAlchemy-2.3.2
Mako-1.0.7 SQLAlchemy-1.2.12 alembic-1.0.0 flask-marshmallow-0.9.0 marshmallow-2.16.0 marshmallow-sqlalchemy-0.14.1 psycopg2-2.7.5 
python-dateutil-2.7.3 python-editor-1.0.3

创建数据库

现在,我们将创建一个 PostgreSQL 10.5 数据库,我们将使用它作为我们的 API 的存储库。如果您还没有在您的计算机或开发服务器上运行 PostgreSQL 数据库服务器,您将需要下载并安装它。您可以从其网页(www.postgresql.org)下载并安装这个数据库管理系统。如果您使用的是 macOS,Postgres.app 提供了一种非常简单的方法来安装和使用 PostgreSQL。您可以从 postgresapp.com 参考它。如果您使用的是 Windows,EnterpriseDB 和 BigSQL 提供了图形安装程序,这些安装程序简化了在现代 Windows 服务器或桌面版本上的配置过程(访问 www.postgresql.org/download/windows)。

配置数据库

如果你使用的是我们为之前示例创建的相同虚拟环境,或者你下载了代码示例,那么service文件夹已经存在。如果你创建了一个新的虚拟环境,请在虚拟环境根文件夹内创建一个名为service的文件夹。

service文件夹内创建一个新的config.py文件。以下行展示了声明用于确定 Flask 和 SQLAlchemy 配置的变量的代码。SQL_ALCHEMY_DATABASE_URI变量生成用于 PostgreSQL 数据库的 SQLAlchemy URI。确保你在DB_NAME的值中指定所需的数据库名称,并根据你的 PostgreSQL 配置配置用户、密码、主机和端口。如果你遵循了之前的步骤,请使用这些步骤中指定的设置。示例代码文件包含在restful_python_2_02_01文件夹中,位于Flask01/service/config.py文件中:

import os 

basedir = os.path.abspath(os.path.dirname(__file__)) 
SQLALCHEMY_ECHO = False 
SQLALCHEMY_TRACK_MODIFICATIONS = True 
# Replace your_user_name with the user name you configured for the database 
# Replace your_password with the password you specified for the database user 
SQLALCHEMY_DATABASE_URI = "postgresql://{DB_USER}:{DB_PASS}@{DB_ADDR}/{DB_NAME}".format(DB_USER="your_user_name", DB_PASS="your_password", DB_ADDR="127.0.0.1", DB_NAME="flask_notifications") 
SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository')

我们将指定之前创建的模块(config)作为创建 Flask 应用的函数的参数。这样,我们有一个模块指定了与 SQLAlchemy 相关的所有不同配置变量的值,另一个模块创建 Flask 应用。我们将创建 Flask 应用工厂作为我们迈向新 API 的最终步骤。

创建具有其关系的模型

现在,我们将创建我们将用于在 PostgreSQL 数据库中表示和持久化通知类别、通知及其关系的模型。

打开service/models.py文件,并用以下代码替换其内容。代码中声明与其它模型相关字段的行被突出显示。如果你创建了一个新的虚拟环境,请在service文件夹内创建一个新的models.py文件。示例代码文件包含在restful_python_2_02_01文件夹中,位于Flask01/service/models.py文件中:

from marshmallow import Schema, fields, pre_load from marshmallow import validate from flask_sqlalchemy import SQLAlchemy from flask_marshmallow import ...

创建用于验证、序列化和反序列化模型的模式

现在,我们将创建我们将用于验证、序列化和反序列化之前声明的NotificationCategoryNotification模型及其关系的 Flask-Marshmallow 模式。

打开service文件夹内的models.py文件,并在最后一行之后添加以下代码。代码中声明与其它模式相关字段的行被突出显示。示例代码文件包含在restful_python_2_02_01文件夹中,位于Flask01/service/models.py文件中:

class NotificationCategorySchema(ma.Schema): 
    id = fields.Integer(dump_only=True) 
    # Minimum length = 3 characters 
    name = fields.String(required=True,  
        validate=validate.Length(3)) 
    url = ma.URLFor('service.notificationcategoryresource',  
        id='<id>',  
        _external=True) 
 notifications = fields.Nested('NotificationSchema',             
      many=True,         
      exclude=('notification_category',)) 

class NotificationSchema(ma.Schema): 
    id = fields.Integer(dump_only=True) 
    # Minimum length = 5 characters 
    message = fields.String(required=True,  
        validate=validate.Length(5)) 
    ttl = fields.Integer() 
    creation_date = fields.DateTime() 
 notification_category =
fields.Nested(NotificationCategorySchema,
         only=['id', 'url', 'name'],
         required=True) 
    displayed_times = fields.Integer() 
    displayed_once = fields.Boolean() 
    url = ma.URLFor('service.notificationresource',  
        id='<id>',  
        _external=True) 

    @pre_load 
    def process_notification_category(self, data): 
        notification_category = data.get('notification_category') 
        if notification_category: 
            if isinstance(notification_category, dict): 
                notification_category_name = notification_category.get('name') 
            else: 
                notification_category_name = notification_category 
            notification_category_dict = dict(name=notification_category_name) 
        else: 
            notification_category_dict = {} 
        data['notification_category'] = notification_category_dict 
        return data 

代码声明了以下两个模式,即ma.Schema类的两个子类:

  • NotificationCategorySchema

  • NotificationSchema

我们不使用 Flask-Marshmallow 允许我们根据模型中声明的字段自动确定每个属性适当类型的特性,因为我们想为每个字段使用特定的选项。

我们将表示字段的属性声明为marshmallow.fields模块中声明的适当类的实例。每当我们将dump_only参数指定为True时,这意味着我们希望该字段为只读。例如,我们无法在任何模式中为id字段提供值。该字段的值将由 PostgreSQL 数据库中的自增主键自动生成。

NotificationCategorySchema类将name属性声明为fields.String类的一个实例。required参数设置为True,以指定该字段不能为空字符串。validate参数设置为validate.Length(3),以指定该字段必须至少有三个字符的长度。

类使用以下行声明了url字段:

url = ma.URLFor('service.notificacion_categoryresource', 
    id='<id>', 
    _external=True)

url属性是ma.URLFor类的一个实例,并且这个字段将输出资源的完整 URL,即通知类别的 URL。第一个参数是 Flask 端点的名称:'service.notificationcategoryresource'。我们将在稍后创建NotificationCategoryResource类,URLFor类将使用它来生成 URL。id参数指定'<id>',因为我们希望从要序列化的对象中提取id。小于(<)和大于(>)符号内的id字符串指定我们希望从必须序列化的对象中提取字段。_external属性设置为True,因为我们希望生成资源的完整 URL。这样,每次序列化NotificationCategory时,它都会在url键或属性中包含资源的完整 URL。

在这种情况下,我们正在使用不安全的 API 在 HTTP 后面。如果我们的 API 配置为 HTTPS,那么在创建ma.URLFor实例时,我们应该将_scheme参数设置为'https'

类使用以下行声明了notifications字段:

notifications = fields.Nested('NotificationSchema', 
    many=True, 
    exclude=('notification_category',)) 

notifications属性是marshmallow.fields.Nested类的一个实例,并且这个字段将嵌套一个Schema集合,因此,我们为many参数指定True。第一个参数指定嵌套Schema类的名称为一个字符串。我们在定义了NotificationCategorySchema类之后声明NotificationSchema类。因此,我们指定Schema类名为一个字符串,而不是使用我们尚未定义的类型。

事实上,我们将得到两个相互嵌套的对象;也就是说,我们将在通知类别和通知之间创建双向嵌套。我们使用一个字符串元组作为exclude参数,以指示我们希望notification_category字段从为每个通知序列化的字段中排除。这样,我们避免了无限递归,因为包含notification_category字段将序列化与该类别相关的所有通知。

当我们声明Notification模型时,我们使用了orm.relationship函数来提供对NotificationCategory模型的多对一关系。backref参数指定了一个调用orm.backref函数的调用,其中'notifications'作为第一个值,表示从相关的NotificationCategory对象返回到Notification对象的关系名称。通过之前解释的行,我们创建了使用我们为db.backref函数指定的相同名称的notifications字段。

NotificationSchema类将notification属性声明为fields.String类的一个实例。required参数设置为True,以指定该字段不能为空字符串。validate参数设置为validate.Length(5),以指定该字段必须至少有五个字符长。该类使用与我们在Message模型中使用的类型相对应的类声明了ttlcreation_datedisplayed_timesdisplayed_once字段。

类使用以下行声明了notification_category字段:

notification_category = fields.Nested(CategorySchema,  
    only=['id', 'url', 'name'],  
    required=True) 

notification_category属性是marshmallow.fields.Nested类的一个实例,并且这个字段将嵌套一个NotificationCategorySchema。我们为required参数指定True,因为通知必须属于一个类别。第一个参数指定了嵌套Schema类的名称。我们已声明了NotificationCategorySchema类,因此我们将NotificationCategorySchema指定为第一个参数的值。我们使用带有字符串列表的only参数来指示在序列化嵌套的NotificationCategorySchema时要包含的字段名称。我们希望包含idurlname字段。我们没有指定notifications字段,因为我们不希望通知类别序列化属于它的通知列表。

类使用以下行声明了url字段:

url = ma.URLFor('service.notificationresource',  
    id='<id>',  
    _external=True)

url属性是ma.URLFor类的一个实例,并且这个字段将输出资源的完整 URL,即通知资源的 URL。第一个参数是 Flask 端点名称:'service.notificationresource'。我们稍后会创建NotificationResource类,URLFor类将使用它来生成 URL。id参数指定为'<id>',因为我们希望从要序列化的对象中提取id_external属性设置为True,因为我们希望为资源生成完整的 URL。这样,每次我们序列化一个Notification时,它都会在url键中包含资源的完整 URL。

NotificationSchema 类声明了一个 process_notification_category 方法,该方法使用 @pre_load 装饰器,具体来说,是 marshmallow.pre_load。这个装饰器注册了一个在反序列化对象之前调用的方法。这样,在 Marshmallow 反序列化通知之前,process_category 方法将被执行。

该方法接收 data 参数中的要反序列化的数据,并返回处理后的数据。当我们收到一个请求以 POST 新通知时,通知类别名称可以指定为名为 'notification_category' 的键。如果存在具有指定名称的类别,我们将使用现有的类别作为与新的通知相关联的类别。如果不存在具有指定名称的类别,我们将创建一个新的通知类别,然后我们将使用这个新类别作为与新的通知相关联的类别。这样,我们使用户创建与类别相关的新通知变得简单直接。

data 参数可能包含一个指定为 'notification_category' 键的字符串形式的通知类别名称。然而,在其他情况下,'notification_category' 键将包含具有字段名称和字段值的键值对,这些值对应于现有的通知类别。

process_notification_category 方法中的代码检查 'notification_category' 键的值,并返回一个包含适当数据的字典,以确保我们能够使用适当的键值对反序列化通知类别,无论传入数据之间的差异如何。最后,该方法返回处理后的字典。当我们在开始组合和发送对新 API 的 HTTP 请求时,我们将深入了解 process_notification_category 方法所做的工作。

将蓝图与资源路由相结合

现在,我们将创建组成我们 RESTful API 主要构建块的资源。首先,我们将创建一些将在不同资源中使用的实例。在 services 文件夹内创建一个新的 views.py 文件,并添加以下行。注意,代码导入了在上一章中创建的 http_status.py 模块中声明的 HttpStatus 枚举。示例代码文件包含在 restful_python_2_02_01 文件夹中,位于 Flask01/service/views.py 文件:

from flask import Blueprint, request, jsonify, make_response from flask_restful import Api, Resource from http_status import HttpStatus from models import orm, NotificationCategory, NotificationCategorySchema, ...

理解和配置资源路由

下表显示了我们要为每个 HTTP 动词和范围组合执行的先前创建的类的操作方法:

HTTP 动词 范围 类和方法
GET 通知集合 NotificationListResource.get
GET 通知 NotificationResource.get
POST 通知集合 NotificationListResource.post
PATCH 通知 NotificationResource.patch
DELETE 通知 NotificationResource.delete
GET 通知类别集合 NotificationCategoryListResource.get
GET 通知类别 NotificationCategoryResource.get
POST 通知类别集合 NotificationCategoryListResource.post
PATCH 通知类别 NotificationCategoryResource.patch
DELETE 通知类别 NotificationCategoryResource.delete

如果请求导致调用一个不支持 HTTP 方法的资源,Flask-RESTful 将返回一个带有 HTTP 405 Method Not Allowed 状态码的响应。

我们必须通过定义 URL 规则来进行必要的资源路由配置,以调用适当的方法,并通过传递所有必要的参数。以下行配置了服务的资源路由。在 service 文件夹中打开之前创建的 views.py 文件,并在最后一行之后添加以下代码。示例的代码文件包含在 restful_python_2_02_01 文件夹中,位于 Flask01/service/views.py 文件:

service.add_resource(NotificationCategoryListResource,  
    '/notification_categories/') 
service.add_resource(NotificationCategoryResource,  
    '/notification_categories/<int:id>') 
service.add_resource(NotificationListResource,  
    '/notifications/') 
service.add_resource(NotificationResource,  
    '/notifications/<int:id>')

每次调用 service.add_resource 方法都会将一个 URL 路由到一个资源;具体来说,是到之前声明的 flask_restful.Resource 超类的一个先前声明的子类。每当有 API 请求,并且 URL 与 service.add_resource 方法中指定的 URL 之一匹配时,Flask 将调用与请求中指定的类匹配的 HTTP 动词的方法。

注册蓝图和运行迁移

service 文件夹中创建一个新的 app.py 文件。以下行显示了创建 Flask 应用程序的代码。示例的代码文件包含在 restful_python_2_02_01 文件夹中,位于 Flask01/service/app.py 文件:

from flask import Flask 
from flask_sqlalchemy import SQLAlchemy 
from flask_migrate import Migrate 
from models import orm 
from views import service_blueprint 

def create_app(config_filename): 
    app = Flask(__name__) 
    app.config.from_object(config_filename) 
    orm.init_app(app) 
    app.register_blueprint(service_blueprint, url_prefix='/service') 
    migrate = Migrate(app, orm) 
    return app 

app = create_app('config') 

service/app.py 文件中的代码声明了一个 create_app 函数...

验证 PostgreSQL 数据库的内容

在我们运行前面的脚本之后,我们可以使用 PostgreSQL 命令行或任何允许我们轻松验证 PostgreSQL 10.5 数据库内容的其他应用程序来检查迁移生成的表。

运行以下命令以列出生成的表。如果您使用的数据库名称不是 flask_notifications,请确保您使用适当的数据库名称。示例的代码文件包含在 restful_python_2_02_01 文件夹中,位于 Flask01/list_database_tables.sql 文件:

psql --username=your_user_name --dbname=flask_notifications --command="\dt"

以下行显示了所有生成的表名的输出:


      **                    List of relations**
 **Schema |         Name          | Type  |     Owner** 
      **--------+-----------------------+-------+----------------** ** public | alembic_version       | table | your_user_name** ** public | notification          | table | your_user_name** ** public | notification_category | table | your_user_name** **(3 rows)**

SQLAlchemy 根据我们模型中包含的信息生成了以下两个表,具有唯一约束和外键:

  • notification_category:此表持久化 NotificationCategory 模型。

  • notification:此表持久化 Notification 模型。

以下命令将在我们向 RESTful API 发送 HTTP 请求并执行两个表上的 CRUD 操作后,允许你检查两个表的内容。这些命令假设你在运行命令的同一台计算机上运行 PostgreSQL 10.5。示例代码文件包含在restful_python_2_02_01文件夹中的Flask01/check_tables_contents.sql文件中:

psql --username=your_user_name --dbname=flask_notifications --command="SELECT * FROM notification_category;"
psql --username=your_user_name --dbname=flask_notifications --command="SELECT * FROM notification;"

而不是使用 PostgreSQL 的命令行工具,你可以使用你喜欢的 GUI 工具来检查 PostgreSQL 数据库的内容。

Alembic 生成了一个名为alembic_version的额外表,该表在version_num列中保存数据库的版本号。这个表使得迁移命令能够检索数据库的当前版本,并根据我们的需求升级或降级。

创建和检索相关资源

现在,我们将使用flask脚本启动 Flask 的开发服务器和我们的 RESTful API。我们想启用调试模式,因此我们将FLASK_ENV环境变量的值设置为development

在 Linux 或 macOS 的 bash shell 中的终端运行以下命令:

    export FLASK_ENV=development

在 Windows 中,如果你正在使用命令提示符,请运行以下命令:

    set FLASK_ENV=development

在 Windows 中,如果你正在使用 Windows PowerShell,请运行以下命令:

    $env:FLASK_ENV = "development"

现在,运行启动 Flask 开发服务器和应用程序的flask脚本。

现在已经将FLASK_ENV环境变量配置为在开发模式下工作...

测试你的知识

让我们看看你是否能正确回答以下问题:

  1. 以下哪个命令启动 Flask 开发服务器和 Flask 应用程序,并使其在5000端口上监听所有接口?

    1. flask run -h 0.0.0.0

    2. flask run -p 0.0.0.0 -h 5000

    3. flask run -p 0.0.0.0

  2. Flask-Migrate是:

    1. 一个轻量级的库,用于将复杂的数据类型转换为原生 Python 数据类型,以及从原生 Python 数据类型转换回复杂的数据类型。

    2. 一个使用 Alembic 包来处理 Flask 应用程序的 SQLAlchemy 数据库迁移的库。

    3. 一个替代 SQLAlchemy 以在 PostgreSQL 上运行查询的库。

  3. Marshmallow 是:

    1. 一个轻量级的库,用于将复杂的数据类型转换为和从原生 Python 数据类型。

    2. 一个 ORM。

    3. 一个轻量级的 Web 框架,用于替代 Flask。

  4. SQLAlchemy 是:

    1. 一个轻量级的库,用于将复杂的数据类型转换为和从原生 Python 数据类型。

    2. 一个 ORM。

    3. 一个轻量级的 Web 框架,用于替代 Flask。

  5. marshmallow.pre_load装饰器:

    1. Resource类的任何实例创建后注册一个要调用的方法。

    2. 在序列化对象后注册一个要调用的方法。

    3. 在反序列化对象之前注册一个要调用的方法。

  6. Schema子类的任何实例的dump方法:

    1. 将 URL 路由到 Python 原语。

    2. 将作为参数传递的实例或实例集合持久化到数据库中。

    3. 接收作为参数传递的实例或实例集合,并将Schema子类中指定的字段过滤和输出格式应用于实例或实例集合。

  7. 当我们将属性声明为marshmallow.fields.Nested类的实例时:

    1. 该字段将根据many参数的值嵌套单个SchemaSchema集合。

    2. 该字段将嵌套单个Schema。如果我们想嵌套Schema集合,我们必须使用marshmallow.fields.NestedCollection类的实例。

    3. 该字段将嵌套一个Schema集合。如果我们想嵌套单个Schema,我们必须使用marshmallow.fields.NestedSingle类的实例。

摘要

在本章中,我们扩展了上一章中创建的 RESTful API 的前一个版本的功能。我们使用 SQLAlchemy 作为我们的 ORM 来与 PostgreSQL 10.5 数据库一起工作。我们添加了许多包来简化许多常见任务,我们为模型及其关系编写了代码,并与模式一起工作以验证、序列化和反序列化这些模型。

我们将蓝图与资源路由相结合,从而能够从模型生成数据库。我们向 RESTful API 发送了许多 HTTP 请求,并分析了我们的代码中每个 HTTP 请求的处理方式以及模型在数据库表中的持久化情况。

现在我们已经使用 Flask、Flask-RESTful 和 SQLAlchemy 构建了一个复杂的 API,...

第三章:使用 Flask 改进我们的 API 并为其添加身份验证

在本章中,我们将改进上一章开始构建的 RESTful API 的功能,并为其添加与身份验证相关的安全功能。我们将执行以下操作:

  • 改进模型中的唯一约束

  • 理解 PUTPATCH 方法之间的区别

  • 使用 PATCH 方法更新资源的字段

  • 编写一个通用的分页类

  • 为 API 添加分页功能

  • 理解添加身份验证和权限的步骤

  • 添加用户模型

  • 创建一个模式来验证、序列化和反序列化用户

  • 为资源添加身份验证

  • 创建用于处理用户的资源类

  • 运行迁移以生成用户表

  • 使用必要的身份验证来编写请求

改进模型中的唯一约束

在上一章中我们编码了 NotificationCategory 模型时,我们在创建名为 nameorm.Column 实例时,为 unique 参数指定了 True 值。因此,迁移过程生成了必要的唯一约束,以确保 notification_category 表中的 name 字段具有唯一值。这样,PostgreSQL 数据库将不允许我们为 notification_category.name 列插入重复值。然而,当我们尝试这样做时生成的错误消息并不清晰。该消息包含了不应该在错误消息中提到的数据库结构详情。

运行以下命令以创建一个具有重复 ... 的类别

理解 PUT 和 PATCH 方法之间的区别

HTTP 的 PUTPATCH 方法有不同的目的。HTTP 的 PUT 方法旨在替换整个资源。HTTP 的 PATCH 方法旨在对现有资源应用一个增量。

我们的 API 能够更新现有资源的单个字段,因此我们提供了 PATCH 方法的实现。例如,我们可以使用 PATCH 方法来更新现有的通知,并将它的 displayed_oncedisplayed_times 字段的值设置为 true1

我们不想使用 PUT 方法来更新两个字段,因为这个方法旨在替换整个通知。PATCH 方法旨在对现有通知应用一个增量,因此它是仅更改这两个字段值的适当方法。

使用 PATCH 方法更新资源的字段

现在我们将编写并发送一个 HTTP 请求来更新现有的通知,具体来说,是更新 displayed_oncedisplayed_times 字段的值。因为我们只想更新两个字段,所以我们将使用 PATCH 方法而不是 PUT。确保将 2 替换为您配置中现有通知的 ID 或主键。示例代码文件包含在 restful_python_2_03_01 文件夹中的 Flask01/cmd307.txt 文件:

http PATCH ":5000/service/notifications/2" displayed_once=true displayed_times=1

以下是对应的curl命令。示例代码文件包含在restful_python_2_03_01文件夹中,位于Flask01/cmd308.txt ...

编写通用分页类

目前,在数据库中持久化通知的表只有几行。然而,在我们开始在真实的生产环境中使用封装在微服务中的 API 后,我们将有数百条通知,因此,我们必须处理大量结果集。我们不希望 HTTP GET请求在一次调用中检索 1,000 条通知。因此,我们将创建一个通用分页类,并使用它来轻松指定我们想要如何将大量结果集拆分为单个数据页。

首先,我们将编写并发送 HTTP POST请求来创建属于我们已创建的通知类别之一(Information)的九条通知。这样,我们将总共在数据库中保留 12 条消息。我们原本有三条消息,然后又添加了九条。示例代码文件包含在restful_python_2_03_01文件夹中,位于Flask01/cmd309.txt文件中:

http POST ":5000/service/notifications/" message='Clash Royale has a new winner' ttl=25 notification_category='Information'
http POST ":5000/service/notifications/" message='Uncharted 4 has a new 2nd position score' ttl=20 notification_category='Information'
http POST ":5000/service/notifications/" message='Fortnite has a new 4th position score' ttl=18 notification_category='Information'
http POST ":5000/service/notifications/" message='Injustice 2 has a new winner' ttl=14 notification_category='Information'
http POST ":5000/service/notifications/" message='PvZ Garden Warfare 2 has a new winner' ttl=22 notification_category='Information'
http POST ":5000/service/notifications/" message='Madden NFL 19 has a new 3rd position score' ttl=15 notification_category='Information'
http POST ":5000/service/notifications/" message='Madden NFL 19 has a new winner' ttl=18 notification_category='Information'
http POST ":5000/service/notifications/" message='FIFA 19 has a new 3rd position score' ttl=16 notification_category='Information'
http POST ":5000/service/notifications/" message='NBA Live 19 has a new winner' ttl=5 notification_category='Information'

以下是对应的curl命令。示例代码文件包含在restful_python_2_03_01文件夹中,位于Flask01/cmd310.txt文件中。

curl -iX POST -H "Content-Type: application/json" -d '{"message":"Clash Royale has a new winner", "ttl":25, "notification_category": "Information"}'
"localhost:5000/service/notifications/"

curl -iX POST -H "Content-Type: application/json" -d '{"message":"Uncharted 4 has a new 2nd position score", "ttl":20, "notification_category": "Information"}' "localhost:5000/service/notifications/"

curl -iX POST -H "Content-Type: application/json" -d '{"message":"Fortnite has a new 4th position score", "ttl":18, "notification_category": "Information"}' "localhost:5000/service/notifications/"

curl -iX POST -H "Content-Type: application/json" -d '{"message":"Injustice 2 has a new winner", "ttl":14, "notification_category": "Information"}' "localhost:5000/service/notifications/"

curl -iX POST -H "Content-Type: application/json" -d '{"message":"PvZ Garden Warfare 2 has a new winner", "ttl":22, "notification_category": "Information"}'
"localhost:5000/service/notifications/"

curl -iX POST -H "Content-Type: application/json" -d '{"message":"Madden NFL 19 has a new 3rd position score", "ttl":15, "notification_category": "Information"}' "localhost:5000/service/notifications/"

curl -iX POST -H "Content-Type: application/json" -d '{"message":"Madden NFL 19 has a new winner", "ttl":18, "notification_category": "Information"}' "localhost:5000/service/notifications/"

curl -iX POST -H "Content-Type: application/json" -d '{"message":"FIFA 19 has a new 3rd position score", "ttl":16, "notification_category": "Information"}' "localhost:5000/service/notifications/"

curl -iX POST -H "Content-Type: application/json" -d '{"message":"NBA Live 19 has a new winner", "ttl":5, "notification_category": "Information"}' 
"localhost:5000/service/notifications/"

前面的命令将编写并发送九个 HTTP POST请求,这些请求指定了具有指定 JSON 键值对的 JSON 键值对。请求指定了/service/notifications/,因此,它们将匹配'/notifications/'并运行NotificationListResource.post方法,即NotificationListResource类的post方法。

在运行前面的命令后,我们将在我们的 PostgreSQL 数据库中保留 12 条通知。然而,当我们向/service/notifications/发送 HTTP GET请求以编写和发送消息时,我们不想检索这 12 条消息。我们将创建一个可定制的通用分页类,以在每个数据页中包含最多四个资源。

打开位于service文件夹中的config.py文件,并添加以下行以声明两个变量,这些变量用于配置全局分页设置。

打开service/config.py文件,并添加以下行以声明两个变量,这些变量用于配置全局分页设置。示例代码文件包含在restful_python_2_03_01文件夹中,位于Flask01/service/config.py文件中:

PAGINATION_PAGE_SIZE = 4 
PAGINATION_PAGE_ARGUMENT_NAME = 'page' 

PAGINATION_PAGE_SIZE变量的值指定了一个全局设置,即页面大小的默认值,也称为限制。PAGINATION_PAGE_ARGUMENT_NAME变量的值指定了一个全局设置,即我们将在请求中使用的默认参数名称,以指定我们想要检索的页面号。

service 文件夹内创建一个新的 helpers.py 文件。以下行显示了创建新的 PaginationHelper 类的代码。示例代码文件包含在 restful_python_2_03_01 文件夹中的 Flask01/service/helpers.py 文件中:

from flask import url_for 
from flask import current_app 

class PaginationHelper(): 
    def __init__(self, request, query, resource_for_url, key_name, schema): 
        self.request = request 
        self.query = query 
        self.resource_for_url = resource_for_url 
        self.key_name = key_name 
        self.schema = schema 
        self.page_size =
current_app.config['PAGINATION_PAGE_SIZE'] 
        self.page_argument_name = current_app.config['PAGINATION_PAGE_ARGUMENT_NAME'] 

    def paginate_query(self): 
        # If no page number is specified, we assume the request requires page #1 
        page_number = self.request.args.get(self.page_argument_name, 1, type=int) 
        paginated_objects = self.query.paginate( 
            page_number, 
            per_page=self.page_size, 
            error_out=False) 
        objects = paginated_objects.items 
        if paginated_objects.has_prev: 
            previous_page_url = url_for( 
                self.resource_for_url,  
                page=page_number-1,  
                _external=True) 
        else: 
            previous_page_url = None 
        if paginated_objects.has_next: 
            next_page_url = url_for( 
                self.resource_for_url, 
                page=page_number+1, 
                _external=True) 
        else: 
            next_page_url = None 
        dumped_objects = self.schema.dump(objects, many=True).data 
        return ({ 
            self.key_name: dumped_objects, 
            'previous': previous_page_url, 
            'next': next_page_url, 
            'count': paginated_objects.total 
        }) 

PaginationHelper 类声明了一个构造函数,即 __init__ 方法,该方法接收以下参数并使用它们来初始化具有相同名称的属性:

  • request:Flask 请求对象,它将允许 paginate_query 方法检索通过 HTTP 请求指定的页码值

  • querypaginate_query 方法必须分页的 SQLAlchemy 查询

  • resource_for_url:一个字符串,表示 paginate_query 方法将使用该资源名来生成上一页和下一页的完整 URL

  • key_name:一个字符串,表示 paginate_query 方法将使用该键名来返回序列化对象

  • schemapaginate_query 方法必须使用的 Flask-Marshmallow Schema 子类来序列化对象

此外,构造函数读取并保存了添加到 config.py 文件中的配置变量的值,并将它们保存在 page_sizepage_argument_name 属性中。

该类声明了 paginate_query 方法。首先,代码检索请求中指定的页码并将其保存到 page_number 变量中。如果没有指定页码,代码假设请求需要第一页。然后,代码调用 self.query.paginate 方法来检索数据库中对象的分页结果中指定的 page_number 页码,每页的结果数量由 self.page_size 属性的值指示。下一行将 paginated_object.items 属性中的分页项保存到 objects 变量中。

如果 paginated_objects.has_prev 属性的值为 True,则表示存在上一页。在这种情况下,代码调用 flask.url_for 函数来生成带有 self.resource_for_url 属性值的上一页的完整 URL。_external 参数设置为 True,因为我们想提供完整的 URL。

如果 paginated_objects.has_next 属性的值为 True,则表示存在下一页。在这种情况下,代码调用 flask.url_for 函数来生成带有 self.resource_for_url 属性值的下一页的完整 URL。

然后,代码调用 self.schema.dump 方法来序列化之前保存在 objects 变量中的部分结果,将 many 参数设置为 Truedumped_objects 变量保存了对调用 dump 方法返回的结果的 data 属性的引用。

最后,该方法返回一个包含以下键值对的字典:

self.key_name 保存于 dumped_objects 变量中的序列化部分结果。
'previous' 存储在 previous_page_url 变量中的上一页的完整 URL。
'next' 存储在 next_page_url 变量中的下一页的完整 URL。
'count' paginated_objects.total 属性检索的完整结果集中可用的对象总数。

添加分页功能

打开 service 文件夹中的 views.py 文件,并将 NotificationListResource.get 方法的代码替换为下一列表中突出显示的行。此外,请确保添加突出显示的导入语句。示例代码文件包含在 restful_python_2_03_01 文件夹中,位于 Flask01/service/views.py 文件:

from helpers import PaginationHelper 

class NotificationListResource(Resource): 
 def get(self): pagination_helper = PaginationHelper( request, query=Notification.query, resource_for_url='service.notificationlistresource', key_name='results', schema=notification_schema) pagination_result = pagination_helper.paginate_query() return pagination_result

新代码为...

理解添加身份验证和权限的步骤

我们当前的 API 版本在不需要任何类型的身份验证的情况下处理所有传入的请求。我们将使用 Flask 扩展和其他包来使用 HTTP 身份验证方案来识别发起请求的用户或签名请求的令牌。然后,我们将使用这些凭据来应用权限,以确定请求是否必须被允许。不幸的是,Flask 和 Flask-RESTful 都没有提供我们可以轻松插入和配置的身份验证框架。因此,我们将不得不编写代码来执行与身份验证和权限相关的许多任务。

我们希望能够在没有任何身份验证的情况下创建新用户。然而,所有其他 API 调用都只对经过身份验证的用户可用。

首先,我们将安装 Flask-HTTPAuth Flask 扩展,以便我们更容易地处理 HTTP 身份验证,并使用 passlib 包来允许我们对密码进行散列并检查提供的密码是否有效。

我们将创建一个新的 User 模型,该模型将代表一个用户。该模型将提供方法,使我们能够对密码进行散列,并验证提供给用户的密码是否有效。我们将创建一个 UserSchema 类来指定我们想要如何序列化和反序列化用户。

然后,我们将配置 Flask 扩展以与我们的 User 模型一起工作,以验证密码并设置与请求关联的已认证用户。我们将对现有资源进行更改以要求身份验证,并将添加新资源以允许我们检索现有用户并创建一个新用户。最后,我们将配置与用户相关的资源的路由。

一旦我们完成了之前提到的任务,我们将运行迁移过程以生成新的表,该表将持久化数据库中的用户。然后,我们将编写并发送 HTTP 请求以了解我们的新版本 API 中的身份验证和权限是如何工作的。

确保您退出 Flask 的开发服务器。您只需在运行它的终端或命令提示符窗口中按 Ctrl + C 即可。

现在,我们将安装许多附加包。请确保您已激活我们在 第一章,“使用 Flask 1.0.2 开发 RESTful API 和微服务”,中创建的虚拟环境,我们命名为 Flask01。激活虚拟环境后,就是运行许多命令的时候了,这些命令对 macOS、Linux 或 Windows 都是一样的。

现在,我们将编辑现有的 requirements.txt 文件,以指定我们的应用程序在任何支持的平台中需要安装的附加包集。这样,在任意新的虚拟环境中重复安装指定包及其版本将变得极其容易。

使用您喜欢的编辑器编辑虚拟环境根文件夹中名为 requirements.txt 的现有文本文件。在最后一行之后添加以下行,以声明我们的 API 新版本所需的附加包及其版本。示例的代码文件包含在 restful_python_2_03_02 文件夹中,在 Flask01/requirements.txt 文件中:

flask-HTTPAuth==3.2.4 
passlib==1.7.1

添加到 requirements.txt 文件中的每一行都表示需要安装的包及其版本。以下表格总结了我们作为附加要求指定的包及其版本号:

包名 要安装的版本
Flask-HTTPAuth 3.2.4
passlib 1.7.1

现在我们必须在 macOS、Linux 或 Windows 上运行以下命令来安装之前表格中解释的附加包及其版本,使用最近编辑的 requirements.txt 文件通过 pip 进行安装。在运行命令之前,请确保您位于包含 requirements.txt 文件的文件夹中:

 pip install -r requirements.txt 

输出的最后几行将指示所有新包及其依赖项已成功安装。如果您下载了示例的源代码,并且您没有使用 API 的先前版本,pip 也会安装 requirements.txt 文件中包含的其他包:

    Installing collected packages: Flask-HTTPAuth, passlib
    Successfully installed Flask-HTTPAuth-3.2.4 passlib-1.7.1  

添加用户模型

现在,我们将创建一个模型,我们将使用它来表示和持久化用户。打开 service 文件夹中的 models.py 文件,并在 ResourceAddUpdateDelete 类声明之后添加以下行。请确保您添加了高亮的导入语句。示例的代码文件包含在 restful_python_2_03_02 文件夹中,在 Flask01/service/models.py 文件中:

from passlib.apps import custom_app_context as password_contextimport re class User(orm.Model, ResourceAddUpdateDelete): id = orm.Column(orm.Integer, primary_key=True) name = orm.Column(orm.String(50), unique=True, nullable=False) # I save the hash for the password (I don't persist the actual password) password_hash = orm.Column(orm.String(120), ...

创建用于验证、序列化和反序列化用户的模式

现在,我们将创建 Flask-Marshmallow 模式,我们将使用它来验证、序列化和反序列化之前声明的 User 模型。打开 service 文件夹中的 models.py 文件,并在现有行之后添加以下代码。示例的代码文件包含在 restful_python_2_03_02 文件夹中,在 Flask01/service/models.py 文件中:

class UserSchema(ma.Schema): 
    id = fields.Integer(dump_only=True) 
    name = fields.String(required=True,  
        validate=validate.Length(3)) 
    url = ma.URLFor('service.userresource',  
        id='<id>',  
        _external=True) 

代码声明了UserSchema模式,具体是ma.Schema类的子类。记住,我们之前为service/models.py文件编写的代码创建了一个名为maflask_marshmallow.Mashmallow实例。

我们将表示字段的属性声明为marshmallow.fields模块中声明的适当类的实例。UserSchema类将name属性声明为fields.String的实例。required参数设置为True,以指定该字段不能为空字符串。validate参数设置为validate.Length(5),以指定该字段必须至少有五个字符长。

密码的验证没有包含在模式中。我们将使用在User类中定义的check_password_strength_and_hash_if_ok方法来验证密码。

为资源添加认证

现在我们将执行以下任务:

  1. 配置Flask-HTTPAuth扩展以与我们的User模型一起工作,以验证密码并设置与请求关联的已验证用户。

  2. 声明一个自定义函数,该函数将被Flask-HTTPAuth扩展用作回调以验证密码。

  3. 为我们的资源创建一个新的基类,该基类将需要认证。

打开service文件夹内的views.py文件,在最后一个使用import语句的行之后和声明名为service_blueprintBlueprint实例之前添加以下代码。示例的代码文件包含在restful_python_2_03_02文件夹中,在Flask01/service/views.py文件中:

from flask_httpauth ...

创建用于处理用户的资源类

我们只想能够创建用户并使用它们来验证请求。因此,我们将只关注创建具有几个方法的资源类。我们不会创建一个完整的用户管理系统。

我们将创建代表用户和用户集合的资源类。首先,我们将创建一个UserResource类,我们将使用它来表示用户资源。打开service文件夹内的views.py文件,在创建名为serviceApi实例的行之后和声明NotificationResource类之前添加以下行。示例的代码文件包含在restful_python_2_03_02文件夹中,在Flask01/service/views.py文件中:

class UserResource(AuthenticationRequiredResource): 
    def get(self, id): 
        user = User.query.get_or_404(id) 
        result = user_schema.dump(user).data 
        return result 

UserResource类是之前编写的AuthenticationRequiredResource的子类,并声明了一个get方法,当具有相同名称的 HTTP 方法作为对表示的资源上的请求被调用时,将调用此方法。

该方法接收 id 参数中要检索的用户 id。代码调用 User.query.get_or_404 方法,如果没有在底层数据库中找到请求的 id 对应的用户,则返回 HTTP 404 Not Found 状态。如果用户存在,代码将调用 user_schema.dump 方法,将检索到的用户作为参数,使用 UserSchema 实例来序列化与指定的 id 匹配的 User 实例。dump 方法接收 User 实例,并应用在 UserSchema 类中指定的字段过滤和输出格式化。字段过滤指定我们不希望序列化密码的哈希值。代码返回 dump 方法返回的结果的 data 属性,即作为主体的 JSON 格式的序列化消息,带有默认的 HTTP 200 OK 状态码。

现在我们将创建 UserListResource 类,我们将使用它来表示用户集合。打开 service 文件夹中的 views.py 文件,并在创建 UserResource 类的代码之后添加以下行。示例代码文件包含在 restful_python_2_03_02 文件夹中,位于 Flask01/service/views.py 文件:

class UserListResource(Resource): 
 @auth.login_required def get(self): 
        pagination_helper = PaginationHelper( 
            request, 
            query=User.query, 
            resource_for_url='service.userlistresource', 
            key_name='results', 
            schema=user_schema) 
        result = pagination_helper.paginate_query() 
        return result 

 def post(self): 
        user_dict = request.get_json() 
        if not user_dict: 
            response = {'user': 'No input data provided'} 
            return response, HttpStatus.bad_request_400.value 
        errors = user_schema.validate(user_dict) 
        if errors: 
            return errors, HttpStatus.bad_request_400.value 
        user_name = user_dict['name'] 
        existing_user = User.query.filter_by(name=user_name).first() 
        if existing_user is not None: 
            response = {'user': 'An user with the name {} already exists'.format(user_name)} 
            return response, HttpStatus.bad_request_400.value 
        try: 
            user = User(name=user_name) 
            error_message, password_ok = \ 
                user.check_password_strength_and_hash_if_ok(user_dict['password']) 
            if password_ok: 
                user.add(user) 
                query = User.query.get(user.id) 
                dump_result = user_schema.dump(query).data 
                return dump_result, HttpStatus.created_201.value 
            else: 
                return {"error": error_message}, HttpStatus.bad_request_400.value 
        except SQLAlchemyError as e: 
            orm.session.rollback() 
            response = {"error": str(e)} 
            return response, HttpStatus.bad_request_400.value

UserListResource 类是 flask_restful.Resource 超类的子类,因为我们不希望所有的方法都需要进行身份验证。我们希望能够在未进行身份验证的情况下创建新用户,因此,我们只为 get 方法应用了 @auth.login_required 装饰器。post 方法不需要身份验证。该类声明了以下两个方法,当到达表示的资源上的具有相同名称的 HTTP 方法请求时,将调用这些方法:

  • get:此方法返回一个包含数据库中持久化的所有 User 实例的列表。首先,代码调用 User.query.all 方法来检索所有 User 实例。然后,代码调用 user_schema.dump 方法,将检索到的用户和 many 参数设置为 True 以序列化对象的可迭代集合。dump 方法将取从数据库检索到的每个 User 实例,并应用由 UserSchema 类指定的字段过滤和输出格式化。代码返回 dump 方法返回的结果的 data 属性,即作为主体的 JSON 格式的序列化消息,带有默认的 HTTP 200 OK 状态码。

  • post:此方法检索 JSON 主体中接收到的键值对,创建一个新的 User 实例并将其持久化到数据库中。首先,代码调用 request.get_json 方法来检索请求中作为参数接收到的键值对。然后,代码调用 user_schema.validate 方法来验证使用检索到的键值对构建的新用户。在这种情况下,对这个方法的调用将仅验证用户的 name 字段。如果有验证错误,代码将返回一个由验证错误和 HTTP 400 Bad Request 状态码组成的元组。如果验证成功,代码将检查数据库中是否已存在具有相同名称的用户,以返回适当的错误信息,该字段必须是唯一的。如果用户名是唯一的,代码将创建一个新的用户,指定其 name,并调用其 check_password_strength_and_hash_if_ok 方法。如果提供的密码满足所有质量要求,代码将使用其密码的散列在数据库中持久化用户。最后,代码返回一个由序列化后的 JSON 格式保存的用户组成的元组,作为正文,并带有 HTTP 201 Created 状态码。

下表显示了与我们之前创建的、与用户相关的类相关的方法,我们希望为每个 HTTP 方法和范围的组合执行:

HTTP 方法 范围 类和方法 需要认证
GET 用户集合 UserListResource.get
GET 用户 UserResource.get
POST 用户集合 UserListResource.post

我们必须通过定义 URL 规则来执行必要的资源路由配置,以调用适当的方法并将所有必要的参数传递给它们。以下几行配置了与用户相关的资源到 service 对象的资源路由。打开 service 文件夹内的 views.py 文件,并在代码末尾添加以下几行。示例代码文件包含在 restful_python_2_03_02 文件夹中,位于 Flask01/service/views.py 文件:

service.add_resource(UserListResource,  
    '/users/') 
service.add_resource(UserResource,  
    '/users/<int:id>') 

每次调用 service.add_resource 方法都会将一个 URL 路由到之前编写的与用户相关的资源之一。当有 API 请求时,如果 URL 与 service.add_resource 方法中指定的 URL 之一匹配,Flask 将调用与请求中指定的类的 HTTP 方法匹配的方法。

运行迁移以生成用户表

现在,我们将运行多个脚本来运行迁移并生成在 PostgreSQL 10.5 数据库中持久化用户所需的必要表。请确保在激活了虚拟环境并位于 service 文件夹的终端或命令提示符窗口中运行这些脚本。

运行第一个命令,用模型中检测到的更改填充迁移脚本。在这种情况下,这是我们第二次填充迁移脚本,因此,迁移脚本将生成新的表,以持久化我们的新User模型:user

    flask db migrate

以下行显示了运行上述命令后生成的示例输出。您的输出将...

组合带有必要认证的请求

现在,我们将组合并发送一个 HTTP 请求,以获取不带认证凭据的通知的第一页。示例的代码文件包含在restful_python_2_03_01文件夹中,在Flask01/cmd317.txt文件中:

    http GET ":5000/service/notifications/?page=1"

以下是对应的curl命令。示例的代码文件包含在restful_python_2_03_01文件夹中,在Flask01/cmd318.txt文件中:

    curl -iX GET "localhost:5000/service/notifications/?page=1"

我们将在响应头中收到401 Unauthorized状态码。以下行显示了示例响应:

    HTTP/1.0 401 UNAUTHORIZED
    Content-Length: 19
    Content-Type: text/html; charset=utf-8
    Date: Sat, 20 Oct 2018 00:30:56 GMT
    Server: Werkzeug/0.14.1 Python/3.7.1
    WWW-Authenticate: Basic realm="Authentication Required"

    Unauthorized Access

如果我们想检索通知,即对/service/notifications/进行GET请求,我们需要通过使用 HTTP 认证提供认证凭据。然而,在我们能够这样做之前,有必要创建一个新的用户。我们将使用这个新用户来测试我们与用户相关的新资源类以及我们在权限策略中的更改。运行以下命令以创建新用户。示例的代码文件包含在restful_python_2_03_01文件夹中,在Flask01/cmd319.txt文件中:

http POST ":5000/service/users/" name='gaston-hillar' password='wrongpassword'

以下是对应的curl命令。示例的代码文件包含在restful_python_2_03_01文件夹中,在Flask01/cmd320.txt文件中:

curl -iX POST -H "Content-Type: application/json" -d '{"name": 
"gaston-hillar", "password": "wrongpassword"}' "localhost:5000/service/users/"

当然,创建用户和执行需要认证的方法只能在 HTTPS 下进行。这样,用户名和密码将被加密。封装 API 的生产级微服务必须在 HTTPS 下运行。

之前的命令将组合并发送一个带有指定 JSON 键值对的 HTTP POST请求。请求指定/service/users/,因此,它将匹配'/users/' URL 路由的UserList资源,并运行不需要认证的UserList.post方法。由于 URL 路由不包含任何参数,因此该方法不接收任何参数。由于请求的 HTTP 动词是POST,Flask 调用post方法。

之前指定的密码仅包含小写字母,因此,它没有满足我们在User.check_password_strength_and_hash_if_ok方法中为密码指定的所有质量要求。因此,我们将在响应头中收到400 Bad Request状态码,并在 JSON 体中显示错误消息,指出密码没有满足质量要求。以下行显示了示例响应:

    HTTP/1.0 400 BAD REQUEST
    Content-Length: 76
    Content-Type: application/json
    Date: Sat, 20 Oct 2018 04:19:45 GMT
    Server: Werkzeug/0.14.1 Python/3.7.1

   {
        "error": "The password must include at least one uppercase 
    letter."
    }

以下命令将创建一个具有有效密码的用户。示例代码文件包含在restful_python_2_03_01文件夹中的Flask01/cmd321.txt文件中:

http POST ":5000/service/users/" name='gaston-hillar' password='TTl#ID16^eplG'

以下是对应的curl命令。示例代码文件包含在restful_python_2_03_01文件夹中的Flask01/cmd322.txt文件中:

 curl -iX POST -H "Content-Type: application/json" -d '{"name": "gaston-hillar", "password": "TTl#ID16^eplG"}' "localhost:5000/service/users/"

如果新的User实例成功持久化到数据库中,调用将返回 HTTP 201 Created状态码,并将最近持久化的User序列化为 JSON 格式放在响应体中。以下行显示了 HTTP 请求的示例响应,其中包含 JSON 响应中的新User对象。注意,响应中包含了创建用户的 URL,url,但没有包含任何与密码相关的信息:

    HTTP/1.0 201 CREATED
    Content-Length: 97
    Content-Type: application/json
    Date: Sat, 20 Oct 2018 15:58:15 GMT
    Server: Werkzeug/0.14.1 Python/3.7.1

    {
        "id": 1,
        "name": "gaston-hillar",
        "url": "http://localhost:5000/service/users/1"
    }

我们可以运行之前解释过的命令来检查 PostgreSQL 数据库中迁移创建的user表的内容。我们会注意到user表中新行的password_hash字段内容是经过散列的。以下截图显示了在运行 HTTP 请求后 PostgreSQL 数据库中user表新行的内容:

如果我们想检索通知的第一页,即向/service/notifications/发送 HTTP GET请求,我们需要使用 HTTP 认证提供认证凭据。

现在,我们将组合并发送一个带有认证凭据的 HTTP 请求来检索消息的第一页,即使用我们最近创建的用户名和他的密码。示例代码文件包含在restful_python_2_03_01文件夹中的Flask01/cmd323.txt文件中:

    http -a 'gaston-hillar':'TTl#ID16^eplG' ":5000/service/notifications/?page=1"

以下是对应的curl命令。示例代码文件包含在restful_python_2_03_01文件夹中的Flask01/cmd324.txt文件中:

curl --user 'gaston-hillar':'TTl#ID16^eplG' -iX GET "localhost:5000/service/notifications/?page=1"

用户将成功认证,我们将能够处理请求以检索数据库中持久化的通知的第一页。随着我们对 API 所做的所有更改,未经认证的请求只能创建新用户。

测试你的知识

让我们看看你是否能正确回答以下问题:

  1. 哪个 HTTP 动词是用来替换整个资源的:

    1. PATCH

    2. POST

    3. PUT

  2. 哪个 HTTP 动词是用来对现有资源应用 delta 的:

    1. PATCH

    2. POST

    3. PUT

  3. 默认情况下,passlib库将为 64 位平台使用 SHA-512 方案,并将最小轮数设置为:

    1. 135,000

    2. 335,000

    3. 535,000

  4. flask.g对象是:

    1. 一个提供对当前请求访问的代理

    2. flask_httpauth.HTTPBasicAuth类的实例

    3. 一个代理,允许我们存储在这个代理上我们想要在单个请求中共享的任何内容

  5. passlib包提供:

    1. 一个支持超过 30 种方案的密码散列框架

    2. 一个认证框架...

摘要

在本章中,我们从多个方面改进了 RESTful API。我们添加了当资源不唯一时的用户友好错误信息。我们测试了如何使用PATCH方法更新单个或多个字段,并创建了自己的通用分页类,以便我们可以分页结果集。

然后,我们开始处理认证和权限。我们添加了用户模型,并更新了底层的 PostgreSQL 数据库。我们在不同的代码片段中进行了许多更改,以实现特定的安全目标,并利用Flask-HTTPAuthpasslib在我们的 API 中使用 HTTP 认证。

现在我们已经构建了一个改进的复杂 API,该 API 使用了分页和认证功能,我们将使用框架中包含的额外抽象,并编写、执行和改进单元测试,为将我们的 API 封装成微服务做好准备,这些内容将是下一章的主题。

第四章:在 Flask 微服务中测试和部署 API

在本章中,我们将配置、编写和执行单元测试,并学习一些与部署相关的内容。我们将执行以下操作:

  • 使用 pytest 设置单元测试

  • 创建一个用于测试的数据库

  • 创建用于执行设置和清理任务的固定值

  • 编写第一轮单元测试

  • 使用 pytest 运行单元测试并检查测试覆盖率

  • 提高测试覆盖率

  • 理解部署和可扩展性的策略

使用 pytest 设置单元测试

到目前为止,我们一直在编写代码以向我们的 RESTful API 添加功能。我们使用命令行和 GUI 工具来了解所有组件如何协同工作,并检查使用 Flask 开发服务器对 RESTful API 发出的各种 HTTP 请求的结果。现在我们将编写单元测试,以确保 RESTful API 如预期那样工作。在我们开始编写单元测试之前,有必要在我们的虚拟环境中安装许多附加包,创建一个新的 PostgreSQL 数据库供测试使用,并构建测试环境的配置文件。

确保你退出 Flask 的开发服务器。你只需在运行它的终端或命令提示符窗口中按 Ctrl + C

现在,我们将安装许多附加包。请确保您已激活名为 Flask01 的虚拟环境,该环境在 第一章 使用 Flask 开发 RESTful API 和微服务 1.0.2 中创建。激活虚拟环境后,是时候运行许多命令了,这些命令在 macOS、Linux 或 Windows 上都是相同的。

现在,我们将编辑现有的 requirements.txt 文件,以指定我们的应用程序在任意支持平台上需要安装的附加包集。这样,在任意新的虚拟环境中重复安装指定包及其版本将变得极其容易。

使用您喜欢的编辑器编辑虚拟环境根目录下名为 requirements.txt 的现有文本文件。在最后一行之后添加以下行,以声明新版本的 API 所需的附加包及其版本。示例代码文件包含在 restful_python_2_04_01 文件夹中,位于 Flask01/requirements.txt 文件:

pytest==4.0.1 
coverage==4.5.2 
pytest-cov==2.6.0 

requirements.txt 文件中添加的每一行都表示需要安装的包及其版本。以下表格总结了我们作为附加要求指定的包及其版本号:

包名 要安装的版本
pytest 4.0.1
coverage 4.5.2
pytest-cov 2.6.0

我们将在我们的虚拟环境中安装以下 Python 包:

  • pytest:这是一个非常流行的 Python 单元测试框架,它使测试变得简单,并减少了样板代码。

  • coverage:这个工具测量 Python 程序的代码覆盖率,我们将使用它来确定哪些代码部分被单元测试执行,哪些部分没有被执行。

  • pytest-cov:这个 pytest 插件使得使用 coverage 工具生成覆盖率报告变得简单,并提供了额外的功能。

现在,我们必须在 macOS、Linux 或 Windows 上运行以下命令,使用最近编辑的 requirements.txt 文件通过 pip 安装之前表格中概述的附加包和版本。在运行命令之前,请确保你位于包含 requirements.txt 文件的文件夹中:

    pip install -r requirements.txt

输出的最后几行将指示所有新包及其依赖项已成功安装。如果你下载了示例的源代码,而你之前没有使用过该 API 的旧版本,pip 也会安装 requirements.txt 文件中包含的其他包:

Installing collected packages: atomicwrites, six, more-itertools, pluggy, py, attrs, pytest, coverage, pytest-cov
Successfully installed atomicwrites-1.2.1 attrs-18.2.0 coverage-4.5.2 more-itertools-4.3.0 pluggy-0.8.0 py-1.7.0 pytest-4.0.1 pytest-cov-2.6.0 six-1.12.0

创建用于测试的数据库

现在,我们将创建我们将用作测试环境存储库的 PostgreSQL 数据库。请注意,测试计算机或服务器必须安装了 PostgreSQL 10.5,正如前几章中为开发环境所解释的那样。我假设你正在运行测试的计算机与你在之前示例中工作的计算机相同。

请记住确保 PostgreSQL 的 bin 文件夹包含在 PATH 环境变量中。你应该能够从当前的 Terminal、命令提示符或 Windows PowerShell 中执行 psql 命令行工具。

我们将使用 PostgreSQL 命令行工具创建一个名为 test_flask_notifications 的新数据库。如果你已经 ...

创建夹具以执行运行干净测试的设置和清理任务

测试夹具提供了一个固定的基线,使我们能够可靠地重复执行测试。Pytest 通过使用 @pytest.fixture 装饰器标记函数,使得声明测试夹具函数变得简单。然后,无论何时我们在测试函数声明中使用夹具函数名作为参数,pytest 都会使得夹具函数提供夹具对象。现在我们将创建以下两个 pytest 夹具函数,我们将在未来的测试函数中使用它们:

  • application:这个测试夹具函数将执行必要的设置任务,以创建具有适当测试配置的 Flask 测试应用,并在测试数据库中创建所有必要的表。夹具将启动测试执行,当测试完成后,夹具将执行必要的清理任务,使数据库在运行测试之前的状态保持不变。

  • client:这个测试固定函数接收application作为参数,因此,它接收在之前解释的应用程序测试固定函数中作为参数创建的 Flask 应用。因此,client测试固定函数为测试配置应用,初始化数据库,为该应用创建一个测试客户端并返回它。我们将在测试方法中使用测试客户端轻松地组合和发送请求到我们的 API。

service文件夹内创建一个新的conftest.py文件。添加以下行,这些行声明了许多import语句以及之前解释过的pytest测试固定函数。示例代码文件包含在restful_python_2_04_01文件夹中,位于Flask01/service/conftest.py文件中:

import pytest 
from app import create_app 
from models import orm 
from flask_sqlalchemy import SQLAlchemy 
from flask import Flask 
from views import service_blueprint 

@pytest.fixture 
def application(): 
    # Beginning of Setup code 
    app = create_app('test_config') 
    with app.app_context():    
        orm.create_all() 
        # End of Setup code 
        # The test will start running here 
        yield app 
        # The test finished running here 
        # Beginning of Teardown code 
        orm.session.remove() 
        orm.drop_all() 
        # End of Teardown code 

@pytest.fixture 
def client(application): 
    return application.test_client() 

application固定函数将在每次使用applicationclient作为参数的测试时执行。该函数使用app模块中声明的create_app函数,并传入'test_config'作为参数。该函数将使用此模块作为配置文件设置 Flask 应用,因此,该应用将使用之前创建的配置文件,该文件指定了测试数据库和环境的所需值。

下一个调用orm.create_all方法来创建在test_config.py文件中配置的测试数据库中所有必要的表。在yield app行之后的所有代码作为清理代码执行,在app被使用和测试执行之后执行。这段代码将移除 SQLAlchemy 会话并删除在测试数据库中创建的所有表,这样,每次测试完成后,测试数据库将再次为空。

编写第一轮单元测试

现在,我们将编写第一轮单元测试。具体来说,我们将编写与用户和通知类别资源相关的单元测试:UserResourceUserListResourceNotificationCategoryResourceNotificationCategoryListResource

service文件夹内创建一个新的tests子文件夹。然后,在新的service/tests子文件夹内创建一个新的test_views.py文件。添加以下行,这些行声明了许多import语句以及我们将在许多测试函数中使用的第一个函数。示例代码文件包含在restful_python_2_04_01文件夹中,位于Flask01/service/tests/test_views.py文件中:

import pytest from base64 import b64encode from flask import current_app, json, ...

使用 pytest 运行单元测试并检查测试覆盖率

service文件夹内创建一个新的setup.cfg文件。以下行显示了指定 pytest 和coverage工具所需配置的代码。示例代码文件包含在restful_python_2_04_01文件夹中,位于Flask01/service/setup.cfg文件中:

[tool:pytest] 
testpaths = tests 

[coverage:run] 
branch = True 
source =  
    models 
    views 

tool:pytest部分指定了 pytest 的配置。testpaths设置将tests值分配给指示测试位于tests子文件夹中。

coverage:run部分指定了coverage工具的配置。branch设置设置为True以启用分支覆盖率测量,除了默认的语句覆盖率之外。source设置指定了我们希望考虑覆盖率测量的模块。我们只想包括modelsviews模块。

现在我们将使用pytest命令来运行测试并测量它们的代码覆盖率。请确保你在激活了虚拟环境的终端或命令提示符窗口中运行此命令,并且你位于service文件夹内。运行以下命令:

    pytest --cov -s

测试运行器将执行所有在test_views.py中定义并以test_前缀开始的函数,并将显示结果。我们将使用-v选项来指示pytest以详细模式打印测试函数名称和状态。--cov选项通过使用pytest-cov插件启用测试覆盖率报告生成。

当我们在 API 上工作时,测试不会更改我们一直在使用的数据库。请记住,我们已将test_flask_notifications数据库配置为我们的测试数据库。

以下行显示了示例输出:

=================================== test session starts ===================================
 latform darwin -- Python 3.7.1, pytest-4.0.1, py-1.7.0, pluggy-0.8.0 - 
 - /Users/gaston/HillarPythonREST2/Flask01/bin/python3
 cachedir: .pytest_cache
 rootdir: /Users/gaston/HillarPythonREST2/Flask01/service, inifile: 
 setup.cfg
 plugins: cov-2.6.0
 collected 5 items 

    tests/test_views.py::test_request_without_authentication PASSED                     
    [ 20%]
    tests/test_views.py::test_create_and_retrieve_notification_category 
    PASSED          [ 40%]
    tests/test_views.py::test_create_duplicated_notification_category 
    PASSED            [ 60%]
    tests/test_views.py::test_retrieve_notification_categories_list 
    PASSED              [ 80%]
    tests/test_views.py::test_update_notification_category PASSED                       
    [100%]

    ---------- coverage: platform darwin, python 3.7.1-final-0 --------
    ---
    Name        Stmts   Miss Branch BrPart  Cover
    ---------------------------------------------
    models.py     101     27     24      7    66%
    views.py      208    112     46     10    43%
    ---------------------------------------------
    TOTAL         309    139     70     17    51%

========================== 5 passed, 1 warnings in 18.15 seconds ==========================

Pytest 使用之前创建的setup.cfg文件中指定的配置来确定哪些路径包含以test前缀开始的模块名称。在这种情况下,唯一符合标准的模块是test_views模块。在符合标准的模块中,pytest从所有以test前缀开始的函数中加载测试。

输出提供了详细信息,测试运行器发现了并执行了五个测试,并且所有测试都通过了。输出显示了test_views模块中每个以test_前缀开始的方法的模块和函数名称,这些方法代表要执行的测试。

coverage包和pytest-cov插件提供的测试代码覆盖率测量报告使用 Python 标准库中包含的代码分析工具和跟踪钩子来确定哪些代码行是可执行的,以及这些行中的哪些已被执行。报告提供了一个包含以下列的表格:

  • Name: Python 模块名称

  • Stmts: Python 模块中可执行语句的数量

  • Miss: 未执行的执行语句数量,即那些未执行的语句

  • Branch: Python 模块中可能的分支数量

  • BrPart: 测试过程中执行的分支数量

  • Cover: 可执行语句和分支的覆盖率,以百分比表示

根据报告中显示的测量结果,我们确实对 views.pymodels.py 模块存在不完整的覆盖率。实际上,我们只编写了一些与通知类别和用户相关的测试,因此,对于 views.py 模块覆盖率低于 50% 是有道理的。我们没有创建与通知相关的测试。

我们可以使用带有 -m 命令行选项的 coverage 命令来显示新 Missing 列中缺失语句的行号:

    coverage report -m

命令将使用上次执行的信息,并显示缺失的语句和缺失的分支。以下行显示了与之前单元测试执行相对应的示例输出。破折号(-)用于表示遗漏的行范围。例如,22-23 表示第 22 行和第 23 行缺少语句。破折号后跟大于号(->)表示从 -> 前一行到其后一行的分支被遗漏。例如,41->42 表示从第 41 行到第 42 行的分支被遗漏:

    Name        Stmts   Miss Branch BrPart  Cover   Missing
    -------------------------------------------------------
    models.py     101     27     24      7    66%   22-23, 38, 40, 42, 
    44, 46, 48, 68-75, 78-80, 94, 133-143, 37->38, 39->40, 41>42, 43-
    >44, 45->46, 47->48, 93->94
    views.py      208    112     46     10    43%   37-39, 45-52, 57-
    58, 61, 65-66, 77-81, 86-88, 91-118, 121-129, 134-141, 144-175, 
    188-189, 192, 199-200, 203-206, 209-217, 230-231, 234, 245-250, 56-
    >57, 60->61, 64->65, 71->77, 187->188, 191->192, 194->201, 196-
    >199, 229->230, 233->234
    -------------------------------------------------------
    TOTAL         309    139     70     17    51%

现在运行以下命令以获取详细说明遗漏行的注释 HTML 列表。该命令不会产生任何输出:

    coverage html

使用您的网页浏览器打开在 htmlcov 文件夹中生成的 index.html HTML 文件。以下截图显示了生成的 HTML 格式报告的示例:

点击或轻触 views.py,网页浏览器将渲染一个显示已运行语句的网页,包括缺失的、排除的和部分执行的语句,并用不同的颜色标出。我们可以点击或轻触运行、缺失、排除和部分按钮来显示或隐藏代表每行代码状态的背景色。默认情况下,缺失的代码行将以粉色背景显示,部分执行的代码行将以黄色背景显示。因此,我们必须编写针对这些代码行的单元测试来提高我们的测试覆盖率。以下截图显示了带有总结的按钮:

下一张截图显示了 views.py 模块中某些代码行的突出显示的缺失行和部分评估的分支:

提高测试覆盖率

现在,我们将编写额外的测试函数来提高测试覆盖率。具体来说,我们将编写与通知和用户相关的单元测试。

打开现有的 service/tests/test_views.py 文件,在最后一行之后插入以下行。示例代码文件包含在 restful_python_2_04_02 文件夹中,位于 Flask01/service/tests/test_views.py 文件:

def create_notification(client, message, ttl,notification_category): 
 url = url_for('service.notificationlistresource', _external=True) data = {'message': message, 'ttl': ttl, 'notification_category': notification_category} response = client.post( url, headers=get_authentication_headers(TEST_USER_NAME, TEST_USER_PASSWORD), data=json.dumps(data)) ...

理解部署和可扩展性的策略

Flask 是一个轻量级的网络微框架,因此,在我们需要提供一个封装在微服务中的 RESTful API 时,它是一个理想的选择。到目前为止,我们一直在使用 Werkzeug 提供的内置开发服务器和纯 HTTP 进行工作。

非常重要的是要理解 Flask 的内置开发服务器不适合生产环境。

Flask 有数十种部署选项,不同的堆栈和流程超出了本书的范围,本书专注于使用最流行的 Python 框架进行 RESTful API 的开发任务。最突出的云服务提供商包括如何使用不同配置部署 Flask 应用程序的说明。此外,还有许多选项可以使用 WSGI(即 Web 服务器网关接口)服务器,这些服务器实现了 WSGI 接口中的网络服务器部分,允许我们在生产环境中运行 Python 网络应用程序,例如 Flask 应用程序。

当然,在生产环境中,我们也会希望使用 HTTPS 而不是 HTTP。我们将不得不配置适当的 TLS 证书,也称为 SSL 证书。

我们使用 Flask 开发了一个 RESTful 网络服务。这类网络服务的关键优势在于它们是无状态的,也就是说,它们不应该在任何服务器上保持客户端状态。我们的 API 就是使用 Flask 实现的无状态 RESTful 网络服务的良好例子。Flask-RESTful 和 PostgreSQL 10.5 可以在 Docker 容器中容器化。例如,我们可以创建一个包含我们的应用程序配置,以运行 NGINX、uWSGI、Redis 和 Flask 的镜像。因此,我们可以使 API 作为微服务运行。

在部署我们 API 的第一个版本之前,我们始终必须确保我们对 API 和数据库进行性能分析。确保生成的查询在底层数据库上运行正常,以及最常用的查询不会最终导致顺序扫描,这一点非常重要。通常需要在数据库中的表上添加适当的索引。

我们一直在使用基本的 HTTP 身份验证。我们可以通过基于令牌的认证来改进它。我们必须确保 API 在生产环境中运行在 HTTPS 下。

使用不同的配置文件进行生产环境部署很方便。然而,另一种越来越受欢迎的方法,尤其是对于云原生应用,是将配置存储在环境中。如果我们想部署云原生 RESTful 网络服务并遵循《十二要素应用》中确立的指南,我们应该将配置存储在环境中。

每个平台都包含了部署我们应用的详细说明。它们都会要求我们生成requirements.txt文件,该文件列出了应用程序的依赖项及其版本。这样,平台就能够安装文件中列出的所有必要依赖项。每当我们需要在我们的虚拟环境中安装新包时,我们都会更新这个文件。然而,在虚拟环境的根目录Flask01中运行以下pip freeze命令以生成最终的requirements.txt文件是一个好主意:

    pip freeze > requirements.txt

以下行显示了生成的示例requirements.txt文件的内容。请注意,生成的文件还包括了我们在原始requirements.txt文件中指定的所有依赖项:

    alembic==1.0.0
    aniso8601==3.0.2
    atomicwrites==1.2.1
    attrs==18.2.0
    certifi==2018.8.24
    chardet==3.0.4
    Click==7.0
    coverage==4.5.1
    Flask==1.0.2
    Flask-HTTPAuth==3.2.4
    flask-marshmallow==0.9.0
    Flask-Migrate==2.3.0
    Flask-RESTful==0.3.6
    Flask-SQLAlchemy==2.3.2
    httpie==1.0.0
    idna==2.7
    itsdangerous==0.24
    Jinja2==2.10
    Mako==1.0.7
    MarkupSafe==1.0
    marshmallow==2.16.3
    marshmallow-sqlalchemy==0.15.0
    more-itertools==4.3.0
    passlib==1.7.1
    pluggy==0.8.0
    psycopg2==2.7.6.1
    py==1.7.0
    Pygments==2.2.0
    pytest==4.0.1
    pytest-cov==2.6.0
    python-dateutil==2.7.3
    python-editor==1.0.3
    pytz==2018.5
    requests==2.19.1
    six==1.11.0
    SQLAlchemy==1.2.12
    urllib3==1.23
    Werkzeug==0.14.1

测试你的知识

让我们看看你是否能正确回答以下问题:

  1. Pytest 通过标记函数以下哪个装饰器来声明一个测试固定函数?

    1. @pytest.fixture_function

    2. @pytest.test_fixture

    3. @pytest.fixture

  2. 默认情况下,pytest 在函数以以下哪个前缀开始时将其发现和执行为文本函数?

    1. test

    2. test_

    3. test-

  3. 以下哪个命令显示覆盖率报告中Missing列中缺失语句的行号?

    1. coverage report -m

    2. coverage report missing

    3. coverage -missing

  4. Pytest 是一个非常流行的 Python:

    1. 使测试变得简单并减少样板代码的单元测试框架

    2. 我们可以运行的 WSGI 服务器...

摘要

在本章中,我们搭建了一个测试环境。我们安装了pytest以便于发现和执行单元测试,并创建了一个新的数据库用于测试。我们编写了一轮单元测试,使用pytest-cov插件和coverage工具测量测试覆盖率,然后编写了额外的单元测试以提高测试覆盖率。最后,我们了解了关于部署和可扩展性的许多考虑因素。

我们使用 Flask 结合 Flask-RESTful 和 PostgreSQL 10.5 数据库构建了一个复杂的 API,我们可以将其作为微服务运行,并对其进行了测试。现在,我们将转向另一个流行的 Python 网络框架 Django,这是下一章的主题。

第五章:使用 Django 2.1 开发 RESTful API

在本章中,我们将开始使用 Django 和 Django REST framework,并创建一个 RESTful 网络 API,该 API 在简单的 SQLite 数据库上执行 CRUD 操作。我们将做以下事情:

  • 设计一个与简单 SQLite 数据库交互的 RESTful API

  • 理解每个 HTTP 方法执行的任务

  • 使用 Django REST framework 设置虚拟环境

  • 创建模型

  • 管理序列化和反序列化

  • 理解响应的状态码

  • 编写 API 视图

  • 使用命令行工具向 API 发送 HTTP 请求

  • 使用 GUI 工具向 API 发送 HTTP 请求

设计一个与简单 SQLite 数据库交互的 RESTful API

想象一下,我们必须开始开发一个需要与 RESTful API 交互以执行游戏 CRUD 操作的移动应用。我们不希望花费时间选择和配置最合适的 ORM对象关系映射);我们只想尽快完成 RESTful API,以便在移动应用中与之交互。我们确实希望游戏持久化在数据库中,但我们不需要它准备好投入生产,因此,我们可以使用最简单的可能的关系数据库,只要我们不需要花费时间进行复杂的安装或配置。

我们需要尽可能短的开发时间。Django Rest FrameworkDRF)将使我们能够轻松完成这项任务,并开始向我们的第一个 RESTful 网络服务发送 HTTP 请求。在这种情况下,我们将使用一个非常简单的 SQLite 数据库,它是新 Django Rest Framework 项目的默认数据库。

首先,我们必须指定我们主要资源:一个游戏。对于一个游戏,我们需要以下属性或字段:

  • 一个整数标识符

  • 一个名称或标题

  • 发布日期

  • 一个 ESRB娱乐软件分级委员会)评级描述,例如 T(青少年)和 EC(幼儿)。您可以在 www.esrb.org 上了解更多关于 ESRB 的信息

  • 一个 bool 值,表示游戏是否至少被玩家玩过一次

  • 一个整数,表示游戏被玩过的次数

此外,我们希望我们的数据库能够保存一个时间戳,记录游戏被插入数据库的日期和时间。

以下表格显示了我们的 API 第一个版本必须支持的 HTTP 动词、范围和语义。每个方法由一个 HTTP 动词和一个范围组成,所有方法都对游戏和游戏集合有明确的含义:

HTTP 动词 范围 语义
GET 游戏集合 获取集合中存储的所有游戏,按名称升序排序
GET 游戏 获取单个游戏
POST 游戏集合 在集合中创建一个新的游戏
PUT 游戏 更新现有的游戏
DELETE 游戏 删除现有的游戏

在 RESTful API 中,每个资源都有自己的唯一 URL。在我们的 API 中,每个游戏都有自己的唯一 URL。

理解每个 HTTP 方法执行的任务

在之前的表格中,GET HTTP 动词出现了两次,但具有两个不同的范围。第一行显示将 GET HTTP 动词应用于游戏集合(资源集合),而第二行显示将 GET HTTP 动词应用于单个游戏(资源)。

假设 http://localhost:8000/games/ 是游戏集合的 URL。如果我们向之前的 URL 添加一个数字和一个斜杠 (/),我们就可以识别一个具有等于指定数值的 ID 的特定游戏。例如,http://localhost:8000/games/25/ 识别 ID 等于 25 的游戏。

我们必须使用以下 HTTP 动词 (POST) 和请求 URL (http://localhost:8000/games/ ...) 编写并发送一个 HTTP 请求

使用 Django REST 框架设置虚拟环境

在 第一章,使用 Flask 1.0.2 开发 RESTful API 和微服务 中,我们了解到,在本书中,我们将使用 Python 3.4 中引入和改进的轻量级虚拟环境。现在我们将遵循许多步骤来创建一个新的轻量级虚拟环境,以便使用 Flask 和 Flask-RESTful。

如果您在现代 Python 中没有轻量级虚拟环境的使用经验,强烈建议您阅读第一章 使用轻量级虚拟环境工作使用 Flask 1.0.2 开发 RESTful API 和微服务 中命名的部分。该章节包含了关于我们将要遵循的步骤的所有详细解释。

以下命令假设您已在 Linux、macOS 或 Windows 上安装了 Python 3.7.1 或更高版本。

首先,我们必须选择我们的轻量级虚拟环境的目标文件夹或目录。以下是我们将在 Linux 和 macOS 中的示例中使用的路径:

    ~/HillarPythonREST2/Django01

虚拟环境的目标文件夹将位于我们主目录内的 HillarPythonREST2/Django01 文件夹中。例如,如果我们的 macOS 或 Linux 中的主目录是 /Users/gaston,则虚拟环境将在 /Users/gaston/HillarPythonREST2/Django01 中创建。您可以在每个命令中将指定的路径替换为您想要的路径。

以下是我们将在 Windows 中的示例中使用的路径:

    %USERPROFILE%\HillarPythonREST2\Django01

虚拟环境的目标文件夹将位于我们用户配置文件文件夹内的 HillarPythonREST2\Django01 文件夹中。例如,如果我们的用户配置文件文件夹是 C:\Users\gaston,则虚拟环境将在 C:\Users\gaston\HillarPythonREST2\Django01 中创建。当然,您可以在每个命令中将指定的路径替换为您想要的路径。

在 Windows PowerShell 中,之前的路径如下:

    $env:userprofile\HillarPythonREST2\Django01

现在我们必须使用 -m 选项后跟 venv 模块名称和所需的路径,以便 Python 将此模块作为脚本运行并创建指定路径中的虚拟环境。根据我们创建虚拟环境的平台,指令可能会有所不同。因此,请确保您遵循您操作系统的说明。

在 Linux 或 macOS 中打开一个终端并执行以下命令以创建虚拟环境:

    python3 -m venv ~/HillarPythonREST2/Django01

在 Windows 中,在命令提示符中执行以下命令以创建虚拟环境:

    python -m venv %USERPROFILE%\HillarPythonREST2\Django01

如果您想使用 Windows PowerShell,请执行以下命令以创建虚拟环境:

    python -m venv $env:userprofile\HillarPythonREST2\Django01 

之前的命令不会产生任何输出。现在我们已经创建了虚拟环境,我们将运行特定于平台的脚本以激活它。激活虚拟环境后,我们将安装仅在此虚拟环境中可用的包。

如果您的终端配置为在 macOS 或 Linux 中使用 bash shell,请运行以下命令以激活虚拟环境。该命令也适用于 zsh shell:

    source ~/PythonREST/Django01/bin/activate

如果您的终端配置为使用 cshtcsh shell,请运行以下命令以激活虚拟环境:

    source ~/PythonREST/Django01/bin/activate.csh 

如果您的终端配置为使用 fish shell,请运行以下命令以激活虚拟环境:

    source ~/PythonREST/Django01/bin/activate.fish

在 Windows 中,您可以在命令提示符中运行批处理文件或 Windows PowerShell 脚本来激活虚拟环境。如果您更喜欢命令提示符,请在 Windows 命令行中运行以下命令以激活虚拟环境:

    %USERPROFILE%\PythonREST\Django01\Scripts\activate.bat

如果您更喜欢 Windows PowerShell,启动它并运行以下命令以激活虚拟环境。但是请注意,您需要在 Windows PowerShell 中启用脚本执行才能运行脚本:

    cd $env:USERPROFILE
    PythonREST\Django01\Scripts\Activate.ps1

激活虚拟环境后,命令提示符将显示括号内的虚拟环境根文件夹名称作为默认提示的前缀,以提醒我们我们正在虚拟环境中工作。在这种情况下,我们将看到(Django01)作为命令提示符的前缀,因为已激活的虚拟环境的根文件夹是 Django01

我们已经遵循了创建和激活虚拟环境的必要步骤。现在我们将创建一个 requirements.txt 文件来指定我们的应用程序需要安装在任何支持平台上的包集。这样,在新的虚拟环境中重复安装指定包及其版本将变得极其容易。

使用您喜欢的编辑器在最近创建的虚拟环境的根文件夹中创建一个名为requirements.txt的新文本文件。以下几行显示了该文件的内容,该文件声明了我们的 API 所需的包及其版本。示例的代码文件包含在restful_python_2_05_01文件夹中的Django01/requirements.txt文件中:

Django==2.1.4 
djangorestframework==3.9.0 
httpie==0.9.9 

requirements.txt文件中的每一行都指示需要安装的包及其版本。在本例中,我们通过使用==运算符使用精确版本,因为我们想确保安装的是指定版本。以下表格总结了我们所指定的作为要求的包及其版本号:

包名 要安装的版本
Django 2.1.4
djangorestframework 3.9.0
httpie 1.0.2

转到虚拟环境的根文件夹:Django01。在 macOS 或 Linux 中,输入以下命令:

    cd ~/PythonREST/Django01

在 Windows 命令提示符中,输入以下命令:

    cd /d %USERPROFILE%\PythonREST\Django01

在 Windows PowerShell 中,输入以下命令:

    cd $env:USERPROFILE
    cd PythonREST\Django01 

现在,在 macOS、Linux 或 Windows 上运行以下命令,使用最近创建的requirements文件通过pip安装之前表格中解释的包和版本。注意,Djangodjangorestframework的依赖项。在运行命令之前,请确保您位于包含requirements.txt文件的文件夹中(Django01):

pip install -r requirements.txt 

输出的最后几行将指示所有已成功安装的包,包括Djangodjangorestframeworkhttpie

Installing collected packages: pytz, Django, djangorestframework, Pygments, certifi, chardet, idna, urllib3, requests, httpie
Successfully installed Django-2.1.4 Pygments-2.2.0 certifi-2018.10.15 chardet-3.0.4 djangorestframework-3.9.0 httpie-1.0.2 idna-2.7 pytz-2018.6 requests-2.20.0 urllib3-1.24

现在,运行以下命令以创建一个名为games_service的新 Django 项目。该命令不会产生任何输出:

    django-admin.py startproject games_service

之前的命令创建了一个包含其他子文件夹和 Python 文件的games_service文件夹。现在转到最近创建的games_service文件夹。只需执行以下命令:

    cd games_service

然后,运行以下命令以在games_service Django 项目中创建一个名为games的新 Django 应用。该命令不会产生任何输出:

    python manage.py startapp games

之前的命令创建了一个新的games_service/games子文件夹,其中包含以下文件:

  • __init__.py

  • admin.py

  • apps.py

  • models.py

  • tests.py

  • views.py

此外,games_service/games文件夹将有一个包含__init__.py Python 脚本的migrations子文件夹。以下截图显示了以games_service文件夹为起点的目录树中的文件夹和文件:

让我们检查位于games_service/games文件夹内的apps.py文件中的 Python 代码。以下几行展示了该文件的代码:

from django.apps import AppConfig 

class GamesConfig(AppConfig): 
    name = 'games'

代码声明GamesConfig类为django.apps.AppConfig超类的子类,该超类代表 Django 应用及其配置。GamesConfig类仅定义了name类属性并将其值设置为'games'

我们必须在games_service/game_service/settings.py文件中将games.apps.GamesConfig添加为已安装应用之一,这将为games_service Django 项目配置设置。我们构建前面的字符串如下:应用名 + .apps. + 类名,即games + .apps. + GamesConfig。此外,我们还需要添加rest_framework应用,以便我们能够使用 Django REST Framework。

games_service/games_service/settings.py文件是一个 Python 模块,其中包含模块级变量,这些变量定义了games_service项目的 Django 配置。我们将对此 Django 设置文件进行一些修改。打开games_service/games_service/settings.py文件,找到以下行,这些行指定了声明已安装应用的字符串列表并将其保存在INSTALLED_APPS变量中:

INSTALLED_APPS = [ 
    'django.contrib.admin', 
    'django.contrib.auth', 
    'django.contrib.contenttypes', 
    'django.contrib.sessions', 
    'django.contrib.messages', 
    'django.contrib.staticfiles', 
] 

将以下两个字符串添加到INSTALLED_APPS字符串列表中,并将更改保存到games_service/games_service/settings.py文件中:

  • 'rest_framework'

  • 'games.apps.GamesConfig'

以下行显示了声明INSTALLED_APPS字符串列表的新代码,其中添加的行被突出显示。示例代码文件包含在restful_python_2_05_01文件夹中,位于Django01/games_service/games-service/settings.py文件中:

INSTALLED_APPS = [ 
    'django.contrib.admin', 
    'django.contrib.auth', 
    'django.contrib.contenttypes', 
    'django.contrib.sessions', 
    'django.contrib.messages', 
    'django.contrib.staticfiles', 
    # Django REST Framework 
    'rest_framework', 
    # Our Games application 
    'games.apps.GamesConfig', 
] 

这样,我们就将 Django REST Framework 和games应用添加到了我们的初始 Django 项目games_service中。

创建模型

现在,我们将创建一个简单的Game模型,我们将使用它来表示和持久化游戏。打开games_service/games/models.py文件。以下行显示了该文件的初始代码,其中只有一个导入语句和一个注释,表明我们应该创建模型:

from django.db import models 

# Create your models here. 

games_service/games/models.py文件的代码替换为以下行。新代码创建了一个Game类,具体来说,是在games/models.py文件中创建了一个Game模型。示例代码文件包含在restful_python_2_05_01文件夹中,位于Django01/games_service/games/apps.py文件中:

from django.db import models class Game(models.Model): created_timestamp = models.DateTimeField(auto_now_add=True) ...

管理序列化和反序列化

我们的 RESTful Web API 必须能够将游戏实例序列化为 JSON 表示,并且也能将 JSON 表示反序列化以构建游戏实例。使用 Django REST Framework,我们只需为游戏实例创建一个序列化器类来管理序列化为 JSON 和从 JSON 反序列化。

Django REST Framework 使用两阶段过程进行序列化。序列化器是模型实例和 Python 原语之间的中介。解析器和渲染器充当 Python 原语和 HTTP 请求和响应之间的中介。

我们将通过创建 rest_framework.serializers.Serializer 类的子类来配置 Game 模型实例和 Python 原始数据之间的中介,以声明字段和必要的序列化和反序列化管理方法。我们将重复一些关于字段的信息,这些信息我们已经包含在 Game 模型中,以便我们理解在 Serializer 类的子类中可以配置的所有内容。然而,我们将使用快捷方式,这将在下一个示例中减少样板代码。我们将通过使用 ModelSerializer 类来在下一个示例中编写更少的代码。

现在,转到 games_service/games 文件夹,并创建一个名为 serializers.py 的新 Python 代码文件。以下行显示了声明新 GameSerializer 类的代码。示例代码文件包含在 restful_python_2_05_01 文件夹中,位于 Django01/games_service/games/serializers.py 文件中:

from rest_framework import serializers 
from games.models import Game 

class GameSerializer(serializers.Serializer): 
    id = serializers.IntegerField(read_only=True) 
    name = serializers.CharField(max_length=200) 
    release_date = serializers.DateTimeField() 
    esrb_rating = serializers.CharField(max_length=150) 
    played_once = serializers.BooleanField(required=False) 
    played_times = serializers.IntegerField(required=False) 

    def create(self, validated_data): 
        return Game.objects.create(**validated_data) 

    def update(self, instance, validated_data): 
        instance.name = validated_data.get('name',  
            instance.name) 
        instance.release_date = validated_data.get('release_date',  
            instance.release_date) 
        instance.esrb_rating = validated_data.get('esrb_rating',  
            instance.esrb_rating) 
        instance.played_once = validated_data.get('played_once',  
            instance.played_once) 
        instance.played_times = validated_data.get('played_times',  
            instance.played_times) 
        instance.save() 
        return instance 

GameSerializer 类声明了代表我们想要序列化的字段的属性。请注意,我们省略了在 Game 模型中存在的 created_timestamp 属性。当对这个类调用继承的 save 方法时,重写的 createupdate 方法定义了如何创建或修改实例。实际上,这些方法必须在我们的类中实现,因为它们在其基类 Serializer 中的声明中只是抛出一个 NotImplementedError 异常。

create 方法接收 validated_data 参数中的验证数据。代码根据接收到的验证数据创建并返回一个新的 Game 实例。

update 方法接收一个正在更新的现有 Game 实例以及 instancevalidated_data 参数中的新验证数据。代码使用从验证数据中检索到的更新属性值来更新实例的属性值,为更新的 Game 实例调用保存方法,并返回更新并保存的实例。

我们可以在启动默认 Python 交互式外壳之前使其包含所有 Django 项目模块。这样,我们可以检查序列化器是否按预期工作。此外,它将帮助我们理解 Django 中的序列化是如何工作的。

运行以下命令以启动交互式外壳。确保你在终端或命令提示符中的 games_service 文件夹内:

    python manage.py shell

你会注意到,在介绍默认 Python 交互式外壳的常规行之后,会显示一行说 (InteractiveConsole) 的内容。在 Python 交互式外壳中输入以下代码以导入测试 Game 模型和其序列化器所需的所有内容。示例代码文件包含在 restful_python_2_05_01 文件夹中,位于 Django01/cmd/serializers_test_01.py 文件中:

from datetime import datetime 
from django.utils import timezone 
from django.utils.six import BytesIO 
from rest_framework.renderers import JSONRenderer 
from rest_framework.parsers import JSONParser 
from games.models import Game 
from games.serializers import GameSerializer 

现在输入以下代码以创建两个Game模型的实例并将它们保存。示例的代码文件包含在restful_python_2_05_01文件夹中,位于Django01/cmd/serializers_test_01.py文件中:

gamedatetime = timezone.make_aware(datetime.now(), timezone.get_current_timezone()) 
game1 = Game(name='PAW Patrol: On A Roll!', release_date=gamedatetime, esrb_rating='E (Everyone)') 
game1.save() 
game2 = Game(name='Spider-Man', release_date=gamedatetime, esrb_rating='T (Teen)') 
game2.save()

执行前面的代码后,我们可以使用之前介绍的命令行命令或 GUI 工具检查 SQLite 数据库中的games_game表的内容。我们会注意到表中有两行,列的值是我们提供给不同Game实例的不同属性的值。然而,请确保你在另一个终端或命令提示符中运行命令,以避免留下我们将继续使用的交互式 shell。以下截图显示了games_game表的内容:

图片

在交互式 shell 中输入以下命令以检查保存的Game实例的标识符值以及包含我们保存实例到数据库的日期和时间的created_timestamp属性值。示例的代码文件包含在restful_python_2_05_01文件夹中,位于Django01/cmd/serializers_test_01.py文件中:

print(game1.id) 
print(game1.name) 
print(game1.created_timestamp) 
print(game2.id) 
print(game2.name) 
print(game2.created_timestamp) 

现在,让我们编写以下代码以序列化第一个游戏实例(game1)。示例的代码文件包含在restful_python_2_05_01文件夹中,位于Django01/cmd/serializers_test_01.py文件中:

game_serializer1 = GameSerializer(game1) 
print(game_serializer1.data) 

以下行显示了生成的字典,具体来说,是一个rest_framework.utils.serializer_helpers.ReturnDict实例:

{'id': 1, 'name': 'PAW Patrol: On A Roll!', 'release_date': '2018-10-24T17:47:30.177610Z', 'esrb_rating': 'E (Everyone)', 'played_once': False, 'played_times': 0}

现在,让我们序列化第二个游戏实例(game2)。示例的代码文件包含在restful_python_2_05_01文件夹中,位于Django01/cmd/serializers_test_01.py文件中:

game_serializer2 = GameSerializer(game2) 
print(game_serializer2.data) 

以下行显示了生成的字典:

{'id': 2, 'name': 'Spider-Man', 'release_date': '2018-10-24T17:47:30.177610Z', 'esrb_rating': 'T (Teen)', 'played_once': False, 'played_times': 0}

我们可以使用rest_framework.renderers.JSONRenderer类轻松地将data属性中持有的字典渲染成 JSON。以下行创建了这个类的实例,然后调用render方法将data属性中持有的字典渲染成它们的 JSON 表示。示例的代码文件包含在restful_python_2_05_01文件夹中,位于Django01/cmd/serializers_test_01.py文件中:

renderer = JSONRenderer() 
rendered_game1 = renderer.render(game_serializer1.data) 
rendered_game2 = renderer.render(game_serializer2.data) 
print(rendered_game1) 
print(rendered_game2) 

以下行显示了从render方法调用生成的输出:

    b'{"id":1,"name":"PAW Patrol: On A Roll!","release_date":"2018-10-24T17:47:30.177610Z","esrb_rating":"E (Everyone)","played_once":false,"played_times":0}'
    b'{"id":2,"name":"Spider-Man","release_date":"2018-10-24T17:47:30.177610Z","esrb_rating":"T (Teen)","played_once":false,"played_times":0}'

现在,我们将反向操作,从序列化数据到Game实例的种群。以下行从 JSON 字符串(序列化数据)生成一个新的Game实例;也就是说,我们将编写反序列化代码。示例的代码文件包含在restful_python_2_05_01文件夹中,位于Django01/cmd/serializers_test_01.py文件中:

json_string_for_new_game = '{"name":"Tomb Raider Extreme Edition","release_date":"2016-05-18T03:02:00.776594Z","game_category":"3D RPG","played":false}' 
json_bytes_for_new_game = bytes(json_string_for_new_game , encoding="UTF-8") 
stream_for_new_game = BytesIO(json_bytes_for_new_game) 
parser = JSONParser() 
parsed_new_game = parser.parse(stream_for_new_game) 
print(parsed_new_game) 

第一行创建了一个新的字符串,其中包含定义新游戏的 JSON(json_string_for_new_game)。然后,代码将字符串转换为bytes,并将转换结果保存在json_bytes_for_new_game变量中。django.utils.six.BytesIO类提供了一个使用内存字节数组的缓冲 I/O 实现。代码使用这个类从之前生成的序列化数据字节数组json_bytes_for_new_game创建一个流,并将生成的实例保存在stream_for_new_game变量中。

我们可以使用rest_framework.parsers.JSONParser类轻松地将流反序列化并解析成 Python 模型。下一行创建了这个类的实例,然后使用stream_for_new_game作为参数调用parse方法,将流解析成 Python 原生数据类型,并将结果保存在parsed_new_game变量中。

执行前面的行后,parsed_new_game包含从流中解析出的 Python 字典。以下行显示了执行前面的代码片段后生成的输出:

{'name': 'Red Dead Redemption 2', 'release_date': '2018-10-26T01:01:00.776594Z', 'esrb_rating': 'M (Mature)'}

以下行使用GameSerializer类从流中解析出的 Python 字典生成一个名为new_game的完全填充的Game实例。示例代码文件包含在restful_python_2_05_01文件夹中,位于Django01/cmd/serializers_test_01.py文件中:

new_game_serializer = GameSerializer(data=parsed_new_game) 
if new_game_serializer.is_valid(): 
    new_game = new_game_serializer.save() 
    print(new_game.name) 

首先,代码使用我们从流中解析出的 Python 字典(parsed_new_game)作为data关键字参数创建GameSerializer类的实例。然后,代码调用is_valid方法来确定数据是否有效。

注意,当我们尝试在创建序列化器时传递data关键字参数以访问序列化数据表示时,我们必须始终调用is_valid

如果方法返回true,我们可以访问data属性中的序列化表示,因此代码调用save方法将相应的行插入数据库,并返回一个完全填充的Game实例,保存在new_game局部变量中。然后,代码打印出完全填充的Game实例中的一个属性,名为new_game

如前述代码所示,Django REST Framework 使得从对象到 JSON 的序列化和从 JSON 到对象的反序列化变得简单,这是我们 RESTful Web API 的核心要求,必须执行 CRUD 操作。

输入以下命令以退出包含我们开始测试序列化和反序列化的 Django 项目模块的 shell:

quit() 

理解响应状态码

Django REST Framework 在status模块中声明了一组用于不同 HTTP 状态码的命名常量。我们将始终使用这些命名常量来返回 HTTP 状态码。

返回数字作为状态代码是不良的做法。我们希望我们的代码易于阅读和理解,因此,我们将使用描述性的 HTTP 状态代码。

例如,如果我们必须返回404 Not Found状态代码,我们将返回status.HTTP_404_NOT_FOUND,而不是仅仅404。如果我们必须返回201 Created状态代码,我们将返回status.HTTP_201_CREATED,而不是仅仅201

编写 API 视图

现在我们将创建 Django 视图,这些视图将使用之前创建的GameSerializer类来为 API 处理的每个 HTTP 请求返回 JSON 表示。打开位于games_service/games文件夹中的views.py文件。以下行显示了该文件的初始代码,只有一个导入语句和一个注释,表明我们应该创建视图:

from django.shortcuts import render 

# Create your views here. 

将现有代码替换为以下行。新代码创建了一个JSONResponse类,并声明了两个函数:game_collectiongame_detail。我们正在创建 API 的第一个版本,我们使用函数来使代码尽可能简单。在下一个示例中,我们将使用类和更复杂的代码。突出显示的行显示了评估request.method属性值的表达式,以确定基于 HTTP 动词要执行的操作。示例代码文件包含在restful_python_2_05_01文件夹中,位于Django01/games-service/games/views.py文件中:

from django.http import HttpResponse 
from django.views.decorators.csrf import csrf_exempt 
from rest_framework.renderers import JSONRenderer 
from rest_framework.parsers import JSONParser 
from rest_framework import status 
from games.models import Game 
from games.serializers import GameSerializer 

class JSONResponse(HttpResponse): 
    def __init__(self, data, **kwargs): 
        content = JSONRenderer().render(data) 
        kwargs['content_type'] = 'application/json' 
        super(JSONResponse, self).__init__(content, **kwargs) 

@csrf_exempt 
def game_collection(request): 
    if request.method == 'GET': 
        games = Game.objects.all() 
        games_serializer = GameSerializer(games, many=True) 
        return JSONResponse(games_serializer.data) 
    elif request.method == 'POST': 
        game_data = JSONParser().parse(request) 
        game_serializer = GameSerializer(data=game_data) 
        if game_serializer.is_valid(): 
            game_serializer.save() 
            return JSONResponse(game_serializer.data,  
                status=status.HTTP_201_CREATED) 
        return JSONResponse(game_serializer.errors,  
            status=status.HTTP_400_BAD_REQUEST) 

@csrf_exempt 
def game_detail(request, id): 
    try: 
        game = Game.objects.get(id=id) 
    except Game.DoesNotExist: 
        return HttpResponse(status=status.HTTP_404_NOT_FOUND) 
    if request.method == 'GET': 
        game_serializer = GameSerializer(game) 
        return JSONResponse(game_serializer.data) 
    elif request.method == 'PUT': 
        game_data = JSONParser().parse(request) 
        game_serializer = GameSerializer(game,  
            data=game_data) 
        if game_serializer.is_valid(): 
            game_serializer.save() 
            return JSONResponse(game_serializer.data) 
        return JSONResponse(game_serializer.errors,  
            status=status.HTTP_400_BAD_REQUEST) 
 elif request.method == 'DELETE': 
        game.delete() 
        return HttpResponse(status=status.HTTP_204_NO_CONTENT) 

JSONResponse类是django.http.HttpResponse类的子类。超类表示一个以字符串为内容的 HTTP 响应。JSONResponse类将其内容渲染为 JSON。该类仅声明了__init__方法,该方法创建了一个rest_framework.renderers.JSONRenderer实例,并调用其render方法将接收到的数据渲染为 JSON,并将返回的bytestring保存到content局部变量中。然后,代码将'content_type'键添加到响应头中,其值为'application/json'。最后,代码调用基类的初始化器,传递 JSON bytestring和添加到头部的键值对。这样,该类代表了一个我们用于两个函数的 JSON 响应,以便轻松返回 JSON 响应。

代码在两个函数中使用@csrf_exempt装饰器来确保视图设置一个CSRF(即跨站请求伪造)cookie。我们这样做是为了使测试这个示例变得简单,这个示例不代表一个生产就绪的 Web 服务。我们将在稍后的 RESTful API 中添加安全功能。

当 Django 服务器接收到 HTTP 请求时,Django 创建一个HttpRequest实例,具体是一个django.http.HttpRequest对象。此实例包含有关请求的元数据,包括 HTTP 动词。method属性提供了一个表示请求中使用的 HTTP 动词或方法的字符串。

当 Django 加载将处理请求的适当视图时,它将 HttpRequest 实例作为第一个参数传递给视图函数。视图函数必须返回一个 HttpResponse 实例,具体来说,是一个 django.http.HttpResponse 实例。

game_collection 函数列出所有游戏或创建一个新的游戏。该函数接收一个 HttpRequest 实例作为 request 参数。该函数能够处理两种 HTTP 动词:GETPOST。代码会检查 request.method 属性的值,以确定根据 HTTP 动词要执行的代码。如果 HTTP 动词是 GET,则 request.method == 'GET' 表达式将评估为 True,代码必须列出所有游戏。代码将从数据库检索所有 Game 对象,使用 GameSerializer 将它们全部序列化,并返回一个由 GameSerializer 生成数据的 JSONResponse 实例。代码使用 many=True 参数创建 GameSerializer 实例,以指定必须序列化多个实例,而不仅仅是单个实例。在底层,当 many 参数值设置为 True 时,Django 使用 ListSerializer

如果 HTTP 动词是 POST,则代码必须根据包含在 HTTP 请求中的 JSON 数据创建一个新的游戏。首先,代码使用一个 JSONParser 实例,并使用 request 作为参数调用其 parse 方法,以解析请求中提供的作为 JSON 数据的游戏数据,并将结果保存在 game_data 本地变量中。然后,代码使用之前检索到的数据创建一个 GameSerializer 实例,并调用 is_valid 方法以确定 Game 实例是否有效。如果实例有效,代码将调用 save 方法将实例持久化到数据库中,并返回一个包含保存数据的 JSONResponse,其状态等于 status.HTTP_201_CREATED,即 201 Created

game_detail 函数检索、更新或删除现有的游戏。该函数接收一个 HttpRequest 实例作为 request 参数,以及要检索、更新或删除的游戏的 ID 作为 id 参数。该函数能够处理三种 HTTP 动词:GETPUTDELETE。代码会检查 request.method 属性的值,以确定根据 HTTP 动词要执行的代码。无论 HTTP 动词是什么,该函数都会调用 Game.objects.get 方法,将接收到的 id 作为 id 参数,从数据库中根据指定的 ID 检索一个 Game 实例,并将其保存在 game 本地变量中。如果数据库中不存在具有指定 ID 的游戏,代码将返回一个状态等于 status.HTTP_404_NOT_FOUNDHttpResponse,即 404 Not Found

如果 HTTP 动词是GET,代码使用game作为参数创建一个GameSerializer实例,并在一个包含默认200 OK状态的JSONResponse中返回序列化游戏的 数据。代码返回检索到的游戏序列化为 JSON。

如果 HTTP 动词是PUT,代码必须根据 HTTP 请求中包含的 JSON 数据创建一个新的游戏,并使用它来替换现有的游戏。首先,代码使用一个JSONParser实例,并使用request作为参数调用其parse方法来解析请求中提供的作为JSON数据的游戏数据,并将结果保存在game_data局部变量中。然后,代码使用从数据库中检索到的Game实例game和将要替换现有数据的检索数据game_data创建一个GameSerializer实例。接着,代码调用is_valid方法来确定Game实例是否有效。如果实例有效,代码调用save方法将实例持久化到数据库中,并返回一个包含保存数据的JSONResponse和默认的200 OK状态。如果解析的数据没有生成有效的Game实例,代码返回一个状态等于status.HTTP_400_BAD_REQUESTJSONResponse,即400 Bad Request

如果 HTTP 动词是DELETE,代码调用从数据库中先前检索到的Game实例(game)的delete方法。对delete方法的调用擦除了games_game表中的底层行,因此,该游戏将不再可用。然后,代码返回一个状态等于status.HTTP_204_NO_CONTENTJSONResponse,即204 No Content

现在我们必须在games_service/games文件夹中创建一个名为urls.py的新 Python 文件,具体来说,是games_service/games/urls.py文件。以下行显示了该文件的代码,该代码定义了 URL 模式,该模式指定了请求中必须匹配的正则表达式,以运行在views.py文件中定义的特定函数。示例的代码文件包含在restful_python_2_05_01文件夹中,在Django01/games-service/games/urls.py文件中:

from django.conf.urls import url 
from games import views 

urlpatterns = [ 
    url(r'^games/$', views.game_collection), 
    url(r'^games/(?P<id>[0-9]+)/$', views.game_detail), 
] 

urlpatterns列表使得将 URL 路由到视图成为可能。代码使用django.conf.urls.url函数调用必须匹配的正则表达式,以及定义在视图模块中的视图函数作为参数,为urlpatterns列表中的每个条目创建一个RegexURLPattern实例。

现在,我们必须替换 Django 在 games_service 文件夹中自动生成的 urls.py 中的代码,具体来说,是 games_service/urls.py 文件。不要将此文件与之前创建并保存在另一个文件夹中的 urls.py 文件混淆。games_service/urls.py 文件定义了根 URL 配置,因此我们必须包含之前编码的 games_service/games/urls.py 文件中声明的 URL 模式。以下行显示了 games_service/urls.py 文件的新代码。示例代码文件包含在 restful_python_2_05_01 文件夹中,位于 Django01/games-service/urls.py 文件:

from django.conf.urls import url, include 

urlpatterns = [ 
    url(r'^', include('games.urls')), 
] 

向 Django API 发送 HTTP 请求

现在,我们可以启动 Django 的开发服务器来编写和发送 HTTP 请求到我们的不安全 Web API(我们肯定会添加安全性)。执行以下命令:

    python manage.py runserver

以下行显示了执行上一个命令后的输出。开发服务器正在监听端口 8000

    Performing system checks...

    System check identified no issues (0 silenced).
    October 24, 2018 - 19:58:03
    Django version 2.1.2, using settings 'games_service.settings'
    Starting development server at http://127.0.0.1:8000/
    Quit the server with CONTROL-C. 

使用上一个命令,我们将启动 Django 开发服务器,并且我们只能在我们的开发计算机上访问它。 ...

使用命令行工具 - curl 和 httpie

我们将开始使用我们在 第一章 中介绍的 curl 和 HTTPie 命令行工具来编写和发送 HTTP 请求,该章名为 使用 Flask 1.0.2 开发 RESTful API 和微服务,在名为 使用命令行工具 - curl 和 httpie 的部分。在执行下一个示例之前,请确保您已阅读此部分。

每当我们使用命令行编写 HTTP 请求时,我们将使用同一命令的两个版本:第一个使用 HTTPie,第二个使用 curl。这样,您就可以使用最方便的一种。

确保您让 Django 开发服务器继续运行。不要关闭运行此开发服务器的终端或命令提示符。在 macOS 或 Linux 中打开一个新的终端,或在 Windows 中打开一个命令提示符,然后运行以下命令。我们将编写并发送一个 HTTP 请求来创建一个新的通知。示例代码文件包含在 restful_python_2_05_01 文件夹中,位于 Django01/cmd/cmd01.txt 文件:

    http ":8000/games/"

以下是对应的 curl 命令。示例代码文件包含在 restful_python_2_05_01 文件夹中,位于 Django01/cmd/cmd02.txt 文件:

    curl -iX GET "localhost:8000/games/"

在指定的情况下,您必须输入结束斜杠(/)非常重要,因为 /service/notifications 不会匹配任何配置的 URL 路由。因此,我们必须输入 /service/notifications/,包括结束斜杠(/)。

之前的命令将编写并发送以下 HTTP 请求:GET http://localhost:8000/games/。这个请求是我们 RESTful API 中最简单的情况,因为它将匹配并运行 views.game_collection 函数,即 game_service/games/views.py 文件中声明的 game_collection 函数。该函数只接收 request 作为参数,因为 URL 模式不包含任何参数。由于请求的 HTTP 动词是 GET,因此 request.method 属性等于 'GET',因此该函数将执行检索所有 Game 对象并生成包含所有这些序列化 Game 对象的 JSON 响应的代码。

以下行显示了 HTTP 请求的一个示例响应,其中 JSON 响应包含三个 Game 对象:

    HTTP/1.1 200 OK
    Content-Length: 438
    Content-Type: application/json
    Date: Wed, 24 Oct 2018 20:25:45 GMT
    Server: WSGIServer/0.2 CPython/3.7.1
    X-Frame-Options: SAMEORIGIN

    [
        {
            "esrb_rating": "E (Everyone)",
            "id": 1,
            "name": "PAW Patrol: On A Roll!",
            "played_once": false,
            "played_times": 0,
            "release_date": "2018-10-24T17:47:30.177610Z"
        },
        {
            "esrb_rating": "M (Mature)",
            "id": 3,
            "name": "Red Dead Redemption 2",
            "played_once": false,
            "played_times": 0,
            "release_date": "2018-10-26T01:01:00.776594Z"
        },
        {
            "esrb_rating": "T (Teen)",
            "id": 2,
            "name": "Spider-Man",
            "played_once": false,
            "played_times": 0,
            "release_date": "2018-10-24T17:47:30.177610Z"
        }
    ]

在我们运行请求后,将在运行 Django 开发服务器的窗口中看到以下行。输出表明服务器接收了一个带有 GET 动词和 /games/ 作为 URI 的 HTTP 请求。服务器处理了 HTTP 请求,返回的状态码为 200,响应长度等于 438 个字符。响应长度可能不同,因为分配给每个游戏的 id 值将对响应长度产生影响。HTTP/1.1." 后的第一个数字表示返回的状态码(200),第二个数字表示响应长度(438):

    [24/Oct/2018 20:25:45] "GET /games/ HTTP/1.1" 200 438 

以下截图显示了 macOS 上并排的两个终端窗口。左侧的终端窗口正在运行 Django 开发服务器,并显示接收和处理的 HTTP 请求。右侧的终端窗口正在运行 http 命令以生成 HTTP 请求。在编写和发送 HTTP 请求时使用类似的配置来检查输出是一个好主意:

现在,我们将从上一个列表中选择一个游戏,并编写一个 HTTP 请求来检索所选的游戏。例如,在上一个列表中,第一个游戏的 id 值等于 3。运行以下命令来检索此游戏。使用上一个命令中检索到的第一个游戏的 id 值,因为 id 号码可能不同。示例代码文件包含在 restful_python_2_05_01 文件夹中,位于 Django01/cmd/cmd03.txt 文件:

    http ":8000/games/3/"

以下是对应的 curl 命令。示例代码文件包含在 restful_python_2_05_01 文件夹中,位于 Django01/cmd/cmd04.txt 文件:

    curl -iX GET "localhost:8000/games/3/"

之前的命令将组合并发送以下 HTTP 请求:GET http://localhost:8000/games/3/。请求在 /games/ 后面有一个数字,因此,它将匹配 '^games/(?P<id>[0-9]+)/$' 并运行 views.game_detail 函数,即 games_service/games/views.py 文件中声明的 game_detail 函数。该函数接收 requestid 作为参数,因为 URL 模式将 /games/ 后面指定的数字作为 id 参数传递。由于请求的 HTTP 动词是 GETrequest.method 属性等于 'GET',因此,该函数将执行检索与作为参数接收的 id 值匹配的 Game 对象的代码,如果找到,则生成包含此 Game 对象序列化的 JSON 响应。以下行显示了 HTTP 请求的示例响应,其中包含 JSON 响应中与 id 值匹配的 Game 对象:

    HTTP/1.1 200 OK
    Content-Length: 148
    Content-Type: application/json
    Date: Wed, 24 Oct 2018 22:04:50 GMT
    Server: WSGIServer/0.2 CPython/3.7.1
    X-Frame-Options: SAMEORIGIN

    {
        "esrb_rating": "M (Mature)",
        "id": 3,
        "name": "Red Dead Redemption 2",
        "played_once": false,
        "played_times": 0,
        "release_date": "2018-10-26T01:01:00.776594Z"
    }

现在我们将组合并发送一个 HTTP 请求以检索一个不存在的游戏。例如,在之前的列表中,没有 id 值等于 888 的游戏。运行以下命令尝试检索此游戏。确保您使用一个不存在的 id 值。我们必须确保实用工具将标题作为响应的一部分显示,因为响应将没有主体。示例代码文件包含在 restful_python_2_05_01 文件夹中,在 Django01/cmd/cmd05.txt 文件中:

    http ":8000/games/888/"

以下是对应的 curl 命令。示例代码文件包含在 restful_python_2_05_01 文件夹中,在 Django01/cmd/cmd06.txt 文件中:

    curl -iX GET "localhost:8000/games/888/"

之前的命令将组合并发送以下 HTTP 请求:GET http://localhost:8000/games/888/。请求与之前我们分析过的请求相同,只是 id 参数的数字不同。服务器将运行 views.game_detail 函数,即 games_service/games/views.py 文件中声明的 game_detail 函数。该函数将执行检索与作为参数接收的 id 值匹配的 Game 对象的代码,并抛出并捕获 Game.DoesNotExist 异常,因为没有与指定的 id 值匹配的游戏。因此,代码将返回 HTTP 404 Not Found 状态码。以下行显示了 HTTP 请求的示例响应头:

    HTTP/1.1 404 Not Found
    Content-Length: 0
    Content-Type: text/html; charset=utf-8
    Date: Wed, 24 Oct 2018 22:12:02 GMT
    Server: WSGIServer/0.2 CPython/3.7.1
    X-Frame-Options: SAMEORIGIN

现在运行以下命令来组合并发送一个 HTTP POST 请求以创建一个新的游戏。示例代码文件包含在 restful_python_2_05_01 文件夹中,在 Django01/cmd/cmd07.txt 文件中:

http POST ":8000/games/" name='Fortnite' esrb_rating='T (Teen)' release_date='2017-05-18T03:02:00.776594Z'

以下是对应的 curl 命令。使用 -H "Content-Type: application/json" 选项来指示 curl-d 选项之后指定的数据作为 application/json 发送,而不是默认的 application/x-www-form-urlencoded,这一点非常重要。示例代码文件包含在 restful_python_2_05_01 文件夹中,位于 Django01/cmd/cmd08.txt 文件:

curl -iX POST -H "Content-Type: application/json" -d '{"name":"Fortnite", "esrb_rating":"T (Teen)", "release_date": "2017-05-18T03:02:00.776594Z"}' "localhost:8000/games/"

之前的命令将组合并发送以下 HTTP 请求:POST http://localhost:8000/games/,并带有以下 JSON 键值对:

{ 
    "name": "Fortnite",  
    "esrb_rating": "T (Teen)",  
    "release_date": "2017-05-18T03:02:00.776594Z" 
}

请求指定了 /games/,因此它将匹配 '^games/$' 并运行 views.game_collection 函数,即 games_service/ames/views.py 文件中声明的 game_collection 函数。该函数仅接收 request 作为参数,因为 URL 模式不包含任何参数。由于请求的 HTTP 动词是 POST,因此 request.method 属性等于 'POST',因此函数执行解析请求中接收到的 JSON 数据的代码,创建一个新的 Game 对象,如果数据有效,则保存新的 Game 实例。如果新的 Game 实例成功持久化到数据库中,函数返回 HTTP 201 Created 状态码,并在响应体中将最近持久化的 Game 序列化为 JSON。以下行显示了 HTTP 请求的示例响应,其中包含 JSON 响应中的新 Game 对象:

    HTTP/1.1 201 Created
    Content-Length: 133
    Content-Type: application/json
    Date: Wed, 24 Oct 2018 22:18:36 GMT
    Server: WSGIServer/0.2 CPython/3.6.2
    X-Frame-Options: SAMEORIGIN

    {
        "esrb_rating": "T (Teen)",
        "id": 4,
        "name": "Fortnite",
        "played_once": false,
        "played_times": 0,
        "release_date": "2017-05-18T03:02:00.776594Z"
    }

现在我们运行以下命令来组合并发送一个 HTTP PUT 请求以更新现有的游戏,具体来说,用新的游戏替换之前添加的游戏。我们必须检查之前响应中分配给 id 的值,并将命令中的 4 替换为返回的值。例如,如果 id 的值为 8,则应使用 games/8/ 而不是 games/4/。示例代码文件包含在 restful_python_2_05_01 文件夹中,位于 Django01/cmd/cmd09.txt 文件:

 http PUT ":8000/games/4/" name='Fortnite Battle Royale' esrb_rating='T (Teen)' played_once=true played_times=3 release_date='2017-05-20T03:02:00.776594Z'

以下是对应的 curl 命令。与之前的 curl 示例一样,使用 -H "Content-Type: application/json" 选项来指示 curl-d 选项之后指定的数据作为 application/json 发送,而不是默认的 application/x-www-form-urlencoded,这一点非常重要。示例代码文件包含在 restful_python_2_05_01 文件夹中,位于 Django01/cmd/cmd10.txt 文件:

curl -iX PUT -H "Content-Type: application/json" -d '{"name":"Fortnite Battle Royale", "esrb_rating":"T (Teen)", "played_once": "true", "played_times": 3, "release_date": "2017-05-20T03:02:00.776594Z"}' "localhost:8000/games/4/"

之前的命令将组合并发送 HTTP 请求 PUT http://localhost:8000/games/15/,并带有以下 JSON 键值对:

{  
    "name": "Fortnite Battle Royale",  
    "esrb_rating": "T (Teen)",  
    "played_once": true,
     "played_times": 3, 
    "release_date": "2017-05-20T03:02:00.776594Z" 
} 

请求在/games/之后有一个数字,因此它将匹配'^games/(?P<id>[0-9]+)/$'并运行views.game_detail函数,即games_service/games/views.py文件中声明的game_detail函数。该函数接收requestid作为参数,因为 URL 模式将/games/之后指定的数字传递给id参数。由于请求的 HTTP 动词是PUT,所以request.method属性等于'PUT',因此函数执行解析请求中接收到的 JSON 数据的代码,从这些数据创建一个Game实例,并更新数据库中现有游戏的全部字段。如果游戏在数据库中成功更新,函数将返回 HTTP 200 OK状态码,并将最近更新的Game序列化为 JSON 格式放在响应体中。以下行显示了 HTTP 请求的一个示例响应,其中在 JSON 响应中显示了更新的Game对象:

    HTTP/1.1 200 OK
    Content-Length: 146
    Content-Type: application/json
    Date: Wed, 24 Oct 2018 22:27:36 GMT
    Server: WSGIServer/0.2 CPython/3.6.2
    X-Frame-Options: SAMEORIGIN

    {
        "esrb_rating": "T (Teen)",
        "id": 4,
        "name": "Fortnite Battle Royale",
        "played_once": true,
        "played_times": 3,
        "release_date": "2017-05-20T03:02:00.776594Z"
    }

为了成功处理更新现有游戏为新游戏的 HTTP PUT请求,我们必须为所有所需字段提供值。我们将组合并发送一个 HTTP 请求来尝试更新一个现有游戏,我们将无法做到这一点,因为我们只为名称提供了一个值。就像之前的请求一样,我们将使用最后添加的游戏中分配给id的值。示例的代码文件包含在restful_python_2_05_01文件夹中,在Django01/cmd/cmd11.txt文件中:

    http PUT ":8000/games/4/" name='Fortnite Forever'

以下是对应的curl命令。示例的代码文件包含在restful_python_2_05_01文件夹中,在Django01/cmd/cmd12.txt文件中:

  curl -iX PUT -H "Content-Type: application/json" -d '{"name":"Fortnite Forever"}' "localhost:8000/games/4/"

之前的命令将组合并发送 HTTP 请求PUT http://localhost:8000/games/15/,带有以下 JSON 键值对:

{  
    "name": "Fortnite Forever",  
} 

请求将执行我们之前解释的相同代码。因为我们没有提供Game实例所需的所有值,所以game_serializer.is_valid()方法将返回False,函数将返回 HTTP 400 Bad Request状态码,并且game_serializer.errors属性中生成的详细信息将序列化为 JSON 格式放在响应体中。以下行显示了 HTTP 请求的一个示例响应,其中列出了我们请求中没有包含值的所需字段,在 JSON 响应中使用字段名作为键,错误信息作为值:

    HTTP/1.1 400 Bad Request
    Content-Length: 86
    Content-Type: application/json
    Date: Wed, 24 Oct 2018 22:33:37 GMT
    Server: WSGIServer/0.2 CPython/3.6.2
    X-Frame-Options: SAMEORIGIN

    {
        "esrb_rating": [
            "This field is required."
        ],
        "release_date": [
            "This field is required."
        ]
    }

当我们希望我们的 API 能够更新现有资源的一个字段时,在这种情况下,一个现有的游戏,我们应该提供一个PATCH方法的实现。PUT方法旨在替换整个资源,而PATCH方法旨在对现有资源应用一个增量。我们可以在PUT方法的处理器中编写代码来对现有资源应用一个增量,但使用PATCH方法来完成这个特定任务是一个更好的实践。当我们编写 API 的改进版本时,我们将使用PATCH方法。

现在运行以下命令以组合并发送一个 HTTP 请求来删除一个现有的游戏,特别是我们最后添加和更新的游戏。就像我们之前的 HTTP 请求一样,我们必须检查之前响应中分配给id的值,并将命令中的4替换为返回的值。示例代码文件包含在restful_python_2_05_01文件夹中的Django01/cmd/cmd13.txt文件:

    http DELETE ":8000/games/4/"  

以下是对应的curl命令。示例代码文件包含在restful_python_2_05_01文件夹中的Django01/cmd/cmd14.txt文件:

    curl -iX DELETE "localhost:8000/games/4/"  

之前的命令将组合并发送以下 HTTP 请求:DELETE http://localhost:8000/games/4/。请求在/games/之后有一个数字,因此,它将匹配'^games/(?P<id>[0-9]+)/$'并运行views.game_detail函数,即games_service/views.py文件中声明的game_detail函数。该函数接收requestid作为参数,因为 URL 模式将/games/之后指定的数字传递给id参数。由于请求的 HTTP 动词是DELETE,所以request.method属性等于'DELETE',因此,该函数将执行解析请求中接收到的 JSON 数据的代码,从这些数据创建一个Game实例,并在数据库中删除现有的游戏。如果游戏在数据库中成功删除,则函数返回 HTTP 204 No Content状态码。

以下行显示了在成功删除现有游戏后对 HTTP 请求的示例响应:

    HTTP/1.1 204 No Content
    Content-Length: 0
    Content-Type: text/html; charset=utf-8
    Date: Wed, 24 Oct 2018 22:39:15 GMT
    Server: WSGIServer/0.2 CPython/3.6.2
    X-Frame-Options: SAMEORIGIN

使用 GUI 工具 - Postman 和其他工具

到目前为止,我们一直在使用两个基于终端或命令行工具来组合并发送 HTTP 请求到我们的 Django 开发服务器:cURL 和 HTTPie。现在我们将使用 Postman,这是我们用于在第一章中组合并发送 HTTP 请求到 Flask 开发服务器的 GUI 工具之一:Developing RESTful APIs and Microservices with Flask 1.0.2。如果你跳过了这一章,请确保检查该章节中名为使用 GUI 工具 - Postman 和其他工具的部分中的安装说明。

一旦启动 Postman,请确保关闭提供常见任务快捷方式的模态对话框。在左上角的+ new 下拉菜单中选择 GET 请求 ...

测试你的知识

让我们看看你是否能正确回答以下问题:

  1. 以下哪个命令运行脚本以创建一个名为recipes的新 Django 应用程序:

    1. python django.py startapp recipes

    2. python manage.py startapp recipes

    3. python starapp.py recipes

  2. 以下哪个字符串必须添加到INSTALLED_APPS变量中,以便在 Django 应用程序中添加 Django REST Framework:

    1. 'rest-framework'

    2. 'django-rest-framework'

    3. 'rest_framework'

  3. Django 的 ORM:

    1. 集成到 Django 中

    2. 必须在 Django 中配置为可选组件

    3. 必须在配置 SQLAlchemy 之后安装

  4. 在 Django REST Framework 中,序列化器作为:

    1. 视图函数和 Python 基本类型之间的中介

    2. URL 和视图函数之间的中介

    3. 模型实例和 Python 基本类型之间的中介

  5. urls.py文件中声明的urlpatterns列表使得:

    1. 将 URL 路由到模型

    2. 将 URL 路由到 Python 基本类型

    3. 将 URL 路由到视图

  6. 在 Django REST Framework 中,解析器和渲染器作为中介处理:

    1. 模型实例和 Python 基本类型

    2. Python 基本类型和 HTTP 请求与响应

    3. URL 和视图函数

  7. 如果我们想在 Django REST Framework 中创建一个简单的Game模型来表示和持久化游戏,我们可以创建:

    1. 一个作为django.db.models.Model超类子类的Game

    2. 一个作为djangorestframework.models.Model超类子类的Game

    3. restframeworkmodels.py文件中的Game函数

摘要

在本章中,我们设计了一个 RESTful API 来与简单的 SQLite 数据库交互,并使用游戏执行 CRUD 操作。我们定义了 API 的要求,并理解了每个 HTTP 方法执行的任务。我们使用 Django 和 Django REST Framework 设置了虚拟环境。

我们创建了一个模型来表示和持久化游戏,并在 Django 中执行了迁移。我们学会了使用 Django REST Framework 管理游戏实例的序列化和反序列化到 JSON 表示。我们编写了 API 视图来处理不同的 HTTP 请求,并配置了 URL 模式列表将 URL 路由到视图。

最后,我们启动了 Django 开发服务器,并使用命令行工具来组合和发送 HTTP 请求...

第六章:在 Django 2.1 中使用基于类的视图和超链接 API

在本章中,我们将扩展我们在上一章中开始构建的 RESTful API 的功能。我们将更改 ORM 设置以与更强大的 PostgreSQL 10.5 数据库一起工作,并利用Django REST FrameworkDRF)中包含的先进功能,这些功能允许我们减少复杂 API(如基于类的视图)的样板代码。我们将查看以下内容:

  • 使用模型序列化器来消除重复代码

  • 使用包装器编写 API 视图

  • 使用默认解析和渲染选项并超越 JSON

  • 浏览 API

  • 设计一个与复杂的 PostgreSQL 10.5 数据库交互的 RESTful API

  • 理解每种 HTTP 方法执行的任务

  • 使用模型声明关系

  • 使用要求文件安装与 PostgreSQL 一起工作的包

  • 配置数据库

  • 运行迁移

  • 验证 PostgreSQL 数据库的内容

  • 使用关系和超链接管理序列化和反序列化

  • 创建基于类的视图并使用通用类

  • 充分利用基于类的通用视图

  • 与 API 端点一起工作

  • 浏览具有关系的 API

  • 创建和检索相关资源

使用模型序列化器来消除重复代码

我们在第五章“使用 Django 2.1 开发 RESTful API”中编写的GameSerializer类声明了许多与我们在Game模型中使用的相同名称的属性,并重复了诸如字段类型和max_length值等信息。GameSerializer类是rest_framework.serializers.Serializer超类的子类,并声明了我们将手动映射到适当类型的属性,并重写了createupdate方法。

现在我们将创建一个新的GameSerializer类版本,它将继承自rest_framework.serializers.ModelSerializer超类。ModelSerializer类自动填充一组默认字段和一组默认...

使用包装器编写 API 视图

我们在games_service/games/views.py文件中编写的代码声明了一个JSONResponse类和两个基于函数的视图。这些函数在需要返回 JSON 数据时返回JSONResponse,而在响应只是一个 HTTP 状态码时返回django.Http.Response.HttpResponse实例。因此,无论 HTTP 请求头中指定的接受内容类型是什么,视图函数始终在响应体中提供相同的内容——JSON。

运行以下两个命令以检索具有不同Accept请求头值的所有游戏:text/htmlapplication/json。示例代码文件包含在restful_python_2_06_01文件夹中的Django01/cmd/cmd601.txt文件中:

    http -v ":8000/games/" "Accept:text/html"
    http -v ":8000/games/" "Accept:application/json"

以下是对应的curl命令。示例代码文件包含在restful_python_2_06_01文件夹中的Django01/cmd/cmd602.txt文件中:

    curl -H "Accept: text/html" -viX GET "localhost:8000/games/"
    curl -H "Accept: application/json" -viX GET "localhost:8000/games/"

之前的命令将组合并发送以下 HTTP 请求:GET http://localhost:8000/games/。我们已经请求了 httpcurl 使用 -v 选项启用详细模式,它们指定了更多关于操作的信息,并显示了整个请求,包括请求头。

第一个命令为 Accept 请求头定义了 text/html 值。第二个命令为 Accept 请求头定义了 application/json 值。您将注意到这两个命令产生了相同的结果,因此,视图函数不会考虑 HTTP 请求中指定的 Accept 请求头值。这两个命令的响应头将包括以下行:

    Content-Type: application/json

第二次请求指定它只接受 text/html,但响应中包含 JSON 主体,即 application/json 内容。因此,我们第一个版本的 RESTful API 没有准备好渲染除 JSON 之外的内容。我们将进行一些更改,以使 API 能够渲染其他内容。

当我们对 RESTful API 中资源或资源集合支持的方法有疑问时,我们可以使用 OPTIONS HTTP 动词和资源或资源集合的 URL 组合并发送 HTTP 请求。如果 RESTful API 为资源或资源集合实现了 OPTIONS HTTP 动词,它会在响应的 Allow 头部提供一个以逗号分隔的 HTTP 动词或方法列表,表示它支持的操作。此外,响应头还将包括有关其他支持选项的附加信息,例如它能够从请求中解析的内容类型以及它能够在响应中呈现的内容类型。

例如,如果我们想知道游戏集合支持哪些 HTTP 动词,我们可以运行以下命令。示例代码文件包含在 restful_python_2_06_01 文件夹中,在 Django01/cmd/cmd603.txt 文件中:

    http OPTIONS ":8000/games/"

下面的命令是等效的 curl 命令。示例代码文件包含在 restful_python_2_06_01 文件夹中,在 Django01/cmd/cmd604.txt 文件中:

    curl -iX OPTIONS "localhost:8000/games/"

之前的命令将组合并发送以下 HTTP 请求:OPTIONS http://localhost:8000/games/。该请求将匹配并运行 views.game_collection 函数,即位于 game_service/games/views.py 文件中声明的 game_collection 函数。此函数仅在 request.method 等于 'GET''POST' 时运行代码。在这种情况下,request.method 等于 'OPTIONS',因此,该函数不会运行任何代码,也不会返回任何响应,特别是,它不会返回 HttpResponse 实例。因此,我们将看到下一张截图在 Django 开发服务器控制台输出中显示的 Internal Server Error

图片

以下行显示了输出头,其中还包括一个包含关于错误详细信息的巨大 HTML 文档,因为 Django 的调试模式已被激活。我们收到500 内部服务器错误状态码。请注意,您需要在终端或命令提示符中向上滚动以找到这些行:

    HTTP/1.1 500 Internal Server Error
    Content-Length: 51566
    Content-Type: text/html
    Date: Thu, 25 Oct 2018 04:14:09 GMT
    Server: WSGIServer/0.2 CPython/3.7.1
    Vary: Cookie
    X-Frame-Options: SAMEORIGIN

显然,我们希望提供一个更一致的 API,并且当我们收到对游戏资源或游戏集合的OPTIONS动词的请求时,我们希望提供一个准确的响应。

如果我们使用OPTIONS动词对游戏资源发送组合和发送 HTTP 请求,我们将看到相同的错误,并且我们将得到类似的响应,因为views.game_detail函数仅在request.method等于'GET''PUT''DELETE'时运行代码。

以下命令将在我们尝试查看 ID 等于2的游戏资源提供的选项时产生所解释的错误。别忘了将2替换为您配置中现有游戏的键值。

    http OPTIONS ":8000/games/3/"

以下是对应的curl命令。示例的代码文件包含在restful_python_2_06_01文件夹中,在Django01/cmd/cmd606.txt文件中:

    curl -iX OPTIONS "localhost:8000/games/3/"

我们只需要在games_service/games/views.py文件中做一些小的修改,以解决我们一直在分析我们 RESTful API 的问题。我们将使用在rest_framework.decorators模块中声明的有用的@api_view装饰器来处理基于函数的视图。这个装饰器允许我们指定我们的函数可以处理哪些 HTTP 动词。如果必须由视图函数处理的请求的 HTTP 动词不包括在作为@api_view装饰器的http_method_names参数指定的字符串列表中,则默认行为返回405 方法不允许状态码。这样,我们确保每当收到不在我们函数视图中考虑的 HTTP 动词时,我们不会生成意外的错误,因为装饰器处理了不受支持的 HTTP 动词或方法。

在底层,@api_view装饰器是一个包装器,它将基于函数的视图转换为rest_framework.views.APIView类的子类。这个类是 Django REST 框架中所有视图的基类。正如我们可能猜测的那样,如果我们想使用基于类的视图,我们可以创建继承自这个类的类,我们将获得与使用装饰器的基于函数的视图相同的优势。我们将在本章接下来的示例中开始使用基于类的视图。

此外,由于我们指定了一个支持 HTTP 动词的字符串列表,装饰器会自动为支持的OPTIONS HTTP 动词构建响应,包括支持的方法、解析器和渲染能力。我们实际的 API 版本仅能渲染 JSON 作为其输出。装饰器的使用确保了当 Django 调用我们的视图函数时,我们总是接收到rest_framework.request.Request类的实例作为request参数。装饰器还处理当我们的函数视图访问可能引起解析问题的request.data属性时产生的ParserError异常。

使用默认解析和渲染选项,并超越 JSON

APIView类为每个视图指定了默认设置,我们可以通过在games_service/settings.py文件中指定适当的值或在APIView超类子类中覆盖类属性来覆盖这些设置。正如我们所学的,APIView类在底层使用装饰器应用这些默认设置。因此,每次我们使用装饰器时,默认解析器类和默认渲染器类都将与函数视图相关联。

默认情况下,DEFAULT_PARSER_CLASSES配置变量的值为以下包含三个解析器类名的字符串元组:

( 'rest_framework.parsers.JSONParser', 'rest_framework.parsers.FormParser', ...

浏览 API

通过最近的编辑,我们使我们的 API 能够使用 Django REST Framework 中配置的默认内容渲染器,因此我们的 API 能够渲染text/html内容。我们可以利用 Django REST Framework 中包含的可浏览 API 功能,该功能在请求指定请求头中的Content-type键值为text/html时,为每个资源生成友好的 HTML 输出。

每当我们在一个网页浏览器中输入 API 资源的 URL 时,浏览器将需要 HTML 响应,因此 Django REST Framework 将提供由 Bootstrap 流行的前端组件库构建的 HTML 响应。您可以在getbootstrap.com了解更多关于 Bootstrap 的信息。此响应将包括一个显示资源内容的 JSON 部分、执行不同请求的按钮以及提交数据到资源的表单。与 Django REST Framework 中的所有内容一样,我们可以自定义用于生成可浏览 API 的模板和主题。

打开一个网页浏览器并输入http://localhost:8000/games/。可浏览 API 将组合并发送一个 HTTP GET请求到/games/,并将显示其执行结果,即头部和 JSON 游戏列表。以下截图显示了在网页浏览器中输入 URL 后渲染的网页,其中包含游戏列表的资源描述。

图片

如果你决定在另一台运行在 LAN 上的计算机或设备上的网页浏览器中浏览 API,请记住你必须使用开发计算机分配的 IP 地址,而不是 localhost。例如,如果计算机分配的 IPv4 IP 地址是192.168.1.103,那么你应该使用http://192.168.1.103:8000/games/而不是http://localhost:8000/games/。当然,你也可以使用主机名而不是 IP 地址。

可浏览的 API 使用关于资源允许的方法的信息,为我们提供运行这些方法的按钮。在资源描述的右侧,可浏览的 API 显示了一个 OPTIONS 按钮和一个 GET 下拉按钮。OPTIONS 按钮允许我们向/games/发送OPTIONS请求,即当前资源。GET 下拉按钮允许我们再次向/games/发送GET请求。如果我们点击或轻触向下箭头,我们可以选择 json 选项,可浏览的 API 将显示对/games/GET请求的原始 JSON 结果,而不显示头部信息。

在渲染网页的底部,可浏览的 API 为我们提供了一些控件来生成一个向/games/发送的POST请求。媒体类型下拉菜单允许我们在 API 配置的支持的解析器之间进行选择:

  • application/json

  • application/x-www-form-urlencoded

  • multipart/form-data

内容文本框允许我们指定要发送到POST请求的数据,格式与媒体类型下拉菜单中指定的一致。在媒体类型下拉菜单中选择 application/json,并在内容文本框中输入以下 JSON 内容:

{ 
    "name": "Assassin's Creed Origins", 
    "release_date": "2018-01-10T03:02:00.776594Z", 
    "esrb_rating": "M (Mature)" 
}

点击或轻触 POST。可浏览的 API 将组合并发送一个包含之前指定数据的POST请求到/games/,我们将在网页浏览器中看到调用结果。以下截图显示了一个网页浏览器在响应中显示了 HTTP 状态码201 Created,以及之前解释过的下拉菜单和文本框,其中包含 POST 按钮,允许我们继续编写并发送POST请求:

现在输入现有游戏资源的 URL,例如http://localhost:8000/games/7/。确保将7替换为之前渲染的“游戏列表”中现有游戏的 ID。可浏览的 API 将组合并发送一个 HTTP GET请求到/games/7/,并将显示其执行结果,即游戏的头部信息和 JSON 数据。以下截图显示了在网页浏览器中输入 URL 后的渲染网页,其中包含“游戏详情”的资源描述:

可浏览的 API 功能使我们能够轻松检查 API 的工作方式,并在任何可以访问我们局域网的 Web 浏览器中组合和发送不同方法的 HTTP 请求。我们将利用可浏览 API 中包含的附加功能,例如 HTML 表单,它允许我们在使用 Python 和 Django REST 框架构建了一个新的更复杂的 RESTful API 之后轻松创建新的资源。

设计一个 RESTful API 以与复杂的 PostgreSQL 10.5 数据库交互

到目前为止,我们的基于 Django 的 RESTful API 在 SQLite 数据库的单个数据库表上执行 CRUD 操作。现在,我们想要使用 Django REST 框架创建一个更复杂的 RESTful API,以与一个复杂的数据库模型交互,该模型必须允许我们为属于 ESRB 评分的游戏注册玩家分数。在我们之前的 RESTful API 中,我们使用一个字符串字段来指定游戏的 ESRB 评分。在这种情况下,我们希望能够轻松检索具有特定 ESRB 评分的所有游戏,因此,我们将有一个游戏和 ESRB 评分之间的关系。

我们必须能够在不同的相关资源上执行 CRUD 操作...

理解每种 HTTP 方法执行的任务

下表显示了我们的 API 必须支持的 HTTP 动词、范围和语义。每个方法由一个 HTTP 动词和一个范围组成,并且所有方法对所有资源和集合都有明确定义的意义:

HTTP 动词 范围 语义
GET ESRB 评分集合 获取集合中存储的所有 ESRB 评分,按描述升序排序。每个 ESRB 评分必须包括属于该评分的每个游戏资源的 URL 列表。
GET ESRB 评分 获取单个 ESRB 评分。ESRB 评分必须包括属于该评分的每个游戏资源的 URL 列表。
POST ESRB 评分集合 在集合中创建一个新的 ESRB 评分。
PUT ESRB 评分 更新现有的 ESRB 评分。
PATCH ESRB 评分 更新现有 ESRB 评分的一个或多个字段。
DELETE ESRB 评分 删除现有的 ESRB 评分。
GET 游戏集合 获取集合中存储的所有游戏,按名称升序排序。每个游戏必须包括其 ESRB 评分描述。
GET 游戏 获取单个游戏。游戏必须包括其 ESRB 评分描述。
POST 游戏集合 在集合中创建一个新的游戏。
PUT ESRB 评分 更新现有的游戏。
PATCH ESRB 评分 更新现有游戏的一个或多个字段。
DELETE ESRB 评分 删除现有的游戏。
GET 玩家集合 获取集合中所有存储的玩家,按姓名升序排序。每个玩家必须包括按分数降序排序的已注册分数列表。列表必须包括玩家获得的分数及其相关游戏的详细信息。
GET 玩家 获取单个玩家。玩家必须包括按分数降序排序的已注册分数列表。列表必须包括玩家获得的分数及其相关游戏的详细信息。
POST 玩家集合 在集合中创建一个新的玩家。
PUT 玩家 更新现有玩家。
PATCH 玩家 更新现有玩家的一个或多个字段。
DELETE 玩家 删除现有玩家。
GET 分数集合 获取集合中所有存储的分数,按分数降序排序。每个分数必须包括获得分数的玩家的姓名和游戏的名称。
GET 分数 获取单个分数。分数必须包括获得分数的玩家的姓名和游戏的名称。
POST 分数集合 在集合中创建一个新的分数。分数必须与现有玩家和现有游戏相关。
PUT 分数 更新现有分数。
PATCH 分数 更新现有分数的一个或多个字段。
DELETE 分数 删除现有分数。

我们希望我们的 API 能够更新现有资源的单个字段,因此我们将提供 PATCH 方法的实现。此外,我们的 RESTful API 必须支持所有资源和资源集合的 OPTIONS 方法。

我们将使用 Django REST 框架中包含的所有特性和可重用元素,以简化我们的 API 构建。我们将使用 PostgreSQL 10.5 数据库。如果你不想花时间安装 PostgreSQL,你可以跳过我们在 Django REST 框架 ORM 配置中做的更改,并继续使用默认的 SQLite 数据库。然而,强烈建议使用 PostgreSQL 作为数据库引擎。

在前一个表中,我们有一个大量方法和范围。以下列表列举了前一个表中提到的每个范围的 URI,其中 {id} 必须替换为资源的数字 ID:

  • ESRB 评分集合: /esrb-ratings/

  • ESRB 评分: /esrb-rating/{id}/

  • 游戏集合: /games/

  • 游戏: /game/{id}/

  • 玩家集合: /players/

  • 玩家: /player/{id}/

  • 分数集合: /player-scores/

  • 分数: /player-score/{id}/

让我们假设 http://localhost:8000/ 是 Django 开发服务器上运行的 API 的 URL。我们必须使用以下 HTTP 动词 (GET) 和请求 URL (http://localhost:8000/esrb-ratings/) 编排并发送一个 HTTP 请求,以检索集合中所有存储的 ESRB 评分:

GET http://localhost:8000/esrb-ratings/

声明与模型的关系

确保你已退出 Django 的开发服务器。记住,你只需要在运行该服务器的终端或命令提示符窗口中按 Ctrl + C。现在我们将创建我们将用来表示和持久化 ESRB 评分、游戏、玩家和分数以及它们之间关系的模型。

games_service/games 文件夹中打开 models.py 文件。将此文件中的代码替换为以下行。与其它模型相关的字段声明在代码列表中被突出显示。示例的代码文件包含在 restful_python_2_06_01 文件夹中,位于 Django01/games-service/games/models.py 文件:

from django.db import models class EsrbRating(models.Model): description ...

使用 requirements.txt 文件安装包以与 PostgreSQL 一起工作

确保你已退出 Django 开发服务器。你只需要在运行该服务器的终端或命令提示符窗口中按 Ctrl + C

现在我们将安装一个额外的包。确保你已经激活了上一章中创建的虚拟环境,我们将其命名为 Django01。激活虚拟环境后,就是运行许多命令的时候了,这些命令在 macOS、Linux 或 Windows 上都是相同的。

现在我们将编辑现有的 requirements.txt 文件,以指定我们的应用程序在任意支持平台上需要安装的额外包。这样,在任意新的虚拟环境中重复安装指定包及其版本将会变得极其容易。

使用你喜欢的编辑器编辑虚拟环境根目录下名为 requirements.txt 的现有文本文件。在最后一行之后添加以下行以声明新版本的 API 所需要的额外包及其版本:psycopg2 版本 2.7.5。示例的代码文件包含在 restful_python_2_06_01 文件夹中,位于 Django01/requirements.txt 文件:

psycopg2==2.7.5 

Psycopg 2 (psycopg2) 是一个 Python-PostgreSQL 数据库适配器,Django 的集成 ORM 将会使用它来与我们的最近创建的 PostgreSQL 数据库进行交互。再次强调,在运行此包的安装之前,确保 PostgreSQL 的 bin 文件夹已包含在 PATH 环境变量中是非常重要的。

现在我们必须在 macOS、Linux 或 Windows 上运行以下命令来安装额外的包以及之前表格中解释的版本,使用 pip 通过最近编辑的 requirements.txt 文件进行安装。确保在运行命令之前你位于包含 requirements.txt 文件(Django01)的文件夹中:

pip install -r requirements.txt 

输出的最后几行将指示新包已成功安装。如果你下载了示例的源代码,并且你没有使用 API 的先前版本,pip 也会安装 requirements.txt 文件中包含的其他包:

    Installing collected packages: psycopg2
    Successfully installed psycopg2-2.7.5

配置数据库

默认的 SQLite 数据库引擎和数据库文件名在games_service/games_service/settings.pyPython 文件中指定。为了在这个示例中使用 PostgreSQL 10.5 而不是 SQLite,请将此文件中DATABASES字典的声明替换为以下行。嵌套字典将名为default的数据库映射到django.db.backends.postgresql数据库引擎、所需的数据库名称及其设置。在这种情况下,我们将创建一个名为 games 的数据库。

确保您在'NAME'键的值中指定了所需的数据库名称,并根据您最近创建的用户和您的 PostgreSQL 10.5 配置配置用户、密码、主机和端口。...

运行迁移

现在运行以下 Python 脚本以生成允许我们首次同步数据库的迁移。确保您位于虚拟环境根目录(Django01)下的games_service文件夹中。请注意,我们在下一个脚本中使用的是 Django 应用名称games,而不是 PostgreSQL 数据库名称django_games

    python manage.py makemigrations games

以下行显示了运行上一条命令后的输出结果:

    Migrations for 'games':
      games/migrations/0001_initial.py
        - Create model EsrbRating
        - Create model Game
        - Create model Player
        - Create model PlayerScore

输出表明games_service/games/migrations/0001_initial.py文件包含了创建EsrbRatingGamePlayerPlayerScore模型的代码。以下行显示了由 Django 及其集成 ORM 自动生成的此文件的代码。示例的代码文件包含在restful_python_2_06_01文件夹中,在Django01/games-service/games/migrations/0001_initial.py文件中:

# Generated by Django 2.1.2 on 2018-10-25 20:15 

from django.db import migrations, models 
import django.db.models.deletion 

class Migration(migrations.Migration): 

    initial = True 

    dependencies = [ 
    ] 

    operations = [ 
        migrations.CreateModel( 
            name='EsrbRating', 
            fields=[ 
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 
                ('description', models.CharField(max_length=200)), 
            ], 
            options={ 
                'ordering': ('description',), 
            }, 
        ), 
        migrations.CreateModel( 
            name='Game', 
            fields=[ 
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 
                ('created', models.DateTimeField(auto_now_add=True)), 
                ('name', models.CharField(max_length=200)), 
                ('release_date', models.DateTimeField()), 
                ('played_once', models.BooleanField(default=False)), 
                ('played_times', models.IntegerField(default=0)), 
                ('esrb_rating', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='games', to='games.EsrbRating')), 
            ], 
            options={ 
                'ordering': ('name',), 
            }, 
        ), 
        migrations.CreateModel( 
            name='Player', 
            fields=[ 
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 
                ('created', models.DateTimeField(auto_now_add=True)), 
                ('name', models.CharField(max_length=50)), 
                ('gender', models.CharField(choices=[('M', 'Male'), ('F', 'Female')], default='M', max_length=2)), 
            ], 
            options={ 
                'ordering': ('name',), 
            }, 
        ), 
        migrations.CreateModel( 
            name='PlayerScore', 
            fields=[ 
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 
                ('score', models.IntegerField()), 
                ('score_date', models.DateTimeField()), 
                ('game', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='games.Game')), 
                ('player', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scores', to='games.Player')), 
            ], 
            options={ 
                'ordering': ('-score',), 
            }, 
        ), 
    ] 

代码定义了一个名为Migrationdjango.db.migrations.Migration类的子类,该类定义了一个包含许多migrations.CreateModeloperations列表。每个migrations.CreateModel方法将为每个相关模型创建一个表。请注意,Django 已自动为每个模型添加了一个id字段。

operations按它们在列表中出现的顺序执行。代码创建了EsrbRatingGamePlayerPlayerScore。当创建这些模型时,代码为GamePlayerScore创建了外键。

现在运行以下 Python 脚本以应用所有生成的迁移:

    python manage.py migrate

以下行显示了运行上一条命令后的输出结果:

    Operations to perform:
      Apply all migrations: admin, auth, contenttypes, games, sessions
    Running migrations:
      Applying contenttypes.0001_initial... OK
      Applying auth.0001_initial... OK
      Applying admin.0001_initial... OK
      Applying admin.0002_logentry_remove_auto_add... OK
      Applying admin.0003_logentry_add_action_flag_choices... OK
      Applying contenttypes.0002_remove_content_type_name... OK
      Applying auth.0002_alter_permission_name_max_length... OK
      Applying auth.0003_alter_user_email_max_length... OK
      Applying auth.0004_alter_user_username_opts... OK
      Applying auth.0005_alter_user_last_login_null... OK
      Applying auth.0006_require_contenttypes_0002... OK
      Applying auth.0007_alter_validators_add_error_messages... OK
      Applying auth.0008_alter_user_username_max_length... OK
      Applying auth.0009_alter_user_last_name_max_length... OK
      Applying games.0001_initial... OK
      Applying sessions.0001_initial... OK

验证 PostgreSQL 数据库的内容

在我们运行上一条命令后,我们可以使用 PostgreSQL 命令行或任何允许我们轻松检查 PostgreSQL 10.5 数据库内容的其他应用程序来检查 Django 生成的表。

运行以下命令以列出生成的表。如果您使用的数据库名称不是django_games,请确保您使用适当的数据库名称。示例的代码文件包含在restful_python_2_06_01文件夹中,在Django01/cmd/list_database_tables.sql文件中:

    psql --username=your_games_user_name --dbname=django_games --    command="\dt"

以下行显示了所有生成的表名输出:

                             List of relations
     Schema | Name ...

使用关系和超链接管理序列化和反序列化

我们的新 RESTful Web API 必须能够将EsrbRatingGamePlayerPlayerScore实例序列化和反序列化为 JSON 表示。在这种情况下,我们创建序列化器类来管理对 JSON 的序列化和从 JSON 的反序列化时,还必须特别注意不同模型之间的关系。

在我们之前 API 的最后一个版本中,我们创建了一个rest_framework.serializers.ModelSerializer类的子类,以便更容易生成序列化器并减少样板代码。在这种情况下,我们还将声明一个继承自ModelSerializer的类,但三个类将继承自rest_framework.serializers.HyperlinkedModelSerializer类。

HyperlinkedModelSerializerModelSerializer的一种类型,它使用超链接关系而不是主键关系,因此,它使用超链接而不是主键值来表示与其他模型实例的关系。此外,HyperlinkedModelSerializer生成一个名为url的字段,其值为资源的 URL。与ModelSerializer一样,HyperlinkedModelSerializer类为createupdate方法提供了默认实现。

打开games_service/games文件夹中的serializers.py文件。用以下行替换此文件中的代码。新代码声明了所需的导入和EsrbRatingSerializer类。我们稍后会将更多类添加到该文件中。示例代码文件包含在restful_python_2_06_01文件夹中,位于Django01/games-service/games/serializers.py文件中:

from rest_framework import serializers 
from games.models import EsrbRating 
from games.models import Game 
from games.models import Player 
from games.models import PlayerScore 
import games.views 

class EsrbRatingSerializer(serializers.HyperlinkedModelSerializer): 
    games = serializers.HyperlinkedRelatedField( 
        many=True, 
        read_only=True, 
        view_name='game-detail') 

    class Meta: 
        model = EsrbRating 
        fields = ( 
            'url', 
            'id', 
            'description', 
            'games') 

EsrbRatingSerializer类是HyperlinkedModelSerializer超类的子类。EsrbRatingSerializer类声明了一个games属性,它是一个serializers.HyperlinkedRelatedField实例,其中manyread_only都设置为True,因为它是一对多关系且只读。我们使用我们在创建Game模型中的esrb_rating字段时指定的related_name字符串值作为games名称。这样,games字段将为我们提供指向属于 ESRB 评分的每个游戏的超链接数组。view_name的值是'game-detail',因为我们希望可浏览的 API 功能使用游戏详情视图来渲染超链接,当用户点击或轻触它时。

EsrbRatingSerializer类声明了一个Meta内部类,该类声明了以下两个属性:

  • model: 此属性指定与序列化器相关的模型,即EsrbRating类。

  • fields:此属性指定了一个字符串的元组,其值表示我们想要包含在从相关模型序列化中的字段名称。我们想要包含主键和 URL,因此,代码将 'id''url' 都指定为元组的成员。

在这种情况下,无需重写 createupdate 方法,因为通用行为将足够。HyperlinkedModelSerializer 超类为这两个方法提供了实现。

打开 games_service/games 文件夹中的 serializers.py 文件,并在最后一行之后添加以下行以声明 GameSerializer 类。示例代码文件包含在 restful_python_2_06_01 文件夹中,位于 Django01/games-service/games/serializers.py 文件中:

class GameSerializer(serializers.HyperlinkedModelSerializer): 
    # We want to display the game ESRB rating description instead of its id 
    esrb_rating = serializers.SlugRelatedField( 
        queryset=EsrbRating.objects.all(),  
        slug_field='description') 

    class Meta: 
        model = Game 
        fields = ( 
            'url', 
            'esrb_rating', 
            'name', 
            'release_date', 
            'played_once', 
            'played_times') 

GameSerializer 类是 HyperlinkedModelSerializer 超类的子类。GameSerializer 类声明了一个 esrb_rating 属性,它是一个 serializers.SlugRelatedField 类的实例,其 queryset 参数设置为 EsrbRating.objects.all(),其 slug_field 参数设置为 'description'

SlugRelatedField 是一个读写字段,它通过一个唯一的 slug 属性表示关系的目标,即描述。

我们在 Game 模型中将 esrb_rating 字段创建为一个 models.ForeignKey 实例,并且我们想要显示 ESRB 评分的 description 值作为相关 EsrbRating 的描述(slug 字段)。因此,我们指定了 'description' 作为 slug_field。如果需要在可浏览的 API 中的表单中显示相关 ESRB 评分的可能选项,Django 将使用在 queryset 参数中指定的表达式检索所有可能实例并显示它们指定的 slug 字段。

EsrbRatingSerializer 类声明了一个 Meta 内部类,该类声明了以下两个属性:

  • model:此属性指定与序列器相关的模型,即 Game 类。

  • fields:此属性指定了一个字符串的元组,其值表示我们想要包含在从相关模型序列化中的字段名称。我们只想包含 URL,因此,代码将 'url' 作为元组的成员,但没有指定 'id'esrb_rating 字段将为相关的 EsrbRating 指定 description 字段。

打开 games_service/games 文件夹中的 serializers.py 文件,并在最后一行之后添加以下行以声明 ScoreSerializer 类。示例代码文件包含在 restful_python_2_06_01 文件夹中,位于 Django01/games-service/games/serializers.py 文件中:

class ScoreSerializer(serializers.HyperlinkedModelSerializer): 
    # We want to display all the details for the related game 
    game = GameSerializer() 
    # We don't include the player because a score will be nested in the player 

    class Meta: 
        model = PlayerScore 
        fields = ( 
            'url', 
            'id', 
            'score', 
            'score_date', 
            'game') 

ScoreSerializer 类是 HyperlinkedModelSerializer 超类的子类。我们将使用 ScoreSerializer 类来序列化与 Player 相关的 PlayerScore 实例,即在我们序列化 Player 时显示特定玩家的所有分数。我们希望显示相关 Game 的所有细节,但我们不包括相关的 Player,因为 Player 将使用这个 ScoreSerializer 序列化器。

ScoreSerializer 类声明了一个 game 属性,其类型为之前编写的 GameSerializer 类的实例。我们在 PlayerScore 模型中创建了一个 game 字段作为 models.ForeignKey 实例,并且我们想要序列化与 GameSerializer 类中编写的相同的数据。

ScoreSerializer 类声明了一个 Meta 内部类,该类声明了以下两个属性:

  • model: 此属性指定与序列化器相关的模型,即 PlayerScore 类。

  • fields: 此属性指定一个字符串元组,其值表示我们想要在序列化中包含的相关模型的字段名称。在这种情况下,我们包括 'url''id'。如前所述,我们不将 'player' 字段名称包含在这个字符串元组中,以避免再次序列化玩家。

我们将使用 PlayerSerializer 作为主类,ScoreSerializer 作为细节类。

打开 games_service/games 文件夹中的 serializers.py 文件,并在最后一行之后添加以下行以声明 PlayerSerializer 类。示例代码文件包含在 restful_python_2_06_01 文件夹中,位于 Django01/games-service/games/serializers.py 文件中:

class PlayerSerializer(serializers.HyperlinkedModelSerializer): 
    scores = ScoreSerializer(many=True, read_only=True) 
    gender = serializers.ChoiceField( 
        choices=Player.GENDER_CHOICES) 
    gender_description = serializers.CharField( 
        source='get_gender_display',  
        read_only=True) 

    class Meta: 
         model = Player 
         fields = ( 
               'url', 
               'name', 
               'gender', 
               'gender_description', 
               'scores') 

PlayerSerializer 类是 HyperlinkedModelSerializer 超类的子类。我们将使用 PlayerSerializer 类来序列化 Player 实例,并使用之前声明的 ScoreSerializer 类来序列化与 Player 相关的所有 PlayerScore 实例。

PlayerSerializer 类声明了一个 scores 属性,其类型为之前编写的 ScoreSerializer 类的实例。many 参数设置为 True,因为这是一个一对多关系。我们使用在创建 PlayerScore 模型中的 player 字段时指定的 scores 名称作为 related_name 字符串值。这样,scores 字段将通过使用之前声明的 ScoreSerializer 来渲染属于 Player 的每个 PlayerScore

Player 模型将 gender 声明为 models.CharField 的实例,其 choices 属性设置为 Player.GENDER_CHOICES 字符串元组。ScoreSerializer 类声明了一个 gender 属性,它是一个 serializers.ChoiceField 的实例,其 choices 参数设置为 Player.GENDER_CHOICES 字符串元组。此外,该类声明了一个 gender_description 属性,其中 read_only 设置为 Truesource 参数设置为 'get_gender_display'source 字符串是通过 get_ 后跟字段名 gender_display 构建的。这样,只读的 gender_description 属性将渲染性别选择的描述,而不是存储的单个字符值。ScoreSerializer 类声明了一个 Meta 内部类,该类声明了 modelfields 属性。model 属性指定了 Player 类。

打开 games_service/games 文件夹中的 serializers.py 文件,并在最后一行之后添加以下行以声明 PlayerScoreSerializer 类。示例代码文件包含在 restful_python_2_06_01 文件夹中,位于 Django01/games-service/games/serializers.py 文件中:

class PlayerScoreSerializer(serializers.ModelSerializer): 
    # We want to display the players's name instead of its id 
   player = serializers.SlugRelatedField(queryset=Player.objects.all(), slug_field='name') 
   # We want to display the game's name instead of its id 
   game = serializers.SlugRelatedField(queryset=Game.objects.all(), slug_field='name') 

   class Meta: 
         model = PlayerScore 
         fields = ( 
               'url', 
               'id', 
               'score', 
               'score_date', 
               'player', 
               'game')

PlayerScoreSerializer 类是 HyperlinkedModelSerializer 超类的子类。我们将使用 PlayerScoreSerializer 类来序列化 PlayerScore 实例。之前,我们创建了 ScoreSerializer 类来序列化 PlayerScore 实例作为玩家的详细信息。当我们想要显示相关玩家的姓名和相关游戏的名称时,我们将使用新的 PlayerScoreSerializer 类。在其他序列化类中,我们没有包含任何与玩家相关的信息,并且包含了游戏的所有详细信息。

PlayerScoreSerializer 类声明了一个 player 属性,它是一个 serializers.SlugRelatedField 的实例,其 queryset 参数设置为 Player.objects.all(),其 slug_field 参数设置为 'name'。我们在 PlayerScore 模型中创建了一个 player 字段作为 models.ForeignKey 实例,我们希望将玩家的名称作为相关 Player 的描述(slug 字段)。因此,我们将 'name' 作为 slug_field 参数。如果需要在可浏览的 API 中的表单中显示相关玩家的可能选项,Django 将使用 queryset 参数中指定的表达式检索所有可能的玩家并显示它们的指定 slug 字段。

PlayerScoreSerializer 类声明了一个 game 属性,它是一个 serializers.SlugRelatedField 的实例,其 queryset 参数设置为 Game.objects.all(),其 slug_field 参数设置为 'name'。我们在 PlayerScore 模型中创建了一个 game 字段作为 models.ForeignKey 实例,我们希望将游戏的名称作为相关 Game 的描述(slug 字段)。

创建基于类的视图和使用通用类

这次,我们将通过声明基于类的视图来编写我们的 API 视图,而不是基于函数的视图。我们可能会编写从rest_framework.views.APIView类继承的类,并声明与我们要处理的 HTTP 动词具有相同名称的方法:getpostputpatchdelete等。这些方法接收一个request参数,就像我们为视图创建的函数一样。然而,这种方法将需要我们编写大量的代码。相反,我们可以利用一组通用视图,我们可以将它们用作基于类的视图的基础类,以将所需的代码量减少到最小,并利用已经泛化的行为 ...

利用通用类视图

打开games_service/games文件夹中的views.py文件。用以下行替换此文件中的代码。新代码声明了所需的导入和基于类的视图。我们稍后将在该文件中添加更多类。示例的代码文件包含在restful_python_2_06_01文件夹中,在Django01/games-service/games/views.py文件中:

from games.models import EsrbRating 
from games.models import Game 
from games.models import Player 
from games.models import PlayerScore 
from games.serializers import EsrbRatingSerializer 
from games.serializers import GameSerializer 
from games.serializers import PlayerSerializer 
from games.serializers import PlayerScoreSerializer 
from rest_framework import generics 
from rest_framework.response import Response 
from rest_framework.reverse import reverse 

class EsrbRatingList(generics.ListCreateAPIView): 
    queryset = EsrbRating.objects.all() 
    serializer_class = EsrbRatingSerializer 
    name = 'esrbrating-list' 

class EsrbRatingDetail(generics.RetrieveUpdateDestroyAPIView): 
    queryset = EsrbRating.objects.all() 
    serializer_class = EsrbRatingSerializer 
    name = 'esrbrating-detail' 

class GameList(generics.ListCreateAPIView): 
    queryset = Game.objects.all() 
    serializer_class = GameSerializer 
    name = 'game-list' 

class GameDetail(generics.RetrieveUpdateDestroyAPIView): 
    queryset = Game.objects.all() 
    serializer_class = GameSerializer 
    name = 'game-detail' 

class PlayerList(generics.ListCreateAPIView): 
    queryset = Player.objects.all() 
    serializer_class = PlayerSerializer 
    name = 'player-list' 

class PlayerDetail(generics.RetrieveUpdateDestroyAPIView): 
    queryset = Player.objects.all() 
    serializer_class = PlayerSerializer 
    name = 'player-detail' 

class PlayerScoreList(generics.ListCreateAPIView): 
    queryset = PlayerScore.objects.all() 
    serializer_class = PlayerScoreSerializer 
    name = 'playerscore-list' 

class PlayerScoreDetail(generics.RetrieveUpdateDestroyAPIView): 
    queryset = PlayerScore.objects.all() 
    serializer_class = PlayerScoreSerializer 
    name = 'playerscore-detail' 

下表总结了每个基于类的视图将要处理的方法:

范围 基于类的视图名称 它将要处理的 HTTP 动词
ESRB 评分集合: /esrb-ratings/ EsrbRatingList GETPOSTOPTIONS
ESRB 评分: /esrb-rating/{id}/ EsrbRatingDetail GETPUTPATCHDELETEOPTIONS
游戏集合: /games/ GameList GET, POST, 和 OPTIONS
游戏: /game/{id}/ GameDetail GETPUTPATCHDELETEOPTIONS
玩家集合: /players/ PlayerList GETPOSTOPTIONS
玩家: /player/{id}/ PlayerDetail GETPUTPATCHDELETEOPTIONS
分数集合: /player-scores/ PlayerScoreList GETPOSTOPTIONS
分数: /player-score/{id}/ PlayerScoreDetail GETPUTPATCHDELETEOPTIONS

与 API 端点一起工作

我们将为 API 的根创建一个端点,以便更容易地使用可浏览的 API 功能浏览 API 并了解一切是如何工作的。打开games_service/games文件夹中的views.py文件,在最后一行之后添加以下代码以声明ApiRoot类。示例的代码文件包含在restful_python_2_06_01文件夹中,在Django01/games-service/games/serializers.py文件中:

class ApiRoot(generics.GenericAPIView): name = 'api-root' def get(self, request, *args, **kwargs): return Response({ 'players': reverse(PlayerList.name, request=request), 'esrb-ratings': reverse(EsrbRatingList.name, request=request), 'games': reverse(GameList.name, request=request), 'scores': ...

使用关系浏览 API

现在我们可以启动 Django 的开发服务器,以组合和发送 HTTP 请求到我们仍然不安全的、但更加复杂的 Web API(我们肯定会稍后添加安全性)。根据您的需求执行以下两个命令之一以访问连接到您的 LAN 的其他设备或计算机上的 API。请记住,我们在上一章中分析了它们之间的区别:

    python manage.py runserver
    python manage.py runserver 0.0.0.0:8000

在运行任何之前的命令后,开发服务器将开始监听端口8000

打开网页浏览器并输入http://localhost:8000/或如果你使用另一台计算机或设备,请输入适当的 URL 以访问可浏览的 API。可浏览的 API 将组合并发送一个GET请求到/,并将显示其执行结果,即views.py文件中ApiRoot类定义的get方法执行的头部和 JSON 响应。以下截图显示了在网页浏览器中输入 URL 后的渲染网页,其中包含 api-root 的资源描述:

图片

api-root为我们提供了超链接,以便查看 ESRB 评级、游戏、玩家和分数的列表。这样,通过可浏览的 API 访问列表并执行不同资源上的操作变得极其容易。此外,当我们访问其他 URL 时,面包屑导航将允许我们返回到api-root

在这个 API 的新版本中,我们使用了提供许多底层功能的通用视图,因此,与之前的版本相比,可浏览的 API 将为我们提供额外的功能。点击或轻触“esrb-ratings”右侧的 URL。如果你正在本地主机上浏览,URL 将是http://localhost:8000/esrb-ratings/。可浏览的 API 将渲染 ESRB 评级列表的网页。

在渲染的网页底部,可浏览的 API 为我们提供了一些控件来生成一个POST请求到/esrb-ratings/。在这种情况下,默认情况下,可浏览的 API 显示 HTML 表单标签,其中包含一个自动生成的表单,我们可以使用它来生成 POST 请求,而无需像我们之前的版本那样处理原始数据。HTML 表单使得生成测试 API 的请求变得容易。以下截图显示了创建新 ESRB 评级的 HTML 表单:

图片

我们只需在“名称”文本框中输入所需的名称,AO (Adults Only),然后点击或轻触POST来创建一个新的 ESRB 评级。可浏览的 API 将组合并发送一个POST请求到/esrb-ratings/,使用之前指定的数据,我们将在网页浏览器中看到调用结果。以下截图显示了网页浏览器显示的 HTTP 状态码201 Created的响应和之前解释的带有 POST 按钮的 HTML 表单,允许我们继续编写并发送POST请求到/esrb-ratings/

图片

现在点击显示在 JSON 数据中"url"键值处的 URL,例如http://localhost:8000/esrb-ratings/1/。确保将1替换为之前渲染的esrbrating-list中存在的 ESRB 评分的 ID。可浏览的 API 将组合并发送一个到/esrb-ratings/1/GET请求,并将显示其执行结果,即 ESRB 评分的头信息和 JSON 数据。网页将显示一个删除按钮,因为我们正在处理 ESRB 评分详情视图。

我们可以使用面包屑导航回到 API 根目录,并开始创建与 ESRB 评分、玩家和最终与游戏和玩家相关的分数相关的游戏。我们可以通过易于使用的 HTML 表单和可浏览的 API 功能来完成所有这些。这个功能对于测试 RESTful API 的 CRUD 操作非常有用。

创建和检索相关资源

现在我们将使用 HTTP 命令或其curl等效命令来组合并发送 HTTP 请求到 API。我们将使用 JSON 进行需要额外数据的请求。记住,你可以使用你喜欢的基于 GUI 的工具或通过可浏览的 API 执行相同的任务。

首先,我们将运行以下命令来组合并发送一个 HTTP POST请求以创建一个新的 ESRB 评分。记住,我们使用可浏览的 API 创建了一个具有以下描述的 ESRB 评分:'AO (Adults Only)'。示例代码文件包含在restful_python_2_06_01文件夹中的Django01/cmd/cmd615.txt文件中:

    http POST ":8000/esrb-ratings/" description='T (Teen)'

以下是对应的

测试你的知识

让我们看看你是否能正确回答以下问题:

  1. 在底层,@api_view装饰器是:

    1. 一个将基于函数的视图转换为rest_framework.views.APIView类子类的包装器

    2. 一个将基于函数的视图转换为序列化器的包装器

    3. 一个将基于函数的视图转换为rest_framework.views.api_view类子类的包装器

  2. Django REST Framework 中的SerializerModelSerializer类类似于 Django Web 框架中的哪两个类?

    1. FormModelForm

    2. ViewModelView

    3. ControllerModelController

  3. 以下哪个类是一个读写字段,通过唯一的 slug 属性(即描述)表示关系的目标?

    1. SlugLinkedField

    2. HyperlinkedRelatedField

    3. SlugRelatedField

  4. 以下哪个类用于生成相关对象的 URL?

    1. SlugLinkedField

    2. HyperlinkedRelatedField

    3. SlugRelatedField

  5. 可浏览的 API 是 Django REST Framework 中包含的一个功能:

    1. 当请求头中的Content-type键指定为application/json值时,为每个资源生成人类友好的 JSON 输出

    2. 当请求头中的Content-type键指定为text/html值时,为每个资源生成人类友好的 HTML 输出

    3. . 当请求指定请求头中的 Content-type 键的值为 application/json 时,为每个资源生成人类友好的 HTML 输出

摘要

在本章中,我们利用了 Django REST Framework 中包含的许多功能,这些功能使我们能够消除重复代码,并重用通用行为来构建我们的 API。我们使用了模型序列化器、包装器、默认解析和渲染选项、基于类的视图和通用类。

我们使用了可浏览的 API 功能,并设计了一个与复杂的 PostgreSQL 10.5 数据库交互的 RESTful API。我们声明了与模型的关系,并使用超链接配置了序列化和反序列化。最后,我们创建了相关资源并检索了它们,并理解了内部的工作原理。

现在我们使用 Django REST Framework 构建了一个复杂的 API,可以封装成微服务,我们将使用额外的 ...

第七章:使用 Django 改进我们的 API 并为其添加认证

在本章中,我们将使用我们在上一章中开始使用的 PostgreSQL 10.5 数据库来改进 Django RESTful API。我们将使用 Django REST 框架中包含的许多功能来向 API 添加新功能,并将添加与认证相关的安全功能。我们将执行以下操作:

  • 在模型中添加唯一约束

  • 使用PATCH方法更新资源的单个字段

  • 利用分页功能

  • 自定义分页类

  • 理解认证、权限和限制

  • 向模型添加与安全相关的数据

  • 为对象级权限创建一个自定义权限类

  • 持久化发起请求的用户并配置权限策略

  • 在迁移中为新的必填字段设置默认值

  • 使用必要的认证来组合请求

  • 使用认证凭据浏览 API

在模型中添加唯一约束

我们的 API 有一些重要的问题需要我们迅速解决。目前,我们可以创建具有相同描述的许多 ESRB 评级。我们不应该能够这样做,因此,我们将对EsrbRating模型进行必要的更改,以在description字段上添加唯一约束。我们还将为GamePlayer模型的name字段添加唯一约束。这样,我们将学习必要的步骤来更改多个模型的约束,并通过迁移反映底层数据库模式的变化。

确保您退出 Django 开发服务器。请记住,您只需在运行它的终端或命令提示符窗口中按Ctrl + C即可。 ...

使用 PATCH 方法更新资源的单个字段

由于使用了基于类的通用视图,我们的 API 能够更新现有资源的单个字段,因此,我们为PATCH方法提供了一个实现。例如,我们可以使用PATCH方法来更新一个现有的游戏,并将它的played_onceplayed_times字段的值设置为True1。我们不希望使用PUT方法,因为这个方法旨在替换整个游戏。请记住,PATCH方法旨在对现有游戏应用一个增量,因此,它是仅更改played_onceplayed_times字段值的适当方法。

现在,我们将组合并发送一个 HTTP PATCH请求来更新一个现有的游戏,特别是更新played_onceplayed_times字段的值,并将它们设置为True10。确保将2替换为配置中现有游戏的id。示例的代码文件包含在restful_python_2_07_01文件夹中,在Django01/cmd/cmd703.txt文件中:

    http PATCH ":8000/games/2/" played_once=true played_times=10  

以下是对应的curl命令。示例的代码文件包含在restful_python_2_07_01文件夹中,在Django01/cmd/cmd704.txt文件中:

 curl -iX PATCH -H "Content-Type: application/json" -d '{"played_once":"true", "played_times": 10}' "localhost:8000/games/2/"

前面的命令将组合并发送一个包含指定 JSON 键值对的 HTTP PATCH请求。请求在/games/之后有一个数字,因此它将匹配'^games/(?P<pk>[0-9]+)/$'并运行views.GameDetail基于类的视图的patch方法。请记住,patch方法是在RetrieveUpdateDestroyAPIView超类中定义的,并最终调用在mixins.UpdateModelMixin中定义的update方法。如果更新played_onceplayed_times字段值的Game实例有效,并且它已成功持久化到数据库中,则对方法的调用将返回200 OK状态码,并将最近更新的Game序列化为 JSON 格式放在响应体中。

以下行显示了示例响应:

    HTTP/1.1 200 OK
    Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
    Content-Length: 204
    Content-Type: application/json
    Date: Fri, 26 Oct 2018 16:40:51 GMT
    Server: WSGIServer/0.2 CPython/3.7.1
    Vary: Accept, Cookie
    X-Frame-Options: SAMEORIGIN

    {
        "esrb_rating": "AO (Adults Only)",
        "name": "Mutant Football League: Dynasty Edition",
        "played_once": true,
        "played_times": 10,
        "release_date": "2018-10-20T03:02:00.776594Z",
        "url": "http://localhost:8000/games/2/"
    }

利用分页

我们的数据库为每个持久化我们定义的模型的表都有几行。然而,在我们开始在现实生产环境中使用我们的 API 之后,我们将有数千个玩家得分、玩家和游戏——尽管 ESRB 评级仍然数量很少。我们绝对必须准备我们的 API 以处理大量结果集。幸运的是,我们可以利用 Django REST framework 中可用的分页功能,使其容易指定我们希望如何将大量结果集拆分为单个数据页。

首先,我们将编写命令来组合和发送 HTTP POST请求以创建 10 个属于我们创建的 ESRB 评级之一(T (Teen))的游戏。这样,...

自定义分页类

我们使用的rest_framework.pagination.LimitOffsetPagination类声明了一个max_limit类属性,默认值为None。此属性允许我们指定可以使用limit查询参数指定的最大允许限制。默认设置下,没有限制,我们将能够处理指定limit查询参数值为1000000的请求。

我们绝对不希望我们的 API 能够通过单个请求生成包含一百万个玩家得分或单个玩家的响应。不幸的是,没有配置设置允许我们更改类分配给max_limit类属性的值。因此,我们被迫创建 Django REST Framework 提供的limit/offset分页风格的定制版本。

games_service/games文件夹内创建一个名为max_limit_pagination.py的新 Python 文件,并输入以下代码,该代码声明了新的MaxLimitPagination类。示例的代码文件包含在restful_python_2_07_03文件夹中,位于Django01/games-service/games/max_limit_pagination.py文件中:

from rest_framework.pagination import LimitOffsetPagination 

class MaxLimitPagination(LimitOffsetPagination): 
    max_limit = 8 

前面的行将MaxLimitPagination类声明为rest_framework.pagination.LimitOffsetPagination超类的子类,并覆盖了为max_limit类属性指定的值,将其设置为8

games_service/games_service文件夹中打开settings.py文件,并将指定REST_FRAMEWORK字典中DEFAULT_PAGINATION_CLASS键值的行替换为高亮行。以下行显示了名为REST_FRAMEWORK的新字典声明。示例的代码文件包含在restful_python_2_07_03文件夹中,在Django01/games-service/games/settings.py文件中:

REST_FRAMEWORK = { 
    'DEFAULT_PAGINATION_CLASS': 
 'games.max_limit_pagination.MaxLimitPagination',    'PAGE_SIZE': 4 
} 

现在通用视图将使用最近声明的games.pagination.MaxLimitPagination类,该类提供了一个基于limit/offset的样式,最大limit值等于8。如果一个请求指定了一个大于8limit值,该类将使用最大限制值,即8,并且我们永远不会在一个分页响应中返回超过8个条目。

现在,我们将编写一个命令来组成并发送一个 HTTP 请求以检索游戏的第一个页面,具体来说,是一个将limit值设置为20/games/的 HTTP GET方法。示例的代码文件包含在restful_python_2_07_03文件夹中,在Django01/cmd/cmd719.txt文件中:

    http GET ":8000/games/?limit=20"

以下是对应的curl命令。示例的代码文件包含在restful_python_2_07_03文件夹中,在Django01/cmd/cmd720.txt文件中:

    curl -iX GET "localhost:8000/games/?limit=20"

结果将使用一个等于8的极限值,而不是指示的20,因为我们正在使用我们的自定义分页类。结果将在results键中提供包含 10 个游戏资源的第一个集合,在count键中提供查询的总游戏数,并在nextprevious键中提供下一页和上一页的链接。在这种情况下,结果集是第一页,因此,next键中下一页的链接是http://localhost:8000/games/?limit=8&offset=8。我们将在响应头中收到200 OK状态码,并在results数组中收到前八个游戏。以下行显示了头信息和输出第一行:

    HTTP/1.1 200 OK
    Allow: GET, POST, HEAD, OPTIONS
    Content-Length: 1542
    Content-Type: application/json
    Date: Fri, 26 Oct 2018 21:25:06 GMT
    Server: WSGIServer/0.2 CPython/3.7.1
    Vary: Accept, Cookie
    X-Frame-Options: SAMEORIGIN

    {
        "count": 12,
        "next": "http://localhost:8000/games/?limit=8&offset=8",
        "previous": null,
        "results": 
            {

配置最大限制以避免生成巨大的响应是一个好习惯。

打开一个网页浏览器并输入http://localhost:8000/games/。如果你使用另一台计算机或设备运行浏览器,请将localhost替换为运行 Django 开发服务器的计算机的 IP 地址。可浏览 API 将组成并发送一个到/games/的 HTTP GET请求,并将显示其执行结果,即头信息和 JSON 游戏列表。因为我们已经配置了分页,所以渲染的网页将包括与我们使用的基分页类关联的默认分页模板,并在网页右上角显示可用的页码。

以下截图显示了在网页浏览器中输入 URL 后渲染的网页,包括资源描述游戏列表和三个页面:

![图片

理解身份验证、权限和节流

我们当前的 API 版本处理所有传入请求,无需任何类型的身份验证。Django REST 框架允许我们轻松使用不同的身份验证方案来识别发起请求的用户或签名请求的令牌。然后,我们可以使用这些凭据来应用权限和速率限制策略,以确定请求是否必须被允许。在生产环境中,我们可以将身份验证方案与运行在 HTTPS 下的 API 结合使用。在我们的开发配置中,我们将继续在 HTTP 下使用 API,但这仅适用于开发。

如同其他配置发生的情况一样,...

将安全相关数据添加到模型中

我们将把一个游戏与创建者或所有者关联起来。只有经过身份验证的用户才能创建新的游戏。只有游戏的创建者才能更新或删除它。未经身份验证的所有请求将只能对游戏有只读访问权限。

打开games_service/games文件夹中的models.py文件。将声明Game类的代码替换为以下代码。代码列表中的新行和编辑行被突出显示。示例的代码文件包含在restful_python_2_07_04文件夹中的Django01/games-service/games/models.py文件中:

class Game(models.Model): 
    created = models.DateTimeField(auto_now_add=True) 
    name = models.CharField(max_length=200, unique=True) 
    esrb_rating = models.ForeignKey( 
        EsrbRating,  
        related_name='games',  
        on_delete=models.CASCADE) 
    release_date = models.DateTimeField() 
    played_once = models.BooleanField(default=False) 
    played_times = models.IntegerField(default=0) 
    owner = models.ForeignKey( 
        'auth.User',  
        related_name='games', 
        on_delete=models.CASCADE) 

    class Meta: 
        ordering = ('name',) 

    def __str__(self): 
        return self.name

新版本的Game模型声明了一个新的owner字段,该字段使用django.db.models.ForeignKey类提供与auth.User模型的许多对一关系,具体来说,是与django.contrib.auth.User模型。这个User模型代表 Django 身份验证系统中的用户。为related_name参数指定的'games'值创建了一个从User模型到Game模型的反向关系。这个值表示用于将User对象关联回Game对象的名称。这样,我们将能够访问特定用户拥有的所有游戏。每次我们删除一个用户时,我们希望删除该用户拥有的所有游戏,因此,我们为on_delete参数指定了models.CASCADE值。

现在,我们将运行manage.py中的createsuperuser子命令来创建 Django 的超级用户,我们将使用它来轻松地验证我们的请求。我们稍后会创建更多用户:

    python manage.py createsuperuser

命令将要求你输入想要用于超级用户的用户名。输入所需的用户名并按Enter键。在这个例子中,我们将使用your_games_super_user作为用户名。你将看到类似以下的一行:

    Username (leave blank to use 'xxxxxxxx'):

然后,命令将要求你输入电子邮件地址:

    Email address: 

输入一个电子邮件地址,例如your_games_super_user@example.com,并按Enter键。

最后,命令将要求你输入新超级用户的密码:

    Password:

输入你想要的密码并按Enter键。在示例中,我们将使用WCS3qn!a4ybX#作为密码。

命令将要求你再次输入密码:

    Password (again):

输入并按 Enter。如果输入的两个密码匹配,将创建超级用户:

    Superuser created successfully.

打开 games_service/games 文件夹中的 serializers.py 文件。在声明导入的最后一行之后,在 GameCategorySerializer 类声明之前添加以下代码。示例的代码文件包含在 restful_python_2_07_04 文件夹中,在 Django01/games-service/games/serializers.py 文件中:

from django.contrib.auth.models import User 

class UserGameSerializer(serializers.HyperlinkedModelSerializer): 
    class Meta: 
        model = Game 
        fields = ( 
            'url', 
            'name') 

class UserSerializer(serializers.HyperlinkedModelSerializer): 
    games = UserGameSerializer(many=True, read_only=True) 

    class Meta: 
        model = User 
        fields = ( 
            'url',  
            'id', 
            'username', 
            'games') 

UserGameSerializer 类是 HyperlinkedModelSerializer 超类的子类。我们使用这个新的序列化器类来序列化与用户相关的游戏。我们只想包含 URL 和游戏名称,因此,代码指定了 'url''name' 作为在 Meta 内部类中定义的字段元组的成员。我们不希望使用 GameSerializer 序列化器类来序列化与用户相关的游戏,因为我们想序列化更少的字段,因此,我们创建了 UserGameSerializer 类。

UserSerializer 类是 HyperlinkedModelSerializer 超类的子类。这个序列化器类与 django.contrib.auth.models.User 模型相关。UserSerializer 类声明了一个 games 属性,它是一个之前解释过的 UserGameSerializer 的实例,其中 manyread_only 都设置为 True,因为它是一个一对多关系,并且是只读的。我们使用 games 名称,我们在将 owner 字段作为 models.ForeignKey 实例添加到 Game 模型时指定的 related_name 字符串值。这样,games 字段将为我们提供每个属于用户的游戏的 URL 和名称数组。

我们将在 games_service/games 文件夹中的 serializers.py 文件进行更多修改。我们将向现有的 GameSerializer 类添加一个 owner 字段。以下行显示了 GameSerializer 类的新代码。新和编辑的行被突出显示。示例的代码文件包含在 restful_python_2_07_04 文件夹中,在 Django01/games-service/games/serializers.py 文件中:

class GameSerializer(serializers.HyperlinkedModelSerializer): 
    # We want to display the game ESRB rating description instead of 
    #

its id 
    esrb_rating = serializers.SlugRelatedField( 
        queryset=EsrbRating.objects.all(),  
        slug_field='description') 
    # We want to display the user name that is the owner 
    owner = serializers.ReadOnlyField(source='owner.username') 

    class Meta: 
        model = Game 
        fields = ( 
            'url', 
            'esrb_rating', 
            'name', 
            'release_date', 
            'played_once', 
            'played_times', 
            'owner') 

现在,GameSerializer 类声明了一个 owner 属性,它是一个 serializers.ReadOnlyField 类的实例,其中 source 等于 'owner.username'。这样,我们将序列化相关 django.contrib.auth.Userowner 字段持有的 username 字段的值。我们使用 ReadOnlyField 类,因为当认证用户创建游戏时,所有者会自动填充,因此,在游戏创建后不可能更改所有者。这样,owner 字段将为我们提供创建游戏的用户名。此外,我们还向在 Meta 内部类中声明的 fields 字符串元组中添加了 'owner'

创建一个用于对象级权限的自定义权限类

games_service/games文件夹内创建一个名为customized_permissions.py的新 Python 文件,并输入以下声明新IsOwnerOrReadOnly类的代码。示例代码文件包含在restful_python_2_07_04文件夹中,位于Django01/games-service/games/customized_permissions.py文件中:

from rest_framework import permissions 

class IsOwnerOrReadOnly(permissions.BasePermission): 
    def has_object_permission(self, request, view, obj): 
        if request.method in permissions.SAFE_METHODS: 
            return True 
        else: 
            return obj.owner == request.user 

rest_framework.permissions.BasePermission类是所有权限类应该继承的基础类。...

持久化发起请求的用户并配置权限策略

我们希望能够列出所有用户并检索单个用户的详细信息。我们将创建rest_framework.generics模块中声明的两个以下通用类视图的子类:

  • ListAPIView:实现了get方法,用于检索queryset的列表

  • RetrieveAPIView:实现了get方法以检索模型实例

games_service/games文件夹中打开views.py文件。在声明导入的最后一行之后,在GameCategoryList类声明之前添加以下代码。示例代码文件包含在restful_python_2_07_04文件夹中,位于Django01/games-service/games/views.py文件中:

from django.contrib.auth.models import User 
from rest_framework import permissions 
from games.serializers import UserSerializer 
from games.customized_permissions import IsOwnerOrReadOnly 

class UserList(generics.ListAPIView): 
    queryset = User.objects.all() 
    serializer_class = UserSerializer 
    name = 'user-list' 

class UserDetail(generics.RetrieveAPIView): 
    queryset = User.objects.all() 
    serializer_class = UserSerializer 
    name = 'user-detail'

继续编辑games_service/games文件夹中的views.py文件。将以下高亮显示的行添加到views.py文件中声明的ApiRoot类中。这样,我们就能通过可浏览 API 导航到与用户相关的视图。示例代码文件包含在restful_python_2_07_04文件夹中,位于Django01/games-service/games/views.py文件中:

class ApiRoot(generics.GenericAPIView): 
    name = 'api-root' 
    def get(self, request, *args, **kwargs): 
        return Response({ 
 'users': reverse(UserList.name, request=request),            'players': reverse(PlayerList.name, request=request), 
            'esrb-ratings': reverse(EsrbRatingList.name, request=request), 
            'games': reverse(GameList.name, request=request), 
            'scores': reverse(PlayerScoreList.name, request=request) 
            }) 

继续编辑games_service/games文件夹中的views.py文件。将以下高亮显示的行添加到GameList类视图以覆盖从rest_framework.mixins.CreateModelMixin超类继承的perform_create方法。记住,generics.ListCreateAPIView类继承自CreateModelMixin类和其他类。新方法中的代码将在将新的Game实例持久化到数据库之前填充owner。此外,新代码覆盖了permission_classes类属性的值,以配置基于类的视图的权限策略。示例代码文件包含在restful_python_2_07_04文件夹中,位于Django01/games-service/games/views.py文件中:

class GameList(generics.ListCreateAPIView): 
    queryset = Game.objects.all() 
    serializer_class = GameSerializer 
    name = 'game-list' 
    permission_classes = ( 
        permissions.IsAuthenticatedOrReadOnly, 
        IsOwnerOrReadOnly) 

    def perform_create(self, serializer): 
        serializer.save(owner=self.request.user)

覆盖的perform_create方法的代码通过为serializer.save方法的调用设置owner参数的值,将额外的owner字段传递给create方法。代码将owner属性设置为self.request.user的值,即与请求关联的用户。这样,每次持久化新的游戏时,它都会将请求关联的用户保存为其所有者。

games_service/games文件夹中的views.py文件中继续编辑。将以下高亮行添加到GameDetail类视图以覆盖permission_classes类属性的值,以配置基于类的视图的权限策略。示例代码文件包含在restful_python_2_07_04文件夹中的Django01/games-service/games/views.py文件中:

class GameDetail(generics.RetrieveUpdateDestroyAPIView): 
    queryset = Game.objects.all() 
    serializer_class = GameSerializer 
    name = 'game-detail' 
    permission_classes = ( 
        permissions.IsAuthenticatedOrReadOnly, 
        IsOwnerOrReadOnly) 

我们在permission_classes元组中为GameListGameDetail类都包含了IsAuthenticatedOrReadOnly类和之前创建的IsOwnerOrReadOnly权限类。

打开games_service/games文件夹中的urls.py文件。将以下元素添加到urlpatterns字符串列表中。新字符串定义了指定请求中必须匹配的正则表达式的 URL 模式,以在views.py文件中运行之前创建的基于类的视图的特定方法:UserListUserDetail。示例代码文件包含在restful_python_2_07_04文件夹中的Django01/games-service/games/serializers.py文件中:

    url(r'^users/$', 
        views.UserList.as_view(), 
        name=views.UserList.name), 
    url(r'^users/(?P<pk>[0-9]+)/$', 
        views.UserDetail.as_view(), 
        name=views.UserDetail.name),

现在打开games_service文件夹中的urls.py文件,特别是games_service/urls.py文件。该文件定义了根 URL 配置,我们希望包含 URL 模式以允许可浏览 API 显示登录和注销视图。以下行显示了添加了高亮的新代码。示例代码文件包含在restful_python_2_07_04文件夹中的Django01/games-service/games/serializers.py文件中:

from django.conf.urls import url, include 

urlpatterns = [ 
    url(r'^', include('games.urls')), 
 url(r'^api-auth/', include('rest_framework.urls')), ] 

新增行添加了在rest_framework.urls模块中定义的 URL 模式,并将它们关联到^api-auth/模式。可浏览 API 使用api-auth/作为所有与用户登录和注销相关的视图的前缀

在迁移中为新的必填字段设置默认值

我们在我们的数据库中持续了很多游戏,并为那些是必填字段的游戏添加了一个新的owner字段。我们不希望删除所有现有的游戏,因此,我们将利用 Django 的一些特性,这些特性使我们能够轻松地在底层数据库中做出更改,而不会丢失现有数据。

现在我们需要检索我们创建的超级用户的id,以便将其用作现有游戏的默认所有者。Django 将允许我们轻松地更新现有游戏,为它们设置所有者用户。

运行以下命令以从auth_user表中检索与username匹配'superuser'的行的id。替换your_games_super_user ...

使用必要的认证来组合请求

现在,我们将编写一个命令来组合并发送一个不需要认证凭据的 HTTP POST请求以创建一个新的游戏。示例代码文件包含在restful_python_2_07_04文件夹中的Django01/cmd/cmd721.txt文件中:

http POST ":8000/games/" name='Super Mario Odyssey' esrb_rating='T (Teen)' release_date='2017-10-27T01:00:00.776594Z'

以下是对应的 curl 命令。示例代码文件包含在 restful_python_2_07_04 文件夹中的 Django01/cmd/cmd722.txt 文件中:

curl -iX POST -H "Content-Type: application/json" -d '{"name":"Super Mario Odyssey", "esrb_rating":"T (Teen)", "release_date": "2017-10-27T01:00:00.776594Z"}' 
"localhost:8000/games/"

我们将在响应头中收到一个 403 Forbidden 状态码,并在 JSON 体的详细消息中指出我们没有提供认证凭据。以下是一些示例响应行:

    HTTP/1.1 403 Forbidden
    Allow: GET, POST, HEAD, OPTIONS
    Content-Length: 58
    Content-Type: application/json
    Date: Sat, 27 Oct 2018 15:03:53 GMT
    Server: WSGIServer/0.2 CPython/3.7.1
    Vary: Accept, Cookie
    X-Frame-Options: SAMEORIGIN

    {
        "detail": "Authentication credentials were not provided."
    }

如果我们想要创建一个新的游戏,即向 /games/ 发送一个 POST 请求,我们需要通过使用 HTTP 认证来提供认证凭据。现在我们将编写并发送一个带有认证凭据的 HTTP 请求来创建一个新的游戏,即使用超级用户名称和他们的密码。请记住将 your_games_super_user 替换为你为超级用户使用的名称,将 WCS3qn!a4ybX# 替换为你为该用户配置的密码。示例代码文件包含在 restful_python_2_07_04 文件夹中的 Django01/cmd/cmd723.txt 文件中:

http -a your_games_super_user:'WCS3qn!a4ybX#' POST ":8000/games/" name='Super Mario Odyssey' esrb_rating='T (Teen)' release_date='2017-10-27T01:00:00.776594Z'

以下是对应的 curl 命令。示例代码文件包含在 restful_python_2_07_04 文件夹中的 Django01/cmd/cmd724.txt 文件中:

curl --user your_games_super_user:'password' -iX POST -H "Content-Type: application/json" -d '{"name":"Super Mario Odyssey", "esrb_rating":"T (Teen)", "release_date": "2017-10-27T01:00:00.776594Z"}' "localhost:8000/games/"  

如果以 your_games_super_user 命名的用户作为其所有者的新 Game 在数据库中成功持久化,则函数将返回一个 HTTP 201 Created 状态码,并在响应体中将最近持久化的 Game 序列化为 JSON。以下是一些示例响应行,其中包含 JSON 响应中的新 Game 对象:

    HTTP/1.1 201 Created
    Allow: GET, POST, HEAD, OPTIONS
    Content-Length: 209
    Content-Type: application/json
    Date: Sat, 27 Oct 2018 15:17:40 GMT
    Location: http://localhost:8000/games/13/
    Server: WSGIServer/0.2 CPython/3.7.1
    Vary: Accept, Cookie
    X-Frame-Options: SAMEORIGIN

    {
        "esrb_rating": "T (Teen)",
        "name": "Super Mario Odyssey",
        "owner": "your_games_super_user",
        "played_once": false,
        "played_times": 0,
        "release_date": "2017-10-27T01:00:00.776594Z",
        "url": "http://localhost:8000/games/13/"
    }

现在我们将使用认证凭据来编写并发送一个 HTTP PATCH 请求,以更新之前创建的游戏的 played_onceplayed_times 字段值。然而,在这种情况下,我们将使用在 Django 中创建的另一个用户来认证请求。请记住将 gaston-hillar 替换为你为用户使用的名称,将 FG$gI⁷⁶q#yA3v 替换为他们的密码。此外,将 13 替换为你配置中为之前创建的游戏生成的 id。示例代码文件包含在 restful_python_2_07_04 文件夹中的 Django01/cmd/cmd725.txt 文件中:

http -a 'gaston-hillar':'FG$gI⁷⁶q#yA3v' PATCH ":8000/games/13/" played_once=true played_times=15

以下是对应的 curl 命令。示例代码文件包含在 restful_python_2_07_04 文件夹中的 Django01/cmd/cmd726.txt 文件中:

curl --user 'gaston-hillar':'FG$gI⁷⁶q#yA3v' -iX PATCH -H "Content-Type: application/json" -d '{"played_once": "true", "played_times": 15}' 
"localhost:8000/games/13/"

我们将在响应头中收到一个 403 Forbidden 状态码,并在 JSON 体的详细消息中指出我们没有权限执行该操作。我们想要更新的游戏的拥有者是 your_games_super_user,而此请求的认证凭据使用了一个不同的用户。因此,操作被 IsOwnerOrReadOnly 类中的 has_object_permission 方法拒绝。以下是一些示例响应行:

    HTTP/1.1 403 Forbidden
    Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
    Content-Length: 63
    Content-Type: application/json
    Date: Sat, 27 Oct 2018 15:23:45 GMT
    Server: WSGIServer/0.2 CPython/3.7.1
    Vary: Accept, Cookie
    X-Frame-Options: SAMEORIGIN

    {
        "detail": "You do not have permission to perform this action."
    }

如果我们使用相同的身份验证凭据,通过 GET 方法发送一个 HTTP 请求来获取该资源,我们就能检索到指定用户不拥有的游戏。请求将成功,因为 GET 是安全方法之一,并且非所有者用户被允许读取游戏。请记住将 gaston-hillar 替换为你为用户使用的名称,将 FG$gI⁷⁶q#yA3v 替换为他们的密码。此外,将 13 替换为你配置中为之前创建的游戏生成的 ID。示例代码文件包含在 restful_python_2_07_04 文件夹中的 Django01/cmd/cmd727.txt 文件中:

    http -a 'gaston-hillar':'FG$gI⁷⁶q#yA3v' GET ":8000/games/13/"

以下是对应的 curl 命令。示例代码文件包含在 restful_python_2_07_04 文件夹中的 Django01/cmd/cmd728.txt 文件中:

 curl --user 'gaston-hillar':'FG$gI⁷⁶q#yA3v' -iX GET "localhost:8000/games/13/"

使用身份验证凭据浏览 API

打开一个网络浏览器并输入 http://localhost:8000/。如果你使用另一台计算机或设备运行浏览器,请将 localhost 替换为运行 Django 开发服务器的计算机的 IP 地址。可浏览 API 将组成并发送一个 GET 请求到 /,并显示其执行的结果,即 API 根。你将注意到右上角有一个“登录”超链接。

点击“登录”,浏览器将显示 Django REST 框架的登录页面。在用户名字段中输入 gaston-hillar,在密码字段中输入 FG$gI⁷⁶q#yA3v,然后点击“登录”。现在,你将作为 gaston-hillar 登录,并且你将通过可浏览 API 组成和发送的所有请求 ...

测试你的知识

让我们看看你是否能正确回答以下问题:

  1. 以下哪一行定义了一个名为 title 的字段,该字段将在模型中生成一个唯一约束?

    1. title = django.db.models.CharField(max_length=250, unique=True)

    2. title = django.db.models.UniqueCharField(max_length=250)

    3. title = django.db.models.CharField(max_length=250, options=django.db.models.unique_constraint)

  2. 以下哪一行定义了一个名为 title 的字段,该字段在模型中不会生成唯一约束?

    1. title = django.db.models.CharField(max_length=250, unique=False)

    2. title = django.db.models.NonUniqueCharField(max_length=250)

    3. title = django.db.models.CharField(max_length=250, options=django.db.models.allow_duplicates)

  3. 以下 REST_FRAMEWORK 字典中哪个设置的键指定了一个全局设置,该设置将使用默认的分页类为通用视图提供分页响应?

    1. DEFAULT_PAGINATED_RESPONSE_PARSER

    2. DEFAULT_PAGINATION_CLASS

    3. DEFAULT_PAGINATED_RESPONSE_CLASS

  4. 以下哪个分页类在 Django REST 框架中提供了基于限制/偏移量的样式?

    1. rest_framework.pagination.LimitOffsetPaging

    2. rest_framework.styles.LimitOffsetPagination

    3. rest_framework.pagination.LimitOffsetPagination

  5. rest_framework.authentication.BasicAuthentication 类:

    1. 与 Django 的会话框架一起用于认证

    2. 提供基于用户名和密码的 HTTP 基本认证

    3. 提供基于简单令牌的认证

  6. rest_framework.authentication.SessionAuthentication类:

    1. 与 Django 的会话框架一起用于认证

    2. 提供基于用户名和密码的 HTTP 基本认证

    3. 提供基于简单令牌的认证

  7. REST_FRAMEWORK字典中,以下哪个设置的键指定了一个全局设置,该设置是一个字符串元组,表示我们想要用于认证的类?

    1. DEFAULT_AUTH_CLASSES

    2. AUTHENTICATION_CLASSES

    3. DEFAULT_AUTHENTICATION_CLASSES

摘要

在本章中,我们从多个方面改进了 RESTful API。我们向模型中添加了唯一约束并更新了数据库,使得使用PATCH方法更新单个字段变得容易,并利用了分页。

然后,我们开始处理认证、权限和速率限制。我们向模型中添加了与安全相关的数据,并更新了数据库。我们在不同的代码片段中进行了许多更改,以实现特定的安全目标,并利用了 Django REST Framework 的认证和权限功能。

现在我们已经构建了一个改进且复杂的 API,它考虑了认证并使用了权限策略,我们将使用框架中包含的额外抽象,添加...

第八章:使用 Django 2.1 节流、过滤、测试和部署 API

在本章中,我们将使用 Django 2.1 和 Django REST Framework 中包含的附加功能来改进我们的 RESTful API。我们还将编写、执行和改进单元测试,并学习一些与部署相关的内容。我们将查看以下内容:

  • 使用requirements.txt文件安装包以与过滤器、节流和测试一起工作

  • 理解过滤、搜索和排序类

  • 为视图配置过滤、搜索和排序

  • 执行 HTTP 请求以测试过滤、搜索和排序功能

  • 在可浏览 API 中进行过滤、搜索和排序

  • 理解节流类和目标

  • 配置节流策略

  • 执行 HTTP 请求以测试节流策略

  • 使用pytest设置单元测试

  • 编写第一轮单元测试

  • 使用pytest运行单元测试

  • 提高测试覆盖率

  • 在云上运行 Django RESTful API

使用requirements.txt文件安装包以与过滤器、节流和测试一起工作

确保您退出 Django 开发服务器。您只需在运行它的终端或命令提示符窗口中按Ctrl + C即可。

现在,我们将安装许多附加包以使用过滤功能,并能够轻松运行测试以及测量它们的代码覆盖率。确保您已激活我们在上一章中创建的虚拟环境,命名为Django01。在激活虚拟环境后,是时候运行许多命令了,这些命令对 macOS、Linux 和 Windows 都是相同的。

现在,我们将编辑现有的requirements.txt文件,以指定我们的应用程序所需的附加包...

理解过滤、搜索和排序类

在上一章中,我们利用 Django REST Framework 中可用的分页功能来指定我们希望将大型结果集分割成单独的数据页面的方式。然而,我们始终以整个queryset作为结果集进行工作;也就是说,我们没有应用任何过滤。

Django REST Framework 使得为已编码的视图自定义过滤、搜索和排序功能变得容易。

games_service/games_service文件夹中打开settings.py文件。在声明名为REST_FRAMEWORK的字典的第一行之后添加以下突出显示的行,以添加新的'DEFAULT_FILTER_BACKENDS'设置键。不要删除新突出显示行之后的行。我们不显示它们以避免重复代码。示例的代码文件包含在restful_python_2_08_01文件夹中,位于Django01/games-service/games_service/settings.py文件中:

REST_FRAMEWORK = { 
    'DEFAULT_FILTER_BACKENDS': ( 
        'django_filters.rest_framework.DjangoFilterBackend', 
        'rest_framework.filters.SearchFilter', 
        'rest_framework.filters.OrderingFilter'),

'DEFAULT_FILTER_BACKENDS'设置键的值指定了一个全局设置,它是一个字符串值的元组,表示我们想要用于过滤后端的默认类。我们将使用以下三个类:

模块 类名 所有者
django_filters.rest_framework DjangoFilterBackend Django 过滤器
rest_framework.filters SearchFilter Django REST 框架
rest_framework.filters OrderingFilter Django REST 框架

DjangoFilterBackend 类通过最近安装的 django-filer 包提供字段过滤功能。我们可以指定我们想要能够过滤的字段集合,或者创建一个具有更多自定义设置的 django_filters.rest_framework.FilterSet 类并将其与所需的视图关联。

SearchFilter 类提供基于单个查询参数的搜索功能,基于 Django 管理员的搜索功能。我们可以指定我们想要包含在搜索中的字段集合,客户端将能够通过在这些字段上执行单个查询来过滤项目。当我们想要使请求能够通过单个查询在多个字段上搜索时,这很有用。

OrderingFilter 类允许请求的客户端通过单个查询参数控制结果的排序方式。我们可以指定哪些字段可以进行排序。

注意,我们还可以通过将之前列出的任何类包含在一个元组中并将其分配给所需通用视图的 filter_backends 类属性来配置过滤后端。然而,在这种情况下,我们将使用所有基于类的视图的默认配置。

每当我们设计 RESTful API 时,我们必须确保我们以合理优化的方式提供所需的功能,并使用可用的资源。因此,我们必须小心确保我们配置的字段在过滤、搜索和排序功能中可用。我们在这些功能中做出的配置将对 Django 集成 ORM 在数据库上生成和执行的查询产生影响。我们必须确保我们有适当的数据库优化,考虑到将要执行的查询。

请保持在 games_service/games_service 文件夹中的 settings.py 文件。在声明字典 INSTALLED_APPS 的第一行之后添加以下突出显示的行,以将 'django_filters' 添加为新安装的应用程序到 Django 项目中。

不要删除新突出显示行之后出现的行。我们不显示它们以避免重复代码。示例代码文件包含在 restful_python_2_08_01 文件夹中的 Django01/games-service/games_service/settings.py 文件:

INSTALLED_APPS = [ 
    # Django Filters 
    'django_filters', 

配置视图的过滤、搜索和排序

打开 games_service/games 文件夹中的 views.py 文件。在声明 UserList 类之前,在声明导入的最后一行之后添加以下代码。示例代码文件包含在 restful_python_2_08_01 文件夹中的 Django01/games-service/games/views.py 文件:

from rest_framework import filters 
from django_filters import AllValuesFilter, DateTimeFilter, NumberFilter 
from django_filters.rest_framework import FilterSet 

继续编辑 games_service/games 文件夹中的 views.py 文件。将以下高亮行添加到 views.py 文件中声明的 EsrbRatingList 类。不要删除此类中未显示的现有行 ...

执行 HTTP 请求以测试过滤、搜索和排序

现在,我们可以启动 Django 的开发服务器来组合并发送 HTTP 请求。根据您的需求,执行以下两个命令之一以访问连接到您的局域网的其他设备或计算机上的 API:

    python manage.py runserver
    python manage.py runserver 0.0.0.0:8000

在我们运行之前的任何命令之后,开发服务器将在端口 8000 上开始监听。

现在,我们将编写一个命令来组合并发送一个 HTTP GET 请求,以检索所有描述匹配 T (Teen) 的 ESRB 评级。示例代码文件包含在 restful_python_2_08_01 文件夹中,位于 Django01/cmd/cmd801.txt 文件中:

    http ":8000/esrb-ratings/?description=T+(Teen)"

以下是对应的 curl 命令。示例代码文件包含在 restful_python_2_08_01 文件夹中,位于 Django01/cmd/cmd802.txt 文件中:

    curl -iX GET "localhost:8000/esrb-ratings/?description=T+(Teen)"

以下行显示了一个与过滤中指定的描述匹配的单个 ESRB 评级的示例响应。以下行仅显示 JSON 主体,不包含头部信息:

    {
        "count": 1,
        "next": null,
        "previous": null,
        "results": [
            {
                "description": "T (Teen)",
                "games": [
                    "http://localhost:8000/games/4/",
                    "http://localhost:8000/games/3/",
                    "http://localhost:8000/games/6/",
                    "http://localhost:8000/games/12/",
                    "http://localhost:8000/games/7/",
                    "http://localhost:8000/games/13/",
                    "http://localhost:8000/games/9/",
                    "http://localhost:8000/games/11/",
                    "http://localhost:8000/games/5/",
                    "http://localhost:8000/games/8/",
                    "http://localhost:8000/games/10/"
                ],
                "id": 2,
                "url": "http://localhost:8000/esrb-ratings/2/"
            }
        ]
    }

现在,我们将编写一个命令来组合并发送一个 HTTP GET 请求,以检索所有相关 ESRB 评级为 1played_times 字段值等于 10 的游戏。我们希望按 release_date 降序排序结果,因此我们在 ordering 的值中指定 -release_date。字段名前的连字符(-)指定使用降序排序功能,而不是默认的升序排序。请确保将 1 替换为描述为 AO (Adults Only) 的 ESRB 评级的 id 值。示例代码文件包含在 restful_python_2_08_01 文件夹中,位于 Django01/cmd/cmd803.txt 文件中:

http ":8000/games/?esrb_rating=1&played_times=10&ordering=-release_date"

以下是对应的 curl 命令。示例代码文件包含在 restful_python_2_08_01 文件夹中,位于 Django01/cmd/cmd804.txt 文件中:

curl -iX GET 
"localhost:8000/games/?esrb_rating=1&played_times=10&ordering=-release_date"

以下行显示了一个与过滤中指定的标准匹配的单个游戏的示例响应。以下行仅显示 JSON 主体,不包含头部信息:

    {
        "count": 1,
        "next": null,
        "previous": null,
        "results": [
            {
                "esrb_rating": "AO (Adults Only)",
                "name": "Mutant Football League: Dynasty Edition",
                "owner": "your_games_super_user",
                "played_once": true,
                "played_times": 10,
                "release_date": "2018-10-20T03:02:00.776594Z",
                "url": "http://localhost:8000/games/2/"
            }
        ]
    }

GameList 类中,我们将 'esrb_rating' 指定为 filterset_fields 字符串元组中的一个字符串。因此,我们必须在过滤中使用 ESRB 评级的 id

现在,我们将运行一个命令,该命令将编写并发送一个使用与注册得分相关的游戏名称的过滤器来组合和发送 HTTP GET 请求。PlayerScoreFilter 类为我们提供了在 game_name 中的相关游戏名称的过滤器。我们将该过滤器与另一个与注册得分相关的玩家名称的过滤器结合起来。PlayerScoreFilter 类为我们提供了一种在 player_name 中过滤相关玩家名称的方法。必须满足准则中指定的两个条件,因此,过滤器使用 AND 运算符组合。示例的代码文件包含在 restful_python_2_08_01 文件夹中,位于 Django01/cmd/cmd805.txt 文件:

http ":8000/player-
scores/?player_name=Enzo+Scocco&game_name=Battlefield+V"

以下是对应的 curl 命令。示例的代码文件包含在 restful_python_2_08_01 文件夹中,位于 Django01/cmd/cmd806.txt 文件:

curl -iX GET "localhost:8000/player-
scores/?player_name=Enzo+Scocco&game_name=Battlefield+V"

以下行显示了与过滤器中指定的条件匹配的得分的示例响应。以下行仅显示没有标题的 JSON 正文:

    {
        "count": 1,
        "next": null,
        "previous": null,
        "results": [
            {
                "game": "Battlefield V",
                "id": 3,
                "player": "Enzo Scocco",
                "score": 43200,
                "score_date": "2019-01-01T03:02:00.776594Z",
                "url": "http://localhost:8000/player-scores/3/"
            }
        ]
    }

我们将编写并发送一个 HTTP GET 请求来检索所有符合以下条件的得分,按 score 降序排序:

  • score 值介于 17,000 和 45,000 之间

  • score_date 值介于 2019-01-01 和 2019-01-31 之间

以下命令将编写并发送之前解释的 HTTP GET 请求。示例的代码文件包含在 restful_python_2_08_01 文件夹中,位于 Django01/cmd/cmd807.txt 文件:

http ":8000/player-scores/?from_score_date=2019-01-01&to_score_date=2019-01-
31&min_score=17000&max_score=45000&ordering=-score"

以下是对应的 curl 命令。示例的代码文件包含在 restful_python_2_08_01 文件夹中,位于 Django01/cmd/cmd808.txt 文件:

curl -iX GET "localhost:8000/player-scores/?from_score_date=2019-01-01&to_score_date=2019-01-
31&min_score=17000&max_score=45000&ordering=-score"

以下行显示了与过滤器中指定的条件匹配的三款游戏的示例响应。以下行仅显示没有标题的 JSON 正文:

    {
        "count": 3,
        "next": null,
        "previous": null,
        "results": [
            {
                "game": "Battlefield V",
                "id": 3,
                "player": "Enzo Scocco",
                "score": 43200,
                "score_date": "2019-01-01T03:02:00.776594Z",
                "url": "http://localhost:8000/player-scores/3/"
            },
            {
                "game": "Battlefield V",
                "id": 1,
                "player": "Gaston Hillar",
                "score": 17500,
                "score_date": "2019-01-01T03:02:00.776594Z",
                "url": "http://localhost:8000/player-scores/1/"
            },
            {
                "game": "Mutant Football League: Dynasty Edition",
                "id": 4,
                "player": "Enzo Scocco",
                "score": 17420,
                "score_date": "2019-01-01T05:02:00.776594Z",
                "url": "http://localhost:8000/player-scores/4/"
            }
        ]
    }

在之前的请求中,没有响应包含超过一页的内容。如果响应需要超过一页,previousnext 键的值将显示包含过滤器、搜索、排序和分页组合的 URL。Django 将所有功能组合起来构建适当的 URL。

我们将编写并发送一个 HTTP 请求来检索所有名称以 'S' 开头的游戏。我们将使用我们配置的搜索功能,将搜索行为限制在 name 字段的以 'S' 开头匹配上。示例的代码文件包含在 restful_python_2_08_01 文件夹中,位于 Django01/cmd/cmd809.txt 文件:

    http ":8000/games/?search=H"

以下是对应的 curl 命令。示例的代码文件包含在 restful_python_2_08_01 文件夹中,位于 Django01/cmd/cmd810.txt 文件:

    curl -iX GET "localhost:8000/games/?search=H"

以下行显示了与指定搜索条件匹配的两个游戏的示例响应;即,那些名称以 'H' 开头的游戏。以下行仅显示没有标题的 JSON 正文:

    {
        "count": 2,
        "next": null,
        "previous": null,
        "results": [
            {
                "esrb_rating": "T (Teen)",
                "name": "Heavy Fire: Red Shadow",
                "owner": "your_games_super_user",
                "played_once": false,
                "played_times": 0,
                "release_date": "2018-06-21T03:02:00.776594Z",
                "url": "http://localhost:8000/games/3/"
            },
            {
                "esrb_rating": "T (Teen)",
                "name": "Honor and Duty: D-Day",
                "owner": "your_games_super_user",
                "played_once": false,
                "played_times": 0,
                "release_date": "2018-06-21T03:02:00.776594Z",
                "url": "http://localhost:8000/games/6/"
            }
        ]
    }

到目前为止,我们一直在使用默认的搜索和排序查询参数:'search''ordering'。我们只需在games_service/games_service文件夹中的settings.py文件中的SEARCH_PARAMORDERING_PARAM设置中指定所需的名称作为字符串。

在可浏览 API 中进行过滤、搜索和排序

我们可以利用可浏览 API 通过网页浏览器轻松测试过滤、搜索和排序功能。打开网页浏览器并输入http://localhost:8000/player-scores/。如果您使用另一台计算机或设备运行浏览器,请将localhost替换为运行 Django 开发服务器的计算机的 IP 地址。

可浏览 API 将组合并发送一个 HTTP GET请求到/player-scores/,并将显示其执行结果;即,头部信息和 JSON 玩家得分列表。您会注意到在 OPTIONS 按钮的左侧有一个新的“过滤器”按钮。

点击“过滤器”,可浏览 API 将显示“过滤器”对话框,...

理解节流类和目标

到目前为止,我们还没有对我们的 API 使用设置任何限制,因此,认证用户和未认证用户都可以随意组合和发送他们想要的请求。我们只是利用了 Django REST Framework 中可用的分页功能来指定我们希望如何将大型结果集拆分为单个数据页。然而,任何用户都可以无限制地组合和发送数千个请求进行处理。

显然,在云平台上部署封装在微服务中的此类 API 不是一个好主意。任何用户对 API 的错误使用都可能导致微服务消耗大量资源,并且云平台的账单将反映这种情况。

我们将使用 Django REST Framework 中可用的节流功能来配置以下基于未认证或认证用户请求的 API 使用全局限制。我们将定义以下配置:

  • 未认证用户:他们每小时最多可以运行5次请求

  • 认证用户:他们每小时最多可以运行20次请求

此外,我们希望将每小时对 ESRB 评分相关视图的请求限制为最多 25 次,无论用户是否已认证。

Django REST Framework 提供了三个节流类(如下表所示),位于 rest_framework.throttling 模块中。所有这些类都是 SimpleRateThrottle 超类的子类,而 SimpleRateThrottleBaseThrottle 超类的子类。这些类允许我们设置每个周期内允许的最大请求数量,该数量将基于不同的机制来确定先前请求信息以指定范围。节流前的请求信息存储在缓存中,并且这些类覆盖了 get_cache_key 方法,该方法确定范围:

节流类名称 描述
AnonRateThrottle 这个类限制了匿名用户可以发起的请求速率。请求的 IP 地址是唯一的缓存键。因此,请注意,来自同一 IP 地址的所有请求将累积总请求数量。
UserRateThrottle 这个类限制了特定用户可以发起的请求速率。对于认证用户,认证用户的 id 是唯一的缓存键。对于匿名用户,请求的 IP 地址是唯一的缓存键。
ScopedRateThrottle 这个类限制了使用 throttle_scope 属性指定的值标识的 API 特定部分的请求速率。当我们需要以不同的速率限制对 API 特定部分的访问时,此类非常有用。

配置节流策略

我们将使用三种节流类的组合来实现我们之前解释的目标。确保您已退出 Django 开发服务器。请记住,您只需在运行 Django 开发服务器的终端或命令提示符窗口中按 Ctrl + C 即可。

games_service/games_service 文件夹中打开 settings.py 文件。在声明名为 REST_FRAMEWORK 的字典的第一行之后添加以下突出显示的行,以添加新的 'DEFAULT_THROTTLE_CLASSES''DEFAULT_THROTTLE_RATES' 设置键。不要删除新突出显示行之后出现的行。我们不显示它们以避免重复代码。示例代码文件包含在 restful_python_2_08_02 文件夹中,...

提高测试覆盖率

现在,我们将编写额外的测试函数以提高测试覆盖率。具体来说,我们将编写与基于玩家类的视图相关的单元测试:PlayerListPlayerDetail。保持 games_service/games 文件夹中的 tests.py 文件。在声明新函数和新测试函数的最后一行之后添加以下代码。示例代码文件包含在 restful_python_2_08_03 文件夹中,在 Django01/games-service/games/tests.py 文件中:

def create_player(client, name, gender): 
    url = reverse('player-list') 
    player_data = {'name': name, 'gender': gender} 
    player_response = client.post(url, player_data, format='json') 
    return player_response 

@pytest.mark.django_db 
def test_create_and_retrieve_player(client): 
    """ 
    Ensure we can create a new Player and then retrieve it 
    """ 
    new_player_name = 'Will.i.am' 
    new_player_gender = Player.MALE 
    response = create_player(client, new_player_name, new_player_gender) 
    assert response.status_code == status.HTTP_201_CREATED 
    assert Player.objects.count() == 1 
    assert Player.objects.get().name == new_player_name 

代码声明了create_player函数,该函数接收新玩家所需的namegender作为参数。该方法构建 URL 和数据字典,以向与player-list视图名称关联的视图发送 HTTP POST方法,并返回此请求生成的响应。代码使用接收到的client来访问允许我们轻松组合和发送 HTTP 请求进行测试的APIClient实例。许多测试函数将调用create_player函数来创建玩家,然后向 API 发送其他 HTTP 请求。

test_create_and_retrieve_player测试函数测试我们是否可以创建一个新的Player对象然后检索它。该方法调用之前解释的create_player函数,然后使用assert检查以下预期结果:

  • 响应的status_code是 HTTP 201 Createdstatus.HTTP_201_CREATED

  • 从数据库中检索到的Player对象总数是1

  • 从数据库中检索到的Player对象的name属性与我们创建对象时指定的描述相匹配

  • 从数据库中检索到的Player对象的gender属性与我们创建对象时指定的描述相匹配

保持位于games_service/games文件夹中的tests.py文件。在最后一行之后添加以下代码以声明新的测试函数。示例代码文件包含在restful_python_2_08_03文件夹中,在Django01/games-service/games/tests.py文件中:

@pytest.mark.django_db 
def test_create_duplicated_player(client): 
    """ 
    Ensure we can create a new Player and we cannot create a duplicate 
    """ 
    url = reverse('player-list') 
    new_player_name = 'Fergie' 
    new_player_gender = Player.FEMALE 
    post_response1 = create_player(client, new_player_name, new_player_gender) 
    assert post_response1.status_code == status.HTTP_201_CREATED 
    post_response2 = create_player(client, new_player_name, new_player_gender) 
    assert post_response2.status_code == status.HTTP_400_BAD_REQUEST 

@pytest.mark.django_db 
def test_retrieve_players_list(client): 
    """ 
    Ensure we can retrieve a player 
    """ 
    new_player_name = 'Vanessa Perry' 
    new_player_gender = Player.FEMALE 
    create_player(client, new_player_name, new_player_gender) 
    url = reverse('player-list') 
    get_response = client.get(url, format='json') 
    assert get_response.status_code == status.HTTP_200_OK 
    assert get_response.data['count'] == 1 
    assert get_response.data['results'][0]['name'] == new_player_name 
    assert get_response.data['results'][0]['gender'] == new_player_gender

代码声明了以下以test_前缀开始的测试函数:

  • test_create_duplicated_player:这个测试函数测试了唯一约束是否使我们能够创建两个具有相同名称的玩家。当我们第二次使用重复的玩家名称组合并发送 HTTP POST请求时,我们应该收到 HTTP 400 Bad Request状态码(status.HTTP_400_BAD_REQUEST)。

  • test_retrieve_player_list:这个测试函数测试我们是否可以通过 HTTP GET请求通过id检索特定的玩家。

我们刚刚编写了一些与玩家相关的测试来提高测试覆盖率。然而,我们绝对应该编写更多的测试来覆盖我们 API 中包含的所有功能。

现在,我们将使用pytest命令再次运行测试。确保你在激活了虚拟环境的终端或命令提示符窗口中运行以下命令,并且你位于包含manage.py文件的games_service文件夹中:

    pytest -v

以下行显示了示例输出:

    ============================== test session starts 
    ==============================
    platform darwin -- Python 3.6.6, pytest-3.9.3, py-1.7.0, pluggy-
    0.8.0 -- /Users/gaston/HillarPythonREST2/Django01/bin/python3
    cachedir: .pytest_cache
    Django settings: games_service.settings (from ini file)
    rootdir: /Users/gaston/HillarPythonREST2/Django01/games_service, 
    inifile: pytest.ini
    plugins: django-3.4.3, cov-2.6.0
    collected 8 items 

    games/tests.py::test_create_and_retrieve_esrb_rating PASSED               
    [ 12%]
    games/tests.py::test_create_duplicated_esrb_rating PASSED                 
    [ 25%]
    games/tests.py::test_retrieve_esrb_ratings_list PASSED                    
    [ 37%]
    games/tests.py::test_update_game_category PASSED                          
    [ 50%]
    games/tests.py::test_filter_esrb_rating_by_description PASSED             
    [ 62%]
    games/tests.py::test_create_and_retrieve_player PASSED                    
    [ 75%]
    games/tests.py::test_create_duplicated_player PASSED                      
    [ 87%]
    games/tests.py::test_retrieve_players_list PASSED                         
    [100%]

    =========================== 8 passed in 1.48 seconds 
    ============================

提供的输出详细说明了pytest执行了8个测试,并且所有测试都通过了。可以使用pytest的固定功能来减少之前编写的函数中的样板代码。然而,我们的重点是使函数易于理解。然后,你可以将代码作为基准,通过充分利用 Pytest 固定功能和pytest-django提供的附加功能来改进它。

我们刚刚创建了一些单元测试来了解我们如何编写它们。然而,当然,编写更多的测试来提供对 API 中包含的所有功能和执行场景的适当覆盖是必要的。

执行 HTTP 请求以测试限流策略

启动 Django 的开发服务器以组合和发送 HTTP 请求。根据您的需求执行以下两个命令之一:

    python manage.py runserver
    python manage.py runserver 0.0.0.0:8000

现在,我们将编写多次组合和发送 HTTP 请求的命令。为了做到这一点,我们将学习如何通过以下任何一种选项与httpcurl命令结合来实现这一目标。根据您的需求选择最合适的一个。不要忘记,您将需要在您选择的任何选项中激活虚拟环境,以便在您使用http命令时运行命令:

  • macOS:带有 Bash shell 的终端。

  • Linux:带有 Bash shell 的终端。

  • Windows:...

使用 pytest 设置单元测试

games_service文件夹内创建一个新的pytest.ini文件(与包含manage.py文件的同一文件夹)。以下行显示了指定 Pytest 所需配置的代码。示例的代码文件包含在restful_python_2_08_02文件夹中,在Django01/game_service/manage.py文件中:

[pytest] 
DJANGO_SETTINGS_MODULE = games_service.settings 
python_files = tests.py test_*.py *_tests.py 

配置变量DJANGO_SETTINGS_MODULE指定了在执行测试时,我们希望使用位于games_service/games_service文件夹中的settings.py文件作为 Django 的设置模块。

配置变量python_files指示pytest将使用哪些过滤器来查找具有测试函数的模块。

编写第一轮单元测试

现在,我们将编写第一轮单元测试。具体来说,我们将编写与 ESRB 评级类视图相关的单元测试:EsrbRatingListEsrbRatingDetail

打开位于games_service/games文件夹中的tests.py文件。用以下行替换现有的代码,这些行声明了许多import语句和两个函数。示例的代码文件包含在restful_python_2_08_02文件夹中,在Django01/games-service/games/tests.py文件中:

import pytest 
from django.urls import reverse 
from django.utils.http import urlencode 
from rest_framework import status 
from games import views 
from games.models import EsrbRating 

def create_esrb_rating(client, description): 
 url = reverse(views.EsrbRatingList.name) ...

使用 pytest 运行单元测试

现在,运行以下命令以创建测试数据库,运行所有迁移,并使用 pytest,结合 pytest-django 插件,发现并执行我们创建的所有测试。测试运行器将执行 tests.py 文件中以 test_ 前缀开始的全部方法,并将显示结果。确保你在激活了虚拟环境的终端或命令提示符窗口中运行此命令,并且你位于包含 manage.py 文件的 games_service 文件夹内:

    pytest -v

在通过 pytest 在 API 上运行请求时,测试不会更改我们一直在使用的数据库。

测试运行器将执行 tests.py 中定义的所有以 test_ 前缀开始的函数,并将显示结果。我们使用 -v 选项指示 pytest 以详细模式打印测试函数名称和状态。

以下行显示了示例输出:

    ============================== test session starts 
    ==============================
    platform darwin -- Python 3.6.6, pytest-3.9.3, py-1.7.0, pluggy-
    0.8.0 -- /Users/gaston/HillarPythonREST2/Django01/bin/python3
    cachedir: .pytest_cache
    Django settings: games_service.settings (from ini file)
    rootdir: /Users/gaston/HillarPythonREST2/Django01/games_service, 
    inifile: pytest.ini
    plugins: django-3.4.3, cov-2.6.0
    collected 5 items 

    games/tests.py::test_create_and_retrieve_esrb_rating PASSED               
    [ 20%]
    games/tests.py::test_create_duplicated_esrb_rating PASSED                 
    [ 40%]
    games/tests.py::test_retrieve_esrb_ratings_list PASSED                    
    [ 60%]
    games/tests.py::test_update_game_category PASSED                          
    [ 80%]
    games/tests.py::test_filter_esrb_rating_by_description PASSED             
    [100%]

    =========================== 5 passed in 1.68 seconds 
    ============================

输出提供了详细信息,表明测试运行器执行了 5 个测试,并且所有测试都通过了。

在云端运行 Django RESTful API

与 Django 和 Django REST 框架相关的一个最大的缺点是每个 HTTP 请求都是阻塞的。因此,每当 Django 服务器收到一个 HTTP 请求时,它不会开始处理传入队列中的任何其他 HTTP 请求,直到服务器收到第一个 HTTP 请求的响应。

然而,RESTful Web 服务的一个巨大优势是它们是无状态的;也就是说,它们不应该在任何服务器上保持客户端状态。我们的 API 是一个无状态 RESTful Web 服务的良好示例。因此,我们可以让 API 在尽可能多的服务器上运行,以实现我们的可扩展性目标。显然,我们必须考虑到我们可以轻松地转换数据库服务器 ...

测试你的知识

让我们看看你是否能正确回答以下问题:

  1. 以下哪个由 pytest-django 插件提供的 fixture 允许我们访问 APIClient 实例,这使得我们能够轻松地编写和发送 HTTP 请求进行测试?

    1. client

    2. api_client

    3. http

  2. 以下哪个在 pytest-django 中声明的装饰器表示测试函数需要与测试数据库一起工作?

    1. @pytest.django.db

    2. @pytest.mark.django_db

    3. @pytest.mark.db

  3. ScopedRateThrottle 类:

    1. 限制特定用户可以发起的请求数量

    2. 限制与 throttle_scope 属性分配的值标识的 API 特定部分的请求数量

    3. 限制匿名用户可以发起的请求数量

  4. UserRateThrottle 类:

    1. 限制特定用户可以发起的请求数量

    2. 限制与 throttle_scope 属性分配的值标识的 API 特定部分的请求数量

    3. 限制匿名用户可以发起的请求数量

  5. DjangoFilterBackend 类:

    1. 提供基于单个查询参数的搜索功能,并基于 Django 管理员的搜索功能

    2. 允许客户端通过单个查询参数控制结果的排序

    3. 提供字段过滤功能

  6. The SearchFilter class:

    1. 提供基于单个查询参数的搜索功能,并基于 Django 管理员的搜索功能

    2. 允许客户端通过单个查询参数控制结果的排序

    3. 提供字段过滤功能

  7. 以下哪个类属性指定了我们想要用于基于类的视图的FilterSet子类?

    1. filters_class

    2. filtering_class

    3. filterset_class

摘要

在本章中,我们利用了 Django REST Framework 中包含的许多功能来定义节流策略。我们使用了类的过滤、搜索和排序,使得在 HTTP 请求中配置过滤器、搜索查询和期望的结果排序变得容易。我们使用了可浏览的 API 功能来测试我们 API 中包含的新特性。

我们编写了第一轮单元测试,并设置了必要的配置以使用流行的现代pytest Python 单元测试框架与 Django REST Framework。然后,我们编写了额外的单元测试以改进测试覆盖率。最后,我们理解了许多关于云部署和可扩展性的考虑因素。

现在我们已经使用 Django REST Framework 构建了一个复杂的 API ...

第九章:使用 Pyramid 1.10 开发 RESTful API

在本章中,我们将使用 Pyramid 1.10 来创建一个执行简单数据源 CRUD 操作的 RESTful Web API。我们将探讨以下主题:

  • 设计一个与简单数据源交互的 RESTful API

  • 理解每个 HTTP 方法执行的任务

  • 使用 Pyramid 1.10 设置虚拟环境

  • 基于模板创建新的 Pyramid 项目

  • 创建模型

  • 使用字典作为存储库

  • 创建 Marshmallow 模式以验证、序列化和反序列化模型

  • 与视图可调用和视图配置一起工作

  • 理解和配置视图处理器

  • 使用命令行工具向 API 发送 HTTP 请求

设计一个与简单数据源交互的 RESTful API

一位赢得数十场国际冲浪比赛的冲浪者成为了一名冲浪教练,并希望构建一个新工具来帮助冲浪者为奥运会训练。与冲浪教练合作的开发团队在与 Pyramid 网络框架合作方面拥有多年的经验,因此,他希望我们使用 Pyramid 构建一个简单的 RESTful API,以处理连接到冲浪板多个传感器的物联网板提供的数据。

每个物联网板将提供以下数据:

  • 状态:每个冲浪者的湿式连体衣中嵌入的许多可穿戴无线传感器和其他包含在冲浪板中的传感器将提供数据,物联网板将对数据进行实时分析以指示...

使用 Pyramid 1.10 设置虚拟环境

在第一章《使用 Flask 1.0.2 开发 RESTful API 和微服务》中,我们了解到,在本书中,我们将使用 Python 3.4 中引入并改进的轻量级虚拟环境。现在,我们将遵循许多步骤来创建一个新的轻量级虚拟环境,以使用 Pyramid 1.10。如果您对现代 Python 中的轻量级虚拟环境没有经验,强烈建议您阅读第一章《使用 Flask 1.0.2 开发 RESTful API 和微服务》中名为与轻量级虚拟环境一起工作的部分。本章包含了我们将遵循的步骤的所有详细解释。

以下命令假设您已在 Linux、macOS 或 Windows 上安装了 Python 3.6.6。

首先,我们必须选择我们的轻量级虚拟环境的目标文件夹或目录。以下是我们将在示例中使用的 Linux 和 macOS 的路径:

    ~/HillarPythonREST2/Pyramid01  

虚拟环境的目标文件夹将是我们家目录中的HillarPythonREST2/Pyramid01文件夹。例如,如果我们的 macOS 或 Linux 的家目录是/Users/gaston,虚拟环境将在/Users/gaston/HillarPythonREST2/Pyramid01中创建。您可以在每个命令中用您想要的路径替换指定的路径。

以下是我们将在示例中使用的 Windows 的路径:

    %USERPROFILE%\HillarPythonREST2\Pyramid01

虚拟环境的目标文件夹将是我们的用户配置文件文件夹中的 HillarPythonREST2\Pyramid01 文件夹。例如,如果我们的用户配置文件文件夹是 C:\Users\gaston,则虚拟环境将在 C:\Users\gaston\HillarPythonREST2\Pyramid01 中创建。当然,您可以在每个命令中将指定的路径替换为您想要的路径。

在 Windows PowerShell 中,之前的路径将是以下:

    $env:userprofile\HillarPythonREST2\Pyramid01

现在,我们必须使用 -m 选项,后跟 venv 模块名称和所需的路径,以便 Python 将此模块作为脚本运行并创建指定路径的虚拟环境。根据我们创建虚拟环境的平台,说明可能会有所不同。因此,请确保您遵循您操作系统的说明:

  1. 在 Linux 或 macOS 中打开终端并执行以下命令以创建虚拟环境:
python3 -m venv ~/HillarPythonREST2/Pyramid01
  1. 在 Windows 中,请在命令提示符中执行以下命令以创建虚拟环境:
python -m venv %USERPROFILE%\HillarPythonREST2\Pyramid01
  1. 如果您想使用 Windows PowerShell,请执行以下命令以创建虚拟环境:
python -m venv $env:userprofile\HillarPythonREST2\Pyramid01 

之前的命令不会产生任何输出。现在我们已经创建了虚拟环境,我们将运行特定于平台的脚本以激活它。激活虚拟环境后,我们将安装仅在此虚拟环境中可用的包。

  1. 如果您的终端配置为在 macOS 或 Linux 中使用 bash shell,请运行以下命令以激活虚拟环境。该命令也适用于 zsh shell:
source ~/HillarPythonREST2/Pyramid01/bin/activate
  1. 如果您的终端配置为使用 cshtcsh shell,请运行以下命令以激活虚拟环境:
source ~/HillarPythonREST2/Pyramid01/bin/activate.csh  
  1. 如果您的终端配置为使用 fish shell,请运行以下命令以激活虚拟环境:
source ~/HillarPythonREST2/Pyramid01/bin/activate.fish  
  1. 在 Windows 中,您可以在命令提示符中运行批处理文件,或者在 Windows PowerShell 中运行脚本以激活虚拟环境。如果您更喜欢命令提示符,请在 Windows 命令行中运行以下命令以激活虚拟环境:
%USERPROFILE%\HillarPythonREST2\Pyramid01\Scripts\activate.bat
  1. 如果您更喜欢 Windows PowerShell,启动它并运行以下命令以激活虚拟环境。但是请注意,您应该在 Windows PowerShell 中启用脚本执行才能运行脚本:
cd $env:USERPROFILE
HillarPythonREST2\Pyramid01\Scripts\Activate.ps1

激活虚拟环境后,命令提示符将显示虚拟环境的根文件夹名称,用括号括起来作为默认提示的前缀,以提醒我们我们正在虚拟环境中工作。在这种情况下,我们将看到(Pyramid01)作为命令提示符的前缀,因为已激活的虚拟环境的根文件夹是 Pyramid01

我们已经遵循了必要的步骤来创建和激活虚拟环境。现在,我们将创建一个requirements.txt文件来指定我们的应用程序在任意支持平台上需要安装的包集。这样,在任意新的虚拟环境中重复安装指定包及其版本将变得极其容易。

使用您喜欢的编辑器在最近创建的虚拟环境的根目录下创建一个名为requirements.txt的新文本文件。以下行显示了声明我们的 API 所需的包和版本的文件内容。示例代码文件包含在restful_python_2_11_01文件夹中,在Pyramid01/requirements.txt文件中:

pyramid==1.10 
cookiecutter==1.6.0 
httpie==1.0.2 

requirements.txt文件中的每一行都指示需要安装的包和版本。在这种情况下,我们通过使用==运算符使用确切版本,因为我们想确保安装了指定的版本。以下表格总结了我们所指定的作为要求的包和版本号:

包名 要安装的版本
pyramid 1.10.1
cookiecutter 1.6.0
httpie 1.0.2

cookiecutter包安装了一个命令行工具,使得可以从项目模板中创建 Pyramid 项目。我们将使用这个工具创建一个基本的 Pyramid 1.10 项目,然后进行必要的更改来构建我们的 RESTful API,而不需要从头开始编写所有代码。请注意,我们将在稍后通过在 Pyramid 的setup.py文件中指定额外的必需包来安装额外的包。

进入虚拟环境的根目录:Pyramid01。在 macOS 或 Linux 中,输入以下命令:

    cd ~/HillarPythonREST2/Pyramid01

在 Windows 命令提示符中,输入以下命令:

    cd /d %USERPROFILE%\HillarPythonREST2\Pyramid01

在 Windows PowerShell 中,输入以下命令:

    cd $env:USERPROFILE
    cd HillarPythonREST2\Pyramid01

现在,我们必须在 macOS、Linux 或 Windows 上运行以下命令,使用pip通过最近创建的requirements.txt文件安装上一表中解释的包和版本。在运行命令之前,请确保您位于包含requirements.txt文件的文件夹中(Pyramid01):

pip install -r requirements.txt 

输出的最后几行将指示pyramidcookiecutterhttpie及其依赖项已成功安装:

Installing collected packages: translationstring, plaster, PasteDeploy, plaster-pastedeploy, zope.deprecation, venusian, zope.interface, webob, hupper, pyramid, future, six, python-dateutil, arrow, MarkupSafe, jinja2, jinja2-time, click, chardet, binaryornot, poyo, urllib3, certifi, idna, requests, whichcraft, cookiecutter, Pygments, httpie
      Running setup.py install for future ... done
      Running setup.py install for arrow ... done
Successfully installed MarkupSafe-1.1.0 PasteDeploy-1.5.2 Pygments-2.2.0 arrow-0.12.1 binaryornot-0.4.4 certifi-2018.10.15 chardet-3.0.4 click-7.0 cookiecutter-1.6.0 future-0.17.1 httpie-1.0.2 hupper-1.4 idna-2.7 jinja2-2.10 jinja2-time-0.2.0 plaster-1.0 plaster-pastedeploy-0.6 poyo-0.4.2 pyramid-1.10.1 python-dateutil-2.7.5 requests-2.20.0 six-1.11.0 translationstring-1.3 urllib3-1.24.1 venusian-1.1.0 webob-1.8.3 whichcraft-0.5.2 zope.deprecation-4.3.0 zope.interface-4.6.0

基于模板创建新的 Pyramid 项目

现在,我们将使用应用程序模板(也称为脚手架)来生成一个 Pyramid 项目。请注意,您需要在开发计算机上安装 Git 才能使用下一个命令。您可以访问以下网页了解更多关于 Git 的信息:git-scm.com

运行以下命令以使用cookiecutter根据pyramid-cookiecutter-starter模板生成新的项目。我们使用--checkout 1.10-branch选项来使用一个特定的分支,确保模板与 Pyramid 1.10 兼容:

cookiecutter gh:Pylons/pyramid-cookiecutter-starter --checkout 1.10-branch  

命令将要求您输入项目的名称。输入metrics并按Enter。您将看到一个...

创建模型

现在,我们将创建一个简单的SurfboardMetricModel类,我们将使用它来表示指标。请记住,我们不会将模型持久化到任何数据库或文件中,因此在这种情况下,我们的类将只提供所需的属性,而不提供映射信息。

metrics/metrics文件夹中创建一个新的models子文件夹。然后,在metrics/metrics/models子文件夹中创建一个新的metrics.py文件。以下行显示了声明我们将需要用于许多类的必要导入的代码。然后,在这个文件中创建一个SurfboardMetricModel类。示例代码文件包含在restful_python_2_09_01文件夹中,位于Pyramid01/metrics/metrics/models/metrics.py文件中:

from enum import Enum 
from marshmallow import Schema, fields 
from marshmallow_enum import EnumField 

class SurfboardMetricModel: 
    def __init__(self, status, speed_in_mph, altitude_in_feet, water_temperature_in_f): 
        # We will automatically generate the new id 
        self.id = 0 
        self.status = status 
        self.speed_in_mph = speed_in_mph 
        self.altitude_in_feet = altitude_in_feet 
        self.water_temperature_in_f = water_temperature_in_f 

SurfboardMetricModel类仅声明了一个构造函数;即__init__方法。该方法接收许多参数,并使用它们来初始化具有相同名称的属性:statusspeed_in_mphaltitude_in_feetwater_temperature_in_fid属性被设置为0。我们将自动递增每个通过 API 调用生成的新的冲浪指标标识符。

使用字典作为存储库

现在,我们将创建一个SurfboardMetricManager类,我们将使用它来在内存字典中持久化SurfboardMetricModel实例。我们的 API 方法将调用SurfboardMetricManager类的相关方法来检索、插入和删除SurfboardMetricModel实例。

保持位于metrics.py文件中,该文件位于metrics/metrics/models子文件夹中。添加以下行以声明SurfboardMetricManager类。示例代码文件包含在restful_python_2_09_01文件夹中,位于Pyramid01/metrics/metrics/models/metrics.py文件中:

class SurfboardMetricManager(): last_id = 0 def __init__(self): self.metrics = {} def insert_metric(self, metric): self.__class__.last_id += 1 metric.id = self.__class__.last_id ...

创建 Marshmallow 模式以验证、序列化和反序列化模型

现在,我们将创建一个简单的 Marshmallow 模式,我们将使用它来验证、序列化和反序列化之前声明的SurfboardMetricModel模型。

保持位于metrics.py文件中,该文件位于metrics/metrics/models子文件夹中。添加以下行以声明SurferStatus枚举和SurfboardMetricSchema类。示例代码文件包含在restful_python_2_09_01文件夹中,位于Pyramid01/metrics/metrics/models/metrics.py文件中:

class SurferStatus(Enum): 
    IDLE = 0 
    PADDLING = 1 
    RIDING = 2 
    RIDE_FINISHED = 3 
    WIPED_OUT = 4 

class SurfboardMetricSchema(Schema): 
    id = fields.Integer(dump_only=True) 
    status = EnumField(SurferStatus, required=True) 
    speed_in_mph = fields.Integer(required=True) 
    altitude_in_feet = fields.Integer(required=True) 
    water_temperature_in_f = fields.Integer(required=True) 

首先,代码声明了我们将用于将描述映射到整数的SurferStatus枚举。我们希望 API 的用户能够指定状态为一个与Enum描述匹配的字符串。例如,如果用户想要创建一个新的指标,并将其状态设置为SurferStatus.PADDLING,他们应该在提供的 JSON 正文中使用'PADDLING'作为状态键的值。

然后,代码将SurfboardMetricSchema类声明为marshmallow.Schema类的子类。我们声明代表字段的属性为marshmallow.fields模块中声明的适当类的实例。每当我们将dump_only参数指定为True值时,这意味着我们希望字段为只读。例如,我们无法在模式中为id字段提供值。该字段的值将由SurfboardMetricManager类自动生成。

SurfboardMetricSchema类将status属性声明为marshmallow_enum.EnumField类的实例。enum参数设置为SurferStatus以指定只有此Enum的成员将被视为有效值。因此,在反序列化过程中,只有与SurferStatus Enum中的描述匹配的字符串将被接受为该字段的有效值。此外,每当此字段被序列化时,将使用Enum描述的字符串表示形式。

speed_in_mphaltitude_in_feetwater_temperature_in_f属性是fields.Integer类的实例,required参数设置为True

与视图调用和视图配置一起工作

我们的 RESTful API 不会使用由应用程序模板生成的位于metrics/metrics/views子文件夹中的两个模块。因此,我们必须删除metrics/metrics/views/default.pymetrics/metrics/views/notfound.py文件。

Pyramid 使用视图调用作为 RESTful API 的主要构建块。每当有请求到达时,Pyramid 会找到并调用适当的视图调用以处理请求并返回适当的响应。

视图调用是可调用的 Python 对象,如函数、类或实现__call__方法的实例。任何视图调用都会接收到一个名为request的参数,该参数将提供代表...的pyramid.request.Request实例。

理解和配置视图处理器

以下表格显示了对于每个 HTTP 动词和范围的组合,我们想要执行的功能以及标识每个资源的路由名称:

HTTP 动词 范围 路由名称 函数
GET 指标集合 'metrics' metrics_collection
GET 指标 'metric' metric
POST 指标集合 'metrics' metrics_collection
DELETE 指标 'metrics' metric

我们必须进行必要的资源路由配置来调用适当的函数,通过定义适当的路由传递所有必要的参数,并将适当的视图调用与路由匹配。

首先,我们将检查我们使用的应用程序模板是如何配置并返回一个将运行我们的 RESTful API 的 Pyramid WSGI 应用程序的。以下行显示了位于metrics/metrics文件夹中的__init__.py文件的代码:

from pyramid.config import Configurator 

def main(global_config, **settings): 
    """ This function returns a Pyramid WSGI application. 
    """ 
    with Configurator(settings=settings) as config: 
        config.include('pyramid_jinja2') 
        config.include('.routes') 
        config.scan() 
    return config.make_wsgi_app() 

我们已经移除了对jinja2模板的使用,因此从之前的代码中删除了高亮行。示例代码文件包含在restful_python_2_09_01文件夹中,位于Pyramid01/metrics/metrics/__init__.py文件中。

代码定义了一个main函数,该函数创建一个名为configpyramid.config.Configurator实例,并将接收到的settings作为参数。main函数使用'.routes'作为参数调用config.include方法,以包含来自routes模块的单个参数名为config的配置可调用项。这个可调用项将接收config参数中的Configurator实例,并将能够调用其方法来执行路由的适当配置。我们在分析完之前的代码后,将替换routes模块的现有代码。

然后,代码调用config.scan方法来扫描 Python 包和子包中具有特定装饰器对象的可调用项,这些装饰器对象执行配置,例如我们使用@view.config装饰器声明的函数。

最后,代码调用config.make_wsgi_app方法来提交任何挂起的配置语句,并返回代表提交的配置状态的 Pyramid WSGI 应用程序。这样,Pyramid 完成配置过程并启动服务器。

打开位于metrics/metrics文件夹中的现有routes.py文件,并用以下行替换现有代码。示例代码文件包含在restful_python_2_09_01文件夹中,位于Pyramid01/metrics/metrics/routes.py文件中:

from metrics.views.metrics import metric, metrics_collection 

def includeme(config): 
    # Define the routes for metrics 
    config.add_route('metrics', '/metrics/') 
    config.add_route('metric', '/metrics/{id:\d+}/')         
    # Match the metrics views with the appropriate routes 
    config.add_view(metrics_collection,  
        route_name='metrics',  
        renderer='json') 
    config.add_view(metric,  
        route_name='metric',  
        renderer='json') 

代码定义了一个includeme函数,该函数接收之前解释过的pyramid.config.Configurator实例作为config参数。首先,代码两次调用config.add_route方法,将名为'metrics'的路由与'/metrics/'模式关联,将名为'metric'的路由与'metrics/{id:\d+}/'模式关联。请注意,id后面的分号(;)后面跟着一个正则表达式,确保id只由数字组成。

然后,代码两次调用config.add_view方法来指定视图可调用metrics_collection作为当路由名称等于'metrics'时必须调用的函数,以及视图可调用metric作为当路由名称等于'metric'时必须调用的函数。在这两种情况下,config.add_view方法指定我们希望使用'json'作为响应的渲染器。

使用命令行工具向 API 发送 HTTP 请求

metrics/development.ini文件是一个设置文件,它定义了开发环境中的 Pyramid 应用程序和服务器配置。与大多数.ini文件一样,配置设置按部分组织。例如,[server:main]部分指定了监听设置的值为localhost:6543,以便waitress服务器在端口6543上监听并绑定到 localhost 地址。

当我们基于模板创建新应用程序时,包含了此文件。打开metrics/development.ini文件,找到指定pyramid.debug_routematch设置bool值的以下行。示例的代码文件包含在restful_python_2_09_01 ...

测试你的知识

让我们看看你是否能正确回答以下问题:

  1. 在 Pyramid 中,视图可调用是以下哪个?

    1. 实现了__call__方法的 Python 对象,如函数、类或实例

    2. 继承自pyramid.views.Callable超类的类

    3. pyramid.views.Callable类的实例

  2. 任何视图可调用接收到的request参数代表一个 HTTP 请求,它是以下哪个类的实例?

    1. pyramid.web.Request

    2. pyramid.request.Request

    3. pyramid.callable.Request

  3. 以下哪个属性允许我们在pyramid.response.Response实例中指定响应的状态码?

    1. status

    2. http_status_code

    3. status_code

  4. pyramid.httpexceptions模块中声明的以下哪个类代表响应的 HTTP 201 Created状态码?

    1. HTTP_201_Created

    2. HTTP_Created

    3. HTTPCreated

  5. 以下哪个属性允许我们在pyramid.response.Response实例中指定 JSON 响应的响应体?

    1. json_body

    2. body

    3. body_as_json

摘要

在本章中,我们使用 Pyramid 1.10 设计了一个 RESTful API 来与一个简单的数据源交互。我们定义了 API 的需求,并理解了每个 HTTP 方法执行的任务。我们使用 Pyramid 设置了一个虚拟环境,从一个现有的模板中构建了一个新的应用程序,并将额外的必需包添加到了 Pyramid 应用程序中。

我们创建了一个表示冲浪板指标的类,以及额外的类,以便能够生成一个简单的数据源,使我们能够专注于特定的 Pyramid 功能来构建 RESTful API。

然后,我们创建了一个 Marshmallow 模式来验证、序列化和反序列化指标模型。然后,我们开始使用视图可调用函数来处理特定的 HTTP ...

第十章:使用 Tornado 5.1.1 开发 RESTful API

在本章中,我们将使用 Tornado 5.1.1 创建一个 RESTful Web API。我们将开始使用这个轻量级 Web 框架。我们将查看以下内容:

  • 设计一个用于与慢速传感器和执行器交互的 RESTful API

  • 理解每个 HTTP 方法执行的任务

  • 使用 Tornado 5.1.1 设置虚拟环境

  • 创建表示无人机的类

  • 编写请求处理器

  • 将 URL 模式映射到请求处理器

  • 向 Tornado API 发送 HTTP 请求

  • 使用命令行工具——curlhttpie

  • 使用 GUI 工具——Postman 和其他工具

设计一个用于与慢速传感器和执行器交互的 RESTful API

假设我们必须创建一个 RESTful API 来控制无人机,也称为 UAV(即 Unmanned Aerial Vehicle)。无人机是一个物联网设备,与许多传感器和执行器交互,包括与发动机、螺旋桨和伺服电机连接的数字电子速度控制器。

物联网设备资源有限,因此我们必须使用轻量级 Web 框架。我们的 API 不需要与数据库交互。我们不需要像 Django 这样具有所有功能和集成 ORM 的重型 Web 框架。我们希望能够处理许多请求而不阻塞 Web 服务器。我们需要 Web 服务器为我们提供良好的可伸缩性 ...

理解每个 HTTP 方法执行的任务

假设 http://localhost:8888/hexacopters/1 是识别我们无人机六旋翼机的 URL。

PATCH http://localhost:8888/hexacopters/1 

我们必须使用 HTTP 动词 (PATCH) 和请求 URL (http://localhost:8888/hexacopters/1) 编写并发送一个 HTTP 请求,以设置六旋翼机的状态和电机转速(以 RPM 为单位)。此外,我们必须提供包含必要字段名称和值的 JSON 键值对,以指定所需的速度。请求的结果是,服务器将验证提供的字段值,确保它是一个有效的速度,并以异步执行的方式调用必要的操作来调整速度。在设置六旋翼机的速度后,服务器将返回一个 HTTP 200 OK 状态码和一个包含最近更新的六旋翼机值序列化为 JSON 的 JSON 主体:

GET http://localhost:8888/hexacopters/1

我们必须使用 HTTP 动词 (GET) 和请求 URL (http://localhost:8888/hexacopter/1) 编写并发送一个 HTTP 请求,以检索六旋翼机的当前值。服务器将以异步执行的方式调用必要的操作来检索六旋翼机的状态和速度。请求的结果是,服务器将返回一个 HTTP 200 OK 状态码和一个包含序列化键值对的 JSON 主体,这些键值对指定了六旋翼机的状态和速度。如果指定的数字不同于 1,服务器将仅返回一个 HTTP 404 Not Found 状态:

PATCH http://localhost:8888/led/{id} 

我们必须使用 HTTP 动词 (PATCH) 和请求 URL (http://localhost:8888/led/{id}) 编写并发送一个 HTTP 请求,以设置特定 LED 的亮度级别,其 id{id} 位置中指定的数值匹配。例如,如果我们使用请求 URL http://localhost:8888/led/1,服务器将为与 1 匹配的 LED 设置亮度级别。此外,我们必须提供包含必要字段名称和值的 JSON 键值对,以指定所需的亮度级别。作为请求的结果,服务器将验证提供的字段值,确保它是一个有效的亮度级别,并以异步执行的方式调用必要的操作来调整亮度级别。在设置 LED 的亮度级别后,服务器将返回一个 200 OK 状态码和一个包含最近更新的 LED 值序列化为 JSON 的 JSON 主体:

GET http://localhost:8888/led/{id} 

我们必须使用 HTTP 动词 (GET) 和请求 URL (http://localhost:8888/led/{id}) 编写并发送一个 HTTP 请求,以检索与 {id} 位置中指定的数值匹配的 LED 的当前值。例如,如果我们使用请求 URL http://localhost:8888/led/1,服务器将检索与 1 匹配的 LED,即绿色 LED。服务器将以异步执行的方式调用必要的操作来检索 LED 的值。作为请求的结果,服务器将返回一个 HTTP 200 OK 状态码和一个包含序列化键值对的 JSON 主体,这些键值对指定了 LED 的值。如果没有 LED 与指定的 id 匹配,服务器将仅返回一个 HTTP 404 Not Found 状态:

GET http://localhost:8888/altimeter/1?unit=feet

我们必须使用 HTTP 动词 (GET) 和请求 URL (http://localhost:8888/altimeter/1?unit=feet) 编写并发送一个 HTTP 请求,以检索以英尺为单位的当前高度计值。服务器将以异步执行的方式调用必要的操作来检索高度计的值。作为请求的结果,服务器将返回一个 HTTP 200 OK 状态码和一个包含序列化键值对的 JSON 主体,这些键值对指定了高度计的值。如果指定的数字不是 1,服务器将仅返回一个 HTTP 404 Not Found 状态:

GET http://localhost:8888/altimeter/1?unit=meters

如果我们想检索以米为单位的高度计值,我们必须使用 HTTP 动词 (GET) 和请求 URL (http://localhost:8888/altimeter/1?unit=meters) 编写并发送一个 HTTP 请求。

使用 Tornado 5.1.1 设置虚拟环境

在 第一章,使用 Flask 1.0.2 开发 RESTful API 和微服务,我们了解到在这本书中,我们将使用 Python 3.4 中引入并改进的轻量级虚拟环境。现在,我们将遵循许多步骤来创建一个新的轻量级虚拟环境,以便与 Tornado 5.1.1 一起工作。如果您对现代 Python 中的轻量级虚拟环境没有经验,强烈建议您阅读 第一章,使用 Flask 1.0.2 开发 RESTful API 和微服务 中名为 使用轻量级虚拟环境 的部分。该章节包含了关于我们将遵循的步骤的所有详细解释。

以下 ...

创建表示无人机的类

我们将创建以下类,这些类将用于表示无人机的不同组件:

类名 描述
HexacopterStatus 这个类存储六旋翼无人机的状态数据
Hexacopter 这个类表示一个六旋翼无人机
LightEmittingDiode 这个类表示连接到无人机的 LED
Altimeter 这个类表示用于测量无人机当前高度的气压计
Drone 这个类表示带有不同传感器和执行器的无人机

在实际生活中,这些类将与与传感器和执行器交互的库进行交互。为了使我们的示例简单,我们将调用 time.sleep 来模拟需要一些时间将值写入传感器和执行器接口的交互。我们将使用相同的程序来模拟需要一些时间从传感器和执行器接口检索值的交互。

首先,我们将创建 Hexacopter 类,我们将使用它来表示六旋翼无人机,以及一个 HexacopterStatus 类,我们将使用它来存储六旋翼无人机的状态数据。

在虚拟环境(Tornado01)的根目录下创建一个名为 drone.py 的新 Python 文件。以下行显示了我们将创建的类所需的全部导入,以及在此文件中声明 HexacopterHexacopterStatus 类的代码。示例的代码文件包含在 restful_python_2_10_01 文件夹中,位于 Django01/drone.py 文件:

from time import sleep 
from random import randint 

class HexacopterStatus: 
    def __init__(self, motor_speed, is_turned_on): 
        self.motor_speed = motor_speed 
        self.is_turned_on = is_turned_on 

class Hexacopter: 
    MIN_MOTOR_SPEED = 0 
    MAX_MOTOR_SPEED = 500 

    def __init__(self): 
        self._motor_speed = self.__class__.MIN_MOTOR_SPEED 
        self._is_turned_on = False 

    @property 
    def motor_speed(self): 
        return self._motor_speed 

    @motor_speed.setter     
    def motor_speed(self, value): 
        if value < self.__class__.MIN_MOTOR_SPEED: 
            raise ValueError('The minimum speed is {0}'.format(self.__class__.MIN_MOTOR_SPEED)) 
        if value > self.__class__.MAX_MOTOR_SPEED: 
            raise ValueError('The maximum speed is {0}'.format(self.__class__.MAX_MOTOR_SPEED)) 
        sleep(2) 
        self._motor_speed = value 
        self._is_turned_on = (self.motor_speed is not 0) 

    @property 
    def is_turned_on(self): 
        return self._is_turned_on 

    @property 
    def status(self): 
        sleep(3) 
        return HexacopterStatus(self.motor_speed, self.is_turned_on) 

HexacopterStatus 类仅声明了一个构造函数,即 __init__ 方法。该方法接收许多参数,并使用它们以相同的名称初始化属性:motor_speedis_turned_on

Hexacopter 类声明了两个类属性,用于指定其电机的最小和最大速度值:MIN_MOTOR_SPEEDMAX_MOTOR_SPEED。构造函数,即 __init__ 方法,将 _motor_speed 属性初始化为 MIN_MOTOR_SPEED 值,并将 _is_turned_on 属性设置为 False

motor_speed 属性获取器,带有 @property 装饰器的 motor_speed 方法,返回 _motor_speed 属性的值。motor_speed 属性设置器,即带有 @motor_speed.setter 装饰器的 motor_speed 方法,检查 value 参数的值是否在有效范围内。如果验证失败,该方法将引发 ValueError 异常。否则,该方法使用接收到的值设置 _motor_speed 属性的值,并且如果 motor_speed 属性大于 0,则将 _is_turned_on 属性的值设置为 True。最后,该方法调用 sleep 来模拟完成这些操作需要两秒钟。

is_turned_on 属性获取器,带有 @property 装饰器的 is_turned_on 方法,返回 _is_turned_on 属性的值。status 属性获取器调用 sleep 来模拟获取六旋翼飞行器状态需要三秒钟,然后返回一个使用 motor_speedturned_on 属性值初始化的 HexacopterStatus 实例。

保持位于虚拟环境根目录下的 drones.py 文件(Tornado01)。添加以下行以声明我们将用于表示每个 LED 的 LightEmittingDiode 类。示例代码文件包含在 restful_python_2_10_01 文件夹中,位于 Django01/drone.py 文件中:

class LightEmittingDiode: 
    MIN_BRIGHTNESS_LEVEL = 0 
    MAX_BRIGHTNESS_LEVEL = 255 

    def __init__(self, id, description): 
        self.id = id 
        self.description = description 
        self._brightness_level = self.__class__.MIN_BRIGHTNESS_LEVEL 

    @property 
    def brightness_level(self): 
        sleep(1) 
        return self._brightness_level 

    @brightness_level.setter 
    def brightness_level(self, value): 
        if value < self.__class__.MIN_BRIGHTNESS_LEVEL: 
            raise ValueError('The minimum brightness level is {0}'.format(self.__class__.MIN_BRIGHTNESS_LEVEL)) 
        if value > self.__class__.MAX_BRIGHTNESS_LEVEL: 
            raise ValueError('The maximum brightness level is {0}'.format(self.__class__.MAX_BRIGHTNESS_LEVEL)) 
        sleep(2) 
        self._brightness_level = value

LightEmittingDiode 类声明了两个类属性,用于指定 LED 的最小和最大亮度级别值:MIN_BRIGHTNESS_LEVELMAX_BRIGHTNESS_LEVEL。构造函数,即 __init__ 方法,使用 MIN_BRIGHTNESS_LEVEL 初始化 _brightness_level 属性,并使用与同名参数接收的值初始化 iddescription 属性。

brightness_level 属性获取器,带有 @property 装饰器的 brightness_level 方法,调用 sleep 来模拟获取有线 LED 的亮度级别需要 1 秒,然后返回 _brightness_level 属性的值。

brightness_level 属性设置器,带有 @brightness_level.setter 装饰器的 brightness_level 方法,检查 value 参数的值是否在有效范围内。如果验证失败,该方法将引发 ValueError 异常。否则,该方法调用 sleep 来模拟设置新的亮度级别需要两秒钟,并最终使用接收到的值设置 _brightness_level 属性的值。

保持位于虚拟环境根目录下的 drones.py 文件(Tornado01)。添加以下行以声明我们将用于表示高度计的 Altimeter 类。示例代码文件包含在 restful_python_2_10_01 文件夹中,位于 Django01/drone.py 文件中:

class Altimeter: 
    @property 
    def altitude(self): 
        sleep(1) 
        return randint(0, 3000) 

Altimeter 类声明了一个 altitude 属性设置器,它调用 sleep 来模拟从高度计获取高度需要一秒钟的时间,并最终生成一个从 03000(包含)的随机整数并返回它。

保持在虚拟环境(Tornado01)根目录下的 drones.py 文件中。添加以下行以声明一个 Drone 类,我们将使用它来表示具有其传感器和执行器的无人机。示例的代码文件包含在 restful_python_2_10_01 文件夹中的 Django01/drone.py 文件中:

class Drone: 
    def __init__(self): 
        self.hexacopter = Hexacopter() 
        self.altimeter = Altimeter() 
        self.red_led = LightEmittingDiode(1, 'Red LED') 
        self.green_led = LightEmittingDiode(2, 'Green LED') 
        self.blue_led = LightEmittingDiode(3, 'Blue LED') 
        self.leds = { 
            self.red_led.id: self.red_led, 
            self.green_led.id: self.green_led, 
            self.blue_led.id: self.blue_led} 

Drone 类仅声明了一个构造函数,即 __init__ 方法,该方法创建了代表无人机不同组件的先前声明的类的实例。leds 属性保存了一个字典,其中每个 LightEmittingDiode 实例都有一个键值对,包含其 id 和其实例。

编写请求处理程序

在 Tornado 中,RESTful API 的主要构建块是 tornado.web.RequestHandler 类的子类,即 Tornado 中 HTTP 请求处理程序的基础类。我们只需执行以下任务即可构建与无人机交互的 RESTful API:

  1. 创建 RequestHandler 类的子类,并声明每个支持的 HTTP 动词的方法

  2. 覆盖方法以处理 HTTP 请求

  3. 将 URL 模式映射到代表 Tornado 网络应用的 tornado.web.Application 实例中的 RequestHandler 超类的每个子类

我们将创建以下 RequestHandler 类的子类:

类名 描述
HexacopterHandler 此类处理 HTTP ...

将 URL 模式映射到请求处理程序

以下表格显示了我们的先前创建的 HTTP 处理程序类的方法,我们希望为每个 HTTP 动词和作用域的组合执行:

HTTP 动词 范围 类和方法
GET Altimeter AltimeterHandler.get
GET Hexacopter HexacopterHandler.get
PATCH Hexacopter HexacopterHandler.patch
GET LED LedHandler.get
PATCH LED LedHandler.patch

如果请求导致调用一个不支持 HTTP 方法的 HTTP 处理程序类,Tornado 将返回一个带有 HTTP 405 Method Not Allowed 状态码的响应。

现在,我们必须将 URL 模式映射到我们之前编写的 RequestHandler 超类的子类。保持在虚拟环境(Tornado01)根目录下的 drone_service.py 文件中。添加以下行以声明 Application 类和 __main__ 方法。示例的代码文件包含在 restful_python_2_10_01 文件夹中的 Django01/drone_service.py 文件中:

class Application(web.Application): 
    def __init__(self, **kwargs): 
        handlers = [ 
            (r"/hexacopters/([0-9]+)", HexacopterHandler), 
            (r"/leds/([0-9]+)", LedHandler), 
            (r"/altimeters/([0-9]+)", AltimeterHandler), 
        ] 
        super(Application, self).__init__(handlers, **kwargs) 

if __name__ == "__main__": 
    application = Application() 
    port = 8888 
    print("Listening at port {0}".format(port)) 
    application.listen(port) 
    tornado_ioloop = ioloop.IOLoop.instance() 
    periodic_callback = ioloop.PeriodicCallback(lambda: None, 500) 
    periodic_callback.start() 
    tornado_ioloop.start()

代码声明一个Application类,作为tornado.web.Application超类的子类。这个类覆盖了继承的构造函数,即__init__方法。构造函数声明了handlers列表,该列表将 URL 模式映射到同步请求处理器,然后调用继承的构造函数,将列表作为其参数之一。handlers列表由一个正则表达式(regexp)和一个tornado.web.RequestHandler子类(request_class)组成。

然后,main方法创建Application类的实例,并调用application.listen方法,在指定的端口上为应用程序构建一个遵循定义规则的 HTTP 服务器。在这种情况下,代码将8888指定为端口号,保存在port变量中,这是 Tornado HTTP 服务器的默认端口号。

然后,代码注册并启动一个名为periodic_callback的周期性回调,该回调将由IOLoop每 500 毫秒执行一次,以便可以使用Ctrl + C来停止 HTTP 服务器。这段代码对于我们的 API 的第二版将很有用。然而,我们现在就写出来以避免以后修改代码。

最后,代码调用tornado_ioloop.start方法来启动服务器。这个服务器是通过之前的application.listen方法创建的。

向 Tornado API 发送 HTTP 请求

现在,我们可以运行drone_service.py脚本,该脚本启动 Tornado 5.1.1 的开发服务器,以便编写和发送 HTTP 请求到我们的非安全简单 Web API。执行以下命令:

    python drone_service.py

以下行显示了执行之前命令后的输出。Tornado HTTP 开发服务器正在端口8888上监听:

    Listening at port 8888

使用之前的命令,我们将启动 Tornado HTTP 服务器,它将在端口8888上的每个接口上监听。因此,如果我们想从连接到我们的局域网的其它计算机或设备向我们的 API 发送 HTTP 请求,我们不需要任何额外的配置。

如果您决定从...编写和发送 HTTP 请求...

使用命令行工具 - curl 和 httpie

我们将开始使用curl和 HTTPie 命令行工具编写和发送 HTTP 请求,这些工具我们在第一章中介绍,在名为使用命令行工具 - curl 和 httpie的部分。在执行下一个示例之前,请确保您已经阅读了这一部分。

每当我们使用命令行编写 HTTP 请求时,我们将使用两个版本的相同命令:第一个使用 HTTPie,第二个使用curl。这样,您就可以使用最方便的一种。

确保您让 Tornado 5.1.1 开发服务器继续运行。不要关闭运行此开发服务器的终端或命令提示符。在 macOS 或 Linux 中打开一个新的终端,或在 Windows 中打开一个命令提示符,激活我们一直在使用的虚拟环境,并运行以下命令。我们将组合并发送一个 HTTP PATCH 请求来打开六旋翼机并将其电机速度设置为 50 RPM。示例代码文件包含在 restful_python_2_10_01 文件夹中,位于 Tornado01/cmd/cmd1101.txt 文件:

    http PATCH ":8888/hexacopters/1" motor_speed_in_rpm=50

以下是对应的 curl 命令。示例代码文件包含在 restful_python_2_10_01 文件夹中,位于 Tornado01/cmd/cmd1102.txt 文件:

 curl -iX PATCH -H "Content-Type: application/json" -d '{"motor_speed_in_rpm":50}' "localhost:8888/hexacopters/1"

之前的命令将组合并发送带有以下 JSON 键值对的 HTTP 请求 PATCH http://localhost:8888/hexacopters/1

{  
   "motor_speed_in_rpm": 50 
} 

请求指定了 /hexacopters/1,因此 Tornado 将遍历包含正则表达式和请求类的元组列表,并匹配正则表达式 '/hexacopters/([0-9]+)'。Tornado 将创建 HexacopterHandler 类的实例,并以 1 作为 id 参数的值调用 HexacopterHandler.patch 方法。

由于请求的 HTTP 动词是 PATCH,Tornado 调用 patch 方法。如果成功设置了六旋翼机的速度,该方法将返回 HTTP 200 OK 状态码,并在响应体中将速度和状态作为键值对序列化为 JSON 格式。以下行显示了 HTTP 请求的示例响应:

    HTTP/1.1 200 OK
    Content-Length: 48
    Content-Type: application/json; charset=UTF-8
    Date: Tue, 30 Oct 2018 17:01:06 GMT
    Server: TornadoServer/5.1.1

    {
    "is_turned_on": true, 
        "motor_speed_in_rpm": 50
    }

现在,我们将编写一个命令来组合并发送一个 HTTP GET 请求以检索六旋翼机的状态和电机速度。运行以下命令。示例代码文件包含在 restful_python_2_10_01 文件夹中,位于 Tornado01/cmd/cmd1103.txt 文件:

    http ":8888/hexacopters/1"

以下是对应的 curl 命令。示例代码文件包含在 restful_python_2_10_01 文件夹中,位于 Tornado01/cmd/cmd1104.txt 文件:

    curl -iX GET -H "localhost:8888/hexacopters/1"

之前的命令将组合并发送以下 HTTP 请求:GET http://localhost:8888/hexacopters/1。请求指定了 /hexacopters/1,因此它将匹配正则表达式 '/hexacopters/([0-9]+)' 并以 1 作为 id 参数的值调用 HexacopterHandler.get 方法。由于请求的 HTTP 动词是 GET,Tornado 调用 get 方法。该方法检索六旋翼机的状态并生成包含键值对的 JSON 响应。

以下行显示了 HTTP 请求的示例响应。前几行显示了 HTTP 响应头,包括状态(200 OK)和内容类型(application/json)。在 HTTP 响应头之后,我们可以在 JSON 响应中看到六旋翼机状态的详细信息:

    HTTP/1.1 200 OK
    Content-Length: 48
    Content-Type: application/json; charset=UTF-8
    Date: Tue, 30 Oct 2018 17:06:10 GMT
    Etag: "172316bfc38ea5a04857465b888cff65c72a228c"
    Server: TornadoServer/5.1.1

    {
    "is_turned_on": true, 
        "motor_speed_in_rpm": 50
    }

在我们运行两个请求之后,将在运行 Tornado HTTP 服务器的窗口中看到以下几行。输出显示了执行描述代码开始设置或检索信息以及完成时的打印语句的结果:

    I've started setting the hexacopter's motor speed
    I've finished setting the hexacopter's motor speed
    I've started retrieving the hexacopter's status
    I've finished retrieving the hexacopter's status

我们在请求处理类中编写的不同方法最终都会调用 time.sleep 来模拟与六旋翼飞行器操作需要花费一些时间。在这种情况下,我们的代码以同步执行运行,因此每次我们组合并发送请求时,Tornado 服务器都会被阻塞,直到与六旋翼飞行器的操作完成并且方法发送响应。我们将在稍后创建这个 API 的新版本,它将使用异步执行,并理解 Tornado 非阻塞特性的优势。然而,首先,我们将了解 API 的同步版本是如何工作的。

以下截图显示了 macOS 上并排的两个终端窗口。左侧的终端窗口正在运行 Tornado HTTP 服务器,并显示处理 HTTP 请求的方法中打印的消息。右侧的终端窗口正在运行 http 命令以生成 HTTP 请求。在我们组合并发送 HTTP 请求时,使用类似的配置来检查输出是一个好主意:

截图

现在,我们将编写一个命令来组合并发送一个 HTTP 请求以检索一个不存在的六旋翼飞行器。记住,我们只有一个六旋翼飞行器在我们的无人机中。运行以下命令尝试检索一个具有无效 id 的六旋翼飞行器的状态。我们必须确保工具显示响应的一部分作为头部信息,以查看返回的状态码。示例代码文件包含在 restful_python_2_10_01 文件夹中,位于 Tornado01/cmd/cmd1105.txt 文件:

    http ":8888/hexacopters/5"

以下是对应的 curl 命令。示例代码文件包含在 restful_python_2_10_01 文件夹中,位于 Tornado01/cmd/cmd1106.txt 文件:

    curl -iX GET "localhost:8888/hexacopters/5"

之前的命令将组合并发送以下 HTTP 请求:GET http://localhost:8888/hexacopters/5。请求与之前我们分析过的请求相同,只是 id 参数的数字不同。服务器将运行 HexacopterHandler.get 方法,将 5 作为 id 参数的值。id 不等于 1,因此代码将返回 HTTP 404 Not Found 状态码。以下几行显示了 HTTP 请求的一个示例响应头:

    HTTP/1.1 404 Not Found
    Content-Length: 0
    Content-Type: text/html; charset=UTF-8
    Date: Tue, 30 Oct 2018 17:22:13 GMT
    Server: TornadoServer/5.1.1

现在,我们将编写一个命令来组合并发送一个 HTTP GET 请求以从无人机中包含的高度计检索高度,以米为单位。运行以下命令。示例代码文件包含在 restful_python_2_10_01 文件夹中,位于 Tornado01/cmd/cmd1107.txt 文件:

    http ":8888/altimeters/1?unit=meters"

以下是对应的 curl 命令。示例的代码文件包含在 restful_python_2_10_01 文件夹中,位于 Tornado01/cmd/cmd1108.txt 文件:

    curl -iX GET -H "localhost:8888/altimeters/1?unit=meters"

之前的命令将编写并发送以下 HTTP 请求:GET http://localhost:8888/altimeters/1?unit=meters。请求指定了 /altimeters/1,因此它将匹配 '/altimeters/([0-9]+)' 正则表达式,并使用 1 作为 id 参数的值调用 AltimeterHandler.get 方法。由于请求的 HTTP 动词是 GET,Tornado 调用 get 方法。该方法将检索单位查询参数的值,检索气压计的海拔高度(以英尺为单位),将其转换为米,并生成包含键值对的 JSON 响应。

以下行显示了 HTTP 请求的一个示例响应:

    HTTP/1.1 200 OK
    Content-Length: 49
    Content-Type: application/json; charset=UTF-8
    Date: Tue, 30 Oct 2018 17:35:59 GMT
    Etag: "e6bef0812295935473bbef8883a144a7740d4838"
    Server: TornadoServer/5.1.1

    {
    "altitude": 126.7968, 
        "unit": "meters"
    }

现在,我们将编写一个命令来编写并发送一个 HTTP GET 请求,以检索无人机中包含的气压计的高度,以默认单位英尺表示。运行以下命令。示例的代码文件包含在 restful_python_2_10_01 文件夹中,位于 Tornado01/cmd/cmd1109.txt 文件:

    http ":8888/altimeters/1" 

以下是对应的 curl 命令。示例的代码文件包含在 restful_python_2_10_01 文件夹中,位于 Tornado01/cmd/cmd1110.txt 文件:

    curl -iX GET -H "localhost:8888/altimeters/1"

之前的命令将编写并发送以下 HTTP 请求:GET http://localhost:8888/altimeters/1。请求指定了 /altimeters/1,因此它将匹配 '/altimeters/([0-9]+)' 正则表达式,并使用 1 作为 id 参数的值调用 AltimeterHandler.get 方法。由于请求的 HTTP 动词是 GET,Tornado 调用 get 方法。在这种情况下,没有单位查询参数,因此该方法将检索气压计的海拔高度(以英尺为单位),并生成包含键值对的 JSON 响应。

以下行显示了 HTTP 请求的一个示例响应:

    HTTP/1.1 200 OK
    Content-Length: 33
    Content-Type: application/json; charset=UTF-8
    Date: Tue, 30 Oct 2018 17:38:58 GMT
    Etag: "985cc8ce1bddf8a96b2a06a76d14faaa5bc03c9b"
    Server: TornadoServer/5.1.1

    {
    "altitude": 263, 
        "unit": "feet"
    }

注意,海拔值是每次我们要求它时生成的随机数。

使用图形用户界面工具 - Postman 及其他

到目前为止,我们一直在使用两个基于终端或命令行的工具来编写并发送 HTTP 请求到我们的 Django 开发服务器:cURL 和 HTTPie。现在,我们将使用我们在 第一章 中编写并发送 HTTP 请求到 Flask 开发服务器时使用的图形用户界面工具之一:使用 Flask 1.0.2 开发 RESTful API 和微服务。如果你跳过了这一章,请确保检查名为 使用图形用户界面工具 - Postman 及其他 的部分中的安装说明。

一旦启动 Postman,请确保关闭提供常见任务快捷方式的模态窗口。在 Postman 主窗口的左上角,选择 + 新的下拉菜单中的 GET 请求 ...

使用 pytest 运行单元测试并检查测试覆盖率

现在,我们将使用pytest命令来运行测试并测量它们的代码覆盖率。请确保您在激活了虚拟环境的终端或命令提示符窗口中运行该命令,并且您位于其根文件夹(Tornado01)内。运行以下命令:

    pytest --cov -v  

测试运行器将执行在tests.py中定义的所有以test_前缀开始的函数,并将显示结果。我们使用-v选项来指示pytest以详细模式打印测试函数名称和状态。--cov选项通过使用pytest-cov插件来开启测试覆盖率报告生成。

以下行显示了示例输出:

================================================ test session starts =================================================
platform darwin -- Python 3.7.1, pytest-4.0.2, py-1.7.0, pluggy-0.8.0 -- /Users/gaston/HillarPythonREST2/Tornado01/bin/python3
cachedir: .pytest_cache
rootdir: /Users/gaston/HillarPythonREST2/Tornado01, inifile: 
setup.cfg
plugins: tornasync-0.5.0, cov-2.6.0
collected 1 item 

tests.py::test_set_and_get_leds_brightness_levels PASSED                                                       [100%]

 ---------- coverage: platform darwin, python 3.6.6-final-0 -----------
 -
    Name                     Stmts   Miss Branch BrPart  Cover
    ----------------------------------------------------------
    async_drone_service.py     141     81     20      4    40%
    drone.py                    63     23     10      3    59%
    ----------------------------------------------------------
    TOTAL                      204    104     30      7    46%

输出提供了测试运行器发现并执行了一个测试,该测试通过。输出显示了test_views模块中每个以test_前缀开始的函数的模块和函数名称,这些函数代表要执行的测试。

coverage包提供的测试代码覆盖率测量报告,结合pytest-cov插件,使用 Python 标准库中包含的代码分析工具和跟踪钩子来确定哪些代码行是可执行的,以及这些行中的哪些已被执行。报告提供了一个表格,其中包含了我们在第四章,“使用 Flask 在微服务中测试和部署 API”,在名为“使用 pytest 运行单元测试并检查测试覆盖率”的部分中检查的列。

根据报告中显示的测量结果,我们确实在async_drone_service.pydrone.py模块中具有非常低的覆盖率。实际上,我们只编写了一个与 LED 相关的测试,因此提高覆盖率是有意义的。我们没有创建与其他六旋翼资源相关的测试。

现在,使用带有-m命令行选项的coverage命令来显示在新的Missing列中遗漏语句的行号:

    coverage report -m

该命令将使用上次执行的信息,并显示遗漏的语句和遗漏的分支。下一行显示了与之前单元测试执行相对应的示例输出。破折号(-)用于表示遗漏行的范围。例如,107-109表示第 107 行和第 109 行缺少语句。破折号后跟一个大于号(->)表示从->之前的行到其后的行之间的分支被遗漏。例如,61->62表示从第 61 行到第 62 行的分支被遗漏:

Name                     Stmts   Miss Branch BrPart  Cover   Missing
---------------------------------------------------------------------
async_drone_service.py     141     81     20      4    40%   20-32, 36, 
40-67, 71-73, 84-86, 107-109, 114-116, 129-135, 148-168, 172, 186-193, 83->84, 106->107, 112->114, 185->186
drone.py                    63     23     10      3    59%   7-8, 21, 25-31, 35, 39-40, 60, 62, 70-71, 88-93, 59->60, 61->62, 87->88
----------------------------------------------------------------------
TOTAL                      204    104     30      7    46%

现在,运行以下命令以获取详细说明遗漏行的注释 HTML 列表。该命令不会产生任何输出:

    coverage html

使用您的网络浏览器打开在htmlcov文件夹中生成的index.html HTML 文件。以下截图显示了以 HTML 格式生成的示例报告覆盖率:

图片

点击或轻触async_drone_service.py,网页浏览器将渲染一个显示已运行、缺失、排除和部分执行的语句的网页,不同颜色表示。我们可以点击或轻触运行、缺失、排除和部分按钮来显示或隐藏代表每行代码状态的背景颜色。默认情况下,缺失的代码行将以粉色背景显示,部分执行的将以黄色背景显示[-]。因此,我们必须编写针对这些代码行的单元测试来提高我们的测试覆盖率。

下一个截图显示了带有总结的按钮:

下一个截图显示了async_drone_service.py模块中一些代码行的突出显示的缺失行和部分评估的分支:

测试你的知识

让我们看看你是否能正确回答以下问题:

  1. 以下哪个方法允许我们在tornado.web.RequestHandler的子类中设置 HTTP 状态码?

    1. self.write_status

    2. self.__cls__.write_status_code

    3. self.set_satus

  2. 以下哪个方法允许我们在tornado.web.RequestHandler的子类中写入响应?

    1. self.write_response

    2. self.write

    3. self.set_response

  3. Tornado 中 RESTful API 的主要构建块是以下哪个类的子类?

    1. tornado.web.GenericHandler

    2. tornado.web.RequestHandler

    3. tornado.web.IncomingHTTPRequestHandler

  4. 如果我们只想支持GETPATCH方法,我们可以覆盖SUPPORTED_METHODS类...

摘要

在本章中,我们设计了一个 RESTful API 来与慢速传感器和执行器交互。我们定义了 API 的要求,并理解了每个 HTTP 方法执行的任务。我们使用 Tornado 设置了虚拟环境。

我们创建了代表无人机类的类,并编写了代码来模拟每个 HTTP 请求方法所需的慢速 I/O 操作。我们编写了代表请求处理器的类,处理不同的 HTTP 请求,并配置了 URL 模式将 URL 路由到请求处理器及其方法。

最后,我们启动了 Tornado 开发服务器,并使用命令行工具向我们的 RESTful API 组合并发送 HTTP 请求,分析了我们的代码中每个 HTTP 请求的处理方式。我们还使用 GUI 工具组合并发送 HTTP 请求。我们意识到,由于模拟慢速 I/O 操作,每个 HTTP 请求都需要一些时间来提供响应。

现在我们已经了解了 Tornado 的基本知识来创建 RESTful API,我们将利用非阻塞特性,结合 Tornado 中的异步操作,在 API 的新版本中,我们将编写单元测试,这是下一章的主题。

第十一章:使用 Tornado 处理异步代码、测试和部署 API

在本章中,我们将利用 Tornado 5.1.1 的非阻塞特性以及异步操作,在上一章中构建的新版本的 API 中。我们将配置、编写和执行单元测试,并学习一些与部署相关的内容。我们将执行以下操作:

  • 理解同步和异步执行

  • 使用异步代码

  • 重新编写代码以利用异步装饰器

  • 将 URL 模式映射到异步和非阻塞请求处理器

  • 向 Tornado 非阻塞 API 发送 HTTP 请求

  • 使用pytest设置单元测试

  • 编写第一轮单元测试

  • 使用pytest运行单元测试并检查测试...

理解同步和异步执行

在我们使用 Tornado 5.1.1 构建的当前版本的 RESTful API 中,每个 HTTP 请求都是阻塞的。因此,每当 Tornado HTTP 服务器收到一个 HTTP 请求时,它不会开始处理队列中的任何其他 HTTP 请求,直到收到第一个 HTTP 请求的响应。我们在请求处理器中编写的代码是以同步执行方式工作的,并且没有利用 Tornado 在异步执行中包含的非阻塞特性。

为了设置红色、绿色和蓝色 LED 的亮度级别,我们必须发出三个 HTTP PATCH请求。我们将发出这些请求以了解我们当前版本的 API 如何处理三个传入的请求。

确保 Tornado 5.1.1 开发服务器正在运行。在 macOS 或 Linux 中打开三个额外的终端,或在 Windows 中打开命令提示符或 Windows PowerShell 窗口。在每个窗口中激活我们为使用 Tornado 构建的 RESTful API 所工作的虚拟环境。我们将在三个窗口中运行命令。

在第一个窗口中写下以下命令。该命令将组合并发送一个 HTTP PATCH请求,将红色 LED 的亮度级别设置为255。在第一个窗口中写下这一行,但不要按Enter键,因为我们将在三个窗口中几乎同时尝试启动三个命令。示例的代码文件包含在restful_python_2_11_01文件夹中,在Tornado01/cmd/cmd1201.txt文件中:

    http PATCH ":8888/leds/1" brightness_level=255

以下是对应的curl命令。示例的代码文件包含在restful_python_2_11_01文件夹中,在Tornado01/cmd/cmd1202.txt文件中:

 curl -iX PATCH -H "Content-Type: application/json" -d '{"brightness_level":255}' "localhost:8888/leds/1"

现在,转到第二个窗口,写下以下命令。该命令将组合并发送一个 HTTP PATCH请求,将绿色 LED 的亮度级别设置为128。在第二个窗口中写下这一行,但不要按Enter键,因为我们将在三个窗口中几乎同时尝试启动两个命令。示例的代码文件包含在restful_python_2_11_01文件夹中,在Tornado01/cmd/cmd1203.txt文件中:

    http PATCH ":8888/leds/2" brightness_level=128

以下是对应的 curl 命令。示例的代码文件包含在 restful_python_2_11_01 文件夹中,位于 Tornado01/cmd/cmd1204.txt 文件中:

 curl -iX PATCH -H "Content-Type: application/json" -d '{"brightness_level":128}' "localhost:8888/leds/2"

现在,转到第三个窗口并输入以下命令。该命令将组合并发送一个 HTTP PATCH 请求来设置蓝色 LED 的亮度级别为 64。在第三个窗口中写下这一行,但不要按 Enter 键,因为我们将在三个窗口中几乎同时尝试启动两个命令。示例的代码文件包含在 restful_python_2_11_01 文件夹中,位于 Tornado01/cmd/cmd1205.txt 文件中:

    http PATCH ":8888/leds/3" brightness_level=64

以下是对应的 curl 命令。示例的代码文件包含在 restful_python_2_11_01 文件夹中,位于 Tornado01/cmd/cmd1206.txt 文件中:

 curl -iX PATCH -H "Content-Type: application/json" -d '{"brightness_level":64}' "localhost:8888/leds/3"

现在,转到每个窗口,从第一个到第三个,并在每个窗口中快速按下 Enter 键。你将在运行 Tornado HTTP 服务器的窗口中看到以下几秒钟的行:

    I've started setting the Red LED's brightness level

几秒钟后,你将看到以下几行,它们显示了执行描述代码完成时以及开始设置 LED 亮度级别的打印语句的结果:

    I've started setting the Red LED's brightness level
    I've finished setting the Red LED's brightness level
    I've started setting the Green LED's brightness level
    I've finished setting the Green LED's brightness level
    I've started setting the Blue LED's brightness level
    I've finished setting the Blue LED's brightness level

在服务器能够处理更改绿色 LED 亮度级别的 HTTP 请求之前,必须等待更改红色 LED 亮度级别的请求完成。更改蓝色 LED 亮度级别的 HTTP 请求必须等待其他两个请求首先完成它们的执行。

以下截图显示了 macOS 上的四个终端窗口。左侧的窗口正在运行 Tornado 5.1.1 HTTP 服务器,并显示处理 HTTP 请求的方法中打印的消息。右侧的三个窗口运行 http 命令以生成更改红色、绿色和蓝色 LED 亮度级别的 HTTP PATCH 请求。在编写和发送 HTTP 请求时使用类似的配置来检查输出是一个好主意,这样我们就可以理解当前 API 版本中同步执行的工作方式:

图片

记住,我们在请求处理类中编写的不同方法最终都会调用 time.sleep 来模拟操作执行所需的时间。

重构代码以利用异步装饰器

由于每个操作都需要一些时间并且会阻止处理其他传入的 HTTP 请求,我们将创建这个 API 的新版本,它将使用异步执行,我们将了解 Tornado 非阻塞特性的优势。这样,在另一个请求更改绿色 LED 亮度级别的同时,就可以更改红色 LED 的亮度级别。Tornado 将能够在 I/O 操作与无人机完成一些时间后开始处理请求。

确保您已退出 Tornado HTTP 服务器。您只需在运行它的终端或命令提示符窗口中按 Ctrl + C 即可。

Tornado 5.1.1 提供 ...

将 URL 模式映射到异步请求处理器

保持处于虚拟环境根目录下的 async_drone_service.py 文件中(Tornado01)。添加以下行以将 URL 模式映射到我们之前编写的 RequestHandler 超类子类,这些子类为我们提供了异步方法。以下行创建应用程序的主入口点,用 API 的 URL 模式初始化它,并开始监听请求。与同步版本相比,以下行是新的或已编辑的,已突出显示。示例代码文件位于 restful_python_2_11_01 文件夹中的 Django01/async_drone_service.py 文件:

class Application(web.Application): 
    def __init__(self, **kwargs): 
        handlers = [ 
            (r"/hexacopters/([0-9]+)", AsyncHexacopterHandler), 
            (r"/leds/([0-9]+)", AsyncLedHandler), 
            (r"/altimeters/([0-9]+)", AsyncAltimeterHandler), 
        ] 
        super(Application, self).__init__(handlers, **kwargs) 

if __name__ == "__main__": 
    application = Application() 
    port = 8888 
    print("Listening at port {0}".format(port)) 
    application.listen(port) 
    tornado_ioloop = ioloop.IOLoop.instance() 
    periodic_callback = ioloop.PeriodicCallback(lambda: None, 500) 
    periodic_callback.start() 
    tornado_ioloop.start() 

代码创建了一个名为 applicationtornado.web.Application 实例,其中包含构成 Web 应用程序的请求处理器集合。我们只是将处理器的名称更改为具有 Async 前缀的新名称。

向 Tornado 非阻塞 API 发送 HTTP 请求

现在,我们可以运行 drone_service.py 脚本,该脚本启动 Tornado 5.1.1 开发服务器,为我们的新版本 Web API 启动,该 API 使用 Tornado 的非阻塞特性,并结合异步执行。确保 drone_service.py 脚本不再运行。执行以下命令:

    python async_drone_service.py

以下行显示了执行上一条命令后的输出。Tornado HTTP 开发服务器正在端口 8888 监听:

    Listening at port 8888

在我们 API 的新版本中,每个 HTTP 请求都是非阻塞的。因此,每当 Tornado HTTP 服务器收到 HTTP 请求并执行异步调用时,它就能够开始 ...

使用 pytest 设置单元测试

确保您已退出 Django 开发服务器。您只需在运行它的终端或命令提示符窗口中按 Ctrl + C 即可。

现在,我们将安装许多附加包,以便能够轻松运行测试并测量它们的代码覆盖率。确保您已激活我们在上一章中创建的虚拟环境,名为 Tornado01。激活虚拟环境后,是时候运行许多命令了,这些命令对 macOS、Linux 和 Windows 都相同。

现在,我们将编辑现有的 requirements.txt 文件,以指定我们的应用程序在任意支持平台上需要安装的附加包。这样,在任意新的虚拟环境中重复安装指定包及其版本将变得极其容易。

使用您喜欢的编辑器编辑虚拟环境根目录内现有的名为 requirements.txt 的文本文件。在最后一行之后添加以下行以声明我们所需的额外包。示例的代码文件包含在 restful_python_2_11_01 文件夹中,在 Tornado01/requirements.txt 文件中:

pytest==3.9.3 
coverage==4.5.1 
pytest-cov==2.6.0 
pytest-tornasync==0.5.0 

添加到 requirements.txt 文件中的每一行额外内容都指示需要安装的包和版本。

以下表格总结了我们作为额外要求指定的之前包含的包及其版本号:

包名 要安装的版本
pytest 4.0.2
coverage 4.5.2
pytest-cov 2.6.0
pytest-tornasync 0.5.0

我们将在我们的虚拟环境中安装以下 Python 包:

  • pytest:这是一个非常流行的 Python 单元测试框架,它使测试变得简单,并减少了样板代码。

  • coverage:这个工具测量 Python 程序的代码覆盖率,我们将使用它来确定哪些代码部分被单元测试执行,哪些部分没有。

  • pytest-cov:这个针对 pytest 的插件使得使用底层的 coverage 工具生成覆盖率报告变得容易,并提供了一些额外的功能。

  • pytest-tornasync:这个针对 pytest 的插件提供了固定装置,使得使用 pytest 测试 Tornado 异步代码变得更加容易。

现在,我们必须在 macOS、Linux 或 Windows 上运行以下命令,使用 pip 安装之前表格中概述的额外包及其版本,使用最近编辑的 requirements.txt 文件。在运行命令之前,请确保您位于包含 requirements.txt 文件的文件夹中:

pip install -r requirements.txt 

输出的最后几行将指示所有新包及其依赖项是否已成功安装。如果您下载了示例的源代码,并且没有使用之前的 API 版本,pip 还将安装 requirements.txt 文件中包含的其他包:

Installing collected packages: pytest, coverage, pytest-cov, pytest-tornasync
Successfully installed coverage-4.5.2 pytest-4.0.2 pytest-cov-2.6.0 pytest-tornasync-0.5.0

在虚拟环境(Tornado01)的根目录下创建一个新的 setup.cfg 文件。以下行显示了指定 pytestcoverage 工具所需配置的代码。示例的代码文件包含在 restful_python_2_11_01 文件夹中,在 Tornado01/setup.cfg 文件中:

[tool:pytest] 
testpaths = tests.py 

[coverage:run] 
branch = True 
source =  
    drone 
    async_drone_service 

tool:pytest 部分指定了 pytest 的配置。testpaths 设置将 tests.py 的值分配给指示测试位于 tests.py 文件中。

coverage:run 部分指定了 coverage 工具的配置。branch 设置设置为 True 以启用除了默认语句覆盖率之外的分支覆盖率测量。source 设置指定了我们希望考虑进行覆盖率测量的模块。我们只想包括 droneasync_drone_service 模块。

在这种情况下,我们不会为每个环境使用配置文件。然而,在更复杂的应用程序中,你肯定会想使用配置文件。

编写第一轮单元测试

现在,我们将编写第一轮单元测试。具体来说,我们将编写与 LED 资源相关的单元测试。测试固定装置提供了一个固定的基线,使我们能够可靠地重复执行测试。Pytest 通过使用@pytest.fixture装饰器标记函数,使我们能够轻松地声明测试固定装置函数。然后,每当我们在测试函数声明中使用固定装置函数名称作为参数时,pytest将使固定装置函数提供固定装置对象。

pytest-tornasync插件为我们提供了许多固定装置,我们将使用这些固定装置轻松编写我们的 Tornado API 测试。为了与该插件一起工作,我们必须声明一个名为app的固定装置函数,该函数返回一个tornado.web.Application ...

提高测试覆盖率

现在,我们将编写额外的单元测试以提高测试覆盖率。具体来说,我们将编写与六旋翼飞行器电机和高度计相关的单元测试。

打开虚拟环境根文件夹(Tornado01)中的tests.py文件。在最后一行之后插入以下行。示例代码文件包含在restful_python_2_11_02文件夹中的Django01/tests.py文件:

async def 
test_set_and_get_hexacopter_motor_speed(http_server_client): 
    """ 
    Ensure we can set and get the hexacopter's motor speed 
    """ 
    patch_args = {'motor_speed_in_rpm': 200} 
    patch_response = await http_server_client.fetch( 
        '/hexacopters/1',  
        method='PATCH',  
        body=json.dumps(patch_args)) 
    assert patch_response.code == HTTPStatus.OK 
    get_response = await http_server_client.fetch( 
        '/hexacopters/1', 
        method='GET') 
    assert get_response.code == HTTPStatus.OK 
    get_response_data = escape.json_decode(get_response.body) 
    assert 'motor_speed_in_rpm' in get_response_data.keys() 
    assert 'is_turned_on' in get_response_data.keys() 
    assert get_response_data['motor_speed_in_rpm'] == patch_args['motor_speed_in_rpm'] 
    assert get_response_data['is_turned_on'] 

async def 
test_get_altimeter_altitude_in_feet(http_server_client): 
    """ 
    Ensure we can get the altimeter's altitude in feet 
    """ 
    get_response = await http_server_client.fetch( 
        '/altimeters/1', 
        method='GET') 
    assert get_response.code == HTTPStatus.OK 
    get_response_data = escape.json_decode(get_response.body) 
    assert 'altitude' in get_response_data.keys() 
    assert 'unit' in get_response_data.keys() 
    assert get_response_data['altitude'] >= 0 
    assert get_response_data['altitude'] <= 3000 
    assert get_response_data['unit'] == 'feet' 

async def 
test_get_altimeter_altitude_in_meters(http_server_client): 
    """ 
    Ensure we can get the altimeter's altitude in meters 
    """ 
    get_response = await http_server_client.fetch( 
        '/altimeters/1?unit=meters', 
        method='GET') 
    assert get_response.code == HTTPStatus.OK 
    get_response_data = escape.json_decode(get_response.body) 
    assert 'altitude' in get_response_data.keys() 
    assert 'unit' in get_response_data.keys() 
    assert get_response_data['altitude'] >= 0 
    assert get_response_data['altitude'] <= 914.4 
    assert get_response_data['unit'] == 'meters'

之前添加了以下三个测试函数,它们的名称以test_前缀开头,并接收http_server_client参数以使用此测试固定装置:

  • test_set_and_get_hexacopter_motor_speed:此测试函数测试我们是否可以设置和获取六旋翼飞行器的电机速度

  • test_get_altimeter_altitude_in_feet:此测试函数测试我们是否可以从高度计检索以英尺为单位的高度值

  • test_get_altimeter_altitude_in_meters:此测试函数测试我们是否可以从高度计检索以米为单位的高度值

我们仅编写了一些与六旋翼飞行器和高度计相关的测试,以提高测试覆盖率并记录对测试覆盖率报告的影响。

现在,我们将使用pytest命令运行测试并测量它们的代码覆盖率。确保你在激活了虚拟环境的终端或命令提示符窗口中运行此命令,并且你位于其根文件夹(Tornado01)内。运行以下命令:

    pytest --cov -s

以下行显示了示例输出:

================================================ test session starts =================================================
platform darwin -- Python 3.7.1 pytest-4.0.2, py-1.7.0, pluggy-0.8.0 -- /Users/gaston/HillarPythonREST2/Tornado01/bin/python3
cachedir: .pytest_cache
rootdir: /Users/gaston/HillarPythonREST2/Tornado01, inifile: 
setup.cfg
plugins: tornasync-0.5.0, cov-2.6.0
collected 4 items 

tests.py::test_set_and_get_leds_brightness_levels PASSED                                                       [ 25%]
tests.py::test_set_and_get_hexacopter_motor_speed PASSED                                                       [ 50%]
tests.py::test_get_altimeter_altitude_in_feet PASSED                                                           [ 75%]
tests.py::test_get_altimeter_altitude_in_meters PASSED                                                         [100%]

 ---------- coverage: platform darwin, python 3.7.1-final-0 -----------
    Name                     Stmts   Miss Branch BrPart  Cover
    ----------------------------------------------------------
    async_drone_service.py     142     41     20      8    69%
    drone.py                    63     10     10      5    79%
    ----------------------------------------------------------
    TOTAL                      205     51     30     13    72%

输出提供了详细信息,表明测试运行器执行了四个测试,并且所有测试都通过了。由 coverage 包提供的测试代码覆盖率测量报告将 async_drone_service.py 模块的 Cover 百分比从 40% 提高到 69%。此外,drone.py 模块的 Cover 百分比从上一次运行的 59% 提高到 79%。我们编写的新测试在多个模块中执行了额外的代码,因此在覆盖率报告中产生了重要影响。总覆盖率从 46% 提高到 72%。

如果我们查看缺失的语句,我们会注意到我们没有测试验证失败的场景。现在,我们将编写额外的单元测试来进一步提高测试覆盖率。具体来说,我们将编写单元测试以确保我们无法为 LED 设置无效的亮度级别,无法为六旋翼机设置无效的电机速度,并且在尝试访问不存在的资源时,我们会收到 HTTP 404 Not Found 状态码。

打开虚拟环境(Tornado01)根目录下的 tests.py 文件。在最后一行之后插入以下行。示例代码文件包含在 restful_python_2_11_03 文件夹中,在 Django01/tests.py 文件中:

async def test_set_invalid_brightness_level(http_server_client): 
    """ 
    Ensure we cannot set an invalid brightness level for a LED 
    """ 
    patch_args_led_1 = {'brightness_level': 256} 
    try: 
        patch_response_led_1 = await http_server_client.fetch( 
            '/leds/1',  
            method='PATCH',  
            body=json.dumps(patch_args_led_1)) 
    except HTTPClientError as err: 
        assert err.code == HTTPStatus.BAD_REQUEST 
    patch_args_led_2 = {'brightness_level': -256} 
    try: 
        patch_response_led_2 = await http_server_client.fetch( 
            '/leds/2',  
            method='PATCH',  
            body=json.dumps(patch_args_led_2)) 
    except HTTPClientError as err: 
        assert err.code == HTTPStatus.BAD_REQUEST 
    patch_args_led_3 = {'brightness_level': 512} 
    try: 
        patch_response_led_3 = await http_server_client.fetch( 
            '/leds/3',  
            method='PATCH',  
            body=json.dumps(patch_args_led_3)) 
    except HTTPClientError as err: 
        assert err.code == HTTPStatus.BAD_REQUEST 

async def 
test_set_brightness_level_invalid_led_id(http_server_client): 
    """ 
    Ensure we cannot set the brightness level for an invalid LED id 
    """ 
    patch_args_led_1 = {'brightness_level': 128} 
    try: 
        patch_response_led_1 = await http_server_client.fetch( 
            '/leds/100',  
            method='PATCH',  
            body=json.dumps(patch_args_led_1)) 
    except HTTPClientError as err: 
        assert err.code == HTTPStatus.NOT_FOUND 

async def 
test_get_brightness_level_invalid_led_id(http_server_client): 
    """ 
    Ensure we cannot get the brightness level for an invalid LED id 
    """ 
    try: 
        patch_response_led_1 = await http_server_client.fetch( 
            '/leds/100',  
            method='GET') 
    except HTTPClientError as err: 
        assert err.code == HTTPStatus.NOT_FOUND

之前的代码添加了以下三个测试函数,它们的名称以 test_ 前缀开头,并接收 http_server_client 参数以使用此测试固定装置:

  • test_set_invalid_brightness_level:这个测试函数确保我们无法通过 HTTP PATCH 请求为 LED 设置无效的亮度级别。在这个方法中,许多 try...except 块捕获 HTTPClientError 异常作为 err 并使用 assert 确保异常的 err.code 属性与 HTTPStatus.BAD_REQUEST 匹配。这样,测试确保每个 HTTP PATCH 请求都生成了 HTTP 400 Bad Request 状态码。

  • test_set_brightness_level_invalid_led_id:这个测试函数确保我们无法通过 HTTP PATCH 请求设置无效 LED id 的亮度级别。

  • test_get_brightness_level_invalid_led_id:这个测试函数确保我们无法通过 HTTP GET 请求获取无效 LED id 的亮度级别。

在最后两种方法中,一个 try...except 块捕获了 HTTPClientError 异常作为 errexcept 块使用 assert 确保异常的 err.code 属性与 HTTPStatus.NOT_FOUND 匹配。这样,测试确保了 HTTP PATCH 和 HTTP GET 请求生成了 HTTP 404 Not Found 状态码。

当 HTTP 请求失败时,http_server_client.fetch 方法会抛出 tornado.httpclient.HTTPClientError 异常,状态码在实例的 code 属性中可用。

在虚拟环境(Tornado01)根目录下的 tests.py 文件中保持。在最后一行之后插入以下行。示例的代码文件包含在 restful_python_2_11_03 文件夹中的 Django01/tests.py 文件:

async def test_set_invalid_motor_speed(http_server_client): 
    """ 
    Ensure we cannot set an invalid motor speed for the hexacopter 
    """ 
    patch_args_hexacopter_1 = {'motor_speed': 89000} 
    try: 
        patch_response_hexacopter_1 = await http_server_client.fetch( 
            '/hexacopters/1',  
            method='PATCH',  
            body=json.dumps(patch_args_hexacopter_1)) 
    except HTTPClientError as err: 
        assert err.code == HTTPStatus.BAD_REQUEST 
    patch_args_hexacopter_2 = {'motor_speed': -78600} 
    try: 
        patch_response_hexacopter_2 = await http_server_client.fetch( 
            '/hexacopters/1',  
            method='PATCH',  
            body=json.dumps(patch_args_hexacopter_2)) 
    except HTTPClientError as err: 
        assert err.code == HTTPStatus.BAD_REQUEST 
    patch_args_hexacopter_3 = {'motor_speed': 8900} 
    try: 
        patch_response_hexacopter_3 = await http_server_client.fetch( 
            '/hexacopters/1',  
            method='PATCH',  
            body=json.dumps(patch_args_hexacopter_3)) 
    except HTTPClientError as err: 
        assert err.code == HTTPStatus.BAD_REQUEST 

async def test_set_motor_speed_invalid_hexacopter_id(http_server_client): 
    """ 
    Ensure we cannot set the motor speed for an invalid hexacopter id 
    """ 
    patch_args_hexacopter_1 = {'motor_speed': 128} 
    try: 
        patch_response_hexacopter_1 = await http_server_client.fetch( 
            '/hexacopters/100',  
            method='PATCH',  
            body=json.dumps(patch_args_hexacopter_1)) 
    except HTTPClientError as err: 
        assert err.code == HTTPStatus.NOT_FOUND 

async def test_get_motor_speed_invalid_hexacopter_id(http_server_client): 
    """ 
    Ensure we cannot get the motor speed for an invalid hexacopter id 
    """ 
    try: 
        patch_response_hexacopter_1 = await http_server_client.fetch( 
            '/hexacopters/5',  
            method='GET') 
    except HTTPClientError as err: 
        assert err.code == HTTPStatus.NOT_FOUND 

async def test_get_altimeter_altitude_invalid_altimeter_id(http_server_clie
nt): 
    """ 
    Ensure we cannot get the altimeter's altitude for an invalid altimeter id 
    """ 
    try: 
        get_response = await http_server_client.fetch( 
            '/altimeters/5', 
            method='GET') 
    except HTTPClientError as err: 
        assert err.code == HTTPStatus.NOT_FOUND         

之前的代码添加了以下四个测试函数,它们的名称以 test_ 前缀开头,并接收 http_server_client 参数以使用此测试固定装置:

  • test_set_invalid_brightness_level: 这个测试函数确保我们无法通过 HTTP PATCH 请求设置 LED 的无效亮度级别

  • test_set_motor_speed_invalid_hexacopter_id: 这个测试函数确保我们无法通过 HTTP PATCH 请求设置无效六旋翼飞行器的 id 的电机速度

  • test_get_motor_speed_invalid_hexacopter_id: 这个测试函数确保我们无法获取无效六旋翼飞行器的 id 的电机速度

  • test_get_altimeter_altitude_invalid_altimeter_id: 这个测试函数确保我们无法获取无效高度计 id 的高度值

我们编写了许多额外的测试,以确保所有验证都能按预期工作。现在,我们将再次使用 pytest 命令来运行测试并测量它们的代码覆盖率。确保你在激活了虚拟环境的终端或命令提示符窗口中运行此命令,并且你位于其根目录(Tornado01)内。运行以下命令:

    pytest --cov -v

以下行显示了示例输出:

================================================ test session starts =================================================
platform darwin -- Python 3.7.1, pytest-4.0.2, py-1.7.0, pluggy-0.8.0 -- /Users/gaston/HillarPythonREST2/Tornado01/bin/python3
cachedir: .pytest_cache
rootdir: /Users/gaston/HillarPythonREST2/Tornado01, inifile: 
setup.cfg
plugins: tornasync-0.5.0, cov-2.6.0
collected 11 items 

tests.py::test_set_and_get_leds_brightness_levels PASSED                                                       [  9%]
tests.py::test_set_and_get_hexacopter_motor_speed PASSED                                                       [ 18%]
tests.py::test_get_altimeter_altitude_in_feet PASSED                                                           [ 27%]
tests.py::test_get_altimeter_altitude_in_meters PASSED                                                         [ 36%]
tests.py::test_set_invalid_brightness_level PASSED                                                             [ 45%]
tests.py::test_set_brightness_level_invalid_led_id PASSED                                                      [ 54%]
tests.py::test_get_brightness_level_invalid_led_id PASSED                                                      [ 63%]
tests.py::test_set_invalid_motor_speed PASSED                                                                  [ 72%]
tests.py::test_set_motor_speed_invalid_hexacopter_id PASSED                                                    [ 81%]
tests.py::test_get_motor_speed_invalid_hexacopter_id PASSED                                                    [ 90%]
tests.py::test_get_altimeter_altitude_invalid_altimeter_id PASSED                                              [100%]

------------ coverage: platform darwin, python 3.7.1-final-0 -----------
    Name                     Stmts   Miss Branch BrPart  Cover
    ----------------------------------------------------------
    async_drone_service.py     142     17     20      2    87%
    drone.py                    63      8     10      3    85%
    ----------------------------------------------------------
    TOTAL                      205     25     30      5    86%

提供的输出详细说明了测试运行器执行了 11 个测试,并且所有测试都通过了。由 coverage 包提供的测试代码覆盖率测量报告将 async_drone_service.py 模块的 Cover 百分比从 69% 提高到 87%。此外,drone.py 模块的 Cover 百分比从上一次运行的 79% 提高到 85%。我们编写的新测试在多个模块中执行了额外的代码,因此在覆盖率报告中产生了重要影响。总覆盖率从 72% 提高到 86%。

理解将 Tornado API 部署到云的策略

Tornado 提供了自己的 HTTP 服务器,因此它可以不使用 WSGI 容器运行。然而,一些云提供商,如 Google App Engine,仅允许在 WSGI 环境中运行 Tornado。当 Tornado 在 WSGI 环境中运行时,它不支持异步操作。因此,在选择我们的云平台时,我们必须考虑到这个重要的限制。

我们必须确保在生产环境中 API 在 HTTPS 下运行。此外,我们还需要确保添加一些身份验证和限流策略。我们的 Tornado 示例是一个简单的 RESTful API,它提供了一些我们可以用作基准来生成更 ...

测试你的知识

让我们看看你是否能正确回答以下问题:

  1. Future 做了以下哪一项?

    1. 封装可调用的异步执行

    2. 封装可调用的同步执行

    3. 在作为参数指定的执行器上同步运行异步方法

  2. concurrent.futures.ThreadPoolExecutor类为我们提供了以下哪个功能?

    1. 同步执行调用的高级接口

    2. 异步执行调用的高级接口

    3. 组合 HTTP 请求的高级接口

  3. @tornado.concurrent.run_on_executor装饰器允许我们做以下哪一项?

    1. 在执行器上同步运行异步方法

    2. 在执行器上运行异步方法而不生成Future

    3. 在执行器上异步运行同步方法

  4. 在 Tornado 中编写异步代码的推荐方法是使用以下哪个?

    1. 协程

    2. 链式回调

    3. 子程序

  5. 以下哪个由pytest-tornasync pytest插件定义的固定值提供了异步 HTTP 客户端用于测试?

    1. tornado_client

    2. http_client

    3. http_server_client

  6. 如果我们想将 JSON 响应体中的字节转换为 Python 字典,我们可以使用以下哪个函数?

    1. tornado.escape.json_decode

    2. tornado.escape.byte_decode

    3. tornado.escape.response_body_decode

摘要

在本章中,我们了解了同步执行和异步执行之间的区别。我们创建了一个新的 RESTful API 版本,它利用了 Tornado 中的非阻塞特性,并结合了异步执行。我们提高了现有 API 的可扩展性,并使其在等待传感器和执行器的慢速 I/O 操作时能够开始执行其他请求。通过使用 Tornado 提供的基于tornado.gen生成器的接口来避免将我们的方法分割成多个带有回调的方法,这使得在异步环境中工作变得更加容易。

然后,我们设置了测试环境。我们安装了pytest以及许多插件,以便更容易地发现和执行单元测试 ...

第十二章:评估

第一章

  1. 用 Python 编写的命令行 HTTP 客户端,使其轻松组合和发送 HTTP 请求

  2. 在 Flask 可插拔视图之上构建的资源

  3. patch

  4. put

  5. post

  6. get

  7. 一个 RESTful 资源

  8. 将在 notification_fields 中指定的字段过滤和输出格式应用于适当的实例

第二章

  1. flask run -h 0.0.0.0

  2. 一个使用 Alembic 包处理 Flask 应用程序 SQLAlchemy 数据迁移的库

  3. 一个轻量级库,用于将复杂的数据类型转换为原生 Python 数据类型,反之亦然

  4. 一个对象关系映射(ORM)

  5. 注册一个在反序列化对象之前调用的方法

  6. 接受作为参数传递的实例或实例集合,并将 Schema 子类中指定的字段过滤和输出格式应用于实例或实例集合

  7. 该字段将嵌套单个 Schema 或基于 many 参数值的 Schema 集合

第三章

  1. PUT

  2. PATCH

  3. 535,000

  4. 一个代理,允许我们只存储一次请求中想要共享的内容

  5. 一个支持超过 30 种方案的密码哈希框架

  6. 使此函数成为 Flask-HTTPAuth 用于验证特定用户密码的回调函数

  7. 资源中声明的所有方法都将应用 auth.login_required 装饰器

  8. page_number = request.args.get('page', 1, type=int)

第四章

  1. @pytest.fixture

  2. test_

  3. coverage report -m

  4. 一个使测试变得简单并减少样板代码的单元测试框架

  5. 测量 Python 程序的代码覆盖率

第五章

  1. python manage.py startapp recipes

  2. 'django-rest-framework'

  3. 与 Django 集成

  4. 模型实例和 Python 原始类型之间的中介

  5. 将 URL 路由到视图

  6. Python 原始类型和 HTTP 请求与响应

  7. 一个作为 django.db.models.Model 超类子类的 Game

第六章

  1. 一个将基于函数的视图转换为 rest_framework.views.APIView 类子类的包装器

  2. FormModelForm

  3. SlugRelatedField

  4. HyperlinkedRelatedField

  5. 当请求指定 text/html 作为请求头中 Content-type 键的值时,为每个资源生成人类友好的 HTML 输出

第七章

  1. title = django.db.models.CharField(max_length=250, unique=True)

  2. title = django.db.models.CharField(max_length=250, unique=False)

  3. DEFAULT_PAGINATION_CLASS

  4. rest_framework.pagination.LimitOffsetPagination

  5. 提供基于用户名和密码的 HTTP 基本身份验证

  6. 与 Django 的会话框架一起用于身份验证

  7. DEFAULT_AUTHENTICATION_CLASSES

第八章

  1. client

  2. @pytest.mark.django_db

  3. 限制特定 API 部分的请求速率,这些部分由分配给 throttle_scope 属性的值标识

  4. 限制特定用户可以发起的请求速率

  5. 提供字段过滤功能

  6. 提供基于单个查询参数的搜索功能,并且它基于 Django 管理员的搜索功能

  7. filterset_class

第九章

  1. 实现了__call__方法的 Python 对象,如函数、类和实例

  2. pyramid.request.Request

  3. status_code

  4. HTTPCreated

  5. json_body

第十章

  1. self.set_status

  2. self.write

  3. tornado.web.RequestHandler

  4. ("GET", "PATCH")

  5. 正则表达式 (regexp) 和 tornado.web.IncomingHTTPRequestHandler 子类 (request_class)

  6. 自动将块写入 JSON 格式,并将Content-Type头设置为application/json

  7. 为请求体的 JSON 字符串生成 Python 对象,并返回生成的字典

第十一章

  1. 封装可调用对象的异步执行

  2. 异步执行可调用对象的高级接口

  3. 在执行器上异步运行同步方法

  4. 协程

  5. http_server_client

  6. tornado.escape.json_decode

posted @ 2025-09-19 10:35  绝不原创的飞龙  阅读(25)  评论(0)    收藏  举报