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

111 阅读27分钟

Flask REST API 构建指南(二)

原文:Building REST APIs with Flask

协议:CC BY-NC-SA 4.0

四、Flask CURD 应用:

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

  1. 电子邮件验证

  2. 文件上传

  3. 讨论 API 文档

  4. 整合霸气

介绍

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

电子邮件验证

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

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

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

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

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

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

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

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

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

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

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

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

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

和 UserSchema 应该具有以下代码。

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

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

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

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

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

    SECRET_KEY= 'your_secured_key_here'
    SECURITY_PASSWORD_SALT= 'your_security_password_here'

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

from itsdangerous import URLSafeTimedSerializer
from flask import current_app

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

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

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

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

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

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

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

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

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

from api.utils.token import generate_verification_token, confirm_verification_token

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

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

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

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

(venv) $ pip install Flask-Mail

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

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

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

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

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

from api.utils.email import mail

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

    mail.init_app(app)

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

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

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

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

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

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

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

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

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

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

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

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

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

img/479840_1_En_4_Fig1_HTML.jpg

图 4-1

用户注册 API

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

img/479840_1_En_4_Fig2_HTML.jpg

图 4-2。

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

img/479840_1_En_4_Fig3_HTML.jpg

图 4-3

未经验证的用户登录

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

img/479840_1_En_4_Fig4_HTML.jpg

图 4-4

用户电子邮件验证

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

img/479840_1_En_4_Fig5_HTML.jpg

图 4-5

用户验证后登录

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

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

文件上传

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

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

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

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

AuthorSchema 类中的下面一行

    avatar = fields.String(dump_only=True)

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

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

    UPLOAD_FOLDER= 'images'

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

from werkzeug.utils import secure_filename

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

from flask import Blueprint, request, url_for, current_app

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

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

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

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

def allowed_file(filename):
       return filetype in allowed_extensions

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

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

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

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

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

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

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

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

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

img/479840_1_En_4_Fig6_HTML.jpg

图 4-6

作者头像上传端点

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

img/479840_1_En_4_Fig7_HTML.jpg

图 4-7

获取化身端点

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

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

img/479840_1_En_4_Fig8_HTML.jpg

图 4-8

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

美国石油学会文件

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

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

API 文档的构建块

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

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

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

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

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

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

OpenAPI 规范

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

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

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

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

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

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

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

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

img/479840_1_En_4_Fig9_HTML.jpg

图 4-9

神气活现的检查员

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

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

img/479840_1_En_4_Fig10_HTML.jpg

图 4-10

创建用户端点 Swagger 检查器

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

img/479840_1_En_4_Fig11_HTML.jpg

图 4-11

登录端点

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

img/479840_1_En_4_Fig12_HTML.jpg

图 4-12

固定请求

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

img/479840_1_En_4_Fig13_HTML.jpg

图 4-13

OpenAPI 规格层代

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

img/479840_1_En_4_Fig14_HTML.jpg

图 4-14

从检查器导入 OpenAPI

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

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

img/479840_1_En_4_Fig15_HTML.jpg

图 4-15

SwaggerHub

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

img/479840_1_En_4_Fig17_HTML.jpg

图 4-17

查看文档页面

img/479840_1_En_4_Fig16_HTML.jpg

图 4-16

查看文档

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

img/479840_1_En_4_Fig18_HTML.jpg

图 4-18

API 请求模式

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

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

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

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

(venv)$ pip install flask_swagger flask_swagger_ui

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

from flask_swagger import swagger
from flask_swagger_ui import get_swaggerui_blueprint

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

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

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

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

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

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

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

img/479840_1_En_4_Fig19_HTML.jpg

图 4-19

Swagger UI

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

构建时文档

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

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

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

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

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

img/479840_1_En_4_Fig20_HTML.jpg

图 4-20

构建时文档生成

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

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

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

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

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

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

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

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

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

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

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

img/479840_1_En_4_Fig21_HTML.jpg

图 4-21

