Python-API-开发基础知识-全-

Python API 开发基础知识(全)

原文:zh.annas-archive.org/md5/2d9af79a5dbf9a8e103c0cfc372271a5

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

关于

本节简要介绍了作者、本书涵盖的内容、开始学习所需的技术技能,以及完成所有包含的活动和练习所需的硬件和软件要求。

关于本书

Python 是一种灵活的语言,它可以用于比脚本开发更多的用途。通过了解 Python RESTful API 的工作原理,你可以使用 Python 构建强大的后端,用于 Web 应用和移动应用。

你将通过构建一个简单的 API 并学习前端 Web 界面如何与后端通信来迈出第一步。你还将学习如何使用 marshmallow 库序列化和反序列化对象。然后,你将学习如何使用 Flask-JWT 进行用户认证和授权。除了所有这些,你还将学习如何通过添加有用的功能来增强你的 API,例如电子邮件、图片上传、搜索和分页。你将通过将 API 部署到云中来结束整本书的学习。

到本书结束时,你将拥有信心和技能,利用 RESTful API 和 Python 的力量构建高效的 Web 应用。

关于作者

陈杰在 10 岁时开始编程。他在大学期间是全世界编程竞赛的积极参与者。毕业后,他在金融和 IT 行业工作了 10 多年,构建了分析数百万笔交易和头寸以发现可疑活动的系统。他利用强大的 Python 分析库为工作在纳秒级的交易系统进行数据分析性能优化。他对现代软件开发生命周期有深入的了解,该生命周期使用自动化测试、持续集成和敏捷方法。在所有编程语言中,他发现 Python 是最具表现力和功能性的。他创建了课程并在世界各地教授学生,使用 Python 作为教学语言。激励有志于软件工程职业道路的开发者一直是陈杰的目标。

钟瑞是一位开发者和讲师。他热爱帮助学生学习编程和掌握软件开发。他现在是自雇人士,使用 Python 开发 Web 应用、网络应用和聊天机器人。他出售的第一个程序是一个网络应用,帮助客户配置、维护和测试数千个多厂商网络设备。他参与过大型项目,如马拉松在线注册系统、租车管理系统等。他与 Google App Engine、PostgreSQL 和高级系统架构设计有广泛的工作经验。他多年来一直是一位自学成才的开发者,并知道学习新技能的最高效方法。

黄杰是一位拥有超过 7 年经验的程序员,擅长使用 Python、Javascript 和.NET 开发 Web 应用程序。他精通 Flask、Django 和 Vue 等 Web 框架,以及 PostgreSQL、DynamoDB、MongoDB、RabbitMQ、Redis、Elasticsearch、RESTful API 设计、支付处理、系统架构设计、数据库设计和 Unix 系统。他为配件商店平台、ERP 系统、占卜 Web 应用程序、播客平台、求职服务、博客系统、沙龙预约系统、电子商务服务等多个项目编写了应用程序。他还拥有处理大量数据和优化支付处理的经验。他是一位热爱编码并不断跟踪最新技术的专家级 Web 应用程序开发者。

学习目标

在本书结束时,您将能够:

  • 理解 RESTful API 的概念

  • 使用 Flask 和 Flask-Restful 扩展构建 RESTful API

  • 使用 Flask-SQLAlchemy 和 Flask-Migrate 操作数据库

  • 使用 Mailgun API 发送纯文本和 HTML 格式的电子邮件

  • 使用 Flask-SQLAlchemy 实现分页功能

  • 使用缓存来提高 API 性能并高效地获取最新信息

  • 将应用程序部署到 Heroku 并使用 Postman 进行测试

目标受众

本书非常适合对 Python 编程有基础到中级知识的软件开发初学者,他们希望使用 Python 开发 Web 应用程序。了解 Web 应用程序的工作原理将有所帮助,但不是必需的。

方法

本书采用实践学习的方法向您解释概念。您将通过实现您在理论上学到的每个概念来构建一个真实的 Web 应用程序。这样,您将巩固您的新技能。

硬件要求

为了获得最佳体验,我们建议以下硬件配置:

  • 处理器:Intel Core i5 或等效处理器

  • 内存:4 GB RAM(8 GB 更佳)

  • 存储:35 GB 可用空间

软件要求

我们还建议您提前安装以下软件:

  • 操作系统:Windows 7 SP1 64 位、Windows 8.1 64 位或 Windows 10 64 位、Ubuntu Linux 或最新版本的 OS X

  • 浏览器:Google Chrome/Mozilla Firefox(最新版本)

  • Python 3.4+(最新版本是 Python 3.8:从https://python.org

  • Pycharm

  • Postman

  • Postgres 数据库

惯例

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称的显示方式如下:

"接下来,我们将处理create_recipe函数,该函数在内存中创建一个食谱。使用/recipes路由来触发create_recipe函数,并通过methods = [POST]参数指定该路由装饰器仅响应 POST 请求。"

新术语和重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“然后,选择定义并设置密码。点击保存”。

代码块设置如下:

    if not recipe:
        return jsonify({'message': 'recipe not found'}), HTTPStatus.NOT_FOUND

安装和设置

在我们能够用数据做些酷的事情之前,我们需要准备好最高效的环境。在本节中,我们将看到如何做到这一点。

安装 Python

访问 www.python.org/downloads/ 并按照您平台的具体说明进行操作。

安装 Pycharm 社区版

访问 www.jetbrains.com/pycharm/download/ 并按照您平台的具体说明进行操作。

安装 Postman

访问 www.getpostman.com/downloads/ 并按照您平台的具体说明进行操作。

安装 Postgres 数据库

我们将在本地机器上安装 Postgres:

  1. 访问 www.postgresql.org 并点击 下载 进入下载页面。

  2. 根据您的操作系统选择 macOS 或 Windows。

  3. EnterpriseDB 的交互式安装程序 下,下载最新版本的安装程序。安装程序包含 PostgreSQL 以及 pgAdmin,这是一个用于管理和开发您数据库的图形工具。

  4. 安装 Postgres 版本 11.4。按照屏幕上的说明安装 Postgres 并设置密码。

  5. 安装完成后,您将被带到 pgAdmin。请设置 pgAdmin 密码。

其他资源

本书代码包托管在 GitHub 上,网址为 github.com/TrainingByPackt/Python-API-Development-Fundamentals。我们还有其他来自我们丰富课程和视频目录的代码包,可在 github.com/PacktPublishing/ 找到。查看它们!

第一章:1. 您的第一步

学习目标

到本章结束时,您将能够:

  • 复制 RESTful API 的概念

  • 描述不同 HTTP 方法和状态的意义

  • 在 PyCharm IDE 中获得实际操作经验

  • 使用 Flask 构建 RESTful API 并执行 CRUD 操作

  • 使用 JSON 消息与 API 进行通信

  • 使用 Postman 和 httpie/curl 命令行工具测试 API 端点

本章介绍了 API,并解释了网络服务、API 和 REST 的概念。

简介

我们处于互联网时代,一个万物互联的世界。数据无缝地从一地流向另一地。我们只需在网站上点击几下,就能获取世界上所有的信息。以 Skyscanner 为例,我们只需输入旅行的日期和地点,它就能在瞬间找到最便宜的航班;背后提供这些数据的英雄就是 API。

在本章中,您将学习什么是网络服务、API 和 REST。我们将从教授 API 的基本概念开始。然后,我们将探讨不同网络服务(如 Google、Facebook 等)如何使用 REST API 的实际例子。

最后,我们将使用 Python 开发我们的第一个简单的 Python RESTful API。Python 是一种流行且功能强大的编程语言。除了在人工智能领域广泛使用外,它还在 Web 应用程序开发、大数据分析、网络爬虫和流程自动化等方面得到广泛应用。Python 在众多领域表现出色的原因是它拥有大量的框架。这些框架承担了所有繁重的工作,这使得开发者能够专注于实际的应用程序设计和开发。

在本章中,您将了解数据是如何在前端和后端之间编码和通信的。您将学习有关 JSON 格式、HTTP 协议、HTTP 状态码等的技术细节。所有开发工作都将使用 Postman 和 httpie/curl 进行验证和测试。我们将带您了解整个 Web 应用程序开发过程。您不仅将学习开发 RESTful API 的必要方面,还将学习思考过程、设计、开发、测试,甚至部署。这是一次学习完整软件开发生命周期的旅程。现在就让我们踏上这段激动人心的旅程吧!

理解 API

API 代表应用程序编程接口;它是网站(或移动应用程序)与后端逻辑通信的接口。简单来说,它就像一个信使,从用户那里接收请求并将其发送到后端系统。一旦后端系统响应,它将把响应传递给用户。这个比喻就像服务员,能够理解不同顾客的订单。然后,他们将在顾客和厨房里的厨师之间充当中间人。

如果你是一家餐厅的老板,在客户和厨房之间有服务员/女服务员在这里的好处是,客户将不会看到你的商业机密。他们不需要知道餐点是如何准备的。他们只需要通过服务员/女服务员下单,他们就会得到他们订购的餐点。在这种情况下,服务员就像 API 一样。以下图示有助于说明这个类比。

![图 1.1:服务员作为客户 API 的体现图 1.1:服务员作为客户 API 的体现

图 1.1:服务员作为客户 API 的体现

类似地,在计算机科学中,拥有 API 的一个关键好处是封装。我们封装逻辑,这样外面的人就无法看到它。在这种安排下,拥有敏感信息的大公司愿意通过 API 向世界提供服务,自信他们的内部信息不会被泄露。以 Skyscanner 为例,该公司对使用 API 让客户预订航班感到满意,但与此同时,存储在他们内部数据库中的其他客户的个人信息不会泄露。

API 也是一个标准接口,可以与不同类型的终端进行通信,它们可以是移动应用程序或网站。只要前端向 API 发送相同的请求,它就会得到相同的结果。如果我们回到我们的比喻,服务员/女服务员将为所有类型的客户提供服务,无论他们的性别、年龄、语言等等。

现在,假设你是 Skyscanner 的一名软件工程师,负责开发 API。你的工作会是什么?让我告诉你。你的工作将是编写一个程序,可以从客户通过网站接收预订请求(日期和位置),然后在 Skyscanner 数据库中查找匹配的航班,并将航班详情返回给客户。在这本书的整个过程中,你将是我们的 API 工程实习生。我们将一步一步地引导你通过开发一个可以服务于你系统用户的 RESTful API 项目的流程。

RESTful API

REST 代表表示状态传输。它最初在 2000 年 Roy Fielding 博士的论文(网络软件架构的设计和架构风格)中被定义。这篇论文被认为是网络领域的圣经。REST 不是一个标准或协议;它更像是一种软件架构风格。许多工程师遵循这种架构风格来构建他们的应用程序,例如 eBay、Facebook 和 Google Maps。这些网络应用程序每秒处理巨大的流量,所以你可以看到 REST 确实是一种可扩展的架构风格。当我们说 RESTful API 时,我们指的是符合 REST 约束/原则的 API。

REST 约束/原则

对于 REST 架构风格,有五个重要的约束/原则:

  • 客户端-服务器:客户端和服务器之间存在一个接口。客户端和服务器通过这个接口进行通信,彼此独立。只要接口保持不变,任何一方都可以被替换。请求总是来自客户端。

  • 无状态:对于请求没有状态的概念。每个请求都被视为独立且完整的。它不依赖于前一个请求,也不依赖于会话来维持连接状态。

  • 可缓存:为了提高性能,服务器或客户端上的事物可以缓存。

  • 分层系统:系统中可以有多个层次,这里的目的是隐藏实际的逻辑/资源。这些层可以执行不同的功能,例如缓存和加密。

  • 统一接口:接口保持不变。这有助于解耦客户端和服务器逻辑。

HTTP 协议

为了更好地理解 REST 是什么,并确保我们正在实现 REST 风格,我们可以简单地谈谈 HTTP 协议。HTTP 是 REST 架构风格的实现。它代表超文本传输协议,是互联网上使用的标准协议。我们每天都在使用它来浏览不同的网站。这就是为什么我们访问的所有网站都以 http 开头。

在 HTTP 协议中,有不同的服务请求方法类型。每种服务请求方法都有其特定的定义。当前端界面通过 URL 与后端 API 交互时,它们需要同时定义此请求的 HTTP 方法。不同的 HTTP 方法就像不同的服务窗口。例如,读取和创建数据是完全不同的服务,因此应该由不同的服务窗口处理,即不同的 HTTP 方法。

  • GET: 用于读取数据

  • POST: 用于创建数据

  • PUT: 通过用新内容完全替换数据来更新数据

  • PATCH: 通过部分修改一些属性来更新数据

  • DELETE: 用于删除数据

简而言之,不同的 HTTP 方法就像 REST API 的动词。它们用于对同一组数据执行不同的操作。

HTTP 方法和 CRUD

我们可以通过利用 HTTP 协议已经提供的内容轻松构建 RESTful API。让我们看看我们可以用来与服务器通信的 HTTP 方法。

在这本书中,我们将构建一个以 RESTful API 作为后端的食谱分享平台。这个平台将允许用户创建和分享他们自己的食谱。同时,用户还可以阅读其他用户分享的食谱。以这个食谱分享平台为例,为了实现这些功能,我们需要我们的 API 能够对食谱执行不同的操作。我们可以利用不同的 HTTP 方法。例如,我们可以使用GET方法请求http://localhost:5000/recipes以获取所有食谱。我们可以使用POST方法请求http://localhost:5000/recipes来创建一个新的食谱。我们还可以使用DELETE方法请求http://localhost:5000/recipes/20来删除 ID 为 20 的食谱。请参考以下表格以获取详细信息。

![图 1.2:HTTP 方法

![img/C15309_01_02.jpg]

图 1.2:HTTP 方法

我们可以看到,让后端 API 为我们工作很简单。我们可以简单地使用 HTTP 协议来传达我们的请求。

实际上,使用这个食谱分享平台,你可以看到我们需要的绝大多数操作都将围绕创建(CREATE)、读取(READ)、更新(UPDATE)和删除(DELETE)展开。这在所有其他 Web 应用程序中也是普遍适用的。在开发者社区中,我们简称为 CRUD。简而言之,CRUD 模型了数据库记录管理的生活周期。

以这种方式构建我们的 Web 应用程序可以帮助我们轻松构建一个功能性的 Web 系统,因为这些操作与 HTTP 方法相关。使用这种架构构建我们的应用程序简单、强大且易于阅读。

如你所能想象,我们需要将信息发送到后端服务器。例如,你可能想在后端数据库中存储一个食谱。你通过 HTTP 以与后端预先约定的格式发送食谱。预先约定的格式可以理解为我们在之前的比喻中与服务员沟通时所使用的语言。在现实生活中,我们有不同的语言,如英语、德语、中文等。我们需要说对方能理解的语言。在 Web API 领域,有两种流行的标准,JSON 和 XML。我们将主要讨论 JSON,因为它更易于阅读且被广泛采用。

JSON 格式

JavaScript 对象表示法JSON)是一种能够表示复杂数据结构的简单纯文本格式。我们可以使用这种格式来表示字符串、数字、数组,甚至对象。一旦我们将信息“JSON 化”,我们就可以使用这种广泛采用的格式与 API 进行通信。

我们将向您展示 JSON 格式文件的样子。在以下示例中,您将看到我们正在用 JSON 格式表示两个食谱。JSON 文档是一个纯文本文档;这里没有加密。它如此易于阅读,以至于我相信您已经可以判断出来(无需进一步解释)这里有两个食谱,每个食谱都有一个 ID、名称和描述。

这里有一些关于 JSON 语法的注意事项:

  • 数组被[]包围

  • 对象可以用{}表示

  • 名称/值总是成对存在,并由":"分隔

  • 字符串被""包围

以下是一个带有 JSON 语法的示例代码文件:

{
  "recipes":[
    {
      "id":1,
      "name":"Egg Salad",
      "description":"Place an egg in a saucepan and..."
    },
    {
      "id":2,
      "name":"Tomato Pasta",
      "description":"Bring a large pot of lightly salted water to a boil..."
    }
  ]
}

HTTP 状态码

HTTP 状态码是在 HTTP 协议中返回的代码。它通常对用户隐藏,所以你可能没有意识到它的存在。事实上,来自服务器的每个 HTTP 响应都包含一个状态码。在我们构建 RESTful API 时,我们需要遵守 HTTP 协议。状态码帮助前端客户端了解请求的状态,即成功或失败。例如,可能有一个关于在后端数据库中创建记录的客户端请求。在这种情况下,一旦数据库记录成功创建,服务器应返回 HTTP 状态码 201(已创建)。如果有错误(例如 JSON 文档中的语法错误),服务器应返回 HTTP 状态码 400(错误请求)。

常用 HTTP 状态码

让我们讨论一些常用的状态码。它们如下:

  • 200 OK 表示请求已成功。请求可以是 GET、PUT 或 PATCH。

  • 201 已创建表示 POST 请求已成功,并已创建记录。

  • 204 无内容表示 DELETE 请求已成功。

  • 400 错误请求表示客户端请求有误。例如,JSON 格式中有语法错误。

  • 401 未授权表示客户端请求缺少认证信息。

  • 403 禁止访问表示请求的资源被禁止访问。

  • 404 未找到表示请求的资源不存在。

开放 API

开放 API 是一个对第三方开放的 API。市面上有很多这样的 API。公司渴望开放他们的 API 以扩大用户基础,同时保持他们的源代码为私有。这些 API 我们也可以访问。让我们看看一些来自 Facebook 的 API。

例如,我们可以使用 HTTP GET 方法访问https://graph.facebook.com/{page_id}/feed,这将给我们提供ID = {page_id}的 Facebook 页面的动态。我们可以通过使用POST方法向https://graph.facebook.com/{page_id}/feed发送 HTTP 请求,然后在ID = {page_id}的 Facebook 页面上创建一个帖子。

注意

Facebook 粉丝页面 API 详情可在developers.facebook.com/docs/pages/publishing找到。

现在,让我们看看另一个互联网巨头,谷歌。谷歌也提供了一些我们可以用来管理邮箱中电子邮件标签的 Gmail API。以下是 Gmail API 文档的截图:

图 1.3:Gmail API 文档

图 1.3:Gmail API 文档

注意

Gmail 标签 API 可在developers.google.com/gmail/api/v1/reference/找到。

Flask Web 框架

Flask 是一个我们可以用来轻松构建网络应用程序的网络框架。网络应用程序通常需要一些核心功能,例如与客户端请求交互、将 URL 路由到资源、渲染网页以及与后端数据库交互。像 Flask 这样的网络应用程序框架提供了必要的包、模块,它们负责繁重的工作。因此,作为开发者,我们只需要关注实际的应用逻辑。

当然,市场上还有其他可用的网络框架。Flask 的一个强大竞争对手是 Django。它也是一个 Python 网络框架。我们选择在本书中使用 Flask 的原因是因为 Flask 是简约的。它被视为一个仅提供开发者起步所需的绝对必要包的微网络框架。正因为如此,它易于学习,非常适合初学者。

之后,如果我们想构建更多的功能,有大量的 Flask 扩展可供选择。随着我们在本书中的学习深入,您将看到 Flask 的强大之处。

构建简单的菜谱管理应用程序

让我们做一些简单的练习来测试你的知识。我们将在这本书中构建一个菜谱分享平台,API 是我们向公众公开的接口。我们首先定义我们想要提供的函数和相应的 URL。这些是我们可能需要的最基本的功能:

图 1.4:HTTP 方法与函数

图 1.4:HTTP 方法与函数

一个典型的菜谱应该具有以下属性

  • ID:菜谱的唯一标识符

  • 名称:菜谱的名称

  • 描述:菜谱的描述

我们将构建一个 API,列出存储在我们系统中的所有菜谱。该 API 将设计为根据不同的 URL 返回不同的结果。例如,http://localhost:5000/recipes 将给我们系统中存储的所有菜谱,而 http://localhost:5000/recipes/20 将给我们 ID = 20 的菜谱。在成功检索菜谱后,我们还将看到 HTTP 状态设置为 200(OK)。这表明我们的请求已成功。

当我们创建一个新的菜谱时,我们使用 HTTP POST 方法,将所有必要的参数以 JSON 格式查询 http://localhost:5000/recipes 来描述我们的菜谱。JSON 格式简单来说就是键/值对。如果我们的请求成功,菜谱将在后端创建,并将返回 HTTP 状态 201(已创建)。与 HTTP 状态一起,它还将以 JSON 格式发送刚刚创建的菜谱。

当我们更新一个菜谱时,我们使用 HTTP PUT 方法,将更新菜谱的所有必要参数以 JSON 格式发送到 http://localhost:5000/recipes/20。如果我们的请求成功,菜谱将在后端更新,并将返回 HTTP 状态 200(OK)。与 HTTP 状态一起,它还将以 JSON 格式发送更新的菜谱。

当我们删除一个菜谱时,我们可以使用 HTTP Delete 方法将数据发送到 http://localhost:5000/recipes/20。这将删除 ID = 20 的菜谱。

现在您知道我们将要走向何方,让我们挽起袖子,大干一场吧!

虚拟环境

开发者始终建议在虚拟环境中开发他们的应用程序,而不是直接在本地环境中开发。

原因是虚拟环境是独立的应用程序开发环境。我们可以在本地机器上创建多个虚拟环境,这些虚拟环境可以有自己的 Python 版本、自己的包、自己的环境变量等等。尽管它们建立在同一台本地机器上,但这些虚拟环境之间不会相互干扰。

在接下来的练习中,我们将在 PyCharm IDE 中创建一个开发项目。我们将向您展示如何在 PyCharm 中为该项目设置虚拟环境。

练习 1:构建我们的第一个 Flask 应用程序

在这个练习中,我们将构建我们的第一个 Flask 应用程序。您将意识到构建应用程序是多么简单。PyCharm 是一个优秀的 集成开发环境IDE),拥有友好的 GUI,这将使我们的开发过程更加容易。我们将学习应用程序开发的流程,包括创建应用程序项目和安装必要的 Python 包:

  1. 使用 basic-api 在 PyCharm 中创建一个新项目。PyCharm 将自动帮助我们为这个新项目创建一个虚拟环境。![图 1.5:创建新项目 图 1.5

    图 1.5:创建新项目

    对于项目在自己的分配的独立虚拟环境中运行来说,这是一个好的做法,这样这些项目就可以在不同的包上运行,而不会相互影响。

  2. 在我们的虚拟环境中安装必要的包。为此,我们可以在项目中创建一个名为 requirements.txt 的文件,并输入以下文本。我们想要安装 Flask(版本 1.0.3)和 httpie(版本 1.0.2):

    Flask==1.0.3
    httpie==1.0.2
    

    以下截图显示了在 requirements.txt 中安装 Flask 和 httpie:

    ![图 1.6:在 requirements.txt 中安装 Flask 和 httpie 图 1.6

    图 1.6:在 requirements.txt 中安装 Flask 和 httpie

    PyCharm 将会提示我们缺少的包,如图所示。点击 app.py

    注意

    要安装 Python 包,我们也可以在终端中运行 pip install -r requirements.txt 命令。它会产生相同的结果。

    我们正在安装的 Flask 包是一个网络微框架。它非常轻量级,只需几行代码就可以构建一个网络服务。

  3. 让我们在 app.py 中输入以下代码,然后在左侧面板中右键点击 app.py 的文件名,选择 app 来执行我们第一个 Flask 网络服务:

    from flask import Flask
    app = Flask(__name__)
    @app.route("/")
    def hello():
        return "Hello World!"
    if __name__ == "__main__":
        app.run()
    

    这段代码首先在 app.py 中导入 Flask 包,然后实例化一个 Flask 对象,最后将其分配给 app 变量。我们已创建主函数作为启动脚本的人口点。这随后启动了 Flask web 服务器。之后,我们定义了我们的第一个 API 函数 hello,它返回一个 "Hello World" 响应。使用 Flask 装饰器,我们可以将 GET 请求 URL 路由到这个函数。

  4. 现在,打开浏览器并输入 http://localhost:5000,你会看到字符串 Hello World!没有特殊格式,只是纯文本。这意味着你的第一个 web 服务通过了测试,它工作!!图 1.7:浏览器以纯文本形式显示 Hello World

    ![图片

图 1.7:浏览器以纯文本形式显示 Hello World

这是一个非常好的开始!尽管这个 web 服务仅仅返回纯文本字符串,但我们可以在此基础上构建很多东西。

我希望你能看到使用 Flask 构建一个 web 服务是多么简单;实际上,这只需要几行代码。实际上,还有更多的 Flask 扩展可以帮助我们构建复杂的函数。要有耐心,我们将在后续章节中讨论这一点。现在,让我们保持简单,首先熟悉一下 Flask。

对于生产级应用,数据通常存储在数据库中。我们还没有查看如何与数据库交互,所以现在我们将简单地存储在内存中。由于我们正在构建一个食谱分享平台,我们将在下一个练习中创建两个食谱,并将它们存储在内存中。

练习 2:使用 Flask 管理食谱

在这个练习中,我们将使用 Flask 来构建我们的食谱管理应用。我们将实现获取食谱、创建食谱和更新食谱的功能。无需多言,让我们开始吧:

注意

对于完整的代码,请参阅 github.com/TrainingByPackt/Python-API-Development-Fundamentals/tree/master/Lesson01/Exercise02

  1. 首先,清理 app.py 并从头开始,从前面的代码中导入我们需要的这个 web 服务包:

    from flask import Flask, jsonify, request
    

    这里的 jsonify 包是用来将我们的 Python 对象(如列表)转换为 JSON 格式的。它还会将我们的 HTTP 响应的内容类型更改为 application/json。简单来说,它为我们处理了转换为 JSON 格式的繁重工作。

  2. 然后我们导入 HTTPStatus 枚举,它包括不同的 HTTP 状态:

    from http import HTTPStatus
    

    例如,我们将有 HTTPStatus.CREATED (201)HTTPStatus.NOT_FOUND (404)

  3. 创建 Flask 类的实例

    app = Flask(__name__)
    
  4. 定义食谱列表。我们在列表中存储两个食谱。它们存储在内存中

    recipes = [
        {
            'id': 1,
            'name': 'Egg Salad',
            'description': 'This is a lovely egg salad recipe.'
        },
        {
            'id': 2, 'name': 'Tomato Pasta',
            'description': 'This is a lovely tomato pasta recipe.'
        }
    ]
    
  5. 使用路由装饰器告诉 Flask /recipes 路由将路由到 get_recipes 函数,并使用 methods = ['GET'] 参数指定路由装饰器将只响应 GET 请求:

    @app.route('/recipes', methods=['GET'])
    def get_recipes():
    

    注意

    请注意,如果我们没有指定方法参数,默认情况下仍然只会响应 GET 请求。

  6. 之后,使用 jsonify 函数将菜谱列表转换为 JSON 格式,并响应客户端:

        return jsonify({'data': recipes})
    
  7. 获取特定菜谱后,如果您只想检索一个特定的菜谱,请使用 /recipes/<int:recipe_id> 路由来触发 get_recipe(recipe_id) 函数。

    @app.route('/recipes/<int:recipe_id>', methods=['GET'])
    

    <int:recipe_id> 语法表示路由中的值将被分配给整数变量 id,并可以在函数中使用。我们的 get_recipe(recipe_id) 函数将遍历整个 "recipes" 列表,找到具有我们正在寻找的 ID 的菜谱。如果该菜谱存在,则返回它。

  8. 仔细查看我们的 get_recipe 函数。通过使用 recipe = next((recipe for recipe in recipes if recipe['id'] == recipe_id), None) 来获取循环中的下一个菜谱。在这里,for recipe in recipes 这行代码遍历了我们菜谱集合中的所有菜谱,并找出具有 id = recipe_id 的菜谱。一旦找到,我们就将其存储在迭代器中,并使用 next 函数检索它。如果没有找到具有该 ID 的菜谱,则返回 None

    def get_recipe(recipe_id):
        recipe = next((recipe for recipe in recipes if recipe['id'] == recipe_id), None)
        if recipe:
            return jsonify(recipe)
        return jsonify({'message': 'recipe not found'}), HTTPStatus.NOT_FOUND
    
  9. 接下来,我们将处理 create_recipe 函数,该函数在内存中创建菜谱。使用 /recipes 路由到 create_recipe 函数,并使用 "methods = [POST]" 参数指定路由装饰器将只响应 POST 请求:

    @app.route('/recipes', methods=['POST'])
    
  10. 之后,使用 request.get_json 方法从客户端 POST 请求中获取名称和描述。这两个值以及我们生成的自增 ID 将存储在菜谱(字典对象)中,然后追加到我们的菜谱列表中。此时,菜谱已创建并存储:

    def create_recipe():
        data = request.get_json()
        name = data.get('name')
        description = data.get('description')
        recipe = {
            'id': len(recipes) + 1,
            'name': name,
            'description': description
        }
        recipes.append(recipe)
    
  11. 最后,以 JSON 格式返回刚刚创建的菜谱,并带有 HTTP 201 (已创建) 状态。以下代码突出了这一点:

        return jsonify(recipe), HTTPStatus.CREATED 
    
  12. 下一部分代码是关于更新菜谱的。同样,在这里使用相同的代码行,recipe = next((recipe for recipe in recipes if recipe['id'] == recipe_id), None) 来获取具有特定 ID 的菜谱:

    @app.route('/recipes/<int:recipe_id>', methods=['PUT'])
    def update_recipe(recipe_id):
        recipe = next((recipe for recipe in recipes if recipe['id'] == recipe_id), None)
    
  13. 下一行代码表示如果我们找不到菜谱,我们将以 JSON 格式返回 recipe not found 消息,并带有 HTTP NOT_FOUND 状态:

        if not recipe:
            return jsonify({'message': 'recipe not found'}), HTTPStatus.NOT_FOUND
    
  14. 如果我们找到了菜谱,那么执行 recipe.update 函数,并输入从客户端请求中获取的新名称和描述:

        data = request.get_json()
        recipe.update(
            {
                'name': data.get('name'),
                'description': data.get('description')
            }
        )
    
  15. 最后,我们使用 jsonify 函数将更新的菜谱转换为 JSON 格式,并与其一起返回,带有默认的 HTTP 状态 200 (OK)。以下代码突出了这一点:

        return jsonify(recipe)
    
  16. 我们程序中最后几行代码是用于启动 Flask 服务器:

    if __name__ == '__main__':
        app.run()
    
  17. 代码完成后,右键单击 app.py 文件并点击运行以启动应用程序。Flask 服务器将启动,我们的应用程序准备进行测试。完整的代码如下所示:

    from flask import Flask, jsonify, request
    from http import HTTPStatus
    app = Flask(__name__)
    recipes = [
        {
            'id': 1,
            'name': 'Egg Salad',
            'description': 'This is a lovely egg salad recipe.'
        },
        {
            'id': 2, 'name': 'Tomato Pasta',
            'description': 'This is a lovely tomato pasta recipe.'
        }
    ]
    @app.route('/recipes/', methods=['GET'])
    def get_recipes():
        return jsonify({'data': recipes})
    @app.route('/recipes/<int:recipe_id>', methods=['GET'])
    def get_recipe(recipe_id):
        recipe = next((recipe for recipe in recipes if recipe['id'] == recipe_id), None)
        if recipe:
            return jsonify(recipe)
        return jsonify({'message': 'recipe not found'}), HTTPStatus.NOT_FOUND
    @app.route('/recipes', methods=['POST'])
    def create_recipe():
        data = request.get_json()
        name = data.get('name')
        description = data.get('description')
        recipe = {
            'id': len(recipes) + 1,
            'name': name,
            'description': description
        }
        recipes.append(recipe)
        return jsonify(recipe), HTTPStatus.CREATED
    @app.route('/recipes/<int:recipe_id>', methods=['PUT'])
    def update_recipe(recipe_id):
        recipe = next((recipe for recipe in recipes if recipe['id'] == recipe_id), None)
        if not recipe:
            return jsonify({'message': 'recipe not found'}), HTTPStatus.NOT_FOUND 
        data = request.get_json()
        recipe.update(
            {
                'name': data.get('name'),
                'description': data.get('description')
            }
        )
        return jsonify(recipe)
    if __name__ == '__main__':
        app.run()
    

输出如下所示:

图 1.8:最终的 Flask 服务器

图 1.8:最终的 Flask 服务器

在以下章节中,我们将向您展示如何使用 curl/httpie 或 Postman 测试您的网络服务。

使用 curl 或 httpie 测试所有端点

在本节中,我们将介绍如何使用命令提示符测试我们的食谱管理应用程序中的 API 服务端点。测试是应用程序开发中的一个非常重要的步骤。这是为了确保我们开发的功能按预期工作。我们可以使用 curl 或 httpie,具体取决于您的个人偏好。在随后的练习中,我们将向您展示这两个工具。

Curl(或 cURL)是一个可以使用 URL 传输数据的命令行工具。我们可以使用这个工具向我们的 API 端点发送请求并检查响应。如果您正在 macOS 上运行,您不需要安装 curl。它已预安装在系统中,您可以在终端中找到它。您也可以在 PyCharm 的终端中运行它。但是,如果您正在 Windows 上运行,您需要免费下载并安装它,请访问curl.haxx.se/download.html

Httpie(aych-tee-tee-pie)是另一个执行类似功能的命令行客户端。它旨在改善 CLI(命令行界面)与网络之间的通信。它非常用户友好。有关 httpie 的更多详细信息,请参阅httpie.org/

我们之前在requirements.txt中添加了httpie==1.0.2,所以 PyCharm 应该已经为我们安装了它。拥有 httpie 的主要好处是它将美观地格式化 JSON 文档,使其更易于阅读。并且请相信我,当我们继续验证来自服务器的 HTTP 响应时,这将为我们节省大量时间。

练习 3:使用 httpie 和 curl 测试我们的 API 端点

在这个练习中,我们将使用 httpie 和 curl 来测试我们的 API 端点。我们将测试从服务器获取所有菜谱的功能,以及创建/更新菜谱:

  1. 我们首先将在 PyCharm 中打开终端。它位于应用程序的底部。它看起来如下所示:图 1.9:PyCharm 终端

    图 1.9:PyCharm 终端
  2. 输入以下 httpie 命令以从我们的 API 端点获取菜谱,http://localhost:5000/recipes;在这里我们将使用 HTTP GET 方法:

    http GET localhost:5000/recipes
    
  3. 如果您更喜欢使用 curl 的方式,请使用以下命令代替。请注意,这里我们有不同的参数:-i用于显示响应中的头部信息,-X用于指定 HTTP 方法。在这里我们将使用GET

    curl -i -X GET localhost:5000/recipes 
    

    注意

    HTTP/1.0 200 OK
    Content-Length: 175
    Content-Type: application/json
    Date: Mon, 15 Jul 2019 12:40:44 GMT
    Server: Werkzeug/0.15.4 Python/3.7.0
    {
        "data": [
            {
                "description": "This is a lovely egg salad recipe.",
                "id": 1,
                "name": "Egg Salad"
            },
            {
                "description": "This is a lovely tomato pasta recipe.",
                "id": 2,
                "name": "Tomato Pasta"
            }
        ]
    }
    
  4. 然后,让我们创建一个食谱。这次,使用 HTTP POST方法,因为我们有大量信息无法编码在 URL 中。请查看以下 httpie 命令:

    http POST localhost:5000/recipes name="Cheese Pizza" description="This is a lovely cheese pizza"
    
  5. 然后下面是 curl 命令。这里的-H是用来指定请求中的头部的。输入Content-Type: application/json,因为我们将要发送新的食谱详情,格式为 JSON。这里的-d是用来指定 HTTP POST数据,即我们的新食谱:

    curl -i -X POST localhost:5000/recipes -H "Content-Type: application/json" -d '{"name":"Cheese Pizza", "description":"This is a lovely cheese pizza"}'
    
  6. 后端中的@app.route('/recipes', methods=['POST'])用于捕获这个客户端请求并调用create_recipe函数。它将从客户端请求中获取食谱详情并将其保存到应用程序内存中的列表中。一旦食谱成功存储在内存中,它将返回 HTTP 状态201 CREATED,并且新的食谱也将作为 HTTP 响应返回,以便我们进行验证:

    HTTP/1.0 201 CREATED
    Content-Length: 77
    Content-Type: application/json
    Date: Mon, 15 Jul 2019 14:26:11 GMT
    Server: Werkzeug/0.15.4 Python/3.7.0
    {
        "description": "This is a lovely cheese pizza",
        "id": 3,
        "name": "Cheese Pizza"
    }
    
  7. 现在,再次获取所有食谱以验证我们之前的食谱是否真的成功创建。我们现在期望在响应中接收到三个食谱:

    http GET localhost:5000/recipes 
    curl -i -X GET localhost:5000/recipes 
    
  8. 使用上述命令中的任何一个。它们做的是同一件事,即触发get_recipes函数,并获取当前存储在应用程序内存中的所有食谱的 JSON 格式。

    在以下响应中,我们可以看到 HTTP 头部表示 OK,并且 Content-Length 现在略长于我们之前的响应,即252字节。这是有道理的,因为我们期望在响应中看到另一个食谱。Content-Type 仍然是application/json,其中正文以 JSON 格式存储食谱。现在我们可以看到我们的新食谱,ID 为3

    HTTP/1.0 200 OK
    Content-Length: 252
    Content-Type: application/json
    Date: Tue, 16 Jul 2019 01:55:30 GMT
    Server: Werkzeug/0.15.4 Python/3.7.0
    {
        "data": [
            {
                "description": "This is a lovely egg salad recipe.",
                "id": 1,
                "name": "Egg Salad"
            },
            {
                "description": "This is a lovely tomato pasta recipe.",
                "id": 2,
                "name": "Tomato Pasta"
            },
            {
                "description": "This is a lovely cheese pizza",
                "id": 3,
                "name": "Cheese Pizza"
            }
        ]
    }
    
  9. 太酷了!到目前为止,我们的状态相当不错。现在,通过尝试修改 ID 为 3 的食谱来测试我们的应用程序。使用 HTTP PUT方法,将修改后的食谱名称和描述发送到localhost:5000/recipes/3

    http PUT localhost:5000/recipes/3 name="Lovely Cheese Pizza" description="This is a lovely cheese pizza recipe."
    

    下面的 curl 命令。同样,-H是用来指定 HTTP 请求中的头部的,我们将它设置为"Content-Type: application/json"-d是用来指定我们的数据应该以 JSON 格式:

    curl -i -X PUT localhost:5000/recipes/3 -H "Content-Type: application/json" -d '{"name":"Lovely Cheese Pizza", "description":"This is a lovely cheese pizza recipe."}'
    
  10. 如果一切正常,那么客户端请求将被@app.route('/recipes/<int:recipe_id>', methods=['PUT'])路由捕获。然后它将调用update_recipe(recipe_id)函数来查找传递的recipe_id对应的食谱,更新它,并返回。我们将收到更新后的食谱的 JSON 格式,以及 HTTP 状态OK200):

    HTTP/1.0 200 OK
    Content-Length: 92
    Content-Type: application/json
    Date: Tue, 16 Jul 2019 02:04:57 GMT
    Server: Werkzeug/0.15.4 Python/3.7.0
    {
        "description": "This is a lovely cheese pizza recipe.",
        "id": 3,
        "name": "Lovely Cheese Pizza"
    }
    
  11. 好的,到目前为止一切顺利。现在,继续看看我们是否可以获取特定的食谱。为此,向localhost:5000/recipes/3发送请求以获取 ID 为3的食谱,并确认我们之前的更新是否成功:

    http GET localhost:5000/recipes/3
    

    我们也可以使用curl命令:

    curl -i -X GET localhost:5000/recipes/3 
    
  12. 应用程序将寻找具有recipe_id的食谱,并以 JSON 格式返回它,同时返回 HTTP 状态200 OK

    HTTP/1.0 200 OK
    Content-Length: 92
    Content-Type: application/json
    Date: Tue, 16 Jul 2019 06:10:49 GMT
    Server: Werkzeug/0.15.4 Python/3.7.0
    {
        "description": "This is a lovely cheese pizza recipe.",
        "id": 3,
        "name": "Lovely Cheese Pizza"
    }
    
  13. 现在,如果我们尝试一个我们知道不存在的食谱 ID,应用程序会如何表现?使用以下 httpie 命令进行测试:

    http GET localhost:5000/recipes/101
    

    或者,使用以下 curl 命令,它将执行与前面代码相同的功能:

    curl -i -X GET localhost:5000/recipes/101 
    
  14. 类似地,应用程序中的 @app.route('/recipes/<int:recipe_id>', methods=['GET']) 将捕获这个客户端请求并尝试查找 ID 为 101 的食谱。应用程序将以 JSON 格式返回 HTTP 状态 message: "recipe not found"

    HTTP/1.0 404 NOT FOUND
    Content-Length: 31
    Content-Type: application/json
    Date: Tue, 16 Jul 2019 06:15:31 GMT
    Server: Werkzeug/0.15.4 Python/3.7.0
    {
        "message": "recipe not found"
    }
    

如果您的应用程序通过了测试,恭喜您!这是一个相当稳固的实现。如果您想的话,可以选择自行进行更多测试。

Postman

Postman 是一个方便的 API 测试工具。它有一个用户友好的图形用户界面,我们可以通过它发送 HTTP 请求。它允许我们使用不同的 HTTP 方法(即 GET、POST、PUT 和 DELETE)发送请求,并且我们可以检查来自服务器的响应。使用这个工具,我们可以通过发送客户端请求并检查 HTTP 响应来轻松测试我们的 API。我们还可以保存我们的测试用例并将它们分组到不同的集合中。

Postman 图形用户界面

我们假设您已经按照前言中的步骤安装了 Postman。当您打开 Postman 时,您应该看到以下截图所示的屏幕。左侧是一个导航面板,用于您浏览历史或保存的请求。在 Postman 中,您的请求将被组织到集合中,就像文件系统中的文件夹一样。您可以将相关的保存请求放在同一个集合中。

顶部面板用于您编写请求。如您从命令行测试工具中学到的,我们可以有不同的 HTTP 动词(如 GET 和 PUT)。我们还需要输入 API 端点以发送请求。对于某些请求,您可能还需要传递额外的参数。所有这些都可以在 Postman 中完成。

底部面板显示了服务器响应:

图 1.10:Postman 界面

图 1.10:Postman 界面

发送 GET 请求

发送 GET 请求很简单;我们只需要填写目标 URL:

  1. 在下拉列表中选择我们的 HTTP 方法为 GET

  2. 输入请求 URL(例如 http://localhost:5000/API1)。

  3. 点击 发送 按钮。

发送 POST 请求

发送 POST 请求需要做更多的工作,因为通常我们会在请求中放入额外的数据。例如,如果您想向 API 端点发送一些 JSON 数据,可以执行以下操作:

  1. 在下拉列表中选择我们的 HTTP 方法为 POST

  2. 输入请求 URL(例如 http://localhost:5000/API2)。

  3. 选择 正文 选项卡。同时,选择“raw”单选按钮。

  4. 从右侧下拉菜单中选择“JSON (application/json)”。将 JSON 数据放入正文内容区域:

    {
         "key1": "value1",
         "key2": "value2"
    }
    
  5. 点击 发送 按钮。

保存请求

非常常见,您可能希望保存请求以供以后使用。Postman 中的此保存功能在回归测试期间特别有用。要保存请求,您只需点击保存按钮,遵循屏幕上的说明,并将其保存到集合中。然后您将在左侧导航面板中看到您保存的请求。

注意

在您能够保存请求之前,可能需要在 Postman 中创建一个账户。请按照屏幕上的说明操作。

如果您想了解更多关于 Postman 的信息,请点击 Postman 底部的“训练营”按钮。您将在屏幕上看到交互式教程,逐步向您展示如何使用 Postman。

活动 1:使用 Postman 向我们的 API 发送请求

现在我们已经学会了如何使用 Postman,我们将使用 Postman 而不是 curl/httpie 命令行测试工具来测试我们的应用程序。在这个活动中,我们将使用此工具来测试我们网络服务的 CRUD 功能:

  1. 在 Postman 中创建一个请求并获取所有菜谱。

  2. 使用 POST 请求创建菜谱。

  3. 创建一个获取所有菜谱的请求。

  4. 发送更新请求以修改我们刚刚创建的菜谱。

  5. 发送请求以获取特定的菜谱。

  6. 发送请求以搜索一个不存在的菜谱。

    注意

    本活动的解决方案可以在第 286 页找到。

如果您的应用程序通过了测试,恭喜您!这是一个相当稳健的实现。

练习 4:使用 Postman 进行自动化测试

在这个练习中,我们想向您展示如何使用 Postman 作为一款强大的自动测试工具。自动测试工具允许我们反复向 API 发送请求,从而实现测试自动化。Postman 允许我们这样做。我们可以将历史请求保存到集合中,以便您下次可以重用相同的测试用例:

  1. 将鼠标悬停在请求上;将出现保存请求按钮:图 1.11:保存请求

    图 1.11:保存请求
  2. 点击“保存请求”按钮,您将看到一个对话框弹出,要求输入更多信息。将请求名称输入为获取所有菜谱,然后在底部点击创建集合。然后,将集合名称输入为基本 API并勾选以确认。点击保存到基本 API图 1.12:输入保存请求的信息

    图 1.12:输入保存请求的信息
  3. 集合将随后创建。现在,将我们的请求保存到此集合以供将来使用。我们还可以点击集合选项卡来查看该集合中的所有请求:图 1.13:创建新集合

图 1.13:创建新集合

现在我们收集中有许多已保存的请求。下次,如果我们对我们的应用程序进行任何更改,我们可以重新运行这些测试以确保之前开发的 API 仍然运行良好。这在开发者社区中被称为回归测试。Postman 是我们执行此类测试的一个简单而强大的工具。

活动二:实现并测试 delete_recipe 函数

现在我们已经对如何实现 API 有了一个基本了解。我们已经编写了创建和更新食谱的函数。在本活动中,你将亲自实现 delete_recipe 函数。

你已经了解了命令行和 GUI 测试工具。在实现之后,你将使用这些工具测试应用程序。你需要做的是:

  1. app.py 中实现一个 delete_recipe 函数,可以删除特定的食谱。相应地创建 API 端点。

  2. 启动应用程序,使其准备就绪以进行测试。

  3. 使用 httpie 或 curl 删除 ID = 1 的食谱。

  4. 使用 Postman 删除 ID = 2 的食谱。

    注意

    本活动的解决方案可以在第 291 页找到。

摘要

在本章中,我们使用 Flask 构建了一个基本的 RESTful API。我们在我们的食谱上执行了 CRUD(创建、读取、更新、删除)操作,通过这个过程,你应该已经掌握了 API 的概念和基础。我们还讨论了相关概念,例如 HTTP 方法、HTTP 状态码、JSON 和路由。我们通过向您展示测试我们构建的 Web 服务不同方式(命令提示符、GUI)来结束本章。

在打下良好基础之后,在下一章中,我们将逐步开发我们的食谱分享平台。你将学习 RESTful API 开发的整个过程。只需继续跟随我们,最好的还在后面!

第二章:2. 开始构建我们的项目

学习目标

到本章结束时,你将能够:

  • 使用 Flask-Restful 包高效构建 Restful API 服务

  • 构建一个可扩展的 Flask 项目

  • 使用模型执行 CRUD 操作

  • 使用 curl、httpie 和 Postman 测试 RESTful API

在本章中,我们将开始着手构建食品菜谱分享平台,并学习如何创建 RESTful API 应用程序。

简介

现在我们已经介绍了 API 并了解了一些关于 HTTP 和 REST 的知识,我们将着手构建一个应用程序(名为 Smilecook 的菜谱分享应用程序)。在本章中,我们的目标是启动实际项目开发。这是一个用户可以创建账户并与其他用户分享他们自己的菜谱的菜谱分享平台。正如你所想象的那样,它将包含许多 API 端点,以便我们的用户可以管理他们的菜谱。我们将使用 Flask-RESTful 包来高效地开发我们的 RESTful API。

本章将讨论这些菜谱的CRUD创建、读取、更新、删除)操作,以及如何设置菜谱的发布状态。

什么是 Flask-RESTful?

Flask-RESTful 是一个 Flask 扩展,它允许我们快速开发 RESTful API。与我们在上一章中讨论的内置包装器@app.route('/')相比,Flask-RESTful 允许我们以更好、更简单的方式维护和结构化 API 端点。

在本章中,我们将使用这个 Flask 扩展来开发我们的项目,这样你将看到我们如何构建我们的端点。

使用 Flask-RESTful 开发我们的菜谱分享平台,“Smilecook”

在这本书中,我们将开发一个名为Smilecook的菜谱分享平台。从本章开始,我们将开始向其中添加功能。我们相信这种方法将帮助你了解你需要的关键概念和技能,以便你可以开发这个应用程序并帮助它发挥其全部潜力,同时帮助你理解整个开发流程。

首先,我们将构建菜谱的基本 CRUD 功能。Flask-RESTful 包允许我们以更全面的方式组织我们的代码。我们将在资源中定义某些方法并将它们链接到端点。例如,GET 请求的流程将是请求被发送到端点(http://localhost:5000/recipes),然后由我们在资源中将要实现的GET方法处理。这将导致菜谱返回给我们。

除了基本的 CRUD 功能外,我们还将实现这些菜谱的发布和取消发布功能。这可以通过PUTDELETE方法完成,这些方法可以在RecipePublishResource类中找到。我们将这两个方法链接到http://localhost:5000/recipes/1/publish端点(对于 ID 为 1 的菜谱)。关于我们端点设计的详细信息,请参考以下表格:

图 2.1:我们端点设计的细节

虚拟环境

PyCharm 将帮助我们创建虚拟环境。我们希望在独立的虚拟环境中开发我们的项目,以便将其隔离。因此,我们将对我们将要使用的包的版本拥有绝对的控制权。

学习的最佳方式是通过实践。现在让我们动手试试吧!

练习 5:在 PyCharm 中创建开发项目

在你开始开发 Python 应用程序之前,你需要在 PyCharm 中创建一个开发项目。PyCharm 使用项目来管理事物。在这个练习中,你将学习如何在 PyCharm 中创建一个名为 Smilecook 的新开发项目。你还需要为这个项目安装必要的包。让我们开始吧:

  1. 创建项目并命名为smilecook图片

    图 2.2:创建项目
  2. 检查项目结构并确保已创建虚拟环境。一旦创建了模块,我们就能在左侧面板上看到项目的层次结构。我们可以看到项目文件夹下的venv文件夹,这是由 PyCharm 创建和激活的。现在,当我们在这个项目下编写代码时,它将在虚拟环境中运行!图片

    图 2.3:检查项目结构和确保已创建虚拟环境
  3. 安装本章所需的包。为此,在我们的项目文件夹下创建一个名为requirements.txt的文件。输入以下代码以指定您想要安装的包:

    Flask==1.0.3
    Flask-RESTful==0.3.7
    httpie==1.0.3
    
  4. 使用pip命令安装这些包。之后,在pip命令中安装我们在requirements.txt文件中指定的包:

    pip install -r requirements.txt
    
  5. 你现在应该能在下面的屏幕截图中看到类似的内容。在这里,我们可以看到包正在虚拟环境中安装!图片

图 2.4:在虚拟环境中安装包

恭喜!你已经为我们的 Smilecook 应用程序创建了一个 PyCharm 项目。这是你作为开发者踏上旅程的第一步!

创建菜谱模型

如你所想,一个菜谱可能有多个属性。为了保存这些属性的每一个细节,我们将使用类来模拟菜谱。这个菜谱类将具有几个基本属性。

下面是我们在菜谱类中将要定义的属性的简要描述:

  • name:菜谱的名称。

  • description:菜谱的描述。

  • num_of_servings:份量。

  • cook_time:所需的烹饪时间。这是一个以秒为单位的整数。

  • directions:步骤。

  • is_publish:菜谱的发布状态;默认为草稿。

在下一个练习中,我们将向您展示如何编码菜谱类,使其具有这些属性。

练习 6:创建菜谱模型

在这个练习中,我们将逐步编写食谱模型。recipe 类将包含我们之前讨论过的属性。本练习的代码文件可以在 Lesson2/Exercise06/models/recipe.py 中找到。

现在,让我们创建一个食谱类:

  1. 在项目名称,即 Smilecook 上,右键点击 创建一个 Python 包。将其命名为 models

    图 2.5:创建一个 Python 包并将其命名为 models
  2. 然后,在 models 下创建一个名为 recipe.py 的文件,并输入以下代码:

    recipe_list = []
    def get_last_id():
        if recipe_list:
            last_recipe = recipe_list[-1]
        else:
            return 1
        return last_recipe.id + 1
    

    让我们暂停一下,检查这里的代码。首先,我们定义 recipe_list = [] 以便在应用程序内存中存储食谱。然后,我们定义 get_last_id 函数以获取我们最后一个食谱的 ID。稍后,当我们创建一个新的食谱时,我们将使用此方法来评估 recipe_list 中的最后一个 ID,以便我们可以为新的食谱生成一个新的 ID。

  3. 使用以下代码定义食谱类。在 recipe.py 文件中,在 get_last_id 函数之后输入以下代码:

    class Recipe:
        def __init__(self, name, description, num_of_servings, cook_time, directions):
            self.id = get_last_id()
            self.name = name
            self.description = description
            self.num_of_servings = num_of_servings
            self.cook_time = cook_time
            self.directions = directions
            self.is_publish = False
    

    Recipe 类有一个 __init__ 构造方法,它将接受如 namedescriptionnum_of_servingscook_timedirections 等参数,并根据这些参数创建食谱对象。ID 是自增的,is_publish 默认设置为 false。这意味着默认情况下,食谱将被设置为草稿(未发布)。

  4. 在相同的 Recipe 类中,定义一个 data 方法,用于将数据作为字典对象返回。你可能会记得,在 Python 中,缩进很重要。以下代码是缩进的,因为它位于 Recipe 类内部:

        @property
        def data(self):
            return {
                'id': self.id,
                'name': self.name,
                'description': self.description,
                'num_of_servings': self.num_of_servings,
                'cook_time': self.cook_time,
                'directions': self.directions
            }
    

现在我们已经构建了食谱模型,我们将继续使用 Flask-RESTful 构建 API 端点。

资源路由

Flask-RESTful 的主要构建块是资源。资源建立在 Flask 的可插拔视图之上。资源路由的概念是我们希望将所有客户端请求围绕资源进行结构化。在我们的食谱分享平台上,我们将对 RecipeResource 下的食谱的 CRUD 操作进行分组。对于发布和取消发布操作,我们将它们分组在不同的 RecipePublishResource 下。这为其他开发者提供了一个清晰的架构。

我们可以实施这些资源的方式很简单:我们只需要从 flask_restful.Resource 类继承,并实现其中对应 HTTP 动词的方法。

在下一个练习中,我们将定义三个子类:一个用于食谱集合,一个用于单个食谱,一个用于发布食谱。

练习 7:为食谱模型定义 API 端点

要构建 API 端点,我们需要定义一个继承自 flask_restful.Resource 的类。然后,我们可以在类内部声明 get 和 post 方法。让我们开始吧:

  1. 在项目下创建一个名为 resources 的文件夹,然后在 resources 文件夹下创建一个名为 recipe.py 的文件。

    注意

    该代码文件可以在github.com/TrainingByPackt/Python-API-Development-Fundamentals/tree/master/Lesson02/Exercise07/resources中找到。

  2. 使用以下代码导入必要的包、类和函数:

    from flask import request
    from flask_restful import Resource
    from http import HTTPStatus
    from models.recipe import Recipe, recipe_list
    
  3. 在前面的代码导入之后,创建RecipeListResource类。这个类有GETPOST方法,分别用于获取和创建食谱资源。我们首先完成GET方法:

    class RecipeListResource(Resource):
        def get(self):
            data = []
            for recipe in recipe_list:
                if recipe.is_publish is True:
                    data.append(recipe.data)
            return {'data': data}, HTTPStatus.OK
    

    在这里,我们创建并实现了RecipeListResource类,它继承自flask-restful.Resource。我们实现的get方法用于获取所有公开的食谱。它是通过声明一个data列表,并在recipe_list中获取所有is_publish等于true的食谱来完成的。这些食谱被追加到我们的data列表中,并返回给用户。

  4. 添加post方法。这是用来创建食谱的:

        def post(self):
            data = request.get_json()
            recipe = Recipe(name=data['name'],
                            description=data['description'],
                            num_of_servings=data['num_of_servings'],
                            cook_time=data['cook_time'],
                            directions=data['directions'])
            recipe_list.append(recipe)
            return recipe.data, HTTPStatus.CREATED
    

在这个练习中,我们构建了两个方法来处理 GET 和 POST 客户端请求。以下表格总结了我们在本练习中构建的方法:

图片

图 2.6:在这个练习中我们使用的客户端请求方法

注意

我们跳过了在将数据返回给客户端之前对对象进行序列化的步骤,因为 Flask-RESTful 已经在幕后为我们做了这件事。

我们在本练习中构建的post方法用于创建新的食谱。这是一个POST方法。它是通过使用request.get_json从请求中获取 JSON 数据,然后创建食谱对象并将其存储在recipe_list中来完成的。最后,它返回带有 HTTP 状态码201 CREATED的食谱记录。

练习 8:定义食谱资源

在这个练习中,我们将定义食谱资源。我们将使用两种方法:get方法,用于获取单个食谱;以及put方法,用于更新食谱。让我们开始吧:

  1. 定义RecipeResource资源,并使用以下示例代码实现get方法:

    class RecipeResource(Resource):
        def get(self, recipe_id):
            recipe = next((recipe for recipe in recipe_list if recipe.id == recipe_id and recipe.is_publish == True), None)
            if recipe is None:
                return {'message': 'recipe not found'}, HTTPStatus.NOT_FOUND
            return recipe.data, HTTPStatus.OK
    

    类似地,RecipeResource也继承自flask-restful.Resource。我们在这里实现的get方法是获取单个食谱。我们通过在recipe_list中搜索recipe_id来实现这一点。我们只会返回那些is_publish = true的食谱。如果没有找到这样的食谱,我们将返回消息食谱未找到。否则,我们将返回食谱,并附带 HTTP 状态200 OK

  2. 使用以下代码实现put方法:

        def put(self, recipe_id):
            data = request.get_json()
            recipe = next((recipe for recipe in recipe_list if recipe.id == recipe_id), None)
            if recipe is None:
                return {'message': 'recipe not found'}, HTTPStatus.NOT_FOUND
            recipe.name = data['name']
            recipe.description = data['description']
            recipe.num_of_servings = data['num_of_servings']
            recipe.cook_time = data['cook_time']
            recipe.directions = data['directions']
            return recipe.data, HTTPStatus.OK
    

    我们在这里实现的第二个方法是put。它通过使用request.get_json从客户端请求中获取食谱详情,并更新食谱对象。如果一切顺利,它将返回 HTTP 状态码200 OK

在这里,我们为食谱资源构建了两个方法。GETPUT 方法用于处理相应的客户端请求。下表显示了在本练习中为 RecipeResource 类构建的方法:

图片

图 2.7:为 RecipeResource 类构建的函数

练习 9:发布和取消发布食谱

在前面的练习中,我们创建了食谱资源和它们相关的方法。现在,我们的 Smilecook 应用程序可以对食谱进行读写操作。然而,在本章的开头,我们提到食谱可以有两种状态(未发布和已发布)。这允许用户在将食谱发布到世界之前继续更新它们的未发布食谱。在本练习中,我们将定义发布和取消发布食谱的资源。让我们开始吧:

  1. 定义 RecipePublic 资源并实现一个 put 方法,它将处理 HTTP PUT 请求:

    class RecipePublishResource(Resource):
        def put(self, recipe_id):
            recipe = next((recipe for recipe in recipe_list if recipe.id == recipe_id), None)
            if recipe is None:
                return {'message': 'recipe not found'}, HTTPStatus.NOT_FOUND
            recipe.is_publish = True
            return {}, HTTPStatus.NO_CONTENT
    

    RecipePublishResource 继承自 flask_restful.Resourceput 方法将定位带有传入的 recipe_id 的食谱,并将 is_publish 状态更新为 true。然后,它将返回 HTTPStatus.NO_CONTENT,这表明食谱已成功发布。

  2. 实现一个 delete 方法,它将处理 HTTP DELETE 请求:

        def delete(self, recipe_id):
            recipe = next((recipe for recipe in recipe_list if recipe.id == recipe_id), None)
            if recipe is None:
                return {'message': 'recipe not found'}, HTTPStatus.NOT_FOUND
            recipe.is_publish = False
            return {}, HTTPStatus.NO_CONTENT
    

    delete 方法是 put 方法的相反。它不是将 is_publish 设置为 true,而是将其设置为 false 以取消发布食谱。

    你也可以看到我们以灵活的方式使用这些方法;put 方法不一定用于更新,delete 方法也不一定用于删除。

下表显示了在本练习中我们创建的所有函数。现在我们已经准备好了所有三个资源(RecipeListResourceRecipeResourceRecipePublishResource),我们将讨论端点配置:

图片

图 2.8:本练习中使用的函数

注意

如果客户端请求使用在资源中没有相应处理方法的 HTTP 动词,Flask-RESTful 将返回 HTTP 状态码 405 方法不允许

配置端点

现在我们已经定义了所有资源,我们将设置一些端点,以便用户可以向它们发送请求。这些端点可供用户访问,并连接到特定资源。我们将使用 API 对象上的 add_resource 方法来指定这些端点的 URL,并将客户端 HTTP 请求路由到我们的资源。

例如,api.add_resource(RecipeListResource, '/recipes') 语法用于将路由(相对 URL 路径)链接到 RecipeListResource,以便 HTTP 请求将指向此资源。根据 HTTP 动词(例如,GETPOST),请求将由资源中的相应方法相应处理。

练习 10:创建主应用程序文件

在这个练习中,我们将创建我们的 app.py 文件,这是我们主要的应用程序文件。我们将在那里设置 Flask 并初始化我们的 flask_restful.API。最后,我们将设置端点,以便用户可以向我们的后端服务发送请求。让我们开始吧:

  1. 在项目文件夹下创建 app.py 文件。

  2. 使用以下代码导入必要的类:

    from flask import Flask
    from flask_restful import Api
    from resources.recipe import RecipeListResource, RecipeResource, RecipePublishResource
    
  3. 使用我们的 Flask 应用设置 Flask 并初始化 flask_restful.API

    app = Flask(__name__)
    api = Api(app)
    
  4. 通过传递 URL 来添加资源路由,以便它将路由到我们的资源。每个资源都将定义其自己的 HTTP 方法:

    api.add_resource(RecipeListResource, '/recipes') 
    api.add_resource(RecipeResource, '/recipes/<int:recipe_id>')
    api.add_resource(RecipePublishResource, '/recipes/<int:recipe_id>/publish')
    if __name__ == '__main__':
        app.run(port=5000, debug=True)
    

    注意

    RecipeListResource 中,我们定义了 getpost 方法。因此,当有 GET HTTP 请求到 "/recipes" URL 路由时,它将调用 RecipeListResource 下的 get 方法,并获取所有已发布的食谱。

    在前面的代码中,你会注意到我们使用了 <int: recipe_id >。它在代码中作为食谱 ID 的占位符。当向 route "/recipes/2" URL 发送 GET HTTP 请求时,这将调用 RecipeResource 下的 get 方法,带有参数,即 recipe_id = 2

  5. 保存 app.py 文件,然后右键单击它以运行应用程序。Flask 将在 localhost (127.0.0.1) 的端口 5000 上启动并运行:

图片

图 2.9:Flask 在 localhost 上启动并运行

恭喜!你已经完成了 API 端点。现在,让我们继续进行测试。你可以在 curl/httpie 或 Postman 中进行测试。

使用 curl 和 httpie 向 Flask API 发送 HTTP 请求

现在,我们将使用 httpiecurl 命令来测试我们的 API 端点。我们将测试从服务器获取所有食谱的函数以及创建/更新/删除、发布和取消发布的食谱。学习这个的最佳方式是通过动手练习。让我们开始吧!

练习 11:使用 curl 和 httpie 测试端点

在这个练习中,我们将使用 httpie 和 curl 命令向端点发送请求,以便我们可以创建我们的第一个食谱。我们希望你能够熟悉使用 httpie 和 curl 命令行测试工具。让我们开始吧:

  1. 在 PyCharm 中打开终端并输入以下命令。你可以使用 httpie 或 curl 命令。以下是一个 httpie 命令(= 是字符串,:= 是非字符串):

    http POST localhost:5000/recipes name="Cheese Pizza" description="This is a lovely cheese pizza" num_of_servings:=2 cook_time:=30 directions="This is how you make it"
    

    以下是一个 curl 命令。-H 参数用于指定客户端请求中的头信息。在这里,我们将设置 Content-Type: application/json 作为头信息。-d 参数用于 HTTP POST 数据,即 JSON 格式的食谱:

    curl -i -X POST localhost:5000/recipes -H "Content-Type: application/json" -d '{"name":"Cheese Pizza", "description":"This is a lovely cheese pizza", "num_of_servings":2, "cook_time":30, "directions":"This is how you make it" }'
    
  2. 检查响应,你应该看到以下内容。仔细检查它,它应该与我们在 步骤 1 中请求的相同食谱:

    HTTP/1.0 201 CREATED
    Content-Type: application/json
    Content-Length: 188
    Server: Werkzeug/0.16.0 Python/3.7.0
    Date: Sun, 03 Nov 2019 03:19:00 GMT
    {
        "id": 1,
        "name": "Cheese Pizza",
        "description": "This is a lovely cheese pizza",
        "num_of_servings": 2,
        "cook_time": 30,
        "directions": "This is how you make it"
    }
    

    注意

    一旦使用 HTTP POST 方法将客户端请求发送到服务器,RecipeResource 中的 post 方法将捕获请求并将食谱保存到应用程序内存中。新的食谱将被追加到 recipe_list 中。一旦完成所有操作,它将返回 HTTP 201 CREATED 状态码和以 JSON 格式的新创建的食谱。

我们已经在平台上成功创建了第一个食谱。这个食谱存储在服务器端,我们已经有获取它的 API。让我们继续创建第二个食谱并一次性检索所有食谱。

练习 12:测试自动递增的食谱 ID

现在我们已经在 Smilecook 应用程序中实现了自动递增的 ID,让我们看看它在实际中是如何工作的。在这个练习中,我们将使用 httpie 和 curl 命令创建第二个食谱。注意,第二个食谱的 ID 会自动递增。让我们开始吧:

  1. 创建第二个食谱并注意 ID 会自动递增。使用 httpie 发送以下客户端请求:

    http POST localhost:5000/recipes name="Tomato Pasta" description="This is a lovely tomato pasta recipe" num_of_servings:=3 cook_time:=20 directions="This is how you make it"
    

    或者,使用 curl 发送请求。同样,-H 参数用于指定客户端请求中的头部。这里我们将设置 "Content-Type: application/json" 作为头部。-d 参数用于 HTTP POST 数据,意味着食谱是以 JSON 格式:

    curl -i -X POST localhost:5000/recipes -H "Content-Type: application/json" -d '{"name":"Tomato Pasta", "description":"This is a lovely tomato pasta recipe", "num_of_servings":3, "cook_time":20, "directions":"This is how you make it"}'
    
  2. 你应该看到以下响应。仔细检查它,它应该与我们在 步骤 1 中请求的相同食谱:

    HTTP/1.0 201 CREATED
    Content-Type: application/json
    Content-Length: 195
    Server: Werkzeug/0.16.0 Python/3.7.0
    Date: Sun, 03 Nov 2019 03:23:37 GMT
    
    {
        "id": 2,
        "name": "Tomato Pasta",
        "description": "This is a lovely tomato pasta recipe",
        "num_of_servings": 3,
        "cook_time": 20,
        "directions": "This is how you make it"
    }
    

    一旦使用 HTTP POST 方法将前面的客户端请求发送到服务器,RecipeResource 中的 post 方法将捕获请求并将食谱保存到应用程序内存中。新的食谱将被追加到 recipe_list 中。这次,ID 将自动分配为 2。

练习 13:获取所有食谱

在这个练习中,我们将使用 httpie 和 curl 命令检索我们创建的所有食谱。我们这样做是为了确保我们的食谱在后端服务器上。让我们开始吧:

  1. 通过使用 httpie 发送以下客户端请求来检索所有食谱:

    http GET localhost:5000/recipes
    

    或者,使用 curl 发送以下请求。-i 参数用于表示我们想要看到响应头部。-X GET 表示我们正在使用 HTTP GET 方法发送客户端请求:

    curl -i -X GET localhost:5000/recipes 
    
  2. 你应该看到以下响应。请仔细检查:

    HTTP/1.0 200 OK
    Content-Length: 19
    Content-Type: application/json
    Date: Sun, 03 Nov 2019 03:24:53 GMT
    Server: Werkzeug/0.16.0 Python/3.7.0
    
    {
        "data": []
    }
    

    一旦使用 HTTP GET 方法将前面的客户端请求发送到服务器,RecipeResource 中的 get 方法将捕获请求并从应用程序内存中的 recipe_list 中检索所有已发布的食谱。

    注意

    我们应该在 HTTP 响应中看到一个空列表,因为我们之前步骤中创建的所有食谱都是草稿形式(未发布)。

练习 14:测试食谱资源

我们已经测试了我们围绕食谱资源构建的端点。在这个练习中,我们将继续使用 httpie 和 curl 命令来测试食谱发布 API。我们可以通过向 API 端点发送请求来发布我们的食谱来测试它。让我们开始吧:

  1. 修改 ID 为 1 的食谱的发布状态。我们可以使用 httpie 命令发送以下客户端请求:

    http PUT localhost:5000/recipes/1/publish
    

    或者,我们可以使用以下 curl 命令:

    curl -i -X PUT localhost:5000/recipes/1/publish 
    

    注意

    一旦使用 HTTP PUT 方法将前面的客户端请求发送到服务器,RecipePublishResource中的put方法将接收到请求并将recipe_id设置为 1。应用程序将寻找 ID 为1的食谱并更新其发布状态为True

  2. 您应该看到以下响应。请仔细检查:

    HTTP/1.0 204 NO CONTENT
    Content-Type: application/json
    Date: Sun, 03 Nov 2019 03:25:48 GMT
    Server: Werkzeug/0.16.0 Python/3.7.0
    
  3. 现在,检索所有已发布的食谱并检查它们。然后,使用 httpie 发送以下客户端请求:

    http GET localhost:5000/recipes
    

    或者,使用 curl 发送以下请求。-i参数表示我们想要看到响应头。-X GET表示我们正在使用 HTTP GET 方法发送客户端请求:

    curl -i -X GET localhost:5000/recipes
    
  4. 您应该看到以下响应。请仔细检查:

    HTTP/1.0 200 OK
    Content-Type: application/json
    Content-Length: 276
    Server: Werkzeug/0.16.0 Python/3.7.0
    Date: Sun, 03 Nov 2019 03:26:43 GMT
    
    {
        "data": [
            {
                "id": 1,
                "name": "Cheese Pizza",
                "description": "This is a lovely cheese pizza",
                "num_of_servings": 2,
                "cook_time": 30,
                "directions": "This is how you make it"
            }
        ]
    }
    

    一旦使用 HTTP GET方法将前面的客户端请求发送到服务器,RecipeResource中的 get 方法将接收到请求并从应用程序内存中的recipe_list检索所有已发布的食谱。这次,因为 ID 为 1 的食谱已被设置为发布,所以我们应该在 HTTP 响应中看到它。

练习 15:负面测试

在上一个练习中,我们成功发布了我们的食谱。这是好事,因为它表明我们开发的 API 是有效的。但测试的全部目的是发现潜在的问题(如果有的话)。我们在这里可以执行所谓的负面测试。这是故意使用不想要的输入测试场景的过程。这个练习将测试一个没有在资源中定义相应方法的 HTTP 动词的请求。让我们开始吧:

  1. 向服务器发送以下请求。这个 HTTP 方法尚未定义;让我们看看会发生什么:

    http DELETE localhost:5000/recipes
    

    以下是一个 curl 命令,它执行相同的事情:

    curl -i -X DELETE localhost:5000/recipes 
    
  2. 您应该看到以下响应。请仔细检查:

    HTTP/1.0 405 METHOD NOT ALLOWED
    Content-Type: application/json
    Content-Length: 70
    Allow: POST, GET, HEAD, OPTIONS
    Server: Werkzeug/0.16.0 Python/3.7.0
    Date: Sun, 03 Nov 2019 03:27:37 GMT
    
    {
        "message": "The method is not allowed for the requested URL."
    }
    

    我们应该看到一个带有RecipeListResource HTTP 状态的响应。

负面测试很重要。我们总是希望我们的测试更加完整,覆盖更多场景。

练习 16:修改食谱

在我们的 Smilecook 应用程序中,作者被允许更新他们的食谱。它就像一个博客平台,作者可以在发布后花时间完善他们的作品。由于我们已经构建了 API,我们希望使用 Postman 来测试它。让我们开始吧:

  1. 使用 PUT 方法向localhost:5000/recipes/1发送请求,并附带新的食谱详情:

    http PUT localhost:5000/recipes/1 name="Lovely Cheese Pizza" description="This is a lovely cheese pizza recipe" num_of_servings:=3 cook_time:=60 directions="This is how you make it"
    

    或者,使用 curl 发送以下请求。-H 参数用于指定客户端请求中的头信息。在这里,我们将设置头信息为 "Content-Type: application/json"。-d 参数用于 HTTP POST 数据,意味着菜谱将以 JSON 格式存在:

    curl -i -X PUT localhost:5000/recipes/1 -H "Content-Type: application/json" -d '{"name":"Lovely Cheese Pizza", "description":"This is a lovely cheese pizza recipe", "num_of_servings":3, "cook_time":60, "directions":"This is how you make it"}'
    
  2. 你应该会看到以下响应。请仔细检查:

    HTTP/1.0 200 OK
    Content-Type: application/json
    Content-Length: 202
    Server: Werkzeug/0.16.0 Python/3.7.0
    Date: Sun, 03 Nov 2019 03:28:57 GMT
    
    {
        "id": 1,
        "name": "Lovely Cheese Pizza",
        "description": "This is a lovely cheese pizza recipe",
        "num_of_servings": 3,
        "cook_time": 60,
        "directions": "This is how you make it"
    }
    

    一旦使用 HTTP PUT 方法将前面的客户端请求发送到服务器,RecipeResource 中的 put 方法将捕获请求并将 recipe_id 赋值为 1。应用程序将寻找 id = 1 的菜谱并使用客户端请求中的详细信息更新其详情。前面的响应显示,ID 为 1 的菜谱已被修改。

我们刚刚测试了另一个重要功能。你做得很好。让我们继续前进!

练习 17:通过特定 ID 获取特定菜谱

到目前为止,我们已经测试了获取所有菜谱。但在现实生活中,用户可能只想获取他们想看的菜谱。他们可以通过使用菜谱 ID 来做到这一点。这个练习将向你展示如何获取具有特定 ID 的特定菜谱。让我们开始吧:

  1. 使用 httpie 发送以下客户端请求:

    http GET localhost:5000/recipes/1
    

    或者,使用以下 curl 命令,它做的是同样的事情:

    curl -i -X GET localhost:5000/recipes/1
    You should see the following response. Please examine it carefully:
    HTTP/1.0 200 OK
    Content-Type: application/json
    Content-Length: 202
    Server: Werkzeug/0.16.0 Python/3.7.0
    Date: Sun, 03 Nov 2019 03:29:59 GMT
    
    {
        "id": 1,
        "name": "Lovely Cheese Pizza",
        "description": "This is a lovely cheese pizza recipe",
        "num_of_servings": 3,
        "cook_time": 60,
        "directions": "This is how you make it"
    }
    

    一旦使用 HTTP GET 方法将前面的客户端请求发送到服务器,RecipeResource 中的 get 方法将捕获请求并将 recipe_id 赋值为 1。它将从应用程序内存中的 recipe_list 获取所有已发布的菜谱,HTTP 状态为 HTTP 200

我们刚刚测试了我们的 Smilecook 应用程序,并确认它可以给我们返回我们想要的菜谱。

活动 3:使用 Postman 测试 API

在前面的练习中,我们添加了相当多的功能。现在,在我们继续开发其他功能之前,我们需要确保它们能正常工作。在这个活动中,我们不会使用 httpie/curl,而是将使用 Postman 来测试我们的 API。请按照以下高级步骤进行:

  1. 使用 Postman 创建第一个菜谱。

  2. 使用 Postman 创建第二个菜谱。

  3. 使用 Postman 获取所有菜谱。

  4. 使用 Postman 将菜谱设置为已发布。

  5. 再次使用 Postman 获取所有菜谱。

  6. 使用 Postman 修改菜谱。

  7. 使用 Postman 获取特定菜谱。

    注意

    本活动的解决方案可以在第 293 页找到。

活动 4:实现删除菜谱功能

在这个活动中,你将自己在 Smilecook 应用程序中实现删除菜谱功能。通过向 RecipeResource 添加删除功能,类似于我们在前面的练习中所做的。然后,我们将遵循标准的软件开发生命周期流程,使用 Postman 来测试我们的实现。按照以下步骤完成此活动:

  1. 将删除功能添加到 RecipeResource

  2. 启动 Flask 服务器进行测试。

  3. 使用 Postman 创建第一个菜谱。

  4. 使用 Postman 删除菜谱。

    注意

    该活动的解决方案可以在第 299 页找到。

摘要

在本章中,我们使用 Flask-RESTful 包构建了 RESTful API。通过这样做,您已经看到了执行此类任务是多么简单和容易。我们以结构化的方式构建项目,这使得我们能够在后续章节中轻松扩展项目。在本章中,我们创建了模型和资源文件夹;本书后面我们将开发更多模型和资源。到目前为止,我们的美食食谱分享平台 Smilecook 能够执行 CRUD 操作,以及设置食谱的发布状态。我们还测试了应用程序,以确保其正常运行。最后,您开始意识到 Postman 的强大功能,它极大地自动化了整个测试过程。在下一章中,我们将学习如何进行数据验证。

第三章:3. 使用 SQLAlchemy 操作数据库

学习目标

在本章结束时,你将能够:

  • 使用 pgAdmin 工具管理数据库

  • 使用 SQLAlchemy 操作数据库

  • 使用 Flask-Migrate 创建数据库表

  • 将数据持久化到数据库中

  • 对机密密码数据进行哈希处理

本章涵盖了使用 SQLAlchemy 访问数据库,包括构建模型、加密密码、确保每个电子邮件地址唯一,然后将食谱数据保存到数据库中。

简介

在上一章中,我们只是在应用程序内存中存储我们的数据。虽然这样做编码起来很容易,但一旦服务器重启,数据就会消失。这显然不是理想的,因为我们期望数据在服务器重启或应用程序迁移等情况下仍然持久化。因此,在本章中,我们将讨论在数据库中持久化数据。我们首先将在本地机器上安装 Postgres 数据库。然后,我们将使用 pgAdmin 创建数据库,并使用 SQLAlchemy 的 ORM对象关系映射)包与之交互。ORM 允许我们通过对象而不是 SQL 查询与数据库交互。之后,我们将定义用户和食谱模型,将它们链接起来,并使用 Flask-Migrate 在数据库中创建相应的表。一旦这部分完成,我们将通过练习来理解 SQLAlchemy 在 Python 控制台中的使用。最后,我们将添加用户资源,以便可以通过 API 创建新用户。

数据库

你可能之前听说过数据库这个词。它基本上是一个数据存储系统。但为什么我们需要一个系统来存储数据?为什么我们不能只是将所有内容存储在文本文件中,然后保存在文件夹系统中?显然,数据库的功能不仅仅是存储数据。它对数据进行分类和组织,并帮助以更少的冗余存储数据。它还使数据更容易维护,使其更安全、更一致。数据库通常由一个数据库管理系统DBMS)管理

数据库管理系统

DBMS 是一个操作和管理数据库的应用程序。它促进了用户与数据库之间的通信。用户可以使用此应用程序创建、使用和维护数据库。

DBMS 对于数据安全和完整性至关重要。流行的数据库软件和 DBMS 包括 PostgreSQL、MySQL、Microsoft SQL Server、MariaDB 和 Oracle 数据库。大多数 DBMS 使用结构化查询语言SQL)来插入和提取数据。

在这本书中,我们将使用 PostgreSQL 作为我们的后端数据库系统。我们还将使用 pgAdmin,这是一个用于管理 PostgreSQL 的工具。PostgreSQL 是一个拥有 15 年历史的强大、开源的对象关系型数据库管理系统。由于其稳定性和数据完整性,它得到了广泛的认可。

SQL

SQL 是一种专门为管理和操作数据而发明的语言。它可以进一步分为以下类型:

  • SELECT column1, column2 FROM table WHERE conditions,它可以查询表并提取满足一定条件的数据(column1, column2)。

  • INSERTUPDATEDELETE

  • 数据控制语言DCL)用于控制数据访问。

虽然我们在这里介绍了许多不同的语言,但好事是我们不需要学习所有这些。事实上,我们不会使用 SQL 查询我们的数据库。我们只需要用 Python 编码,ORM 包将在幕后将我们的 Python 代码转换为 SQL。现在与数据库一起工作要容易得多。

ORM

对象关系映射ORM)是一种编程技术,允许开发者在编程语言中将对象映射到数据库中的数据模型。不再需要使用 SQL 与数据库交互。这种技术的优点是,开发者可以用自己的编程语言进行编码,并且它可以在不同类型的数据库上工作。

映射工作如下:

  • Python 中的类 = 数据库中的表架构

  • 类中的属性 = 表架构中的字段

  • 对象 = 表中的数据行

SQLAlchemy 是 Python 社区中最受欢迎的 ORM。接下来,让我们进一步深入,尝试创建一个数据库。

练习 18:设置 Smilecook 数据库

应用程序现在大多需要数据库来存储和管理数据。我们的应用程序 Smilecook 也不例外。它是一个食谱分享平台,对公众开放。显然,它将需要存储用户数据和食谱数据。在这个练习中,我们将创建数据库管理员并为我们的 Smilecook 应用程序设置数据库:

  1. 首先,我们将创建一个角色。角色是 PostgreSQL 用于管理访问的一个简单概念。在这里,我们可以将其视为用户。在 Servers 下的 PostgreSQL 11右键单击,选择 Create,然后选择 Login/Group Role…:![图 3.1:选择登录/组角色… 图 3.4:使用创建的账户登录数据库

    图 3.1:选择登录/组角色…
  2. 填写登录名,稍后将用于连接到数据库:![图 3.2:填写登录名 图 3.4:使用创建的账户登录数据库

    图 3.2:填写登录名
  3. 然后,选择 Definition 并设置密码。点击 Save:![图 3.3:设置密码 图 3.4:使用创建的账户登录数据库

    图 3.3:设置密码
  4. 现在,转到 Privileges,并选择 Yes 以允许 Can login?这将允许我们使用此账户登录到数据库:图 3.4:使用创建的账户登录数据库

    图 3.4:使用创建的账户登录数据库

    图 3.4:使用创建的账户登录数据库
  5. 右键单击 Databases,并从那里创建数据库:![图 3.5:创建数据库 图 3.4:使用创建的账户登录数据库

    图 3.5:创建数据库
  6. 将数据库命名为smilecook,并将我们刚刚创建的角色设置为所有者。点击保存图 3.6:命名数据库和设置角色

图 3.6:命名数据库和设置角色

现在我们已经创建了 Smilecook 数据库,但目前它是空的。在下一个练习中,我们将使用 Flask-SQLAlchemy 和 Flask-Migrate 来创建我们的数据库表。你会注意到其中没有涉及 SQL 查询。

定义我们的模型

在我们进入实现之前,我们需要首先定义和了解我们将要工作的字段。我们将涵盖两个基本模型:UserRecipe。模型在数据库中类似于模式。一个模型是一个类,可以被实例化。它包含与数据库模式中的字段相对应的属性。

用户模型

用户模型将被映射到数据库中的用户表。我们为我们的用户模型定义的字段和方法如下:

  • id:用户的标识。

  • username:用户的用户名。允许的最大长度是 80 个字符。它不能为空,并且是一个唯一字段。

  • email:用户的电子邮件。允许的最大长度是 200。它不能为空,并且是一个唯一字段。

  • password:用户的密码。允许的最大长度是 200。

  • is_active:这是用来表示账户是否通过电子邮件激活。它是一个布尔字段,默认值为False

  • recipes:这不会在数据库表中创建一个字段。这只是为了定义与菜谱模型的关系。因此,我们可以使用user.recipes获取所有菜谱。

  • created_at:用户的创建时间。

  • updated_at:用户的最后更新时间。

我们还将在用户模型中定义三个方法:

  • get_by_username:这个方法用于通过用户名搜索用户。

  • get_by_email:这个方法用于通过电子邮件搜索用户。

  • save:这是为了将数据持久化到数据库中。

菜谱模型

菜谱模型将被映射到数据库中的用户表。我们为我们的菜谱模型定义的字段如下:

  • id:菜谱的标识。

  • name:菜谱的名称。允许的最大长度是 100 个字符。它不能为空。

  • description:菜谱的描述。允许的最大长度是 200。

  • num_of_servings:份量数量。这需要是一个整数。

  • cook_time:烹饪时间(分钟)。此字段只接受整数。

  • directions:菜谱的说明。这个字段的最大长度为 1,000。

  • is_publish:这是用来表示菜谱是否已发布。默认设置为False

  • created_at:菜谱的创建时间。

  • updated_at:菜谱的最后更新时间。

在我们心中有了模型设计之后,我们现在准备在下一个练习中使用这些模型。在此之前,让我们也简要了解一下我们将要使用的一些关键包。这些包如下:

  • Flask-SQLAlchemy:这是一个非常流行的 ORM 软件包,它允许我们访问对象而不是数据库表来处理数据。使用 ORM,我们不再需要依赖 SQL。

  • Flask-Migrate:这是一个数据库迁移软件包;它基于 Alembic。

  • Psycopg2-binary:这是 Postgres 数据库的适配器。

  • Passlib:这是一个 Python 密码散列库。

练习 19:安装软件包和定义模型

本练习旨在安装必要的软件包并定义用户和食谱模型。用户和食谱模型将是 Python 类;本练习中不会涉及任何 SQL 编码。我们想通过简单的 Python 编码向您展示如何与数据库交互:

  1. 我们将在requirements.txt文件中添加所需的软件包。如果您还记得,通过在requirements.txt中放置软件包名称和版本,我们可以通过单个pip命令在 Python 虚拟环境中安装它们:

    Flask-SQLAlchemy==2.4.0
    Flask-Migrate==2.5.2
    psycopg2-binary==2.8.3
    passlib==1.7.1
    
  2. 我们可以通过以下pip install命令安装必要的软件包:

    pip install -r requirements.txt
    

    安装结果将在屏幕上显示:

    Installing collected packages: SQLAlchemy, Flask-SQLAlchemy, alembic, Flask-Migrate, psycopg2-binary, passlib
      Running setup.py install for SQLAlchemy ... done
      Running setup.py install for alembic ... done
    Successfully installed Flask-Migrate-2.5.2 Flask-SQLAlchemy-2.4.0 SQLAlchemy-1.3.6 alembic-1.0.11 passlib-1.7.1 psycopg2-binary-2.8.3
    
  3. 创建一个Config.py文件并输入以下代码:

    class Config:
        DEBUG = True
        SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://{your_name}:{your_password}@localhost/{db_name}
        SQLALCHEMY_TRACK_MODIFICATIONS = False
    

    我们可以在这里设置DEBUG = True以进行调试。至于SQLALCHEMY_DATABASE_URI,这是数据库的路径。请将用户名和密码替换为我们为pgAdmin创建的。同时,也请替换数据库名称。

  4. 现在,在 Smilecook 项目下创建extensions.py并输入以下代码:

    from flask_sqlalchemy import SQLAlchemy
    db = SQLAlchemy()
    
  5. models文件夹下创建user.py并输入以下代码:

    from extensions import db
    class User(db.Model):
        __tablename__ = 'user'
        id = db.Column(db.Integer, primary_key=True)
        username = db.Column(db.String(80), nullable=False, unique=True)
        email = db.Column(db.String(200), nullable=False, unique=True)
        password = db.Column(db.String(200))
        is_active = db.Column(db.Boolean(), default=False)
        created_at = db.Column(db.DateTime(), nullable=False, server_default=db.func.now())
        updated_at = db.Column(db.DateTime(), nullable=False, server_default=db.func.now(), onupdate=db.func.now())
        recipes = db.relationship('Recipe', backref='user')
        @classmethod
        def get_by_username(cls, username):
            return cls.query.filter_by(username=username).first()
        @classmethod
        def get_by_email(cls, email):
            return cls.query.filter_by(email=email).first()
    
        def save(self):
            db.session.add(self)
            db.session.commit()
    
  6. recipe.py替换为以下代码。我们在这里添加了import db语句,并且也修改了Recipe类。与recipe_list相关的代码仍然有效,因此我们保留了这部分代码:

    from extensions import db
    recipe_list = []
    def get_last_id():
        if recipe_list:
            last_recipe = recipe_list[-1]
        else:
            return 1
        return last_recipe.id + 1
    class Recipe(db.Model):
        __tablename__ = 'recipe'
        id = db.Column(db.Integer, primary_key=True)
        name = db.Column(db.String(100), nullable=False)
        description = db.Column(db.String(200))
        num_of_servings = db.Column(db.Integer)
        cook_time = db.Column(db.Integer)
        directions = db.Column(db.String(1000))
        is_publish = db.Column(db.Boolean(), default=False)
        created_at = db.Column(db.DateTime(), nullable=False, server_default=db.func.now())
        updated_at = db.Column(db.DateTime(), nullable=False, server_default=db.func.now(), onupdate=db.func.now())
        user_id = db.Column(db.Integer(), db.ForeignKey("user.id"))
    
  7. 现在,用以下代码重写app.py。我们以更合适的方式组织代码,使其更易于阅读和维护。首先,在代码文件的开头导入所需的软件包。

    注意

    from flask import Flask
    from flask_migrate import Migrate
    from flask_restful import Api
    from config import Config
    from extensions import db
    from models.user import User
    from resources.recipe import RecipeListResource, RecipeResource, RecipePublishResource
    
  8. 使用create_app()函数创建 Flask 应用程序。这将调用register_extensions(app)以初始化 SQLAlchemy 并设置 Flask-Migrate。然后它将调用register_resources(app)来设置资源路由:

    def create_app():
        app = Flask(__name__)
        app.config.from_object(Config)
        register_extensions(app)
        register_resources(app)
        return app
    def register_extensions(app):
        db.init_app(app)
        migrate = Migrate(app, db)
    def register_resources(app):
        api = Api(app)
        api.add_resource(RecipeListResource, '/recipes')
        api.add_resource(RecipeResource, '/recipes/<int:recipe_id>')
        api.add_resource(RecipePublishResource, '/recipes/<int:recipe_id>/publish')
    
  9. 最后,使用app = create_app()创建 Flask 应用程序,并使用app.run()启动应用程序:

    if __name__ == '__main__':
        app = create_app()
        app.run()
    
  10. 保存app.py并在其上右键单击以运行应用程序。然后 Flask 将在本地主机(127.0.0.1)的 5000 端口启动并运行:

![图 3.7:在本地主机上启动 Flask]

图片 C15309_03_07.jpg

图 3.7:在本地主机上启动 Flask

我们已成功安装必要的 ORM 相关软件包,并定义了用户和食谱模型。首先安装软件包后,我们在虚拟环境中运行了安装。我们创建了config.pyextensions.pyuser.py文件,并替换了app.py。最后,我们对 Flask 应用程序进行了重构,并看到了它运行得有多好。

练习 20:使用 Flask-Migrate 构建数据库升级脚本

成功理解如何使用我们的两个主要模型,用户和食谱后,我们现在已经建立了完美的基础。下一步是执行。我们将使用 Flask-Migrate 来构建一个脚本来创建用户和食谱表:

  1. 在终端中使用以下命令初始化我们的数据库。这将创建一个迁移存储库:

    flask db init
    

    您应该在屏幕上看到以下内容:

    Creating directory /Python-API-Development-Fundamentals/smilecook/migrations ... done
    Creating directory /Python-API-Development-Fundamentals/smilecook/migrations/versions ... done
    Generating /Python-API-Development-Fundamentals/smilecook/migrations/script.py.mako ... done
    Generating /Python-API-Development-Fundamentals/smilecook/migrations/env.py ... done
    Generating /Python-API-Development-Fundamentals/smilecook/migrations/README ... done
    Generating /Python-API-Development-Fundamentals/smilecook/migrations/alembic.ini ... done
    Please edit configuration/connection/logging settings in '/Python-API-Development-
    Fundamentals/smilecook/migrations/alembic.ini' before proceeding.
    

    您现在应该在 PyCharm 中看到以下新文件:

    图 3.8:PyCharm 中的新文件夹

    图 3.8:PyCharm 中的新文件夹
  2. 现在,运行 flask db migrate 命令来创建数据库和表。我们在这里不需要使用 SQL:

    flask db migrate
    

    Flask-Migrate 检测到两个对象(userrecipe)并为它们创建了相应的两个表:

    INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
    INFO  [alembic.runtime.migration] Will assume transactional DDL.
    INFO  [alembic.autogenerate.compare] Detected added table 'user'
    INFO  [alembic.autogenerate.compare] Detected added table 'recipe'
      Generating /Python-API-Development-Fundamentals/smilecook/migrations/versions/a6d248ab7b23_.py ... done
    
  3. 现在,请检查 versions 文件夹下的 /migrations/versions/a6d248ab7b23_.py。此文件由 Flask-Migrate 创建。请注意,您在这里可能得到不同的修订 ID。请在运行 flask db upgrade 命令之前查看该文件。因为有时它可能无法检测到您对模型所做的每个更改:

    """empty message
    Revision ID: a6d248ab7b23
    Revises: 
    Create Date: 2019-07-22 16:10:41.644737
    """
    from alembic import op
    import sqlalchemy as sa
    # revision identifiers, used by Alembic.
    revision = 'a6d248ab7b23'
    down_revision = None
    branch_labels = None
    depends_on = None
    def upgrade():
        # ### commands auto generated by Alembic - please adjust! ###
        op.create_table('user',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('username', sa.String(length=80), nullable=False),
        sa.Column('email', sa.String(length=200), nullable=False),
        sa.Column('password', sa.String(), nullable=True),
        sa.Column('is_active', sa.Boolean(), nullable=True),
        sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
        sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
        sa.PrimaryKeyConstraint('id'),
        sa.UniqueConstraint('email')
        )
        op.create_table('recipe',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('name', sa.String(length=100), nullable=False),
        sa.Column('description', sa.String(length=500), nullable=True),
        sa.Column('num_of_servings', sa.Integer(), nullable=True),
        sa.Column('cook_time', sa.Integer(), nullable=True),
        sa.Column('directions', sa.String(), nullable=True),
        sa.Column('is_publish', sa.Boolean(), nullable=True),
        sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
        sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
        sa.Column('user_id', sa.Integer(), nullable=True),
        sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
        sa.PrimaryKeyConstraint('id')
        )
        # ### end Alembic commands ###
    def downgrade():
        # ### commands auto generated by Alembic - please adjust! ###
        op.drop_table('recipe')
        op.drop_table('user')
        # ### end Alembic commands ###
    

    在此自动生成的文件中有两个函数;一个是升级的,用于将新的食谱和用户添加到表中,另一个是降级的,用于回到上一个版本。

  4. 然后,我们将执行 flask db upgrade 命令,这将使我们的数据库符合模型中最新规范:

    flask db upgrade
    

    此命令将调用 upgrade() 来升级数据库:

    INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
    INFO  [alembic.runtime.migration] Will assume transactional DDL.
    INFO  [alembic.runtime.migration] Running upgrade  -> a6d248ab7b23, empty message
    

    注意

    在未来,无论何时我们需要升级数据库,我们只需调用 flask db migrateflask db upgrade

  5. pgAdmin 中检查数据库表。现在,我们可以查看表是否已在数据库中创建。转到 smilecook >> Schemas >> Tables to verify图 3.9:检查数据库表

图 3.9:检查数据库表

如果您在我们的 Smilecook 数据库中看到食谱和用户表,这意味着您已成功在 Python 中创建它们,而没有使用任何 SQL。这不是很酷吗?!

接下来,我们将尝试数据库插入。让我们看看以下练习。

练习 21:应用数据库插入

这个练习是为了让我们测试数据库插入。我们首先创建一个用户,然后在该用户下创建两个食谱:

  1. 在 Python 控制台中导入模块。在 PyCharm 底部打开 Python 控制台,并输入以下代码以导入必要的类:

    from app import *
    from models.user import User
    from models.recipe import Recipe
    app = create_app()
    
  2. 创建我们的第一个 user 对象,并将其保存到数据库中,请在 Python 控制台中输入以下代码:

    user = User(username='jack', email='jack@gmail.com', password='WkQa')
    db.session.add(user)
    db.session.commit()
    
  3. 现在,检查 user 的详细信息。请注意,用户的 ID 已经被分配为 1

    >>>user.username
    'jack'
    >>>user.id
    1
    >>>user.email
    'jack@gmail.com'
    
  4. 由于用户已保存在数据库中,我们将验证那里:图 3.10:在数据库中验证用户

    图 3.10:在数据库中验证用户
  5. 我们可以在 user 表中看到一条记录:

  6. 图 3.11:用户表中的记录

    图 3.11:用户表中的记录
  7. 接下来,我们将使用以下代码创建两个菜谱。需要注意的是,菜谱的 user_id 属性被设置为 user.id。这是为了表明菜谱是由用户 Jack 创建的:

    pizza = Recipe(name='Cheese Pizza', description='This is a lovely cheese pizza recipe', num_of_servings=2, cook_time=30, directions='This is how you make it', user_id=user.id)
    db.session.add(pizza)
    db.session.commit()
    pasta = Recipe(name='Tomato Pasta', description='This is a lovely tomato pasta recipe', num_of_servings=3, cook_time=20, directions='This is how you make it', user_id=user.id)
    db.session.add(pasta)
    db.session.commit()
    
  8. 我们将检查是否在数据库中创建了两个菜谱:![图 3.12:检查两个菜谱是否已创建 img/C15309_03_12.jpg

    图 3.12:检查两个菜谱是否已创建
  9. 我们将在数据库中搜索用户名为 jack 的用户,并获取该用户创建的所有菜谱,这些菜谱存储在他们的对象属性 recipes 中:

    >>> user = User.query.filter_by(username='jack').first()
    >>> user.recipes
    

    我们将获取两个菜谱的列表:

    [<Recipe 1>, <Recipe 2>]
    
  10. 我们可以使用 for 循环来显示菜谱的详细信息。我们通过 recipe.name 获取菜谱名称,而通过 recipe.user.username 获取用户的名称:

    >>> for recipe in user.recipes:
        print('{} recipe made by {} can serve {} people.'.format(recipe.name, recipe.user.username, recipe.num_of_servings))
    

    你应该在屏幕上看到以下结果:

    Cheese Pizza recipe made by jack can serve 2 people.
    Tomato Pasta recipe made by jack can serve 3 people.
    

你刚刚学会了如何使用 Python 控制台来命令你的应用程序。你已经创建了用户和菜谱模型,并将它们保存在数据库中。整个过程是无 SQL 的,正如你所看到的。让我们通过一个活动来巩固你的知识。

活动 5:创建用户和菜谱

在这个活动中,我们将通过运行一些额外的测试用例来测试我们的 API。我们想要创建一个新用户 Peter,并在数据库中为他创建两个菜谱。让我们看看你是否知道如何在 Python 交互控制台中编写这段代码:

  1. 导入 UserRecipe 类,并使用 Python 控制台创建 Flask 应用程序。

  2. 创建一个新用户,Peter

  3. 创建两个菜谱并将 Peter 设为作者。

    注意

    本活动的解决方案可以在第 302 页找到。

如果你看到数据已成功创建在数据库中,恭喜你——你已经知道如何使用 Python 控制台与数据库进行交互!接下来,我们将实现用户注册功能。

密码哈希

哈希是一种单向数学函数。将明文字符串转换为哈希值(哈希)需要很少的计算能力。然而,要从哈希值中检索原始字符串则需要巨大的计算能力(几乎是不可能的)。因此,我们称它为单向函数:

![图 3.13:哈希函数的工作原理img/C15309_03_13.jpg

图 3.13:哈希函数的工作原理

利用这一特性,哈希函数非常适合用于密码哈希。在我们将其保存到数据库之前,我们将用户的密码哈希化,使其不可识别且不可逆。下次用户登录时,平台所做的是将输入的密码转换为它的哈希值,然后将其与数据库中存储的哈希值进行比较。这样,我们就可以在不向他人泄露敏感密码信息的情况下执行密码比较。

练习 22:实现用户注册功能并哈希用户密码

在这个练习中,我们将处理用户注册功能。我们还将实现两个用于哈希用户密码的函数:

  1. 在应用程序项目文件夹下创建utils.py,并输入以下代码。这段代码用于哈希密码。由于安全考虑,我们不希望在数据库中存储明文密码。因此,我们将使用passlib模块进行哈希。我们在这里定义了两种方法:

    from passlib.hash import pbkdf2_sha256
    def hash_password(password):
        return pbkdf2_sha256.hash(password)
    def check_password(password, hashed):
        return pbkdf2_sha256.verify(password, hashed)
    

    hash_password(password)函数用于密码哈希,check_password(password, hashed)用于用户认证。它将用户输入的密码进行哈希处理,并将其与我们保存在数据库中的密码进行比较。

  2. resources文件夹中创建user.py,然后输入以下代码。我们首先导入必要的模块并在UserListResource中实现Post方法:

    from flask import request
    from flask_restful import Resource
    from http import HTTPStatus
    from utils import hash_password
    from models.user import User
    class UserListResource(Resource):
        def post(self):
            json_data = request.get_json()
            username = json_data.get('username')
            email = json_data.get('email')
            non_hash_password = json_data.get('password')
    

    当客户端请求以 HTTP POST方法击中http://localhost/users时,应用程序将获取请求中的 JSON 格式数据。应该有一个用户名、电子邮件和密码。

  3. 通过User.get_by_user(username)检查用户是否已存在于数据库中。如果找到这样的条目,这意味着用户已经注册,我们将简单地返回一个错误消息。我们也会对email进行相同的检查:

            if User.get_by_username(username):
                return {'message': 'username already used'}, HTTPStatus.BAD_REQUEST
            if User.get_by_email(email):
                return {'message': 'email already used'}, HTTPStatus.BAD_REQUEST
    
  4. 一旦所有验证都通过,就可以在数据库中创建用户。密码将被哈希,然后创建用户对象。然后使用user.save()将用户对象保存到数据库中。最后,以 JSON 格式返回用户详细信息,并带有HTTP状态码201

        password = hash_password(non_hash_password)
            user = User(
                username=username,
                email=email,
                password=password
            )
            user.save()
            data = {
                'id': user.id,
                'username': user.username,
                'email': user.email
            }
            return data, HTTPStatus.CREATED
    
  5. 将用户资源路由添加到app.py

    from extensions import db
    from resources.user import UserListResource
    from resources.recipe import RecipeListResource, RecipeResource, RecipePublishResource
    def register_resources(app):
        api = Api(app)
        api.add_resource(UserListResource, '/users')
        api.add_resource(RecipeListResource, '/recipes')
    

    app.py中的from models.user import User替换为from resources.user import UserListResource。用户模型已经在resources.user中导入,因此不需要再次导入。请将api.add_resource(UserListResource, '/users')也添加到代码中。

    运行应用程序。Flask 将在本地主机(127.0.0.1)的5000端口上启动并运行:

    图 3.14:Flask 在本地主机上启动

图 3.14:Flask 在本地主机上启动

因此,我们刚刚完成了密码哈希练习。从现在起,每当有新用户在我们的 Smilecook 应用程序中注册时,他们的密码将被哈希并安全地存储在数据库中。让我们在下一个练习中测试一下这是否如此。

注意

我们在这里不讨论食谱资源的原因是,食谱中会有一个作者 ID。作者 ID 将是一个外键,它将链接到用户模型。我们将在下一章中讨论用户登录功能。只有在那之后,我们才能获取用户 ID 并完成食谱资源。

练习 23:在 Postman 中测试应用程序

在这个练习中,我们将测试 Postman 中的应用程序。我们首先注册一个用户账户,并确保用户数据已存储在数据库中。我们还需要验证密码是否已哈希。创建用户后,现在让我们测试我们的 API 端点:

  1. 在 Postman 中点击集合选项卡。

  2. 创建一个新的集合,并将其命名为User

  3. 在该集合下创建一个新的请求,UserList。你可以通过点击User集合来完成此操作。

  4. 编辑POST

  5. 在 URL 字段中输入http://localhost:5000/users

  6. 转到主体选项卡,将数据类型选择为raw,然后选择数据格式为JSON (application/json)

  7. 插入以下用户详细信息并保存:

    {
        "username": "jack",
        "email": "jack@gmail.com",
        "password": "WkQa"
    }
    
  8. 点击发送。结果如下所示截图:![图 3.15:使用现有用户名创建用户 图片

    图 3.15:使用现有用户名创建用户

    你将看到以下返回的数据;HTTP 状态为400 BAD REQUEST。我们还可以在主体字段中看到错误消息,显示用户名已被注册。

  9. 使用以下代码创建另一个具有以下详细信息的用户:

    {
        "username": "ray",
        "email": "ray@gmail.com",
        "password": "WkQa"
    }
    

    结果如下所示截图:

    ![图 3.16:创建另一个用户 图片

    图 3.16:创建另一个用户

    现在,第二个账户已成功创建。

  10. 按如下方式检查数据库中的数据:![图 3.17:检查数据库中的数据 图片

图 3.17:检查数据库中的数据

现在,我们可以看到数据库表中创建了一个新的用户记录。并且你可以看到密码已被哈希。

通过进行此测试练习,我们可以确保我们的用户注册工作流程运行良好。最重要的是,用户密码以哈希值的形式保存在数据库中。这是一种更安全的存储密码的方式,即使数据库管理员也无法看到它。

活动 6:升级和降级数据库

  1. 在此活动中,我们将升级和降级数据库以模拟我们需要在user类下添加属性的场景,但后来我们改变了主意,需要删除它。以下是我们完成此活动所需执行的高级步骤:

  2. user类添加一个新属性。此属性应命名为bio,它将是一个表示用户信息的字符串。

  3. 运行flask db migrate命令以创建数据库和表。

  4. 现在,检查versions文件夹下的/migrations/versions/6971bd62ec60_.py。此文件由 Flask-Migrate 创建。

  5. 执行flask db upgrade命令以将我们的数据库升级到符合我们模型中最新的规范。

  6. 检查新字段是否已创建在数据库中。

  7. 运行downgrade命令以删除新字段。

  8. 检查字段是否已被删除。

    注意

    此活动的解决方案可在第 303 页找到。

如果你看到新字段已被删除,这意味着你已经在 Python 中成功降级了数据库,而没有编写任何 SQL。别忘了删除 models/user.py 中用户模型的 bio 属性,也删除我们在 migrations/versions 文件夹中创建的脚本a6d248ab7b23.py。你刚刚学会了一个非常有用的技能,你将来可能会经常用到。给你一个提示,你应该在数据库模式更新之前备份你的数据库。这是为了确保数据不会丢失。

摘要

在本章中,我们在本地构建了 Postgres 数据库,并学习了如何使用 pgAdmin 工具来管理它。然后,通过 SQLAlchemy 模块,我们开发了一个对象库来操作数据库。这比直接使用 SQL 语法要容易得多。而且,只要我们定义了模型之间的关系,我们就可以轻松地获取我们想要的信息。这导致代码可读性更高,代码行数更少,并消除了重复的 SQL。然后,我们使用 Flask-Migrate 构建所有数据表。然后,当我们将来迁移数据库时,我们只需要两个命令——flask db migrateflask db upgrade;这很简单,也很容易。尽管 Flask-Migrate 可以帮助我们更轻松地设置和迁移数据库,但在生产环境中,执行此类迁移仍然需要额外的谨慎。我们应该始终备份数据库以保护我们宝贵的数据。

在开发过程中,我们应该经常测试我们的代码以确保其按预期行为。我们不应该等到最后才进行大范围测试。一旦完成,我们可以对函数和 API 端点进行单元测试。使用 Python 控制台进行此类简单测试是推荐的。迭代测试我们的应用程序也可以培养最佳编程实践。这迫使我们思考如何以优雅的方式组织代码并避免技术债务的积累。

最后,我们为用户注册创建了一个 API。在下一章中,我们将致力于为认证用户开发用户登录和食谱创建功能。

第四章:4. 使用 JWT 进行认证服务和安全

学习目标

到本章结束时,你将能够:

  • 应用 JWT 知识

  • 使用 Flask-JWT-Extended 创建访问令牌

  • 开发会员登录系统

  • 实现访问控制系统(认证和权限)

  • 使用刷新令牌进行操作

  • 使用黑名单限制访问

本章介绍了如何使用 JWT 开发用户登录/注销功能。

简介

在上一章中,我们完成了数据库的设置和配置,并使用 ORM 将数据库链接到代码中。然后我们在其基础上实现了用户注册 API。本章分为四个部分。第一部分是关于用户认证并允许他们登录到自己的私有个人资料页面。第二部分完成了食谱分享系统,允许用户发布或取消发布他们的食谱。第三部分展示了如何刷新安全令牌并实现注销功能。最后,我们将讨论如何使用blacklist函数强制用户注销。

用户认证在现代系统中非常重要,尤其是如果它们部署在互联网上。数千名用户访问同一个网站,使用相同的 Web 应用程序。如果没有用户认证和访问控制,所有内容都会共享。看看你的 Facebook/Instagram 账户——系统中也实现了用户认证和访问控制。只有你才能登录到你的账户并管理你的帖子和个人照片。对于我们的 Smilecook 应用程序,我们同样需要这样的功能。

我们将首先讨论 JWT。

JWT

JWT用于用户认证,并在用户和服务器之间传递。该缩写的全称是JSON Web Token。它们的工作方式是编码用户身份并对其进行数字签名,使其成为一个不可伪造的令牌,用于识别用户,并且应用程序可以根据用户的身份控制对用户的访问。

JWT 是一个由头部、载荷和签名组成的字符串。这三部分由.分隔。以下是一个示例:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1NjQ5ODI5OTcs Im5iZiI6MTU2NDk4Mjk5NywianRpIjoiMGIzOTVlODQtNjFjMy00NjM3LTkwMzYtZjgyZDgy YTllNzc5IiwiZXhwIjoxNTY0OTgzODk3LCJpZGVudGl0eSI6MywiZnJlc2giOmZhbHNlLCJ 0eXBlIjoiYWNjZXNzIn0.t6F3cnAmbUXY_PwLnnBkKD3Z6aJNvIDQ6khMJWj9xZM

"alg": "HS256"的头部,表示加密算法,"typ": "JWT"。如果我们对头部字符串进行base64解码,可以清楚地看到这一点:

>>> import base64
>>> header = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9'
>>> base64.b64decode(header)
b'{"typ":"JWT","alg":"HS256"}'

解码base64内容并获取其中的信息。需要注意的是,这些信息未加密,因此不建议在此处存储信用卡详情或密码:

>>> import base64
>>> payload = 'eyJpYXQiOjE1NjQ5ODI5OTcsIm5iZiI6MTU2NDk4Mjk5NywianRpI joiMGIzOTVlODQtNjFjMy00NjM3LTkwMzYtZjgyZDgyYTllNzc5IiwiZXhwIjoxNTY0 OTgzODk3LCJpZGVudGl0eSI6MywiZnJlc2giOmZhbHNlLCJ0eXBlIjoiYWNjZXNzIn0'
>>> base64.b64decode(payload + '==')
b'{"iat":1564982997,"nbf":1564982997,"jti":"0b395e84-61c3-4637-9036-f82d82a9e779","exp":1564983897,"identity":3,"fresh":false,"type":"access"}'

HS256算法。该算法使用只有应用程序服务器才知道的密钥加密编码的头部和载荷数据。尽管任何人都可以修改 JWT 内容,但这会导致不同的签名,从而保护数据完整性。

我们可以利用jwt.io/提供的免费服务,更好地查看 JWT 令牌的结构和内容:

图 4.1:JWT 网站

图 4.1:JWT 网站

使用简单的结构header.payload.secret,我们有一个 JWT,它将在这个项目中用于用户认证。基于用户的身份,我们可以应用访问控制或其他类型的逻辑。

Flask-JWT-Extended

Flask-JWT-Extended 是一个用户认证包,它提供了create_access_token函数来创建新的访问 JWT。它还提供了jwt_required装饰器来保护 API 端点(用于检查用户是否已登录)。此外,还提供了get_jwt_identity()函数来获取受保护端点中 JWT 的身份。这使得我们可以知道谁是认证用户。这是一个非常实用的用户认证包。

在我们深入到即将到来的练习之前,让我们首先讨论两个我们将要使用的重要关键配置。它们如下:

  • SECRET_KEY:这是加密消息和生成签名的密钥。我们建议您使用一个复杂的字符串。

  • msg,但我们在这里将其设置为message

我们将在下一个练习中一起工作用户登录功能。您将了解用户登录的工作原理以及我们如何确定认证用户是谁。

注意

关于 Flask-JWT-Extended 的更多信息,您可以参考此链接:flask-jwt-extended.readthedocs.io/en/latest/options.html

练习 24:实现用户登录功能

在这个练习中,我们将构建用户登录功能。我们将使用 Flask-JWT-Extended 包。通过这个练习,您将学习我们如何在 Flask 中生成 JWT。用户将在http://localhost:5000/token中输入他们的凭据,然后他们将获得一个令牌。他们可以使用该令牌来访问http://localhost:5000/users/{username}并检查系统中注册的个人资料。如果没有令牌,他们将只能看到自己的 ID 和用户名。这是我们 Smilecook 应用程序的访问控制功能:

  1. 通过在requirements.txt文件中添加以下行来安装Flask-JWT-Extended包:

    Flask-JWT-Extended==3.20.0
    
  2. 运行以下命令以安装所有必要的包:

    pip install -r requirements.txt
    

    您应该在屏幕上看到以下安装结果:

    Installing collected packages: PyJWT, Flask-JWT-Extended
      Running setup.py install for Flask-JWT-Extended ... done
    Successfully installed Flask-JWT-Extended-3.20.0 PyJWT-1.7.1
    
  3. 通过在config.py文件中的Config类中添加以下设置来配置Flask-JWT-Extended

    SECRET_KEY = 'super-secret-key'
    JWT_ERROR_MESSAGE_KEY = 'message'
    
  4. 将以下代码放入extension.py

    from flask_jwt_extended import JWTManager
    jwt = JWTManager()
    

    在这里,我们试图创建一个Flask-JWT-Extended的实例。我们首先从flask_jwt_extended中导入JWTManager类,然后通过调用JWTManager()创建一个Flask-JWT-Extended实例,并将其分配给jwt变量。

  5. app.py中输入以下代码:

    from extensions import db, jwt
    def register_extensions(app):
        db.init_app(app)
        migrate = Migrate(app, db)
        jwt.init_app(app)
    

    我们首先从extensions中导入jwt,然后在register_extensions(app)中通过jwt.init_app(app)初始化jwt

  6. 现在,我们将创建登录资源。我们首先在 resources 文件夹中创建 token.py 文件,并输入以下代码。我们首先导入所有必要的模块、函数和类:

    from http import HTTPStatus
    from flask import request
    from flask_restful import Resource
    from flask_jwt_extended import create_access_token
    from utils import check_password
    from models.user import User
    
  7. 然后,定义一个名为 TokenResource 的类。这个类继承自 flask_restful.Resource

    class TokenResource(Resource):
    
  8. 在类内部,我们创建了一个 post 方法。当用户登录时,此方法将被调用,并且它将从客户端 JSON 请求中获取 emailpassword。它将使用 get_by_email 方法来验证用户的凭据是否正确:

        def post(self):
            json_data = request.get_json()
            email = json_data.get('email')
            password = json_data.get('password')
            user = User.get_by_email(email=email)
            if not user or not check_password(password, user.password):
                return {'message': 'email or password is incorrect'}, HTTPStatus.UNAUTHORIZED
            access_token = create_access_token(identity=user.id)
            return {'access_token': access_token}, HTTPStatus.OK
    

    如果它们无效,该方法将停止并返回 email 或 password is incorrect。否则,它将创建一个带有用户 ID 作为身份的访问令牌。

    注意

    check_password 函数的工作方式是通过散列客户端传递的密码,并使用 pbkdf2_sha256.verify(password, hashed) 函数将散列值与数据库中存储的散列值进行比较。这里没有明文密码比较。

  9. 然后,我们将创建一个新的资源,用于获取用户详细信息。如果用户未认证,他们只能看到他们的 ID 和用户名。否则,他们还将看到他们的个人电子邮件。我们可以在 resources/user.py 中添加以下代码:

    我们首先导入必要的模块、函数和类:

    from flask_jwt_extended import jwt_optional, get_jwt_identity
    
  10. 然后,我们定义一个继承自 flask_restful.ResourceUserResource 类:

    class UserResource(Resource):
    
  11. 在这个类中,我们定义了一个 get 方法,并用 jwt_optional 装饰器包装它。这意味着端点是无论令牌的处理过程如何都可以访问的:

        @jwt_optional
        def get(self, username):
    
  12. 然后,我们执行与上一步类似的常规操作,检查 username 是否可以在数据库中找到:

    
            user = User.get_by_username(username=username)
    
            if user is None:
                return {'message': 'user not found'}, HTTPStatus.NOT_FOUND
    
  13. 如果在数据库中找到,我们将进一步检查它是否与 JWT 中的用户 ID 身份匹配:

            current_user = get_jwt_identity()
    
  14. 根据上一步的结果,我们应用访问控制并输出不同的信息:

            if current_user == user.id:
                data = {
                    'id': user.id,
                    'username': user.username,
                    'email': user.email,
                }
            else:
                data = {
                    'id': user.id,
                    'username': user.username,
                }
            return data, HTTPStatus.OK
    
  15. 最后,我们将导入之前创建的资源,并将它们添加到 app.py 中的 api

    from resources.user import UserListResource, UserResource
    from resources.token import TokenResource
    def register_resources(app):
        api = Api(app)
      api.add_resource(UserListResource, '/users')
        api.add_resource(UserResource, '/users/<string:username>')
        api.add_resource(TokenResource, '/token')
    
  16. 右键单击它以运行应用程序。Flask 将随后在本地主机(127.0.0.1)的 5000 端口启动并运行:图 4.2:运行应用程序以在本地主机上启动和运行 Flask

图 4.2:运行应用程序以在本地主机上启动和运行 Flask

因此,我们已经完成了用户登录功能。这将使用户在登录后能够访问受控的 API。让我们在我们的下一个练习中测试它!

练习 25:测试用户登录功能

在这个练习中,我们将测试登录功能并验证存储在数据库中的用户信息。我们还将测试从 http://localhost:5000/users/{username} API 获取的用户信息在用户登录前后是否不同:

  1. 首先要做的事情是创建一个用户。点击 Collections 选项卡,选择 POST UserList

  2. 选择 Body 选项卡,选择 raw 单选按钮,并从下拉列表中选择 JSON (application/json)。在 Body 字段中输入以下用户详情(JSON 格式):

    {
        "username": "james",
        "email": "james@gmail.com",
        "password": "WkQad19"
    }
    
  3. 点击 "id": 3 这里表示该用户是成功注册的第三个用户。

  4. 我们将尝试在不登录的情况下检查用户信息。让我们看看我们能得到什么信息。点击 User,并将其保存到 User 文件夹下。

  5. 编辑请求,在 URL 字段中输入 http://localhost:5000/users/james保存请求以便以后重用。

  6. 点击 Send 获取用户详情。结果如下截图所示:![图 4.4:未登录时检查用户信息

    图片

    图 4.4:未登录时检查用户信息

    你将看到响应。HTTP 状态是 200 OK,表示请求已成功。我们可以在响应体中看到 ID 和用户名。然而,我们在这里看不到电子邮件地址,因为它属于私人信息,并且只有经过认证的用户才能看到。

  7. 现在,通过 API 登录。点击 Collections 选项卡。创建一个名为 Token 的新文件夹,并在其中创建一个名为 Token 的新请求。

  8. 编辑请求,将方法更改为 URL 字段中的 http://localhost:5000/token

  9. 点击 Body 选项卡,检查 raw 单选按钮,并在下拉菜单中选择 JSON (application/json)。在 Body 字段中输入以下 JSON 内容,然后点击 Save

    {
        "email": "james@gmail.com",
        "password": "WkQad19"
    }
    
  10. 点击 Send 登录。结果如下截图所示:![图 4.5:创建令牌后检查用户信息

    图片

    图 4.5:创建令牌后检查用户信息

    你将看到响应。HTTP 状态码 200 表示登录成功。我们可以在响应体中看到访问令牌。我们将依靠这个令牌来显示用户已经登录。

  11. 现在,在我们登录后再次检查用户信息。点击 Collections 选项卡,并选择 GET User 请求。

  12. VALUE 字段中选择 Bearer {token},其中令牌是我们在第 10 步中获得的。

  13. 点击 Send 获取用户详情。结果如下截图所示:![图 4.6:登录后检查用户信息

    图片

图 4.6:登录后检查用户信息

你将看到响应,HTTP 状态码 200 表示请求成功。在响应体中,我们可以看到包括 idusernameemail 在内的信息。

在这个练习中,我们可以看到访问控制是如何真正工作的。我们可以看到用户认证前后 HTTP 响应的差异。这对我们的 Smilecook 应用程序非常重要,因为我们想保护用户的隐私。有一些信息只有经过认证的用户才能看到。

练习 26:创建 me 端点

在这个练习中,我们将创建一个特殊的端点/users/me。这将允许我们通过access_token获取认证用户信息。我们首先在user模型下创建一个新的resource类。它将有一个get方法,最后我们将将其与新的 API 端点关联起来:

  1. models/user.py中添加get_by_id方法。为了方便起见,我们将使用此方法通过 ID 获取用户对象:

    @classmethod 
    def get_by_id(cls, id):         
            return cls.query.filter_by(id=id).first() 
    
  2. resources/user.py中导入jwt_required并创建一个MeResource类:

    from flask_jwt_extended import jwt_optional, get_jwt_identity, jwt_required
    class MeResource(Resource):
        @jwt_required
        def get(self):
            user = User.get_by_id(id=get_jwt_identity())
            data = {
                    'id': user.id,
                    'username': user.username,
                    'email': user.email,
            }
            return data, HTTPStatus.OK
    

    这里使用的get方法将通过 JWT 中的 ID 获取用户信息。

  3. app.py中导入MeResource类。添加/me端点:

    from resources.user import UserListResource, UserResource, MeResource
    api.add_resource(MeResource, '/me')
    
  4. 右键单击以运行应用程序。Flask将在本地主机(127.0.0.1)的5000端口启动并运行:![图 4.7:运行应用程序以在本地主机上启动和运行 Flask

    ![图 4.7:运行应用程序以在本地主机上启动和运行 Flask

    图 4.7:运行应用程序以在本地主机上启动和运行 Flask
  5. 在使用 users/me 端点登录后,再次检查用户信息。点击收藏夹选项卡,在用户文件夹中创建一个名为Me的新请求。

  6. 在 URL 字段中输入http://localhost:5000/me

  7. 字段中选择Bearer {token},其中 token 是我们之前练习中获得的。

  8. 点击发送以获取用户详情。结果如下截图所示:![图 4.8:登录后检查用户信息

    ![图 4.7:运行应用程序以在本地主机上启动和运行 Flask

图 4.8:登录后检查用户信息

这个新的 API 端点允许我们仅通过访问令牌获取认证用户信息。这意味着当用户处于认证状态时,我们可以获取他们的信息。现在我们已经基本了解了用户,让我们来处理食谱。

在食谱模型中设计方法

现在我们已经完成了用户注册和登录功能,我们将着手处理 Smilecook 应用程序的食谱管理功能。这需要在Recipe类中实现一些方法。在我们的设计中,我们将有以下五种方法:

  • data:这是用来以字典格式返回数据的。

  • get_all_published:此方法获取所有已发布的食谱。

  • get_by_id:此方法通过 ID 获取食谱。

  • save:此方法将数据持久化到数据库。

  • delete:此方法从数据库中删除数据。

这五种方法涵盖了几乎所有必要的食谱管理功能。在下一个练习中,我们将实现这些方法在我们的 Smilecook 应用程序中。

练习 27:实现受访问控制的食谱管理功能

本练习的目的是在我们的平台上实现不同的食谱管理功能,以便用户可以在我们的 Smilecook 应用程序中管理自己的食谱。我们还将修改RecipeListResourceRecipeResource以限制对某些方法的访问:

  1. models/recipe.py中,向Recipe类添加dataget_all_publishedget_by_idsavedelete方法:

        def data(self):
            return {
                'id': self.id,
                'name': self.name,
                'description': self.description,
                'num_of_servings': self.num_of_servings,
                'cook_time': self.cook_time,
                'directions': self.directions,
                'user_id': self.user_id
            }
        @classmethod
        def get_all_published(cls):
            return cls.query.filter_by(is_publish=True).all()
        @classmethod
        def get_by_id(cls, recipe_id):
            return cls.query.filter_by(id=recipe_id).first()
        def save(self):
            db.session.add(self)
            db.session.commit()
        def delete(self):
            db.session.delete(self)
            db.session.commit()
    
  2. models/recipe.py中删除以下代码:

    recipe_list = []
    
    def get_last_id():
        if recipe_list:
            last_recipe = recipe_list[-1]
        else:
            return 1
        return last_recipe.id + 1
    
  3. resources/recipe.py中导入get_jwt_identityjwt_requiredjwt_optional

    from flask_jwt_extended import get_jwt_identity, jwt_required, jwt_optional
    
  4. 删除导入recipe_list

    from models.recipe import Recipe
    
  5. 我们将修改RecipeListResource类中的get方法。我们将通过触发Recipe.get_all_published()来获取所有已发布的食谱。然后,在for循环中,它遍历食谱列表,将每个食谱转换为字典对象,并返回字典列表:

    class RecipeListResource(Resource):
        def get(self):
            recipes = Recipe.get_all_published()
            data = []
            for recipe in recipes:
                data.append(recipe.data())
             return {'data': data}, HTTPStatus.OK
    
  6. 我们继续修改RecipeListResource类中的post方法。这里的@jwt_required装饰器表示该方法只能在用户登录后调用。在方法内部,它从客户端请求中获取所有食谱详情并将其保存到数据库中。最后,它将返回带有 HTTP 状态码201 CREATED的数据:

        @jwt_required
        def post(self):
            json_data = request.get_json()
            current_user = get_jwt_identity()
            recipe = Recipe(name= json_data['name'],
                            description= json_data['description'],
                            num_of_servings= json_data['num_of_servings'],
                            cook_time= json_data['cook_time'],
                            directions= json_data['directions'],
                            user_id=current_user)
            recipe.save()
            return recipe.data(), HTTPStatus.CREATED
    
  7. 我们将修改RecipeResource中的get方法以获取特定的食谱。@jwt_optional装饰器指定 JWT 是可选的。在方法内部,我们使用Recipe.get_by_id(recipe_id=recipe_id)来获取食谱。如果找不到特定食谱,我们将返回404 NOT_FOUND。如果找到了,然后更改食谱的所有者和状态。这里存在访问控制,所以它将根据情况返回403 FORBIDDEN200 OK

    class RecipeResource(Resource):
        @jwt_optional
        def get(self, recipe_id):
            recipe = Recipe.get_by_id(recipe_id=recipe_id)
            if recipe is None:
                return {'message': 'Recipe not found'}, HTTPStatus.NOT_FOUND
            current_user = get_jwt_identity()
            if recipe.is_publish == False and recipe.user_id != current_user:
                return {'message': 'Access is not allowed'}, HTTPStatus.FORBIDDEN
            return recipe.data(), HTTPStatus.OK
    
  8. 我们将修改RecipeResource中的put方法以获取特定的食谱。这个put方法用于更新食谱详情。它将首先检查食谱是否存在以及用户是否有更新权限。如果一切正常,它将继续更新食谱详情并将其保存到数据库中:

        @jwt_required
        def put(self, recipe_id):
            json_data = request.get_json()
            recipe = Recipe.get_by_id(recipe_id=recipe_id)
            if recipe is None:
                return {'message': 'Recipe not found'}, HTTPStatus.NOT_FOUND
            current_user = get_jwt_identity()
            if current_user != recipe.user_id:
                return {'message': 'Access is not allowed'}, HTTPStatus.FORBIDDEN
            recipe.name = json_data['name']
            recipe.description = json_data['description']
            recipe.num_of_servings = json_data['num_of_servings']
            recipe.cook_time = json_data['cook_time']
            recipe.directions = json_data['directions']
            recipe.save()
            return recipe.data(), HTTPStatus.OK
    
  9. 我们将修改RecipeResource中的delete方法以获取特定的食谱。这是用于删除食谱的。@jwt_required装饰器意味着 JWT 是必需的。当用户登录时,他们可以访问此路径并删除存在的指定食谱:

        @jwt_required
        def delete(self, recipe_id):
            recipe = Recipe.get_by_id(recipe_id=recipe_id)
            if recipe is None:
                return {'message': 'Recipe not found'}, HTTPStatus.NOT_FOUND
            current_user = get_jwt_identity()
            if current_user != recipe.user_id:
                return {'message': 'Access is not allowed'}, HTTPStatus.FORBIDDEN
            recipe.delete()
            return {}, HTTPStatus.NO_CONTENT
    

因此,在这个练习中,我们已经实现了食谱管理功能并添加了对资源的访问控制。现在,只有授权用户才能管理他们的食谱。让我们在下一个练习中测试一下这是否真的如此。

练习 28:测试食谱管理功能

这个练习的目的是使用 Postman 测试所有食谱管理功能。我们在之前的练习中注册了一个账户并登录。我们将使用相同的认证用户来测试添加、更新和删除食谱:

  1. 通过我们的 API 创建一个食谱。点击Collections标签,并选择我们之前创建的POST RecipeList请求。

  2. 前往VALUE字段中的Bearer {token},这里的 token 是我们之前练习中获得的 JWT token。结果如下所示:图 4.9:通过 API 创建食谱

    图 4.9:通过 API 创建食谱

    图 4.9:通过 API 创建食谱
  3. 前往 Body 选项卡并输入以下菜谱详情:

    {
        "name": "Cheese Pizza",
        "description": "This is a lovely cheese pizza",
        "num_of_servings": 2,
        "cook_time": 30,
        "directions": "This is how you make it"
    }
    
  4. 点击 user_id3,这是当前登录用户的用户 ID。

  5. 在用户登录的状态下获取 id = 3 的菜谱。点击 Collections 选项卡并选择我们之前创建的 GET 菜谱请求。

  6. 前往 VALUE 字段中的 Bearer {token},其中令牌是我们之前练习中获得的 JWT 令牌。

  7. 点击 Send 检查菜谱。结果如下截图所示:![图 4.11:用户登录后的 ID 为 3 的菜谱 图片

    图 4.11:用户登录后的 ID 为 3 的菜谱

    你将看到响应。我们可以在正文中看到菜谱详情。那是因为用户已经经过身份验证。

  8. 在用户未登录的状态下获取 id = 3 的菜谱。预期结果是我们将无法看到未发布的菜谱。点击 Collections 选项卡并选择我们之前创建的 GET 菜谱 请求。

  9. 前往 Headers 选项卡并取消选中 Authorization,这意味着我们不会输入 JWT 令牌。点击 Send 检查菜谱。结果如下截图所示:![图 4.12:用户未登录时的 ID 为 3 的菜谱 图片

图 4.12:用户未登录时的 ID 为 3 的菜谱

你将看到响应;HTTP 状态码是 403 禁止。这是因为菜谱尚未发布,我们在我们的 API 上实现了访问控制,以便只有经过身份验证的用户才能看到他们自己的草稿菜谱。因为我们还没有登录,所以我们看到消息 不允许访问。未发布的菜谱对公众不可用。

因此,我们已经测试了访问控制菜谱管理功能。我们可以看到这些功能如何在现实场景中使用。接下来,我们将讨论刷新令牌,这是为了保持用户登录状态。

刷新令牌

为了安全起见,我们通常为我们的令牌设置一个过期时间(flask-jwt-extended 默认为 15 分钟)。因为令牌会过期,我们需要一个函数来刷新它,而无需用户再次输入凭据。

Flask-JWT-Extended 提供了与刷新令牌相关的函数。刷新令牌是一个长期有效的令牌,可以用来生成新的访问令牌。请不要混淆刷新令牌和访问令牌。刷新令牌只能用来获取新的访问令牌;它不能用作访问令牌来访问受限制的端点。例如,具有 jwt_required()jwt_optional() 装饰器的端点需要一个访问令牌。

下面是 Flask-JWT-Extended 中与刷新令牌相关的函数的简要说明:

  • create_access_token: 这个函数创建一个新的访问令牌。

  • create_refresh_token: 这个函数创建一个刷新令牌。

  • jwt_refresh_token_required: 这是一个指定需要刷新令牌的装饰器。

  • get_jwt_identity:此函数获取持有当前访问令牌的用户。

在下一个练习中,你将了解更多关于这些函数的信息。我们还将为我们的令牌添加一个fresh属性。当用户通过输入凭证获取令牌时,此fresh属性将仅设置为True。当用户仅刷新令牌时,他们将获得一个fresh = false的令牌。刷新令牌的原因是我们希望避免用户反复输入他们的凭证。然而,对于一些关键功能,例如更改密码,我们仍然需要他们拥有一个新鲜的令牌。

练习 29:添加刷新令牌功能

在这个练习中,我们将向我们的 Smilecook 应用程序添加刷新令牌功能,以便当用户的访问令牌过期时,他们可以使用刷新令牌获取新的访问令牌:

  1. resources/token.py中,从flask_jwt_extended导入必要的函数:

    from flask_jwt_extended import (
        create_access_token,
        create_refresh_token,
        jwt_refresh_token_required,
        get_jwt_identity
    )
    
  2. 修改TokenResource下的post方法以生成用户的tokenrefresh_token

        def post(self):
            data = request.get_json()
            email = data.get('email')
            password = data.get('password')
            user = User.get_by_email(email=email)
            if not user or not check_password(password, user.password):
                return {'message': 'username or password is incorrect'}, HTTPStatus.UNAUTHORIZED
            access_token = create_access_token(identity=user.id, fresh=True)
            refresh_token = create_refresh_token(identity=user.id)
            return {'access_token': access_token, 'refresh_token': refresh_token}, HTTPStatus.OK
    

    我们将fresh=True参数传递给create_access_token函数。然后调用create_refresh_token函数生成刷新令牌。

  3. RefreshResource类添加到token.py中。请添加以下代码:

    class RefreshResource(Resource):
        @jwt_refresh_token_required
        def post(self):
            current_user = get_jwt_identity()
            access_token = create_access_token(identity=current_user, fresh=False)
            return {access_token: access_token}, HTTPStatus.OK
    

    @jwt_refresh_token_required装饰器指定此端点将需要刷新令牌。在此方法中,我们为用户生成一个fresh=false的令牌。

  4. 最后,添加RefreshResource的路由:

    from resources.token import TokenResource, RefreshResource
    def register_resources(app):
        api.add_resource(RefreshResource, '/refresh')
    
  5. 保存app.py,然后右键单击它以运行应用程序。在端口5000上运行(127.0.0.1):![图 4.13:运行应用程序以在本地主机上启动和运行 Flask]

    图 4.13:运行应用程序以在本地主机上启动和运行 Flask

图 4.13:运行应用程序以在本地主机上启动和运行 Flask

恭喜!我们刚刚添加了刷新令牌功能。让我们继续进行测试部分。

练习 30:使用刷新令牌获取新的访问令牌

在这个练习中,我们将使用 Postman 登录用户账户并获取访问令牌和刷新令牌。稍后,我们将使用刷新令牌获取新的访问令牌。这是为了模拟现实生活中的场景,我们希望保持用户登录状态:

  1. 我们将首先测试日志记录功能。点击集合标签页。选择我们之前创建的POST Token请求。

  2. 选中原始单选按钮,并从下拉菜单中选择JSON (application/json)

  3. 正文字段中添加以下 JSON 内容:

    {
        "email": "james@gmail.com",
        "password": "WkQad19"
    }
    
  4. 点击发送以登录账户。结果如下截图所示:![图 4.14:测试登录]

    图 4.14:测试登录

    图 4.14:测试登录

    我们可以看到 HTTP 状态码是200 OK,这意味着登录成功。我们还可以在正文看到访问令牌和刷新令牌。

  5. 接下来,我们将使用刷新令牌获取access令牌。点击集合标签页。创建一个新的请求,命名为刷新,并将其保存在令牌文件夹中。

  6. 选择这个新的请求,并在 URL 字段中选择 http://localhost:5000/refresh

  7. VALUE 字段中的 Bearer {token} 处,其中令牌是我们第 4 步中获得的 JWT

  8. 点击 发送 以刷新令牌。结果如下截图所示:图 4.15:使用刷新令牌访问令牌

图 4.15:使用刷新令牌访问令牌

我们可以看到 HTTP 状态 200 OK,这意味着请求已成功。我们还可以在响应体中看到新的访问令牌。如果访问令牌将来过期,我们可以使用刷新令牌来获取新的访问令牌。

用户登出机制

Flask-JWT-Extended 包支持登出功能。其工作原理是在用户登出时将令牌放入黑名单。一个 token_in_blacklist_loader 来验证用户是否已登出:

图 4.16:使用黑名单的用户登出机制

图 4.16:使用黑名单的用户登出机制

在下一个练习中,我们希望您尝试实现这个登出功能。这将测试您对登录和登出流程的理解。

练习 31:实现登出功能

在这个练习中,我们将实现登出功能。我们首先声明一个 black_list 来存储所有 已登出 的访问令牌。稍后,当用户想要访问受控的 API 端点时,我们将首先使用黑名单检查访问令牌是否仍然有效:

  1. 导入 get_raw_jwt。在 resources/token.py 中,我们将从 flask_jwt_extended 导入 jwt_requiredget_raw_jwt

    from flask_jwt_extended import (
        create_access_token,
        create_refresh_token,
        jwt_refresh_token_required,
        get_jwt_identity,
        jwt_required,
        get_raw_jwt
    )
    
  2. resources/token.py 中,将 set() 分配给 black_list

    black_list = set()
    
  3. 创建 RevokeResource 类并定义 post 方法。我们将在这里应用 @jwt_required 装饰器以控制对端点的访问。在这个方法中,我们使用 get_raw_jwt()['jti'] 获取令牌并将其放入黑名单:

    class RevokeResource(Resource):
        @jwt_required
        def post(self):
            jti = get_raw_jwt()['jti']
            black_list.add(jti)
             return {'message': 'Successfully logged out'}, HTTPStatus.OK
    
  4. 然后,在 config.py 中添加以下代码。正如你所见,我们正在启用黑名单功能,并告知应用程序检查 accessrefresh 令牌:

    class Config:
        JWT_BLACKLIST_ENABLED = True
        JWT_BLACKLIST_TOKEN_CHECKS = ['access', 'refresh']
    
  5. 然后,在 app.py 中导入 RevokeResourceblack_list

    from resources.token import TokenResource, RefreshResource, RevokeResource, black_list
    
  6. 然后,在 register_extensions(app) 内部,我们将添加以下代码行。这是为了检查令牌是否在黑名单上:

    def register_extensions(app):
        db.app = app
        db.init_app(app)
        migrate = Migrate(app, db)
        jwt.init_app(app)
        @jwt.token_in_blacklist_loader
        def check_if_token_in_blacklist(decrypted_token):
            jti = decrypted_token['jti']
            return jti in black_list
    
  7. 最后,在 register_resources 中添加路由:

    def register_resources(app):
        api.add_resource(TokenResource, '/token')
        api.add_resource(RefreshResource, '/refresh')
        api.add_resource(RevokeResource, '/revoke')
    
  8. 保存 app.py 并右键单击它以运行应用程序。此时,Flask 将在本地主机(127.0.0.1)的端口 5000 上启动并运行:图 4.17:运行应用程序以启动 Flask

图 4.17:运行应用程序以启动 Flask

一旦服务器启动,这意味着我们已经准备好测试我们的刷新令牌 API。

练习 32:测试登出功能

在这个练习中,我们将测试我们在之前练习中刚刚实现的登出功能。一旦我们登出,我们将尝试访问受访问控制的端点,并确保我们不再有访问权限:

  1. 我们将登出我们的应用程序。点击收藏标签页,创建一个新的请求,命名为撤销,并将其保存在令牌文件夹中。

  2. 选择这个新请求,并在 URL 字段中选择http://localhost:5000/revoke

  3. 字段中的Bearer {token}处,其中令牌是我们之前练习中获得的 JWT。

  4. 点击发送以登出。结果如下截图所示:图 4.18:从应用程序中登出

    图 4.18:从应用程序中登出

    你将看到响应,HTTP 状态200 OK,这意味着用户已成功登出。除此之外,我们还可以看到消息说用户已成功登出

  5. 再次登出并查看发生了什么。再次点击发送,然后你会看到以下响应:图 4.19:再次登出

图 4.19:再次登出

我们可以看到 HTTP 状态401 未授权,这意味着用户没有访问此端点的权限,因为原始访问令牌已经被列入黑名单。在响应体中,我们可以看到消息令牌已被撤销,这意味着用户已成功登出。

活动 7:在发布/取消发布食谱功能上实现访问控制

在这个活动中,我们将对publish/unpublish食谱 API 端点实现访问控制,以便只有认证用户可以publish/unpublish自己的食谱。按照以下步骤完成活动:

  1. 修改RecipePublishResource中的put方法以限制对认证用户的访问。

  2. 修改RecipePublishResource中的delete方法。

  3. 登录用户账户并获取访问令牌。

  4. 在用户已登录的状态下发布id = 3的食谱。

  5. 在用户已登录的状态下取消发布id = 3的食谱

    注意

    本活动的解决方案可以在第 307 页找到。

如果你一切都做对了,恭喜你!这意味着你已经为发布和取消发布的食谱功能添加了访问控制。现在,食谱在 Smilecook 应用程序中受到保护。只有食谱的作者现在可以管理自己的食谱。

摘要

在本章中,我们学习了如何使用 Flask-JWT-Extended 进行访问控制。这是一个重要且基本的功能,几乎所有在线平台都将需要。在本章末尾,我们简要提到了维护令牌活跃性的话题。这是高级但实用的知识,你将在开发实际的 RESTful API 时使用。在下一章中,我们将开始讨论数据验证。

第五章:5. 使用 marshmallow 进行对象序列化

学习目标

到本章结束时,你将能够:

  • 为序列化/反序列化创建一个模式

  • 验证客户端请求中的数据

  • 在向客户端显示数据之前执行数据过滤

  • 使用 HTTP PATCH 方法部分更新数据

本章涵盖了序列化和反序列化,以及使用 marshmallow 的数据过滤和验证。

简介

在信息爆炸的时代,数据的正确性至关重要。我们需要确保客户端传入的数据是我们期望的格式。例如,我们期望 cooking time 变量是一个整型数据,值为 30,但客户端可能会传入一个字符串数据类型,value = "thirty minutes"。它们意味着相同的事情,并且两者对人类来说都是可理解的,但系统无法解释它们。在本章中,我们将学习数据验证,确保系统只接受有效数据。marshmallow 包不仅帮助我们验证客户端的数据,还验证我们发送回的数据。这确保了双向数据完整性,将大大提高系统的质量。

在本章中,我们将关注做三件重要的事情:首先,我们将修改 User 类并添加 API 验证。这主要是为了展示 marshmallow 的基本功能。然后,我们将修改 Recipe 类,添加自定义认证方法,并优化代码。最后,将添加一个新功能,允许我们查询特定用户的全部食谱,并通过可见性参数过滤不同发布状态的食谱。考虑到这一点,让我们继续探讨第一个主题:序列化反序列化

序列化与反序列化

图 5.1:序列化与反序列化

图 5.1:序列化与反序列化

对象是存在于应用程序内存中的东西。我们可以在应用程序中调用其方法或访问其属性。然而,当我们想要传输或存储对象时,我们必须将其转换为可存储或可传输的格式,而这个格式将是一串字节。然后它可以存储在文本文件中,存储在数据库中,或通过互联网传输。将对象转换为字节流的过程称为序列化。这串字节流持久化对象的当前状态,以便稍后可以重新创建。从字节流中重新创建对象的过程称为反序列化。

序列化/反序列化是 RESTful API 开发的一个关键部分。在实际开发过程中,与业务逻辑相关的数据验证通常会包含在序列化和反序列化的实现过程中。

marshmallow

marshmallow 本身是一个用于 Python 中序列化和反序列化的优秀包,同时也提供了验证功能。它允许开发者定义模式,这些模式可以用不同的方式(必需和验证)来表示字段,并在反序列化过程中自动执行验证。在本章中,我们将首先实现一个数据验证函数。我们将使用 marshmallow 包来实现它,以确保用户输入的信息是正确的。我们将通过各种练习和活动与您一起测试使用 Postman 之后的序列化和反序列化。

简单模式

我们将使用 marshmallow 的 Schema 类来指定我们想要序列化/反序列化的对象的字段。如果我们不知道对象的模式以及我们想要如何序列化字段,我们就无法执行序列化或反序列化。在下面的示例中,你可以看到一个简单的 SimpleSchema 类,它扩展了 marshmallow.Schema,并且那里定义了两个字段,idusername

from marshmallow import Schema, fields
class SimpleSchema(Schema):
    id = fields.Int() 
    username = fields.String()

字段的类型使用 marshmallow 字段定义。从前面的示例中,id 字段是一个 username 字段是一个 字符串。在 marshmallow 中有几种不同的数据类型,包括 StrIntBoolFloatDateTimeEmailNested 等。

在指定了模式之后,我们可以开始进行对象的序列化和反序列化。我们可以在我们的应用程序中序列化对象并在 HTTP 响应中返回它们。或者反过来,我们可以接收用户的请求并将它反序列化为一个对象,以便在应用程序中使用。

字段验证

我们也可以在序列化/反序列化过程中添加字段级验证。同样,这可以在模式定义中完成。例如,如果我们想指定一个字段为必需的,我们可以添加 required=True 参数。使用相同的 SimpleSchema 示例,我们可以指定 username 字段为必需的,如下所示:

class SimpleSchema(Schema):
    id = fields.Int() 
    username = fields.String(required=True)

如果使用此 SimpleSchema 来反序列化用户的 JSON 请求,并且 username 字段没有填写,将会出现错误信息,“验证错误”,并且 HTTP 状态码将是400 Bad Request

{
    "message": "Validation errors",
    "errors": {
        "username": [
            "Missing data for the required field."
        ]
    }
}

现在我们将学习如何自定义反序列化方法。

自定义反序列化方法

我们还可以自定义我们想要反序列化的某些字段的格式。我们可以通过在 marshmallow 中使用 Method 字段来实现这一点。一个 Method 字段接收一个可选的 deserialize 参数,它定义了字段应该如何反序列化。

从下面的 SimpleSchema 示例中,我们可以定义一个自定义方法来反序列化 password 字段。我们只需要传递 deserialize='load_password' 参数。它将调用 load_password 方法来反序列化 password 字段:

class SimpleSchema(Schema):
    id = fields.Int() 
    username = fields.String(required=True)
    password = fields.Method(required=True, deserialize='load_password')
    def load_password(self, value): 
        return hash_password(value)

在下一节中,我们将学习如何使用 UserSchema 设计。

UserSchema 设计

现在我们已经学习了为什么需要使用 Schema 以及如何定义一个模式,我们将在我们的 Smilecook 应用程序中开始工作。在用户注册的情况下,我们期望用户在一个网页表单中填写他们的信息,然后将详细信息以 JSON 格式发送到服务器。我们的 Smilecook 应用程序然后将它反序列化为 User 对象,这样我们就可以在我们的应用程序中对其进行操作。

因此,我们需要定义一个 UserSchema 类来指定前端发送的 JSON 请求中期望的属性。我们需要以下字段:

  • id:使用 fields.Int() 来表示一个整数。此外,dump_only=True 表示这个属性仅适用于序列化,不适用于反序列化。这是因为 id 是自动生成的,不是由用户传入的。

  • username:使用 fields.String() 来表示一个字符串,并应用 required=True 来表示这个属性是必须的。当客户端发送没有用户名的 JSON 数据时,将出现验证错误。

  • email:使用 fields.Email() 来指示需要 email 格式,并应用 required=True 来表示这个属性是必须的。

  • password:fields.Method() 是一个 Method 字段。这里的 Method 字段接收一个可选的 deserialize 参数,它定义了字段应该如何进行反序列化。我们使用 deserialize='load_password' 来表示当使用 load() 反序列化时将调用 load_password(self, value) 方法。请注意,这个 load_password(self, value) 方法仅在 load() 反序列化期间被调用。

  • created_at:fields.DateTime() 表示时间格式,dump_only=True 表示这个属性将仅在序列化时可用。

  • updated_at:fields.DateTime() 表示时间格式,dump_only=True 表示这个属性将仅在序列化时可用。

在我们的下一个练习中,我们将在 Smilecook 项目中安装 marshmallow 包。然后,我们将定义 UserSchema 并将其用于 UserListResourceUserResource

练习 33:使用 marshmallow 验证用户数据

首先,我们将使用 marshmallow 进行数据验证。我们将安装 marshmallow 包并构建 UserSchema,然后将其用于 UserListResource 以传输 User 对象:

  1. 我们将首先安装 marshmallow 包。请在 requirements.txt 中输入以下内容:

    marshmallow==2.19.5
    
  2. 运行 pip install 命令:

    pip install -r requirements.txt
    

    你应该看到以下结果:

    Installing collected packages: marshmallow
    Successfully installed marshmallow-2.19.5
    
  3. Smilecook 项目下创建一个文件夹,命名为 schemas。我们将在这里存储所有的模式文件。

  4. 在该目录下创建一个 user.py 文件,并输入以下代码。使用模式来定义我们期望客户端请求内容的基本结构。以下代码创建 UserSchema 以定义我们将接收到的客户端请求中的属性:

    from marshmallow import Schema, fields
    from utils import hash_password
    class UserSchema(Schema):
        class Meta:
            ordered = True
        id = fields.Int(dump_only=True)
        username = fields.String(required=True)
        email = fields.Email(required=True)
        password = fields.Method(required=True, deserialize='load_password')
        created_at = fields.DateTime(dump_only=True)
        updated_at = fields.DateTime(dump_only=True)
        def load_password(self, value):
            return hash_password(value)
    

    在定义UserSchema之前,我们首先需要从 marshmallow 导入Schemafields。所有自定义的 marshmallow 模式都必须继承marshmallow.Schema。然后,我们导入hash_password,并在UserSchema中定义四个属性:idusernameemailpassword

  5. resources/user.py中添加以下代码。我们将首先从上一步导入UserSchema类,并在本处实例化两个UserSchema对象。其中一个用于公共用途,我们可以看到电子邮件被排除在外:

    from schemas.user import UserSchema
    user_schema = UserSchema()
    user_public_schema = UserSchema(exclude=('email', ))
    

    对于我们的用户资源,当认证用户访问其users/<username>端点时,他们可以获取idusernameemail。但如果他们未认证或访问其他人的/users/<username>端点,则电子邮件地址将被隐藏。

  6. 我们将修改UserListResource如下,以验证用户请求中的数据:

    class UserListResource(Resource):
        def post(self):
            json_data = request.get_json()
            data, errors = user_schema.load(data=json_data)
            if errors:
                return {'message': 'Validation errors', 'errors': errors}, HTTPStatus.BAD_REQUEST
    
  7. 在相同的UserListResource.post中,如果没有错误,我们将继续进行。它将检查usernameemail是否存在,如果一切正常,我们将使用User(**data)创建用户实例,**data将为User类提供关键字参数,然后我们使用user.save()将事物存储在数据库中:

            if User.get_by_username(data.get('username')):
                return {'message': 'username already used'}, HTTPStatus.BAD_REQUEST
            if User.get_by_email(data.get('email')):
                return {'message': 'email already used'}, HTTPStatus.BAD_REQUEST
            user = User(**data)
            user.save()
    
  8. 最后,在UsersLitResource.post中,我们也使用user_schema.dump(user).data来返回成功注册的用户数据。它将包含idusernamecreated_atupdated_atemail

            return user_schema.dump(user).data, HTTPStatus.CREATED
    
  9. 接下来,我们将修改UserResource。在这里,我们将看到使用user_schemauser_public_schema进行电子邮件过滤和不进行过滤之间的区别:

    class UserResource(Resource):
        @jwt_optional
        def get(self, username):
            user = User.get_by_username(username=username)
            if user is None:
                return {'message': 'user not found'}, HTTPStatus.NOT_FOUND
            current_user = get_jwt_identity()
            if current_user == user.id:
                data = user_schema.dump(user).data
            else:
                data = user_public_schema.dump(user).data
            return data, HTTPStatus.OK
    

    当用户向/users/<username/>发送请求时,我们将获取他们的用户名。如果找不到用户,我们将获取user_schema.dump(user).data,其中包含所有信息。否则,将使用user_public_schema.dump(user).data,它不包括电子邮件信息。最后,它将返回带有 HTTP 状态码200 OK的数据。

  10. 接下来,我们将修改MeResource。它将使用user_schema.dump(user).data进行序列化,其中包含用户的所有信息:

    class MeResource(Resource):
        @ jwt_required
        def get(self): 
             user = User.get_by_id(id=get_jwt_identity())
             return user_schema.dump(user).data, HTTPStatus.OK
    
  11. 保存app.py,然后右键单击它以运行应用程序。然后 Flask 将在本地主机(127.0.0.1)的端口5000上启动并运行:![图 5.2:运行应用程序,然后在本地主机上运行 Flask]

    ![图片 C15309_05_02.jpg]

![图 5.2:运行应用程序,然后在本地主机上运行 Flask]

因此,我们已经完成了将棉花糖添加到图片中的工作。从现在开始,当我们在前端和后端之间传输User对象时,它将首先进行序列化/反序列化。在这个过程中,我们可以利用 marshmallow 提供的数据验证函数来使我们的 API 端点更加安全。

练习 34:在认证前后测试用户端点

在之前的练习中,我们实现了不同的用户模式,一个用于私有查看,一个用于公共查看。在这个练习中,我们将测试它们是否按预期工作。我们将检查 HTTP 响应中的数据,并验证在认证前后我们是否得到不同的用户信息。我们希望从公共中隐藏用户的电子邮件地址,以保护用户隐私。

我们将使用 Postman 进行整个测试。让我们开始吧!

  1. 在用户登录前检查 user 详情。我们不应该在结果中看到用户的电子邮件地址。点击 集合 选项卡。

  2. 选择 GET User 请求。

  3. 在 URL 字段中输入 http://localhost:5000/users/james。你可以将用户名 James 替换为任何合适的用户名。

  4. 点击 user 详情。在响应体中,我们可以看到 James 的用户详情。我们可以看到 usernamecreated_atupdated_atid,但没有电子邮件地址。

  5. 现在,让我们使用 Postman 登录。选择 POST Token 请求。点击 发送 进行登录。结果如下所示:

  6. ![图 5.4:登录并选择 POST Token 请求 图片

    图 5.4:登录并选择 POST Token 请求

    然后,你会看到访问令牌和刷新令牌的响应体。

  7. 在用户登录后检查 user 详情。你应该在结果中看到用户的电子邮件地址。点击 集合 选项卡。选择 GET User。选择 头部 选项卡。

  8. 在我们获得的 JWT 令牌的 Bearer {token} 中输入 Authorization

  9. 点击 发送 检查 James 的用户详情。结果如下所示:![图 5.5:用户登录后查看详情 图片

图 5.5:用户登录后查看详情

然后,你会看到返回的响应。在响应体中,我们可以看到 James 的用户详情。我们可以看到他的所有信息,包括电子邮件地址。

因此,通过使用用户模式中的 exclude 参数,我们可以轻松地排除某些敏感字段在 HTTP 响应中显示。除了 exclude 参数之外,marshmallow 还有一个 include 参数,如果你感兴趣,可以自己进一步探索。

RecipeSchema 设计

因此,我们已经对 User 对象进行了序列化和反序列化。现在我们将为 Recipe 对象设计模式。在 Recipe 更新的情况下,我们期望用户在一个网页表单中填写更新的食谱详情,然后将详情以 JSON 格式发送到服务器。我们的 Smilecook 应用程序然后将它反序列化为 Recipe 对象,我们可以在应用程序中对其进行操作。

RecipeSchema 应该继承 marshmallow.Schema 并包含以下属性:

  • id: 使用 fields.Int() 来表示一个整数,并应用 dump_only=True 来指定这个属性仅用于序列化。

  • name:使用 fields.String() 来表示一个字符串,并应用 required=True 来指示此属性是必需的。

  • description:使用 fields.String() 来表示一个字符串。

  • num_of_servings:使用 fields.Int() 来表示一个整数。

  • cook_time:使用 fields.Int() 来表示一个整数。

  • directions:使用 fields.String() 来表示一个字符串。

  • is_publish:使用 fields.Boolean() 来表示布尔值,并应用 dump_only=True 来指定此属性仅可用于序列化。

  • author:此属性用于显示菜谱的作者。

  • created_at:使用 fields.DateTime 来表示时间的格式,dump_only=True 表示此属性仅可用于序列化。

  • updated_at:使用 fields.DateTime 来表示时间的格式,dump_only=True 表示此属性仅可用于序列化。

练习 35:实现 RecipeSchema

现在我们已经构思了 RecipeSchema 的设计。在这个练习中,我们将通过实现 RecipeSchema 来学习更多关于 marshmallow 的知识。我们不仅可以验证 fields 的数据类型,还可以构建自己的验证函数。让我们开始吧:

  1. 首先,我们导入 schemafieldspost_dumpvalidatevalidatesValidationError,并在 schemas/recipe.py 文件中输入以下代码来创建 recipe schema

    from marshmallow import Schema, fields, post_dump, validate, validates, ValidationError
    class RecipeSchema(Schema):
        class Meta:
            ordered = True
        id = fields.Integer(dump_only=True)
        name = fields.String(required=True, validate=[validate.Length(max=100)])
        description = fields.String(validate=[validate.Length(max=200)])
        directions = fields.String(validate=[validate.Length(max=1000)])
        is_publish = fields.Boolean(dump_only=True)
        created_at = fields.DateTime(dump_only=True)
        updated_at = fields.DateTime(dump_only=True)
    

    我们可以通过传递 validate 参数来对字段执行额外的验证。我们使用 validate.Length(max=100) 来限制此属性的最大长度为 100。当它超过 100 时,将触发验证错误。这可以防止用户传递过长的字符串,从而给我们的数据库带来负担。使用 marshmallow 的 validation 函数可以轻松防止这种情况。

  2. 然后,我们在 RecipeSchema 中定义 validate_num_of_servings(n) 方法,这是一个自定义的验证函数。这将验证此属性的最小值为 1,且不能大于 50。如果其值不在此范围内,它将引发错误信息:

    def validate_num_of_servings(n):
        if n < 1:
            raise ValidationError('Number of servings must be greater than 0.')
        if n > 50:
            raise ValidationError('Number of servings must not be greater than 50.')
    
  3. 接下来,在 RecipeSchema 中添加 num_of_servings 属性。使用 validate=validate_num_of_servings 来链接到我们的自定义函数,该函数将验证此菜谱的份量数:

    num_of_servings = fields.Integer(validate=validate_num_of_servings)
    
  4. 我们还可以通过添加自定义验证方法来添加另一个方法。我们可以在 RecipeSchema 中添加 cooktime 属性:

    cook_time = fields.Integer()
    
  5. 然后,在 RecipeSchema 中,使用 @validates('cook_time') 装饰器来定义验证方法。当验证 cook_time 属性时,它将调用 validate_cook_time 方法来指定烹饪时间应在 1 分钟到 300 分钟之间:

        @validates('cook_time')
        def validate_cook_time(self, value):
            if value < 1:
                raise ValidationError('Cook time must be greater than 0.')
            if value > 300:
                raise ValidationError('Cook time must not be greater than 300.')
    
  6. schemas/recipe.py 文件之上,导入 UserSchema 从 marshmallow,因为我们将在显示菜谱信息时一起显示作者信息:

    from schemas.user import UserSchema
    
  7. 然后,在RecipeSchema中定义属性author。我们使用fields.Nested将此属性链接到外部对象,在这种情况下是UserSchema

    author = fields.Nested(UserSchema, attribute='user', dump_only=True, only=['id', 'username'])
    

    为了避免任何混淆,此属性在 JSON 响应中命名为author,但原始属性名是user。此外,dump_only=True表示此属性仅可用于序列化。最后,添加only=['id', 'username']以指定我们只显示用户的 ID 和用户名。

  8. 此外,我们添加了@post_dump(pass_many=True)装饰器,以便在配方序列化时进行进一步处理。代码如下:

        @post_dump(pass_many=True)
        def wrap(self, data, many, **kwargs):
            if many:
                return {'data': data}
            return data
    

    在只返回一个配方的情况下,它将简单地以 JSON 字符串的形式返回。但是当我们返回多个配方时,我们将配方存储在列表中,并使用 JSON 中的{'data': data}格式返回它们。这种格式将对我们开发分页功能有益。

  9. schemas/recipe.py中的代码现在应该如下所示——请检查它:

    from marshmallow import Schema, fields, post_dump, validate, validates, ValidationError
    from schemas.user import UserSchema
    def validate_num_of_servings(n):
        if n < 1:
            raise ValidationError('Number of servings must be greater than 0.')
        if n > 50:
            raise ValidationError('Number of servings must not be greater than 50.')
    class RecipeSchema(Schema):
        class Meta:
            ordered = True
        id = fields.Integer(dump_only=True)
        name = fields.String(required=True, validate=[validate.Length(max=100)])
        description = fields.String(validate=[validate.Length(max=200)])
        num_of_servings = fields.Integer(validate=validate_num_of_servings)
        cook_time = fields.Integer()
        directions = fields.String(validate=[validate.Length(max=1000)])
        is_publish = fields.Boolean(dump_only=True)
        author = fields.Nested(UserSchema, attribute='user', dump_only=True, only=['id', 'username'])
        created_at = fields.DateTime(dump_only=True)
        updated_at = fields.DateTime(dump_only=True)
        @post_dump(pass_many=True)
        def wrap(self, data, many, **kwargs):
            if many:
                return {'data': data}
            return data
        @validates('cook_time')
        def validate_cook_time(self, value):
            if value < 1:
                raise ValidationError('Cook time must be greater than 0.')
            if value > 300:
                raise ValidationError('Cook time must not be greater than 300.'
    

    一旦我们完成了配方模式,我们就可以开始在相关资源中使用它。

  10. 然后,我们将修改resources/recipe.py如下所示:

    from schemas.recipe import RecipeSchema
    recipe_schema = RecipeSchema()
    recipe_list_schema = RecipeSchema(many=True)
    

    我们首先从schemas.recipe导入RecipeSchema,然后定义recipe_schema变量和recipe_list_schema;它们用于存储单个和多个配方。

  11. 修改RecipeListResourceget方法,使用recipe_list_schema.dump(recipes).data方法将所有已发布的配方返回给客户端:

    class RecipeListResource(Resource):
        def get(self):
            recipes = Recipe.get_all_published()
            return recipe_list_schema.dump(recipes).data, HTTPStatus.OK
    
  12. 修改RecipeListResourcepost方法以使用配方模式:

        @jwt_required
        def post(self):
            json_data = request.get_json()
            current_user = get_jwt_identity()
            data, errors = recipe_schema.load(data=json_data)
            if errors:
                return {'message': "Validation errors", 'errors': errors}, HTTPStatus.BAD_REQUEST
            recipe = Recipe(**data)
            recipe.user_id = current_user
            recipe.save()
            return recipe_schema.dump(recipe).data, HTTPStatus.CREATED
    

    接收到 JSON 数据后,通过recipe_schema.load(data=json_data)验证数据。如果有错误,将使用Recipe(**data)创建一个recipe对象,然后通过recipe.user_id = current_user将其指定为当前登录用户的 ID。然后,通过recipe.save()将配方保存到存储库,并最终使用recipe_schema.dump(recipe).data将 JSON 转换为客户端,并带有 HTTP 状态码201 CREATED消息。

  13. 由于我们的数据渲染是通过棉花糖完成的,因此我们不需要在配方中使用data方法,所以我们可以从model/recipe.py中删除data方法。也就是说,从文件中删除以下代码:

        def data(self):
            return {
                'id': self.id,
                'name': self.name,
                'description': self.description,
                'num_of_servings': self.num_of_servings,
                'cook_time': self.cook_time,
                'directions': self.directions,
                'user_id': self.user_id
            }
    
  14. 现在我们已经完成了实现。右键单击它以运行应用程序。然后 Flask 将在本地主机(127.0.0.1)的端口5000上启动并运行:图 5.6:运行应用程序,然后在本地主机上运行 Flask

图 5.6:运行应用程序,然后在本地主机上运行 Flask

因此,我们刚刚完成了RecipeSchema的工作,以及修改 API 端点以使用序列化/反序列化方法传输对象。在下一个练习中,我们将测试我们的实现是否有效。

练习 36:测试配方 API

为了测试对象的序列化/反序列化是否正常工作,我们还需要在 Postman 中再次进行测试。这个练习是为了测试使用 Postman 创建和获取所有食谱详情。

  1. 首先,登录账户。我们之前的令牌只有效 15 分钟。如果它过期,我们需要通过/token再次登录或使用Refresh令牌重新获取令牌。点击Collections标签页。

  2. 选择POST Token请求。

  3. 点击Send进行登录。结果如下所示:![图 5.7:登录账户并选择 POST Token 请求 图 5.9:发布 ID 为 4 的食谱

    图 5.7:登录账户并选择 POST Token 请求

    你将看到返回的响应,HTTP 状态是 200 OK,表示登录成功,我们将在响应体中看到访问令牌。这个访问令牌将在后续步骤中使用。

  4. 接下来,我们将创建一个新的食谱。点击Collections标签页。选择POST RecipeList

  5. 在我们上一步获取的 JWT 令牌中的Bearer {token}中选择Authorization

  6. 选择Body标签页。按照以下内容填写食谱详情:

    {
        "name": "Blueberry Smoothie",
        "description": "This is a lovely Blueberry Smoothie",
        "num_of_servings": 2,
        "cook_time": 10,
        "directions": "This is how you make it"
    }
    
  7. 点击Send创建一个新的食谱。结果如下所示:![图 5.8:创建一个新的食谱 图 5.9:发布 ID 为 4 的食谱

    图 5.8:创建一个新的食谱

    你将看到返回的响应,HTTP 状态是 201 CREATED,表示新食谱已成功创建。在响应体中,我们可以看到食谱详情。我们还可以看到以嵌套格式显示的作者详情。

  8. 然后,我们将使用id = 4发布食谱。点击Enter request URL中的http://localhost:5000/recipes/4/publish

  9. id = 4Bearer {token}中选择Authorization。结果如下所示:![图 5.9:发布 ID 为 4 的食谱 图 5.9:发布 ID 为 4 的食谱

    图 5.9:发布 ID 为 4 的食谱

    你将看到返回的响应,HTTP 状态是204 NO CONTENT,表示已成功发布。你将在正文中看到没有内容。

  10. 然后,我们将获取所有食谱。选择GET RecipeList请求。点击Send以获取所有食谱。结果如下所示:![图 5.10:通过选择 GET RecipeList 请求获取所有食谱 图 5.7:登录账户并选择 POST Token 请求

图 5.10:通过选择 GET RecipeList 请求获取所有食谱

你将看到返回的响应,HTTP 状态是200 OK,表示我们已成功检索到所有食谱详情。在响应体中,我们可以看到一个数据列表,其中包含所有已发布的食谱。

因此,我们已经成功实现了对食谱相关 API 端点的序列化(创建食谱)和反序列化(检索食谱)的测试。我们在这一方面取得了良好的进展!

PATCH 方法

我们一直在使用PUTHTTP 方法进行数据更新。然而,PUT方法的实际用法是PUT /items/1意味着替换/items/1中的所有内容。如果该项目已存在,它将被替换。否则,它将创建一个新的项目。PUT必须包含items/1的所有属性数据。

这似乎在所有情况下都不太有效。如果你只想更新items/1的其中一个属性,你需要重新传输items/1的所有属性到服务器,这非常低效。因此,有一个新的 HTTP 方法:PATCH方法被发明出来以进行部分更新。使用此方法,我们只需要将需要修改的属性传递到服务器。

练习 37:使用 PATCH 方法更新食谱

在这个练习中,我们将将食谱更新方法从PUT更改为PATCH。我们还将使用序列化/反序列化方法来传输食谱。最后,我们将在 Postman 中测试我们的更改,以确保一切按预期工作。这个练习的目的是在更新食谱数据时减少带宽和服务器处理资源:

  1. RecipeListResource中创建patch方法。我们首先使用request.get_json()获取客户端发送的 JSON 食谱详细信息,然后使用recipe_schema.load(data=json_data, partial=('name',))来验证数据格式。我们使用partial=('name',)是因为原始名称是模式中的必填字段。当客户端只想更新单个属性时,使用partial允许我们指定Name属性是可选的,因此即使我们没有传递此属性也不会发生错误:

       @jwt_required
        def patch(self, recipe_id):
            json_data = request.get_json()
            data, errors = recipe_schema.load(data=json_data, partial=('name',))
    
  2. 然后,在同一个patch方法中,我们将检查是否有错误消息。如果有,它将返回HTTP 状态码 400 错误请求的错误消息。如果验证通过,然后检查用户是否有权更新此食谱。如果没有,将返回HTTP 状态码禁止 403

            if errors:
                return {'message': 'Validation errors', 'errors': errors}, HTTPStatus.BAD_REQUEST
            recipe = Recipe.get_by_id(recipe_id=recipe_id)
            if recipe is None:
                return {'message': 'Recipe not found'}, HTTPStatus.NOT_FOUND
            current_user = get_jwt_identity()
            if current_user != recipe.user_id:
                return {'message': 'Access is not allowed'}, HTTPStatus.FORBIDDEN
    
  3. 我们继续在同一个patch方法上工作。recipe.name = data.get('name') or recipe.name意味着它将尝试获取数据的键值名称。如果此值存在,它将被使用。否则,recipe.name将保持不变。这基本上是我们如何进行更新的:

            recipe.name = data.get('name') or recipe.name
            recipe.description = data.get('description') or recipe.description
            recipe.num_of_servings = data.get('num_of_servings') or recipe.num_of_servings
            recipe.cook_time = data.get('cook_time') or recipe.cook_time
            recipe.directions = data.get('directions') or recipe.directions
    
  4. 在同一个patch方法中,我们使用save方法将所有内容保存到数据库,并以 JSON 格式返回食谱数据:

            recipe.save()
            return recipe_schema.dump(recipe).data, HTTPStatus.OK
    
  5. 现在我们已经有了新的patch方法。右键单击它以运行应用程序。Flask 将在本地主机(127.0.0.1)的端口5000上启动并运行:![图 5.11:运行应用程序然后在本地主机上运行 Flask img/C15309_05_06.jpg

    图 5.11:运行应用程序然后在本地主机上运行 Flask

    接下来,我们将使用id = 4更新食谱。我们只更新两个字段:num_of_servingscook_time

  6. 点击收藏夹选项卡。选择PUT Recipe请求。将HTTP方法从PUT更改为PATCH

  7. 在我们之前练习中获得的 JWT 令牌中的 Bearer {token} 中选择 Authorization

  8. 选择 Body 选项卡。在 Body 字段中输入以下内容:

    {
        "num_of_servings": 4,
        "cook_time": 20
    }
    

    点击 发送 更新食谱。结果如下截图所示:

    ![图 5.12:更新食谱]

    ![图片 C15309_05_12.jpg]

图 5.12:更新食谱

您将看到返回的响应中 num_of_servingscook_time 已更新。我们还可以看到 updated_at 时间戳也已自动更新。

搜索作者和未发布的食谱

Smilecook平台上,将会有来自世界各地的许多不同的美食爱好者(在这里,我们称他们为作者)分享他们的食谱。在这些杰出的作者中,我们肯定会有一个喜欢的作者,并且我们肯定想学习他们所有的食谱。因此,我们增加了一个新的端点(或功能),即列出特定作者的食谱。这个端点不仅列出了某个美食家发布的所有食谱,还可以允许作者搜索他们所有已发布/未发布的食谱。

使用 webargs 包解析请求参数

请求参数,也称为查询字符串,是我们可以通过 URL 传递的参数。例如,在 URL http://localhost/testing?abc=123 中,abc=123 是请求参数。

GET http://localhost:5000/user/{username}/recipes,以获取特定作者的已发布食谱。对于这个端点,我们将传递可见性请求参数。visibility 请求参数可以具有 publicprivateall 的值。默认值是 public。如果是 privateall,用户需要先进行认证。

如果您只想获取未发布的食谱,可以添加请求参数 visibility=private。因此,URL 将看起来像这样:http://localhost:5000/user/{username}/recipes?visibility=privatewebargs 包提供了解析这个 visibility=private 参数的函数,然后我们的 Smilecook 应用程序将知道这个请求是要求获取食谱的私人信息。然后,我们的 Smilecook 应用程序将确定认证用户是否是作者。如果是,它将返回所有未发布的食谱。否则,用户没有权限查看未发布的食谱。

练习 38:在食谱上实现访问控制

在这个练习中,我们将实现食谱的访问控制。因此,只有认证用户才能看到他们所有的食谱,包括未发布的食谱。用户将通过使用 request 参数传递 visibility 模式。我们使用 webargs 解析可见模式,并相应地返回已发布、未发布或所有食谱:

  1. models/recipe.py 中的 Recipe 类中创建 get_all_by_user 方法:

        @classmethod
        def get_all_by_user(cls, user_id, visibility='public'):
            if visibility == 'public':
                return cls.query.filter_by(user_id=user_id, is_publish=True).all()
            elif visibility == 'private':
                return cls.query.filter_by(user_id=user_id, is_publish=False).all()
            else:
                return cls.query.filter_by(user_id=user_id).all()
    

    此方法需要接收 user_idvisibility。如果 visibility 未定义,则默认为 public。如果 visibilitypublic,它将根据 user_idis_publish=True 获取所有食谱。如果 visibilityprivate,它将搜索 is_publish=False 的食谱。如果 visibility 不是 publicprivate,它将获取此用户的全部食谱。

  2. 我们将安装 webargs 包,这是一个用于解释和验证 HTTP 参数(例如,visibility)的包。请在 requirements.txt 中添加以下包:

    webargs==5.4.0
    
  3. 使用以下命令安装包:

    pip install -r requirements.txt
    

    你应该看到以下类似的结果:

    Installing collected packages: webargs
    Successfully installed webargs-5.4.0
    
  4. resources/user.py 中导入必要的模块、函数和类:

    from flask import request
    from flask_restful import Resource
    from flask_jwt_extended import get_jwt_identity, jwt_required, jwt_optional
    from http import HTTPStatus
    from webargs import fields
    from webargs.flaskparser import use_kwargs
    from models.recipe import Recipe
    from models.user import User
    from schemas.recipe import RecipeSchema
    from schemas.user import UserSchema
    

    首先,导入 webargs.fieldswebargs.flaskparser.use_kwargs,然后我们需要使用食谱数据,因此还需要导入食谱模型和模式。

  5. 然后,我们将声明 recipe_list_schema 变量。使用 RecipeSchema 并带有 many=True 参数。这是为了表明我们将有多个食谱:

    recipe_list_schema = RecipeSchema(many=True)
    
  6. 然后,我们将创建 UserRecipeListResource 类。此资源主要用于获取特定用户的食谱。请参考以下代码:

    class UserRecipeListResource(Resource):
        @jwt_optional
        @use_kwargs('visibility': fields.Str(missing='public')})
        def get(self, username, visibility):
    

    首先,定义 @jwt_optional 表示此端点可以在用户未登录的情况下访问。然后,使用 @use_kwargs({'visibility': fields.Str(missing='public')}) 指定我们期望在这里接收 visibility 参数。如果参数不存在,默认将是 public。然后,visibility 参数将被传递到 def get(self, username, visibility)

  7. 我们将在 UserRecipeListResource.get 中实现访问控制。如果用户名(食谱的作者)是当前认证的用户,则他们可以查看所有食谱,包括私有的。否则,他们只能查看已发布的食谱:

    def get(self, username, visibility):
            user = User.get_by_username(username=username)
            if user is None:
                return {'message': 'User not found'}, HTTPStatus.NOT_FOUND
            current_user = get_jwt_identity()
            if current_user == user.id and visibility in ['all', 'private']:
                pass
            else:
                visibility = 'public'
            recipes = Recipe.get_all_by_user(user_id=user.id, visibility=visibility)
            return recipe_list_schema.dump(recipes).data, HTTPStatus.OK
    

    然后通过 User.get_by_username(username=username) 获取用户。如果用户找不到,将返回 HTTP 状态码 get_jwt_identity() 并将其保存到 current_user 变量中。

    根据用户及其权限,我们将显示不同的食谱集合。在获取食谱后,使用 recipe_list_schema.dump(recipes).data 将食谱转换为 JSON 格式,并带有 HTTP 状态码 200 OK 返回给客户端。

  8. 然后,在 app.py 中导入 UserRecipeListResource

    from resources.user import UserListResource, UserResource, MeResource, UserRecipeListResource
    
  9. 最后,添加以下端点:

    api.add_resource(UserListResource, '/users')
    api.add_resource(UserResource, '/users/<string:username>')
    api.add_resource(UserRecipeListResource, '/users/<string:username>/recipes')
    
  10. 现在,我们已经完成了实现。右键单击它以运行应用程序。Flask 将在本地主机 (127.0.0.1) 的端口 5000 上启动并运行:图 5.13:在本地主机上运行 Flask

图 5.13:在本地主机上运行 Flask

现在我们已经学习了如何使用 webargs 解析 request 参数,并将其应用于我们的 Smilecook 应用程序。接下来,像往常一样,我们想要测试并确保它正常工作。

练习 39:从特定作者检索食谱

这个练习是为了测试我们在上一个练习中实现的内容。我们将确保 API 能够解析用户传入的可见性模式,并相应地返回不同的食谱集合。我们将使用特定的用户(James)进行测试。我们将看到在认证前后,用户将能够看到不同的食谱集合:

  1. 在用户登录之前,我们将获取特定用户的全部已发布食谱。首先,点击收藏集标签。

  2. UserRecipeList下添加一个新的请求并保存。

  3. URL字段中选择新创建的http://localhost:5000/users/james/recipes(如有必要,更改用户名)。

  4. 点击发送以检查特定用户(此处为 James)下的所有已发布食谱。结果如下所示:图 5.14:在用户登录之前获取用户的全部已发布食谱

    图 5.14:在用户登录之前获取用户的全部已发布食谱

    然后,您将看到返回的响应。这里的 HTTP 状态码200 OK表示请求已成功,在正文中,我们可以看到这位作者下有一个已发布的食谱。

  5. 与上一步类似,我们将查看在用户登录之前是否可以获取特定用户下的所有食谱——这不应该被允许。选择可见性。设置为all。点击发送以检查特定用户下的所有食谱。结果如下所示:图 5.15:检查特定用户下的所有食谱

    图 5.15:检查特定用户下的所有食谱

    然后,您将看到返回的响应。这里的 HTTP 状态码200 OK表示请求已成功,在正文中再次显示,尽管我们请求所有食谱,但我们只能看到这位作者下有一个已发布的食谱,因为用户尚未登录。

  6. 登录并点击收藏集标签。选择POST Token请求。点击发送以检查特定用户下的所有食谱。结果如下所示:图 5.16:选择 POST 令牌请求并发送请求

    图 5.16:选择 POST 令牌请求并发送请求

    然后,您将看到返回的响应。这里的HTTP 状态码 200 OK表示请求已成功,在正文中,我们可以获取到我们将用于下一步的访问令牌和刷新令牌。

  7. 字段中选择Bearer {token}中的Authorization,其中令牌是我们上一步中获得的JWT令牌。点击发送进行查询。结果如下所示:图 5.17:使用 JWT 令牌并发送查询

图 5.17:使用 JWT 令牌并发送查询

您将看到返回的响应。这里的HTTP 状态码 200 OK表示请求已成功。在响应体中,我们可以获取此用户下的所有食谱,包括未发布的食谱。

这个测试练习总结了我们对webargs包的了解,以及测试了我们为查看食谱添加的新访问控制功能。

活动八:使用 Marshmallow 序列化食谱对象

在这个活动中,我们希望您专注于RecipeResource.get方法的序列化。我们之前在练习中已经对UserRecipeList进行了序列化。现在,轮到您处理这个最后的任务了。

目前,RecipeResource.get正在使用recipe.data()返回recipe对象。我们希望您用 Marshmallow 序列化recipe对象来替换它。recipe对象应转换为 JSON 格式并返回到前端客户端。为此,您需要修改resources/recipe.py中的recipe_schema。您还必须在最后使用 Postman 测试您的实现。

执行以下步骤:

  1. 修改食谱模式,包括所有属性,除了email

  2. RecipeResource中的get方法修改为使用食谱模式将recipe对象序列化为 JSON 格式。

  3. 运行应用程序,以便 Flask 在本地主机上启动和运行。

  4. 通过 Postman 获取一个特定的已发布食谱来测试实现。

    注意

    活动的解决方案可以在第 312 页找到。

在这个活动之后,您应该对如何使用模式来序列化对象有很好的理解。我们可以灵活地指定需要序列化的属性以及它们的序列化方式。与另一个对象链接的属性也可以进行序列化。正如您从这个活动中可以看到的,作者信息包含在这个食谱响应中。

摘要

在本章中,我们学到了很多。通过 Marshmallow 进行 API 数据验证非常重要。这个功能在生产环境中也应该不断更新,以确保我们接收到的信息是正确的。

在本章中,我们首先从注册成员的验证开始,然后讨论了基本验证方法,例如设置必填字段、执行数据类型验证等。除了数据验证之外,Marshmallow 还可以用于数据过滤。我们可以使用exclude参数来显示用户电子邮件字段。基于我们所学的内容,我们随后为我们的应用程序开发了定制的验证,例如验证食谱创建时间的长度。

在本章的最后,我们添加了获取我们最喜欢的作者所写所有食谱的功能。然后,我们通过visibility参数搜索不同的发布状态,并相应地应用访问控制。

第六章:6. 电子邮件确认

学习目标

到本章结束时,您将能够:

  • 使用 Mailgun API 发送纯文本和 HTML 格式的电子邮件

  • 使用 itsdangerous 包创建用于账户激活的令牌

  • 利用整个用户注册工作流程

  • 利用环境变量的优势开发应用程序

本章介绍了如何使用电子邮件包在食品食谱分享平台上开发电子邮件激活功能,以及用户注册和电子邮件验证。

简介

在上一章中,我们使用 marshmallow 验证了 API。在本章中,我们将向我们的应用程序添加功能,使我们能够向用户发送电子邮件。

每个人都有自己的电子邮件地址。有些人甚至可能为了不同的需求拥有多个邮箱。为了确保用户在创建我们应用程序的账户时输入的电子邮件地址的正确性,我们需要在注册时验证他们的电子邮件地址。获取他们的电子邮件地址是重要的,因为我们可能需要将来向用户发送电子邮件。

在本章中,我们将实现一个验证邮箱的功能,学习如何通过第三方 Mailgun API 发送消息,并创建一个唯一的令牌以确保它被用户验证。这可以通过 itsdangerous 包实现。在本章结束时,我们将通过将其分类到环境变量中,使我们的机密信息(例如,Mailgun API 密钥)更加安全。这样,当我们将来将项目上传到 GitHub 或其他平台时,这些机密信息将不会在项目中共享。以下是新用户注册流程的步骤:

图 6.1:新用户注册流程

图 6.1:新用户注册流程

图 6.1:新用户注册流程

在我们的第一部分,我们将向您介绍 Mailgun 平台。无需多言,让我们开始吧。

Mailgun

Mailgun 是一家第三方 SMTP简单邮件传输协议)和 API 发送电子邮件的服务提供商。通过 Mailgun,不仅可以发送大量电子邮件,还可以追踪每封邮件的日志。您每月有 10,000 个免费配额。这意味着,在免费计划中,我们最多只能发送 10,000 封电子邮件。这对于我们的学习目的来说已经足够了。

Mailgun 还提供了一个易于理解和使用的开放 RESTful API。在接下来的练习中,我们将注册一个 Mailgun 账户,并通过 API 发送电子邮件。

练习 40:开始使用 Mailgun

首先,我们需要在 Mailgun 中注册一个账户。正如我们之前所解释的,Mailgun 是一个第三方平台。在这个练习中,我们将注册一个 Mailgun 账户,然后获取使用他们电子邮件发送服务 API 所需的必要设置信息:

  1. 访问 Mailgun 网站 www.mailgun.com/。点击 注册 来注册一个账户。主页将看起来像以下截图:图 6.2:Mailgun 主页

    图片

    图 6.2:Mailgun 主页

    一旦完成注册,Mailgun 将发送包含账户激活链接的验证邮件。

  2. 点击验证邮件中的链接以激活账户,如下截图所示:![图 6.3:Mailgun 账户激活邮件 图片

    图 6.3:Mailgun 账户激活邮件
  3. 然后,我们将遵循 Mailgun 的验证流程。输入您的电话号码以获取验证码。使用该代码激活您的账户。屏幕将看起来像这样:![图 6.4:验证账户 图片

    图 6.4:验证账户
  4. 账户激活后,登录您的账户,然后转到发送下的概览屏幕。在那里,您可以找到域名、API 密钥和基本 URL。这些信息是我们后续编程工作所需的信息。Mailgun 还提供了快速入门的示例代码:![图 6.5:Mailgun 仪表板 图片

图 6.5:Mailgun 仪表板

现在我们已经在 Mailgun 中开设了一个账户,这将允许我们使用他们的服务向我们的用户发送邮件。API URL 和密钥是用于我们的 Smilecook 应用程序连接到 Mailgun API 的。我们很快就会向您展示如何做到这一点。

注意

目前,我们正在使用沙盒域进行测试。您只能向自己的电子邮件地址发送邮件(即与 Mailgun 注册的电子邮件地址)。如果您想向其他电子邮件地址发送邮件,您可以在右侧添加授权收件人,并将邮件发送给该收件人。收件人需要接受您发送的邮件。

我们将在下一个练习中讲解如何发送第一封邮件。

练习 41:使用 Mailgun API 发送邮件

因此,我们已经在 Mailgun 上注册了一个账户。有了这个 Mailgun 账户,我们将能够使用 Mailgun API 向我们的用户发送邮件。在这个练习中,我们将使用 Mailgun 在我们的 Smilecook 项目中以编程方式发送第一封测试邮件:

  1. mailgun.py文件下,Smilecook项目中导入 requests 并创建MailgunApi类:

    import requests
    class MailgunApi:
    
  2. 在相同的MailgunApi类中,将API_URL设置为https://api.mailgun.net/v3/{}/messages;这是 Mailgun 提供的API_URL

        API_URL = 'https://api.mailgun.net/v3/{}/messages'
    
  3. 在相同的MailgunApi类中,定义用于实例化对象的__init__构造方法:

        def __init__(self, domain, api_key):
            self.domain = domain
            self.key = api_key
            self.base_url = self.API_URL.format(self.domain)
    
  4. 在相同的MailgunApi类中,定义用于通过 Mailgun API 发送邮件的send_email方法。此方法接受tosubjecttexthtml作为输入参数并组成邮件:

       def send_email(self, to, subject, text, html=None):
            if not isinstance(to, (list, tuple)):
                to = [to, ]
            data = {
                'from': 'SmileCook <no-reply@{}>'.format(self.domain),
                'to': to,
                'subject': subject,
                'text': text,
                'html': html
            }
            response = requests.post(url=self.base_url,
                                                      auth=('api', self.key),
                                                      data=data)
            return response
    
  5. 使用MailgunApi发送第一封邮件。从mailgun中打开MailgunApi,然后通过传递之前练习中提供的域名和 API 密钥创建一个mailgun对象:

    >>>from mailgun import MailgunApi
    >>>mailgun = MailgunApi(domain='sandbox76165a034aa940feb3ef785819641871.mailgun.org',
    api_key='441acf048aae8d85be1c41774563e001-19f318b0-739d5c30')
    
  6. 然后,使用MailgunApi中的send_mail()方法发送我们的第一封电子邮件。我们可以将emailsubjectbody作为参数传递。我们将得到 HTTP 状态码smilecook.api@gmail.com

  7. 检查注册电子邮件地址的邮箱。你应该会收到一封电子邮件。如果你找不到它,它可能在你垃圾邮件文件夹中:![图 6.6:通过 Mailgun 发送电子邮件 img/C15309_06_06.jpg

图 6.6:通过 Mailgun 发送电子邮件

因此,我们刚刚使用第三方Mailgun API 发送了第一封电子邮件。现在我们知道了如何在不设置自己的邮件服务器的情况下,将电子邮件功能添加到我们的应用程序中。稍后,我们将把这个电子邮件功能整合到我们的 Smilecook 应用程序中。我们打算在用户账户激活工作流程中使用它。

用户账户激活工作流程

我们希望在我们的食谱分享平台上添加一个账户激活步骤,这样当用户在我们的系统中注册账户时,账户将不会默认激活。此时,用户无法登录到他们的账户仪表板。只有当他们通过点击我们的激活电子邮件中的链接激活账户后,他们才能登录到他们的账户仪表板:

![图 6.7:用户账户激活工作流程img/C15309_06_07.jpg

图 6.7:用户账户激活工作流程

为了构建这个工作流程,我们将使用用户模型中的is_active属性来指示账户是否已激活(激活电子邮件的链接是否已被点击),然后创建一个在用户注册时发送验证电子邮件的方法,以及一个端点可以用来激活账户。为了创建一个唯一的链接,我们将使用itsdangerous包,这将帮助我们创建一个用于账户激活链接的唯一令牌。这个包确保我们生成的电子邮件不会被任何人修改,这样我们就可以在激活用户的账户之前验证用户的身份。

注意

如果你想了解更多关于itsdangerous包的信息,请访问pythonhosted.org/itsdangerous/

在下一个练习中,我们将生成账户激活令牌。

练习 42:生成账户激活令牌

如前所述,我们希望在 Smilecook 应用程序中实现一个用户账户激活流程。这是为了确保在注册过程中提供的电子邮件地址是有效的,并且属于用户本人。在这个练习中,我们将创建一个生成激活令牌的函数,以及另一个验证令牌的函数。然后,它们将在账户激活流程中稍后使用:

  1. 将以下代码行添加到requirements.txt文件中:

    itsdangerous==1.1.0
    
  2. 使用以下命令安装itsdangerous包:

    pip install -r requirements.txt
    

    在成功安装包之后,你应该会看到以下结果返回:

    Installing collected packages: itsdangerous
    Successfully installed itsdangerous-1.1.0
    
  3. 确保在config.py中添加了密钥;当我们稍后使用itsdangerous包时,它会很有用:

    class Config:
        SECRET_KEY = 'super-secret-key'
    
  4. utils.py中,从itsdangerous导入URLSafeTimedSerializer模块:

    from itsdangerous import URLSafeTimedSerializer
    from flask import current_app
    
  5. 再次在utils.py中定义generate_token函数:

    def generate_token(email, salt=None):
        serializer = URLSafeTimedSerializer(current_app.config.get('SECRET_KEY'))
        return serializer.dumps(email, salt=salt)
    

    generate_token方法中,我们使用URLSafeTimedSerializer类通过电子邮件和current_app.config.get('SECRET_KEY')密钥创建令牌,这是我们在config.py设置中设置的密钥。这个相同的密钥将在未来验证这个令牌时使用。此外,请注意,时间戳将包含在这个令牌中,之后我们可以验证消息创建的时间。

  6. 再次在utils.py中定义verify_token函数:

    def verify_token(token, max_age=(30 * 60), salt=None):
        serializer = URLSafeTimedSerializer(current_app.config.get('SECRET_KEY'))
        try:
            email = serializer.loads(token, max_age=max_age, salt=salt)
        except:
            return False
        return email
    

    verify_token函数将尝试从令牌中提取电子邮件地址,这将确认令牌中的有效期限是否在 30 分钟内(30 * 60 秒)通过max_age属性。

    注意

    你可以在步骤 5步骤 6中看到,这里使用salt来区分不同的令牌。例如,当通过电子邮件创建令牌时,在开户、重置密码和升级账户的场景中,会发送一封验证邮件。你可以使用salt='activate-salt'salt='reset-salt'salt='upgrade-salt'来区分这些场景。

现在我们有了这两个方便的函数来生成和验证激活令牌,在下一个练习中,我们将它们用于用户账户激活流程中。

练习 43:发送用户账户激活电子邮件

现在,我们已经从上一个练习中准备好了激活令牌,并且我们也学习了如何使用 Mailgun API 发送电子邮件。在这个练习中,我们将结合这两者,将激活令牌放入激活电子邮件中,以完成整个账户激活工作流程:

  1. url_forMailgunAPI类以及generate_tokenverify_token函数导入到resources/user.py中:

    from flask import request, url_for
    from mailgun import MailgunApi
    from utils import generate_token, verify_token
    
  2. 通过传递我们在上一个练习中获得的Mailgun域名和 API 密钥来创建一个MailgunApi对象:

    mailgun = MailgunApi(domain='sandbox76165a034aa940feb3ef785819641871.mailgun.org',
               api_key='441acf048aae8d85be1c41774563e001-19f318b0-739d5c30')
    
  3. UserListResource类中,在user.save()之后添加以下代码:

            token = generate_token(user.email, salt='activate')
            subject = 'Please confirm your registration.'
    

    我们首先使用generate_token(user.email, salt='activate')生成一个令牌。这里,salt='activate'表示令牌主要用于激活账户。电子邮件的主题设置为请确认您的注册

  4. 在同一个UserListResource类中创建一个激活链接并定义电子邮件文本:

            link = url_for('useractivateresource',
                                 token=token,
                                 _external=True)
            text = 'Hi, Thanks for using SmileCook! Please confirm your registration by clicking on the link: {}'.format(link)
    

    我们使用url_for函数创建激活链接。它将需要UserActivateResource(我们将在下一步创建)。这个端点也需要一个令牌。_external=True参数用于将默认的相对 URL /users/activate/<string:token> 转换为绝对 URL http://localhost:5000/users/activate/<string:token>

  5. 最后,我们使用mailgun.send_email方法在同一个UserListResource类中发送电子邮件:

            mailgun.send_email(to=user.email,
                                             subject=subject,
                                             text=text)
    
  6. resources/user.py下创建一个新的UserActivateResource类,并在其中定义get方法:

    class UserActivateResource(Resource):
        def get(self, token):
            email = verify_token(token, salt='activate')
            if email is False:
                return {'message': 'Invalid token or token expired'}, HTTPStatus.BAD_REQUEST
    

    首先,此方法使用verify_token(token, salt='activate')验证令牌。令牌有默认的 30 分钟过期时间。如果令牌有效且未过期,我们将获取用户电子邮件并可以继续账户激活。否则,电子邮件将被设置为False,我们可以返回错误消息Invalid token or token expired,并带有HTTP 状态码 400 Bad Request

  7. 继续在UserActivateResource.get方法上工作:

            user = User.get_by_email(email=email)
            if not user:
                return {'message': 'User not found'}, HTTPStatus.NOT_FOUND
            if user.is_active is True:
                return {'message': 'The user account is already activated'}, HTTPStatus.BAD_REQUEST
            user.is_active = True
            user.save()
    

    如果我们有用户的电子邮件,我们可以查找user对象并修改其is_active属性。如果用户账户已经激活,我们将简单地返回用户已激活。否则,我们将激活账户并保存。

  8. 最后,我们将返回 HTTP 状态码204 No Content以指示请求已成功处理:

            return {}, HTTPStatus.NO_CONTENT
    

    注意

    通常,在现实世界的场景中,电子邮件中的激活链接将指向系统的前端层。前端层会通过 API 与后端通信。因此,当前端接收到 HTTP 状态码204 No Content时,意味着账户已激活。然后它可以转发用户到账户仪表板。

  9. 然后,通过以下代码将新的UserActivateResource类添加到app.py中。首先从resources.user导入UserActivateResource类,然后添加路由:

    from resources.user import UserListResource, UserResource, MeResource, UserRecipeListResource, UserActivateResource
        api.add_resource(UserActivateResource, '/users/activate/<string:token>')
    
  10. 最后,我们想确保用户在账户激活之前不能登录到应用程序。我们将更改resources/token.py中的POST方法。在检查密码后立即返回 HTTP 状态码403 Forbidden,如果用户账户未激活:

            if user.is_active is False:
                return {'message': 'The user account is not activated yet'}, HTTPStatus.FORBIDDEN
    
  11. 右键点击以运行应用程序。然后我们准备测试整个用户注册工作流程。

恭喜!您已完成了整个用户注册工作流程的开发。我们的 Smilecook 应用程序将能够发送带有激活链接的电子邮件。用户可以点击激活链接来激活他们的用户账户。

在下一个活动中,我们希望您走完整个流程并测试它是否工作。

活动九:测试完整的用户注册和激活工作流程

在此活动中,我们将测试完整的用户注册和激活工作流程:

  1. 通过 Postman 注册新用户。

  2. 通过 API 登录。

  3. 使用发送到邮箱的链接来激活账户。

  4. 账户激活后重新登录。

    注意

    此活动的解决方案可以在第 314 页找到。

设置环境变量

我们将使用环境变量来确保我们的敏感信息,例如密钥,是安全的。这确保了当我们与他人共享代码时不会泄露这些敏感和机密信息。环境变量仅保存在本地环境中,它们不会出现在代码中。这是将代码与机密信息分离的常用最佳实践。

练习 44:在 PyCharm 中设置环境变量

环境变量是在本地系统中存储的键值对,可以被我们的应用程序访问。在这个练习中,我们将通过 PyCharm 设置环境变量:

  1. PyCharm 界面的顶部,选择 运行 然后点击 编辑配置:![图 6.8:选择运行并点击编辑配置 图片

    图 6.8:选择运行并点击编辑配置
  2. 点击 MAILGUN_DOMAINMAILGUN_API_KEY 环境变量。

    你的屏幕将如下所示:

    ![图 6.9:添加 MAILGUN_DOMAIN 和 MAILGUN_API_KEY 环境变量 图片

    图 6.9:添加 MAILGUN_DOMAIN 和 MAILGUN_API_KEY 环境变量

    注意

    对于 Python 控制台,要读取环境变量,我们可以在 Pycharm >> 首选项 >> 构建、执行、部署 >> 控制台 >> Python 控制台 下设置。

  3. 然后,我们在 resources/user.py 中导入 os 包,并使用 os.environ['MAILGUN_DOMAIN']os.environ['MAILGUN_API_KEY'] 获取环境变量中的值:

    import os
    mailgun = MailgunApi(domain=os.environ.get('MAILGUN_DOMAIN'),
                                api_key=os.environ.get('MAILGUN_API_KEY'))
    

    因此,这就是如何将秘密的 API_KEY 和其他相关信息从代码中移除。这些秘密数据现在存储在环境变量中,并且与代码隔离。

    注意

    如果我们使用 os.environ['KEY'] 获取环境变量,如果环境变量未定义,则会引发 'KeyError'。我们可以使用 os.environ.get('KEY')os.getenv('Key') 获取值。如果变量未定义,这将返回 None。如果我们想在环境变量未定义时设置一个默认值,我们可以使用这个语法:os.getenv('KEY', default_value)

HTML 格式电子邮件

我们可以通过使用 HTML 格式的电子邮件而不是纯文本电子邮件来给我们的电子邮件添加一些颜色。HTML 格式的电子邮件无处不在。我相信你一定在电子邮件中看到过图片,或者有复杂布局的电子邮件。这些都是 HTML 格式的电子邮件。理论上,要使用 Mailgun API 发送 HTML 格式的电子邮件,可能只需将 HTML 代码作为参数传递给 mailgun.send_email 方法即可。

请参考以下示例代码,了解如何使用 Mailgun 发送 HTML 格式的电子邮件。我们可以看到,我们只是在这里添加了新的 html 参数:

mailgun.send_email(to=user.email,
                         subject=subject,
                         text=text, 
                         html='<html><body><h1>Test email</h1></body></html>')

然而,将 HTML 代码与 Python 代码耦合的这种方式比较繁琐。如果我们有一个复杂的布局,HTML 代码可能会相当长,这会使得将其包含在实际的 Python 代码中变得过于复杂。为了解决这个问题,我们可以利用 Flask 中的 render_template() 函数。这是一个利用 Jinja2 模板引擎的函数。通过它,我们只需将 HTML 代码放置在应用程序项目下的 /templates 文件夹中的单独的 HTML 文件中。然后,我们可以将这个 HTML 文件(也称为模板文件)传递给这个 render_template 函数以生成 HTML 文本。

从以下示例代码中,我们可以看到,使用 render_template 函数,我们可以大大简化代码:

template/sample.html
<html><body><h1>Test email</h1></body></html>

然后,我们可以使用以下代码将主题设置为Test email来渲染 HTML:

mailgun.send_email(to=user.email,
                         subject=subject,
                         text=text, 
                         html=render_template('sample.html'))

这里提供的示例代码将在应用程序项目文件夹下查找templates/sample.html文件,并为我们渲染 HTML 代码。

这个函数被命名为render_template而不是render_html是有原因的。render_template函数不仅仅是从文件中直接输出 HTML 代码。实际上,我们可以在 HTML 模板文件中插入变量,并由render_template函数渲染它。

例如,我们可以这样修改sample.html(这里的{{content}}是一个占位符):

template/sample.html
<html><body><h1>{{content}}</h1></body></html>

然后,我们可以使用以下代码将主题设置为test email来渲染 HTML:

mailgun.send_email(to=user.email,
                         subject=subject,
                         text=text, 
                         html=render_template('sample.html', content='Test email'))

在下一个活动中,我们希望你能发送 HTML 格式的激活邮件。

活动十:创建 HTML 格式的用户账户激活邮件

我们之前已经发送过纯文本格式的邮件。在这个活动中,我们将创建一个 HTML 格式的邮件,使其对我们的用户更具吸引力:

  1. 将用户的电子邮件地址放入Mailgun授权收件人列表中。

  2. Mailgun网站上复制一个 HTML 模板。

  3. 在 HTML 模板中添加激活令牌。

  4. 使用render_template函数渲染 HTML 代码,并通过Mailgun API 发送激活邮件。

  5. 在 Postman 中注册一个新账户,并获取 HTML 格式的账户激活邮件。

    注意

    这个活动的解决方案可以在第 317 页找到。

现在,你已经学会了如何以 HTML 格式发送电子邮件。从现在起,你可以设计自己的 HTML 模板。

摘要

在本章中,我们学习了如何使用第三方Mailgun API 发送用户账户激活邮件。稍后,我们可以使用MailgunAPI类发送不同的邮件,例如通知邮件。Mailgun 不仅提供了发送邮件的 API,还为我们提供了一个后端仪表板,以便我们跟踪已发送邮件的状态。这是一个非常方便的服务。用户账户激活是确保我们正在欢迎经过验证的用户的重要步骤。尽管不是每个平台都执行这种验证,但它减少了垃圾邮件和机器人对我们平台的负面影响。在本章中,我们使用了itsdangerous包来创建一个唯一的令牌,以确认用户电子邮件地址的所有权。这个包包含时间戳,这样我们就可以验证令牌是否已过期。

在下一章中,我们将继续为我们的 Smilecook 应用程序添加更多功能。我们将在下一章中处理图片。我相信你将在那里学到很多实用的技能。让我们继续我们的旅程。

第七章:7. 处理图像

学习目标

到本章结束时,你将能够:

  • 构建用户头像功能

  • 使用 Flask-Uploads 开发图像上传 API

  • 使用 API 调整图像大小

  • 使用 Pillow 压缩图像以增强 API 性能

在本章中,我们将学习如何执行图像上传,以便我们能让用户在我们的 Smilecook 应用程序中发布个人资料图片和食谱封面图片。

简介

在上一章中,我们通过通过电子邮件激活用户账户来完成账户开通工作流程。在本章中,我们将开发一个功能,以便我们可以上传图片。这些图片是用户的个人资料图片和食谱封面图片。除了上传图像外,我们还将讨论图像压缩。Pillow 是一个图像处理包,我们将使用它将图像压缩到 90%。这可以在不牺牲图像质量的情况下大大提高我们 API 的性能。

从技术角度讲,在本章中,我们将介绍两个 Python 包,Flask-Uploads 和 Pillow。Flask-Uploads 允许我们快速开发图像上传功能。对于图像压缩,我们将使用 Pillow。它可以生成我们指定的格式的图像,并相应地进行压缩。

构建用户头像功能

在我们的 Smilecook 应用程序中,有列出用户信息的用户个人资料页面。虽然这已经足够有用,但如果我们可以允许用户将个人资料图片(头像)上传到他们的个人资料页面,那就更好了。这将使应用程序更具社交性。

为了存储用户头像,我们将在用户模型中创建一个新的属性(avatar_image)。我们不会直接在这个属性中存储图像。相反,我们将图像存储在服务器上,而新的属性将包含图像的文件名。稍后,当我们的 API 收到客户端请求请求图像时,我们将在这个属性中找到文件名,生成指向图像位置的 URL,并将其返回给前端客户端。然后前端客户端将根据图像 URL 从服务器获取它:

![图 7.1:构建用户模型头像图]

img/C15309_07_01.jpg

图 7.1:构建用户模型头像图

我们将创建一个新的端点,http://localhost:5000/users/avatar,它将接受PUT请求。我们之所以设计它来接受PUT请求,是因为每个用户应该只有一张头像图片。所以,每次有客户端请求时,它应该要么是第一次用新图像替换空图像,要么是替换旧图像为新的。这是一个替换操作。在这种情况下,我们应该使用 HTTP 动词,PUT。

现在,让我们在我们的模型中添加avatar_image属性。我们将不得不使用 Flask-Migrate 来更新底层的数据库表。

练习 45:向用户模型添加 avatar_image 属性

在这个练习中,我们将修改用户模型。首先,我们将在用户模型中创建一个额外的属性(avatar_image)。然后,我们将它在数据库模式中反映出来,并使用 Flask-Migrate Python 包在数据库表中创建相应的字段。最后,我们将使用 pgAdmin 确认更改是否成功。让我们开始吧:

  1. avatar_image 属性添加到用户模型。代码文件是 models/user .py

    avatar_image = db.Column(db.String(100), default=None)
    

    avatar_image 属性被设计用来存储上传图片的文件名。因此,它是一个长度为 100 的字符串。默认值为 None

  2. 运行以下命令以生成数据库迁移脚本:

    flask db migrate
    

    你将看到检测到一个名为 user.avatar_image 的新列:

    INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
    INFO  [alembic.runtime.migration] Will assume transactional DDL.
    INFO  [alembic.autogenerate.compare] Detected added column 'user.avatar_image'
      Generating /TrainingByPackt/Python-API-Development-Fundamentals/Lesson07/smilecook/migrations/versions/7aafe51af016_.py ... done
    
  3. 检查 /migrations/versions/7aafe51af016_.py 中的内容,这是我们之前步骤中生成的数据库迁移脚本:

    """empty message
    Revision ID: 7aafe51af016
    Revises: 983adee75c9a
    Create Date: 2019-09-18 20:54:51.823725
    """
    from alembic import op
    import sqlalchemy as sa
    # revision identifiers, used by Alembic.
    revision = '7aafe51af016'
    down_revision = '983adee75c9a'
    branch_labels = None
    depends_on = None
    def upgrade():
        # ### commands auto generated by Alembic - please adjust! ###
        op.add_column('user', sa.Column('avatar_image', sa.String(length=100), nullable=True))
        # ### end Alembic commands ###
    def downgrade():
        # ### commands auto generated by Alembic - please adjust! ###
        op.drop_column('user', 'avatar_image')
        # ### end Alembic commands ###
    

    从其内容中,我们可以看到脚本中生成了两个函数:upgradedowngradeupgrade 函数用于将新的 avatar_image 列添加到数据库表中,而 downgrade 函数用于移除 avatar_image 列,以便它可以恢复到原始状态。

  4. 运行以下 flask db upgrade 命令以更新数据库模式:

    flask db upgrade
    

    你将看到以下输出:

    INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
    INFO  [alembic.runtime.migration] Will assume transactional DDL.
    INFO  [alembic.runtime.migration] Running upgrade 983adee75c9a -> 7aafe51af016, empty message
    
  5. 在 pgAdmin 中检查 schema change。在 user 表上右键点击并选择 属性。将出现一个新窗口。然后,点击 Columns 选项卡以检查列:图 7.2:在 Columns 选项卡中检查所有列

图 7.2:在 Columns 选项卡中检查所有列

在这里,我们可以看到新的 avatar_image 列被添加到 user 表中。现在,我们的 Smilecook 应用程序已准备好接收用户头像的图像路径。

Flask-Uploads

我们将使用 Flask-Uploads 包来完成我们的图像上传功能。这是一个非常强大的包,为我们简化了大多数繁琐的编码工作。通过简单地调用包提供的几个方法,它允许我们高效灵活地开发文件上传功能。Flask-Uploads 可以直接处理各种常见的文件类型。我们需要定义的是分类上传文件类型的 Set,例如 IMAGESDOCUMENTAUDIO 等。然后,我们只需设置上传文件的目的地。

在我们实现它们之前,让我们看看 Flask-Uploads 中的几个基本概念和函数。

上传集

在我们上传任何文件之前,我们需要定义 UploadSet。上传集是一组单独的文件。以 images 为例;我们可以定义图像上传集如下,其中 'images' 是上传集的名称:

image_set = UploadSet('images', IMAGES)

一旦你有了 image_set,你可以使用 save 方法来保存从传入的 HTTP 请求上传的图片,如下所示:

    image_set.save(image, folder=folder, name=filename)

上传集的配置也需要存储在应用中。我们可以使用 Flask-Uploads 的 configure_uploads 函数来完成此操作:

configure_uploads(app, image_set)

此外,您还可以使用 patch_request_class 来限制上传文件的最大上传大小。在下一个练习中,我们将一起工作,开发图像上传功能。图像用户将上传他们的头像图片。我们将定义目标为 static/images/avatars

练习 46:实现用户头像上传功能

在这个练习中,我们将首先将 Flask-Uploads 包安装到我们的虚拟环境中。然后,我们将进行一些简单的配置,并开始开发图像上传功能。通过完成这个练习,我们将看到客户端返回的图像 URL。让我们开始吧:

  1. requirements.txt 中添加以下行:

    Flask-Uploads==0.2.1
    
  2. 在 PyCharm 控制台中运行以下命令来安装 Flask-Uploads 包:

    pip install -r requirements.txt
    

    您将看到以下安装结果:

    Installing collected packages: Flask-Uploads
    Running setup.py install for Flask-Uploads ... done
    Successfully installed Flask-Uploads-0.2.1
    
  3. UploadSetIMAGES 导入到 extensions.py 中:

    from flask_uploads import UploadSet, IMAGES
    
  4. 在相同的 extensions.py 文件中,定义一个名为 'images' 的集合和一个名为 IMAGES 的扩展。这将涵盖常见的图像文件扩展名(.jpg.jpeg.png 等):

    image_set = UploadSet('images', IMAGES)
    
  5. Config.py 中设置图像目标:

    UPLOADED_IMAGES_DEST = 'static/images'
    

    注意

    UPLOADED_IMAGES_DEST 属性名称由上传集的名称决定。由于我们将上传集名称设置为 'images',因此这里的属性名称必须是 UPLOADED_IMAGES_DEST

  6. configure_uploadspatch_request_classimage_set 导入到 app.py 中:

    from flask_uploads import configure_uploads, patch_request_class
    from extensions import db, jwt, image_set
    
  7. 使用我们刚刚导入的 configure_uploads 函数,传入我们想要上传的 image_set

    configure_uploads(app, image_set)
    
  8. 使用 patch_request_class 将允许上传的最大文件大小设置为 10 MB。这一步很重要,因为默认情况下,没有上传大小限制:

    patch_request_class(app, 10 * 1024 * 1024)
    
  9. schemas/user.py 中导入 url_for 函数,并在 UserSchema 类下添加 avatar_url 属性和 dump_avatar_url 方法:

    from flask import url_for
    class UserSchema(Schema):
        avatar_url = fields.Method(serialize='dump_avatar_url')
        def dump_avatar_url(self, user):
            if user.avatar_image:
                return url_for('static', filename='images/avatars/{}'.format(user.avatar_image), _external=True)
            else:
                return url_for('static', filename='images/assets/default-avatar.jpg', _external=True)
    

    使用 url_for 函数来帮助生成图像文件的 URL。使用 dump_avatar_url 方法来返回序列化后的用户头像的 URL。如果没有上传图像,我们将直接返回默认头像的 URL。

  10. static/images 下创建一个名为 assets 的文件夹,并将 default-avatar.jpg 图像放入其中。这张图像将成为我们的默认用户头像:图 7.3:添加图像后的文件夹结构

    图 7.3:添加图像后的文件夹结构

    注意

    您可以在这里放置任何喜欢的图像。我们还在我们的示例代码文件夹中提供了一个默认头像图像。

  11. uuid 扩展和 image_set 导入到 utils.py 中。您将在下面看到这些模块/方法的使用:

    import uuid
    from flask_uploads import extension
    from extensions import image_set
    
  12. save_image 函数添加到 utils.py 中:

    def save_image(image, folder):
        filename = '{}.{}'.format(uuid.uuid4(), extension(image.filename))
        image_set.save(image, folder=folder, name=filename)
        return filename
    

    save_image 方法中,我们使用了 uuid 函数来生成上传图片的文件名。我们使用 Flask-Uploads 的扩展函数从上传的图片中获取文件扩展名。然后,我们使用 image_set.save 函数保存图片;保存目的地是 static/images。如果我们传递 folder='avatar' 作为参数,目的地将是 static/images/avatar

  13. utils 中导入 image_setsave_image 函数到 resources/user.py

    from extensions import image_set
    from utils import generate_token, verify_token, save_image
    
  14. user_avatar_schema 添加到 resources/user.py。此模式只是为了显示 avatar_url

    user_avatar_schema = UserSchema(only=('avatar_url', ))
    
  15. resources/user.py 中创建 UserAvatarUploadResource 类,并在其中定义 put 方法:

    class UserAvatarUploadResource(Resource):
        @jwt_required
        def put(self):
            file = request.files.get('avatar')
            if not file:
                return {'message': 'Not a valid image'}, HTTPStatus.BAD_REQUEST
            if not image_set.file_allowed(file, file.filename):
                return {'message': 'File type not allowed'}, HTTPStatus.BAD_REQUEST
            user = User.get_by_id(id=get_jwt_identity())
            if user.avatar_image:
                avatar_path = image_set.path(folder='avatars', filename=user.avatar_image)
                if os.path.exists(avatar_path):
                    os.remove(avatar_path)
    

    put 方法之前的 @jwt_required 装饰器意味着在触发此方法之前需要登录。在 put 方法中,我们从 request.files 获取头像图片文件。然后,我们验证了图片文件是否存在以及文件扩展名是否允许。如果一切正常,我们将获取用户对象并检查是否已存在头像。如果存在,我们将将其替换为上传的图片之前将其删除。

  16. 然后,我们使用 save_image 来保存上传的图片。一旦图片保存,我们将获取图片的文件名并将其保存到 user.avatar_image。然后,我们使用 user.save() 将更新保存到数据库中:

            filename = save_image(image=file, folder='avatars')
            user.avatar_image = filename
            user.save()
    
  17. 使用 user_avatar_schema.dump(user).data 返回图片 URL 和 HTTP 状态码,200 OK

            return user_avatar_schema.dump(user).data, HTTPStatus.OK
    
  18. UserAvatarUploadResource 类导入到 app.py

    from resources.user import UserListResource, UserResource, MeResource, UserRecipeListResource, UserActivateResource, UserAvatarUploadResource
    
  19. 将资源链接到路由,即在 app.py 中的 /users/avatar

    api.add_resource(UserAvatarUploadResource, '/users/avatar')
    

我们已经在 Smilecook 应用程序中成功创建了用户头像图片上传功能。现在,我们可以在用户个人资料页面上传图片。在下一个练习中,我们将使用 Postman 进行测试。

练习 47:使用 Postman 测试用户头像上传功能

在上一个练习中,我们完成了头像上传功能的开发。为了确保一切按预期工作,我们需要从客户端测试该功能。我们将使用 Postman 发送包含用户头像图片的客户端请求。让我们开始吧:

  1. 首先,登录到一个用户账户。现在,点击 收藏夹 选项卡并选择 POST Token 请求。然后,点击 发送 按钮。结果可以在下面的屏幕截图中看到:图 7.4:发送 POST Token 请求

    ](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-api-dev-fund/img/C15309_07_04.jpg)

    图 7.4:发送 POST Token 请求
  2. 接下来,我们将使用 PUT 方法上传头像。向以下 URL 发送 HTTP PUT 请求:http://localhost:5000/users/avatar。点击 用户 文件夹,然后创建一个新的请求。

  3. UserAvatarUpload 设置保存到 用户 文件夹中。

  4. 选择 PUT 作为 HTTP 方法,并在请求 URL 中输入 http://locaohost:5000/users/avatar

  5. 现在,将 Authorization 中的 Bearer {token} 选择到 字段中,其中 token 是我们在上一步中获得的访问令牌。

  6. 选择主体选项卡。然后,选择表单数据单选按钮,并将“头像”作为

  7. 旁边的下拉菜单中选择文件,然后选择要上传的图像文件。

  8. 现在,点击响应中的avatar_url,这意味着我们的图像上传请求已成功。

  9. 点击avatar_url应该会带您到上传的图像。检查路径,static/images/avatars,在 PyCharm 中。您应该在那里看到上传的图像:![图 7.6:检查上传的图像 图片

    图 7.6:检查上传的图像
  10. 通过username获取用户请求。点击集合选项卡并选择GET 用户请求。

  11. 在 URL 字段中输入http://localhost:5000/users/john。您可以将用户名,即John,替换为任何合适的用户名,然后点击发送按钮。结果可以在以下屏幕截图中看到:

![图 7.7:检查用户头像 URL图片

图 7.7:检查用户头像 URL

这里,我们可以看到用户中的新头像 URL 属性。

这个测试练习证明图像上传功能按预期工作。我们还可以通过在端点 URL 中放置用户名来查找用户。

注意

您可以测试头像图像上传功能的两个方面。第一个是上传大于 10 MB 大小的图像。第二个是测试是否将默认头像图像(即default-avatar.jpg)用于没有上传头像的用户账户。

图像调整大小和压缩

图像的大小会影响网站的速度。想象一下看一个 10 MB 大小的图片。如果一页上有 10 张图片,这个网站的大小将是 100 MB,因此加载一个页面将花费很多时间。因此,减少图像大小并压缩它,使其大小约为 500 KB 是一个好的做法。

此外,我们还将图像转换为 JPEG 格式(它具有.JPG文件扩展名)。JPEG 是一种图像压缩技术,可以去除图像中不明显、不重要的细节,从而实现更小的文件大小。此外,对于网络使用,通常认为可以接受较低的图像质量。

在我们的 Smilecook 应用程序中,我们将把所有上传的图像转换为 JPG 格式并压缩它们。我们将通过 Pillow 包来完成这项工作。

注意

JPEG 格式中不能有透明图像。如果我们保存一个去除背景的 JPEG 图像,背景将变成白色,而不是透明的。其他两种常用的图像格式,PNG 和 GIF。这两种图像格式将支持图像的透明度。

然而,在我们的 Smilecook 应用程序中,我们不会显示透明图像,因此使用 JPG 图像在这里就足够了。

Pillow 简介

Pillow,之前被称为Image。我们可以使用Image.open从图像文件创建一个对象。然后,我们可以通过使用属性size来获取图像的像素尺寸。我们还可以通过使用属性mode来找出图像的颜色模式。

你应该会看到一些常见的颜色模式,包括L代表黑白,RGB代表红绿蓝,以及CMYK代表青色-品红色-黄色-黑色:

>>>image = Image.open('default-avatar.jpg')
>>>image.size
(1600, 1066)
>>>image.mode
'RGB'

如果我们要将图片的颜色模式更改为 RGB,可以使用convert函数。我们通常更改颜色模式以确保图像的颜色准确性。RGB 是计算机显示器上最常用的颜色模式:

>>>image = image.convert("RGB")

如果我们想要调整图像大小以使其具有更小的尺寸,我们应该使用thumbnail方法。此方法可以保持图像的宽高比,同时确保图像的每一边都小于我们定义的限制。

例如,结果图像的边长将小于1600像素,同时保持宽高比不变:

>>>maxsize = (1600, 1600)
>>>image.thumbnail(maxsize)

当我们使用 Pillow 包保存更改时,我们可以传递一个quality参数。这样做是为了指定我们想要的 JPEG 压缩程度。质量值范围从 1 到 100,1 是最差的,95 是最佳的。我们应该避免使用高于 95 的值,因为这几乎意味着没有压缩。默认质量值是 75:

>>>image.save('compressed_image.jpg', optimize=True, quality=85)

让我们完成一个练习来实现图像压缩。

练习 48:在我们的 Smilecook 应用程序中实现图像压缩

现在我们已经了解了理论和我们可以使用的工具来执行图像压缩,让我们将其应用到我们的 Smilecook 应用程序中。我们希望压缩用户的头像。我们将使用 Pillow 包来完成这项工作。让我们开始吧:

  1. Pillow包添加到requirements.txt中:

    Pillow==6.2.1
    
  2. 通过运行pip install命令安装Pillow包,如下所示:

    pip install -r requirements.txt
    

    运行前面的命令后,你应该看到以下安装结果:

    Installing collected packages: Pillow
    Successfully installed Pillow-6.2.1
    
  3. 将必要的包和模块导入到utils.py中:

    import os
    from PIL import Image
    
  4. utils.py中定义compress_image函数,该函数接受文件名和folder作为参数。

    首先,我们将使用image_set.path(filename=filename, folder=folder)来获取实际图像文件的路径。然后,通过使用Image.open(file_path),我们将从图像文件创建image对象:

    def compress_image(filename, folder):
        file_path = image_set.path(filename=filename, folder=folder)
        image = Image.open(file_path)
    
  5. 将颜色模式更改为RGB,并调整大小以确保每一边都不超过1600像素:

        if image.mode != "RGB":
            image = image.convert("RGB")
        if max(image.width, image.height) > 1600:
            maxsize = (1600, 1600)
            image.thumbnail(maxsize, Image.ANTIALIAS)
    
  6. 为我们的压缩图像生成新的文件名和路径:

        compressed_filename = '{}.jpg'.format(uuid.uuid4())
        compressed_file_path = image_set.path(filename=compressed_filename, folder=folder)
    
  7. 使用quality = 85保存压缩后的图像:

        image.save(compressed_file_path, optimize=True, quality=85)
    
  8. 使用os.stat(file_path)来获取字节数。通过这样做,我们将在测试中有一个原始大小和压缩后的比较:

        original_size = os.stat(file_path).st_size
      compressed_size = os.stat(compressed_file_path).st_size
        percentage = round((original_size - compressed_size) / original_size * 100)
        print("The file size is reduced by {}%, from {} to {}.".format(percentage, original_size, compressed_size))
    

    注意

    os.stat方法是一个 Python 方法,它返回基本文件夹/文件信息(例如,所有者 ID,组所有者 ID 和文件大小)。

  9. 删除原始图像,然后使用以下代码返回压缩后的图像文件名:

        os.remove(file_path)
        return compressed_filename
    
  10. 最后,在 utils.py 中的 save_image 函数中,在图像保存后立即调用 compress_image 函数:

    def save_image(image, folder):
        filename = '{}.{}'.format(uuid.uuid4(), extension(image.filename))
        image_set.save(image, folder=folder, name=filename)
        filename = compress_image(filename=filename, folder=folder)
        return filename
    

在这里,我们已经创建了我们的 compress_image 函数。该函数只需要知道图像文件的位置,它就会为我们压缩图像。

在下一个练习中,我们将测试图像压缩功能。

练习 49:测试图像压缩功能

到目前为止,我们已经开发了一个可以压缩用户上传的头像的图像压缩功能。在这个练习中,我们将测试并查看图像压缩功能的表现。让我们开始吧:

  1. 首先,我们将使用 PUT 方法上传头像。我们将发送一个 HTTP 请求到 http://localhost:5000/users/avatar。点击 PUT UserAvatarUpload 并选择 Body 选项卡。

  2. 选择一个大的图片文件进行上传,然后点击 Send 按钮。结果可以在下面的屏幕截图中查看:![图 7.8:使用 PUT 方法上传头像

    ![图片 15309_07_08.jpg]

    图 7.8:使用 PUT 方法上传头像
  3. 从 PyCharm 的应用程序日志中,我们可以看到上传的图像原始大小为 7.6 MB;压缩后减少到 618 KB:

![图 7.9:压缩后的图片大小

![图片 15309_07_09.jpg]

图 7.9:压缩后的图片大小

通过这样做,我们可以看到我们之前实现的图像压缩功能是有效的。现在,图像大小已经显著减少。在下一个活动中,我们将实现食谱封面图像上传功能。

活动 11:实现食谱封面图像上传功能

到目前为止,我们已经学习了如何开发图像上传和压缩功能。在这个活动中,我们将为 Smilecook 应用程序开发食谱封面图像上传功能。我们希望通过提供封面图像来使我们的食谱更具吸引力。与用户头像类似,每个食谱只允许有一个封面图像。按照以下步骤完成此活动:

  1. models/recipe.py 中将 cover_image 属性添加到用户模型中。

  2. 使用 flask db migrate 命令更新相应的 DB 架构。

  3. 创建 recipe_cover_schema 以在 HTTP 响应中显示 cover_url

  4. 为食谱封面图像上传功能创建 RecipeCoverUploadResource

    注意

    此活动的解决方案可以在第 323 页找到。

活动 12:测试图像上传功能

在这个活动中,我们将测试食谱封面图像上传功能。首先,我们将创建一个新的食谱,上传一个食谱封面图像,并通过获取食谱来验证它是否已上传。按照以下步骤完成此活动:

  1. 使用 Postman 登录到 Smilecook 用户账户。

  2. 向我们的 API 发送客户端请求以创建一个食谱。

  3. 上传食谱图像。

  4. 在 PyCharm 中检查图像是否已压缩。

  5. 检查上传的图像在 static/images/recipes 中。

  6. 恢复食谱并确认 cover_url 属性已填充。

    注意

    本活动的解决方案可在第 328 页找到。

摘要

在本章中,我们学习了如何使用 Flask-Uploads 上传用户头像和食谱封面图像。由于我们的最大上传图像大小为 10 MB,这使得用户可以上传大图像,从而降低网站的性能。为了解决这个性能问题,我们引入了图像缩放和压缩的概念。从这里,我们开始使用 Pillow 包开发该功能。

除了学习关于图像处理的新技术外,我们还回顾了之前章节中学到的内容,例如使用 Flask-Migrate 更新数据库模式,以及在反序列化过程中使用 marshmallow 的 schema 显示上传图像的 URL。

我们已经完成了 Smilecook 食谱分享平台的大部分关键功能。在下一章中,我们将开发食谱搜索和分页功能。

第八章:8. 分页、搜索和排序

学习目标

到本章结束时,你将能够:

  • 使用 Flask-SQLAlchemy 实现分页功能

  • 使用 marshmallow 序列化分页结果以供前端显示

  • 使用搜索功能构建 API

  • 以你自己的方式对返回的记录进行排序和排序

  • 使用 Postman 测试所有这些功能

本章涵盖了分页以及如何更改食谱列表的顺序,以及如何为食谱和成分添加搜索功能。

简介

在上一章中,我们实现了用户头像食谱封面图像上传功能。我们处理了图像压缩功能以提高图像加载速度的性能。一旦上传了图像,用户可以通过 API 检索图像的 URL。

在本章中,我们将处理分页食谱数据。我们将解释为什么我们需要执行分页。这是优化我们的 API 的重要步骤。我们还将讨论一些更重要的功能,包括搜索和排序,我相信你在其他在线应用中已经遇到过。

分页

在测试环境中,我们可能只有几个开发者将食谱放在 Smilecook 平台上。那里只有少数几个食谱,性能从未成为问题。然而,在生产环境中,即平台向公众开放后,可能会有成千上万的用户在平台上分享食谱。如果你考虑像 Facebook 这样的社交媒体平台,那么数量将更大。

因此,我们需要引入分页。分页意味着我们不是从数据库中查询所有记录的总体,我们只查询其中的一小部分。当用户想要查看更多内容时,他们总是可以转到下一页。例如,当你浏览购物网站时,通常你一次会查看一页的商品。每一页可能显示 40 个商品,你必须导航到后续页面来查看所有可用的商品。这就是分页的本质。

每页显示的记录数受页面大小的限制。这样,服务器加载时间和数据传输时间将大大节省,最重要的是,这将增强用户的导航体验。

这里好的一点是,我们正在使用 Web 框架来构建我们的 API。这类常见功能已经被考虑到了。我们只需要使用 Flask-SQLAlchemy 来帮助我们构建分页 API。

分页 API

分页 API 意味着当你查询 API 时,只会返回当前页面的数据记录。它还包括其他信息,例如记录总数、总页数、其他页面的链接等。以下是一个分页 API 的示例响应。它是一个序列化的分页对象,因此它是以 JSON 格式表示的:

{
    "links": {
        "first": "http://localhost:5000/recipes?per_page=2&page=1",
        "last": "http://localhost:5000/recipes?per_page=2&page=5",
        "prev": "http://localhost:5000/recipes?per_page=2&page=1",
        "next": "http://localhost:5000/recipes?per_page=2&page=3"
    },
    "page": 2,
    "pages": 5,
    "per_page": 2,
    "total": 9,
    "data": [
        {
            "data": "data"
        },
        {
            "data": "data"
        }
    ]
}

在这里,你可以看到 HTTP 响应中的以下属性:

  • first: 首页的链接

  • last: 最后一页的链接

  • prev: 上一页的链接

  • next: 下一页的链接

  • page: 当前页

  • pages: 总页数

  • per_page: 每页的记录数

  • total: 总记录数

  • data: 此页上的实际数据记录

这些属性由 Flask-SQLAlchemy 中的分页对象自动生成。我们只需要使用 marshmallow 序列化分页对象,这样我们就可以以 JSON 格式将结果返回给前端客户端。

练习 50:在已发布菜谱检索函数中实现分页

既然我们已经讨论了分页的重要性,我们想在 Smilecook 平台上添加这个功能。我们将从这个练习开始着手。让我们开始吧:

  1. schema 文件夹中创建 pagination.py 并导入必要的模块和函数:

    from flask import request
    from marshmallow import Schema, fields
    from urllib.parse import urlencode
    
  2. 创建 PaginationSchema 类:

    class PaginationSchema(Schema):
        class Meta:
            ordered = True
        links = fields.Method(serialize='get_pagination_links')
        page = fields.Integer(dump_only=True)
        pages = fields.Integer(dump_only=True)
        per_page = fields.Integer(dump_only=True)
        total = fields.Integer(dump_only=True)
    

    在这一步中,我们可以看到 PaginationSchema 继承自 marshmallow.SchemaPaginationSchema 用于序列化 Flask-SQLAlchemy 中的分页对象。links 属性是一个自定义字段,这意味着我们可以指定如何序列化它。get_pagination_links 函数将在 步骤 4 中创建。

    注意

    我们已经在这里解释了其他属性。这些属性在 HTTP 响应中是必需的,因此我们需要将它们添加到模式中。

    最终 JSON 响应中可以有不同的键名。例如,如果我们想将 total_count 作为键名而不是 total,我们可以使用 attribute 参数,如下所示:total_count = fields.Integer(dump_only=True, attribute='total')

  3. 将以下 get_url 方法添加到 PaginationSchema

        @staticmethod
        def get_url(page):
            query_args = request.args.to_dict()
            query_args['page'] = page
            return '{}?{}'.format(request.base_url, urlencode(query_args))
    

    PaginationSchema.get_url 方法用于根据页码生成页面 URL。它接收页码参数并将其添加到 request 参数的字典中。最后,它对新的 URL 进行编码并返回,包括页码,作为参数。

    注意

    例如,如果 request.base_urlhttp://localhost:5000/recipes,并且 urlencode (query_args) 给我们 per_page=2&page=1。格式化函数将它们拼接在一起并返回新的 URL,即 http://localhost:5000/recipes?per_page=2&page=1

  4. get_pagination_links 方法添加到 PaginationSchema

        def get_pagination_links(self, paginated_objects):
            pagination_links = {
                'first': self.get_url(page=1),
                'last': self.get_url(page=paginated_objects.pages)
            }
            if paginated_objects.has_prev:
                pagination_links['prev'] = self.get_url(page=paginated_objects.prev_num)
            if paginated_objects.has_next:
                pagination_links['next'] = self.get_url(page=paginated_objects.next_num)
            return pagination_links
    

    PaginationSchema.get_pagination_links 方法用于生成指向不同页面的 URL 链接。它从 paginated_objects 获取页面信息,并依赖于我们在 步骤 3 中构建的 get_url 方法来生成链接。

  5. 接下来,在 schemas/recipe.py 中导入 PaginationSchema

    from schemas.pagination import PaginationSchema
    
  6. 删除 schemas/recipe.py 中的以下代码:

        @post_dump(pass_many=True)
        def wrap(self, data, many, **kwargs):
            if many:
                return {'data': data}
            return data
    

    这部分代码已被删除,因为我们正在构建分页函数。我们不再需要用 data 键包装多个数据记录。

  7. 定义 RecipePaginationSchema,它从 schema/pagination.py 中的 PaginationSchema 继承:

    class RecipePaginationSchema(PaginationSchema):
        data = fields.Nested(RecipeSchema, attribute='items', many=True)
    

    如您所回忆的那样,最终 JSON 响应中的属性名在这里将是data,因为这是在RecipePaginationSchema中定义的。attribute = 'items'意味着它从items获取源数据到pagination对象的属性。

  8. 现在,将acsdescsqlalchemy导入到model/recipe.py中,并修改get_all_published方法:

    from sqlalchemy import asc, desc
        @classmethod
        def get_all_published(cls, page, per_page):
            return cls.query.filter_by(is_publish=True).order_by(desc(cls.created_at)).paginate(page=page, per_page=per_page)
    

    我们在这里构建的get_all_published方法用于利用 Flask-SQLAlchemy 的paginate方法。我们将过滤和排序记录,然后paginate方法接收pageper_page参数并生成一个分页对象。

  9. fieldsuse_kwargsRecipePaginationSchema导入到resources/recipe.py中:

    from webargs import fields
    from webargs.flaskparser import use_kwargs
    from schemas.recipe import RecipeSchema, RecipePaginationSchema
    
  10. resources/recipe.py中声明recipe_pagination_schema属性,以便序列化分页的菜谱:

    recipe_pagination_schema = RecipePaginationSchema()
    
  11. 修改resources/recipe.py中的RecipeListResource.get方法,以便返回分页的菜谱:

    class RecipeListResource(Resource):
            @use_kwargs({'page': fields.Int(missing=1),
                               'per_page': fields.Int(missing=20)})
        def get(self, page, per_page):
            paginated_recipes = Recipe.get_all_published(page, per_page)
            return recipe_pagination_schema.dump(paginated_recipes).data, HTTPStatus.OK
    

    在这里,我们已将@user_kwargs装饰器添加到RecipeListResource.get方法中。page参数的默认值是 1,而per_page参数的默认值是 20。这意味着如果没有传入任何内容,我们将获取第一页,包含前 20 个菜谱记录。

然后,我们将这两个参数传递给get_all_published方法以获取分页对象。最后,分页的菜谱将被序列化并返回给前端客户端。

在这里,我们已经成功实现了分页功能并显示了结果。在下一个练习中,我们将测试分页功能。

练习 51:测试分页功能

在这个练习中,我们将测试我们刚刚构建的分页功能。我们将在我们的 Smilecook 应用程序中创建八个菜谱,并将它们全部发布。然后,我们将模拟一个用户场景,我们将逐页获取所有菜谱。让我们开始吧:

  1. 点击集合标签。

  2. 然后,选择POST Token请求并发送请求。这是为了登录用户账户。结果如下截图所示:![图 8.1:发送 POST Token 请求

    ![img/C15309_08_01.jpg]

    图 8.1:发送 POST Token 请求
  3. 通过在 PyCham 控制台中运行以下httpie命令创建八个菜谱。将{token}占位符替换为我们第二步中获得的访问令牌:

    http POST localhost:5000/recipes "Authorization: Bearer {token}" name="Vegetable Paella" description="This is a lovely vegetable paella" num_of_servings=5 cook_time=60 directions="This is how you make it"
    http POST localhost:5000/recipes "Authorization: Bearer {token}" name="Minestrone Soup" description="This is a lovely minestrone soup" num_of_servings=4 cook_time=60 directions="This is how you make it"
    http POST localhost:5000/recipes "Authorization: Bearer {token}" name="Thai Red Curry" description="This is a lovely thai red curry" 
    num_of_servings=4 cook_time=40 directions="This is how you make it"
    http POST localhost:5000/recipes "Authorization: Bearer {token}" name="Coconut Fried Rice" description="This is a lovely coconut fried rice" num_of_servings=2 cook_time=30 directions="This is how you make it"
    http POST localhost:5000/recipes "Authorization: Bearer {token}" name="Vegetable Fried Rice" description="This is a lovely vegetable fried rice" num_of_servings=2 cook_time=30 directions="This is how you make it"
    http POST localhost:5000/recipes "Authorization: Bearer {token}" name="Burrito Bowls" description="This is a lovely coconut fried rice" num_of_servings=5 cook_time=60 directions="This is how you make it"
    http POST localhost:5000/recipes "Authorization: Bearer {token}" name="Fresh Huevos Rancheros" description="This is a lovely fresh huevos rancheros" num_of_servings=4 cook_time=40 directions="This is how you make it"
    http POST localhost:5000/recipes "Authorization: Bearer {token}" name="Bean Enchiladas" description="This is a lovely coconut fried rice" num_of_servings=4 cook_time=60 directions="This is how you make it"
    

    注意

    您也可以使用 Postman 逐个创建菜谱。我们在这里使用httpie命令是因为它更快。

  4. 使用以下httpie命令发布所有八个菜谱。将{token}占位符替换为访问令牌。确保 URL 中的菜谱 ID 指的是我们在上一步中创建的菜谱:

    http PUT localhost:5000/recipes/6/publish "Authorization: Bearer {token}"
    http PUT localhost:5000/recipes/7/publish "Authorization: Bearer {token}"
    http PUT localhost:5000/recipes/8/publish "Authorization: Bearer {token}"
    http PUT localhost:5000/recipes/9/publish "Authorization: Bearer {token}"
    http PUT localhost:5000/recipes/10/publish "Authorization: Bearer {token}"
    http PUT localhost:5000/recipes/11/publish "Authorization: Bearer {token}"
    http PUT localhost:5000/recipes/12/publish "Authorization: Bearer {token}"
    http PUT localhost:5000/recipes/13/publish "Authorization: Bearer {token}"
    

    现在,我们已经创建并发布了八个菜谱。接下来,我们将以每页两个菜谱的大小逐页获取菜谱。

  5. 点击per_page2)到firstlastnext页面。我们在这里看不到prev,因为我们是在第一页。总共有五页,每页有两条记录。你还可以在 HTTP 响应中看到排序后的食谱详情。

  6. 接下来,让我们测试食谱中的链接是否正常工作。我们只需点击next URL 链接,这将在 Postman 中打开一个新标签页,并填充请求 URL(http://localhost:5000/recipes?per_page=2&page=2)。然后,我们只需点击发送来发送请求。结果如下截图所示:![图 8.3:测试食谱中的链接 图片

图 8.3:测试食谱中的链接

在这里,我们可以看到有指向firstlastnextprev页面的链接。我们还可以看到我们目前在第 2 页。所有的食谱数据都在这里。

我们已经成功创建了分页函数。现在,我将把它交给你来测试。

分页的好处是你可以将成千上万的记录分页。数据是按页检索的,这将减少服务器的负载。但是,如果用户设置的页面大小为,比如说,100,000 呢?我们如何防止用户利用系统漏洞?我们可以做的是传递分页的max_per_page参数。这将限制用户可以设置的页面大小的最大值。如果用户设置的页面大小超过最大页面大小,则将使用最大页面大小。

活动十三:在用户特定食谱检索 API 上实现分页

在上一个练习中,我们实现了并测试了所有已发布食谱检索 API 的分页函数。在这个活动中,我们将处理用户特定食谱检索 API 的分页函数。相应的 API 可以在UserRecipeListResource中找到,它用于获取特定作者的食谱。按照以下步骤完成此活动:

  1. 修改model/recipe.py中的get_all_by_user方法。

  2. RecipePaginationSchema导入到resources/user.py中。

  3. resources/user.py中声明recipe_pagination_schema属性。

  4. 修改resources/user.py中的UserRecipeListResource.get方法。

  5. UserRecipeListResource.get添加@user_kwargs装饰器。它包含一些参数,包括pageper_pagevisibility

    注意

    本活动的解决方案可在第 332 页找到。

现在,你应该已经完成了用户食谱的分页函数。让我们遵循同样的程序,在下一个活动中测试这个函数。

活动十四:测试用户特定食谱检索 API 的分页

在这个活动中,我们将测试我们刚刚构建的用户食谱分页功能。我们在之前的练习中发布了八个食谱。我们将使用它们作为我们的测试对象。我们将创建一个 Postman 请求并测试我们是否可以逐页获取它们。按照以下步骤完成此活动:

  1. 使用 Postman,按页获取上一练习中作者的所有食谱,每页两个。

  2. 点击 links 中的下一个 URL 来查询下两条记录。

    注意

    此活动的解决方案可以在第 334 页找到。

食谱搜索

在之前的练习中,我们实现了 pagination 函数,并看到了使用它的好处。这可以大大减少一次性返回给用户的所有食谱的数量。从用户的角度来看,他们可以浏览不同的页面来查找他们想要的食谱。

用户查找食谱的更好方式是通过搜索。搜索功能是互联网上的一个基本功能。看看搜索巨头 Google;他们的搜索引擎带来了巨大的收入。当然,我们不会在我们的 Smilecook 应用程序中实现像 Google 那样规模的东西。我们在这里将只进行简单的文本匹配搜索。

在下一个练习中,我们将在我们的 Smilecook 平台上实现搜索功能。我们将构建一个允许客户端提供 q 参数以通过名称或食谱描述搜索特定食谱的食谱搜索 API。这可以通过使用 LIKE 比较运算符来完成。LIKE 运算符通过将搜索字符串与目标字符串匹配来工作。我们可以在搜索字符串中使用 % 作为通配符。如果这里不是精确匹配,那么它更像是 SIMILAR TO 匹配。所以,%Chicken% 搜索字符串将与 Hainanese Chicken Rice 字符串匹配。

可能更好的比较运算符选择是 ILIKELIKE 是大小写敏感的,而 ILIKE 是大小写不敏感的。例如,我们无法使用 LIKE 运算符匹配 Thai Red Curry%curry%。你可以看到这里的 C 是大写的。然而,如果我们使用 ILIKE,它将完美匹配。

查看以下表格,了解比较运算符是如何工作的:

图 8.4:比较运算符

图 8.4:比较运算符

图 8.4:比较运算符

在我们的 Smilecook 平台上,我们不希望我们的搜索那么严格。搜索应该是大小写不敏感的。现在,让我们看看我们如何将这个功能添加到我们的 Smilecook 平台上。

练习 52:实现搜索功能

在了解了食谱搜索概念后,我们想在我们的 Smilecook 平台上实现这个功能。为此,我们将添加一个 q 参数,该参数将搜索字符串传递到 API 中。然后,我们将使用搜索字符串来查找我们需要的食谱。让我们开始吧:

  1. or_sqlalchemy 导入到 models/recipe.py

    from sqlalchemy import asc, desc, or_
    
  2. 修改 models/recipe.py 中的 Recipe.get_all_published 方法,使其获取所有满足搜索条件的已发布食谱:

      @classmethod
        def get_all_published(cls, q, page, per_page):
            keyword = '%{keyword}%'.format(keyword=q)
            return cls.query.filter(or_(cls.name.ilike(keyword),
                    cls.description.ilike(keyword)),
                    cls.is_publish.is_(True)).\
                    order_by(desc(cls.created_at)).paginate(page=page, per_page=per_page)
    

    上述代码用于将搜索模式分配给变量 keyword。然后,它通过这个关键字搜索 namedescription 字段。

  3. 修改 resources/recipe.py 中的 RecipeListResource

    class RecipeListResource(Resource):
        @use_kwargs({'q': fields.Str(missing='),
                                       'page': fields.Int(missing=1),
                                       'per_page': fields.Int(missing=20)})
        def get(self, q, page, per_page):
            paginated_recipes = Recipe.get_all_published(q, page, per_page)
            return recipe_pagination_schema.dump(paginated_recipes).data, HTTPStatus.OK
    

    我们在 user_kwargs 装饰器和 get 函数中添加了 q 参数。这个 q 参数的默认值是一个空字符串。这个 q 参数也将传递给 get_all_published 函数。

现在我们已经完成了搜索功能。接下来,我们将测试这个功能。

练习 53:测试搜索功能

在这个练习中,我们将测试我们刚刚构建的搜索功能。我们将通过搜索包含 fried rice 字符串的食谱来测试。让我们开始吧:

  1. 点击 RecipeList 请求并选择 参数 选项卡。

  2. 插入第一个键值对 (q, fried rice)。

  3. 插入第二个键值对 (per_page, 2)。

  4. 发送请求。结果如下截图所示:图 8.5:搜索包含“fried rice”字符串的食谱

    图 8.5:搜索包含“fried rice”字符串的食谱

    在这里,我们可以看到四个炒饭食谱记录,分为两页。

  5. 接下来,测试食谱中的链接是否仍然正常工作。我们只需点击下一个 URL 链接,这将在 Postman 中打开一个新标签页,并填充请求 URL (http://localhost:5000/recipes?q=fried+rice&per_page=2&page=2)。然后,我们只需点击 发送 来发送请求。结果如下截图所示:图 8.6:测试食谱中的链接是否工作

图 8.6:测试食谱中的链接是否工作

从结果中,我们可以看到我们现在在第 2 页。食谱记录也按创建时间排序。最新的食谱位于顶部。

到目前为止,我们已经创建了分页和搜索功能。这是一个巨大的成就,但我们还没有完成。我们需要继续增强我们的 Smilecook 应用程序。无需多言,让我们继续前进。

排序和排序

排序是另一个重要的功能,有助于用户导航。再次强调,当我们构建任何应用程序时,我们需要考虑用户体验。我们的应用程序最终可能存储数百万个食谱,因此我们需要提供一个简单的方法,让我们的用户能够浏览食谱并找到他们想要的食谱。

之前,我们发送回的食谱默认按时间排序。让我们在我们的 Smilecook 应用程序中实现一些其他的排序标准。我们仍然可以保留默认的排序标准,如时间,但我们希望允许用户定义他们想要的搜索标准;例如,他们可以指定他们想要按烹饪时间排序的食谱。这是一个可能性,因为用户可能想要快速烹饪,这意味着他们只对烹饪时间短的食谱感兴趣。

对于我们的 Smilecook 应用程序,排序和排序可以通过添加 sortorder 参数来完成。我们可以将排序标准(例如,created_atcook_timenum_of_servings)放入 sort 参数中,我们可以使用 created_at 作为默认值。order 参数用于指定是 asc(升序)还是 desc(降序)。我们可以将 desc 作为默认值。

在语法方面,如果我们想要我们的 SQLAlchemy 查询结果按升序排序,我们可以这样做:

Import asc        

sort_logic_asc = asc(getattr(cls, sort))
cls.query.filter(cls.is_publish=True).order_by(sort_logic_asc)

如果我们想要按降序排序,我们只需使用 desc

Import desc        

sort_logic_desc = desc(getattr(cls, sort))
cls.query.filter(cls.is_publish=True).order_by(sort_logic_desc)

注意

除了 cls.is_published=True,您还可以使用 SQLAlchemy 列操作符,即 cls.is_published.is_(True)。您将得到相同的结果。

在下一个练习中,我们将实现 Smilecook 平台的排序和排序功能。这将使我们的应用程序更加用户友好。

练习 54:实现排序和排序

在这个练习中,我们将实现 Smilecook 平台的排序和排序功能。我们将向获取所有已发布食谱的 API 中添加 sortorder 参数,以便用户可以对已发布的食谱进行排序和排序。让我们开始吧:

  1. resources/recipe.py 中,使用装饰器中的 use_kwargs 方法RecipeListResource.get 方法添加两个参数(sortorder)。分别为这两个参数设置默认值 created_atdesc

    @use_kwargs({'q': fields.Str(missing='),
                            'page': fields.Int(missing=1),
                            'per_page': fields.Int(missing=20),
                            'sort': fields.Str(missing='created_at'),
                            'order': fields.Str(missing='desc')})
    def get(self, q, page, per_page, sort, order):
    
  2. 限制 sort 参数只接受 created_atcook_timenum_of_servings 的值。如果传入其他值,则默认为 created_at

            if sort not in ['created_at', 'cook_time', 'num_of_servings']:
                sort = 'created_at'
    
  3. 限制 order 参数只接受 ascdesc 的值。如果传入其他值,则默认为 desc

            if order not in ['asc', 'desc']:
                order = 'desc'
    
  4. sortorder 参数传递给 get_all_published 函数:

            paginated_recipes = Recipe.get_all_published(q, page, per_page, sort, order)
    
  5. 修改 models/recipe.py 中的 get_all_published 方法,使其如下所示。它接受两个额外的参数,即 sortorder,以定义逻辑:

        @classmethod
        def get_all_published(cls, q, page, per_page, sort, order):
            keyword = '%{keyword}%'.format(keyword=q)
            if order == 'asc':
                sort_logic = asc(getattr(cls, sort))
            else:
                sort_logic = desc(getattr(cls, sort))
            return cls.query.filter(or_(cls.name.ilike(keyword),
                                        cls.description.ilike(keyword)),
                                    cls.is_publish.is_(True)).\
                order_by(sort_logic).paginate(page=page, per_page=per_page)
    

在这里,我们已经创建了排序和排序函数。代码没有做太多修改。接下来,我们将使用 Postman 测试我们的实现。

练习 55:测试排序和排序功能

在上一个练习中,我们创建了自定义排序函数。用户应该能够通过指定的列对 Smilecook 平台上的食谱记录进行排序,无论是升序还是降序。在这个练习中,我们将测试这是否真的可行。我们将传递sortorder参数到 Postman 并验证它们。让我们开始吧:

  1. 我们将发送一个请求以获取所有食谱记录。然后,按cook_time升序排序数据。首先,点击RecipeList请求并选择Params标签页。

  2. 插入第一个键值对(sortcook_time)。

  3. 插入第二个键值对(orderdesc)。

  4. 发送请求。结果如下截图所示:图 8.7:发送请求以获取所有食谱记录

    img/C15309_08_07.jpg

    图 8.7:发送请求以获取所有食谱记录

    从前面的搜索结果中,我们可以看到食谱的cook_time是按升序排序的。第一个食谱的cook_time是 20 分钟,而第二个是 30 分钟。

  5. 发送请求以获取所有食谱记录。然后,按num_of_servings降序排序数据。点击RecipeList并选择Params标签页。

  6. 插入第一个键值对(sortnum_of_servings)。

  7. 插入第二个键值对(orderdesc)。

  8. 发送请求。结果如下截图所示:图 8.8:发送请求并按 num_of_servings 降序排序数据

    img/C15309_08_08.jpg

    图 8.8:按 num_of_servings 降序发送请求并排序数据
  9. 从前面的搜索结果中,我们可以看到食谱的num_of_servings已经按降序排序。第一个食谱的num_of_servings是为五人准备的,而第二个是为四人准备的。

现在,你已经完成了本章所学所有功能的开发和测试。接下来,我们将完成一个活动,以确保你能够灵活地使用到目前为止所学的内容。

活动 15:搜索含有特定成分的食谱

在这个活动中,我们将使用特定的属性搜索食谱。我们将添加一个新的ingredients属性,然后传递参数以搜索食谱。按照以下步骤完成此活动:

  1. 将成分属性添加到Recipe模型中。

  2. 运行 Flask-Migrate 以更新数据库。

  3. ingredients属性添加到RecipeSchema

  4. 修改RecipeResource.patch方法以支持ingredients属性更新。

  5. 修改Recipe.get_all_published方法,以便可以通过成分进行搜索。

  6. 创建两个带有ingredients属性的食谱并发布它们。

  7. 使用ingredients属性搜索食谱。

    注意

    这个活动的解决方案可以在第 336 页找到。

恭喜!你已经完成了这个活动。现在,请进行评估,以测试你对本章内容的理解。

摘要

在本章中,我们实现了许多出色的功能,使用户能够以简单高效的方式找到他们想要的食谱信息。我们实现的分页功能允许用户快速了解总共有多少食谱,并逐页浏览。它还节省了服务器的资源,因为它不需要一次性渲染成千上万的食谱。

搜索功能是另一个节省时间的特性。用户现在可以通过简单的搜索来查找他们想要的食谱。我们还在 Smilecook 应用程序中完成了排序和排序功能,这为用户提供了更好的浏览体验。

到目前为止,我们几乎创建了所有需要的用户功能。我们的 Smilecook 平台开发即将结束。在下一章中,我们将致力于内部系统优化,例如 HTTP 缓存和速率限制。

第九章:9. 构建更多功能

学习目标

到本章结束时,你将能够:

  • 使用缓存来提高 API 性能并高效获取最新信息

  • 使用 Flask-Caching 包将缓存功能添加到 Smilecook 应用程序中

  • 在 API 中实现速率限制功能

  • 使用 IP 地址进行速率限制

在本章中,我们将介绍缓存以提高性能,并熟悉使用速率限制功能。

简介

在上一章中,我们向 Smilecook 应用程序添加了分页、搜索和排序功能,以便用户可以更容易地导航到他们的食谱。这也帮助减轻了服务器负担并提高了性能。我们已经解释了为什么在当今世界使我们的 API 快速响应很重要。

在本章中,我们将从另一个方面进一步改进我们的 API 性能。我们将添加cache功能,它将临时将数据保存到应用内存中。这将使我们能够节省每次查询数据库所需的时间。这可以大大提高 API 性能并减轻服务器负担。有一个 Flask 扩展包,Flask-Caching,可以帮助我们实现缓存功能。我们首先将讨论缓存背后的理论,然后通过实际练习,向您展示如何在我们的 Smilecook 应用程序中实现此功能。

除了缓存之外,我们还将实现一个速率限制功能。这将通过限制某些高使用量用户的访问来防止他们危害整个系统。确保我们 API 的公平使用对于保证服务质量至关重要。我们将使用 Flask 扩展包Flask-Limiter来实现这一点。

这两个缓存和速率限制功能在现实场景中非常常见且强大。让我们了解它们是如何工作的。

缓存

缓存意味着将数据存储在临时空间(缓存)中,以便在后续请求中更快地检索。这个临时空间可以是应用内存、服务器硬盘空间或其他。缓存的整体目的是通过避免再次查询数据的任何重过程来减轻工作负载。例如,在我们的 Smilecook 应用程序中,如果我们认为来自热门作者的食谱总是会由用户查询,我们可以缓存这些食谱。因此,下次用户请求这些食谱时,我们只需从缓存中发送这些食谱,而不是查询我们的数据库。你可以在任何地方看到缓存。现在几乎所有应用程序都实现了缓存。即使在我们的本地浏览器中,我们也会将网站结果保存在本地硬盘上,以便下次访问更快。

对于服务器级别的缓存,大多数情况下,缓存存储在与应用程序相同的 Web 服务器上。但从技术上讲,它也可以存储在另一个服务器上,例如Redis远程字典服务器)或Memcached(高性能分布式缓存内存)。它们都是内存数据存储系统,允许键值存储以及存储数据。对于简单应用和易于实现,我们也可以使用单个全局字典作为缓存(简单缓存)。

缓存的优点

通过缓存,我们不仅可以减少需要传输的数据量,还可以提高整体性能。这是通过减少所需的带宽、减少服务器加载时间以及更多方式实现的。以我们的 Smilecook 应用为例:如果我们流量较低,缓存可能帮助不大,因为缓存将在下一次查询到来之前几乎到期。但想象一下,如果我们有高流量,比如说每分钟 10,000 个请求,都在请求菜谱。如果这些菜谱都缓存了,并且缓存尚未过期,我们就可以直接将缓存中的菜谱返回给客户端前端。在这种情况下,我们将节省 10,000 个数据库查询,这可能是一项重大的成本节约措施。

Flask-Caching

cache作为一个包含键值对的字典对象。这里的键用于指定要缓存的资源,而值用于存储实际要缓存的数据。以检索所有菜谱的资源为例。流程包含以下阶段:

  1. 请求获取/recipes资源。

  2. 使用键来搜索现有的缓存(例如,Flask-Caching 将使用request.pathhashed_args作为键值,例如,recipesbcd8b0c2eb1fce714eab6cef0d771acc)。

  3. 如果之前已经缓存了这些菜谱,则返回缓存的数据。

  4. 如果这些菜谱不存在缓存,则遵循标准流程从数据库中获取菜谱。

  5. 将结果(菜谱数据)保存到缓存中。

  6. 返回菜谱数据。

通过以下图示可以更好地说明这个过程:

![图 9.1:Flask-Caching 流程图]

![img/C15309_09_01.jpg]

图 9.1:Flask-Caching 流程图

通过遵循这个流程,您可以看到缓存的数据可以在我们查询数据库之前提供服务。

希望您对缓存背后的理论有了更好的理解。让我们卷起袖子,通过接下来的练习,将这个功能和我们的 Smilecook 应用结合起来。

练习 56:使用 Flask-Caching 实现缓存功能

在这个练习中,我们将安装 Flask-Caching 包。然后,我们将在RecipeListResource中实现cache函数。我们还将添加两个装饰器,@app.before_request@app.after_request,以打印应用程序日志,便于测试:

  1. requirements.txt中添加 Flask-Caching 包和版本:

    Flask-Caching==1.7.2
    
  2. 运行pip命令来安装包:

    pip install -r requirements.txt
    

    一旦我们运行了 install 命令,我们应该看到以下结果:

    Installing collected packages: Flask-Caching
    Successfully installed Flask-Caching-1.7.2
    
  3. extensions.py 中导入 Cache 并实例化它:

    from flask_caching import Cache
    cache = Cache()
    
  4. app.py 中从 extensions 导入 cache

    from extensions import db, jwt, image_set, cache
    
  5. app.py 中,在 register_extensions 函数下添加 cache.init_app(app)。传递 app 对象以初始化缓存功能:

    def register_extensions(app):
        db.app = app
        db.init_app(app)
        migrate = Migrate(app, db)
        jwt.init_app(app)
        configure_uploads(app, image_set)
        patch_request_class(app, 10 * 1024 * 1024)
        cache.init_app(app)
    
  6. config.py 中添加与缓存相关的配置:

    CACHE_TYPE = 'simple' 
    CACHE_DEFAULT_TIMEOUT = 10 * 60
    

    默认的 CACHE_TYPENull,表示没有缓存。这里,我们将 CACHE_TYPE 设置为 simple,这意味着我们将使用 SimpleCache 策略。默认过期时间是 10 * 60 秒,即 10 分钟。

  7. resources/recipe.py 中从 extensions 导入 cache

    from extensions import image_set, cache
    
  8. resources/recipe.py 中,将 cache 装饰器放在 RecipeListResourceget 方法中:

    class RecipeListResource(Resource):
        @use_kwargs({'q': fields.Str(missing=''),
                                    'page': fields.Int(missing=1),
                                    'per_page': fields.Int(missing=20),
                                    'sort': fields.Str(missing='created_at'),
                                    'order': fields.Str(missing='desc')})
        @cache.cached(timeout=60, query_string=True)
        def get(self, q, page, per_page, sort, order):
    

    我们在这里将缓存过期时间(timeout)设置为 60 秒。query_string = True 表示允许传递参数。

  9. 为了测试,在 RecipeListResource.get 方法中打印一行 Querying database

        def get(self, q, page, per_page, sort, order):
            print('Querying database...')
    
  10. 为了测试,在 app.pyregister_extensions(app) 函数底部添加以下装饰器定义:

    @app.before_request
        def before_request():
            print('\n==================== BEFORE REQUEST ====================\n')
            print(cache.cache._cache.keys())
            print('\n=======================================================\n')
        @app.after_request
        def after_request(response):
            print('\n==================== AFTER REQUEST ====================\n')
            print(cache.cache._cache.keys())
            print('\n=======================================================\n')
            return response
    

我们已经在 RecipeListResource 上完成了第一个缓存功能。这应该会减少从数据库获取食谱的频率。让我们在下一个练习中测试它以确保它正常工作。

练习 57:使用 Postman 测试缓存功能

在这个练习中,我们将使用 Postman 来测试缓存功能。并且我们将在 PyCharm 控制台中验证它是否正常工作:

  1. 首先,获取所有食谱详情。点击 GET RecipeList

  2. 然后,发送请求。结果如下截图所示:图 9.2:获取所有食谱详情

    图 9.2:获取所有食谱详情
  3. 在 PyCharm 控制台中检查应用程序日志。图 9.3:检查应用程序日志

    图 9.3:检查应用程序日志

    在控制台中,我们可以看到在请求之前,缓存是空的。在数据库查询之后,数据被缓存并返回给前端客户端。

  4. 再次获取所有食谱详情并检查 PyCharm 控制台中的结果:图 9.4:再次获取所有食谱详情

图 9.4:再次获取所有食谱详情

因为这是我们第二次请求数据,所以我们从缓存中获取它,而不是从数据库中获取;之前的结果被缓存了。我们可以从 PyCharm 控制台中看到结果被缓存,并且没有对数据库的查询。

因此,我们在这里完成了缓存功能的实现和测试。由于我们这里只是缓存了一条记录,性能提升可能不明显。但想象一下,如果我们在一个短时间内收到了数千个同类的请求;这种缓存功能可以大大减少我们数据库的工作量。

注意

如果我们想查看缓存中的数据,可以使用以下代码行:print(cache.cache._cache.items()),以检查存储在那里的键值。在那里我们可以看到缓存中的值是我们返回给客户端前端的 JSON 数据。

数据更新时清除缓存

当数据更新时,之前缓存的那些数据立即变得过时。例如,如果食谱的封面图片被更新,旧的封面图片将被移除。但在缓存中,仍然会有旧封面图片的 URL,这将不再有效。因此,我们需要一个机制来清除旧缓存,并将新封面图片的 URL 存储到我们的缓存中。

活动 16:更新食谱详情后获取缓存数据

当我们获取所有食谱详情时,它们将被存储在缓存中,可以直接用于下一个请求。在这个活动中,我们将检查在更新食谱数据后尝试获取食谱详情会发生什么:

  1. 首先,获取所有食谱详情。

  2. 更新其中一个食谱详情。

  3. 再次获取所有食谱详情并检查食谱详情。

    注意

    这个活动的解决方案可以在第 340 页找到。

在我们的下一个练习中,我们将找到所有涉及更新数据的资源。我们将在数据更新后添加一个清除缓存的步骤。

练习 58:实现缓存清除功能

在这个练习中,我们将尝试在更新食谱数据时清除缓存。这里涉及很多资源。我们将逐一解决它们:

  1. 从 utils.py 中导入缓存:

    from extensions import image_set, cache
    
  2. utils.py 下创建一个新的用于清除缓存的功能。该函数应使用特定的前缀清除缓存:

    def clear_cache(key_prefix):
        keys = [key for key in cache.cache._cache.keys() if key.startswith(key_prefix)]
        cache.delete_many(*keys)
    

    在这里,代码是使用 for 循环遍历 cache.cache._cache.keys() 中的 key,以迭代缓存中的所有键。如果键以传入的前缀开头,它将被放置在 keys 列表中。然后,我们将使用 cache.delete_many 方法来清除缓存。前述代码中的单个星号 * 是用于将列表解包为位置参数。

  3. resources/recipe.py 中导入 clear_cache 函数:

    from utils import clear_cache
    
  4. 在更新食谱数据的资源中调用 clear_cache('/recipes')。在 RecipeResource.patchRecipeResource.deleteRecipePublishResource.putRecipePublishResource.deleteRecipeCoverUploadResource.put 方法中,在 return 之前添加 clear_cache('/recipes')

    clear_cache('/recipes')
    

    因此,在这里,如果操作得当,当数据更新时,旧缓存数据将被清除。下次当请求这些更新后的数据时,它将再次存储在缓存中。

  5. resources/user.py 中导入 generate_tokenverify_tokensave_imageclear_cache 函数:

    from utils import generate_token, verify_token, save_image, clear_cache
    
  6. UserAvatarUploadResource.put 中调用 clear_cache('/recipes') 以在数据更新时清除缓存:

    clear_cache('/recipes')
    

    当用户更新他们的头像图片时,这将改变 avatar_url 属性。因此,我们还需要在那里清除过时的缓存。

在这个练习之后,我相信您将对整个缓存流程有更深入的理解。我们在这里构建缓存功能是为了提高性能,但同时我们还想确保缓存被刷新以保证数据质量。

练习 59:验证缓存清除功能

在我们之前的练习中,我们将清除缓存的步骤添加到了涉及数据更新的资源中。在这个活动中,我们将验证我们实现的缓存清除功能。我们可以通过更新数据并查看 API 是否返回更新后的数据来测试它:

  1. 获取所有食谱数据。点击RecipeList并发送请求。结果如下所示:![图 9.5:获取所有食谱数据并发送请求 图片

    图 9.5:获取所有食谱数据并发送请求
  2. 在 PyCharm 控制台中检查应用程序日志:![图 9.6:检查 PyCharm 控制台中的应用程序日志 图片

    图 9.6:检查 PyCharm 控制台中的应用程序日志

    我们可以看到在请求之前缓存是空的。然后,在查询数据库后,新的数据被缓存。

  3. 登录您的账户。点击Collections标签并选择POST Token请求。

  4. 发送请求。结果如下所示:![图 9.7:选择 POST Token 请求并发送请求 图片

    图 9.7:选择 POST Token 请求并发送请求
  5. 使用PATCH方法修改食谱记录。首先,选择PATCH Recipe请求。现在,选择Bearer {token};令牌应该是访问令牌。

  6. 选择num_of_servings10cook_time100。请检查以下内容:

    { 
        "num_of_servings": 10, 
        "cook_time": 100 
    } 
    
  7. 发送请求。结果如下所示:![图 9.8:使用 PATCH 方法修改食谱记录 图片

    图 9.8:使用 PATCH 方法修改食谱记录
  8. 在 PyCharm 控制台中检查应用程序日志:![图 9.9:检查应用程序日志 图片

图 9.9:检查应用程序日志

我们可以看到在请求之前缓存是存在的。但是,在食谱记录更新后,缓存变得过时并被移除。

因此,在这个练习中,我们已经完成了缓存清除功能的测试。这将确保我们获取到最新的数据。

注意

应用程序日志的打印仅用于测试。在我们继续之前,我们需要对before_requestafter_request中的print命令进行注释。我们可以在 Mac 上使用command + /,或在 Windows 机器上使用Ctrl + /

API 速率限制

当我们提供 API 服务时,我们需要确保每个用户都能公平使用,以便系统资源能够有效且公平地为所有用户服务。我们希望确保大多数用户都能获得良好的服务器性能;因此,我们需要施加限制。通过限制少数高流量用户,我们可以确保大多数用户都感到满意。

要这样做,我们需要为每个用户设置一个限制。例如,我们可以限制每个用户的请求次数不超过每秒100次。这个数字足以满足我们 API 的正常使用。如果用户在每秒发起100次以上的请求,超出的请求将不会被处理。这是为了为其他用户保留系统资源(如 CPU 处理和带宽资源)。

为了实现这一点,我们引入了速率限制的概念。通过限制每个用户的 API 服务“速率”,我们保证大多数用户能够享受到他们应得的服务性能。

HTTP 头信息和响应代码

我们可以使用 HTTP 头信息来显示速率限制信息。以下 HTTP 头信息中的属性可以告诉我们允许的请求数量(速率)、剩余配额以及限制何时将重置:

  • X-RateLimit-Limit:显示此 API 端点的速率限制

  • X-RateLimit-Remaining:显示下一次重置前允许的剩余请求数量

  • X-RateLimit-Reset:速率限制将被重置的时间(UTC 纪元时间)

  • Retry-After:下一次重置前的秒数

当用户开始违反速率限制时,API 将返回 HTTP 状态码Too Many Requests,并在响应体中包含错误信息:

{ 
    "errors": "Too Many Requests" 
}

要实现此速率限制功能,我们可以使用 Flask 扩展包 Flask-Limiter。Flask-Limiter 包可以帮助我们轻松地将速率限制功能添加到我们的 API 中。

Flask-Limiter

RATELIMIT_HEADERS_ENABLED配置。因此,我们不需要自己编写 HTTP 头信息代码。除此之外,它还支持可配置的后端存储,当前实现包括 Redis、内存、Memcached 等。

我们甚至可以设置多个限制;我们只需要使用分隔符来界定它们。例如,我们可以同时设置每分钟100次请求和每小时1000次请求的限制。

使用以下语法为我们的 API 端点设置速率限制:

[count] [per|/] [n (optional)] [second|minute|hour|day|month|year]

下面是一些示例:

100 per minute
100/minute
100/minute;1000/hour;5000/day

现在我们已经了解了速率限制的工作原理。我们将一起进行一个实际练习,将这个有用的功能添加到我们的 Smilecook 应用程序中。

练习 60:实现 API 速率限制功能

在这个练习中,我们将使用Flask-Limiter实现 API 速率限制功能。我们将安装并设置Flask-Limiter,然后将其速率限制添加到RecipeListResource

  1. Flask-Limiter版本1.0.1添加到requirements.txt

    Flask-Limiter==1.0.1
    
  2. 使用pip install命令安装包:

    pip install -r requirements.txt
    

    你应该看到以下安装结果:

    Installing collected packages: limits, Flask-Limiter
      Running setup.py install for limits ... done
      Running setup.py install for Flask-Limiter ... done
    Successfully installed Flask-Limiter-1.0.1 limits-1.3
    
  3. extensions.py 中导入 Limiterget_remote_address 并实例化一个 limiter 对象:

    from flask_limiter import Limiter
    from flask_limiter.util import get_remote_address
    limiter = Limiter(key_func=get_remote_address)
    

    get_remote_address 函数将返回当前请求的 IP 地址。如果找不到 IP 地址,它将返回 127.0.0.1,这意味着本地主机。在这里,我们的策略是按 IP 地址限制速率。

  4. app.py 中从 extensions 导入 limiter

    from extensions import db, jwt, image_set, cache, limiter
    
  5. app.py 中,在 register_extensions 下初始化 limiter 对象。将 app 对象传递给 limiter.init_app 方法:

        limiter.init_app(app)
    
  6. config.py 中,将 RATELIMIT_HEADERS_ENABLED 设置为 True

    RATELIMIT_HEADERS_ENABLED = True
    

    这将允许 Flask-Limiter 在 HTTP 头中放入与速率限制相关的信息,包括 X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-ResetRetry-After

  7. resources/recipe.py 中从 extensions 导入 limiter

    from extensions import image_set, cache,  limiter
    
  8. RecipeListResource 中,将 limiter.limit 函数放入 decorators 属性:

    class RecipeListResource(Resource):
        decorators = [limiter.limit('2 per minute', methods=['GET'], error_message='Too Many Requests')]
    

    我们将请求次数设置为每分钟仅两个。HTTP 方法是 GET,错误信息是 Too Many Requests

  9. 点击 运行 以启动 Flask 应用程序;然后,我们就可以测试它了:![图 9.10:启动 Flask 应用程序然后测试 图片

图 9.10:启动 Flask 应用程序然后测试

现在这个练习已经完成,我们的 API 已经有了速率限制功能。在下一个练习中,我们必须测试我们的速率限制函数。

练习 61:验证速率限制功能

在上一个练习中,我们设置了获取所有食谱详情的 API,每分钟只能获取两次。所以,在这个练习中,我们将看到结果是否符合我们的预期:

  1. 获取所有食谱数据。点击 GET RecipeList 并发送请求。

  2. 选择 60 秒后。

  3. 再次获取所有食谱数据并发送两次请求。

  4. 在 HTTP 响应中选择 Body。结果如下截图所示:![图 9.12:再次获取所有食谱数据并发送两次请求 图片

图 9.12:再次获取所有食谱数据并发送两次请求

我们可以看到,在第三次请求时,我们将收到错误 HTTP 状态码 429 太多请求。这意味着速率限制正在起作用。

在这个练习中,我们已经完成了速率限制功能。通过限制少量滥用用户,我们确保大多数用户可以享受高性能的服务。

练习 62:添加白名单

我们希望减轻我们开发人员和 API 测试人员的速率限制,因为他们可能确实需要频繁地向 API 发送请求进行测试。在这种情况下我们应该怎么做?在这个练习中,我们将看到如何使用 Flask-Limiter 来满足这一要求。

我们希望设置一个 IP 白名单,允许某些 IP 地址使用 API 而不受任何速率限制:

  1. app.py 中导入 request

    from flask import Flask, request
    
  2. app.py中,使用@limiter.request_filter装饰器并设置白名单函数。将127.0.0.1(本地主机)放入白名单中:

        @limiter.request_filter
        def ip_whitelist():
            return request.remote_addr == '127.0.0.1'
    
  3. 运行app.py:![图 9.13:运行 app.py 文件 图 C15309_09_13.jpg

    图 9.13:运行 app.py 文件
  4. 通过发送一个获取所有菜谱的GET请求来测试应用程序,并检查 HTTP 头部的速率限制。点击GET RecipeList并发送请求。在响应中选择Header选项卡。结果如下所示:![图 9.14:检查速率限制的 HTTP 头部 图 C15309_09_14.jpg

![图 9.14:检查速率限制的 HTTP 头部

我们可以看到速率限制已经消失。在这个练习中,您已经看到速率限制功能可以灵活运用。它可以根据不同的情况实施或撤销。

活动十七:添加多个速率限制

在这个活动中,我们将向同一资源添加多个速率限制。但请记住,我们在之前的练习中添加了一个白名单。我们需要注释掉这段代码,以便我们可以进行测试:

  1. UserRecipeListResource中添加速率限制。限制为每分钟3次,每小时30次,每天300次。

  2. 注释掉白名单代码。

  3. 使用 Postman 测试速率限制功能。

    注意

    本活动的解决方案可以在第 343 页找到。

恭喜!现在您已经完成了这个活动,您知道如何灵活地使用速率限制功能。

摘要

在本章中,我们学习了并在 Smilecook API 中实现了缓存和速率限制功能。这些功能使我们的 API 更加高效。然而,我们的 Smilecook 应用程序将缓存保存在应用内存中,这意味着在服务器重启后缓存将消失。为了解决这个问题,我们可以在未来与 Redis 或 Memcached 一起工作,它们可以在服务器重启后持久化缓存。它们还支持多个服务器之间的缓存共享。这是我们鼓励您在本书之外探索的内容。目前最重要的事情是您要学习本书中涵盖的所有基本概念。因此,如果您以后想要扩展到更高级的实现,这对您来说应该不会太难。

在下一章和最后一章中,我们将为您构建 Smilecook 前端客户端,以便您与后端 API 协同工作。通过这个前端客户端,我们将更好地理解整体情况。您将看到前端和后端的交互方式。最后,我们将整个应用程序部署到 Heroku 云平台,这意味着我们的 Smilecook 应用程序将被每个人使用。

第十章:10. 部署

学习目标

到本章结束时,你将能够:

  • 解释将应用程序部署到云的过程

  • 解释 SaaS、PaaS 和 IaaS 之间的区别

  • 在开发和生产环境之间设置不同的配置

  • 设置 Heroku 云平台

  • 安装和配置 Heroku Postgres

  • 使用 Heroku 命令行界面(Heroku CLI)部署应用程序

  • 设置 Postman 环境变量

在本章中,我们将部署我们的应用程序到 Heroku,并使用 Postman 进行测试。

简介

在上一章中,我们向我们的 Smilecook 应用程序添加了缓存和速率限制功能。这两个功能非常有用,尤其是在我们处理大量流量时。缓存和速率限制可以提高响应速度,也可以提高安全性。

在本章中,我们将讨论如何将我们的应用程序部署到云服务器。部署应用程序就像出版一本书或发布一部电影。这就像将我们的应用程序推向市场。如今,许多云服务提供商提供免费使用配额。只要资源使用量低于一定阈值,它们允许开发者免费将其应用程序部署到其云平台。对于我们的 Smilecook 应用程序,我们只需要对代码和配置文件进行一些小的修改。其他一切将由云平台处理。你很快就会看到这是多么简单。

我们首先将对应用程序代码进行一些小的修改,以区分生产环境和开发环境配置。然后,我们将讨论 Heroku 云服务平台,我们将在该平台上部署 Smilecook 应用程序。我们将向您介绍 Heroku 云服务平台中的账户注册、配置和部署过程。

部署完成后,我们将使用 Postman 直接在生产环境中测试 API。这不是很令人兴奋吗?!无需多言,让我们开始吧。

部署

部署的目的是什么?我们之前编写的 API 应用程序只是在本地机器上运行代码。我们可以使用本地机器上的一个端口,从客户端向本地服务器发送请求。这对于开发目的来说很好。我们可以在开发环境中快速测试和调整我们的应用程序。然而,我们的本地机器并不是作为服务器来使用的;其他人无法访问它。他们也不能向托管在我们本地机器上的 API 发送 HTTP 请求。

如果我们想将此 API 服务对外开放,我们需要将其托管在服务器上。服务器应连接到互联网,拥有域名和 URL,以便其他人可以访问它。

将应用程序从本地机器迁移到运行在互联网上的服务器称为部署。这涉及到环境设置、依赖包安装和构建 Web 服务器等工作。

比较 SaaS、PaaS 和 IaaS

在过去,建立自己的网络服务器成本高昂。需要考虑的因素很多,包括网络连接、存储、服务器配置和操作系统设置。如今,云计算服务已经出现,提供所有基础设施服务,这显著降低了成本,尤其是对于个人开发者和中小型企业。云计算服务主要分为三大类。这些是软件即服务SaaS)、平台即服务PaaS)和基础设施即服务IaaS)。每种服务都有其优缺点,这些将在本节中讨论。

IaaS:用户无需购买自己的服务器、软件、网络设备等。这些基础设施作为服务提供,用户无需关心设置和维护。他们仍然有能力配置这些服务,例如安装软件和设置防火墙。IaaS 的例子包括AWS EC2Google Compute EngineGCE)。

与过去相比,这种 IaaS 模型可以大大降低硬件和网络设置成本,以及与空间和资源相关的所有其他成本。个人开发者或中小型企业通常不需要那么多的系统资源。因此,这种模式允许他们按需租用基础设施作为服务;他们只需支付所需的资源费用。

  • 优点:开发者拥有更大的灵活性。IaaS 为应用程序运行提供必要的计算资源。开发者可以根据应用程序的需求轻松请求额外的资源,或减少资源。这是易于定制的。

  • 缺点:开发者需要花费时间学习如何根据他们的需求配置云平台。

PaaS:PaaS 位于 SaaS 和 IaaS 之间。用户无需管理和维护基础设施。服务提供商已经将这些基础设施和相关服务打包成一个平台,并以服务的形式出租给用户。用户无需担心后端设置,也不必担心扩展服务器数量和负载均衡等方面。用户(开发者)只需专注于他们的开发,并根据云平台相应地部署他们的工作。PaaS 的例子包括 Heroku、Windows Azure 和 AWS Elastic Beanstalk。

  • 优点:减少了设置时间。通过利用平台提供的服务,开发者可以专注于开发。

  • 缺点:可能会产生不必要的费用。与 IaaS 相比,PaaS 在基础设施设置和配置方面灵活性较低,因为你对基础设施的控制较少。由于整个平台都被打包成服务,一些未使用的打包资源可能会浪费。在这种情况下,费用可能比 IaaS 高。

SaaS:SaaS 基本上是指互联网上可用的 Web 应用。用户不需要维护软件。软件作为服务提供。一个非常典型的例子是 Gmail。SaaS 的例子包括 Dropbox、Salesforce 和 Slack。

  • 优点:成本较低,因为我们不需要关心硬件购买和其他设置成本。如果用户有可以通过此服务解决的问题,SaaS 可能是最简单、最有效的解决方案。

  • 缺点:由于大量用户数据将存储在云端平台,可能会对数据安全产生一些担忧。此外,一旦应用部署,我们还需要考虑服务的可用性。

作为个人开发者,我们需要一个稳定且可扩展的服务器来部署我们的应用。PaaS 是最佳选择。它为应用提供运行的计算平台,开发者无需担心硬件维护,因为服务提供商会处理所有这些。因此,这是一个节省时间和成本的开发者解决方案。开发者可以专注于开发优秀的软件。

Heroku 平台

Heroku 是一个流行的 PaaS。我们可以在那里部署我们的 API,以便全世界任何人都可以访问。而且它不仅支持 Python,还支持其他编程语言,包括 Ruby 和 Go。

Heroku 为开发者提供免费计划,以便他们在那里部署和测试他们的应用。当然,他们也有付费计划,以及许多更强大的功能,可以使我们的 API 更加安全和高效。稍后,如果您需要这些强大的功能和系统资源来支持您的应用,您可以考虑这一点。但现在,出于教学目的,免费计划已经足够好。

注意

除了 Heroku,还有其他云服务提供商。云服务市场的一些领导者包括亚马逊网络服务AWS)、谷歌云平台GCP)、IBM Cloud、Microsoft Azure 和 Rackspace Cloud。

Smilecook 中的配置处理

大多数应用都需要多个配置;至少需要一个用于生产服务器,另一个用于开发使用。它们之间会有所不同,例如调试模式、密钥和数据库 URL。我们可以使用一个始终加载的默认配置,以及为生产服务器和开发环境分别创建的配置,以便根据环境继承默认配置。对于特定环境的配置,我们将创建两个新的类——DevelopmentConfigProductionConfig

练习 63:生产环境和开发环境的配置处理

在这个练习中,我们将把应用程序配置在开发和生产环境之间分开。对于如DEBUG这样的配置,我们将需要两个环境的不同值。数据库 URL 也是如此。因此,我们将创建两组配置,DevelopmentConfigProductionConfig。前者用于开发环境和系统增强,而后者将在生产环境中运行。按照以下步骤完成练习:

  1. 首先,在config.py中添加一个默认配置,它将在所有环境中使用:

    import os
    class Config:
        DEBUG = False
    
        SQLALCHEMY_TRACK_MODIFICATIONS = False
    
        JWT_ERROR_MESSAGE_KEY = 'message'
    
        JWT_BLACKLIST_ENABLED = True
        JWT_BLACKLIST_TOKEN_CHECKS = ['access', 'refresh']
    
        UPLOADED_IMAGES_DEST = 'static/images'
    
        CACHE_TYPE = 'simple'
        CACHE_DEFAULT_TIMEOUT = 10 * 60
    
        RATELIMIT_HEADERS_ENABLED = True
    
  2. Config类之后添加DevelopmentConfig

    class DevelopmentConfig(Config):
        DEBUG = True
        SECRET_KEY = 'super-secret-key'
        SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://your_name:your_password@localhost:5432/smilecook'
    

    新的DevelopmentConfig类扩展了父类Config。将DEBUG值设置为True。这样我们就可以在开发过程中看到错误信息。

  3. Development Config类之后添加ProductionConfig

    class ProductionConfig(Config):
        SECRET_KEY = os.environ.get('SECRET_KEY')
        SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
    

    这里的ProductionConfig类也扩展了父类Config。与DevelopmentConfig类类似,我们在这里设置了SECRET_KEYSQLALCHEMY_DATABASE_URI。在生产环境中,这些值是从环境变量中获取的。我们将在稍后教你如何在云平台上设置这些值。

  4. app.py中导入os

    import os
    
  5. app.py中,进行以下更改以动态获取配置:

    def create_app():
        env = os.environ.get('ENV', 'Development')
        if env == 'Production':
            config_str = 'config.ProductionConfig'
        else:
            config_str = 'config.DevelopmentConfig'
        app = Flask(__name__)
        app.config.from_object(config_str)
        ...
    
        return app
    

    ENV环境变量将通过os.environ.get获取。如果是Production,则使用生产环境配置。此外,还将使用开发环境配置。

  6. 右键点击 PyCharm 并运行应用程序。因为我们没有在本地机器上设置ENV环境变量,所以 Flask 将选择config.DevelopmentConfig并执行它。我们可以从输出中看到调试模式:开启:![图 10.1:在开发环境中运行应用程序 图片

图 10.1:在开发环境中运行应用程序

因此,我们已经将生产环境和开发环境之间的配置分开。将来,如果有两个环境共享的通用配置,我们将它们放在Config类中。否则,它们应该放在相应的DevelopmentConfigProductionConfig类下。

练习 64:添加一个预发布配置类

为了便于内部测试,在这个练习中,我们需要添加一个StagingConfig类。这个配置将扩展通用Config类。预发布环境与生产环境不会相差太多,因为它主要是为了测试而设计,以模仿生产环境。并且我们将从环境变量中获取密钥和数据库 URI:

  1. config.py中创建一个扩展ConfigStagingConfig类:

    class StagingConfig(Config):
        SECRET_KEY = os.environ.get('SECRET_KEY')
        SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
    
  2. app.py中修改StagingConfig的条件语句:

        if env == 'Production':
            config_str = 'config.ProductionConfig'
        elif env == 'Staging':
            config_str = 'config.StagingConfig'
        else:
            config_str = 'config.DevelopmentConfig'
    

因此,我们已经为预发布环境设置了配置。但尚未完成,因为需要从云服务器获取环境变量。接下来,我们将开始处理云平台,Heroku

Heroku 应用

在我们将应用部署到 Heroku(云平台)之前,我们首先会在那里创建一个账户并设置环境。我们将创建一个新的 Heroku 应用。然后,我们需要在 Heroku 上安装 Postgres 数据库。安装过程可以在 Heroku 平台上完成;一切都是集成的。最后,我们设置虚拟环境变量,例如数据库 URL 和密钥。一旦所有这些前期准备工作完成,我们就会开始部署过程。

练习 65:在 Heroku 创建新应用

在这个练习中,我们首先注册 Heroku 账户。然后,我们将在上面创建一个新应用。Heroku 提供了一个用户界面友好、易于遵循的设置流程。我们只需点击几个按钮就完成了。由于 Heroku 是 PaaS,我们不需要管理任何硬件或设置操作系统。这些都是 Heroku 负责的:

  1. 访问 Heroku 网站,www.heroku.com/,并点击注册:![图 10.2:访问 Heroku 网站

    ![img/C15309_10_02.jpg]

    图 10.2:访问 Heroku 网站
  2. 签到过程完成后,点击登录并访问仪表板。点击创建新应用以在 Heroku 中创建新应用:![图 10.3:登录并访问 Heroku 仪表板

    ![img/C15309_10_03.jpg]

    图 10.3:登录并访问 Heroku 仪表板
  3. 输入应用名称,然后选择服务器区域(目前,唯一的选择是美国和欧洲;请选择离目标用户更近的一个)。然后,点击创建应用继续:

![图 10.4:输入应用名称并选择服务器区域

![img/C15309_10_04.jpg]

图 10.4:输入应用名称并选择服务器区域

注意

应用名称将用于 Heroku 提供的应用 URL 中,例如,https://{app_name}.herokuapp.com/。用户可以使用此 URL 访问我们的 API。

应用创建后,我们可以看到应用管理界面,大致如下:

![图 10.5:Heroku 应用管理界面

![img/C15309_10_05.jpg]

图 10.5:Heroku 应用管理界面

应用管理界面提供了我们了解应用状态的信息:

概览:显示我们产生的费用或其他协作者的活动

资源:用于管理附加组件和Procfile设置

部署:用于选择部署方法

指标:用于显示应用的指标

活动:用于跟踪用户活动

访问:用于管理协作者的访问权限

设置:包括环境变量配置、构建包设置和其他高级功能

注意

Heroku 平台的核心是能够使用轻量级容器 Dynos 运行应用程序。容器化是将您的应用程序代码、配置和依赖项打包成一个单一对象的标准方式。容器化可以减少对管理硬件、虚拟机或环境设置的负担。

一旦应用程序创建完成,我们将在 Heroku 中安装 Postgres 仓库,并通过 Heroku 扩展直接安装它。

Heroku 扩展

Heroku 拥有一个丰富的扩展库。扩展类似于插件,为开发、扩展和运行您的应用程序提供工具和服务,包括数据存储、监控、日志记录、分析和安全。对于我们的 Smilecook 应用程序,我们将使用 Heroku 提供的 Heroku Postgres,这是一个基于 PostgreSQL 的可靠且强大的数据库服务。入门级免费,提供 10,000 行限制,并保证 99.5% 的正常运行时间。这适合开发爱好应用。

练习 66:安装 Heroku Postgres

在这个练习中,我们将安装 Heroku Postgres。与从 Postgres 官方网站安装相比,从 Heroku 安装 Postgres 更方便。我们只需在 Heroku 扩展数据存储 类别中直接选择 Heroku Postgres 来安装。Heroku 提供了一个后端管理界面,使我们能够一目了然地查看数据库状态:

  1. 切换到 Heroku 的 资源 选项卡,然后右键单击 查找更多扩展 按钮:![图 10.6:切换到 Heroku 的资源选项卡 图 10.06

    图 10.6:切换到 Heroku 的资源选项卡
  2. 扩展 页面上,点击 数据存储 并选择 Heroku Postgres:![图 10.7:Heroku 的扩展页面 图 10.07

    图 10.7:Heroku 的扩展页面
  3. 然后,点击 安装 Heroku Postgres 以在我们的云服务器上安装扩展:![图 10.8:安装 Heroku Postgres 扩展 图 10.08

    图 10.8:安装 Heroku Postgres 扩展
  4. 选择默认的 Hobby Dev - Free Plan。此计划免费。在 要部署的应用 中输入我们在上一个练习中使用的应用程序名称,然后点击 部署扩展:![图 10.9:选择 Heroku Postgres 扩展计划 图 10.09

    图 10.9:选择 Heroku Postgres 扩展计划
  5. 完成这些操作后,我们可以在 扩展 页面上检查 Heroku Postgres 是否已安装:![图 10.10:检查 Heroku Postgres 是否已安装 图 10.10

    图 10.10:检查 Heroku Postgres 是否已安装
  6. 然后,点击 Heroku Postgres 扩展 以进入管理页面:

![图 10.11:Heroku Postgres 管理页面图 10.11

图 10.11:Heroku Postgres 管理页面

概览允许我们检查数据库状态、利用率等。耐用性允许我们管理数据安全和备份。设置存储数据库凭据和其他高级设置。数据片段允许您使用 SQL 命令在线查询数据库数据。您可以在那里导出或分享结果。

如您所见,在 Heroku 上安装 Postgres 非常简单;只需几个步骤。接下来,我们将着手在云平台上设置环境变量。

为 Heroku 应用设置环境变量

我们之前修改了config.py并在其中添加了ProductionConfig。现在我们必须在 Heroku 中添加环境变量,包括密钥和数据库 URL。除此之外,别忘了 Mailgun API 密钥和 Mailgun 域名。我们将在下一个练习中将所有这些一起设置。

练习 67:设置应用环境变量

在这个练习中,我们将设置生产环境中的环境变量。幸运的是,因为我们使用的是 Heroku Postgres,数据库 URL 环境变量已经为我们设置好了。我们只需要设置ENVSECRET_KEYMAILGUN KEYDOMAIN。然后,一旦设置完成,在Deploy代码完成后,应用程序将读取App config中新增的环境变量:

  1. 在 PyCharm 的 Python 控制台中,使用以下两行代码生成密钥:

    >>>import os
    >>>os.urandom(24)
    

    注意

    密钥应该尽可能随机。有很多随机生成器我们可以利用。但可能最简单的方法是在 PyCharm 的 Python 控制台中生成它。

  2. 前往设置选项卡,并设置ENVMAILGUN_API_KEYMAILGUN_DOMAINSECRET_KEY环境变量,如下所示:

![图 10.12:在 Heroku 中设置环境变量图片

图 10.12:在 Heroku 中设置环境变量

现在我们已经在 Heroku 上完成了必要的准备工作,我们将直接进入部署过程。

使用 Heroku Git 进行部署

Heroku 提供了一份关于如何部署我们的应用的指南。该指南可以在部署选项卡中找到。它主要分为三个部分。它们是安装 Heroku CLI创建一个新的 Git 仓库部署您的应用。具体细节如下:

![图 10.13:使用 Heroku Git 指南进行部署图片

图 10.13:使用 Heroku Git 指南进行部署

部署选项卡中的指南有三个部分:

安装 Heroku CLI

  • heroku login – 使用 Heroku CLI 工具登录 Heroku。

创建一个新的 Git 仓库

  • cd my-project/ – 切换到my-project文件夹。

  • git init – 初始化git,这是一个版本控制系统。我们很快就会讨论这个问题。

  • heroku git:remote -a smilecook – 将应用程序(Smilecook)仓库添加到本地 Git 的远程仓库列表中。

部署你的应用程序

  • git add . – 将所有文件和文件夹添加到当前目录及其子目录到 Git。

  • git commit -am "make it better" – 提交一个更改并插入提交信息make it better

  • git push heroku master – 这将上传本地仓库内容到远程仓库,即 Heroku 中的仓库。一旦推送,Heroku 将运行应用程序启动程序。

在我们开始部署应用程序之前,还有一些术语需要解释。

Git 是什么?

Git 是一个分布式版本控制系统。版本控制系统主要是一个可以跟踪你源代码每个版本的系统。源代码中的任何更改都将被记录在系统中。它允许开发者轻松地恢复到之前的版本。无需手动备份。

Git 还支持协作和其他高级功能。如果您感兴趣,可以访问官方 Git 网站了解更多信息:git-scm.com

gitignore 是什么?

gitignore 是一个包含 Git 应该忽略的文件和文件夹列表的文件。列表中的文件和文件夹将不会存储在 Git 中。通常,我们会将环境配置、日志等内容包含在这个列表中。

Procfile 是什么?

Procfile 是一个在 Heroku 应用程序启动过程中执行的文件。开发者将在此处放入他们希望在应用程序启动过程中让 Heroku 运行的命令。通常,我们会将设置脚本和服务器启动脚本放在这里。

Gunicorn 是什么?

Gunicorn 是一个兼容各种 Web 应用程序的 Python WSGI HTTP 服务器。它可以作为 Web 服务器和 Web 应用程序之间的接口。Gunicorn 可以与多个 Web 服务器或启动多个 Web 应用程序进行通信。它是一个强大且快速的 HTTP 服务器。

现在我们已经了解了部署流程以及一些关键概念和术语,我们将在下一项练习中一起进行部署。

练习 68:设置 Git 和 Heroku CLI

在这个练习中,我们将部署我们的 Smilecook 应用程序到生产环境。我们首先下载并安装 Heroku CLI 和 Git,以便我们可以在本地机器上运行部署命令。然后,我们将添加gitignore文件以确保某些文件不会被上传到 Heroku。最后,我们将main.pyProcfile添加到项目的根目录中,然后将其部署到 Heroku:

  1. devcenter.heroku.com/articles/heroku-cli 安装Heroku CLI。选择适合您操作系统的版本并下载它:![图 10.14:安装 Heroku CLI 图片

    图 10.14:安装 Heroku CLI
  2. 如果您还没有安装 Git,请从 git-scm.com/ 安装:图 10.15:安装 Git

    图 10.15:安装 Git
  3. 在 PyCharm 底部打开终端。运行 git --version 命令以确认 Git 已成功安装:

    $ git --version
     git version 2.19.1 // You may see a different value inside the brackets depending on your OS
    
  4. 右键点击在项目中创建一个 .gitignore 文件。此文件将包含我们不想添加到 Git 中的文件或文件夹列表:

    static/images/avatars/*
    static/images/recipes/*
    .idea/
    venv/
    

    static/images/avatars/* – 我们不希望将之前章节中创建的所有测试图像上传到生产环境。

    static/images/recipes/* – 我们不希望将之前章节中创建的所有测试图像上传到生产环境。

    .idea/ – 这是 IDE 项目特定设置文件夹。在生产环境中我们不需要它。

    venv/ – 这是虚拟环境。

  5. 登录您的 Heroku 账户:

    $ heroku login
    
  6. 然后,输入以下 git init 命令以初始化 Git。这是为了将版本控制添加到我们的项目中:

    $ git init
    
  7. 将 Heroku 仓库添加到 Git 远程仓库(请将 your-heroku-app 替换为您的 Heroku 应用名称)。

    $ heroku git:remote -a your-heroku-app
    

    注意

    在添加远程仓库之前,我们所有的更改只能提交到本地仓库。

  8. requirements.txt 文件中添加 gunicorn 包,它将成为我们的 HTTP 服务器:

    gunicorn==19.9.0
    
  9. 在项目根目录下创建 main.py。这将由 Gunicorn 执行以启动我们的 web 应用程序:

    from app import create_app
    app = create_app()
    
  10. 右键点击在项目根目录下创建一个文件。命名为 Procfile(不带扩展名),然后插入以下两个命令:

    release: flask db upgrade
    web: gunicorn main:app
    

    Procfile 文件是用于 Heroku 在应用程序启动过程中运行的。第一行是要求 Heroku 在每次部署后运行 flask db upgrade。这是为了确保我们的数据库模式始终保持最新。

    第二行是为了让 Heroku 识别它为启动 web 服务器的任务。

  11. 在 PyCharm 的 Python 控制台中运行 git add . 命令。这将把我们的源代码添加到 Git 中,用于版本控制和部署:

    $ git add .
    
  12. 运行 git commit 命令以提交我们的源代码。-a 参数告诉 Git 将已修改或删除的文件放入暂存区。-m 参数用于包含提交信息:

    $ git commit -am "first commit"
    
  13. 使用 git push 将源代码推送到 Heroku 仓库以部署应用程序:

    $ git push heroku master
    

    Heroku 将自动设置环境。我们可以看到以下输出:

    图 10.16:将应用程序部署到 Heroku

图 10.16:将应用程序部署到 Heroku

注意

在部署过程中,如果我们想了解更多关于幕后发生的事情,可以通过点击右上角的 更多 按钮,然后点击 查看日志 来检查应用程序日志。

图 10.17:将应用程序部署到 Heroku

图 10.17:将应用程序部署到 Heroku

从前面的日志中,我们可以看到数据库升级后,将运行 Gunicorn。最后,你可以看到消息 状态从启动变为运行

我们已成功将 Smilecook 应用程序部署到 Heroku,这意味着它已准备好为公众提供服务。稍后,我们将使用 Postman 进行测试。

注意

在未来,如果有新版本,我们只需要使用三个命令来重新部署应用程序。首先,使用 git add . 将我们的源代码添加到 Git 中,然后使用 git commit -am "make it better"。最后,使用 git push heroku master 将源代码推送到 Heroku。

练习 69:在 pgAdmin 中检查 Heroku Postgres 表

在上一个练习中,我们完成了部署。现在我们需要检查数据库中是否已创建了表。因此,在这个练习中,我们将使用 pgAdmin 连接到 Heroku Postgres:

  1. 获取 Heroku Postgres 数据库的凭据,转到 Add-ons > Settings,然后点击 View Credentials,你将看到以下屏幕:![图 10.18:获取 Heroku Postgres 数据库的凭据

    ![img/C15309_10_18.jpg]

    图 10.18:获取 Heroku Postgres 数据库的凭据
  2. Servers 上右键单击,然后在 pgAdmin 中创建新服务器:![图 10.19:在 pgAdmin 中创建新服务器

    ![img/C15309_10_19.jpg]

    图 10.19:在 pgAdmin 中创建新服务器
  3. General 选项卡中,将服务器命名为 Heroku:![图 10.20:在常规选项卡中输入服务器的名称

    ![img/C15309_10_20.jpg]

    图 10.20:在常规选项卡中输入服务器的名称
  4. Connection 选项卡中,输入凭据,包括 主机名/地址端口维护数据库用户名密码,然后点击 保存:![图 10.21:将凭据添加到连接选项卡

    ![img/C15309_10_21.jpg]

    图 10.21:将凭据添加到连接选项卡
  5. 现在,在 pgAdmin 中检查数据库表。转到 Heroku >> Databases >> (你的数据库名称) >> Schemas >> Public >> Tables 以验证此操作:![图 10.22:在 pgAdmin 中检查数据库表

    ![img/C15309_10_22.jpg]

图 10.22:在 pgAdmin 中检查数据库表

现在,我们可以查看数据库中是否已创建了表。如果你可以看到表已成功创建,我们可以继续到下一步,即使用 Postman 测试我们的 API。

在 Postman 中设置变量

我们已成功将项目部署到 Heroku。现在您可以使用我们之前设置的保存请求在 Postman 中测试它们。然而,我们之前在 Postman 中保存的请求都是针对 localhost 运行的。我们不必逐个更改 URL 到生产 URL,我们可以利用 Postman 中的变量。我们可以在 Postman 中设置一个url变量,并将生产 URL 分配给它,然后从保存的请求中将 URL 替换为{{url}}。然后 Postman 将动态地为我们替换{{url}}为生产 URL。

练习 70:在 Postman 中设置变量

在这个练习中,我们将设置 Postman 中的变量,以便根据环境动态地结合适当的值。我们将设置 URL 作为一个变量,这样当我们处于开发环境测试时,我们只需更改 URL 变量为http://localhost:5000。如果我们处于生产环境测试,我们可以将其更改为https://your_heroku_app.herokuapp.com

  1. 将环境名称设置为Smilecook。然后,创建一个名为url的变量,其值为https://your_heroku_app.herokuapp.com。如果当前值未设置,它将自动采用初始值。请将your_heroku_app替换为您的 Heroku 应用名称,然后点击更新图 10.23:在 Postman 中添加环境变量

    图 10.23:在 Postman 中添加环境变量
  2. 一旦添加,通过点击右上角的眼睛图标来验证变量:图 10.24:在 Postman 中验证环境变量

    图 10.24:在 Postman 中验证环境变量
  3. UserList请求中,将 URL 更新为{{url}}/users,然后在请求发送时点击https://your_heroku_app.herokuapp.com/users):图 10.25:在 URL 中使用环境变量

图 10.25:在 URL 中使用环境变量

Postman 是一个非常强大的测试工具。它甚至允许我们有效地在不同的环境中测试我们的 API 端点。在未来,如果您想在生产环境中测试其他 API 端点,您只需更改之前保存的请求中的 URL。在下一个活动中,我们将测试您的这一知识。

活动 18:在 Postman 中将 access_token 更改为变量

在上一个练习中,您学习了如何将 URL 更改为变量。在这个活动中,我们希望您对access_token也做同样的操作:

  1. 通过使用之前保存的POST Token请求来获取访问令牌。

  2. 在 Postman 中将access_token作为一个变量添加。

  3. 测试一个需要访问令牌的 Smilecook API 端点。

    注意

    本活动的解决方案可以在第 345 页找到。

太棒了。当你完成这个活动时,这意味着你已经部署并测试了 Smilecook API 的生产环境。这是本书的最后一项活动,我们很高兴你走到了这一步!

现在,我们将设置 Smilecook 前端网站,它将使用你刚刚开发的 API。

设置前端界面以与 Smilecook API 一起工作

请从 github.com/TrainingByPackt/Python-API-Development-Fundamentals/tree/master/Lesson10/Frontend 下载包含前端网站源代码的 smilecook-vuejs 文件夹。

  1. 在 Heroku 平台上创建一个新应用,用于部署我们的前端网页界面:![图 10.26:在 Heroku 平台上创建新应用

    ![img/C15309_10_26.jpg]

    图 10.26:在 Heroku 平台上创建新应用
  2. 应用程序创建后,我们转到 设置 选项卡,然后是 配置变量。在这里,我们将设置一个环境变量,该变量将用于存储后端 API URL:![图 10.27:设置环境变量

    ![img/C15309_10_27.jpg]

    图 10.27:设置环境变量
  3. 将变量名设置为 VUE_APP_API_URL,并将后端 Smilecook API URL 插入此处。

  4. 在 PyCharm 中打开 smilecook-vuejs 项目。

  5. 在 PyCharm 控制台中,输入以下命令以登录到 Heroku CLI:

    $ heroku login
    
  6. 然后,初始化 git 并将 Heroku 仓库添加到 git:remote 仓库:

    $ git init
    $ heroku git:remote -a your_heroku_app_name
    
  7. 然后,将源代码添加到 git 中,提交,并将它们推送到 Heroku。

    $ git add .
    $ git commit -am "make it better"
    $ git push heroku master
    
  8. 部署完成后,你应该在屏幕上看到以下信息:

    remote: -----> Compressing...
    remote:        Done: 30M
    remote: -----> Launching...
    remote:        Released v1
    remote:        https://your_heroku_app_name.herokuapp.com/ deployed to Heroku
    remote: 
    remote: Verifying deploy... done.
    To https://git.heroku.com/your_heroku_app_name.git
       59c4f7f..57c0642  master -> master
    
  9. 在浏览器中输入 https://your_heroku_app_name.herokuapp.com/;我们可以看到前端界面已成功设置:![图 10.28:前端设置成功

    ![img/C15309_10_28.jpg]

图 10.28:前端设置成功

现在,你可以使用这个前端网站界面与 Smilecook API 进行交互。

摘要

在本章中,我们成功地将 Smilecook API 部署到了 Heroku 云服务器。由于我们利用了 Heroku 提供的服务,部署过程非常简单。我们不需要担心购买硬件、设置服务器操作系统、将服务器连接到互联网等等。一切都有 Heroku 提供。云平台服务可以快速帮助开发者将他们的应用程序/API 部署到互联网上。这种简单的部署过程允许开发者专注于开发,而不是基础设施/平台设置。一旦 API 部署完成,互联网上的数百万用户可以通过他们的客户端应用程序连接到该 API。

当然,Heroku 只是众多云服务中的一种。至于应该选择哪种云服务,您应该考虑诸如成本、提供的附加服务和我们应用程序的规模等重要因素。我们不会限制您使用特定的平台。实际上,我们希望这本书能成为您作为专业开发者的旅程的起点。凭借您所学的基础知识,您应该能够探索和进一步发展新技能,并使用新工具构建更高级的 API。

恭喜!我们已经完成了整本书。您不仅学习了 API 是什么,而且还自己开发和部署了一个真实的 API 服务,Smilecook。在整个书中,您学习了如何设置开发环境、构建 API、与数据库交互、对象序列化、安全令牌、与第三方 API 交互、缓存,以及最终的部署。我们横向覆盖了许多不同的主题,同时也深入探讨了每个主题。除了学习理论外,您还在练习和活动中进行了实际的编码,并对您的作品进行了彻底的测试。

您的下一步应该包括通过参与开发项目来继续学习。最重要的是要有实际的开发经验,并保持好奇心。每当遇到问题时,都要寻找更好的解决方案。您不应该只满足于完成任务。相反,您应该致力于把事情做对。这正是将您带到下一个层次的关键。

我们希望您与我们一同享受了学习之旅。谢谢!

附录

关于

本节包含帮助学生执行书中活动的说明。它包括学生为实现活动目标需要执行的详细步骤。

1: 第一步

活动 1:使用 Postman 向我们的 API 发送请求

解决方案

  1. 首先,我们将获取所有食谱。在下拉列表中选择我们的HTTP方法为GET

  2. 输入请求 URL http://localhost:5000/recipes

  3. 点击Send按钮。结果可以在下述屏幕截图查看:![图 1.14:获取所有食谱 图片

    图 1.14:获取所有食谱

    在 HTTP 响应中,你将在响应面板右上角看到 HTTP 状态200 OK。这意味着请求已成功。旁边的显示7ms,这是请求花费的时间。响应的大小,包括头和体,是322字节。食谱的详细信息以 JSON 格式显示在 Body 面板中。

  4. 接下来,我们将使用 POST 方法创建一个食谱。我们将发送 HTTP http://localhost:5000/recipes

  5. 通过点击http://localhost:5000/recipes作为请求 URL,在 Get 请求标签旁边创建一个新标签页。

  6. 选择Body标签页。同时,选择raw单选按钮。

  7. 在右侧下拉菜单中选择JSON (application/json)。在Body内容区域以 JSON 格式输入以下数据。点击Send按钮:

    {
         "name": "Cheese Pizza",
         "description": "This is a lovely cheese pizza"
    }
    

    结果显示在下述屏幕截图:

    ![图 1.15:创建食谱 图片

    图 1.15:创建食谱

    你应该在 Postman 界面中的 HTTP 响应中看到以下信息,状态201 OK,表示创建成功,我们可以看到我们的新食谱以 JSON 格式显示。你还会注意到分配给食谱的 ID 是3

  8. 现在,再次从服务器应用程序获取所有食谱。我们想看看现在是否有三个食谱。在历史面板中,选择我们之前获取所有食谱的请求,点击它,并重新发送。

    响应中,我们可以看到有三个食谱。它们显示在下述屏幕截图:

    ![图 1.16:从服务器应用程序获取所有食谱 图片

    图 1.16:从服务器应用程序获取所有食谱
  9. 然后,修改我们刚刚创建的食谱。为此,通过点击+按钮在Get请求标签旁边创建一个新标签页。选择PUT作为 HTTP 方法。

  10. http://localhost:5000/recipes/3作为请求 URL 输入。

  11. 选择Body标签页,然后选择raw单选按钮。

  12. 在右侧下拉菜单中选择JSON (application/json)。在Body内容区域以 JSON 格式输入以下数据。点击Send

    {
    "name": "Lovely Cheese Pizza",
    "description": "This is a lovely cheese pizza recipe."
    }
    

    结果显示在下述屏幕截图:

    ![图 1.17:修改食谱 图片

    图 1.17:修改食谱

    在 HTTP 响应中,您将看到200 OK的 HTTP 状态,表示更新已成功。您还可以看到请求花费的时间(以毫秒为单位)。您还应看到响应的大小(头和体)。响应内容以 JSON 格式。我们可以在 JSON 格式中看到我们的更新后的食谱。

  13. 接下来,我们将看看是否可以使用其 ID 来查找食谱。我们只想在响应中看到 ID 为3的食谱。为此,通过点击+按钮在获取请求标签旁边创建一个新标签页。

  14. 将请求 URL 选择为http://localhost:5000/recipes/3

  15. 点击发送。结果如下截图所示:![图 1.18:查找具有 ID 的食谱 图 1.18:查找具有 ID 的食谱

    图 1.18:查找具有 ID 的食谱

    我们可以在响应中看到只返回了 ID 为3的食谱。它包含了我们刚刚设置的修改后的详细信息。

  16. 当我们搜索一个不存在的食谱时,我们将看到以下响应,其中包含消息http://localhost:5000/recipes/101端点。结果如下截图所示:![图 1.19:显示“食谱未找到”的响应 图 1.19:显示“食谱未找到”的响应

图 1.19:显示“食谱未找到”的响应

活动二:实现和测试 delete_recipe 函数

解决方案

  1. delete_recipe函数从内存中删除食谱。使用recipe = next((recipe for recipe in recipes if recipe['id'] == recipe_id), None)获取具有特定 ID 的食谱:

    @app.route('/recipes/<int:recipe_id>', methods=['DELETE'])
    def delete_recipe(recipe_id):
        recipe = next((recipe for recipe in recipes if recipe['id'] == recipe_id), None)
        if not recipe:
            return jsonify({'message': 'recipe not found'}), HTTPStatus.NOT_FOUND
        recipes.remove(recipe)
        return '', HTTPStatus.NO_CONTENT
    
  2. 与之前显示的update_recipe函数类似,如果您找不到食谱,则返回与 HTTP 状态NOT_FOUND一起的"recipe not found"。否则,我们将继续从我们的食谱集合中删除具有给定 ID 的食谱,HTTP 状态为204 No Content

  3. 代码完成后,在app.py文件上右键单击并点击运行以启动应用程序。Flask 服务器将启动,我们的应用程序准备进行测试。

  4. 使用 httpie 或 curl 删除 ID 为1的食谱:

    http DELETE localhost:5000/recipes/1
    

    以下是与之前相同的命令的curl版本。

    curl -i -X DELETE localhost:5000/recipes/1
    

    @app.route('/recipes/<int:recipe_id>', methods=['DELETE'])路由将捕获客户端请求并调用delete_recipe(recipe_id)函数。该函数将查找具有recipe_id ID 的食谱,如果找到,则将其删除。响应中我们可以看到删除操作已成功。并且我们看到 HTTP 状态是204 NO CONTENT

    HTTP/1.0 204 NO CONTENT
    Content-Type: text/html; charset=utf-8
    Date: Fri, 06 Sep 2019 05:57:50 GMT
    Server: Werkzeug/0.15.6 Python/3.7.0
    
  5. 最后,使用 Postman 删除 ID 为2的食谱。为此,通过点击+按钮在获取请求标签旁边创建一个新标签页。

  6. 选择HTTP方法。输入http://localhost:5000/recipes/2作为请求 URL。

  7. 点击发送。结果如下截图所示:![图 1.20:删除食谱 图 1.20:删除食谱

图 1.20:删除食谱

然后,我们可以看到带有 HTTP 状态204 NO CONTENT的响应。这意味着食谱已被成功删除。

2:开始构建我们的项目

活动 3:使用 Postman 测试 API

解决方案

  1. 首先,构建一个客户端请求,请求一个新的食谱。然后,利用 Postman 中的集合功能使测试更高效。

  2. 点击集合标签页,然后通过点击+创建一个新的集合。

  3. 输入Smilecook作为名称并点击创建

  4. Smilecook旁边的...右键单击,在Smilecook下创建一个新的文件夹,并在名称字段中输入Recipe

  5. 食谱右键单击以创建一个新的请求。然后,将名称设置为RecipeList,并将其保存到食谱集合下。

  6. 在请求 URL 字段中选择http://localhost:5000/recipes

  7. 现在,转到body字段:

    {
        "name": "Cheese Pizza",
        "description": "This is a lovely cheese pizza",
        "num_of_servings": 2,
        "cook_time": 30,
        "directions": "This is how you make it" 
    }
    
  8. 保存并发送食谱。结果如下截图所示:![图 2.10:通过发送 JSON 格式的详细信息创建我们的第一个食谱 图片

    图 2.10:通过发送 JSON 格式的详细信息创建我们的第一个食谱

    在 HTTP 响应中,您将看到 HTTP 状态201 已创建,表示请求成功,并且在正文中,您应该看到我们刚刚创建的相同食谱。食谱的 ID 应该是 1。

  9. 通过发送客户端请求创建第二个食谱。接下来,我们将通过以下 JSON 格式的详细信息创建第二个食谱:

    { 
        "name": "Tomato Pasta",
        "description": "This is a lovely tomato pasta recipe",
        "num_of_servings": 3,
        "cook_time": 20,
        "directions": "This is how you make it" 
    }
    
  10. 点击发送。结果如下截图所示:![图 2.11:通过发送 JSON 格式的详细信息创建我们的第二个食谱 图片

    图 2.11:通过发送 JSON 格式的详细信息创建我们的第二个食谱

    在 HTTP 响应中,您将看到 HTTP 状态201 已创建,表示请求成功,并且在正文中,您应该看到我们刚刚创建的相同食谱。食谱的 ID 应该是 2。

    到目前为止,我们已经创建了两个食谱。让我们使用 Postman 检索这些食谱,并确认这两个食谱是否在应用程序内存中。

  11. 食谱文件夹下创建一个新的请求,命名为RecipeList,然后保存。

  12. 选择我们刚刚创建的RecipeList(HTTP 方法设置为 GET)。

  13. 在请求 URL 中输入http://localhost:5000/recipes。然后,点击ID = 1以发布。

  14. 食谱文件夹下创建一个新的请求,命名为RecipePublish,然后保存。

  15. 点击我们刚刚创建的RecipePublish请求(HTTP 方法设置为 GET)。

  16. 在请求 URL 中选择http://localhost:5000/recipes/1/publish。然后,点击保存并发送请求。结果如下截图所示:![图 2.13:检索已发布的食谱 图片

    图 2.13:检索已发布的食谱

    在 HTTP 响应中,您将看到 HTTP 状态204 无内容,表示请求已成功发布,并且响应正文中没有返回数据。

  17. 再次使用 Postman 获取所有食谱。从左侧面板中选择 RecipeList (GET) 并发送请求。结果如下截图所示:图 2.14:使用 Postman 获取所有食谱

    ](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-api-dev-fund/img/C15309_02_14.jpg)

    图 2.14:使用 Postman 获取所有食谱

    在 HTTP 响应中,您将看到 localhost:5000/recipes/1

  18. 在请求 URL 下的 http://localhost:5000/recipes/1 下创建一个新的请求。

  19. 现在,转到 主体 选项卡,选择原始,从下拉菜单中选择 JSON (application/json),并将以下代码插入到主体字段中。这是修改后的食谱:

    {
        "name": "Lovely Cheese Pizza",
        "description": "This is a lovely cheese pizza recipe",
        "num_of_servings": 3,
        "cook_time": 60,
        "directions": "This is how you make it"
    }
    
  20. 保存并发送它。结果如下截图所示:图 2.15:修改 ID 为 1 的食谱

    ](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-api-dev-fund/img/C15309_02_15.jpg)

    图 2.15:修改 ID 为 1 的食谱

    在 HTTP 响应中,您将看到 HTTP 状态 200 OK,表示修改成功。正文应包含以 JSON 格式更新的食谱 1 的详细信息。我们将检索 ID 为 1 的食谱。

  21. 在请求 URL 下的 http://localhost:5000/recipes/1 下创建一个新的请求。

  22. 保存并发送它。结果如下截图所示:图 2.16:检索 ID 为 1 的食谱

    ](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-api-dev-fund/img/C15309_02_16.jpg)

图 2.16:检索 ID 为 1 的食谱

在 HTTP 响应中,您将看到以 JSON 格式的 recipe 1

活动 4:实现删除食谱功能

解决方案

  1. delete 函数添加到 RecipeResource。通过以下示例代码实现 delete 方法:

        def delete(self, recipe_id):
            recipe = next((recipe for recipe in recipe_list if recipe.id == recipe_id), None)
            if recipe is None:
                return {'message': 'recipe not found'}, HTTPStatus.NOT_FOUND
            recipe_list.remove(recipe)
            return {}, HTTPStatus.NO_CONTENT
    

    在这里我们构建的第三个方法已被删除。我们通过定位具有相应食谱 ID 的食谱并将其从食谱列表中删除来实现这一点。最后,我们返回 HTTP 状态 204 无内容

  2. 右键单击 app.py 文件并单击 运行 以启动应用程序。Flask 服务器将启动,我们的应用程序将准备好测试。现在,使用 Postman 创建第一个食谱。我们将构建一个客户端请求,请求一个新的食谱。

  3. 首先,选择 RecipeList POST 请求。现在,通过单击以下截图所示的 发送 按钮发送请求:图 2.17:使用 Postman 创建第一个食谱

    ](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-api-dev-fund/img/C15309_02_17.jpg)

    图 2.17:使用 Postman 创建第一个食谱
  4. 现在,我们将使用 Postman 删除一个食谱。为此,删除 ID 为 1 的食谱。

  5. Recipe 文件夹下创建一个新的请求。然后,将 请求名称 设置为 Recipe保存

  6. HTTP 方法更改为 DELETE 并在请求 URL 中输入 http://localhost:5000/recipes/1。然后,保存并发送请求。结果如下截图所示:图 2.18:使用 Postman 删除食谱

    ](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-api-dev-fund/img/C15309_02_18.jpg)

图 2.18:使用 Postman 删除食谱

在 HTTP 响应中,您将看到 RecipeResource 类在此活动中的状态:

图 2.19:为 RecipeResource 类构建的方法

](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/py-api-dev-fund/img/C15309_02_19.jpg)

![图 2.19:为 RecipeResource 类构建的方法

3:使用 SQLAlchemy 操作数据库

活动五:创建用户和菜谱

解决方案

  1. 在 PyCharm 底部的 Python 控制台中输入以下代码以导入必要的模块和类:

    from app import *
    from models.user import User
    from models.recipe import Recipe
    app = create_app()
    
  2. 在 Python 控制台中输入以下代码创建一个 user 对象并将其保存到数据库中:

    user = User(username='peter', email='peter@gmail.com', password='WkQa')
    db.session.add(user)
    db.session.commit()
    
  3. 接下来,我们将使用以下代码创建两个菜谱。需要注意的是,菜谱的 user_id 属性被设置为 user.id。这是为了表明菜谱是由用户 Peter 创建的:

    carbonara = Recipe(name='Carbonara', description='This is a lovely carbonara recipe', num_of_servings=4, cook_time=50, directions='This is how you make it', user_id=user.id)
    db.session.add(carbonara)
    db.session.commit()
    risotto = Recipe(name='Risotto', description='This is a lovely risotto recipe', num_of_servings=5, cook_time=40, directions='This is how you make it', user_id=user.id)
    db.session.add(risotto)
    db.session.commit()
    
  4. 我们可以在 user 表中看到一条新记录:![图 3.18:用户表中的新记录

    ![图片 C15309_03_18.jpg]

    ![图 3.18:用户表中的新记录
  5. 我们将检查两个菜谱是否已在数据库中创建![图 3.19:检查两个菜谱是否已创建

    ![图片 C15309_03_19.jpg]

    :

![图 3.19:检查两个菜谱是否已创建

活动六:升级和降级数据库

解决方案

  1. user 类添加一个新属性:

    bio= db.Column(db.String())
    
  2. 现在,运行 flask db migrate 命令来创建数据库和表:

    flask db migrate
    

    Flask-Migrate 检测到新列并为此创建了脚本:

    INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
    INFO  [alembic.runtime.migration] Will assume transactional DDL.
    INFO  [alembic.ddl.postgresql] Detected sequence named 'user_id_seq' as owned by integer column 'user(id)', assuming SERIAL and omitting
    INFO  [alembic.ddl.postgresql] Detected sequence named 'recipe_id_seq' as owned by integer column 'recipe(id)', assuming SERIAL and omitting
    INFO  [alembic.autogenerate.compare] Detected added column 'user.bio'
      Generating /Python-API-Development-Fundamentals/smilecook/migrations/versions/6971bd62ec60_.py ... done
    
  3. 现在,检查 versions 文件夹下的 /migrations/versions/6971bd62ec60_.py。此文件由 Flask-Migrate 创建。请注意,您可能在这里获得不同的修订 ID。请在运行 flask db upgrade 命令之前检查该文件。这是因为有时它可能无法检测到您对模型所做的每个更改:

    """empty message
    
    Revision ID: 6971bd62ec60
    Revises: 1b69a78087e5
    Create Date: 2019-10-08 12:11:47.370082
    
    """
    from alembic import op
    import sqlalchemy as sa
    
    # revision identifiers, used by Alembic.
    revision = '6971bd62ec60'
    down_revision = '1b69a78087e5'
    branch_labels = None
    depends_on = None
    
    def upgrade():
        # ### commands auto generated by Alembic - please adjust! ###
        op.add_column('user', sa.Column('bio', sa.String(), nullable=True))
        # ### end Alembic commands ###
    
    def downgrade():
        # ### commands auto generated by Alembic - please adjust! ###
        op.drop_column('user', 'bio')
        # ### end Alembic commands ###
    

    在这个自动生成的文件中有两个函数;一个用于升级,这是为了将新的菜谱和用户添加到表中,另一个用于降级,即回到之前的版本。

  4. 然后,我们将执行 flask db upgrade 命令,这将使我们的数据库升级以符合模型中的最新规范:

    flask db upgrade
    

    此命令将调用 upgrade() 来升级数据库:

    INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
    INFO  [alembic.runtime.migration] Will assume transactional DDL.
    INFO  [alembic.runtime.migration] Running upgrade a6d248ab7b23 -> 6971bd62ec60, empty message
    
  5. 检查新字段是否已在数据库中创建。转到 smilecook >> Schemas >> Tables >> user >> Properties 进行验证:![图 3.20:检查新字段是否已在数据库中创建

    ![图片 C15309_03_20.jpg]

![图 3.20:检查新字段是否已在数据库中创建

运行 downgrade 命令删除新字段:

flask db downgrade

此命令将调用 downgrade() 来降级数据库:

INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.runtime.migration] Running downgrade 6971bd62ec60 -> a6d248ab7b23, empty message

检查字段是否已被删除。转到 smilecookSchemasTablesuserProperties 进行验证:

![图 3.21:检查字段是否已从数据库中删除

![图片 C15309_03_21.jpg]

![图 3.21:检查字段是否已从数据库中删除## 4:使用 JWTs 进行认证服务和安全性### 活动七:在发布/取消发布菜谱功能上实现访问控制解决方案1. 修改RecipePublishResource中的put方法,以限制只有认证用户才能访问。在resources/token.py中,在RecipePublishResource.put方法上方添加@jwt_required装饰器。使用get_jwt_identity()函数来识别认证用户是否是食谱的所有者: py     @jwt_required     def put(self, recipe_id):         recipe = Recipe.get_by_id(recipe_id=recipe_id)         if recipe is None:             return {'message': 'Recipe not found'}, HTTPStatus.NOT_FOUND         current_user = get_jwt_identity()         if current_user != recipe.user_id:             return {'message': 'Access is not allowed'}, HTTPStatus.FORBIDDEN         recipe.is_publish = True         recipe.save()         return {}, HTTPStatus.NO_CONTENT 这是为了发布食谱。只有已登录的用户可以发布他们自己的食谱。该方法将执行各种检查以确保用户有发布权限。一旦食谱发布,它将返回204 NO_CONTENT。1. 修改RecipePublishResource中的delete方法。只有认证用户才能取消发布食谱: py @jwt_required     def delete(self, recipe_id):         recipe = Recipe.get_by_id(recipe_id=recipe_id)         if recipe is None:             return {'message': 'Recipe not found'}, HTTPStatus.NOT_FOUND         current_user = get_jwt_identity()         if current_user != recipe.user_id:             return {'message': 'Access is not allowed'}, HTTPStatus.FORBIDDEN         recipe.is_publish = False         recipe.save()         return {}, HTTPStatus.NO_CONTENT 这将取消发布食谱。类似于之前的代码,只有已登录的用户可以取消发布他们自己的食谱。一旦食谱发布,它将返回状态码 204 NO_CONTENT。1. 登录用户账户并获取访问令牌。选择我们之前创建的POST令牌请求。1. 选择raw单选按钮,并从下拉菜单中选择JSON (application/json)。在Body字段中输入以下 JSON 内容: py {     "email": "james@gmail.com",     "password": "WkQad19" } 1. 点击Send以登录账户。结果如下所示:图 4.20:登录用户账户
###### 图 4.20:登录用户账户

您将看到 HTTP **状态码** **200 OK**,表示登录成功。我们可以在响应体中看到**访问令牌**和**刷新令牌**。
  1. 在用户登录状态下发布id = 3的食谱。选择PUT RecipePublish

  2. 前往VALUE字段中的Bearer {token},其中 token 是我们之前步骤中获得的 JWT 令牌。

  3. 点击Send以发布食谱。结果如下所示:图 4.21:发布食谱

    图 4.21:发布食谱

    然后,您将看到响应,HTTP 状态码 204表示食谱已成功发布。

    最后,尝试获取所有已发布的食谱。选择GET RecipeList请求,然后点击Send以获取所有已发布食谱的详细信息。结果如下所示:

    图 4.22:检索所有已发布的食谱

    图 4.22:检索所有已发布的食谱

    然后,您将看到响应,HTTP 状态码 200表示请求成功,您可以看到我们创建的一个已发布的食谱被返回。

  4. 在用户登录状态下取消发布id = 3的食谱。在Recipe文件夹下创建一个新的请求,命名为RecipePublish,然后保存。

  5. 点击我们刚刚创建的RecipePublish请求(HTTP 方法设置为GET)。

  6. 在请求 URL 中选择http://localhost:5000/recipes/3/publish

  7. 前往VALUE字段中的Bearer {token},其中 token 是我们第 5 步中获得的 JWT 令牌。

  8. 保存发送 取消发布的请求。结果如下所示:![图 4.23:取消发布菜谱

    ![图片 C15309_04_23.jpg]

图 4.23:取消发布菜谱

5:使用 marshmallow 验证 API

活动八:使用 marshmallow 序列化菜谱对象

解决方案

  1. 修改菜谱模式以包含除 email 之外的所有属性。在 schemas/recipe.py 中,将 only=['id', 'username'] 修改为 exclude=('email', )。这样,我们将显示除用户的电子邮件地址之外的所有内容。此外,如果我们将来为 recipe 对象添加新的属性(例如,user avatar URL),我们就不需要再次修改模式,因为它将显示所有内容:

         author = fields.Nested(UserSchema, attribute='user', dump_only=True, exclude=('email', ))
    
  2. 修改 RecipeResource 中的 get 方法,使用菜谱模式将 recipe 对象序列化为 JSON 格式:

            return recipe_schema.dump(recipe).data, HTTPStatus.OK
    

    这主要是为了修改代码以使用 recipe_schema.dump(recipe).data 通过菜谱模式返回菜谱详情。

  3. 右键单击以运行应用程序。Flask 将启动并在本地主机(127.0.0.1)的端口 5000 上运行:![图 5.18:在本地主机上运行 Flask

    ![图片 C15309_05_18.jpg]

    图 5.18:在本地主机上运行 Flask
  4. 通过 Postman 获取一个特定的已发布菜谱来测试实现。在 输入请求 URL 中选择 http://localhost:5000/recipes/4。点击 发送 以获取特定的菜谱详情。结果如下所示:![图 5.19:选择 GET 菜谱请求并发送请求

    ![图片 C15309_05_19.jpg]

图 5.19:选择 GET 菜谱请求并发送请求

你将看到返回的响应。HTTP 状态码 created_at

6:电子邮件确认

活动九:测试完整的用户注册和激活工作流程

解决方案

  1. 我们将首先通过 Postman 注册一个新用户。点击 集合 选项卡并选择 POST UserList 请求。

  2. 选择 主体 选项卡,然后选择 原始 单选按钮,并从下拉列表中选择 JSON (application/json)

  3. 主体 字段中输入以下用户详情(JSON 格式)。将用户名和密码更改为适当的值:

    {
        "username": "john",
        "email": "smilecook.api@gmail.com",
        "password": "Kwq2z5"
    }
    
  4. 发送请求。你应该看到以下输出:![图 6.10:通过 Postman 注册用户

    ![图片 C15309_06_10.jpg]

    图 6.10:通过 Postman 注册用户

    你应该在响应中看到新的用户详情(ID = 4),HTTP 状态为 201 OK。这意味着新用户在后台已成功创建。

  5. 通过 API 登录并点击 集合 选项卡。然后,选择我们之前创建的 POST Token 请求。

  6. 现在,点击 主体 选项卡。检查 原始 单选按钮,并从下拉菜单中选择 JSON(application/json)

  7. 主体 字段中输入以下 JSON 内容(电子邮件和密码):

    {
        "email": "smilecook.api@gmail.com",
        "password": "Kwq2z5"
    }
    
  8. 发送请求。你应该看到以下输出:![图 6.11:使用 JSON 发送请求

    ![图片 C15309_06_11.jpg]

    图 6.11:使用 JSON 发送请求

    您应该收到一条消息,说明用户账户尚未激活,HTTP 状态为 403 禁止。这是预期行为,因为我们的应用程序会要求用户首先激活账户。

  9. 请检查您的邮箱以获取激活邮件。那里应该有一个链接供您激活用户账户。点击该链接以激活账户。它应该看起来如下:图 6.12:激活邮件

    图 6.12:激活邮件
  10. 账户激活后,请重新登录。点击收藏标签页。

  11. 选择我们之前创建的 POST Token 请求并发送请求。您将看到以下内容:图 6.13:激活账户后,选择 POST 令牌请求

图 6.13:激活账户后,选择 POST 令牌请求

您应该在响应中看到访问令牌和刷新令牌,HTTP 状态为 200 OK。这意味着登录成功。

活动 10:创建 HTML 格式用户账户激活邮件

解决方案

  1. 点击 Mailgun 控制台,然后在右侧将我们新用户的电子邮件添加到授权收件人列表中。Mailgun 将然后向该电子邮件地址发送确认邮件:图 6.14:向我们的新用户发送确认邮件

    图 6.14:向我们的新用户发送确认邮件

    注意

    由于我们使用的是 Mailgun 的沙盒版本,向外部电子邮件地址发送电子邮件有限制。这些电子邮件必须首先添加到授权收件人列表中。

  2. 检查新用户的邮箱,并点击我同意。这将在以下屏幕截图中显示:图 6.15:新用户邮箱中的 Mailgun 邮件

    图 6.15:新用户邮箱中的 Mailgun 邮件
  3. 在确认页面上,点击以激活账户。屏幕将显示如下:图 6.16:激活完成消息

    图 6.16:激活完成消息
  4. Mailgun 默认提供 HTML 模板代码。我们可以在发送 > 模板下找到它。在那里,点击创建消息模板并选择操作模板。我们将找到一个确认邮件模板并预览它:图 6.17:预览确认邮件地址模板

    图 6.17:预览确认邮件地址模板
  5. 然后,在我们项目的templates文件夹下创建一个templates文件夹。从现在起,我们将把所有的 HTML 模板放在这个文件夹中。在templates文件夹内部,为与电子邮件相关的 HTML 模板创建一个子文件夹,email

  6. 现在,创建一个模板文件,confirmation.html,并将 Mailgun步骤 4 中的示例 HTML 代码粘贴进去。看看以下 Mailgun 的示例 HTML 代码:图 6.18:来自 Mailgun 的示例 HTML 代码

    图 6.18:来自 Mailgun 的示例 HTML 代码

    注意

    请注意,我们需要将www.mailgun.com链接更改为{{link}}。此占位符将被程序性地替换为账户激活链接。

  7. resources/user.py中通过输入以下代码行从 Flask 导入render_template函数:

    from flask import request, url_for, render_template
    
  8. send_mail方法中。可以使用render_template函数渲染 HTML 代码。你可以看到这里的link = link参数是为了将 HTML 模板中的{{link}}占位符替换为实际的账户验证链接:

    mailgun.send_email(to=user.email,
                                     subject=subject,
                                     text=text,
                                     html=render_template('email/confirmation.html', link=link))
    
  9. 使用 Postman 注册新账户:

    {
        "username": "emily",
        "email": "smilecook.user@gmail.com",
        "password": "Wqb6g2"
    }
    

    备注

    请注意,在Mailgun中事先验证了电子邮件地址。

    输出将如下所示:

    ![图 6.19:使用 Postman 注册新账户 图片

    图 6.19:使用 Postman 注册新账户
  10. 账户激活邮件将以 HTML 格式接收。输出如下截图所示:![图 6.20:账户确认邮件 图片

图 6.20:账户确认邮件

7:处理图像

活动 11:实现食谱封面图像上传功能

解决方案

  1. models/recipe.py模型中添加cover_image属性:

    cover_image = db.Column(db.String(100), default=None)
    

    cover_image属性将包含图像文件名作为字符串,最大长度为 100 个字符。

  2. 使用flask db migrate命令生成数据库表更新脚本:

    flask db migrate
    

    你将看到检测到一个新列,'recipe.cover_image'

    INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
    INFO  [alembic.runtime.migration] Will assume transactional DDL.
    INFO  [alembic.autogenerate.compare] Detected added column 'recipe.cover_image'
      Generating /TrainingByPackt/Python-API-Development-Fundamentals/Lesson07/smilecook/migrations/versions/91c7dc71b826_.py ... done
    
  3. /migrations/versions/xxxxxxxxxx_.py检查脚本:

    """empty message
    Revision ID: 91c7dc71b826
    Revises: 7aafe51af016
    Create Date: 2019-09-22 12:06:36.061632
    """
    from alembic import op
    import sqlalchemy as sa
    # revision identifiers, used by Alembic.
    revision = '91c7dc71b826'
    down_revision = '7aafe51af016'
    branch_labels = None
    depends_on = None
    def upgrade():
        # ### commands auto generated by Alembic - please adjust! ###
        op.add_column('recipe', sa.Column('cover_image', sa.String(length=100), nullable=True))
        # ### end Alembic commands ###
    def downgrade():
        # ### commands auto generated by Alembic - please adjust! ###
        op.drop_column('recipe', 'cover_image')
        # ### end Alembic commands ###
    

    从其内容中,我们可以看到脚本中生成了两个函数。upgrade函数用于将新的cover_image列添加到数据库表中,而downgrade函数用于删除cover_image列,使其恢复到原始状态。

  4. 运行flask db upgrade命令以更新数据库并反映User模型中的更改:

    flask db upgrade
    

    运行上述命令后,我们应该看到以下输出:

    INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
    INFO  [alembic.runtime.migration] Will assume transactional DDL.
    INFO  [alembic.runtime.migration] Running upgrade 7aafe51af016 -> 91c7dc71b826, empty message
    
  5. 在 pgAdmin 中检查新的cover_image列:![图 7.10:pgAdmin 中的 cover_image 列 图片

    图 7.10:pgAdmin 中的 cover_image 列

    这确认了新的cover_image列已添加到食谱表中。

  6. schemas/recipe.py中,导入url_for包并添加cover_url属性和dump_cover_url方法:

    from flask import url_for
        cover_url = fields.Method(serialize='dump_cover_url')
        def dump_cover_url(self, recipe):
            if recipe.cover_image:
                return url_for('static', filename='images/recipes/{}'.format(recipe.cover_image), _external=True)
            else:
                return url_for('static', filename='images/assets/default-recipe-cover.jpg', _external=True)
    

    default-recipe-cover.jpg图像添加到static/images

    ![图 7.11:添加 default-recipe-cover.jpg 后的文件夹结构 图片

    图 7.11:添加default-recipe-cover.jpg后的文件夹结构
  7. resources/recipe.py中,添加导入osimage_setsave_image函数:

    import os
    from extensions import image_set
    from utils import save_image
    In resources/recipe.py, add recipe_cover_schema, which just shows the cover_url column:
    recipe_cover_schema = RecipeSchema(only=('cover_url', ))
    
  8. resources/recipe.py中,添加RecipeCoverUpload资源以将食谱封面上传到食谱文件夹:

        class RecipeCoverUploadResource(Resource):
            @jwt_required
            def put(self, recipe_id):
                file = request.files.get('cover')
                if not file:
                    return {'message': 'Not a valid image'}, HTTPStatus.BAD_REQUEST
                if not image_set.file_allowed(file, file.filename):
                    return {'message': 'File type not allowed'}, HTTPStatus.BAD_REQUEST
    

    PUT方法之前的@jwt_required装饰器表示该方法只能在用户登录后调用。在PUT方法中,我们试图在request.files中获取封面图片文件。然后,我们试图验证它是否存在以及文件扩展名是否允许。

  9. 之后,我们使用recipe_id检索菜谱对象。首先,我们检查用户是否有修改菜谱的权限。如果有,我们将继续修改菜谱的封面图片:

                recipe = Recipe.get_by_id(recipe_id=recipe_id)
                if recipe is None:
                    return {'message': 'Recipe not found'}, HTTPStatus.NOT_FOUND
                current_user = get_jwt_identity()
                if current_user != recipe.user_id:
                    return {'message': 'Access is not allowed'}, HTTPStatus.FORBIDDEN
                if recipe.cover_image:
                    cover_path = image_set.path(folder='recipes', filename=recipe.cover_image)
                    if os.path.exists(cover_path):
                        os.remove(cover_path)
    
  10. 然后,我们使用save_image函数保存上传的图像并将recipe.cover_image = filename设置为菜谱的封面图像。最后,我们使用recipe.save()保存菜谱,并返回带有 HTTP 状态码200的图像 URL:

                filename = save_image(image=file, folder='recipes')
                recipe.cover_image = filename
                recipe.save()
                return recipe_cover_schema.dump(recipe).data, HTTPStatus.OK
    
  11. app.py中导入RecipeCoverUploadResource

    from resources.recipe import RecipeListResource, RecipeResource, RecipePublishResource, RecipeCoverUploadResource
    
  12. app.py中,将RecipeCoverUploadResource链接到路由,即/recipes/<int:recipe_id>/cover

    api.add_resource(RecipeCoverUploadResource, '/recipes/<int:recipe_id>/cover')
    

现在,我们已经创建了上传菜谱封面图像的功能。让我们继续并测试它。

活动十二:测试图像上传功能

解决方案

  1. 使用 Postman 登录用户账户。点击集合选项卡并选择POST 令牌请求。然后,点击发送按钮。结果可以在以下屏幕截图中查看:图 7.12:发送 POST 令牌请求

    图 7.12:发送 POST 令牌请求
  2. 向我们的 API 发送创建菜谱的客户端请求并点击集合选项卡。

  3. 字段中的Bearer {token}中选择Authorization,其中令牌是我们上一步中检索到的访问令牌。然后,点击发送按钮。结果可以在以下屏幕截图中查看:图 7.13:向我们的 API 发送客户端请求以创建菜谱

    图 7.13:向我们的 API 发送客户端请求以创建菜谱
  4. 上传菜谱图片。点击Recipe文件夹以创建新的请求。

  5. 设置RecipeCoverUpload并将其保存在Recipe文件夹中。

  6. 将 HTTP 方法选择为PUT,并在请求 URL 中输入http://localhost:5000/recipes/<recipe_id>/cover(将<recipe_id>替换为我们上一步中获取的菜谱 ID)。

  7. 字段中的Bearer {token}中选择Authorization,其中令牌是我们上一步中检索到的访问令牌。

  8. 选择主体选项卡。然后,选择表单数据单选按钮,并在中输入封面。

  9. 旁边的下拉菜单中选择文件,并选择要上传的图片文件。

  10. 点击保存按钮然后点击发送按钮。结果可以在以下屏幕截图中查看:图 7.14:上传菜谱图片

    图 7.14:上传菜谱图片
  11. 在 PyCharm 中检查图像是否已压缩。我们可以从 PyCharm 中的应用日志中看到文件大小已减少97%图 7.15:检查在 PyCharm 中图像是否已压缩

    图 7.15:在 PyCharm 中检查图片是否已压缩
  12. static/images/recipes中检查上传的图片:![图 7.16:检查路径中的上传图片 图片

    图 7.16:检查上传的图片在路径中
  13. 获取食谱并确认cover_url属性已填充。现在,将http://localhost:5000/recipes/5点击到URL字段中。你可以用任何合适的 ID 替换食谱 ID,即 5。然后,点击发送按钮。结果如下截图所示:![图 7.17:获取食谱并确认 cover_url 属性已填充 图片

图 7.17:获取食谱并确认 cover_url 属性已填充

恭喜!我们已经测试了食谱封面图片上传功能。它运行得很好!

8:分页、搜索和排序

活动 13:在用户特定食谱检索 API 上实现分页

解决方案

  1. 修改models/recipe.py下的get_all_by_user方法中的代码,如下所示:

        @classmethod
        def get_all_by_user(cls, user_id, page, per_page, visibility='public'):
            query = cls.query.filter_by(user_id=user_id)
            if visibility == 'public':
                query = cls.query.filter_by(user_id=user_id, is_publish=True)
            elif visibility == 'private':
                query = cls.query.filter_by(user_id=user_id, is_publish=False)
            return query.order_by(desc(cls.created_at)).paginate(page=page, per_page=per_page)
    
  2. RecipePaginationSchema导入到resources/user.py中:

    from schemas.recipe import RecipeSchema, RecipePaginationSchema
    
  3. resources/user.py中声明recipe_pagination_schema属性:

    recipe_pagination_schema = RecipePaginationSchema()
    
  4. 在这里,我们向UserRecipeListResource.get方法添加了@user_kwargs装饰器。它包含一些参数,包括pageper_pagevisibility

    class UserRecipeListResource(Resource):
        @jwt_optional
        @use_kwargs({'page': fields.Int(missing=1),
                     'per_page': fields.Int(missing=10),
                     'visibility': fields.Str(missing='public')})
    
  5. 修改resources/user.py中的UserRecipeListResource.get方法:

        def get(self, username, page, per_page, visibility):
            user = User.get_by_username(username=username)
            if user is None:
                return {'message': 'User not found'}, HTTPStatus.NOT_FOUND
            current_user = get_jwt_identity()
            if current_user == user.id and visibility in ['all', 'private']:
                pass
            else:
                visibility = 'public'
            paginated_recipes = Recipe.get_all_by_user(user_id=user.id, page=page, per_page=per_page, visibility=visibility)
            return recipe_pagination_schema.dump(paginated_recipes).data, HTTPStatus.OK
    

    Recipe.get_all_by_user方法通过特定作者获取分页食谱,然后让recipe_pagination_schema序列化分页对象并返回。

活动 14:测试用户特定食谱检索 API 上的分页

解决方案

  1. 使用 Postman 分页,每页两个,逐页获取 John 的所有食谱。首先,点击UserRecipeList请求。

  2. 在此处输入http://localhost:5000/{username}/recipes,这里的{username}应与我们在前面的练习中插入的相同。在我们的例子中,它将是john

  3. 选择per_page,即2)。

  4. 发送请求。结果如下截图所示:![图 8.9:使用 Postman 获取 John 的所有食谱 图片

    图 8.9:使用 Postman 获取 John 的所有食谱

    在食谱的详细信息中,我们可以看到有带有firstlastnext页面 URL 的链接。因为我们处于第一页,所以我们看不到prev页面。总共有四页,每页有两个记录。我们还可以在 HTTP 响应中看到排序后的食谱详情。

  5. 点击链接中的下一个 URL,在 Postman 中查询下一个两个记录,请求 URL 已填写(http://localhost:5000/users/john/recipes?per_page=2&page=2)。然后,我们只需点击发送来发送请求。结果如下截图所示:![图 8.10:在 Postman 中查询已填写请求 URL 的下一个两个记录 图片

图 8.10:在 Postman 中使用请求 URL 查询下两条记录

从结果中,我们可以看到有链接到firstlastnextprev页面。我们还可以看到我们目前在第 2 页。所有的配方数据都在那里。

活动 15:搜索含有特定配料的食谱

解决方案

  1. 首先,在models/recipe.py中,将ingredients属性添加到Recipe模型中:

        ingredients = db.Column(db.String(1000))
    
  2. 运行以下命令以生成数据库迁移脚本:

    flask db migrate
    

    你将看到检测到一个名为recipe.ingredients的新列:

    INFO  [alembic.autogenerate.compare] Detected added column 'recipe.ingredients'
      Generating /TrainingByPackt/Python-API-Development-Fundamentals/smilecook/migrations/versions/0876058ed87e_.py ... done
    
  3. 检查/migrations/versions/0876058ed87e_.py中的内容,这是上一步中生成的数据库迁移脚本:

    """empty message
    
    Revision ID: 0876058ed87e
    Revises: 91c7dc71b826
    Create Date: 2019-10-24 15:05:10.936752
    
    """
    from alembic import op
    import sqlalchemy as sa
    
    # revision identifiers, used by Alembic.
    revision = '0876058ed87e'
    down_revision = '91c7dc71b826'
    branch_labels = None
    depends_on = None
    
    def upgrade():
        # ### commands auto generated by Alembic - please adjust! ###
        op.add_column('recipe', sa.Column('ingredients', sa.String(length=1000), nullable=True))
        # ### end Alembic commands ###
    
    def downgrade():
        # ### commands auto-generated by Alembic - please adjust! ###
        op.drop_column('recipe', 'ingredients')
        # ### end Alembic commands ###
    

    在这里,我们可以看到脚本中生成了两个函数。upgrade函数用于将新列ingredients添加到配方表中,而downgrade函数用于删除ingredients列,使其恢复到原始状态。

  4. 运行以下flask db upgrade命令以更新数据库模式:

    flask db upgrade
    

    你将看到以下输出:

    INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
    INFO  [alembic.runtime.migration] Will assume transactional DDL.
    INFO  [alembic.runtime.migration] Running upgrade 91c7dc71b826 -> 0876058ed87e, empty message
    
  5. schemas/recipe.py中,将ingredients属性添加到RecipeSchema

            ingredients = fields.String(validate=[validate.Length(max=1000)])
    
  6. 修改resources/recipe.py中的RecipeResource.patch方法,以便能够更新ingredients

    recipe.ingredients = data.get('ingredients') or recipe.ingredients
    
  7. 修改models/recipe.py中的Recipe.get_all_published方法,使其通过配料获取所有已发布的配方:

    return cls.query.filter(or_(cls.name.ilike(keyword),
                       cls.description.ilike(keyword),
                       cls.ingredients.ilike(keyword)),
                     cls.is_publish.is_(True)).\
      order_by(sort_logic).paginate(page=page, per_page=per_page)
    
  8. 右键单击它以运行应用程序。Flask 将启动并在localhost127.0.0.1)的端口5000上运行:![图 8.11:在本地主机上运行 Flask img/C15309_03_07.jpg

    图 8.11:在本地主机上运行 Flask
  9. 登录用户账户,并在 PyCharm 控制台中运行以下httpie命令创建两个配方。应将{token}占位符替换为访问令牌:

    http POST localhost:5000/recipes "Authorization: Bearer {token}" name="Sweet Potato Casserole" description="This is a lovely Sweet Potato Casserole" num_of_servings=12 cook_time=60 ingredients="4 cups sweet potato, 1/2 cup white sugar, 2 eggs, 1/2 cup milk" directions="This is how you make it"
    http POST localhost:5000/recipes "Authorization: Bearer {token}" name="Pesto Pizza" description="This is a lovely Pesto Pizza" num_of_servings=6 cook_time=20 ingredients="1 pre-baked pizza crust, 1/2 cup pesto, 1 ripe tomato" directions="This is how you make it"
    
  10. 使用以下httpie命令发布这两个食谱:

    http PUT localhost:5000/recipes/14/publish "Authorization: Bearer {token}"
    http PUT localhost:5000/recipes/15/publish "Authorization: Bearer {token}"
    
  11. 搜索名称、描述或配料中包含eggs字符串的食谱。点击RecipeList请求并选择qeggs)并发送请求。结果如下截图所示:![图 8.12:通过发送请求搜索鸡蛋配料 img/C15309_08_12.jpg

图 8.12:通过发送请求搜索鸡蛋配料

从前面的搜索结果中,我们可以看到有一个配料中含有鸡蛋的配方。

9: 构建更多功能

活动 16:更新食谱详情后的缓存数据获取

解决方案

  1. 获取所有配方数据,点击RecipeList并发送请求。结果如下截图所示:![图 9.15:获取配方数据并发送请求 img/C15309_09_15.jpg

    图 9.15:获取配方数据并发送请求
  2. 登录您的账户,点击收藏集标签并选择POST 令牌请求。然后,发送请求。结果如下截图所示:![图 9.16:选择 POST 令牌请求并发送它 图片

    图 9.16:选择 POST Token 请求并发送
  3. 使用PATCH方法修改食谱记录。首先,选择PATCH Recipe请求。

  4. 现在选择Bearer {token};该令牌应该是访问令牌。

  5. 选择num_of_servings5,以及cook_time50

    { 
        "num_of_servings": 5, 
        "cook_time": 50 
    } 
    
  6. 发送请求。结果如下截图所示:![图 9.17:使用 PATCH 方法修改食谱记录 图片

    图 9.17:使用 PATCH 方法修改食谱记录
  7. 再次获取所有食谱数据,点击RecipeList

  8. 发送请求。结果如下截图所示:![图 9.18:再次获取所有食谱数据 图片

图 9.18:再次获取所有食谱数据

我们可以看到,当我们再次获取所有食谱详情时,详情没有更新,这将导致用户看到错误的信息。

活动 17:添加多个速率限制限制

解决方案

  1. resources/user.py中,从extensions导入limiter

    from extensions import image_set, limiter
    
  2. UserRecipeListResource中,将limiter.limit函数放入decorators属性:

    class UserRecipeListResource (Resource):
        decorators = [limiter.limit('3/minute;30/hour;300/day', methods=['GET'], error_message='Too Many Requests')]
    
  3. app.py中注释掉白名单:

    #  @limiter.request_filter
    #   def ip_whitelist():
    #      return request.remote_addr == '127.0.0.1'
    

    在 PyCharm 中,如果您使用的是 Mac,可以使用Command + /来注释掉一行代码,如果您使用的是 Windows,可以使用Ctrl + /

  4. 当我们完成时,点击运行以启动 Flask 应用程序;然后,我们就可以开始测试它:![图 9.19:启动 Flask 应用程序 图片

    图 9.19:启动 Flask 应用程序
  5. 获取用户的全部食谱并检查响应头中的速率限制信息。首先,点击UserRecipeList并发送请求。

  6. 然后,在响应头部选项卡中选择头部。结果如下截图所示:

![图 10.27:在 Postman 中添加更多环境变量图片

图 9.20:检查响应头中的速率限制信息

在 HTTP 响应中,我们可以看到此端点的速率限制为三个,而我们只剩下两个剩余的请求数额。限制将在 60 秒后重置。

10:部署

活动 18:在 Postman 中将 access_token 更改为变量

解决方案

  1. 执行用户登录并获取访问令牌。使用POST Token请求获取访问令牌。您应该看到以下输出:![图 10.26:执行用户登录以获取访问令牌 图片

    图 10.29:执行用户登录以获取访问令牌
  2. 点击access_token变量。其值是我们上一步获得的访问令牌。然后,点击更新:![图 10.27:在 Postman 中添加更多环境变量 图片

    图 10.30:在 Postman 中添加更多环境变量
  3. 选择Bearer {{access_token}},这是我们之前步骤中添加的环境变量,然后发送请求。您应该看到以下输出:图 10.28:在 Postman 中使用更多环境变量

图 10.31:在 Postman 中使用更多环境变量
posted @ 2025-09-19 10:34  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报