Flask-REST-API-构建指南-全-

Flask REST API 构建指南(全)

原文:Building REST APIs with Flask

协议:CC BY-NC-SA 4.0

一、从 Flask 开始

Flask 是一个 BSD 许可的 Python 微框架,基于 Werkzeug 和 Jinja2。作为一个微框架并不会降低它的功能性;Flask 是一个非常简单但高度可扩展的框架。这使得开发人员能够选择他们想要的配置,从而使编写应用或插件变得容易。Flask 最初是由 Pocoo(一个开源开发团队)在 2010 年创建的,现在由 Pallets 项目开发和维护,该项目为 Flask 背后的所有组件提供动力。Flask 由一个活跃的、有帮助的开发者社区支持,包括一个活跃的 IRC 频道和一个邮件列表。

Flask 简介

Flask 有两个主要组件,Werkzeug 和 Jinja2。Werkzeug 负责提供路由、调试和 Web 服务器网关接口(WSGI),而 Flask 利用 Jinja2 作为模板引擎。Flask 本身不支持数据库访问、用户认证或任何其他高级实用程序,但它支持扩展集成来添加所有这些功能,这使 Flask 成为一个用于开发 web 应用和服务的微型生产就绪框架。一个简单的 Flask 应用可以放入一个 Python 文件中,也可以模块化来创建一个生产就绪的应用。Flask 背后的想法是为所有的应用建立一个良好的基础,把其他的一切都留在扩展上。

Flask 社区非常大,并且非常活跃,有数百个开源扩展。Flask 核心团队不断审查扩展,并确保批准的扩展与未来版本兼容。Flask 作为一个微框架,为开发人员提供了选择适合他们项目的设计决策的灵活性。它维护一个定期更新和持续维护的扩展注册表。

起始 Flask

Flask 就像所有其他 Python 库一样,可以从 Python 包索引(PPI)安装,并且非常容易设置和开始开发,只需要几分钟就可以开始使用 Flask。为了能够理解这本书,你应该熟悉 Python、命令行(或者至少是 PIP)和 MySQL。

正如承诺的那样,Flask 非常容易上手,仅五行代码就可以让您开始使用一个最小的 Flask 应用。

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, From Flask!'

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

Listing 1-1Basic Flask Application

前面的代码导入 Flask 库,通过创建 Flask 类的实例启动应用,声明路由,然后定义调用路由时要执行的函数。这段代码足以启动您的第一个 Flask 应用。

下面的代码启动了一个非常简单的内置服务器,这对于测试来说已经足够好了,但是当您想要投入生产时,可能就不行了,但是我们将在后面的章节中介绍这一点。

当该应用启动时,索引路由将根据请求返回“您好,来自 Flask!”如图 1-1 所示。

img/479840_1_En_1_Fig1_HTML.jpg

图 1-1

Flask 最小应用

本书涵盖的 Flask 组件

既然已经向您介绍了 Flask,我们将讨论本书 Flask REST API 开发中涉及的组件。

这本书将作为使用 Flask 开发 REST API 的实用指南,我们将使用 MySQL 作为后端数据库。正如已经讨论过的,Flask 没有自带数据库访问支持,为了弥补这一缺陷,我们将使用一个名为 Flask-SQLAlchemy 的 Flask 扩展,它在 Flask 中增加了对 SQLAlchemy 的支持。SQLAlchemy 本质上是一个 Python SQL 工具包和对象关系映射器,它为开发人员提供了 SQL 的全部功能和灵活性。

SQLAlchemy 提供了对企业级设计模式的全面支持,是为高性能数据库访问而设计的,同时保持了效率和易用性。我们将构建一个用户认证模块,CRUD(创建、读取、更新和删除)REST APIs,用于对象创建、检索、操作和删除。我们还将集成一个名为 Swagger 的文档实用程序,用于创建 API 文档、编写单元和集成测试、学习应用调试,最后,了解在云平台上部署和监控 REST APIs 以供生产使用的不同方法。

对于单元测试,我们将使用 pytest,这是一个全功能的 Python 测试工具——pytest 易于编写测试,并且可扩展以支持复杂的用例。我们还将使用 Postman,它是一个完整的 REST API 平台——Postman 为 API 生命周期的每个阶段提供集成工具,使 API 开发更容易、更可靠。

API 部署和监控是 REST API 开发的关键部分;当谈到为生产用例扩展 API 时,开发范式发生了巨大的变化,为了本书,我们将使用 uWSGI 和 Nginx 在云 Ubuntu 服务器上部署我们的 REST APIs。我们还将在 Heroku 上部署 REST APIs,Heroku 是一个云平台,有助于 Flask 应用的部署和开箱即用。

最后但同样重要的是,我们将讨论调试常见的 Flask 错误和警告,调试 Nginx 请求,并检查 Flask 应用监控,以确保生产使用的停机时间最少。

RESTful 服务简介

表述性状态转移(REST)是 web 服务的一种软件架构风格,它为不同类型的系统之间的数据通信提供了标准。Web 服务是开放的标准 web 应用,它以交换数据为目的与其他应用进行交互,使其成为现代 web 和移动应用中客户端服务器架构的重要组成部分。简而言之,REST 是为了计算机系统之间的互操作性而在 Web 上交换数据的标准。符合 REST 架构风格的 Web 服务被称为 RESTful web 服务,它允许请求系统使用一组统一的、预定义的无状态操作来访问和操作数据。

自从 Roy Feilding 在 2000 年提出 RESTful 架构以来,RESTful 架构已经有了很大的发展,并且已经在数百万个系统中实现。REST 现在已经成为基于 web 的应用的最重要的技术之一,并且随着它在移动和基于物联网的应用中的集成,可能会增长得更快。每一种主要的开发语言都有构建 REST web 服务的框架。REST 原则是它受欢迎和被大量使用的原因。REST 是无状态的,这使得任何类型的系统都可以直接使用 REST,并且每个请求都可以由不同的系统提供服务。

REST 使我们能够区分客户机和服务器,让我们独立地实现客户机和服务器。REST 最重要的特性是它的无状态性,也就是说客户机和服务器都不需要知道对方的状态就可以进行通信。这样,客户端和服务器都可以理解接收到的任何消息,而无需查看之前的消息。既然我们在讨论 RESTful web 服务,那么让我们深入 web 服务并比较其他 web 服务标准。

简单地说,Web 服务是一种由一个电子设备向另一个电子设备提供的服务,能够通过万维网进行通信。在实践中,web 服务提供面向资源的、基于 web 的接口给数据库服务器,并由另一个 web 客户机使用。Web 服务为不同类型的系统相互通信提供了一个平台,使用一种解决方案,程序能够以它们理解的语言相互通信(图 1-2 )。

img/479840_1_En_1_Fig2_HTML.jpg

图 1-2

REST 架构图

SOAP(简单对象访问协议)是另一种 web 服务通信协议,近年来已经被 REST 取代。根据 Stormpath 的数据,REST 服务现在主导着这个行业,代表了超过 70%的公共 API。它们通过公开一致的接口来访问命名资源。然而,SOAP 将应用逻辑的组件公开为服务,而不是数据。SOAP 现在是最初由微软创建的遗留协议,与 REST 相比,它有许多其他限制。SOAP 只通过 XML 交换数据,REST 提供了通过各种数据格式交换数据的能力。RESTful 服务相对来说速度更快,资源消耗更少。然而,SOAP 仍然有自己的用例,在这些用例中,它是比 REST 更受欢迎的协议。

当健壮的安全性至关重要时,最好使用 SOAP,因为它提供了对 web 服务安全性(WS-Security)的支持,这是一种规范,定义了如何在 Web 服务中实现安全措施以保护它们免受外部攻击。SOAP 优于 REST 的另一个优点是它内置的重试逻辑,可以补偿失败的请求,这与 REST 不同,REST 中客户端必须通过重试来处理失败的请求。SOAP 与其他技术和协议(如 WS-Security、WS-addressing、WS-coordination 等)高度可扩展,这使它比其他 web 服务协议更具优势。

现在,当我们简要讨论了 web 服务——REST 和 SOAP——之后,让我们来讨论 REST 协议的特性。一般来说,REST 服务是使用以下特性定义和实现的:

  1. 统一界面

  2. 陈述

  3. 信息

  4. 资源之间的链接

  5. 贮藏

  6. 无国籍的

统一界面

RESTful 服务应该有一个统一的接口来访问资源,顾名思义,API 的系统接口在整个系统中应该是统一的。一个具有统一的获取和操作数据方式的逻辑 URI 系统使得 REST 易于使用。HTTP/1.1 提供了一组处理基于名词的资源的方法;为此,这些方法通常被称为动词。

在 REST 架构中,有一个安全和幂等方法的概念。安全方法是不像 GET 或 HEAD 方法那样修改资源的方法。幂等方法是一种无论执行多少次都会产生相同结果的方法。表 1-1 提供了 RESTful 服务中常用的 HTTP 动词列表。

表 1-1

RESTful 服务中常用的 HTTP 动词

|

动词

|

create, read, update, and delete

|

操作

|

安全的

|

幂等

|
| --- | --- | --- | --- | --- |
| 得到 | 阅读 | 获取单个或多个资源 | 是 | 是 |
| 邮政 | 创造 | 插入新资源 | 不 | 不 |
| 放 | 更新/创建 | 插入新资源或更新现有资源 | 不 | 是 |
| 删除 | 删除 | 删除单个或多个资源 | 不 | 是 |
| 选择 | 阅读 | 列出资源上允许的操作 | 是 | 是 |
| 头 | 阅读 | 只返回响应头,不返回正文 | 是 | 是 |
| 修补 | 更新/修改 | 仅更新对资源提供的更改 | 不 | 不 |

陈述

RESTful 服务关注资源并提供对资源的访问。在 OOP 中,资源很容易被认为是一个对象。设计 RESTful 服务时要做的第一件事是识别不同的资源并确定它们之间的关系。表示是定义资源当前状态的机器可读解释。

一旦确定了资源,下一步就是表示。REST 让我们能够使用任何格式来表示系统中的资源。不像 SOAP 限制我们使用 XML 来表示数据,我们可以使用 JSON 或者 XML。通常,JSON 是表示移动或 web 客户端调用的资源的首选方法,但是 XML 可以用来表示更复杂的资源。

下面是一个用两种格式表示资源的小例子。

{
      "ID": "1",
      "Name": "Building REST APIs wiith Flask",
      "Author": "Kunal Relan",
      "Publisher": "Apress"
}

In REST Systems, you can use either of the methods or both the methods depending on the requesting client to represent the data.

Listing 1-3JSON Representation of a Book resource

<?xml version="1.0" encoding="UTF-8"?>
<Book>
  <ID> 1 </ID>
  <Name> Building REST APIs with Flask </Name>
  <Author> Kunal Relan </Author>
  <Publisher > Apress </ Publisher >
</Book>

Listing 1-2XML Representation of a Book Resource

信息

在 REST 架构中,消息是一个重要的关键,它本质上建立了客户机-服务器风格的数据通信方式。客户端和服务器通过消息相互通信,其中客户端向服务器发送消息,这通常被称为请求,而服务器发送响应。除了客户端和服务器之间以请求和响应主体的形式交换的实际数据之外,客户端和服务器还以请求和响应头的形式交换一些元数据。HTTP 1.1 以如下方式定义了请求和响应头格式,以便在不同种类的系统之间实现统一的数据通信方式(图 1-3 )。

img/479840_1_En_1_Fig3_HTML.jpg

图 1-3

HTTP 示例请求

在图 1-4 中,GET 是请求方法,“/comments”是服务器中的路径,“postId=1”是请求参数,“HTTP/1.1”是客户端正在请求的协议版本,“jsonplaceholder.typicode.com”是服务器主机,内容类型是请求头的一部分。所有这些结合起来就是服务器能够理解的 HTTP 请求。

作为回报,HTTP 服务器发送对所请求资源的响应。

[
  {
    "postId": 1,
    "id": 1,
    "name": "id labore ex et quam laborum",
    "email": "Eliseo@gardner.biz",
    "body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium"
  },
  {
    "postId": 1,
    "id": 2,
    "name": "quo vero reiciendis velit similique earum",
    "email": "Jayne_Kuhic@sydney.com",
    "body": "est natus enim nihil est dolore omnis voluptatem numquam\net omnis occaecati quod ullam at\nvoluptatem error expedita pariatur\nnihil sint nostrum voluptatem reiciendis et"
  },
  {
    "postId": 1,
    "id": 3,
    "name": "odio adipisci rerum aut animi",
    "email": "Nikita@garfield.biz",
    "body": "quia molestiae reprehenderit quasi aspernatur\naut expedita occaecati aliquam eveniet laudantium\nomnis quibusdam

delectus saepe quia accusamus maiores nam est\ncum et ducimus et vero voluptates excepturi deleniti ratione"
  },
  {
    "postId": 1,
    "id": 4,
    "name": "alias odio sit",
    "email": "Lew@alysha.tv",
    "body": "non et atque\noccaecati deserunt quas accusantium unde odit nobis qui voluptatem\nquia voluptas consequuntur itaque dolor\net qui rerum deleniti ut occaecati"
  },
  {
    "postId": 1,
    "id": 5,
    "name": "vero eaque aliquid doloribus et culpa",
    "email": "Hayden@althea.biz",
    "body": "harum non quasi et ratione\ntempore iure ex voluptates in ratione\nharum architecto fugit inventore cupiditate\nvoluptates magni quo et"
  }]

img/479840_1_En_1_Fig4_HTML.jpg

图 1-4

HTTP 示例响应

在上图中,“HTTP/2”是响应 HTTP 版本,“200”是响应代码。“cf-ray”下面的部分是响应头,“cf-ray”下面的 post 注释数组是请求的响应体。

资源之间的链接

资源是 REST 架构世界中的基本概念。资源是一个具有类型、相关数据、与其他资源的关系以及一组可以在其上执行的方法的对象。REST API 中的资源可以包含到应该驱动流程流的其他资源的链接。例如在 HTML 网页中,主页中的链接驱动用户流,REST API 中的资源应该能够在用户不知道流程图的情况下驱动用户流。

{
      "ID": "1",
      "Name": "Building REST APIs wiith Flask",
      "Author": "Kunal Relan",
      "Publisher": "Apress",
       "URI" : "https://apress.com/us/book/123456789"
}

Listing 1-4A Book with Link to Buy

贮藏

缓存是一种技术,它存储给定资源的副本,并在请求时返回,从而节省额外的数据库调用和处理时间。它可以在不同的层次上完成,如客户机、服务器或中间件代理服务器。缓存是提高 API 性能和扩展应用的重要工具;但是,如果管理不当,会导致向客户端提供旧的结果。REST APIs 中的缓存是使用 HTTP 头来控制的。缓存头是 HTTP 头规范的重要组成部分,也是高效扩展 web 服务的重要组成部分。在 REST 规范中,当在资源 URL 上使用安全方法时,反向代理通常会缓存结果,以便在下次请求相同的资源时使用缓存的数据。

无国籍的

从客户端到服务器的每个请求都必须包含理解请求所需的所有信息,并且不能利用服务器上存储的任何上下文。因此,会话状态完全保存在客户端

—罗伊·菲尔丁

这里的无状态意味着每个 HTTP 响应本身是一个完整的实体,足以提供要执行的信息,而不需要另一个 HTTP 请求。无状态的意义在于破坏了与服务器保持一致的目的,即允许基础设施具有预期的灵活性。为了方便起见,REST 服务器在 HTTP 响应中提供了客户机可能需要的足够信息。无状态是能够扩展基础设施的重要部分,使我们能够部署多个服务器来服务数百万并发用户,因为不存在服务器会话状态依赖性。它还支持 REST 基础设施的缓存特性,因为它让缓存服务器决定是否缓存请求,只需查看特定的请求,而不考虑之前的任何请求。

规划 REST API

下面是我们在计划创建 REST APIs 时需要检查的事项列表:

  1. 理解用例。知道你为什么要构建 API 以及 API 将提供什么服务是非常重要的。

  2. 列出 API 特性来理解你的 API 将要做的所有动作。这还包括列出行动,并将它们组合在一起,以处理冗余的端点。

  3. 确定将使用 API 的不同平台,并相应地提供支持。

  4. 对支持增长和扩展基础架构进行长期规划。

  5. 规划 API 版本控制策略,确保对不同版本的 API 提供持续支持。

  6. 规划 API 访问策略,即身份验证、ACL 和限制。

  7. 计划 API 文档和测试。

  8. 理解如何在你的 API 中使用超媒体。

因此,这是在规划你的 API 时需要确保的八件重要的事情,并且对于开发一个稳定的、以生产为中心的 API 系统是非常关键的。

API 设计

现在让我们来看看 API 设计。在这里,我们将讨论设计 REST APIs 的标准,记住我们刚刚谈到的一系列事情。

长期实施

长期实施帮助你在实际实施前分析设计上的缺陷。这有助于开发人员选择合适的平台和工具,以确保相同的系统以后可以扩展到更多的用户。

规格驱动开发

规范驱动的开发使用定义而不仅仅是代码来实施 API 设计,这确保了在 API 设计完好无损的情况下对代码库进行更改。使用像 API Designer 这样的工具在开发之前理解 API 设计是一个很好的实践,这也可以让你预见缺陷。像 swagger 或 RAML 这样的工具可以让您保持 API 设计的标准化,并在需要时将 API 移植到不同的平台上。

样机研究

一旦 API 规范到位,原型设计就可以让开发人员创建模拟 API,帮助他们理解 API 的每个潜在方面,从而帮助您在实际开发之前将 API 可视化。

认证和授权

身份验证涉及到知道这个人是谁的验证过程,但它还不涉及授予对所有资源的访问权,这就是授权的由来,它涉及到授权经过身份验证的人使用访问控制列表(ACL)检查允许访问的资源。

我们有不同的用户认证和授权方式,如基本认证、HMAC 和 OAuth。然而,OAuth 2.0 是实现这一点的首选方法,并且是企业和小公司在其 REST APIs 中用于身份验证和授权的标准协议。

所以,这些是 REST 基础设施的关键特性,我们将在后面的章节中更多地讨论 REST 是如何工作的以及如何实现更好的通信。

现在,我们将开始设置我们的开发环境,并了解使用 Python 开发应用的一些关键因素。

设置开发环境

在这一部分中,我们将讨论为 Flask 应用设置 Python 开发环境。我们将使用虚拟环境作为我们的依赖项的独立隔离环境。在设置开发环境的过程中,我们将使用 PIP 来安装和管理我们的依赖项以及一些其他有用的实用程序。为了这本书,我们将在 macOS Mojave 和 Python 2.7 上做所有的事情,但是你可以根据自己的方便随意使用任何操作系统。因此,如果您的操作系统中没有安装 Python 的正确版本,您可以使用此链接继续在您选择的操作系统上安装 Python:www.python.org/downloads/(图 1-5 )。

img/479840_1_En_1_Fig5_HTML.jpg

图 1-5

Python 下载

使用 PIP

PIP 是 PyPi 推荐的项目依赖管理工具。如果您使用从 www.python.org 下载的 Python,PIP 会预装 Python。

但是,如果您的系统中没有安装 PIP,请按照此处的指南安装 PIP。

要安装 PIP,请在终端中使用以下命令(或 Windows 中的命令行)下载 get-pip.py。

$ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py

获得 get-pip.py 文件后,安装并运行下一个命令:

$ python get-pip.py

前面的命令将安装 PIP、setuptools(安装源发行版所需的)和 wheel。

如果您已经有 pip,您可以使用以下命令升级到 PIP 的最新版本:

$ pip install -U pip

要测试您的安装,您应该在您的终端(或 Windows 中的命令行)中运行以下命令(图 1-6 ):

img/479840_1_En_1_Fig6_HTML.jpg

图 1-6

检查 Python 和 PIP 安装

$ python -V
$ pip -V

选择 IDE

在我们开始写代码之前,我们需要一些东西来写。在本书中,我们将使用 Visual Studio 代码,这是一个在所有主要操作系统上都可用的开源免费 IDE。Visual Studio 代码可以从 www.code.visualstudio.com 下载,它为开发 Python 应用提供了很好的支持,提供了大量方便的插件来方便开发。您可以选择使用自己喜欢的文本编辑器或 IDE 来阅读这本书(图 1-7 )。

img/479840_1_En_1_Fig7_HTML.jpg

图 1-7

Visual Studio 代码

一旦我们有了 IDE 设置,我们就可以开始安装和设置虚拟环境了。

了解 Python 虚拟环境

Python 就像其他现代编程语言一样,提供了大量的第三方库和 SDK。不同的应用可能需要各种特定版本的第三方模块,一个 Python 安装不可能满足每个应用的这种需求。因此,在 Python 的世界中,这个问题的解决方案是虚拟环境,它创建一个独立的自包含目录树,包含所需版本的 Python 安装以及所需的包。

从本质上讲,虚拟环境的主要目的是创建一个隔离的环境来包含 Python 的安装和应用所需的包。您可以创建的虚拟环境的数量没有限制,并且创建它们非常容易。

使用虚拟环境

在 Python 2.7 中,我们需要一个名为 virtualenv 的模块,它是使用 PIP 安装的,以便开始使用 Python 虚拟环境。

注意

在 Python 3 中,venv 模块是作为标准库的一部分预装的。

要安装 virtualenv,请在终端中键入以下命令(如果是 Windows,请键入命令行)。

$ pip install virtualenv

一旦我们在系统中安装了 virtualenv 模块,接下来我们将创建一个新目录,并在其中创建一个虚拟环境。

现在,键入以下命令创建一个新目录,并在终端中打开它。

$ mkdir pyenv && cd pyenv

前面的命令将创建一个目录,并在您的终端中打开它,然后我们将使用 virtualenv 模块在目录中创建一个新的虚拟环境。

$ virtualenv venv

前面的命令将使用 virtualenv 模块并创建一个名为 venv 的虚拟环境。你可以给你的虚拟环境起任何名字,但是在这本书里,为了统一起见,我们只使用 venv。