重新加载 Swagger UI 以查看端点

结论

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

五、Flask 测试

未经检验的东西坏了。

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

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

介绍

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

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

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

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

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

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

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

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

设置单元测试

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

让我们先安装库。

(venv)$ pip install unittest2

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

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

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

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

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

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

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

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

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

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

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

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

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

单元测试用户端点

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

img/479840_1_En_5_Fig1_HTML.jpg

图 5-1

运行单元测试

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

使用以下代码安装 nose。

(venv)$ pip install nose

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

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

(venv)$ nosetests

img/479840_1_En_5_Fig2_HTML.jpg

图 5-2

用 nose 运行单元测试

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

img/479840_1_En_5_Fig3_HTML.jpg

图 5-3

test_users.py 上的 Nosetests

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

img/479840_1_En_5_Fig4_HTML.jpg

图 5-4

作者测试

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

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

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

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

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

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

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

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

        response = self.app.post(

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

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

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

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

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

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

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

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

        )
        self.assertEqual(204, response.status_code)

测试覆盖率

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

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

(venv)$ pip install coverage

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

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

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

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

img/479840_1_En_5_Fig5_HTML.jpg

图 5-5

测试覆盖率

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

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

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

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

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

img/479840_1_En_5_Fig6_HTML.jpg

图 5-6

HTML 格式的测试覆盖报告

结论

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

六、部署 Flask 应用

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

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

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

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

  3. 在 Heroku 部署 Flask

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

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

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

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

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

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

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

(venv)$ pip freeze > requirements.txt

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

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

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

img/479840_1_En_6_Fig1_HTML.jpg

图 6-1

阿里云 ECS 控制台

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

img/479840_1_En_6_Fig2_HTML.jpg

图 6-2

SSH 到 Ubuntu 实例

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

$ adduser flask

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

$ usermod -aG sudo flask

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

$ su - flask

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

$ sudo apt-get install git

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

$ sudo git clone <repo_name>

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

$ sudo pip install virtualenv uwsgi

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

$ pip install -r requirements.txt

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

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

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

[uwsgi]
module = run:application

master = true
processes = 5

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

die-on-term = true

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

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

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

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

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

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

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

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

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

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

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

$ sudo apt-get install nginx

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

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

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

server {
    listen 80;
    server_name flaskapp;

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

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

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

有了这些,我们现在可以使用下面的命令测试我们的更改是否有语法错误。

$ sudo nginx -t

如果没有语法错误,您应该在您的终端上看到这个输出。

$ nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
$ nginx: configuration file /etc/nginx/nginx.conf test is successful

现在,当您访问您的域名时,它应该可以正常工作。如果您没有域,并且想要访问应用,您应该在“启用站点”中编辑默认服务器块,而不是创建一个新的服务器块,或者删除默认应用,这样您就可以使用 IP 地址访问应用。

还要确保如果你在云服务上运行 Ubuntu 服务器,防火墙上的端口 80 是允许的,在我们的例子中,它是使用安全组设置的。

img/479840_1_En_6_Fig3_HTML.jpg

图 6-3

部署的应用

在 Gunicorn 上部署 Flask,在阿里云 ECS 上部署 Apache

现在我们将使用 Gunicorn 安装我们的 Flask 应用,guni corn 是用于 Unix 的 Python WSGI HTTP 服务器,它将运行我们的应用,然后我们将使用 Apache 服务器反向代理请求。

要了解本节内容,您需要具备以下条件:

  1. 拥有 sudo 权限的非 root 用户的 Ubuntu 服务器

  2. Apache 服务器已安装

  3. 主目录中我们的 Flask 应用的副本

正如我提到的,我们将使用 Gunicorn 来运行我们的应用,所以让我们使用 PIP 来安装 Gunicorn。

$ pip install gunicorn

接下来,我们将创建一个系统服务,就像我们在上一节中所做的那样,所以继续用下面的命令创建我们的新服务。

$ sudo nano /etc/systemd/system/flask-app.service

接下来在你的 nano 编辑器中添加下面几行。

[Unit]
Description= Flask App service
After=network.target

[Service]
User=flask
Group=www-data
Restart=on-failure
Environment="WORK_ENV=PROD"
WorkingDirectory=/home/flask/flask-api-app/src
ExecStart=/home/flask/flask-api-app/src/venv/bin/gunicorn -c /home/flask/flask-api-app/src/gunicorn.conf -b 0.0.0.0:5000 wsgi:application

[Install]
WantedBy=multi-user.target

现在保存文件并退出,我们应该有我们的系统服务。接下来,我们将使用以下命令启用并启动我们的服务。

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

所以现在我们的应用应该运行在端口 5000 上;接下来,我们需要配置 Apache 来反向代理我们的应用。

默认情况下,反向代理模块在 Apache 中是禁用的,要启用它,请输入以下命令。

$ a2enmod

它会提示激活模块;输入以下要激活的模块:

$ proxy proxy_ajp proxy_http rewrite deflate headers proxy_balancer proxy_connect proxy_html

现在将我们的应用添加到 Apache web 服务器配置文件中。将以下行(在 VirtualHost 块中)添加到/etc/Apache 2/sites-available/000-default . conf 中

    <Proxy *>
        Order deny,allow
        Allow from all
    </Proxy>
    ProxyPreserveHost On
    <Location "/">
          ProxyPass "http://127.0.0.1:5000/"
          ProxyPassReverse "http://127.0.0.1:5000/"
    </Location>

现在,它应该在根路由上代理我们的应用,您的最终服务器块应该如下所示。

<VirtualHost *:80>
    # The ServerName directive sets the request scheme, hostname and port that
    # the server uses to identify itself. This is used when creating
    # redirection URLs. In the context of virtual hosts, the ServerName
    # specifies what hostname must appear in the request's Host: header to
    # match this virtual host. For the default virtual host (this file) this
    # value is not decisive as it is used as a last resort host regardless.
    # However, you must set it for any further virtual host explicitly.
    #ServerName www.example.com

    ServerAdmin webmaster@localhost
    DocumentRoot /var/www/html

    # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
    # error, crit, alert, emerg.
    # It is also possible to configure the loglevel for particular
    # modules, e.g.
    #LogLevel info ssl:warn

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined
     <Proxy *>
        Order deny,allow
        Allow from all
    </Proxy>
    ProxyPreserveHost On
    <Location "/">
          ProxyPass "http://127.0.0.1:5000/"
          ProxyPassReverse "http://127.0.0.1:5000/"
    </Location>
    # For most configuration files from conf-available/, which are
    # enabled or disabled at a global level, it is possible to
    # include a line for only one particular virtual host. For example the
    # following line enables the CGI configuration for this host only
    # after it has been globally disabled with "a2disconf".
    #Include conf-available/serve-cgi-bin.conf
</VirtualHost>

# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

保存文件并退出,我们已经全部介绍过了。只需用下面的命令重启服务器。

$ sudo service apache2 restart

现在使用 IP 地址访问浏览器中的应用。

img/479840_1_En_6_Fig4_HTML.jpg

图 6-4

在 Gunicorn 上部署 Flask 应用

在 AWS 弹性豆茎上展开 Flask

在本节中,我们将使用 AWS Elastic Beanstalk 部署我们的 Flask 应用。AWS Elastic Beanstalk 是一个易于使用的服务,用于部署和扩展 web 应用和服务。

我们假设您在开发机器中已经有一个活动的 AWS 帐户和 AWS CLI 设置,或者您可以在其上使用 AWS 文档。

要创建应用环境并部署应用,请使用 eb init 命令初始化您的 EB CLI 存储库。

$ eb init -p python-2.7 flask-app --region <your_region>

注意

有关地区列表,请参考本指南: https://docs.aws.amazon.com/general/latest/gr/rande.html

您应该在终端中看到以下响应。

$ Application flask-app has been created.

前面的命令创建一个名为 flask-app 的新应用,并配置您的本地存储库,以使用最新的 Python 2.7 创建环境。

接下来再次运行 eb init,为 SSH 登录配置一个密钥对。

接下来,我们将创建一个环境,并使用 eb create 将您的应用部署到该环境中。

$ eb create

接下来输入环境名称、DNS 前缀和负载平衡器类型,这将消除向外界公开 web 服务器的需要。

img/479840_1_En_6_Fig5_HTML.jpg

图 6-5

eb 创建

现在大约需要 5 分钟来部署。部署完成后,我们只需要配置一些更多的细节,这将直接在 AWS web 控制台中完成。

img/479840_1_En_6_Fig6_HTML.jpg

图 6-6

eb 创造成功

一旦应用完成部署,您应该会看到类似于上图的输出。接下来登录 AWS web 控制台,打开 Elastic Beanstalk 配置选项卡。

img/479840_1_En_6_Fig7_HTML.jpg

图 6-7

弹性豆茎应用配置

接下来点击修改;在“软件”选项卡和“容器内选项”中,将 WSGIPath 更新为 run.py。

img/479840_1_En_6_Fig8_HTML.jpg

图 6-8

WSGIpath 集

现在向下滚动,在环境变量中,提供 WORK_ENV 并将其设置为 Prod,以便我们的应用在生产模式下运行。接下来单击 apply,应用应该会重新加载并开始工作。

img/479840_1_En_6_Fig9_HTML.jpg

图 6-9

弹性豆茎环境变量

现在,您可以返回 dashboard 来查找应用的 URL,它应该已经启动并运行了。

注意

在 Elastic Beanstalk 中,您还可以配置并启动一个连接到环境的 MySQL RDS 服务器来运行应用,但这超出了本书的范围。

img/479840_1_En_6_Fig10_HTML.jpg

图 6-10

在弹性豆茎上部署 Flask 应用

在 Heroku 上部署 Flask 应用

Heroku 是一个平台即服务(PaaS ),支持各种现代应用,为在云中大规模部署和管理应用提供基于容器的环境。在 Heroku 上部署应用非常简单快捷。你可以使用 Heroku git,用你当前的 GitHub 账户连接,或者使用容器注册。这里我们将使用 Heroku CLI 部署我们的应用;因此,请确保您在 https://signup.heroku.com/ 有一个有效的 Heroku 帐户,它可以让您免费部署多达五个应用。

您还需要 Heroku CLI,可从 https://devcenter.heroku.com/articles/heroku-command-line 下载。拥有 CLI 后,使用以下命令登录 Heroku CLI,该命令将提示您提供登录凭据。

$ heroku login

添加配置文件

为了在 Heroku 上成功部署我们的应用,我们必须向该应用添加 Procfile,它定义了应用运行时要执行的命令。

对于 Heroku,我们将使用一个名为 Gunicorn 的 web 服务器,因此在创建 Procfile 之前,使用以下命令安装 Gunicorn。

(venv)$ pip install gunicorn

现在使用 pip freeze 命令更新 requirements.txt 文件。

(venv)$ pip freeze > requirements.txt

现在让我们首先测试 Gunicorn 是否能很好地与我们的应用一起工作;运行以下命令在本地启动 Gunicorn 服务器。

(venv)$ gunicorn run:application

运行后,您应该在终端上得到以下输出,这意味着服务器工作正常。

[2019-04-29 22:54:41 +0530] [37191] [INFO] Starting gunicorn 19.9.0
[2019-04-29 22:54:41 +0530] [37191] [INFO] Listening at: http://127.0.0.1:8000 (37191)
[2019-04-29 22:54:41 +0530] [37191] [INFO] Using worker: sync
[2019-04-29 22:54:41 +0530] [37194] [INFO] Booting worker with pid: 37194

注意

默认情况下,Gunicorn 从端口 8000 开始。

接下来,在 src 目录中创建一个名为 Procfile 的文件,并在其中添加以下代码行。

web: gunicorn run:application

这里为 Heroku 指定了 web,以便为应用启动一个 Web 服务器。现在,在我们在 Heroku 上创建和部署我们的应用之前,还需要做一件事情。由于 Heroku 默认使用 Python 3.6 运行时,我们必须创建另一个名为 runtime.txt 的文件,并添加以下代码行,以便 Heroku 为我们的应用使用正确的 Python 版本。

python-2.7.16

现在我们已经准备好部署我们的应用了;在应用的 src 目录中,运行以下命令创建一个新的 Heroku 应用。

$ heroku create <app_name>

这需要几秒钟的时间,您应该会在终端上看到类似的输出。

Creating flask-app-2019... done
https://flask-app-2019.herokuapp.com/ | https://git.heroku.com/flask-app-2019.git

接下来用下面的命令初始化一个新的 Heroku git repo。

$ git init
$ heroku git:remote -a flask-app-2019

现在添加所有文件,并用下面的命令提交代码。

$ git add .
$ git commiti -m "init"

在推送和部署代码之前,我们需要做的最后一件事是设置 WORK_ENV 环境变量,因此使用下面的命令来完成。

$ heroku config:set WORK_ENV=PROD

接下来我们需要将代码推送到 Heroku git,它将被自动部署。

$ git push heroku master

几分钟后,您的应用就应该部署并运行了。您的应用的 URL 是htttps://<app_name>.herokuapp.com

img/479840_1_En_6_Fig11_HTML.jpg

图 6-11

Heroku 上的 Flask 应用

这就是在 Heroku 上部署我们的应用的原因;您可以在 https://devcenter.heroku.com 上了解更多关于 Heroku 的信息

在谷歌应用引擎上部署 Flask 应用

在本节中,我们将在 Google Cloud App Engine 上部署我们的应用,这是一个完全托管的无服务器平台,用于在云上部署和扩展应用。App Engine 支持包括 Python 在内的各种平台,并为部署后端服务提供完全托管的服务。因此,在我们开始之前,请确保您有一个有效的谷歌云帐户,或者您可以在 https://cloud.google.com/products/search/apply/ 注册。谷歌云也提供一年 300 美元的积分。

接下来使用以下指南安装 Google Cloud CLI:https://cloud.google.com/sdk/docs/quickstarts。设置完成后,运行以下命令登录您的 Google Cloud 帐户。

$ gcloud auth login

一旦成功,我们就可以开始创建我们的 Google App Engine 应用,但是在此之前,我们需要创建几个配置文件。

首先用下面的代码在 src 目录中创建 app.yaml,为 Google App Engine 配置应用基础。

runtime: python27
api_version: 1
threadsafe: true
handlers:
- url: /avatar
  static_dir: images
- url: /.*
  script: wsgi.application

libraries:
  - name: ssl
    version: latest
env_variables:
  WORK_ENV: PROD

接下来创建 appengine_config.py,它将从我们的虚拟环境中获取已安装的模块,以便 appengine 知道第三方模块安装在哪里。

from google.appengine.ext import vendor

vendor.add('venv/lib/python2.7/site-packages/')

现在我们准备初始化我们的应用;对同一运行以下命令:

$ gcloud init

这将提示您创建应用、名称和项目,并选择区域,因此请正确输入。

完成后,运行以下命令在 Google Cloud App Engine 上部署您的应用。

几分钟之内,它就应该部署并运行了。现在,您可以在终端中运行以下命令,在默认浏览器中打开应用。

$ gcloud app browse

img/479840_1_En_6_Fig12_HTML.jpg

图 6-12

谷歌云应用引擎上的 Flask 应用

结论

因此,在本章中,我们使用不同的方法在各种云平台上部署了我们的应用,这应该已经为您提供了入门知识,或者部署 Flask 应用的各种部署和扩展选项。在下一章,我们将讨论管理和调试已部署应用的部署后步骤。