一旦这个命令停止执行,您将看到一个名为 venv 的目录。这个目录现在将保存您的虚拟环境。

venv 文件夹的目录结构应类似于图 1-8 中的目录结构。

img/479840_1_En_1_Fig8_HTML.jpg

图 1-8

虚拟环境目录结构

下面是结构中每个文件夹包含的内容:

  1. bin:与虚拟环境交互的文件。

  2. include: C 头文件来编译 Python 包。

  3. lib:这个文件夹包含 Python 版本和所有其他第三方模块的副本。

接下来,有不同 Python 工具的副本或符号链接,以确保所有 Python 代码和命令都在当前环境中执行。这里重要的部分是 bin 文件夹中的激活脚本,它将 shell 设置为使用虚拟环境的 Python 和 site 包。为此,您需要通过在终端中键入以下命令来激活虚拟环境。

$ source venv/bin/activate

一旦执行了这个命令,您的 shell 提示符将会以虚拟环境的名称为前缀,如图 1-9 所示。

img/479840_1_En_1_Fig9_HTML.jpg

图 1-9

激活虚拟环境

现在,让我们使用以下命令在我们的虚拟环境中安装 Flask:

$ pip install flask

前面的命令应该在我们的虚拟环境中安装 Flask。我们将使用我们在示例 Flask 应用中使用的相同代码。

$ nano app.py

并在 nano 文本编辑器中键入以下代码:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, From Flask!'

Now,  try running your app.py using python app.py command.

$ FLASK_APP=app.py flask run

使用前面的命令,您应该能够运行简单的 Flask 应用,并且应该在您的终端中看到类似的输出(图 1-10 )。

img/479840_1_En_1_Fig10_HTML.jpg

图 1-10

在虚拟环境中运行 Flask 应用

现在,要停用虚拟环境,您需要执行以下命令:

$ deactivate

该命令执行后,shell 中的(venv)前缀将消失,如果您尝试再次运行该应用,它将抛出一个错误(图 1-11 )。

img/479840_1_En_1_Fig11_HTML.jpg

图 1-11

不使用虚拟环境运行 Flask 应用

现在,您已经了解了虚拟环境的概念,我们可以更深入地了解虚拟环境内部的情况。

理解虚拟环境的工作方式可以真正帮助您调试应用和理解执行环境。首先,让我们检查一下激活和停用虚拟环境的 Python 可执行文件,以便理解基本的区别。

让我们在激活虚拟环境的情况下执行以下命令(图 1-12 ):

img/479840_1_En_1_Fig12_HTML.jpg

图 1-12

使用虚拟环境检查 Python 可执行文件

$ which python

如下图所示,外壳正在使用虚拟环境的 Python 可执行文件,如果您停用该环境并重新运行 Python 命令,您会注意到外壳现在正在使用系统的 Python(图 1-13 )。

img/479840_1_En_1_Fig13_HTML.jpg

图 1-13

在没有虚拟环境的情况下检查 Python 可执行文件

因此,一旦激活了虚拟环境,就会修改$path 环境变量,使其指向我们的虚拟环境,从而使用我们虚拟环境中的 Python,而不是系统环境。然而,这里需要注意的一件重要的事情是,它基本上是系统的 Python 可执行文件的一个副本或一个符号链接。

安装 Flask

我们已经在前面的模块中安装了 Flask,但是让我们重新开始并设置 Flask 微框架。

安装 Flask

激活虚拟环境后,执行以下命令安装最新版本的 Flask。

$pip install flask

前面的命令将在您的虚拟环境中安装 Flask。

但是,如果您希望在发布之前使用最新的 Flask,请通过执行以下命令,使用其存储库的主分支安装/更新 Flask 模块:

$pip install -U https://github.com/pallets/flask/archive/master.tar.gz

当您安装 Flask 时,以下发行版会随主框架一起安装:

  1. Werkzeug(http://werkzeug.pocoo.org/):Werkzeug 实现了 WSGI,应用和服务器之间的标准 Python 接口。

  2. Jinja ( http://jinja.pocoo.org/ ): Jinja 是 Flask 中的模板引擎,为应用呈现页面。

  3. MarkupSafe (

  4. its dangerous(https://pythonhosted.org/itsdangerous/):its dangerous 负责安全地对数据进行签名,以确保数据的完整性,并用于保护 Flask 会话 cookies。

  5. Click ( http://click.pocoo.org/ ): Click 是一个写 CLI 应用的框架。它提供了“Flask”CLI 命令。

结论

一旦在虚拟环境中安装了 Flask,就可以进入开发阶段的下一步了。在此之前,我们将讨论 MySQL 和 Flask-SQLAlchemy,它是我们将在 Flask 应用中使用的 ORM。数据库是 REST 应用的重要组成部分,在下一章,我们将讨论 MySQL 数据库和 Flask-SQLAlchemy ORM,并学习如何将我们的 Flask 应用与 Flask-SQLAlchemy 连接起来。

二、Flask 中的数据库建模

本章涵盖了 REST 应用开发的一个最重要的方面,即与数据库系统的连接和交互。在本章中,我们将讨论 NoSQL 和 SQL 数据库,以及它们之间的连接和交互。

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

  1. NoSQL 与 SQL 数据库

  2. 连接 Flask-SQLAlchemy

  3. 使用 Flask-SQLAlchemy 与 MySQL 数据库交互

  4. 连接 Flask-MongoEngine

  5. 使用 Flask-MongoEngine 与 MongoDB 交互

介绍

Flask 作为一个微框架,为应用提供了数据源的灵活性,并为与不同种类的数据源进行交互提供了库支持。Flask 中有一些库可以连接到基于 SQL 和基于 NoSQL 的数据库。它还提供了使用原始数据库库或使用 ORM(对象关系映射器)/ODM(对象文档映射器)与数据库进行交互的灵活性。在这一章中,我们将简要讨论基于 NoSQL 和 SQL 的数据库,并通过 Flask-SQLAlchemy 学习如何在 Flask 应用中使用 ORM 层,之后我们将通过 Flask-MongoEngine 使用 ODM 层。

大多数应用在某些时候确实需要数据库,MySQL 和 MongoDB 只是众多工具中的两个。为您的应用选择正确的方法完全取决于您要存储的数据。如果表中的数据集是相互关联的,那么 SQL 数据库是一个不错的选择,或者 NoSQL 数据库也可以达到这个目的。

现在,让我们简要地看一下 SQL 和 NoSQL 数据库。

SQL 数据库

SQL 数据库使用结构化查询语言(SQL)进行数据操作和定义。SQL 是一个通用的、被广泛使用和接受的选项,这使它成为数据存储的最佳选择。当使用的数据需要是关系型的并且模式是预定义的时,SQL 系统非常适合。然而,预定义的模式也有缺点,因为它要求整个数据集遵循相同的结构,这在某些情况下可能很困难。SQL 数据库以由行和列组成的表格的形式存储数据,并且是垂直可伸缩的。

NoSQL 数据库

NoSQL 数据库有一个非结构化数据的动态模式,并以不同的方式存储数据,包括基于列的(Apache Cassandra)、基于文档的(MongoDB)和基于图形的(Neo4J)或作为键值存储(Redis)。这提供了在没有预定义结构的情况下存储数据的灵活性,以及随时向数据结构添加字段的多功能性。无模式是 NoSQL 数据库的主要特点,这也使它们更适合分布式系统。与 SQL 数据库不同,NoSQL 数据库是水平可伸缩的。

既然我们已经简要解释了 SQL 和 NoSQL 数据库,我们将跳转到 MySQL 和 MongoDB 之间的功能差异,因为这是我们将在本章中研究的两个数据库引擎。

主要区别:MySQL 与 MongoDB

如前所述,MySQL 是一个基于 SQL 的数据库,它将数据存储在具有列和行的表中,并且只处理结构化数据。另一方面,MongoDB 可以处理非结构化数据,存储类似 JSON 的文档而不是表格,并使用 MongoDB 查询语言与数据库通信。MySQL 是一个非常成熟的数据库,具有巨大的社区和极大的稳定性,而 MongoDB 是一项相当新的技术,社区不断增长,由 MongoDB Inc .开发。MySQL 是垂直可伸缩的,其中单个服务器上的负载可以通过升级 RAM、SSD 或 CPU 来增加,而在 MongoDB 的情况下,它需要共享和添加更多的服务器,以便增加服务器负载。MongoDB 是高写负载和大数据集的首选,MySQL 非常适合高度依赖多行事务的应用,如会计系统。对于具有动态结构和高数据负载的应用,如实时分析应用或内容管理系统,MongoDB 是一个很好的选择。

Flask 提供了与 MySQL 和 MongoDB 交互的支持。有各种本地驱动程序以及 ORM/ODM 用于与数据库通信。MySQL 是一个 Flask 扩展,允许本地连接到 MySQL;PyMongo 是在 Flask 中使用 MongoDB 的本地扩展,也是 MongoDB 推荐的。Flask-MongoEngine 是一个 Flask 扩展,用于 Flask 和 MongoDB 的 ODM。Flask-SQLAlchemy 是一个 ORM 层,用于 Flask 应用连接 MySQL。

接下来,我们将讨论 Flask-SQLAlchemy 和 Flask- MongoEngine,并使用它们创建 Flask CRUD 应用。

使用 SQLAlchemy 创建 Flask 应用

Flask-SQLAlchemy 是 Flask 的扩展,为应用增加了对 SQLAlchemy 的支持。SQLAlchemy 是一个 Python 工具包和对象关系映射器,使用 Python 提供对 SQL 数据库的访问。SQLAlchemy 提供了企业级的持久性模式和高效高性能的数据库访问。如果安装了适当的 DBAPI 驱动程序,Flask-SQLAlchemy 支持以下基于 SQL 的数据库引擎:

  • 一种数据库系统

  • 关系型数据库

  • 神谕

  • 数据库

  • 搜寻配置不当的

  • 火鸟赛贝斯

我们将在应用中使用 MySQL 作为数据库引擎,所以让我们开始安装 SQLAlchemy 并设置我们的应用。

让我们创建一个名为 flask-MySQL 的新目录,创建一个虚拟环境,然后安装 flask-sqlalchemy。

$ mkdir flask-mysql && cd flask-mysql

现在,使用以下命令在目录中创建一个虚拟环境:

$ virtualenv venv

如前所述,我们可以使用以下命令激活虚拟环境:

$ source venv/bin/activate

一旦虚拟环境被激活,让我们安装 flask-sqlalchemy。

Flask 和 Flask-SQLAlchemy 可以使用 PIP 和以下命令进行安装。

(venv)$ pip install flask flask-sqlalchemy

除了 SQLite,所有其他数据库引擎都需要单独的库与 Flask-SQLAlchemy 一起安装才能运行。SQLAlchemy 使用 MySQL-Python 作为与 MySQL 连接的默认 DBAPI。

现在,让我们安装 PyMySQL 来启用 MySQL 与 Flask-SQLAlchemy 的连接。

(venv) $ pip install pymysql

现在,我们应该拥有了创建示例 flask-MySQL 应用所需的一切。

让我们从创建 app.py 开始,它将包含我们的应用的代码。创建文件后,我们将启动 Flask 应用。

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://<mysql_username>:<mysql_password>@<mysql_host>:<mysql_port>/<mysql_db>'
db = SQLAlchemy(app)

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

这里,我们导入 Flask 框架和 Flask-SQLAlchemy,然后初始化 Flask 的一个实例。之后,我们配置 SQLAlchemy 数据库 URI 以使用我们的 MySQL 数据库 URI,然后我们创建一个名为 DB 的 SQLAlchemy 对象,它将处理我们的 ORM 相关活动。

现在,如果您正在使用 MySQL,请确保您提供了正在运行的 MySQL 服务器的连接字符串,并且所提供的数据库名称确实存在。

注意

使用环境变量在应用中提供数据库连接字符串。

确保您有一个正在运行的 MySQL 服务器来跟踪这个应用。但是,您也可以通过在 SQLAlchemy 数据库 URI 中提供 SQLite 配置详细信息来使用 SQLite,如下所示:

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/<db_name>.db'

为了运行应用,您需要在终端中执行以下代码:

(venv) $ python app.py

如果没有错误,您应该在终端中看到类似的输出:

(venv) $ python app.py
* Serving Flask app "app" (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 with stat
 * Debugger is active!
 * Debugger PIN: 779-301-240

创建作者数据库

我们现在将创建一个作者数据库应用,它将提供 RESTful CRUD APIs。所有作者都将存储在名为“authors”的表中。

在声明的 db 对象之后,添加以下代码行,以将一个类声明为 Authors,该类将保存 author 表的模式:

class Author (db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(20))
    specialisation = db.Column(db.String(50))

    def __init__(self, name, specialisation):
        self.name = name
        self.specialisation = specialisation
    def __repr__(self):
      return '<Product %d>' % self.id
db.create_all()

使用这段代码,我们创建了一个名为“Authors”的模型,它有三个字段——ID、name 和 specialisation。Name 和 specialisation 是字符串,但是 ID 是一个自动生成并自动递增的整数,它将作为主键。注意最后一行“db.create_all()”,它指示应用创建应用中指定的所有表和数据库。

为了使用 SQLAlchemy 返回的数据为来自 API 的 JSON 响应提供服务,我们需要另一个名为 marshmallow 的库,它是 SQLAlchemy 的附加组件,用于将 SQLAlchemy 返回的数据对象序列化到 JSON。

(venv)$ pip install flask-marshmallow

以下命令将在我们的应用中安装 marshmallow 的 Flask 版本,我们将使用 marshmallow 从 Authors 模型中定义输出模式。

在应用文件的顶部,其他导入的下面添加下面几行来导入 marshmallow。

from marshmallow_sqlalchemy import ModelSchema
from marshmallow import fields

在 db.create_all()之后,使用以下代码定义输出模式:

class AuthorSchema(ModelSchema):
    class Meta(ModelSchema.Meta):
        model = Authors
        sqla_session = db.session

    id = fields.Number(dump_only=True)
    name = fields.String(required=True)
    specialisation = fields.String(required=True)

前面的代码将变量属性映射到字段对象,在 Meta 中,我们定义了与我们的模式相关的模型。所以这应该有助于我们从 SQLAlchemy 返回 JSON。

在建立了模型和返回模式之后,我们可以开始创建端点了。让我们创建第一个 GET /authors 端点来返回所有注册的作者。这个端点将查询 Authors 模型中的所有对象,并以 JSON 的形式返回给用户。但是在我们编写端点之前,将第一个导入行编辑为以下内容,以便从 Flask 导入 jsonify、make_response 和 request。

from flask import Flask, request, jsonify, make_response

在 AuthorSchema 之后,用以下代码编写第一个端点/作者:

@app.route('/authors', methods = ['GET'])
def index():
    get_authors = Authors.query.all()
    author_schema = AuthorSchema(many=True)
    authors, error = author_schema.dump(get_authors)
    return make_response(jsonify({"authors": authors}))

在这个方法中,我们获取数据库中的所有作者,将其转储到 AuthorSchema 中,并在 JSON 中返回结果。

如果您现在启动应用并点击端点,它将返回一个空数组,因为我们还没有在 DB 中添加任何东西,但是让我们继续尝试端点。

使用 Python app.py 运行应用,然后使用首选的 REST 客户端查询端点。我将使用 Postman 来请求端点。

所以只需打开你的 Postman,让http://localhost:5000/authors查询端点(图 2-1 )。

img/479840_1_En_2_Fig1_HTML.jpg

图 2-1

获得/作者回应

您应该在您的 Postman 客户端中看到类似的结果。现在让我们创建 POST 端点,将作者添加到我们的数据库中。

我们可以通过在我们的方法中直接创建一个 Authors 类,或者通过创建一个 classMethod 在 Authors 类中创建一个新对象,然后在我们的端点中调用该方法,来将一个对象添加到表中。让我们在 Authors 类中添加 class 方法来创建一个新对象。

在字段定义之后,在 Authors 类中添加以下代码:

    def create(self):
      db.session.add(self)
      db.session.commit()
      return self

前面的方法使用数据创建一个新的对象,然后返回创建的对象。现在,您的 Authors 类应该如下所示:

class Authors(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(20))
    specialisation = db.Column(db.String(50))

    def create(self):
      db.session.add(self)
      db.session.commit()
      return self

    def __init__(self, name, specialisation):
        self.name = name
        self.specialisation = specialisation
    def __repr__(self):
        return '<Author %d>' % self.id

现在,我们将创建 POST authors 端点,并在 GET 端点后编写以下代码:

@app.route('/authors', methods = ['POST'])
def create_author():
    data = request.get_json()
    author_schema = AuthorsSchema()
    author, error = author_schema.load(data)
    result = author_schema.dump(author.create()).data
    return make_response(jsonify({"author": authors}),201)

前面的方法将获取 JSON 请求数据,将数据加载到 marshmallow 模式中,然后调用我们在 Authors 类中创建的 create 方法,该方法将返回带有 201 状态代码的已创建对象。

因此,让我们用示例数据请求 POST 端点并检查响应。让我们用 JSON 请求体打开 Postman 和 POST /authors。我们需要在主体中添加名称和专门化字段来创建对象。我们的示例请求体应该如下所示:

{
      "name" : "Kunal Relan",
      "specialisation" : "Python"
}

一旦我们请求了端点,我们将获得 Author 对象作为对我们新创建的 Author 的响应。注意,在这种情况下,返回状态代码是 201,这是新对象的状态代码(图 2-2 )。

img/479840_1_En_2_Fig2_HTML.jpg

图 2-2

帖子/作者端点

所以现在,如果我们请求 GET /authors 端点,我们将在响应中获得新创建的作者。

重新访问 Postman 中的 GET /authors 选项卡并再次点击请求;这一次,您应该得到一个由我们新创建的作者组成的作者数组(图 2-3 )。

img/479840_1_En_2_Fig3_HTML.jpg

图 2-3

用新对象获取所有作者

到目前为止,我们已经创建了注册新作者和获取作者列表的端点。接下来,我们将创建一个端点来使用作者 ID 返回作者,然后更新端点来使用作者 ID 更新作者详细信息,最后一个端点使用作者 ID 删除作者。

对于通过 ID 获取作者,我们将有一个类似/authors/ 的路径,它将从请求参数中获取作者 ID 并找到匹配的作者。

将以下代码添加到 GET author by ID 端点的 GET all authors 路径下。

@app.route('/authors/<id>', methods = ['GET'])
def get_author_by_id(id):
    get_author = Authors.query.get(id)
    author_schema = AuthorsSchema()
    author, error = author_schema.dump(get_author)
    return make_response(jsonify({"author": author}))

接下来我们需要测试这个端点,我们将请求 ID 为 1 的 author,就像我们在前面的 GET all authors API response 中看到的那样,所以让我们再次打开 Postman 并请求应用服务器上的/authors/1 来检查响应。

img/479840_1_En_2_Fig4_HTML.jpg

图 2-4

通过 ID 端点获取作者

正如您在前面的屏幕截图中看到的,我们正在返回一个具有密钥 author 的对象,该对象包含 ID 为 1 的 author 对象。现在,您可以使用 POST 端点添加更多作者,并使用返回的 ID 获取他们。

接下来,我们需要创建一个端点来更新作者姓名或专业,为了更新任何对象,我们将使用在“RESTful 服务简介”一节中讨论过的 PUT HTTP 动词。这个端点将类似于 GET authors by ID 端点,但是将使用 PUT 动词而不是 GET 动词。

下面是 PUT 端点更新 author 对象的代码

@app.route('/authors/<id>', methods = ['PUT'])
def update_author_by_id(id):
    data = request.get_json()
    get_author = Authors.query.get(id)
    if data.get('specialisation'):
        get_author.specialisation = data['specialisation']
    if data.get('name'):
        get_author.name = data['name']
    db.session.add(get_author)
    db.session.commit()
    author_schema = AuthorsSchema(only=['id', 'name', 'specialisation'])
    author, error = author_schema.dump(get_author)
    return make_response(jsonify({"author": author}))

因此,让我们测试我们的 PUT 端点,并更改 author ID 1 的专门化。

我们将在下面的 JSON 主体中更新作者专门化。

{
      "specialisation" : "Python Applications"
}

img/479840_1_En_2_Fig5_HTML.jpg

图 2-5

按 ID 端点更新作者

如图 2-5 所示,我们用 ID 1 更新了作者,现在专门化已经更新为“Python 应用”。

现在,从数据库中删除作者的最后一个端点。添加以下代码以添加一个删除端点,该端点类似于 get author by ID endpoint,但将使用删除谓词并返回没有内容的 204 状态代码。

@app.route('/authors/<id>', methods = ['DELETE'])
def delete_author_by_id(id):
    get_author = Authors.query.get(id)
    db.session.delete(get_author)
    db.session.commit()
    return make_response("",204)

现在我们将请求删除端点删除 ID 为 1 的作者(图 2-6 )。

img/479840_1_En_2_Fig6_HTML.jpg

图 2-6

按 ID 删除作者

现在,如果您请求获取所有作者端点,它将返回一个空数组。

现在,您的 app.py 应该具有以下代码:

from flask import Flask, request, jsonify, make_response
from flask_sqlalchemy import SQLAlchemy
from marshmallow_sqlalchemy import ModelSchema
from marshmallow import fields

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = 'mysql+pymysql://<mysql_username>:<mysql_password>@<mysql_host>:<mysql_port>/<mysql_db>'

db = SQLAlchemy(app)

class Authors(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(20))
    specialisation = db.Column(db.String(50))

    def create(self):
      db.session.add(self)
      db.session.commit()
      return self

    def __init__(self, name, specialisation):
        self.name = name
        self.specialisation = specialisation

    def __repr__(self):
        return '<Author %d>' % self.id

db.create_all()

class AuthorsSchema(ModelSchema):
    class Meta(ModelSchema.Meta):
        model = Authors
        sqla_session = db.session

    id = fields.Number(dump_only=True)
    name = fields.String(required=True)
    specialisation = fields.String(required=True)

@app.route('/authors', methods = ['GET'])
def index():
    get_authors = Authors.query.all()
    author_schema = AuthorsSchema(many=True)
    authors, error = author_schema.dump(get_authors)
    return make_response(jsonify({"authors": authors}))

@app.route('/authors/<id>', methods = ['GET'])
def get_author_by_id(id):
    get_author = Authors.query.get(id)
    author_schema = AuthorsSchema()
    author, error = author_schema.dump(get_author)
    return make_response(jsonify({"author": author}))

@app.route('/authors/<id>', methods = ['PUT'])
def update_author_by_id(id):
    data = request.get_json()
    get_author = Authors.query.get(id)
    if data.get('specialisation'):
        get_author.specialisation = data['specialisation']
    if data.get('name'):
        get_author.name = data['name']
    db.session.add(get_author)
    db.session.commit()

    author_schema = AuthorsSchema(only=['id', 'name', 'specialisation'])
    author, error = author_schema.dump(get_author)
    return make_response(jsonify({"author": author}))

@app.route('/authors/<id>', methods = ['DELETE'])
def delete_author_by_id(id):
    get_author = Authors.query.get(id)
    db.session.delete(get_author)
    db.session.commit()
    return make_response("",204)

@app.route('/authors', methods = ['POST'])
def create_author():
    data = request.get_json()
    author_schema = AuthorsSchema()
    author, error = author_schema.load(data)
    result = author_schema.dump(author.create()).data
    return make_response(jsonify({"author": result}),200)

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

因此,我们现在已经创建并测试了我们的样本 Flask-MySQL CRUD 应用。我们将在后面的章节中使用 Flask-SQLAlchemy 检查复杂的对象关系,接下来我们将使用 MongoEngine 创建一个类似的 Flask CRUD 应用。

示例 Flask MongoEngine 应用

正如我们所讨论的,MongoDB 是一个强大的基于文档的 NoSQL 数据库。它使用类似 JSON 的文档模式结构,具有高度的可伸缩性。在这个例子中,我们将再次创建一个 Authors 数据库 CRUD 应用,但是这次我们将使用 MongoEngine 而不是 SQLAlchemy。MongoEngine 增加了对 Flask 的 MongoDB 支持,与 SQLAlchemy 非常相似,但是它缺少一些特性,因为 MongoDB 仍然没有广泛用于 Flask。

让我们开始为 flask-mongodb 应用设置项目。就像上次一样,创建一个新的目录 flask-mongodb,并在其中初始化一个新的虚拟环境。

$ mkdir flask-mongodb && cd flask-mongodb

创建目录后,让我们生成虚拟环境并激活它。

$ virtualenv venv
$ source venv/bin/activate

现在让我们使用 PIP 安装我们的项目依赖项。

(venv) $ pip install flask

我们需要 Flask-MongoEngine 和 Flask-marshmallow,所以让我们也安装它们。

(venv) $ pip install flask-mongoengine
(venv) $ pip install flask-marshmallow

安装完依赖项后,我们可以创建 app.py 文件并开始编写代码。

因此,下面的代码是应用的框架,其中导入 flask,创建一个应用实例,然后导入 MongoEngine 创建一个 db 实例。

from flask import Flask, request, jsonify, make_response
from flask_mongoengine import MongoEngine
from marshmallow import Schema, fields, post_load
from bson import ObjectId

app = Flask(__name__)
app.config['MONGODB_DB'] = 'authors'
db = MongoEngine(app)

Schema.TYPE_MAPPING[ObjectId] = fields.String

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

Here TYPE_MAAPPING helps marshmallow understand the ObjectId type while serializing and de-serializing the data.

注意

这里我们不需要 db.create_all(),因为 MongoDB 会在您第一次将值保存到集合中时动态创建它。

如果您现在运行应用,您的服务器应该会启动,但是它没有什么要处理的,只是创建 db 实例并建立连接。接下来,让我们使用 MongoEngine 创建一个作者模型。

在这种情况下,创建作者模型的代码相当简单,如下所示:

class Authors(db.Document):
    name = db.StringField()
    specialisation = db.StringField()

现在让我们创建 marshmallow 模式,我们需要用它将 db 对象转储到序列化的 JSON 中。

class AuthorsSchema(Schema):
    name = fields.String(required=True)
    specialisation = fields.String(required=True)

前面的代码让我们创建一个模式,我们将使用该模式将 db 对象映射到 marshmallow。请注意,这里我们没有使用 marshmallow-sqlalchemy,它对 sqlalchemy 有一个额外的支持层,因此代码看起来略有变化。

现在,我们可以编写 GET 端点来从数据库中获取所有作者。

@app.route('/authors', methods = ['GET'])
def index():
    get_authors = Authors.objects.all()
    author_schema =AuthorsSchema(many=True,only=['id','name','specialisation'])
    authors, error = author_schema.dump(get_authors)
    return make_response(jsonify({"authors": authors}))

注意

MongoEngine 在“Id”字段中返回唯一的 ObjectId,它是自动生成的,因此没有在模式中指定。

现在,让我们使用下面的命令再次启动应用。

(venv) $ python app.py

如果没有错误,您应该会看到下面的输出,并且您的应用应该已经启动并正在运行。

(venv) $ python app.py
* Serving Flask app "app" (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 with stat
 * Debugger is active!
 * Debugger PIN: 779-301-240

img/479840_1_En_2_Fig7_HTML.jpg

图 2-7

请求获取/作者

既然我们的 GET 端点正在工作(图 2-7 ),让我们创建一个 POST /authors 端点来在数据库中注册作者。

@app.route('/authors', methods = ['POST'])
def create_author():
    data = request.get_json()
    author = Authors(name=data['name'],specialisation=data['specialisation'])
    author.save()
    author_schema = AuthorsSchema(only=['name', 'specialisation'])
    authors, error = author_schema.dump(author)
    return make_response(jsonify({"author": authors}),201)

前面的代码将请求 JSON 数据放在数据变量中,创建一个类 Authors 对象,并对其调用 save()方法。接下来,它使用 AuthorsSchema 创建一个模式,并转储新对象以将其返回给用户,确认用户是用 201 状态代码创建的。

现在重新运行应用,并使用示例作者详细信息请求 POST 端点进行注册。

我们将使用相同的 JSON 数据发送到这个应用,就像我们在另一个应用中所做的那样。

{
      "name" : "Kunal Relan",
      "specialisation" : "Python"
}

img/479840_1_En_2_Fig8_HTML.jpg

图 2-8

请求帖子/作者

在请求时,您应该得到类似于图 2-8 中所示的输出,现在只是为了确认我们的 get 端点工作正常,我们将再次请求它,看看它是否返回数据。

如图 2-9 所示,我们在 GET /authors 端点中获得了最近注册的作者。

img/479840_1_En_2_Fig9_HTML.jpg

图 2-9

请求获取/作者

接下来,我们将创建一个端点,使用作者 ID 返回作者,然后更新端点,使用作者 ID 更新作者详细信息,最后一个端点使用作者 ID 删除作者。

对于通过 ID 获取作者,我们将有一个类似/authors/ 的路径,它将从请求参数中获取作者 ID 并找到匹配的作者。

将以下代码添加到 GET author by ID 端点的 GET all authors 路径下。

@app.route('/authors/<id>', methods = ['GET'])
def get_author_by_id(id):
    get_author = Authors.objects.get_or_404(id=ObjectId(id))
    author_schema = AuthorsSchema(only=['id', 'name', 'specialisation'])
    author, error = author_schema.dump(get_author)
    return make_response(jsonify({"author": author}))

现在,当您请求端点/作者/ 时,它将返回用户匹配的 ObjectId(图 2-10 )。

img/479840_1_En_2_Fig10_HTML.jpg

图 2-10

按 ID 获取作者

因此,接下来我们将创建 PUT 端点来使用作者 ID 更新作者信息。为 PUT author 端点添加以下代码。

@app.route('/authors/<id>', methods = ['PUT'])
def update_author_by_id(id):
    data = request.get_json()
    get_author = Authors.objects.get(id=ObjectId(id))
    if data.get('specialisation'):
        get_author.specialisation = data['specialisation']
    if data.get('name'):
        get_author.name = data['name']
    get_author.save()
    get_author.reload()
    author_schema = AuthorsSchema(only=['id', 'name', 'specialisation'])
    author, error = author_schema.dump(get_author)
    return make_response(jsonify({"author": author}))

打开 Postman 并点击与我们在其他模块中所做的相同的路由来更新作者信息,但是这里使用 GET 端点中返回的 ObjectID。

img/479840_1_En_2_Fig11_HTML.jpg

图 2-11

放置作者端点

正如您在图 2-11 中看到的,我们能够使用 PUT 端点更新作者专业化。接下来,我们将创建删除端点,使用作者 ID 删除作者,以完成我们的 CRUD 应用。

添加以下代码,为我们的应用创建删除端点。

@app.route('/authors/<id>', methods = ['DELETE'])
def delete_author_by_id(id):
    Authors.objects(id=ObjectId(id)).delete()
    return make_response("",204)

现在,让我们使用作者 ID 删除新创建的作者,与上一个应用类似,这个端点不会返回任何数据,只会返回 204 状态代码。

使用您之前使用的作者 ID 请求删除端点,它将返回如图 2-12 所示的类似响应。

img/479840_1_En_2_Fig12_HTML.jpg

图 2-12

删除作者端点

这就结束了我们的 flask-mongo CRUD 应用,app.py 中的最终代码应该是这样的。

from flask import Flask, request, jsonify, make_response
from flask_mongoengine import MongoEngine
from marshmallow import Schema, fields, post_load
from bson import ObjectId

app = Flask(__name__)
app.config['MONGODB_DB'] = 'DB_NAME'
db = MongoEngine(app)

Schema.TYPE_MAPPING[ObjectId] = fields.String

class Authors(db.Document):
    name = db.StringField()
    specialisation = db.StringField()

class AuthorsSchema(Schema):
    name = fields.String(required=True)
    specialisation = fields.String(required=True)

@app.route('/authors', methods = ['GET'])
def index():
    get_authors = Authors.objects.all()

    author_schema = AuthorsSchema(many=True, only=['id', 'name', 'specialisation'])
    authors, error = author_schema.dump(get_authors)
    return make_response(jsonify({"authors": authors}))

@app.route('/authors/<id>', methods = ['GET'])
def get_author_by_id(id):
    get_author = Authors.objects.get_or_404(id=ObjectId(id))
    author_schema = AuthorsSchema(only=['id', 'name', 'specialisation'])
    author, error = author_schema.dump(get_author)
    return make_response(jsonify({"author": author}))

@app.route('/authors/<id>', methods = ['PUT'])
def update_author_by_id(id):
    data = request.get_json()
    get_author = Authors.objects.get(id=ObjectId(id))
    if data.get('specialisation'):
        get_author.specialisation = data['specialisation']
    if data.get('name'):
        get_author.name = data['name']
    get_author.save()
    get_author.reload()
    author_schema = AuthorsSchema(only=['id', 'name', 'specialisation'])
    author, error = author_schema.dump(get_author)
    return make_response(jsonify({"author": author}))

@app.route('/authors/<id>', methods = ['DELETE'])
def delete_author_by_id(id):
    Authors.objects(id=ObjectId(id)).delete()

    return make_response("",204)

@app.route('/authors', methods = ['POST'])
def create_author():
    data = request.get_json()
    author = Authors(name=data['name'],specialisation=data['specialisation'])
    author.save()
    author_schema = AuthorsSchema(only=['id','name', 'specialisation'])
    authors, error = author_schema.dump(author)
    return make_response(jsonify({"author": authors}),201)

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

结论

现在我们已经介绍了 SQLAlchemy 和 MongoEngine,并使用它们创建了示例 CRUD 应用。在下一章,我们将详细讨论 REST API 的架构,并为我们的 Flask REST API 应用建立基础。

三、Flask CURD 应用:第一部分

在上一章中,我们讨论了数据库并实现了基于 NoSQL 和 SQL 的例子。在本章中,我们将从头开始创建一个 RESTful Flask 应用。这里我们将维护一个作者对象的数据库,以及他们写的书。这个应用将有一个用户认证机制,只允许登录的用户执行某些功能。我们现在将为 REST 应用创建以下 API 端点:

  1. GET /authors:这将获取作者及其书籍的列表。

  2. GET /authors/ :获取带有指定 ID 的作者及其书籍。

  3. POST /authors:这将创建一个新的 Author 对象。

  4. PUT /authors/ :这将编辑具有给定 ID 的作者对象。

  5. DELETE /authors/ :这将删除具有给定 ID 的作者。

  6. GET /books:这将返回所有的书。

  7. GET /books/ :获取指定 ID 的书籍。

  8. POST /books:这将创建一个新的 book 对象。

  9. PUT / books/ :这将编辑给定 ID 的 book 对象。

  10. DELETE /book/ :删除给定 ID 的图书。

让我们直接进入主题,我们将从创建一个新项目开始,并将其命名为 author-manager。因此,创建一个新目录,并从创建一个新的虚拟环境开始。

$ mkdir author-manager && cd author-manager

$ virtualenv venv

现在我们应该有我们的虚拟环境设置;接下来,我们需要激活环境并安装依赖项,就像我们在上一章中所做的那样。

我们将从安装以下依赖项开始,并在需要时添加更多依赖项。

(venv) $ pip install flask flask-sqlalchemy marshmallow-sqlalchemy

我们还将在这个应用中使用蓝图。Flask 使用蓝图的概念来制作应用组件,并支持应用中的通用模式。蓝图有助于为应用创建更小的模块,使其易于管理。Blueprint 对于大型应用非常有价值,它简化了大型应用的工作方式。

我们将应用构建成小模块,并将所有应用代码保存在 app 文件夹内的/src 文件夹中。因此,在当前工作目录下创建一个 src 文件夹,然后在其中创建 run.py 文件。

(venv) $ mkdir src && cd src

在 src 文件夹中,我们将有我们的 run.py 文件和另一个名为 api 的目录,它将导出我们的模块,所以继续在 src 中创建一个 api 文件夹。我们将在 src 内的 main.py 文件中初始化我们的 Flask 应用,然后创建另一个文件 run.py,该文件将导入 main.py、config 文件并运行应用。

先说 main.py。

添加以下代码以导入所需的库,然后初始化 app 对象。这里我们将定义一个函数,它将接受应用配置,然后初始化我们的应用。

import os
from flask import Flask
from flask import jsonify

app = Flask(__name__)

if os.environ.get('WORK_ENV') == 'PROD':
    app_config = ProductionConfig
elif os.environ.get('WORK_ENV') == 'TEST':
    app_config = TestingConfig
else:
    app_config = DevelopmentConfig

app.config.from_object(app_config)

if __name__ == "__main__":
    app.run(port=5000, host="0.0.0.0", use_reloader=False)

这就是我们 main.py 的框架。接下来,我们将创建 run.py 来调用 app 并运行应用。稍后我们将添加路由,初始化我们的 db 对象,并在 main.py 中配置日志记录。

将以下代码添加到 run.py 中,以导入 create_app 并运行应用。

from main import app as application

if __name__ == "__main__":
    application.run()

这里我们已经定义了配置,导入了 create_app,并初始化了应用。接下来,我们将把配置移动到一个单独的目录,并指定特定于环境的配置。我们将在 src 中创建另一个目录/api,并从 api 目录中导出配置、模型和路由,因此现在在 src 中创建一个名为 api 的目录,然后在 api 中创建另一个名为 config 的目录。

注意

创建一个名为 init 的空文件。py,让 Python 知道它包含模块。

现在,在 config 目录中创建 config.py 和 init.py

class Config(object):
    DEBUG = False
    TESTING = False
    SQLALCHEMY_TRACK_MODIFICATIONS = False

class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI =  <Production DB URL>

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI =  <Development DB URL>
    SQLALCHEMY_ECHO = False

class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = <Testing DB URL>
    SQLALCHEMY_ECHO = False

前面的代码定义了我们在 main.py 中所做的基本配置,然后在顶部添加了特定于环境的配置。

因此,除了 main 之外,我们还从配置模块导入开发、测试和生产配置,并导入 OS 模块以读取环境模块。之后,我们检查是否提供了 WORK_ENV 环境变量来相应地启动应用;否则,我们默认使用开发配置启动应用。

所以我们已经提供了数据库配置,但还没有在我们的应用中初始化数据库;接下来,我们现在就开始吧。

现在在 api 中创建另一个名为 utils 的目录,它将保存我们的实用程序模块;现在,我们将在那里启动我们的 db 对象。

在实用程序中创建 database.py,并在其中添加以下代码。

from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

这将开始创建我们的数据库对象;接下来,我们将在 main.py 中导入 db 对象并初始化它。

在我们导入库的地方添加下面的代码,以导入 db 对象。

from api.utils.database import db

def create_app(config):
    app = Flask(__name__)

    app.config.from_object(config)

    db.init_app(app)
    with app.app_context():
        db.create_all()
    return app

并更新 create_app 来初始化 db 对象。

现在我们有了 REST 应用的基础,您的应用结构应该是这样的。

venv/
src
├── api/
│   ├── __init__.py
│   ├── utils
│   │     └── __init__.py
│   │     └── database.py
│   └── config
│           └── __init__.py
│           └── database.py
├── run.py
├── main.py
└── requirements.txt

接下来让我们定义我们的数据库模式。这里我们将处理两个资源,即作者和书。所以让我们先创建图书模式。我们将把所有的模式放在 api 目录中一个名为 models 的目录中,所以继续启动 models 模块,然后创建 books.py

将以下代码添加到 books.py 中,以创建 books 模型。

from api.utils.database import db
from marshmallow_sqlalchemy import ModelSchema
from marshmallow import fields

class Book(db.Model):
    __tablename__ = 'books'

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    title = db.Column(db.String(50))
    year = db.Column(db.Integer)
    author_id = db.Column(db.Integer, db.ForeignKey('authors.id'))

    def __init__(self, title, year, author_id=None):
        self.title = title
        self.year = year
        self.author_id = author_id

    def create(self):
        db.session.add(self)
        db.session.commit()
        return self

class BookSchema(ModelSchema):
    class Meta(ModelSchema.Meta):
        model = Book
        sqla_session = db.session

    id = fields.Number(dump_only=True)
    title = fields.String(required=True)
    year = fields.Integer(required=True)
    author_id = fields.Integer()

这里我们正在导入 db 模块 marshmallow,就像我们之前做的那样来映射字段并帮助我们返回 JSON 对象。

注意,这里有一个字段 author_id,它是 authors 模型中 id 字段的外键。接下来,我们将创建 authors.py 和创建 authors 模型。

from api.utils.database import db
from marshmallow_sqlalchemy import ModelSchema
from marshmallow import fields
from api.models.books import BookSchema

class Author(db.Model):
    __tablename__ = 'authors'

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    first_name = db.Column(db.String(20))
    last_name = db.Column(db.String(20))
    created = db.Column(db.DateTime, server_default=db.func.now())
    books = db.relationship('Book', backref="Author", cascade="all, delete-orphan")

    def __init__(self, first_name, last_name, books=[]):
        self.first_name = first_name
        self.last_name = last_name
        self.books = books

    def create(self):
        db.session.add(self)
        db.session.commit()
        return self

class AuthorSchema(ModelSchema):
    class Meta(ModelSchema.Meta):
        model = Author
        sqla_session = db.session

    id = fields.Number(dump_only=True)
    first_name = fields.String(required=True)
    last_name = fields.String(required=True)
    created = fields.String(dump_only=True)
    books = fields.Nested(BookSchema, many=True, only=['title','year','id']

前面的代码将创建我们的作者模型。请注意,我们还在这里导入了 books 模型,并创建了作者和他们的书籍之间的关系,这样当我们检索 author 对象时,我们还可以获得与其 ID 相关联的书籍,因此我们在这个模型中建立了作者和书籍之间的一对多关系。

现在,一旦我们有了 DB 模式,接下来我们需要开始创建我们的路由,但是在我们开始编写路由之前,作为应用模块化的一部分,我们还应该做一件事,创建另一个模块 responses.py 来创建 HTTP 响应的标准类。

之后,我们将在 main.py 中创建全局 HTTP 配置

在 api/utils 内部创建 responses.py,这里我们将使用来自 Flask 库的 jsonify 和 make_response 为我们的 api 创建标准响应。

因此,在 responses.py 中编写以下代码来启动该模块。

from flask import make_response, jsonify

def response_with(response, value=None, message=None, error=None, headers={}, pagination=None):
    result = {}
    if value is not None:
        result.update(value)

    if response.get('message', None) is not None:
        result.update({'message': response['message']})

    result.update({'code': response['code']})

    if error is not None:
        result.update({'errors': error})

    if pagination is not None:
        result.update({'pagination': pagination})

    headers.update({'Access-Control-Allow-Origin': '*'})
    headers.update({'server': 'Flask REST API'})

    return make_response(jsonify(result), response['http_code'], headers)

前面的代码公开了一个函数 response_with,供我们的 API 端点使用和响应;除此之外,我们还将创建标准响应代码和消息。

这里是我们的应用将支持的响应列表。

表 3-1 提供了我们将在应用中使用的 HTTP 响应。在 response_with 上面添加以下代码,以便在 responses.py 中定义它们。

表 3-1

HTTP 响应

| Two hundred | 200 好吧 | 对 HTTP 请求的标准响应 | | Two hundred and one | 201 已创建 | 意味着请求得到满足,并且创建了新的资源 | | Two hundred and four | 204 无内容 | 请求成功,但未返回任何数据 | | four hundred | 400 个错误请求 | 意味着由于客户端错误,服务器无法处理请求 | | Four hundred and three | 403 未授权 | 有效的请求,但发出请求的客户端无权获取资源 | | Four hundred and four | 404 未找到 | 服务器上不存在请求的资源 | | Four hundred and twenty-two | 422 无法处理的实体 | 由于语义错误,无法处理请求 | | Five hundred | 500 内部服务器错误 | 暗示服务器中出现意外情况的一般错误 |
INVALID_FIELD_NAME_SENT_422 = {
    "http_code": 422,
    "code": "invalidField",
    "message": "Invalid fields found"

}

INVALID_INPUT_422 = {
    "http_code": 422,
    "code": "invalidInput",
    "message": "Invalid input"
}

MISSING_PARAMETERS_422 = {
    "http_code": 422,
    "code": "missingParameter",
    "message": "Missing parameters."
}

BAD_REQUEST_400 = {
    "http_code": 400,
    "code": "badRequest",
    "message": "Bad request"
}

SERVER_ERROR_500 = {
    "http_code": 500,
    "code": "serverError",
    "message": "Server error"
}

SERVER_ERROR_404 = {
    "http_code": 404,
    "code": "notFound",
    "message": "Resource not found"
}

UNAUTHORIZED_403 = {
    "http_code": 403,
    "code": "notAuthorized",
    "message": "You are not authorised to execute this."
}

SUCCESS_200 = {
    'http_code': 200,
    'code': 'success'
}

SUCCESS_201 = {
    'http_code': 201,
    'code': 'success'
}

SUCCESS_204 = {
    'http_code': 204,
    'code': 'success'
}

现在我们应该有我们的工作响应。py 模块;接下来,我们将添加用于处理错误的全局 HTTP 配置。

接下来在 main.py 中导入 status 和 response_with 函数。

from api.utils.responses import response_with
import api.utils.responses as resp

然后在 db.init_app 函数上方添加以下代码来配置全局 HTTP 配置。

    @app.after_request
    def add_header(response):
        return response

    @app.errorhandler(400)
    def bad_request(e):
        logging.error(e)
        return response_with(resp.BAD_REQUEST_400)

    @app.errorhandler(500)
    def server_error(e):
        logging.error(e)
        return response_with(resp.SERVER_ERROR_500)

    @app.errorhandler(404)
    def not_found(e):
        logging.error(e)
        return response_with(resp. SERVER_ERROR_404)

下面的代码添加了错误情况下的全局响应。现在你的 main.py 应该是这样的。

from flask import Flask
from flask import jsonify
from api.utils.database import db
from api.utils.responses import response_with
import api.utils.responses as resp

app = Flask(__name__)

if os.environ.get('WORK_ENV') == 'PROD':
    app_config = ProductionConfig
elif os.environ.get('WORK_ENV') == 'TEST':
    app_config = TestingConfig
else:
    app_config = DevelopmentConfig

app.config.from_object(app_config)

db.init_app(app)
with app.app_context():
    db.create_all()

# START GLOBAL HTTP CONFIGURATIONS
@app.after_request
def add_header(response):
    return response

@app.errorhandler(400)
def bad_request(e):
    logging.error(e)
    return response_with(resp.BAD_REQUEST_400)

@app.errorhandler(500)
def server_error(e):
    logging.error(e)
    return response_with(resp.SERVER_ERROR_500)

@app.errorhandler(404)
def not_found(e):
    logging.error(e)
    return response_with(resp.SERVER_ERROR_404)

db.init_app(app)
with app.app_context():
    db.create_all()

if __name__ == "__main__":
    app.run(port=5000, host="0.0.0.0", use_reloader=False)

接下来,我们需要创建 API 端点,并使用蓝图将它们包含在 main.py 中。

我们将把我们的路由放在 api 中一个名为 routes 的目录中,所以继续创建这个文件夹;接下来添加 authors.py 来创建图书路线。

接下来,使用下面的代码导入所需的模块。

from flask import Blueprint
from flask import request
from api.utils.responses import response_with
from api.utils import responses as resp
from api.models.authors import Author, AuthorSchema
from api.utils.database import db

这里,我们从 Flask 导入 Blueprint 和 request 模块,从 responses util、Author schema 和 db 对象导入 resp _ with 和 resp 方法。

接下来,我们将配置蓝图。

author_routes = Blueprint("author_routes", __name__)

一旦完成,我们可以从我们的 POST author 路径开始,并在 book_routes 下面添加以下代码。

@author_routes.route('/', methods=['POST'])
def create_author():
    try:
        data = request.get_json()
        author_schema = AuthorSchema()
        author, error = author_schema.load(data)
        result = author_schema.dump(author.create()).data
        return response_with(resp.SUCCESS_201, value={"author": result})
    except Exception as e:
        print e
        return response_with(resp.INVALID_INPUT_422)

因此,前面的代码将从请求中获取 JSON 数据,并在 Author 模式上执行 create 方法,然后使用 response_with 方法返回响应,为该端点提供响应类型 201 和数据值,该数据值是带有新创建作者的 JSON 对象。

现在,在我们设置所有其他路线之前,让我们在应用中注册 author routes Blueprint,并运行应用来测试一切是否正常。

所以在你的 main.py 中,导入作者路线,然后注册蓝图。

from api.routes.authors import author_routes

然后在@app.after_request 的正上方添加下面一行。

app.register_blueprint(author_routes, url_prefix='/api/authors')

现在使用 Python run.py 命令运行应用,我们的 Flask 服务器应该启动并运行了。

让我们试试 POST authors 端点,因此在http://localhost:5000/api/authors/用下面的 JSON 数据打开 postmand 请求。

{
       "first_name" : "kunal",
       "last_name" : "Relan"
 }

img/479840_1_En_3_Fig1_HTML.jpg

图 3-1

帖子作者端点

如您所见,books 是一个空数组,因为我们还没有创建任何书;接下来让我们添加 GET authors 端点(图 3-1 )。

@author_routes.route('/', methods=['GET'])
def get_author_list():
    fetched = Author.query.all()
    author_schema = AuthorSchema(many=True, only=['first_name', 'last_name','id'])
    authors, error = author_schema.dump(fetched)
    return response_with(resp.SUCCESS_200, value={"authors": authors})

前面的代码将添加 GET all authors route,这里我们将用一个只包含作者 ID、名字和姓氏的作者数组来响应。所以让我们来测试一下。

img/479840_1_En_3_Fig2_HTML.jpg

图 3-2

获取作者路线

正如您在图 3-2 中看到的,端点响应了一组作者。

接下来,让我们添加另一个 GET route 来使用作者的 ID 获取特定的作者,并添加以下代码来添加该 route。

@author_routes.route('/<int:author_id>', methods=['GET'])
def get_author_detail(author_id):
    fetched = Author.query.get_or_404(author_id)
    author_schema = AuthorSchema()
    author, error = author_schema.dump(fetched)
    return response_with(resp.SUCCESS_200, value={"author": author})

前面的代码从 route 参数中获取一个整数,查找具有相应 ID 的作者,并返回 author 对象。

因此,让我们尝试获取 ID 为 1 的作者(图 3-3 )。

img/479840_1_En_3_Fig3_HTML.jpg

图 3-3

正在获取 ID 为 1 的作者

如果具有该 ID 的作者存在,我们将得到带有 200 状态代码和作者对象的响应,否则为 404,如下图所示。正如您所看到的,没有 ID 为 2 的作者,get_or_404 方法在端点上抛出 404 错误,然后由 app.errorhandler(404)按照我们在 main.py 中提到的方式进行处理(图 3-4 )。

img/479840_1_En_3_Fig4_HTML.jpg

图 3-4

找不到 ID 为 2 的作者

在我们继续为 author 对象创建 PUT 和 DELETE 端点之前,让我们启动图书路由。在同一个 routes 文件夹中创建 books.py,并添加以下代码来启动路由。

from flask import Blueprint, request
from api.utils.responses import response_with
from api.utils import responses as resp
from api.models.books import Book, BookSchema
from api.utils.database import db

book_routes = Blueprint("book_routes", __name__)

然后在 main.py 中注册 book routes,就像我们注册 author routes 一样。将下面的代码添加到导入作者路径的位置。

from api.routes.books import book_routes

然后,在您添加了 author route blueprint 注册的地方的正下方,添加以下代码。

app.register_blueprint(book_routes, url_prefix='/api/books')

现在您的 main.py 应该有以下代码。

import logging
import sys
import api.utils.responses as resp
from flask import Flask, jsonify
from api.utils.database import db
from api.utils.responses import response_with
from api.routes.authors import author_routes
from api.routes.books import book_routes

def create_app(config):
    app = Flask(__name__)

    app.config.from_object(config)

    db.init_app(app)
    with app.app_context():
        db.create_all()
    app.register_blueprint(author_routes, url_prefix='/api/authors')
    app.register_blueprint(book_routes, url_prefix='/api/books')

    @app.after_request
    def add_header(response):
        return response

    @app.errorhandler(400)
    def bad_request(e):
        logging.error(e)
        return response_with(resp.BAD_REQUEST_400)

    @app.errorhandler(500)
    def server_error(e):
        logging.error(e)
        return response_with(resp.SERVER_ERROR_500)

    @app.errorhandler(404)
    def not_found(e):
        logging.error(e)
        return response_with(resp.SERVER_ERROR_404)

    db.init_app(app)
    with app.app_context():
        db.create_all()

    logging.basicConfig(stream=sys.stdout,
                        format='%(asctime)s|%(levelname)s|%(filename)s:%(lineno)s|%(message)s',
                        level=logging.DEBUG)
    return app

接下来让我们从创建 POST book 端点开始;打开 routes 文件夹中的 books.py,并在 book_routes 下添加以下代码。

@book_routes.route('/', methods=['POST'])
def create_book():
    try:
        data = request.get_json()
        book_schema = BookSchema()
        book, error = book_schema.load(data)
        result = book_schema.dump(book.create()).data
        return response_with(resp.SUCCESS_201, value={"book": result})
    except Exception as e:
        print e
        return response_with(resp.INVALID_INPUT_422)

前面的代码将获取用户数据,然后对 book schema 执行 create()方法,就像我们在 author object 中所做的一样;让我们保存文件并测试端点。

{
       "title" : "iOS Penetration Testing",
       "year" : 2016,
       "author_id": 1
 }

我们将使用前面的 JSON 数据发送到端点,我们应该得到一个包含 200 状态代码和新创建的 book 对象的响应。正如我们之前所讨论的,我们已经建立了作者和书籍之间的关系,在前面的例子中,我们已经为新书指定了 ID 为 1 的作者,所以一旦这个 API 成功,我们将能够获取 ID 为 1 的作者,作为响应,books 数组将把这本书作为一个对象(图 3-5 )。

img/479840_1_En_3_Fig5_HTML.jpg

图 3-5

获取 ID 为 1 的作者

正如您在图 3-6 中看到的,当我们请求/authors/1 端点时,除了作者详细信息,我们还获得了 books 数组,其中包含作者链接到的书籍列表。

img/479840_1_En_3_Fig6_HTML.jpg

图 3-6

获取作者端点

所以我们的模特关系很好。现在我们可以继续为作者路由创建其余的端点。继续添加下面的代码来获取作者路由的 PUT 端点,以更新作者对象。

@author_routes.route('/<int:id>', methods=['PUT'])
def update_author_detail(id):
    data = request.get_json()
    get_author = Author.query.get_or_404(id)
    get_author.first_name = data['first_name']
    get_author.last_name = data['last_name']
    db.session.add(get_author)
    db.session.commit()
    author_schema = AuthorSchema()
    author, error = author_schema.dump(get_author)
    return response_with(resp.SUCCESS_200, value={"author": author})

前面的代码将创建我们的 PUT 端点来更新 author 对象。在前面的代码中,我们在数据变量中获取一个请求 JSON,然后用请求参数中提供的 ID 获取作者。如果没有找到具有该 ID 的作者,请求以 404 状态代码结束,或者 get_author 包含 author 对象,然后我们用请求 JSON 中提供的数据更新 first_name 和 last_name,然后保存会话。

所以让我们继续更新我们不久前创建的作者的名字和姓氏(图 3-7 )。

img/479840_1_En_3_Fig7_HTML.jpg

图 3-7

放置作者端点

所以这里我们更新了作者的名和姓。然而,在 PUT 中,我们需要发送对象的整个请求体,正如我们在第二章中所讨论的,所以接下来我们将创建一个补丁端点来只更新 author 对象的一部分。为修补程序端点添加以下代码。

@author_routes.route('/<int:id>', methods=['PATCH'])
def modify_author_detail(id):
    data = request.get_json()
    get_author = Author.query.get(id)
    if data.get('first_name'):
        get_author.first_name = data['first_name']
    if data.get('last_name'):
        get_author.last_name = data['last_name']
    db.session.add(get_author)
    db.session.commit()
    author_schema = AuthorSchema()
    author, error = author_schema.dump(get_author)
    return response_with(resp.SUCCESS_200, value={"author": author})

前面的代码像另一个端点一样获取请求 JSON,但并不期望整个请求体,而只是请求体中需要更新的字段,同样,它更新 author 对象并保存会话。让我们尝试一下,这次我们将只更改 author 对象的名字。

img/479840_1_En_3_Fig8_HTML.jpg

图 3-8

更改作者对象的名字

正如您在图 3-8 中看到的,我们只在请求体中提供了名字,它已经被更新了。接下来,我们将最终创建删除作者端点,它将从请求参数中获取作者 ID 并删除作者对象。注意,在这个例子中,我们将用 204 状态码来响应,没有任何内容。

@author_routes.route('/<int:id>', methods=['DELETE'])
def delete_author(id):
    get_author = Author.query.get_or_404(id)
    db.session.delete(get_author)
    db.session.commit()
    return response_with(resp.SUCCESS_204)

添加前面的代码,现在这将创建我们的删除端点。让我们继续尝试删除 ID 为 1 的作者(图 3-9 )。

img/479840_1_En_3_Fig9_HTML.jpg

图 3-9

删除作者端点

有了这个端点,我们的 author 对象应该从数据库中删除,并且在创建 author 模式时,我们配置了 book 关系中的所有级联。因此,所有与作者 ID 1 相关的书籍也将被删除,以确保我们没有任何没有作者 ID 的书籍。

这就是我们的作者路线,接下来我们将致力于我们的书端点的其余部分。接下来,在 books.py 中添加以下代码来创建 GET books 端点。

@book_routes.route('/', methods=['GET'])
def get_book_list():
    fetched = Book.query.all()
    book_schema = BookSchema(many=True, only=['author_id','title', 'year'])
    books, error = book_schema.dump(fetched)
    return response_with(resp.SUCCESS_200, value={"books": books})

保存文件并尝试端点;现在你将得到一个空数组,因为当我们删除作者时,作者 ID 为 1 的书也被删除了。

img/479840_1_En_3_Fig10_HTML.jpg

图 3-10

获取书籍端点

正如您在图 3-10 中看到的,到目前为止,表中没有书籍,所以继续创建一个作者,然后添加几本具有该作者 ID 的书籍,因为我们不能添加没有作者的书籍,否则它会以 422 不可处理的实体错误结束。

接下来,我们将通过 ID 端点创建 GET Book。

@book_routes.route('/<int:id>', methods=['GET'])
def get_book_detail(id):
    fetched = Book.query.get_or_404(id)
    book_schema = BookSchema()
    books, error = book_schema.dump(fetched)
    return response_with(resp.SUCCESS_200, value={"books": books})

以下代码将创建按 ID 端点获取图书;接下来,我们将创建 PUT、PATCH 和 DELETE 端点,并为其添加以下代码。

book_routes.route('/<int:id>', methods=['PUT'])
def update_book_detail(id):
    data = request.get_json()
    get_book = Book.query.get_or_404(id)
    get_book.title = data['title']
    get_book.year = data['year']
    db.session.add(get_book)
    db.session.commit()
    book_schema = BookSchema()
    book, error = book_schema.dump(get_book)
    return response_with(resp.SUCCESS_200, value={"book": book})

@book_routes.route('/<int:id>', methods=['PATCH'])
def modify_book_detail(id):
    data = request.get_json()
    get_book = Book.query.get_or_404(id)
    if data.get('title'):
        get_book.title = data['title']
    if data.get('year'):
        get_book.year = data['year']
    db.session.add(get_book)
    db.session.commit()
    book_schema = BookSchema()
    book, error = book_schema.dump(get_book)
    return response_with(resp.SUCCESS_200, value={"book": book})

@book_routes.route('/<int:id>', methods=['DELETE'])
def delete_book(id):
    get_book = Book.query.get_or_404(id)
    db.session.delete(get_book)
    db.session.commit()
    return response_with(resp.SUCCESS_204)

因此,这将结束我们的图书和作者路线,现在我们有一个工作休息应用。现在,您可以尝试在 author 和 book 路径上执行 CRUD。

用户认证

一旦我们准备好了所有的路由,我们需要添加用户身份验证以确保只有登录的用户才能访问特定的路由,所以现在我们将添加用户登录和注册路由,但在此之前,我们需要添加用户模式。

在模型中创建 users.py。在模式中,我们将添加两个静态方法来加密密码和验证密码,为此我们需要一个名为 passlib 的 Python 库,所以在创建模式之前,让我们使用 PIP 安装 passlib。

(venv)$ pip install passlib

完成后,添加以下代码来添加用户模式和方法。

from api.utils.database import db
from passlib.hash import pbkdf2_sha256 as sha256
from marshmallow_sqlalchemy import ModelSchema
from marshmallow import fields

class User(db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key = True)
    username = db.Column(db.String(120), unique = True, nullable = False)
    password = db.Column(db.String(120), nullable = False)

    def create(self):
        db.session.add(self)
        db.session.commit()
        return self

    @classmethod
    def find_by_username(cls, username):
        return cls.query.filter_by(username = username).first()

    @staticmethod
    def generate_hash(password):
        return sha256.hash(password)

    @staticmethod

    def verify_hash(password, hash):
        return sha256.verify(password, hash)

class UserSchema(ModelSchema):
    class Meta(ModelSchema.Meta):
        model = User
        sqla_session = db.session

    id = fields.Number(dump_only=True)
    username = fields.String(required=True)

因此,我们在这里添加了一个类方法,通过用户名查找用户,创建一个用户,然后创建两个静态方法来生成散列并验证它。我们将在创建用户路线时使用这些方法。

接下来在 routes 目录中创建 users.py,这是我们添加用户登录和注册路由的地方。

对于跨应用的用户认证,我们将使用 JWT (JSON Web 令牌)认证。JWT 是一个开放标准,它定义了一种紧凑的、自包含的方式,以 JSON 对象的形式安全地传输信息。JWT 是 REST 世界中一种流行的用户授权方式。在 Flask 中有一个名为 Flask-JWT 扩展的开源扩展,它提供了 JWT 支持和其他有用的方法。

让我们继续安装 Flask-JWT-扩展。

(venv)$ pip install flask-jwt-extended

接下来,我们将在 main.py 中初始化应用中的 JWT 模块,以便在 main.py 中导入库

from flask_jwt_extended import JWTManager

接下来用 db.init_app()上面的代码初始化 JWTManager。

jwt = JWTManager(app)

安装和初始化之后,让我们导入用户路由文件所需的模块。

from flask import Blueprint, request
from api.utils.responses import response_with
from api.utils import responses as resp
from api.models.users import User, UserSchema
from api.utils.database import db
from flask_jwt_extended import create_access_token

这些是我们在用户路线中需要的模块;接下来,我们将使用 Blueprint 配置路由,代码如下。

user_routes = Blueprint("user_routes", __name__)

接下来,我们将在 main.py 文件中导入并注册/users 路由,因此在 main.py 中添加以下代码来导入用户路由。

from api.routes.users import user_routes

现在,在我们已经声明了其他路由的地方的正下方,添加以下代码行。

app.register_blueprint(user_routes, url_prefix='/api/users')

接下来,我们将创建我们的 POST 用户路由来创建一个新用户,并在 routes 内部的 users.py 中添加以下代码。

@user_routes.route('/', methods=['POST'])
def create_user():
    try:
        data = request.get_json()
        data['password'] = User.generate_hash(data['password'])
        user_schmea = UserSchema()
        user, error = user_schmea.load(data)
        result = user_schmea.dump(user.create()).data
        return response_with(resp.SUCCESS_201)
    except Exception as e:
        print e
        return response_with(resp.INVALID_INPUT_422)

这里,我们将用户请求数据放在一个变量中,然后对密码执行 generate_hash()函数并创建用户。一旦完成,我们将返回一个 201 响应。

接下来,我们将为注册用户创建一个登录路径。为相同的添加以下代码。

@user_routes.route('/login', methods=['POST'])
def authenticate_user():
      try:
        data = request.get_json()
        current_user = User.find_by_username(data['username'])
        if not current_user:
            return response_with(resp.SERVER_ERROR_404)
        if User.verify_hash(data['password'], current_user.password):
            access_token = create_access_token(identity = data['username'])
            return response_with(resp.SUCCESS_201, value={'message': 'Logged in as {}'.format(current_user.username), "access_token": access_token})
        else:
            return response_with(resp.UNAUTHORIZED_401)
      except Exception as e:
        print e
        return response_with(resp.INVALID_INPUT_422)

下面的代码将从请求数据中获取用户名和密码,并使用我们在模式中创建的 find_by_username()方法检查具有所提供用户名的用户是否存在。接下来,如果用户不存在,我们将使用 404 进行响应,或者使用模式中的 verify_hash()函数来验证密码。如果用户存在,我们将生成一个 JWT 令牌并用 200 来响应;否则以 401 回应。现在我们已经有了用户登录。接下来,我们需要将 jwt required decorator 添加到我们想要保护的路由中。因此,在 routes 中导航到 authors.py,并使用下面的代码导入装饰器。

from flask_jwt_extended import jwt_required

然后在端点定义之前,使用下面的代码添加装饰器。

@jwt_required

我们将把装饰器添加到 authors.py 和 books.py 的 DELETE、PUT、POST 和 PATCH 端点,这些函数现在应该是这样的。

@author_routes.route('/', methods=['POST'])
@jwt_required
def create_author():
      ....Function code

让我们继续测试我们的用户端点。打开 Postman,用用户名和密码请求 POST 用户端点。我们将使用下面的样本数据。

{
       "username" : "admin",
       "password" : "flask2019"
 }

img/479840_1_En_3_Fig11_HTML.jpg

图 3-11

用户注册端点

这样我们的新用户就创建好了(图 3-11);接下来,我们将尝试使用相同的凭据登录并获取 JWT。

img/479840_1_En_3_Fig12_HTML.jpg

图 3-12

用户登录端点

如图 3-12 所示,我们已经使用新创建的用户成功登录。现在让我们尝试访问最近添加了 jwt_required decorator 的 POST author 路径(图 3-13 )。

img/479840_1_En_3_Fig13_HTML.jpg

图 3-13

无 JWT 令牌的帖子作者路线

如图 3-14 所示,我们无法再访问 POST author 路径,jwt_required decorator 返回 401 错误。现在,让我们通过在报头中提供 JWT 来尝试访问相同的路由。在 Postman 中请求的头部分,添加带有名为 Authorization 的密钥的令牌,然后在 value 中添加 Bearer <令牌>来提供 JWT 令牌,如图 3-14 所示。

img/479840_1_En_3_Fig14_HTML.jpg

图 3-14

JWT 后作者路线

如您所见,添加 JWT 令牌后,我们能够再次访问端点,这就是我们保护 REST 端点的方式。

因此,在下面的场景中,我们允许任何人登录平台,然后访问路线。然而,在实际应用中,我们还可以进行电子邮件验证和限制用户注册,同时我们还可以启用基于用户的访问控制,不同类型的用户可以访问特定的 API。

结论

本章到此结束,我们已经成功地创建了一个带有用户认证的 REST 应用。在下一章,我们将致力于记录 REST APIs,集成单元测试,以及部署我们的应用。

四、Flask CURD 应用:

在上一章中,我们使用 Flask 创建了 REST APIs,现在我们有了一个工作的 CRUD 应用。在这一章中,我们将讨论和实现支持和扩展 REST APIs 的特性。虽然我们已经做好了部署的一切准备,但是,在部署应用之前,我们还需要讨论一些事情。

  1. 电子邮件验证

  2. 文件上传

  3. 讨论 API 文档

  4. 整合霸气

介绍

在上一章中,我们使用 Flask 和 MySQL 创建了一个 REST 应用。在这一章中,我们将讨论如何扩展应用的附加功能。我们首先将电子邮件验证添加到我们的用户模型中。接下来,我们还将向用户对象添加文件上传端点,我们还将讨论 API 文档的需求、记录 API 的最佳实践以及使用 Swagger 作为 API 文档工具。

电子邮件验证

在上一章中,我们创建了使用唯一用户名和密码的用户注册和登录。在这一章中,我们将通过在用户模型中添加电子邮件注册来扩展用户身份验证,并添加电子邮件验证。为此,我们将在模型中添加 email 字段,一旦使用 signup API 创建了新的用户对象,我们将创建一个验证令牌,并向用户发送一封电子邮件,其中包含验证帐户的链接。我们还将禁用用户登录,直到电子邮件被验证。首先,让我们在用户模型中添加必需的字段。

在模型中浏览到 users.py,并在 User 类中的 password 下添加以下几行。

    isVerified = db.Column(db.Boolean,  nullable=False, default=False)
    email = db.Column(db.String(120), unique = True, nullable = False)

并在 UserSchema 类中的 username 下面添加下面一行。

    email = db.Column(db.String(120), unique = True, nullable = False)

此外,由于现在我们有了用户电子邮件,我们将更新 find_by_username 类方法以通过电子邮件查找。因此,将 find_by_username 方法更新为以下内容。

    @classmethod
    def find_by_email(cls, email):
        return cls.query.filter_by(email = email).first()

现在,您的用户类应该具有以下代码。

class User(db.Model):
    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key = True)
    username = db.Column(db.String(120), unique = True, nullable = False)
    password = db.Column(db.String(120), nullable = False)
    isVerified = db.Column(db.Boolean,  nullable=False, default=False)
    email = db.Column(db.String(120), unique = True, nullable = False)
    def create(self):
        db.session.add(self)
        db.session.commit()
        return self
    @classmethod
    def find_by_email(cls, email):
        return cls.query.filter_by(email = email).first()

    @classmethod
    def find_by_username(cls, email):
        return cls.query.filter_by(username = username).first()

    @staticmethod
    def generate_hash(password):
        return sha256.hash(password)

    @staticmethod
    def verify_hash(password, hash):
        return sha256.verify(password, hash)

和 UserSchema 应该具有以下代码。

class UserSchema(ModelSchema):
    class Meta(ModelSchema.Meta):
        model = User
        sqla_session = db.session

    id = fields.Number(dump_only=True)
    username = fields.String(required=True)
    email = fields.String(required=True)

请注意,默认情况下,isVerified 字段设置为 False,一旦用户验证了电子邮件,我们会将其设置为 True,以便用户登录。

接下来,我们将添加一个名为 token.py 的 util,它将包含生成验证令牌和确认验证令牌的方法。邮件中的验证链接将包含一个带有验证令牌的唯一 URL,该验证令牌应该类似于htttp://host/api/users/confirm/<verification_token>,并且这里的令牌应该始终是唯一的。我们将使用它的危险包来编码用户电子邮件和时间戳,所以让我们继续在 api/utils 中创建 token.py。

在我们编写生成令牌的代码之前,我们需要向 app config 添加几个变量,因为它的 dangerous 需要一个密钥和密码 salt 才能工作,我们将从 config.py 中提供这些密钥和密码 salt。

    SECRET_KEY= 'your_secured_key_here'
    SECURITY_PASSWORD_SALT= 'your_security_password_here'

接下来,在 token.py 中添加以下代码来导入需求。

from itsdangerous import URLSafeTimedSerializer
from flask import current_app

然后添加以下代码来生成令牌。

def generate_verification_token(email):
    serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])
    return serializer.dumps(email,salt=current_app.config['SECURITY_PASSWORD_SALT'])

在前面的方法中,我们使用 URLSafeTimedSerializer 生成一个使用电子邮件地址的令牌,电子邮件被编码在令牌中。接下来,我们将创建另一种方法来验证令牌和过期时间,只要令牌有效且未过期,我们将返回电子邮件并验证用户电子邮件。

    def confirm_verification_token(token, expiration=3600):
    serializer = URLSafeTimedSerializer(current_app.config['SECRET_KEY'])
    try:
        email = serializer.loads(
            token,
            salt=current_app.config['SECURITY_PASSWORD_SALT'],
            max_age=expiration
        )
    except Exception as e :
        return e
    return email

一旦我们有了令牌工具,我们现在就可以修改用户路由了。让我们从在电子邮件验证之前禁用用户登录开始。更新登录路由,使其具有以下代码;在这里,我们将 find_by_username 更改为 find_by_email,现在我们希望用户在登录端点 JSON 数据中发送电子邮件地址,如果用户没有通过验证,我们将返回一个 400 错误代码的请求,但不包含令牌。

现在,您的登录方法应该包含以下代码。

@user_routes.route('/login', methods=['POST'])
def authenticate_user():
      try:
        data = request.get_json()
        if data.get('email') :
            current_user = User.find_by_email(data['email'])
        elif data.get('username') :
            current_user = User.find_by_username(data['username'])
        if not current_user:
            return response_with(resp.SERVER_ERROR_404)
        if current_user and not current_user.isVerified:
            return response_with(resp.BAD_REQUEST_400)
        if User.verify_hash(data['password'], current_user.password):
            access_token = create_access_token(identity = current_user.username)
            return response_with(resp.SUCCESS_201, value={'message': 'Logged in as {}'.format(current_user.username), "access_token": access_token})
        else:
            return response_with(resp.UNAUTHORIZED_401)
      except Exception as e:
        return response_with(resp.INVALID_INPUT_422)

现在,让我们创建一个端点来验证电子邮件令牌。

我们将从导入 user.py 中最近创建的方法开始

from api.utils.token import generate_verification_token, confirm_verification_token

接下来,在用户注册方法的正下方添加下面的 GET 端点来处理电子邮件验证。

@user_routes.route('/confirm/<token>', methods=['GET'])
def verify_email(token):
    try:
        email = confirm_verification_token(token)
    except:
        return response_with(resp.SERVER_ERROR_401)
    user = User.query.filter_by(email=email).first_or_404()
    if user.isVerified:
        return response_with(resp. INVALID_INPUT_422)
    else:
        user.isVerified = True
        db.session.add(user)
        db.session.commit()
        return response_with(resp.SUCCESS_200, value={'message': 'E-mail verified, you can proceed to login now.'})

下一步是更新用户注册方法,以生成令牌并将电子邮件发送到指定的地址进行验证,因此这里我们将从在我们的实用程序中创建一个电子邮件实用程序来发送电子邮件开始。

为了做到这一点,我们需要一个 flask-mail 库;让我们从安装相同的开始。确保您仍然在虚拟环境中,使用下面一行在您的终端中安装 flask-mail。

(venv) $ pip install Flask-Mail

安装完成后,让我们启动并配置 flask-mail。在 config.py 中添加以下变量来配置邮件。

    MAIL_DEFAULT_SENDER= 'your_email_address'
    MAIL_SERVER= 'email_providers_smtp_address'
    MAIL_PORT= <mail_server_port>
    MAIL_USERNAME= 'your_email_address'
    MAIL_PASSWORD= 'your_email_password'
    MAIL_USE_TLS= False
    MAIL_USE_SSL= True

接下来在 utils 中创建 email.py 并添加以下代码。

from flask_mail import Message,Mail
from flask import current_app
mail = Mail()

接下来,让我们在 main.py 中导入邮件,并使用 app config 启动它。

from api.utils.email import mail

将它添加到 main.py 中的其他导入中,然后在 create_app 中启动 JWTManager 的位置的正下方,添加以下代码。

    mail.init_app(app)

现在我们的邮件对象应该用 app config 初始化;接下来在 email.py 中,我们来写一个发送邮件的方法。

在 email.py 中添加以下代码,以创建一个 send_email 方法,该方法将接收发件人的地址、主题和要发送的邮件模板。

def send_email(to, subject, template):
    msg = Message(
        subject,
        recipients=[to],
        html=template,
        sender=current_app.config['MAIL_DEFAULT_SENDER']
    )
    mail.send(msg)

因此,这就是我们为了发送验证邮件所需要做的全部工作;让我们返回到 users.py 并更新用户注册方法来合并这些更改。

让我们首先使用下面的代码行在 users.py 中导入 send_email、url_for 和 render_template_string 方法。

from api.utils.email import send_email
from flask import url_for, render_template_string

更新 users.py 中 create_user()方法的以下代码,就在 return 函数之前。

    try:
        data = request.get_json()
        if(User.find_by_email(data['email']) is not None or User.find_by_username(data['username']) is not None):
            return response_with(resp.INVALID_INPUT_422)
        data['password'] = User.generate_hash(data['password'])
        user_schmea = UserSchema()
        user, error = user_schmea.load(data)
        token = generate_verification_token(data['email'])
        verification_email = url_for('user_routes.verify_email', token=token, _external=True)
        html = render_template_string("<p>Welcome! Thanks for signing up. Please follow this link to activate your account:</p> <p><a href='{{ verification_email }}'>{{ verification_email }}</a></p> <br> <p>Thanks!</p>", verification_email=verification_email)
        subject = "Please Verify your email"
        send_email(user.email, subject, html)
        result = user_schmea.dump(user.create()).data
        return response_with(resp.SUCCESS_201)
    except Exception as e:
        print e
        return response_with(resp.INVALID_INPUT_422)

在这里,我们提供电子邮件来生成 _ 验证 _ 令牌,并获得令牌作为回报。接下来,我们使用 Flask 的 url_for,使用我们刚刚创建的验证路由和令牌来生成验证 url。之后,我们使用 Jinja2 的 render_template_string 呈现 HTML 模板,其中我们提供 HTML 字符串和验证变量,然后我们将所有用户提供的电子邮件、主题和 HTML 提供给 send_email 方法,以发送验证电子邮件。

这就是我们设置电子邮件验证所需的全部内容。让我们开始测试注册、登录和验证路径,检查是否一切正常。

让我们从注册端点开始;打开你的邮递员,请求 POST/users API;但是,在 JSON 主体中,添加一个有效的电子邮件地址。

{
      "username" : "kunalrelan",
      "password" : "helloworld",
      "email" : "kunal.relan@hotmail.com"
 }

我们将在请求数据中使用以下 JSON 并访问端点;响应应该与前面类似;但是,您应该会从您在 JSON 数据中使用令牌指定的电子邮件上配置的邮件地址收到一封验证电子邮件(图 4-1 )。

img/479840_1_En_4_Fig1_HTML.jpg

图 4-1

用户注册 API

接下来,让我们检查电子邮件收件箱,以检查电子邮件是否到达并验证用户。

img/479840_1_En_4_Fig2_HTML.jpg

图 4-2。

正如您在图 4-2 中看到的,验证电子邮件到达时带有验证用户帐户的链接。在我们激活用户帐户之前,让我们尝试使用用户凭证登录,以检查电子邮件验证是否正常工作(图 4-3 )。

img/479840_1_En_4_Fig3_HTML.jpg

图 4-3

未经验证的用户登录

如图 4-4 所示,用户未经验证,因此无法登录。现在让我们打开电子邮件中提供的链接来验证用户,然后允许用户登录并获得 JWT 令牌。

img/479840_1_En_4_Fig4_HTML.jpg

图 4-4

用户电子邮件验证

一旦用户通过验证,让我们再次尝试登录,现在我们应该能够登录并获得 JWT 令牌。

img/479840_1_En_4_Fig5_HTML.jpg

图 4-5

用户验证后登录

正如您在图 4-5 中看到的,我们现在再次能够在验证电子邮件地址后登录到帐户。

这就是本节的内容。我们已经成功地实现了用户电子邮件验证,这里我们所做的只是电子邮件验证的一个用例;有很多方法可以使用电子邮件验证。在许多应用中,用户甚至可以在电子邮件验证之前登录;但是,对于未经验证的用户,某些功能是禁用的,可以通过我们在登录端点中所做的更改来复制这些功能。在下一节中,我们将实现文件上传和处理。

文件上传

文件上传是 REST APIs 中的另一个常见用例。在本节中,我们将实现作者模型的头像上传和访问头像的端点。这里的想法很简单;我们将更新作者模型以存储头像 URL,为登录用户创建另一个端点以使用作者 ID 为作者上传头像,将文件保存在文件系统中,并创建另一个端点以处理静态图像文件。

在我们开始开发这个特性之前,让我们再谈一谈在 Flask 中处理文件上传的问题。这里,我们将使用 multipart/form-data 内容类型,它向客户端指示请求资源的媒体类型,并使用 request.files。我们还将定义一组允许的文件扩展名,因为除了要上传的图像之外,我们不需要任何其他文件类型,否则会导致很大的安全漏洞。然后,我们将使用 werkzeug.secure_filename()对上传文件的名称进行转义,这遵循了“永远不要相信用户输入”的原则,因此文件名可能包含恶意代码,从而导致安全漏洞被利用。因此,该方法将对文件名中的特殊字符进行转义。

首先,让我们更新作者模型,添加头像字段。因此,在模型中打开 authors.py,并在模型声明中,在 Author 类中添加以下行

    avatar = db.Column(db.String(20), nullable=True)

AuthorSchema 类中的下面一行

    avatar = fields.String(dump_only=True)

之后,在/src 中创建一个新文件夹,命名为 images,并在 app config 中添加上传文件夹配置,我们稍后将使用它来保存和获取上传的头像。

因此,在 config 中打开 config.py 并添加以下参数。

    UPLOAD_FOLDER= 'images'

现在,我们将从 Flask 中导入 werkzeug.secure_filename()和 url_for,我们将在将要创建的端点中需要它们,因此在 routes 中 authors.py 的其他导入下面添加以下代码行。

from werkzeug.utils import secure_filename

接下来,我们从 Flask 导入蓝图和请求,添加 url_for,如下所示。

from flask import Blueprint, request, url_for, current_app

在导入之后,声明 allowed_extensions,它将包含一组允许的文件扩展名。

allowed_extensions = set(['image/jpeg', 'image/png', 'jpeg'])

一旦我们有了集合,让我们创建一个方法来检查上传文件的扩展名是否是图像的扩展名。

将以下代码添加到 allowed_extensions 的正下方。

def allowed_file(filename):
       return filetype in allowed_extensions

The above function will take the filename from the file and check if the extension is valid and return.

现在添加以下端点以添加头像上传端点。

@author_routes.route('/avatar/<int:author_id>', methods=['POST'])
@jwt_required
def upsert_author_avatar(author_id):
    try:
        file = request.files['avatar']
        get_author = Author.query.get_or_404(author_id)
        if file and allowed_file(file.content_type):
            filename = secure_filename(file.filename)
            file.save(os.path.join(current_app.config['UPLOAD_FOLDER'], filename))
        get_author.avatar = url_for('uploaded_file', filename=filename, _external=True)
        db.session.add(get_author)
        db.session.commit()
        author_schema = AuthorSchema()
        author, error = author_schema.dump(get_author)
        return response_with(resp.SUCCESS_200, value={"author": author})
    except Exception as e:
        print e
        return response_with(resp.INVALID_INPUT_422)

在下面的代码中,我们在 request.files 中查找 avatar 字段,然后用提供的用户 ID 查找用户。一旦我们有了它,我们将检查一个文件是否被上传,然后使用我们刚刚导入的 secure_filename 函数对文件名进行转义。然后,我们将使用 file.save 方法,并通过连接配置中的 UPLOAD_FOLDER 和文件名来提供路径,从而将文件保存在 images 文件夹中。现在,一旦保存了文件,我们将使用 url_for 方法创建一个用于访问上传文件的 url,为此,我们将使用 uploaded_file 方法创建一个路由,该方法接受一个文件名,并从我们接下来将创建的已配置的上传文件夹中提供该文件名。完成后,我们将更新作者模型,并用上传的头像的 URL 更新头像字段。

接下来转到 main.py,并在 create_app 函数中的路由蓝图声明之后添加以下路由。

 @app.route('/avatar/<filename>')
  def uploaded_file(filename):
      return send_from_directory(app.config['UPLOAD_FOLDER'],filename)

因此,这个函数将接受文件名,并在响应中从配置的 UPLOAD_FOLDER 返回文件。

这就是文件上传,现在我们应该能够上传一个作者的头像并取回它。让我们回到邮差那里试一试。

现在用表单数据请求更新头像端点,指定关键头像,选择你想上传的图片,然后发送。我们将得到 200 个成功响应,用户对象作为响应;现在注意带有文件链接的头像字段(图 4-6 )。

img/479840_1_En_4_Fig6_HTML.jpg

图 4-6

作者头像上传端点

接下来,单击头像链接获取您刚刚创建的图像,检查它是否存在。

img/479840_1_En_4_Fig7_HTML.jpg

图 4-7

获取化身端点

正如您在图 4-7 中看到的,我们能够使用我们创建的路线获取图像。接下来,让我们尝试上传一个 HTML 文件,检查允许的扩展名检查是否工作正常。为此,只需创建一个包含任何文本的 HTML 文件,或者使用任何已有的 HTML 文件并尝试上传。

现在,正如您在图 4-8 中看到的,我们在尝试上传该端点上不允许的 HTML 文件时出错,确保我们的扩展检查工作正常。

img/479840_1_En_4_Fig8_HTML.jpg

图 4-8

上传具有无效文件类型的头像端点

美国石油学会文件

API 开发的过程并不仅仅在编程之后就结束了。由于 REST APIs 被各种各样的客户端使用,因此也被其他开发人员使用,这些开发人员要么通过 REST 客户端直接访问它们,要么与某种 REST 客户端集成,API 文档提供了一种理解 REST 端点功能的简单方法,这使得 API 文档成为开发基于 REST 的应用的重要部分。

在本节中,我们将讨论 API 文档、OpenAPI 规范和 Swagger 的基础知识,使用 OpenAPI 规范生成 API 文档,发布 API 文档,以及使用 Swagger UI 测试 API。

API 文档的构建块

在 REST API 参考文档中,文档基于五个部分,即:

  1. 资源描述:如前所述,资源指的是从 API 返回的信息;在本书的上下文中,作者、书籍和用户都是资源。资源描述通常很简短,只有一到两句话。每个资源都有可以访问的特定动词。

  2. 端点和方法:端点定义如何访问所提供的资源,方法指示资源上允许的交互或动词,例如 GET、POST、PUT、DELETE 等。任何资源都有路径和方法不同的相关端点,但都将围绕同一资源。

  3. 参数:参数是端点的可变部分,它指定了您正在处理的数据。

  4. 请求示例:请求示例包括一个示例请求,其中包含必需字段、可选字段及其示例值。请求示例通常应该尽可能丰富,并包含所有可接受的字段。

  5. 响应示例和模式:顾名思义,响应示例包含一个根据请求的 API 响应的详细示例。另一方面,模式定义了如何格式化和标记响应。响应的描述通常称为响应模式,它是描述所有可能的参数和响应类型的复杂文档。

OpenAPI 规范

OpenAPI 规范(OAS)为 REST API 定义了一个标准的、与语言无关的接口,允许人类和计算机在不查看源代码或进行网络检查的情况下理解应用的功能,使 API 消费者在不知道实现逻辑的情况下理解应用的工作。

OpenAPI 定义可以有多个用例,包括显示 API 的文档生成、测试工具等等。

对于本书的上下文,我们将使用 OpenAPI 规范和 Swagger UI 来生成和显示 API 参考文档。

OpenAPI 定义了一个标准集,用于描述 API 的各个部分;通过这样做,像 Swagger UI 这样的发布工具可以以编程方式解析信息,并使用定制的样式和交互功能来显示信息。OpenAPI 规范文档可以用 YAML (YAML 不是标记语言)或 JSON 表示,但最终规范文件将是一个 JSON 文档。由于 YAML 可读性更强,格式更通用,我们将使用 YAML 创建 OpenAPI 规范文档,然后使用 Swagger UI 发布。

因此,在我们开始为我们的端点编写 OpenAPI 规范之前,让我们了解一下 OpenAPI 规范的基础。OpenAPI 规范文档具有三个必需的组成部分,即定义 openapi 规范的语义版本号的 OpenAPI,这对于用户理解文档是如何格式化的以及对于解析工具相应地解析文档是必不可少的;Info,包含 API 的元数据,其本质上具有作为必填字段的标题和 API 版本,以及附加字段,如描述、法律信息和联系人;和路径,其中包含有关端点及其可用操作的信息。

Paths 对象是 OpenAPI 规范文档的核心,它包含了可用端点的详细信息,也就是我们在上一节中讨论的五个组件。

OpenAPI 规范 3.0 是最新版本;它的旧版本是 Swagger specification 2.0,后来被更新为 OpenAPI spec。对于本书,我们将使用 Swagger 2.0 规范并定义 API 文档;为此,您可以使用 Swagger 中的 Inspector,或者使用构建时 Swagger 生成工具来生成它们。让我们看看这两种方法。我们将从检查 Swagger Inspector 开始,然后继续构建时间生成器,我们将把它集成到我们的应用中。

首先,在您的浏览器窗口(Chrome 浏览器)中打开 https://inspector.swagger.io ,用您喜欢的调制解调器登录/注册(图 4-9 )。

img/479840_1_En_4_Fig9_HTML.jpg

图 4-9

神气活现的检查员

一旦你登录,你就可以使用 Swagger Inspector 的所有功能;接下来,我们将需要使用他们的 REST 客户端访问我们的 API 资源,一旦我们这样做了,它将出现在历史中,我们将能够将其转换为 OpenAPI 规范文件,但在我们可以访问我们在本地服务器上运行的应用之前,我们需要添加 Swagger Inspector Chrome 扩展,为此,使用 https://chrome.google.com/webstore/detail/swagger-inspector-extensi/biemppheiopfggogojnfpkngdkchelik 添加扩展。一旦您安装了扩展,Swagger Inspector 将能够在本地服务器上运行请求。

完成后,让我们从访问创建用户端点开始。因此,继续操作,类似于我们在 Postman 中所做的,添加 URL 并选择 POST 方法,在 body 中添加 JSON body 数据并单击 send(图 4-10 )。

img/479840_1_En_4_Fig10_HTML.jpg

图 4-10

创建用户端点 Swagger 检查器

与 Postman 类似,您应该能够在响应窗口中检查 API 响应,如上图所示。接下来,您可以验证电子邮件,然后访问登录端点(图 4-11 )。

img/479840_1_En_4_Fig11_HTML.jpg

图 4-11

登录端点

一旦您请求了您希望 API 文档生成的所有端点,只需单击 History 选项卡并选择您希望规范文档生成的端点并固定它们。一旦您锁定了它们,单击 Create API Definition 按钮旁边的小箭头并选择 OAS 2.0 以使用规范的 2.0 版本(图 4-12 )。

img/479840_1_En_4_Fig12_HTML.jpg

图 4-12

固定请求

现在,单击创建定义,一旦完成,将打开一个弹出窗口,其中包含打开 SwaggerHub 的链接,您可以在其中导入 OpenAPI 规范并查看 API 文档(图 4-13 )。

img/479840_1_En_4_Fig13_HTML.jpg

图 4-13

OpenAPI 规格层代

现在点击链接,SwaggerHub 将打开,要求您输入 API 的标题和版本。在这里,我们将添加 Author DB 并让版本默认为 0.1,使可见性为 private,并单击导入 API,如图 4-14 所示。

img/479840_1_En_4_Fig14_HTML.jpg

图 4-14

从检查器导入 OpenAPI

一旦完成,你应该能够检查你的 API 的文档,如下图所示。在本教程中,我只选择了两个端点,但是您可以在这里记录您的所有端点。

在图 4-15 中你可以看到选择的服务器是我们本地服务器的地址,然后我们有了我们选择的端点。

img/479840_1_En_4_Fig15_HTML.jpg

图 4-15

SwaggerHub

接下来,让我们通过点击顶栏上的纸张图标查看 API 文档,如图 4-16 和 4-17 所示。

img/479840_1_En_4_Fig17_HTML.jpg

图 4-17

查看文档页面

img/479840_1_En_4_Fig16_HTML.jpg

图 4-16

查看文档

一旦页面加载完毕,您就进入了 API 文档的交互模式,在这里您可以看到端点、参数、示例请求和示例响应。接下来点击“试用”,请求正文窗口将变为可编辑状态,您可以在其中填写请求正文数据,如图 4-18 所示。在它下面,你还可以看到响应和它们的格式。

img/479840_1_En_4_Fig18_HTML.jpg

图 4-18

API 请求模式

因此,请继续编辑电子邮件和密码,然后单击“执行”请求访问 API。

接下来,您还可以导出规范文档的 YAML/JSON 版本,以便与您的 Swagger UI 版本一起使用。

接下来,我们将使用我们自己安装的 Swagger UI 和构建时规范来集成 API 文档。

同样,我们将使用 flask_swagger 和 flask_swagger_ui 扩展;让我们继续使用 PIP 安装它们。

(venv)$ pip install flask_swagger flask_swagger_ui

安装完成后,让我们将它集成到我们的应用中;为此,打开 main.py 并使用以下代码行导入这两个库。

from flask_swagger import swagger
from flask_swagger_ui import get_swaggerui_blueprint

我们将在/api/docs 端点上提供 Swagger UI。

现在我们将使用 Swagger 2.0 创建一个端点来服务我们定义的 API 规范

因此,在 errorhandler 函数下面添加以下代码,我们将在其中定义/api/spec route,并启动我们的 Swagger 定义,然后返回生成的 JSON 文件。

@app.route("/api/spec")
    def spec():
        swag = swagger(app, prefix='/api')
        swag['info']['base'] = "http://localhost:5000"
        swag['info']['version'] = "1.0"
        swag['info']['title'] = "Flask Author DB"
        return jsonify(swag)

现在,我们将启动 flask_swagger_ui 来获取这个 JSON 文件,并使用它呈现 Swagger UI。在新路径下添加以下代码,以启动我们刚刚从 flask_swagger_ui 导入的 get_swagger_blueprint 方法,这里我们将在配置变量中提供 docs 路径、JSON 文件路由器和 app_name,然后注册 blueprint。

    swaggerui_blueprint = get_swaggerui_blueprint('/api/docs', '/api/spec', config={'app_name': "Flask Author DB"})
    app.register_blueprint(swaggerui_blueprint, url_prefix=SWAGGER_URL)

现在当你尝试访问htttp://localhost:5000/api/docs时,你应该能看到 Swagger UI(图 4-19 )。

img/479840_1_En_4_Fig19_HTML.jpg

图 4-19

Swagger UI

在前面的 URL 栏中,您还可以提供从 SwaggerHub 导出的 JSON 文件的 URL,以探索您的 API。

构建时文档

接下来,我们将使用构建时文档记录 API,并生成 JSON 文档文件;然而,在描述端点时,我们将使用 YAML。

Flask Swagger 将自动从方法定义中选取 YAML 文档,方法定义中使用' ' " " '后跟描述。我们将在创建用户端点中使用一个示例定义来学习它。因此,在 users.py routes 中的 def create_user()之后添加以下行。

    """
    Create user endpoint
    ---
    parameters:
        - in: body
          name: body
          schema:
            id: UserSignup
            required:
                - username
                - password
                - email
            properties:
                username:
                    type: string
                    description: Unique username of the user
                    default: "Johndoe"
                password:
                    type: string
                    description: Password of the user
                    default: "somethingstrong"
                email:
                    type: string
                    description: email of the user
                    default: "someemail@provider.com"
    responses:
            201:
                description: User successfully created
                schema:
                  id: UserSignUpSchema
                  properties:
                    code:
                      type: string
            422:
                description: Invalid input arguments

                schema:
                    id: invalidInput
                    properties:
                        code:
                            type: string
                        message:
                            type: string
    """

这里我们使用 YAML 来定义参数和响应,正如你在前面的例子中看到的;我们定义参数的种类,在我们的例子中,它是一个主体参数,然后我们用样本数据和字段名定义所需参数的模式。在响应中,我们定义了不同类型的预期响应及其模式(图 4-20 )。

img/479840_1_En_4_Fig20_HTML.jpg

图 4-20

构建时文档生成

现在,如果您重新加载您的应用并访问 Swagger UI,您应该能够看到您创建的用户端点并使用 Swagger UI 访问它。

请注意描述、参数和响应是如何被解释并放置在 Swagger UI 中的。

接下来将它添加到登录方法中,为登录端点生成文档。

    """
    User Login
    ---
    parameters:
        - in: body
          name: body
          schema:
            id: UserLogin
            required:
                - password
                - email
            properties:
                email:
                    type: string
                    description: email of the user
                    default: "someemail@provider.com"
                password:
                    type: string
                    description: Password of the user
                    default: "somethingstrong"
    responses:
            200:
                description: User successfully logged In
                schema:
                  id: UserLoggedIn
                  properties:
                    code:
                      type: string
                    message:
                      type: string
                    value:
                      schema:
                        id: UserToken
                        properties:
                            access_token:
                                type: string
                            code:
                                type: string
                            message:
                                type: string
            401:
                description: Invalid input arguments
                schema:
                    id: invalidInput
                    properties:
                        code:
                            type: string
                        message:
                            type: string
    """

接下来,我们将转到 authors.py 路由文件,并为 Create author 创建 doc,如果您还记得,此路由需要用户登录,在这里我们将添加一个额外的 header 参数,该参数将接受授权头。

"""
    Create author endpoint
    ---
    parameters:
        - in: body
          name: body
          schema:
            id: Author
            required:
                - first_name
                - last_name
                - books
            properties:
                first_name:
                    type: string
                    description: First name of the author
                    default: "John"
                last_name:
                    type: string
                    description: Last name of the author
                    default: "Doe"
        - in: header
          name: authorization
          type: string
          required: true
    security:
        - Bearer: []
    responses:
            200:
                description: Author successfully created

                schema:
                  id: AuthorCreated
                  properties:
                    code:
                      type: string
                    message:
                      type: string
                    value:
                      schema:
                        id: AuthorFull
                        properties:
                            first_name:
                                type: string
                            last_name:
                                type: string
                            books:
                                type: array
                                items:
                                    schema:
                                        id: BookSchema
            422:
                description: Invalid input arguments
                schema:
                    id: invalidInput
                    properties:
                        code:
                            type: string
                        message:
                            type: string
    """

接下来,为 Upsert 作者头像端点添加以下行;注意,在这种情况下,我们将为 author ID 添加一个参数,作为 path 中的一个变量。

    """
    Upsert author avatar
    ---
    parameters:
        - in: body
          name: body
          schema:
            id: Author
            required:
                - avatar
            properties:
                avatar:
                    type: file
                    description: Image file
        - name: author_id
          in: path
          description: ID of the author
          required: true
          schema:
            type: integer
    responses:
            200:
                description: Author avatar successfully upserted
                schema:
                  id: AuthorCreated
                  properties:
                    code:
                      type: string
                    message:
                      type: string
                    value:
                      schema:
                        id: AuthorFull
                        properties:
                            first_name:
                                type: string

                            last_name:
                                type: string
                            books:
                                type: array
                                items:
                                    schema:
                                        id: BookSchema
            422:
                description: Invalid input arguments
                schema:
                    id: invalidInput
                    properties:
                        code:
                            type: string
                        message:
                            type: string
    """

现在您可以重新加载您的 Swagger UI,您应该能够看到所有记录的端点(图 4-21 )。

img/479840_1_En_4_Fig21_HTML.jpg

图 4-21

重新加载 Swagger UI 以查看端点

结论

对于本章,我们将只为给定的端点创建文档,您可以使用相同的方法在它的基础上构建,这将帮助您为 REST 端点创建完整的文档。在下一章,我们将讨论测试我们的 REST 端点,涵盖单元测试、模拟、代码覆盖率等主题。

五、Flask 测试

未经检验的东西坏了。

这段引文来源不明;然而,这并不完全正确,但大部分是正确的。未经测试的应用总是不安全的赌注。虽然开发人员对他们的工作很有信心,但在现实世界中,事情会有所不同;因此,从头到尾测试应用总是一个好主意。未经测试的应用也很难改进现有的代码。然而,有了自动化测试,总是很容易做出改变,并立即知道什么时候出了问题。因此,测试不仅可以确保应用是否按照预期的方式运行,还可以促进持续的开发。

本章涵盖了 REST APIs 的自动化单元测试,在我们进入实际的实现之前,我们将了解什么是单元测试以及背后的原理。

介绍

大多数软件开发人员通常已经熟悉了“单元测试”这个术语,但是对于那些不熟悉的人来说,单元测试围绕着将大量代码分解成单独的单元进行独立测试的概念。所以通常在这种情况下,一个更大的代码集是软件,而单独的组件是被隔离测试的单元。因此,在我们的例子中,一个单独的 API 请求是一个需要测试的单元。单元测试是软件开发的第一级,通常由软件开发人员完成。

让我们来看看单元测试的一些好处:

  1. 单元测试是对非常窄的代码块的简单测试,作为更大范围的应用测试的构建块。

  2. 由于范围狭窄,单元测试是最容易编写和实现的。

  3. 单元测试增加了修改代码的信心,如果实现正确的话,单元测试也是第一个失败点,它会提示开发人员部分逻辑破坏了应用。

  4. 编写单元测试使开发过程更快,因为它使开发人员做更少的模糊测试,并帮助他们更快地发现错误。

  5. 在开发过程中使用单元测试来捕捉和修复 bug 比在产品中部署代码后进行更容易,也更便宜。

  6. 与手动模糊测试相比,单元测试也是一种更可靠的测试方式。

设置单元测试

因此,在这一节中,我们将直接进入行动,开始实现测试;同样,我们将使用一个名为 unittest2 的库,它是 Python 的原始单元测试框架 unittest 的扩展。

让我们先安装库。

(venv)$ pip install unittest2

这将为我们安装 unittest2 接下来,我们将设置一个基本的测试类,并将其导入到所有的测试文件中。顾名思义,这个基类将为测试建立基础并启动测试客户机。因此,在 utils 文件夹中创建一个名为 test_base.py 的文件。

现在让我们配置我们的测试环境,打开 config.py 并添加下面的代码来添加测试配置。

class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_ECHO = False
    JWT_SECRET_KEY = 'JWT-SECRET'
    SECRET_KEY= 'SECRET-KEY'
    SECURITY_PASSWORD_SALT= 'PASSWORD-SALT'
    MAIL_DEFAULT_SENDER= '
    MAIL_SERVER= 'smtp.gmail.com'
    MAIL_PORT= 465
    MAIL_USERNAME= "
    MAIL_PASSWORD= "
    MAIL_USE_TLS= False
    MAIL_USE_SSL= True
    UPLOAD_FOLDER= 'images'

注意,我们不会在这里配置 SQLAlchemy URI,我们将在 test_base.py 中进行配置

接下来,添加以下代码行,以便在 test_base.py 中导入所需的依赖项

import unittest2 as unittest
from main import create_app
from api.utils.database import db
from api.config.config import TestingConfig
import tempfile

接下来,使用以下代码添加 BaseTestCase 类。

class BaseTestCase(unittest.TestCase):
    """A base test case"""
    def setUp(self):
        app = create_app(TestingConfig)
        self.test_db_file = tempfile.mkstemp()[1]
        app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + self.test_db_file
        with app.app_context():
            db.create_all()
        app.app_context().push()
        self.app = app.test_client()

    def tearDown(self):
        db.session.close_all()
        db.drop_all()

在这里,我们使用 tempfile 动态创建 SQLAlchemy sqlite 数据库。

我们之前刚刚创建的称为存根,它是一个模块,作为被调用模块的临时替代,提供与实际产品相同的输出。

因此,前面的方法将在每个测试运行之前运行,并产生一个新的测试客户端。我们将在我们创建的所有测试中导入这个方法。以 test_ prefix 开头的类中的所有方法都可以识别测试。在这里,我们每次都会有一个唯一的数据库 URL,因为我们已经配置了 tempfile,我们将用时间戳作为它的后缀,然后我们在 app config 中配置 TESTING= True,这将禁用错误捕获以实现更好的测试,最后我们运行 db.create_all()为应用创建 db 表。

接下来,我们定义了另一个方法 tearDown,它将删除当前的数据库文件,并为每个测试使用一个新的数据库文件。

单元测试用户端点

现在我们将开始编写测试,第一步是在 api 目录中创建一个名为 tests 的文件夹,在这里我们将创建所有的测试文件。因此,继续创建 tests 文件夹,并创建我们的第一个测试文件 test_users.py。

现在在 test_users.py 中添加以下导入

import json
from api.utils.test_base import BaseTestCase
from api.models.users import User
from datetime import datetime
import unittest2 as unittest
from api.utils.token import generate_verification_token, confirm_verification_token

一旦完成,我们将定义另一种方法来使用 SQLAlchemy 模型创建用户,以便于测试。

接下来将它添加到文件中。

def create_users():
    user1 = User(email="kunal.relan12@gmail.com", username="kunalrelan12",
    password=User.generate_hash('helloworld'), isVerified=True).create()
    user2 = User(email="kunal.relan123@gmail.com", username="kunalrelan125",
    password=User.generate_hash('helloworld')).create()

现在我们有了导入和创建用户的方法;接下来,我们将定义 TestUsers 类来保存我们所有的测试。

class TestUsers(BaseTestCase):
    def setUp(self):
        super(TestUsers, self).setUp()
        create_users()

if __name__ == '__main__':
    unittest.main()

将这段代码添加到文件中,该文件将导入我们的基本测试类,并设置测试客户机和调用 create_users()方法来创建用户。注意,在 create_users()方法中,我们创建了一个已验证的用户和一个未验证的用户,这样我们就可以覆盖所有的测试用例。现在我们可以开始编写单元测试了。在 TestUsers()类中添加以下代码。

我们将从测试登录端点开始,因为我们刚刚创建了一个经过验证的用户,所以应该允许我们使用一组有效的凭证登录。

    def test_login_user(self):
        user = {
          "email" : "kunal.relan12@gmail.com",
          "password" : "helloworld"
        }
        response = self.app.post(
            '/api/users/login',
            data=json.dumps(user),
            content_type='application/json'
        )
        data = json.loads(response.data)
        self.assertEqual(200, response.status_code)
        self.assertTrue('access_token' in data)

将下面的代码添加到 TestUsers 类中,我们应该有了我们的第一个单元测试,其中我们创建了一个用户对象并将用户发送到登录端点。一旦我们收到响应,我们将使用断言来检查我们是否在响应中获得了预期的状态代码和 access_token。断言是一个布尔表达式,除非有错误或者条件语句不匹配,否则它将为真。单元测试提供了一个断言方法列表,我们可以用它来验证我们的测试。

但是 assertEqual()、assertNotEqual()、assertTrue()和 assertNotTrue()涵盖了大部分。

这里,assertEqual()和 assertNotEqual()匹配值,assertTrue()和 assertNotTrue()检查传递的变量的值是否为布尔值。

现在让我们运行我们的第一个测试,所以只需打开您的终端并激活您的虚拟环境。

在您的终端中运行以下命令来运行测试。

(venv)$ python -m unittest discover api/tests

前面的命令将运行测试目录中的所有测试文件;由于我们现在只有一个测试,我们可以在下图中看到我们的测试结果。

img/479840_1_En_5_Fig1_HTML.jpg

图 5-1

运行单元测试

这是运行单元测试的一种方式,在我们进一步编写更多测试之前,我想向您介绍 unittest 库的另一个扩展,称为 nose,它使测试更容易,所以让我们继续安装 nose。

使用以下代码安装 nose。

(venv)$ pip install nose

现在,一旦我们有了 nose,让我们看看如何使用 nose 来运行我们的测试,因为接下来我们将使用 nose 来运行我们所有的测试。

默认情况下,nose 会使用(?:\b|_)[Tt]est 正则表达式;但是,您也可以指定要测试的文件名。让我们使用 nose 再次运行相同的测试。

(venv)$ nosetests

img/479840_1_En_5_Fig2_HTML.jpg

图 5-2

用 nose 运行单元测试

正如您在前面的图中看到的,我们可以使用一个简单的 nosetest 命令来运行我们的测试。接下来,让我们再次为用户模型编写单元测试。

因此,我们的目标是涵盖所有场景,并检查每个场景中的应用行为;接下来,我们将在用户未被验证和提交错误凭证时测试登录 API。

为各个测试添加以下代码。

    def test_login_user_wrong_credentials(self):
        user = {
          "email" : "kunal.relan12@gmail.com",
          "password" : "helloworld12"
        }
        response = self.app.post(
            '/api/users/login',
            data=json.dumps(user),
            content_type='application/json'
        )
        data = json.loads(response.data)
        self.assertEqual(401, response.status_code)

    def test_login_unverified_user(self):
        user = {
          "email" : "kunal.relan123@gmail.com",
          "password" : "helloworld"
        }
        response = self.app.post(
            '/api/users/login',
            data=json.dumps(user),
            content_type='application/json'
        )
        data = json.loads(response.data)
        self.assertEqual(400, response.status_code)

在前面的代码中,在 test _ log in _ user _ error _ credentials 方法中,我们检查响应中的 401 状态代码,因为我们提供了错误的凭据;在 test_login_unverified_user()方法中,我们尝试使用未经验证的用户登录,这将引发 400 错误。

接下来,让我们测试 create_user 端点,首先创建一个测试,用正确的字段创建一个用户,以创建一个新用户。

    def test_create_user(self):
        user = {
          "username" : "kunalrelan2",
          "password" : "helloworld",
          "email" : "kunal.relan12@hotmail.com"
        }

        response = self.app.post(
            '/api/users/',
            data=json.dumps(user),
            content_type='application/json'
        )
        data = json.loads(response.data)
        self.assertEqual(201, response.status_code)
        self.assertTrue('success' in data['code'])

前面的代码将使用新的用户对象请求创建用户端点,并且应该能够这样做,并使用 201 状态代码进行响应。

接下来,我们将添加另一个测试,此时 username 没有提供给 Create user 端点,在这种情况下,我们将得到 422 响应。这是代码。

    def test_create_user_without_username(self):
        user = {
          "password" : "helloworld",
          "email" : "kunal.relan12@hotmail.com"
        }

        response = self.app.post(
            '/api/users/',
            data=json.dumps(user),
            content_type='application/json'
        )
        data = json.loads(response.data)
        self.assertEqual(422, response.status_code)

现在我们可以继续测试我们的确认电子邮件端点,这里我们将首先创建一个包含有效电子邮件的单元测试,因此您会注意到我们在 create_users()方法中创建了一个未经验证的用户,这里我们将首先生成一个验证令牌,因为我们没有使用单元测试读取电子邮件,然后将令牌发送到确认电子邮件端点。

    def test_confirm_email(self):
        token = generate_verification_token('kunal.relan123@gmail.com')

        response = self.app.get(
            '/api/users/confirm/'+token
        )
        data = json.loads(response.data)
        self.assertEqual(200, response.status_code)
        self.assertTrue('success' in data['code'])

接下来,我们将使用已经验证的用户的电子邮件编写另一个测试,以测试我们是否在响应状态代码中得到 422。

    def test_confirm_email_for_verified_user(self):
        token = generate_verification_token('kunal.relan12@gmail.com')

        response = self.app.get(
            '/api/users/confirm/'+token
        )
        data = json.loads(response.data)
        self.assertEqual(422, response.status_code)

这个端点的最后一个问题是,我们将提供一个不正确的电子邮件,并应该得到一个 404 响应状态代码。

    def test_confirm_email_with_incorrect_email(self):
        token = generate_verification_token('kunal.relan43@gmail.com')

        response = self.app.get(
            '/api/users/confirm/'+token
        )
        data = json.loads(response.data)
        self.assertEqual(404, response.status_code)

一旦我们的测试就绪,就该对它们进行测试了,所以继续使用 nosetests 并运行测试。

img/479840_1_En_5_Fig3_HTML.jpg

图 5-3

test_users.py 上的 Nosetests

所以这些都是我们想用用户模型覆盖的测试;接下来我们可以继续讨论作者和书籍。

接下来,让我们创建 test_authors.py,我们将添加一些变化的依赖项,因此添加以下行来导入所需的依赖项。

import json
from api.utils.test_base import BaseTestCase
from api.models.authors import Author
from api.models.books import Book
from datetime import datetime
from flask_jwt_extended import create_access_token
import unittest2 as unittest
import io

接下来,我们将定义两个助手方法,即 create_authors 和 login,并为其添加以下代码。

def create_authors():
    author1 = Author(first_name="John", last_name="Doe").create()
    author2 = Author(first_name="Jane", last_name="Doe").create()

我们将使用前面定义的方法为测试创建两个作者,login 方法将生成一个登录令牌,并返回仅授权的路由。

def login():
    access_token = create_access_token(identity = 'kunal.relan@hotmail.com')
    return access_token

接下来让我们像前面一样定义我们的测试类并初始化它。

class TestAuthors(BaseTestCase):
    def setUp(self):
        super(TestAuthors, self).setUp()
        create_authors()

if __name__ == '__main__':
    unittest.main()

现在我们有了作者单元测试的基础,我们可以添加下面的测试用例,它们应该是不言自明的。

这里,我们将使用 POST author 端点创建一个新的作者,该端点带有我们使用 login 方法生成的 JWT 令牌,并期待 author 对象以 201 状态代码作为响应。

    def test_create_author(self):
        token = login()
        author = {
            'first_name': 'Johny',
            'last_name': 'Doee'
        }
        response = self.app.post(
            '/api/authors/',
            data=json.dumps(author),
            content_type='application/json',
            headers= { 'Authorization': 'Bearer '+token }
        )
        data = json.loads(response.data)
        self.assertEqual(201, response.status_code)
        self.assertTrue('author' in data)

这里我们将尝试创建一个带有 authorization 头的 author,它应该在响应状态代码中返回 401。

    def test_create_author_no_authorization(self):
        author = {
            'first_name': 'Johny',
            'last_name': 'Doee'
        }

        response = self.app.post(
            '/api/authors/',
            data=json.dumps(author),
            content_type='application/json',
        )
        data = json.loads(response.data)
        self.assertEqual(401, response.status_code)

在这个测试用例中,我们将尝试创建一个没有姓氏字段的作者,它应该会返回 422 状态代码。

    def test_create_author_no_name(self):
        token = login()
        author = {
            'first_name': 'Johny'
        }

        response = self.app.post(
            '/api/authors/',
            data=json.dumps(author),
            content_type='application/json',
            headers= { 'Authorization': 'Bearer '+token }
        )
        data = json.loads(response.data)
        self.assertEqual(422, response.status_code)

在这个例子中,我们将测试上传头像端点,并使用 io 创建一个临时图像文件,并将其作为多部分/表单数据发送以上传图像。

    def test_upload_avatar(self):
        token = login()
        response = self.app.post(
            '/api/authors/avatar/2',
            data=dict(avatar=(io.BytesIO(b'test'), 'test_file.jpg')),
            content_type='multipart/form-data',
            headers= { 'Authorization': 'Bearer '+ token }
        )
        self.assertEqual(200, response.status_code)

在这里,我们将通过提供一个 CSV 文件来测试上传头像,正如预期的那样,它不应该响应 200 状态代码。

    def test_upload_avatar_with_csv_file(self):
        token = login()
        response = self.app.post(
            '/api/authors/avatar/2',
            data=dict(file=(io.BytesIO(b'test'), 'test_file.csv)),
            content_type='multipart/form-data',
            headers= { 'Authorization': 'Bearer '+ token }
        )
        self.assertEqual(422, response.status_code)

在这个测试中,我们将使用 GET all authors 端点获取所有作者。

    def test_get_authors(self):
        response = self.app.get(
            '/api/authors/',
            content_type='application/json'
        )
        data = json.loads(response.data)
        self.assertEqual(200, response.status_code)
        self.assertTrue('authors' in data)

这里我们有一个通过 ID 端点获取作者的单元测试,它将返回 200 个响应状态代码和作者对象。

    def test_get_author_detail(self):
        response = self.app.get(
            '/api/authors/2',
            content_type='application/json'
            )
        data = json.loads(response.data)
        self.assertEqual(200, response.status_code)
        self.assertTrue('author' in data)

在这个测试中,我们将更新最近创建的 author 上的 author 对象,它也将在响应中返回 200 状态代码。

    def test_update_author(self):
        token = login()
        author = {
            'first_name': 'Joseph'
        }
        response = self.app.put(
            '/api/authors/2',
            data=json.dumps(author),
            content_type='application/json',
            headers= { 'Authorization': 'Bearer '+token }
        )
        self.assertEqual(200, response.status_code)

在这个测试中,我们将删除 author 对象,并期待 204 响应状态代码。

    def test_delete_author(self):
        token = login()
        response = self.app.delete(
            '/api/authors/2',
            headers= { 'Authorization': 'Bearer '+token }
        )
        self.assertEqual(204, response.status_code)

img/479840_1_En_5_Fig4_HTML.jpg

图 5-4

作者测试

所以现在你可以像上图一样运行 authors test,它应该像上图一样全部通过;接下来,我们将进行图书模型测试。

对于书籍模型测试,我们可以在同一个模块中修改作者测试并为书籍设置单元测试,所以让我们更新 create_authors 方法来创建一些书籍;继续用下面的代码更新这个方法。

def create_authors():
    author1 = Author(first_name="John", last_name="Doe").create()
    Book(title="Test Book 1", year=datetime(1976, 1, 1), author_id=author1.id).create()
    Book(title="Test Book 2", year=datetime(1992, 12, 1), author_id=author1.id).create()

    author2 = Author(first_name="Jane", last_name="Doe").create()
    Book(title="Test Book 3", year=datetime(1986, 1, 3), author_id=author2.id).create()
    Book(title="Test Book 4", year=datetime(1992, 12, 1), author_id=author2.id).create()

这是图书路线的单元测试。

    def test_create_book(self):
        token = login()
        author = {
            'title': 'Alice in wonderland',
            'year': 1982,
            'author_id': 2
        }

        response = self.app.post(
            '/api/books/',
            data=json.dumps(author),
            content_type='application/json',
            headers= { 'Authorization': 'Bearer '+token }
        )
        data = json.loads(response.data)
        self.assertEqual(201, response.status_code)
        self.assertTrue('book' in data)

    def test_create_book_no_author(self):
        token = login()
        author = {
            'title': 'Alice in wonderland',
            'year': 1982
        }

        response = self.app.post(

            '/api/books/',
            data=json.dumps(author),
            content_type='application/json',
            headers= { 'Authorization': 'Bearer '+token }
        )
        data = json.loads(response.data)
        self.assertEqual(422, response.status_code)

    def test_create_book_no_authorization(self):
        author = {
            'title': 'Alice in wonderland',
            'year': 1982,
            'author_id': 2
        }

        response = self.app.post(
            '/api/books/',
            data=json.dumps(author),
            content_type='application/json'

        )
        data = json.loads(response.data)
        self.assertEqual(401, response.status_code)

    def test_get_books(self):
        response = self.app.get(
            '/api/books/',
            content_type='application/json'
        )
        data = json.loads(response.data)
        self.assertEqual(200, response.status_code)
        self.assertTrue('books' in data)

    def test_get_book_details(self):
        response = self.app.get(
            '/api/books/2',
            content_type='application/json'
            )
        data = json.loads(response.data)
        self.assertEqual(200, response.status_code)
        self.assertTrue('books' in data)

    def test_update_book(self):
        token = login()
        author = {
            'year': 1992,
            'title': 'Alice'
        }
        response = self.app.put(
            '/api/books/2',
            data=json.dumps(author),
            content_type='application/json',
            headers= { 'Authorization': 'Bearer '+token }
        )
        self.assertEqual(200, response.status_code)

    def test_delete_book(self):
        token = login()
        response = self.app.delete(
            '/api/books/2',
            headers= { 'Authorization': 'Bearer '+token }

        )
        self.assertEqual(204, response.status_code)

测试覆盖率

现在我们已经学会了为我们的应用编写测试用例,单元测试的目标是测试尽可能多的代码,所以我们必须确保每个函数及其所有分支都被覆盖,你越接近 100%,在做出改变之前你就越放心。测试覆盖率是开发中使用的重要工具;然而,100%的覆盖率并不能保证没有 bug。

您可以通过以下命令使用 PIP 安装 coverage.py。

(venv)$ pip install coverage

Nose 库有一个内置的插件,可以与覆盖率模块一起工作,因此要运行测试覆盖率,您需要在运行 nosetests 时向终端添加两个参数。

使用下面的命令运行 nosetests,并启用测试覆盖率。

(venv)$ nosetests  --with-coverage --cover-package=api.routes

因此,我们在这里使用- with-coverage 标志启用覆盖,并指定只覆盖 routes 模块,否则默认情况下,它还将覆盖已安装的模块。

img/479840_1_En_5_Fig5_HTML.jpg

图 5-5

测试覆盖率

正如你所看到的,我们已经获得了大量的代码测试覆盖率,你可以覆盖所有其他的边缘案例来实现 100%的测试覆盖率。

接下来,您还可以启用- cover-html 标志,以 html 格式输出信息,这种格式更具可读性和可预置性。

(venv)$ nosetests --with-coverage --cover-package=api.routes --cover-html

前面的命令会生成测试覆盖率的 HTML 格式结果,现在你应该会在你的工作目录中看到一个名为 cover 的文件夹;打开文件夹,使用浏览器打开 index.html,查看 HTML 格式的测试覆盖率报告。

正如您在前面的图中所看到的,我们已经得到了测试覆盖报告的 HTML 版本。

img/479840_1_En_5_Fig6_HTML.jpg

图 5-6

HTML 格式的测试覆盖报告

结论

这就是本章的内容。我们已经学习了单元测试的基础知识,为我们的应用实现了测试用例,并且使用 nose 测试库覆盖了所有路线的单元测试和集成测试。这涵盖了我们这个应用的开发旅程。在下一章,我们将讨论部署,并在不同的云服务提供商上部署我们的应用。

六、部署 Flask 应用

因此,到目前为止,在本书中,我们完全专注于开发应用,在本章中,我们将讨论下一步,即部署我们的应用和管理应用部署后,这是应用开发的一个非常重要的部分。在本章中,我们将主要讨论安全部署 Flask 应用的各种方法。部署 Flask 应用有多种方式,每种方式都有其优缺点,因此我们将权衡它们,讨论它们的成本效益和安全性,并执行部署我们的应用的方式。正如我前面提到的,Flask 的服务器不适合生产部署,只用于开发和调试,所以我们将研究各种选项。

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

  1. 在阿里云 ECS 上部署带有 uWSGI 和 Nginx 的 Flask

  2. 在阿里云 ECS 上部署带 Gunicorn 的 Flask

  3. 在 Heroku 部署 Flask

  4. 在 AWS 弹性豆茎上展开 Flask

  5. 在 Google 应用引擎上部署 Flask

因此,在这一章中,我们将完全专注于在所有这些平台上部署我们的应用,并讨论每个平台的优缺点。虽然它们都是很好的选择,但完全是业务用例及资源决定了我们在哪里部署应用。

在阿里云 ECS 上部署带有 uWSGI 和 Nginx 的 Flask

以这种方式部署应用通常被称为传统托管,其中依赖项是手动安装的或通过脚本安装程序安装的,这涉及手动安装应用及其依赖项并保护它。在本节中,我们将使用 uWSGI 和 Nginx 在阿里云弹性计算服务上托管的 Linux 操作系统上安装和运行我们的应用。

uWSGI 是一个成熟的 HTTP 服务器和一个能够运行生产应用的协议。uWSGI 是一个流行的 uwsgi(协议)服务器,而 Nginx 是一个免费、开源、高性能的 HTTP 服务器和反向代理。在我们的例子中,我们将使用 Nginx 来反向代理我们与 uwsgi 服务器之间的 HTTP 调用,我们将在 Ubuntu OS 上部署 uw SGI 服务器。

因此,让我们直接进入业务并部署我们的应用,但在此之前,我们必须使用 pip freeze 冻结 requirements.txt 中的库。运行以下命令,确保该文件包含所有必需依赖项的列表。

(venv)$ pip freeze > requirements.txt

所以这里 pip freeze 将以需求格式输出所有需要的安装包。接下来,我们需要将我们的代码库推送到一个版本管理系统,比如 GitHub,稍后我们将在我们的 Linux 实例中使用它。为此,我们将在阿里云上创建一个 Ubuntu 实例,你可以在 www.alibabacloud.com 注册,或者你可以在任何其他云提供商上使用你的 Ubuntu 实例,甚至可以使用虚拟实例。

因此,在我们开始部署之前,我们还需要一个 MySQL 服务器,因为这是关于部署 Flask 应用的,所以我们不会涉及部署 MySQL 服务器。但是,您可以在同一个实例上部署一个,或者使用托管的 MySQL 服务器服务,并在 config.py 中编辑 DB 配置细节。

一旦你设置好了云账户,创建一个 Ubuntu 实例,最好是 16.04 或更高版本。这里我们有阿里云 ECS(弹性计算服务),一旦我们有了实例,我们将使用密钥对或密码进行 SSH。

img/479840_1_En_6_Fig1_HTML.jpg

图 6-1

阿里云 ECS 控制台

一旦你的 Ubuntu 实例启动并运行,SSH 进入并从你的首选版本管理系统中提取代码库。

img/479840_1_En_6_Fig2_HTML.jpg

图 6-2

SSH 到 Ubuntu 实例

正如您默认看到的,我们已经以 root 用户身份登录,所以在继续之前,我们将创建另一个名为 Flask 的 sudo 用户,这是一个很好的安全措施。为了限制应用中的安全漏洞可能造成的损害,在每个应用自己的用户帐户下运行是一个好主意。

$ adduser flask

接下来它会提示你为新用户设置一个密码,并输入一些细节;如果愿意,您可以只输入密码,将其他字段留空,然后运行以下命令将用户添加到 sudoers 列表中。

$ usermod -aG sudo flask

现在,一旦我们有了新用户,让我们使用下面的命令在 shell 中使用该用户登录。

$ su - flask

接下来,我们将从 GitHub repo 中提取我们的应用,因此请确保您已经安装了 git 客户端,如果您没有安装,请使用以下命令。

$ sudo apt-get install git

使用以下命令克隆应用存储库。

$ sudo git clone <repo_name>

接下来,将您当前的目录更改为 app 源代码,并安装 virtualenv 和 uwsgi,因为我们的 reqiurements.txt 中没有这些内容。

$ sudo pip install virtualenv uwsgi

像我们在上一章所做的那样创建一个虚拟 env,并在用下面的命令激活虚拟环境之后安装依赖项。

$ pip install -r requirements.txt

我们将从 Ubuntu 库安装设置应用所需的所有依赖项,我们将从安装 python-pip 开始,它是 python 和 python-dev 的包管理器,包含编译 Python 扩展所需的头文件。

$ sudo apt-get install python-pip python-dev

安装完依赖项后,我们将创建一个名为 flask-app.ini 的 uWSGI 配置文件,在当前目录下创建一个名为 flask-app.ini 的文件,并在其中添加以下代码行。

[uwsgi]
module = run:application

master = true
processes = 5

socket = flask-app.sock
chmod-socket = 660
vacuum = true

die-on-term = true

这个文件以[uwsgi]头开始,以便 wsgi 知道如何应用这些设置。我们还指定了模块和可调用程序,在我们的例子中是 run.py,不包括扩展名和可调用程序 application。

然后,我们指示 uwsgi 作为主进程启动该进程,并派生五个工作进程来处理请求。

接下来,我们将为 Nginx 提供 Unix 套接字文件,以遵循应用的 uWSGI 请求。让我们也改变套接字上的权限。稍后我们将把 uWSGI 进程的所有权交给 Nginx 组,因此我们需要确保套接字的组所有者可以从它那里读取信息并向它写入信息。我们还将通过添加真空选项在进程停止时清理插座。

我们要做的最后一件事是设置定期死亡选项。这有助于确保 init 系统和 uWSGI 对每个进程信号的含义有相同的假设。

接下来,我们将创建一个 systemd 服务单元文件,它将允许 Ubuntu 的 init 系统在服务器启动时自动启动我们的应用。

This file will be called flask-app.service and will be placed in /etc/systemd/system
Directory.
$ sudo nano /etc/systemd/system/flask-app.service

并将下面几行粘贴到文件中。

#Metadata and dependencies section
[Unit]
Description=Flask App service
After=network.target
#Define users and app working directory
[Service]
User=flask
Group=www-data
WorkingDirectory=/home/flask/flask-api-app/src
Environment="WORK_ENV=PROD"
ExecStart=/home/flask/flask-api-app/src/venv/bin/uwsgi --ini flask-app.ini
#Link the service to start on multi-user system up
[Install]
WantedBy=multi-user.target

之后,运行以下命令来启用并启动我们的新服务。

$ sudo systemctl start flask-app
$ sudo systemctl enable flask-app

我们的 uWSGI 服务器现在应该启动并运行,等待我们前面创建的套接字文件中的请求。我们现在将安装和配置 Nginx,使用 uwsgi 协议传递和处理请求。

$ sudo apt-get install nginx

现在我们应该有一个 Nginx 服务器启动并运行,我们将开始在/etc/nginx/sites-available 中创建一个新的服务器块配置文件,我们将它命名为 flask-app。

$ sudo nano /etc/nginx/sites-available/flask-app

我们将打开一个服务器块,指示它监听端口 80,并定义服务器名,它应该是您的服务的域名。接下来,我们将在服务器块中定义一个位置块来定义基本位置,并在其中导入 uwsgi_params 头,指定一些需要设置的常规 uwsgi 参数。然后,我们将把请求传递给使用 uwsgi_pass 指令定义的套接字。

server {
    listen 80;
    server_name flaskapp;

    location / {
        include uwsgi_params;
        uwsgi_pass unix:/home/flask/flask-api-app/src/flask-app.sock;
    }
}

前面几行应该配置我们的服务器块来监听套接字上的服务器请求。一旦我们都准备好了,接下来我们将创建一个网站启用目录的符号链接。

$ sudo ln -s /etc/nginx/sites-available/flask-app /etc/nginx/sites-enabled

有了这些,我们现在可以使用下面的命令测试我们的更改是否有语法错误。

$ sudo nginx -t

如果没有语法错误,您应该在您的终端上看到这个输出。

$ nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
$ nginx: configuration file /etc/nginx/nginx.conf test is successful

现在,当您访问您的域名时,它应该可以正常工作。如果您没有域,并且想要访问应用,您应该在“启用站点”中编辑默认服务器块,而不是创建一个新的服务器块,或者删除默认应用,这样您就可以使用 IP 地址访问应用。

还要确保如果你在云服务上运行 Ubuntu 服务器,防火墙上的端口 80 是允许的,在我们的例子中,它是使用安全组设置的。

img/479840_1_En_6_Fig3_HTML.jpg

图 6-3

部署的应用

在 Gunicorn 上部署 Flask,在阿里云 ECS 上部署 Apache

现在我们将使用 Gunicorn 安装我们的 Flask 应用,guni corn 是用于 Unix 的 Python WSGI HTTP 服务器,它将运行我们的应用,然后我们将使用 Apache 服务器反向代理请求。

要了解本节内容,您需要具备以下条件:

  1. 拥有 sudo 权限的非 root 用户的 Ubuntu 服务器

  2. Apache 服务器已安装

  3. 主目录中我们的 Flask 应用的副本

正如我提到的,我们将使用 Gunicorn 来运行我们的应用,所以让我们使用 PIP 来安装 Gunicorn。

$ pip install gunicorn

接下来,我们将创建一个系统服务,就像我们在上一节中所做的那样,所以继续用下面的命令创建我们的新服务。

$ sudo nano /etc/systemd/system/flask-app.service

接下来在你的 nano 编辑器中添加下面几行。

[Unit]
Description= Flask App service
After=network.target

[Service]
User=flask
Group=www-data
Restart=on-failure
Environment="WORK_ENV=PROD"
WorkingDirectory=/home/flask/flask-api-app/src
ExecStart=/home/flask/flask-api-app/src/venv/bin/gunicorn -c /home/flask/flask-api-app/src/gunicorn.conf -b 0.0.0.0:5000 wsgi:application

[Install]
WantedBy=multi-user.target

现在保存文件并退出,我们应该有我们的系统服务。接下来,我们将使用以下命令启用并启动我们的服务。

$ sudo systemctl start flask-app
$ sudo systemctl enable flask-app

所以现在我们的应用应该运行在端口 5000 上;接下来,我们需要配置 Apache 来反向代理我们的应用。

默认情况下,反向代理模块在 Apache 中是禁用的,要启用它,请输入以下命令。

$ a2enmod

它会提示激活模块;输入以下要激活的模块:

$ proxy proxy_ajp proxy_http rewrite deflate headers proxy_balancer proxy_connect proxy_html

现在将我们的应用添加到 Apache web 服务器配置文件中。将以下行(在 VirtualHost 块中)添加到/etc/Apache 2/sites-available/000-default . conf 中

    <Proxy *>
        Order deny,allow
        Allow from all
    </Proxy>
    ProxyPreserveHost On
    <Location "/">
          ProxyPass "http://127.0.0.1:5000/"
          ProxyPassReverse "http://127.0.0.1:5000/"
    </Location>

现在,它应该在根路由上代理我们的应用,您的最终服务器块应该如下所示。

<VirtualHost *:80>
    # The ServerName directive sets the request scheme, hostname and port that
    # the server uses to identify itself. This is used when creating
    # redirection URLs. In the context of virtual hosts, the ServerName
    # specifies what hostname must appear in the request's Host: header to
    # match this virtual host. For the default virtual host (this file) this
    # value is not decisive as it is used as a last resort host regardless.
    # However, you must set it for any further virtual host explicitly.
    #ServerName www.example.com

    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html

    # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
    # error, crit, alert, emerg.
    # It is also possible to configure the loglevel for particular
    # modules, e.g.
    #LogLevel info ssl:warn

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
     <Proxy *>
        Order deny,allow
        Allow from all
    </Proxy>
    ProxyPreserveHost On
    <Location "/">
          ProxyPass "http://127.0.0.1:5000/"
          ProxyPassReverse "http://127.0.0.1:5000/"
    </Location>
    # For most configuration files from conf-available/, which are
    # enabled or disabled at a global level, it is possible to
    # include a line for only one particular virtual host. For example the
    # following line enables the CGI configuration for this host only
    # after it has been globally disabled with "a2disconf".
    #Include conf-available/serve-cgi-bin.conf
</VirtualHost>

# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

保存文件并退出,我们已经全部介绍过了。只需用下面的命令重启服务器。

$ sudo service apache2 restart

现在使用 IP 地址访问浏览器中的应用。

img/479840_1_En_6_Fig4_HTML.jpg

图 6-4

在 Gunicorn 上部署 Flask 应用

在 AWS 弹性豆茎上展开 Flask

在本节中,我们将使用 AWS Elastic Beanstalk 部署我们的 Flask 应用。AWS Elastic Beanstalk 是一个易于使用的服务,用于部署和扩展 web 应用和服务。

我们假设您在开发机器中已经有一个活动的 AWS 帐户和 AWS CLI 设置,或者您可以在其上使用 AWS 文档。

要创建应用环境并部署应用,请使用 eb init 命令初始化您的 EB CLI 存储库。

$ eb init -p python-2.7 flask-app --region <your_region>

注意

有关地区列表,请参考本指南: https://docs.aws.amazon.com/general/latest/gr/rande.html

您应该在终端中看到以下响应。

$ Application flask-app has been created.

前面的命令创建一个名为 flask-app 的新应用,并配置您的本地存储库,以使用最新的 Python 2.7 创建环境。

接下来再次运行 eb init,为 SSH 登录配置一个密钥对。

接下来,我们将创建一个环境,并使用 eb create 将您的应用部署到该环境中。

$ eb create

接下来输入环境名称、DNS 前缀和负载平衡器类型,这将消除向外界公开 web 服务器的需要。

img/479840_1_En_6_Fig5_HTML.jpg

图 6-5

eb 创建

现在大约需要 5 分钟来部署。部署完成后,我们只需要配置一些更多的细节,这将直接在 AWS web 控制台中完成。

img/479840_1_En_6_Fig6_HTML.jpg

图 6-6

eb 创造成功

一旦应用完成部署,您应该会看到类似于上图的输出。接下来登录 AWS web 控制台,打开 Elastic Beanstalk 配置选项卡。

img/479840_1_En_6_Fig7_HTML.jpg

图 6-7

弹性豆茎应用配置

接下来点击修改;在“软件”选项卡和“容器内选项”中,将 WSGIPath 更新为 run.py。

img/479840_1_En_6_Fig8_HTML.jpg

图 6-8

WSGIpath 集

现在向下滚动,在环境变量中,提供 WORK_ENV 并将其设置为 Prod,以便我们的应用在生产模式下运行。接下来单击 apply,应用应该会重新加载并开始工作。

img/479840_1_En_6_Fig9_HTML.jpg

图 6-9

弹性豆茎环境变量

现在,您可以返回 dashboard 来查找应用的 URL,它应该已经启动并运行了。

注意

在 Elastic Beanstalk 中,您还可以配置并启动一个连接到环境的 MySQL RDS 服务器来运行应用,但这超出了本书的范围。

img/479840_1_En_6_Fig10_HTML.jpg

图 6-10

在弹性豆茎上部署 Flask 应用

在 Heroku 上部署 Flask 应用

Heroku 是一个平台即服务(PaaS ),支持各种现代应用,为在云中大规模部署和管理应用提供基于容器的环境。在 Heroku 上部署应用非常简单快捷。你可以使用 Heroku git,用你当前的 GitHub 账户连接,或者使用容器注册。这里我们将使用 Heroku CLI 部署我们的应用;因此,请确保您在 https://signup.heroku.com/ 有一个有效的 Heroku 帐户,它可以让您免费部署多达五个应用。

您还需要 Heroku CLI,可从 https://devcenter.heroku.com/articles/heroku-command-line 下载。拥有 CLI 后,使用以下命令登录 Heroku CLI,该命令将提示您提供登录凭据。

$ heroku login

添加配置文件

为了在 Heroku 上成功部署我们的应用,我们必须向该应用添加 Procfile,它定义了应用运行时要执行的命令。

对于 Heroku,我们将使用一个名为 Gunicorn 的 web 服务器,因此在创建 Procfile 之前,使用以下命令安装 Gunicorn。

(venv)$ pip install gunicorn

现在使用 pip freeze 命令更新 requirements.txt 文件。

(venv)$ pip freeze > requirements.txt

现在让我们首先测试 Gunicorn 是否能很好地与我们的应用一起工作;运行以下命令在本地启动 Gunicorn 服务器。

(venv)$ gunicorn run:application

运行后,您应该在终端上得到以下输出,这意味着服务器工作正常。

[2019-04-29 22:54:41 +0530] [37191] [INFO] Starting gunicorn 19.9.0
[2019-04-29 22:54:41 +0530] [37191] [INFO] Listening at: http://127.0.0.1:8000 (37191)
[2019-04-29 22:54:41 +0530] [37191] [INFO] Using worker: sync
[2019-04-29 22:54:41 +0530] [37194] [INFO] Booting worker with pid: 37194

注意

默认情况下,Gunicorn 从端口 8000 开始。

接下来,在 src 目录中创建一个名为 Procfile 的文件,并在其中添加以下代码行。

web: gunicorn run:application

这里为 Heroku 指定了 web,以便为应用启动一个 Web 服务器。现在,在我们在 Heroku 上创建和部署我们的应用之前,还需要做一件事情。由于 Heroku 默认使用 Python 3.6 运行时,我们必须创建另一个名为 runtime.txt 的文件,并添加以下代码行,以便 Heroku 为我们的应用使用正确的 Python 版本。

python-2.7.16

现在我们已经准备好部署我们的应用了;在应用的 src 目录中,运行以下命令创建一个新的 Heroku 应用。

$ heroku create <app_name>

这需要几秒钟的时间,您应该会在终端上看到类似的输出。

Creating flask-app-2019... done
https://flask-app-2019.herokuapp.com/ | https://git.heroku.com/flask-app-2019.git

接下来用下面的命令初始化一个新的 Heroku git repo。

$ git init
$ heroku git:remote -a flask-app-2019

现在添加所有文件,并用下面的命令提交代码。

$ git add .
$ git commiti -m "init"

在推送和部署代码之前,我们需要做的最后一件事是设置 WORK_ENV 环境变量,因此使用下面的命令来完成。

$ heroku config:set WORK_ENV=PROD

接下来我们需要将代码推送到 Heroku git,它将被自动部署。

$ git push heroku master

几分钟后,您的应用就应该部署并运行了。您的应用的 URL 是htttps://<app_name>.herokuapp.com

img/479840_1_En_6_Fig11_HTML.jpg

图 6-11

Heroku 上的 Flask 应用

这就是在 Heroku 上部署我们的应用的原因;您可以在 https://devcenter.heroku.com 上了解更多关于 Heroku 的信息

在谷歌应用引擎上部署 Flask 应用

在本节中,我们将在 Google Cloud App Engine 上部署我们的应用,这是一个完全托管的无服务器平台,用于在云上部署和扩展应用。App Engine 支持包括 Python 在内的各种平台,并为部署后端服务提供完全托管的服务。因此,在我们开始之前,请确保您有一个有效的谷歌云帐户,或者您可以在 https://cloud.google.com/products/search/apply/ 注册。谷歌云也提供一年 300 美元的积分。

接下来使用以下指南安装 Google Cloud CLI:https://cloud.google.com/sdk/docs/quickstarts。设置完成后,运行以下命令登录您的 Google Cloud 帐户。

$ gcloud auth login

一旦成功,我们就可以开始创建我们的 Google App Engine 应用,但是在此之前,我们需要创建几个配置文件。

首先用下面的代码在 src 目录中创建 app.yaml,为 Google App Engine 配置应用基础。

runtime: python27
api_version: 1
threadsafe: true
handlers:
- url: /avatar
  static_dir: images
- url: /.*
  script: wsgi.application

libraries:
  - name: ssl
    version: latest
env_variables:
  WORK_ENV: PROD

接下来创建 appengine_config.py,它将从我们的虚拟环境中获取已安装的模块,以便 appengine 知道第三方模块安装在哪里。

from google.appengine.ext import vendor

vendor.add('venv/lib/python2.7/site-packages/')

现在我们准备初始化我们的应用;对同一运行以下命令:

$ gcloud init

这将提示您创建应用、名称和项目,并选择区域,因此请正确输入。

完成后,运行以下命令在 Google Cloud App Engine 上部署您的应用。

几分钟之内,它就应该部署并运行了。现在,您可以在终端中运行以下命令,在默认浏览器中打开应用。

$ gcloud app browse

img/479840_1_En_6_Fig12_HTML.jpg

图 6-12

谷歌云应用引擎上的 Flask 应用

结论

因此,在本章中,我们使用不同的方法在各种云平台上部署了我们的应用,这应该已经为您提供了入门知识,或者部署 Flask 应用的各种部署和扩展选项。在下一章,我们将讨论管理和调试已部署应用的部署后步骤。

七、监控 Flask 应用

到目前为止,我们已经介绍了 flask 应用的开发、测试和部署。在这一章中,我们将讨论一些管理和支持你的 Flask 应用的附加组件,以及从这里开始的步骤。

应用监控

即使在对我们的应用执行了各种各样的测试之后,在现实世界中,总会有一些我们在开发时没有意识到的异常场景和瓶颈,当人们开始使用应用时,它们会作为 bug 和错误出现在产品中。这就是我们需要应用监控的时候,它可以监控您的应用在生产中的行为,包括停机检查、端点错误、崩溃、异常和与性能相关的问题。监控对于应用来说至关重要,因为原始日志文件很难解释,而且开发人员需要花费大量时间来理解它们。日志文件在大多数情况下可以检测到功能错误,但是它不会告诉您太多与性能相关的问题,而这些问题对于您的应用也是至关重要的,因为业务依赖于应用能够及时地为其客户提供服务。因此,主动应用监控对于处理应用中的稳定性、性能和错误至关重要。

所有主要的云服务提供商都支持监控虚拟机的功能。我们在上部署我们的应用,用于操作系统级别的 CPU 利用率、内存利用率和网络。但是,应用监控通常包括以下内容:

  1. 应用错误和警告

  2. 每个事务的应用性能

  3. 数据库和第三方集成查询性能

  4. 基本服务器监控和指标

  5. 应用日志数据

市场上有许多优秀的应用监控工具,在这一章中,我们将介绍其中一些工具的集成,因为它们都具有不同的功能、支付选项等。

以下是一些开源监控项目的列表:

  1. 哨兵

  2. 扇子

  3. 服务金丝雀

  4. Flask 监控仪表板

市场上也有一些监控服务,如 New Relic、Sentry.io、Scout 等,它们可以减轻部署监控软件的负担,但需要付出一定的代价。我们将研究应用监控工具的类型。

哨兵

Sentry 是一个用 Python 开发的开源应用监控系统,但可用于所有主要平台。Sentry 还有一个云托管服务,有不同的订阅模式,从面向开发者的免费版本到每月花费约 80 美元的商业版本。在这方面,我们将检查免费版本,并将其与我们的集成,这是非常简单的。

img/479840_1_En_7_Fig1_HTML.jpg

图 7-1

创建新项目

首先,在 https://sentry.io/signup/ 上注册哨兵,注册成功后,登录您的仪表板。完成后,点击 add project 按钮,选择 Flask 作为框架,输入名称,提交创建项目。

一旦完成,它将把我们带到下一页,这一页有集成的细节,所以只需使用 PIP 安装 sentry SDK,并把集成代码复制粘贴到 main.py 中。

(venv)$pip install sentry-sdk[flask]

接下来在 app = Flask(name)前添加以下代码。

sentry_sdk.init(
    dsn="<your_dsn_here>"
    integrations=[FlaskIntegration()]
)

一旦完成,您就可以部署应用,剩下的工作就交给 sentry 了。让我们通过在 sentry dashboard 中点击 404 端点来测试一下。

在您的浏览器中,请求任何不存在的端点,sentry SDK 将从应用中触发一个事件,如下图所示。

img/479840_1_En_7_Fig2_HTML.jpg

图 7-2

哨兵问题清单

点击该问题将使您深入了解该问题及其详细信息,从而帮助您解决该问题,如下图所示。

img/479840_1_En_7_Fig3_HTML.jpg

图 7-3

哨兵问题详情

Sentry 有更多的特性供你探索,让你的应用稳定无误。

Flask 监控仪表板

Flask 监控仪表板是 Flask 应用的扩展。它严格监控应用的性能和利用率,分析请求,还可以配置为运行特定的作业来管理您的应用。它是开源和免费的,所以让我们开始吧。

使用 PIP 安装 Flask 监控仪表板,代码如下。

(venv)$pip install flask_monitoringdashboard

接下来,在 main.py 文件中,使用下面的代码导入扩展,这里有其他库导入。

import flask_monitoringdashboard as dashboard

现在,就在我们初始化 app 对象的下方,添加以下代码。

dashboard.bind(app)

差不多就是这样。现在重启你的应用,访问 http:// : /dashboard,就会打开登录页面。默认凭据是 admin 和 admin,您应该在之后立即更改。

img/479840_1_En_7_Fig4_HTML.jpg

图 7-4

Flask 监控仪表盘

登录后,您将被重定向到控制面板,该面板将概述您的终端,您可以单击每个终端以获得更深入的了解,还可以根据您的偏好设置每个终端的监控级别。

img/479840_1_En_7_Fig5_HTML.jpg

图 7-5

Flask 监控仪表板概述

单击端点会将您重定向到每个端点的 insights 页面,在这里您可以生成每个请求的图形数据,并获得有关端点使用情况的更深入的信息。

img/479840_1_En_7_Fig6_HTML.jpg

图 7-6

API 利用率洞察

新遗迹

New Relic 是市场上最可靠、最具竞争力的应用监控工具之一;他们提供了一个全面的数据库和实时监控服务。New Relic 是一项基于订阅的付费服务,但他们有 14 天的试用期,我们将用它来检验一下。那就去报名参加 https://newrelic.com/signup 的活动吧

一旦你注册,你将被要求选择平台;继续选择 Python 作为平台。

img/479840_1_En_7_Fig7_HTML.jpg

图 7-7

新遗迹设置

在您的应用目录中,使用以下命令安装新的 Relic 代理。

(venv)$ pip install newrelic

接下来,我们需要使用新的遗留密钥来生成配置,因此单击屏幕上的“显示许可证密钥”按钮,这将显示您的私钥。

现在用您的密钥执行以下命令。

(venv)$ newrelic-admin generate-config <your-key-goes-here> newrelic.ini

所以现在我们的应用应该配置好了;接下来,使用下面的命令运行您的应用并测试配置。

(venv)$ NEW_RELIC_CONFIG_FILE=newrelic.ini newrelic-admin run-program python run.py

一旦你这样做,点击监听我的应用按钮,如下图所示,这将开始监听来自你的应用的请求,以配置你的仪表板,一旦它成功地接收到数据,你会看到一个按钮重定向到你的新的遗迹仪表板。

img/479840_1_En_7_Fig8_HTML.jpg

图 7-8

新遗迹听申请

现在你会看到应用仪表板,其中列出了你的应用;单击 Python 应用,因为我们这里只有一个应用。

img/479840_1_En_7_Fig9_HTML.jpg

图 7-9

新遗迹应用仪表板

点击后,您将被重定向到您的应用仪表板,它看起来非常全面,但提供了许多关于应用状态的见解。参考以下截图为例。

img/479840_1_En_7_Fig10_HTML.jpg

图 7-10

Python 应用仪表板

如您所见,我们可以查看最新的事务和事务时间图,然后我们可以查看吞吐量图和 apdex 分数,该分数根据 0.5 秒响应时间的设定值对应用进行评分。在“事件”下,您可以查看错误分析和错误,它们提供了系统中出现的错误的详细信息。

你可以在 https://docs.newrelic.com/docs/apm/new-relic-apm/guides 的官方文档中了解更多关于新遗迹的信息。

奖励服务

因此,我们已经涵盖了所有主题,让您开始使用 Flask 开发 REST 应用。然而,这只是一个初学者指南,对于真正的业务用例,还有更多的东西要涵盖,包括集成第三方服务,如搜索、缓存、发布订阅、实时通信等等。这本书涵盖了 Flask REST API 开发的基础知识,可以作为构建 REST 应用的基础。在本模块中,我们将介绍一些附加库和工具的基础知识,这些库和工具可以为您的应用增加价值。

使用 Flask 进行全文搜索

Flask 与一些库兼容,这些库提供了与全文搜索应用(如 Elasticsearch)的集成。

肾盂弹性研究

Pyelasticsearch 是一个集成 elasticsearch 的干净的库,Elasticsearch 是 flask 应用中一个非常流行和强大的搜索引擎。然而,Elasticsearch 提供了 REST 端点来连接搜索引擎,但 pyelasticsearch 使与端点的通信更容易。您可以在 https://pyelasticsearch.readthedocs.io 了解更多关于弹性搜索的信息,并查看此链接了解更多关于弹性搜索 www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html

Flask 化学

Whoosh 是一个用 Python 构建的快速搜索引擎库,非常灵活,支持基于自由格式或结构化文本的复杂数据搜索。Flask-WhooshAlchemy 是一个 Flask 扩展,用于将 Whoosh 搜索引擎库与 Flask 中的 SQLAlchemy 集成在一起。不像 pyelasticsearch 那样,不需要任何第三方应用就可以集成并获得全文搜索,这非常简单。可以在 https://pythonhosted.org/Flask-WhooshAlchemy/ 了解更多

电子邮件

我们使用 Flask mail 来验证用户邮件。这里有一个其他库的列表,您可以使用它们在您的应用中有效地集成电子邮件。

弗拉斯克-SendGrid

Flask-SendGrid 是一个 Flask 扩展,使用 SendGrid 简化发送电子邮件,sendgrid 是一个著名的电子邮件服务;可以在 https://sendgrid.com/ 报名 SendGrid 他们提供免费订阅,包括前 30 天的 40,000 封电子邮件和永远的 100 封/天。您可以在 https://github.com/frankv/flask-sendgrid 查看 Flask-SendGrid,这让发送电子邮件变得轻而易举。

使用 Boto3 的 AWS SNS

Boto3 是一个用于 Python 的 AWS SDK,你可以使用 AWS 的 SNS(简单通知服务)通过 Boto 发送电子邮件和文本消息。下面是一个使用 Python 和 Boto https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-using-sdk-python.html 发送电子邮件的指南。你需要一个有效的 AWS 帐户,并从 https://boto3.readthedocs.io/en/latest/guide/quickstart.html#installation 安装 Boto。

文件存储器

文件存储是应用的一个重要方面;我们将用户头像存储在应用服务器文件系统中,这对于生产应用来说并不是一个好方法,在这种情况下,您可能希望使用文件存储服务来存储和访问您的文件。这里有一些相同的建议。

使用 Boto3 的自动气象站 S3

你可以利用 Boto3 来管理亚马逊 AWS S3 上的文件,这是一个强大的文件管理系统。本指南将为您提供使用 Flask https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3.html 管理文件所需的一切

阿里巴巴云 OSS

阿里云提供了一个复杂的文件存储平台,叫做对象存储服务;您可以从 https://github.com/aliyun/aliyun-oss-python-sdk 使用他们的 Python SDK,并使用他们在 https://help.aliyun.com/document_detail/32026.html 提供的 OSS 指南轻松设置文件管理

结论

这标志着本章和本书的结束。在优化和升级您的应用方面还有很多需要探索的地方,但这将是您成长的正确基础;如果您在集成任何提到的服务时遇到问题,请查看它们的官方文档,寻求支持,或寻求堆栈溢出来解决它们。您也可以使用此链接询问或阅读有关 Flask https://stackoverflow.com/search?q=flask 的问题。

posted @ 2024-08-13 14:28  绝不原创的飞龙  阅读(308)  评论(0)    收藏  举报