精通-Flask-Web-API-开发-全-

精通 Flask Web API 开发(全)

原文:zh.annas-archive.org/md5/82ba6adc70e4941205c46ebff3a4ad3d

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:前言

自 2009 年以来,我开始使用该框架进行软件开发项目以来,Flask 一直是一个强大、轻量级、无缝且易于使用的 Python 框架,用于 API 和 Web 应用程序开发。 这个非模板 WSGI 框架已经扩展了其支持,现在它有几个实用工具来支持不同的功能,甚至实现了 异步组件。

根据我的经验,Flask 的灵活性使其成为构建各种应用程序的最佳工具,从小型电子商务到中型企业应用程序,以及许多需要 XLSX 和 CSV 自动化、报告和 图形生成的应用程序。 生成。

本书展示了 Flask 3 及其如何使用最新功能将所有之前的软件开发规范与之前 Flask 版本进行翻译和升级。 我希望这本书能帮助你理解 Flask 3,并将其组件应用于创建解决方案和解决具有挑战性的 现实世界问题。

本书面向对象

本书面向那些寻求对 Flask 框架有更深入理解的熟练 Python 开发者,作为解决企业挑战的解决方案。 它也是 Flask 熟练读者学习框架高级功能和 新特性的绝佳资源。

本书涵盖内容

第一章, 深入浅出 Flask 框架,介绍了 Flask 作为一个简单且轻量级的 Python Web 框架,并展示了使用基本 Flask 功能(如视图函数、基于类的视图、数据库连接、内置 Werkzeug 服务器和库以及自定义 环境变量)的非标准项目目录结构安装 Flask 3 以启动 Web 应用程序开发。

第二章, 添加高级核心功能,提供了 Web 应用程序的 Flask 3 核心功能,如会话管理、使用 对象关系映射 (ORM) 进行数据管理、使用 Jinja2 模板进行视图渲染、消息闪现、错误处理、软件日志记录、添加静态内容以及将蓝图和应用工厂设计应用于 项目结构。

第三章, 创建 REST Web 服务,介绍了使用 Flask 3 进行 API 开发,包括请求和响应处理,实现 JSON 编码器和解码器以解析传入的请求体和输出的响应,使用 <st c="2549">@before_request</st> <st c="2569">@after_request</st> 事件访问请求和应用程序上下文,异常处理,以及实现客户端应用程序以消费 REST 服务。

第四章, 利用 Flask 扩展,讨论了如何通过使用有用的和高效的 Flask 模块来替代其底层等效模块来节省开发和努力时间,例如 Flask-Session 用于非浏览器基于的会话处理,Bootstrap-Flask 用于提供表示层,Flask-WTF 用于构建基于模型的 Web 表单,Flask-Caching 用于创建缓存,Flask-Mail 用于发送电子邮件,以及 Flask-Migrate 用于从数据模型构建数据库模式。

第五章, 构建异步事务,解释了 Flask 3 的异步特性,包括创建异步视图和 API 端点函数,使用 SQLAlchemy 实现异步存储库层,使用 Celery 和 Redis 构建异步后台任务,实现 WebSocket 和 <st c="3510">asyncio</st> 实用工具,应用异步信号以触发事务,应用响应式编程,并介绍了 Quart 作为 Flask 3 的 ASGI 变体。

第六章, 开发计算和科学应用,讨论了在构建科学应用中使用 Flask,包括使用 XLSX 和 CSV 上传以及使用流行的 Python 库(如 <st c="3905">numpy</st>,《st c="3912">pandas<st c="3918">,《st c="3920">matplotlib</st>,《st c="3932">seaborn<st c="3939">,《st c="3941">scipy</st>,和 <st c="3952">sympy</st>),JavaScript 库(如 Chart.js,Bokeh 和 Plotly),用于 PDF 生成的 LaTeX 工具,Celery 和 Redis 用于耗时的后台计算,以及其他科学工具,例如 Julia。

第七章, 使用非关系型数据存储,解释了 Flask 如何使用流行的 NoSQL 数据库(如 Apache HBase/Hadoop、Apache Cassandra、Redis、MongoDB、Couchbase 和 Neo4J)来管理非关系型和大数据。

第八章, 使用 Flask 构建工作流程,讨论了如何使用 Celery 和 Redis、SpiffWorkflow、Camunda 的 Zeebe 服务器、Airflow 2 和 Temporal.io 在 Flask 3 中实现非 BPMN 和 BPMN 工作流程。

第九章, 确保 Flask 应用程序安全,提供了多种确保基于 Web 和 API 的 Flask 应用程序安全的方法,例如使用 HTTP Basic、Digest 和 Bearer-token 认证方案实现身份验证和授权机制,OAuth2 授权方案和 Flask-Login;利用编码/解码和加密/解密库来保护用户凭证;应用表单验证和数据清理以避免不同的 Web 攻击;用安全的 HTTPS 替换 HTTP 来运行应用程序;以及控制响应头以限制或限制 用户访问。

第十章, 为 Flask 创建测试用例,提供了使用 PyTest 框架测试 Flask 3 组件(如模型类、存储库事务、本地服务、视图和 API 端点函数、数据库连接、异步进程和 WebSockets)的技术,无论是否进行模拟。

第十一章, 部署 Flask 应用程序,讨论了部署和运行 Web 或 API 应用程序的不同选项,包括使用 Gunicorn 为标准和非异步 Flask 应用程序、uWSGI、通过 Docker Compose 和 Kubernetes 部署的 Docker 平台以及 Apache HTTP 服务器。

第十二章, 将 Flask 与其他工具和框架集成,提供了将 Flask 应用程序集成到不同流行工具的解决方案,例如 GraphQL、React 客户端应用程序和 Flutter 移动应用程序,以及使用 Flask 的互操作性功能在微服务应用程序中构建由 Django、FastAPI、Tornado 和 Flask 框架构建的子应用程序。

为了充分利用本书

为了完全理解本书的前几章,您应该具备使用任何框架进行 Python 网络和 API 编程的背景,或者至少对 Flask 有一些了解。 但是,如果您有使用标准 Python 编写脚本的背景,这也有助于您至少理解第一章,该章节介绍了如何使用 Python 语言和基本的 Flask 概念来构建网络应用程序。 经验丰富的开发者可以使用本书进一步丰富他们的 Flask 经验,利用 Flask 3.x 框架的新实用类和函数。

本书涵盖的软件/硬件 操作系统要求 操作系统 要求
Python 3.11.x Windows 10,至少
React 18.3.1 Ubuntu(使用 PowerShell 和 WSL2)
Flutter 3.19.5
PostgreSQL 13.4
MongoDB 社区 服务器 7.0.11
Redis 服务器 7.2.3 (Ubuntu)
Redis 服务器 3.0.504 (Windows)
HBase/Hadoop 2.5.5
Couchbase 7.2.0
Cassandra 4.1.5
Neo4J 桌面版 1.5.8
Julia 1.9.2
Docker 25.0.3
Kubernetes 5.0.4 (Docker 捆绑)
Zeebe 1.1.0 (Docker)
Airflow 2.5
Temporal.io 服务器 1.22.0
Apache HTTP 服务器 2.4
Jaeger 1.5
VS Code 1.88.0

可选地,如果您使用授权的 Microsoft Excel 打开 XLSX 和 CSV 文件,以及使用 Foxit PDF Reader 打开生成的 PDF 文件,这将有所帮助。

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

我们建议您安装指定的 Python 版本,以避免与版本不兼容相关的意外错误。 此外,在阅读章节的同时下载和阅读 GitHub 上的代码,以了解讨论内容,这也是一个明智的选择。 GitHub 上的代码只是一个原型,可以作为构建您版本的应用程序的指南。

下载示例代码文件

您可以从 GitHub 下载本书的示例代码文件,网址为 https://github.com/PacktPublishing/Mastering-Flask-Web-Development。如果代码有更新,它将在 GitHub 仓库中更新。

我们还有其他来自我们丰富的图书和视频目录的代码包可供下载,网址为 https://github.com/PacktPublishing/。查看它们吧!

使用的约定

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

<st c="8530">文本中的代码</st>:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。 以下是一个示例:“然而,要完全使用此功能,请使用以下 <st c="8765">flask[async]</st> 模块,使用以下 <st c="8805">pip</st> 命令安装:”

代码块按照以下方式设置: 如下:

 from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, scoped_session
DB_URL = "postgresql://<username>:<password>@localhost:5433/sms"

当我们希望您注意代码块中的特定部分时,相关的行或项目会被设置为 粗体:

<st c="9186">engine = create_engine(DB_URL)</st>
<st c="9217">db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine))</st> Base = declarative_base()
def init_db():
    import modules.model.db

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

 pip install flask[async]

粗体:表示新术语、重要单词或屏幕上看到的单词。 例如,菜单或对话框中的单词会以 粗体显示。以下是一个示例:“点击此选项将带您到 输入解释器路径… 菜单命令,最终到 查找… 选项。”

提示或重要注意事项

看起来像这样。

联系我们

我们欢迎读者的反馈。

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

勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。 如果您在这本书中发现了错误,我们非常感谢您向我们报告。 请访问 www.packtpub.com/support/errata 并填写 表格。

盗版:如果您在互联网上以任何形式发现我们作品的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。 请通过以下邮箱联系我们 copyright@packtpub.com 并提供 材料的链接。

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

分享您的想法

一旦您阅读了 精通 Flask Web 和 API 开发,我们非常乐意听听您的想法! 点击此处直接进入此书的亚马逊评论页面 并分享 您的反馈。

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

免费下载此书的 PDF 副本

感谢您购买 此书!

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

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

不用担心!现在,每本 Packt 书籍都免费提供该书的 DRM 免费 PDF 版本。 无需付费。

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

优惠远不止于此,您还可以每天在您的 收件箱中获得独家折扣、时事通讯和优质免费内容

按照以下简单步骤获取 以下好处:

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

https://packt.link/free-ebook/9781837633227

  1. 提交您的购买 证明。

  2. 这就完了! 我们将直接将您的免费 PDF 和其他福利发送到您的 电子邮件。

第一部分:学习 Flask 3.x 框架

在本部分中,您将学习和理解实现 Flask 接受的 Web 和 API 应用程序的基本和核心组件,包括使用蓝图和应用工厂函数构建适当的 Flask 项目结构。 本部分还将教授您如何使用 psycopg2 和 asyncpg 驱动程序将 Flask 集成到 PostgreSQL 数据库中,并使用原生数据库驱动程序或 对象关系映射 (ORM)工具实现应用程序的存储库层。 此外,您还将学习如何使用外部 Flask 模块实现应用程序的功能,而无需花费太多时间和精力。

本部分包括以下章节: :

  • 第一章, 深入探索 Flask 框架

  • 第二章, 添加高级核心功能

  • 第三章, 创建 REST Web 服务

  • 第四章, 利用 Flask 扩展

第二章:1

深入探讨 Flask 框架

Flask 是一个 Python Web 框架,由 Armin Ronacher 创建,旨在解决需要快速开发方法的基于 Web 和 API 相关的 需求。 它是一个轻量级框架,具有辅助类和方法、内置服务器、调试器和重新加载器,这些都是构建可扩展 Web 应用程序和 Web 服务所必需的。

与 Django 框架不同,Flask 更为简约和精简,因为它需要更多的 Python 使用经验来制作各种编码技巧和解决方案以实现其组件。 它比全栈的 Django 更具开放性和可扩展性,Django 因其易于构建的项目和可重用组件而更适合新手。

本章将展示涵盖 Flask 3.x 初始组件和基础功能的必要任务流程,这些功能对于启动我们的 Web 开发至关重要。

在本章中,我们将涵盖以下 开发任务:

  • 设置 项目基线

  • 创建路由 和导航

  • 管理请求和 响应数据

  • 实现 视图模板

  • 创建 Web 表单

  • 使用 PostgreSQL 构建数据层

  • 管理项目结构

技术要求

第一章将专注于构建一个 在线个人咨询系统 的原型,该系统模拟患者与咨询师之间的面对面咨询,同时突出显示 Flask 3.x的基础组件。 该应用程序将涵盖用户管理、问卷和一些报告等模块。 本章的代码可以在 github.com/PacktPublishing/Mastering-Flask-Web-Development/tree/main/ch01找到。

设置项目基线

收集和研究 提议项目的开发环境系统需求是至关重要的。 这些需求包括安装程序和库的正确版本、适当的服务器以及包含其他必要依赖项。 在启动我们的项目之前,我们必须进行各种设置。

安装最新版本的 Python

我们所有的应用程序都将 *Python 11 *环境中运行,以实现更快的性能。 适用于所有操作系统的最新 Python 安装程序可在 以下位置获取 https://www.python.org/downloads/

安装 Visual Studio (VS) Code 编辑器

Django 框架有一个 <st c="2364">django-admin</st> 命令可以生成项目结构,但 Flask 没有这个功能。 我们可以使用终端控制台或像 Visual Studio (VS)** Code** 编辑器这样的工具来帮助开发者创建 Flask 项目。 VS Code 安装程序可在 以下位置获取 https://code.visualstudio.com/download

在安装了 VS Code 编辑器之后,我们可以通过它创建一个文件系统文件夹并启动一个 Flask 项目。 要创建文件夹,我们应该前往 打开文件夹 选项,位于 文件 下,或者使用 *Ctrl *+K *+O *快捷键来打开 打开文件夹 迷你窗口。 图 1.1**.1 展示了使用 编辑器创建 Flask 项目的示例过程:

图 1.1 – 使用 VS Code 编辑器创建 Flask 项目文件夹

图 1.1 – 使用 VS Code 编辑器创建 Flask 项目文件夹

创建虚拟环境

开发 Flask 项目的另一个方面是拥有一个 称为 虚拟环境 的仓库,可以存放其库。 这是一个机制或工具,可以通过隔离这些依赖项从全局仓库和其他项目 依赖项中,来管理项目的所有依赖项。 以下是在开发基于 Flask 的应用程序时使用此工具的优势:

  • 它可以避免损坏的模块版本,以及与其他现有类似的全局 仓库库的冲突。

  • 它可以帮助为 项目构建依赖树。

  • 它可以帮助简化将应用程序与库部署到物理和 基于云的服务器。

需要一个名为 <st c="4700">virtualenv</st> 的 Python 扩展来设置这些虚拟环境。 要安装扩展,请在 终端 中运行以下命令:

 pip install virtualenv

安装完成后,我们需要运行 <st c="4893">python virtualenv -m ch01-01</st> 来为我们的 Flask 项目创建第一个虚拟环境。 图 1**.2 显示了创建我们的 <st c="5029">ch01-env</st> 存储库的快照:

图 1.2 – 创建虚拟环境

图 1.2 – 创建虚拟环境

下一步是打开 项目并将其链接到为其创建的虚拟环境。 在 VS Code 中按 Ctrl + Shift + P 将打开 <st c="5522">Python: Select Interpreter</st>。点击此选项将带您到 <st c="5730">Python.exe</st> 文件所在的 <st c="5753">/Scripts</st> 文件夹。 图 1**.3 显示了在存储库的 <st c="5853">/</st>``<st c="5854">Scripts</st> 文件夹中定位 Python 解释器的快照:

图 1.3 – 定位虚拟环境的 Python 解释器

图 1.3 – 定位虚拟环境的 Python 解释器

之后,必须激活虚拟环境才能让项目使用它。 您必须在 Windows 中运行 <st c="6928">/Scripts/activate.bat</st> 或在 Linux 中通过编辑器的内部 控制台 运行 <st c="6964">/bin/activate</st> 激活后,终端的提示符应显示虚拟环境名称(例如, <st c="7132">(</st>``<st c="7133">ch01-env) C:\</st>)。

安装 Flask 3.x 库

VS Code 的集成 终端将在右键点击编辑器中的资源管理器部分后出现,这将导致 <st c="7497">pip</st> <st c="7501">install flask</st>.

一旦所有要求都满足,我们就可以创建我们的 基线应用程序

创建 Flask 项目

必须在主项目文件夹中(即, <st c="7714">ch01</st>)实现第一个组件,该组件是应用程序文件,可以是 <st c="7759">main.py</st> 有时 <st c="7780">app.py</st>。此组件将成为服务器启动时 Flask 识别的顶级模块。 以下是我们的 *在线个人咨询 * *系统 * 原型 的基本应用程序文件:

<st c="7975">from flask import Flask</st>
<st c="7999">app = Flask(__name__)</st> @app.route('/', methods = ['GET'])
def index():
    return "This is an online … counseling system (OPCS)" <st c="8233">main.py</st> file:

				*   <st c="8246">An imported</st> `<st c="8259">Flask</st>` <st c="8264">class from the</st> `<st c="8280">flask</st>` <st c="8285">package plays a considerable role in building the application.</st> <st c="8349">This class provides all the utilities that implement the</st> `<st c="8708">Flask</st>` <st c="8713">instance is the main element in building a</st> **<st c="8757">Web Server Gateway Interface</st>** <st c="8785">(</st>**<st c="8787">WSGI</st>**<st c="8791">)-compliant</st> <st c="8804">application.</st>

			<st c="8816">Werkzeug</st>
			`<st c="8825">Werkzeug</st>` <st c="8834">is a WSGI-based library</st> <st c="8859">or module that provides Flask with the necessary utilities, including a built-in server, for running</st> <st c="8960">WSGI-based applications.</st>

				*   <st c="8984">The imported</st> `<st c="8998">Flask</st>` <st c="9003">instance must be instantiated once per application.</st> <st c="9056">The</st> `<st c="9060">__name__</st>` <st c="9068">argument must be passed to its constructor to provide</st> `<st c="9123">Flask</st>` <st c="9128">with a reference to the main module without explicitly setting its actual package.</st> <st c="9212">Its purpose is to provide Flask with the reach it needs in providing the utilities across the application and to register the components of the project to</st> <st c="9367">the framework.</st>
				*   <st c="9381">The</st> `<st c="9386">if</st>` <st c="9388">statement tells the Python interpreter to run Werkzeug’s built-in development server if the module is</st> `<st c="9491">main.py</st>`<st c="9498">. This line validates the</st> `<st c="9524">main.py</st>` <st c="9531">module as the top-level module of</st> <st c="9566">the project.</st>
				*   `<st c="9578">app.run()</st>` <st c="9588">calls and starts the built-in development server of Werkzeug.</st> <st c="9651">Setting its</st> `<st c="9663">debug</st>` <st c="9668">parameter to</st> `<st c="9682">True</st>` <st c="9686">sets development or debug mode and enables Werkzeug’s debugger tool and automatic reloading.</st> <st c="9780">Another way is to create a configuration file that will set</st> `<st c="9840">FLASK_DEBUG</st>` <st c="9851">to</st> `<st c="9855">True</st>`<st c="9859">. We can also set development mode by running</st> `<st c="9905">main.py</st>` <st c="9912">using the</st> `<st c="9923">flask run</st>` <st c="9932">command with the</st> `<st c="9950">--debug</st>` <st c="9957">option.</st> <st c="9966">Other configuration approaches before Flask 3.0, such as using</st> `<st c="10029">FLASK_ENV</st>`<st c="10038">, are</st> <st c="10044">already deprecated.</st>

			<st c="10063">Running the</st> `<st c="10076">python main.py</st>` <st c="10090">command on the VS Code terminal will start the built-in development server and run our application.</st> <st c="10191">A server log will be displayed on the console with details that include the development mode, the debugger ID, and the</st> `<st c="10343">5000</st>`<st c="10347">, while the host</st> <st c="10364">is</st> `<st c="10367">localhost</st>`<st c="10376">.</st>
			<st c="10377">Now, it is time to explore the view functions of our Flask application.</st> <st c="10450">These are the components that manage</st> <st c="10487">the incoming requests and</st> <st c="10513">outgoing responses.</st>
			<st c="10532">Creating routes and navigations</st>
			**<st c="10564">Routing</st>** <st c="10572">is a mapping of</st> <st c="10588">URL pattern(s) and other related details to a view function that’s done using Flask’s route decorators.</st> <st c="10693">On the other hand, the view function is a transaction that processes an incoming request from the clients and, at the same time, returns the necessary response to them.</st> <st c="10862">It follows a life cycle and returns an HTTP status as part of</st> <st c="10924">its response.</st>
			<st c="10937">There are different approaches to assigning URL patterns to view functions.</st> <st c="11014">These include creating static and dynamic URL patterns, mapping URLs externally, and mapping multiple URLs to a</st> <st c="11126">view function.</st>
			<st c="11140">Creating static URLs</st>
			<st c="11161">Flask has several built-in route</st> <st c="11194">decorators that implement some of its components, and</st> `<st c="11249">@route</st>` <st c="11255">decorator is one of these.</st> `<st c="11283">@route</st>` <st c="11289">directly maps the URL address to the view function seamlessly.</st> <st c="11353">For instance,</st> `<st c="11367">@route</st>` <st c="11373">maps the</st> `<st c="11383">index()</st>` <st c="11390">view function presented in the project’s</st> `<st c="11432">main.py</st>` <st c="11439">file to the root URL or</st> `<st c="11464">/</st>`<st c="11465">, which makes</st> `<st c="11479">index()</st>` <st c="11486">the view function of the</st> <st c="11512">root URL.</st>
			<st c="11521">But</st> `<st c="11526">@route</st>` <st c="11532">can map any valid URL pattern to any view function.</st> <st c="11585">A URL pattern is accepted if it follows the following</st> <st c="11639">best practices:</st>

				*   <st c="11654">All characters must be</st> <st c="11678">in lowercase.</st>
				*   <st c="11691">Use only forward slashes to establish</st> <st c="11730">site hierarchy.</st>
				*   <st c="11745">URL names must be concise, clear, and within the</st> <st c="11795">business context.</st>
				*   <st c="11812">Avoid spaces and special symbols and characters as much</st> <st c="11869">as possible.</st>

			<st c="11881">The following</st> `<st c="11896">home()</st>` <st c="11902">view function renders an introductory page of our</st> `<st c="11953">ch01</st>` <st c="11957">application and uses the URL pattern of</st> `<st c="11998">/home</st>` <st c="12003">for</st> <st c="12008">its access:</st>

@app.route('/home')

def home(): return '''

<html><head><title>Online Personal … System</title>

    </head><body>

    <h1>Online … Counseling System (OPCS)</h1>

    <p>这是一个基于网络的咨询

        application where counselors can … … …</em>

    </body></html>

'''

			<st c="12282">Now, Flask accepts simple URLs such as</st> `<st c="12322">/home</st>` <st c="12327">or complex ones with slashes and path-like hierarchy, including</st> <st c="12391">these</st> <st c="12398">multiple URLs.</st>
			<st c="12412">Assigning multiple URLs</st>
			<st c="12436">A view function can have a</st> <st c="12464">stack of</st> `<st c="12473">@route</st>` <st c="12479">decorators annotated on it.</st> <st c="12508">Flask allows us to map these valid multiple URLs if there is no conflict with other view functions and within that stack of</st> `<st c="12632">@route</st>` <st c="12638">mappings.</st> <st c="12649">The following version of the</st> `<st c="12678">home()</st>` <st c="12684">view function now has three URLs, which means any of these addresses can render the</st> <st c="12769">home page:</st>

@app.route('/home')

@app.route('/information')

@app.route('/introduction')

def home(): return '''

        <title>Online Personal … System</title>

    </head><body>

    <h1>Online … Counseling System (OPCS)</h1>

        … … … … …

    </body></html>

'''

			<st c="13015">Aside from complex URLs, Flask is</st> <st c="13050">also capable of creating</st> *<st c="13075">dynamic routes</st>*<st c="13089">.</st>
			<st c="13090">Applying path variables</st>
			<st c="13114">Adding path variables makes a URL</st> <st c="13149">dynamic and changeable depending on the variations of the values passed to it.</st> <st c="13228">Although some SEO experts may disagree with having dynamic URLs, the Flask framework can allow view functions with changeable URL patterns to</st> <st c="13370">be implemented.</st>
			<st c="13385">In Flask, a path variable is declared inside a diamond operator (</st>`<st c="13451"><></st>`<st c="13454">) and placed within the URL path.</st> <st c="13489">The following view function has a dynamic URL with several</st> <st c="13548">path variables:</st>

@app.route('/exam/passers/list/float:rate/uuid:docId') def report_exam_passers(rating:float, docId:uuid4 = None):

exams = list_passing_scores(<st c="13712">rating</st>)

response = make_response(

render_template('exam/list_exam_passers.html',

    exams=exams, docId=<st c="13814">docId</st>), 200)

return response

			<st c="13844">As we can see, path variables are identified with data types inside the diamond operator (</st>`<st c="13935"><></st>`<st c="13938">) using the</st> `<st c="13951"><type:variable></st>` <st c="13966">pattern.</st> <st c="13976">These parameters are set to</st> `<st c="14004">None</st>` <st c="14008">if the path variables are optional.</st> <st c="14045">The path variable is considered a string type by default if it has no associated type hint.</st> *<st c="14137">Flask 3.x</st>* <st c="14146">offers these built-in data types for</st> <st c="14184">path variables:</st>

				*   **<st c="14199">string</st>**<st c="14206">: Allows all valid characters except</st> <st c="14244">for slashes.</st>
				*   **<st c="14256">int</st>**<st c="14260">: Takes</st> <st c="14269">integer values.</st>
				*   **<st c="14284">float</st>**<st c="14290">: Accepts</st> <st c="14301">real numbers.</st>
				*   **<st c="14314">uuid</st>**<st c="14319">: Takes unique 32 hexadecimal digits that are used to identify or represent records, documents, hardware gadgets, software licenses, and</st> <st c="14457">other information.</st>
				*   **<st c="14475">path</st>**<st c="14480">: Fetches characters,</st> <st c="14503">including slashes.</st>

			<st c="14521">These path variables can’t function</st> <st c="14558">without the corresponding parameters of the same name and type declared in the view function’s parameter list.</st> <st c="14669">In the previous</st> `<st c="14685">report_exam_passers()</st>` <st c="14706">view function, the local</st> `<st c="14732">rating</st>` <st c="14738">and</st> `<st c="14743">docId</st>` <st c="14748">parameters are the variables that will hold the values of the path</st> <st c="14816">variables, respectively.</st>
			<st c="14840">But there are particular or rare cases where path variables should be of a type different than the supported ones.</st> <st c="14956">View functions with path variables declared as</st> `<st c="15003">list</st>`<st c="15007">,</st> `<st c="15009">set</st>`<st c="15012">,</st> `<st c="15014">date</st>`<st c="15018">, or</st> `<st c="15023">time</st>` <st c="15027">will throw</st> `<st c="15039">Status Code 500</st>` <st c="15054">in Flask.</st> <st c="15065">As a workaround, the Werkzeug bundle of libraries offers a</st> `<st c="15124">BaseConverter</st>` <st c="15137">utility class that can help customize a variable type for paths that allows other types to be part of the type hints.</st> <st c="15256">The following view function requires a</st> `<st c="15295">date</st>` <st c="15299">type hint to generate a certificate in</st> <st c="15339">HTML format:</st>

@app.route('/certificate/accomp/string:name/string:course/date:accomplished_date') def show_certification(name:str, course:str, accomplished_date:date):

certificate = """<html><head>

    <title>Certificate of Accomplishment</title>

    </head><body>

    <h1>成就证书</h1>

    <p>参与者 {} 被授予此成就证书,在 {} 课程于 {} 日期通过所有考试。他/她证明了自己为未来的任何努力都做好了准备。</em>

    </body></html>

""".format(<st c="15860">name, course, accomplished_date</st>)

return certificate, 200

			`<st c="15918">accomplished_date</st>` <st c="15936">in</st> `<st c="15940">show_certification()</st>` <st c="15960">is a</st> `<st c="15966">date</st>` <st c="15970">hint type and will not be valid until the following tasks</st> <st c="16029">are implemented:</st>

				*   <st c="16045">First, subclass</st> `<st c="16062">BaseConverter</st>` <st c="16075">from the</st> `<st c="16085">werkzeug.routing</st>` <st c="16101">module.</st> <st c="16110">In the</st> `<st c="16117">/converter</st>` <st c="16127">package of this project, there is a module called</st> `<st c="16178">date_converter.py</st>` <st c="16195">that implements our</st> `<st c="16216">date</st>` <st c="16220">hint type, as shown in the</st> <st c="16248">following code:</st>

    ```

    <st c="16263">from werkzeug.routing import BaseConverter</st> from datetime import datetime

    class DateConverter(<st c="16357">BaseConverter</st>): <st c="16375">def to_python(self, value):</st> date_value = datetime.strptime(value, "%Y-%m-%d")

        return date_value

    ```py

    <st c="16470">The given</st> `<st c="16481">DateConverter</st>` <st c="16494">will custom-handle date variables within our</st> <st c="16540">Flask application.</st>

    				*   `<st c="16558">BaseConverter</st>` <st c="16572">has a</st> `<st c="16579">to_python()</st>` <st c="16590">method that must be overridden to implement the necessary conversion</st> <st c="16659">process.</st> <st c="16669">In the case of</st> `<st c="16684">DateConverter</st>`<st c="16697">, we need</st> `<st c="16707">strptime()</st>` <st c="16717">so that we can convert the path variable value in the</st> `<st c="16772">yyyy-mm-dd</st>` <st c="16782">format into the</st> <st c="16799">datetime type.</st>
				*   <st c="16813">Lastly, declare our new custom converter in the Flask instance of the</st> `<st c="16884">main.py</st>` <st c="16891">module.</st> <st c="16900">The following snippet registers</st> `<st c="16932">DateConverter</st>` <st c="16945">to</st> `<st c="16949">app</st>`<st c="16952">:</st>

    ```

    app = Flask(__name__) <st c="16977">app.url_map.converters['date'] = DateConverter</st>

    ```py

			<st c="17023">After following all these steps, the custom path variable type – for instance,</st> `<st c="17103">date</st>` <st c="17107">– can now be utilized across</st> <st c="17137">the application.</st>
			<st c="17153">Assigning URLs externally</st>
			<st c="17179">There is also a way to</st> <st c="17202">implement a routing mechanism without using the</st> `<st c="17251">@route</st>` <st c="17257">decorator, and that’s by utilizing Flask’s</st> `<st c="17301">add_url_rule()</st>` <st c="17315">method to register views.</st> <st c="17342">This approach binds a valid request handler to a unique URL pattern for every call to</st> `<st c="17428">add_url_rule()</st>` <st c="17442">of the</st> `<st c="17450">app</st>` <st c="17453">instance in the</st> `<st c="17470">main.py</st>` <st c="17477">module, not in the handler’s module scripts, thus making this approach an external way of building routes.</st> <st c="17585">The following arguments are needed by the</st> `<st c="17627">add_url_rule()</st>` <st c="17641">method to</st> <st c="17652">perform mapping:</st>

				*   <st c="17668">The URL pattern with or without the</st> <st c="17705">path variables.</st>
				*   <st c="17720">The URL name and, usually, the exact name of the</st> <st c="17770">view function.</st>
				*   <st c="17784">The view</st> <st c="17794">function itself.</st>

			<st c="17810">The invocation of this method must be in the</st> `<st c="17856">main.py</st>` <st c="17863">file, anywhere after its</st> `<st c="17889">@route</st>` <st c="17895">implementations and view imports.</st> <st c="17930">The following</st> `<st c="17944">main.py</st>` <st c="17951">snippet shows the external route mapping of the</st> `<st c="18000">show_honor_dismissal()</st>` <st c="18022">view function to its dynamic URL pattern.</st> <st c="18065">This view function generates a termination letter for the counseling and consultation agreement between a clinic and</st> <st c="18182">a patient:</st>

app = Flask(name)

def show_honor_dissmisal(counselor:str, effective_date:date, patient:str):

letter = """

… … … … …

</head><body>

    <h1> 咨询终止 </h1>

    <p>发件人: <st c="18377">{}</st> <p>顾问负责人

    <p>日期: <st c="18408">{}</st> <p>收件人: <st c="18418">{}</st> <p>主题: 咨询终止

                <p>亲爱的 {},

                … … … … … …

                <p>此致敬礼,

                <p><st c="18508">{}</st> </body>

        </html>

""".format(<st c="18539">counselor, effective_date, patient, patient, counselor</st>)

return letter, 200 <st c="18818">add_url_rule()</st>不仅限于装饰函数视图,对于</st> *<st c="18912">基于类的视图</st>* 也是必要的。

        <st c="18930">实现基于类的视图</st>

        <st c="18961">创建视图层还有另一种方法是通过 Flask 的基于类的视图方法。</st> <st c="18984">与使用混入编程实现其基于类的视图的 Django 框架不同,Flask 提供了两个 API 类,即</st> `<st c="19178">View</st>` <st c="19182">和</st> `<st c="19187">MethodView</st>`<st c="19197">,可以直接从任何自定义</st> <st c="19237">视图实现中继承。</st>

        <st c="19258">实现 HTTP</st> `<st c="19311">GET</st>` <st c="19314">操作的通用类是来自</st> `<st c="19333">flask.views</st>` <st c="19337">模块的</st> `<st c="19353">View</st>` <st c="19364">类。</st> <st c="19373">它有一个</st> `<st c="19382">dispatch_request()</st>` <st c="19400">方法,该方法执行请求-响应事务,就像典型的视图函数一样。</st> <st c="19486">因此,子类必须重写这个核心方法来实现它们自己的视图事务。</st> <st c="19572">以下类,</st> `<st c="19593">ListUnpaidContractView</st>`<st c="19615">,渲染了需要支付给诊所的患者的列表:</st>
<st c="19676">from flask.views import View</st>
<st c="19705">class ListUnpaidContractView(View):</st><st c="19741">def dispatch_request(self):</st> contracts = select_all_unpaid_patient()
        return render_template("contract/ list_patient_contract.html", contracts=contracts)
        `<st c="19893">select_all_unpaid_patient()</st>` <st c="19921">将从数据库中提供患者记录。</st> <st c="19974">所有这些记录都将渲染到</st> `<st c="20016">list_patient_contract.html</st>` <st c="20042">模板中。</st> <st c="20053">现在,除了重写</st> `<st c="20084">dispatch_request()</st>` <st c="20102">方法外,</st> `<st c="20111">ListUnpaidContractView</st>` <st c="20133">还从</st> `<st c="20195">View</st>` <st c="20199">类继承了所有属性和辅助方法,包括</st> `<st c="20221">as_view()</st>` <st c="20230">静态方法,该方法为视图创建一个视图名称。</st> <st c="20286">在视图注册期间,此视图名称将作为</st> `<st c="20345">view_func</st>` <st c="20354">名称</st> <st c="20360">,在</st> `<st c="20374">View</st>` <st c="20378">类的</st> `<st c="20392">add_url_rule()</st>` <st c="20406">方法中,与其映射的 URL 模式一起使用。</st> <st c="20443">以下</st> `<st c="20457">main.py</st>` <st c="20464">片段显示了如何</st> <st c="20486">注册</st> `<st c="20495">ListUnpaidContractView</st>`<st c="20517">:</st>
 app.<st c="20636">View</st> subclass needs an HTTP <st c="20664">POST</st> transaction, it has a built-class class attribute called <st c="20726">methods</st> that accepts a list of HTTP methods the class needs to support. Without it, the default is the <st c="20829">[ "GET" ]</st> value. Here is another custom <st c="20869">View</st> class of our *<st c="20887">Online Personal Counselling System</st>* app that deletes existing patient contracts of the clinic:

class DeleteContractByPIDView(View):methods = ['GET', 'POST'] … … … … … …

def dispatch_request(self):

if request.method == "GET":

    pids = list_pid()

    return render_template("contract/ delete_patient_contract.html", pids=pids)

else:

    pid = int(request.form['pid'])

    result = delete_patient_contract_pid(pid)

    if result == False:

        pids = list_pid()

        return render_template("contract/ delete_patient_contract.html", pids=pids)

    contracts = select_all_patient_contract()

    return render_template("contract/ list_patient_contract.html", contracts=contracts)

			`<st c="21523">DeleteContractByPIDView</st>` <st c="21547">handles a typical form-handling transaction, which has both a</st> `<st c="21610">GET</st>` <st c="21613">operation for loading the form page and a</st> `<st c="21656">POST</st>` <st c="21660">operation to manage the submitted form data.</st> <st c="21706">The</st> `<st c="21710">POST</st>` <st c="21714">operation will verify if the patient ID submitted by the form page exists, and it will eventually delete the contract(s) of the patient using the patient ID and render an updated list</st> <st c="21899">of contracts.</st>
			<st c="21912">Other than the</st> `<st c="21928">View</st>` <st c="21932">class, an alternative API that</st> <st c="21963">can also build view transactions is the</st> `<st c="22004">MethodView</st>` <st c="22014">class.</st> <st c="22022">This class is suitable for web forms since it has the built-in</st> `<st c="22085">GET</st>` <st c="22088">and</st> `<st c="22093">POST</st>` <st c="22097">hints or templates that subclasses need to define but without the need to identify the</st> `<st c="22185">GET</st>` <st c="22188">transactions from</st> `<st c="22207">POST</st>`<st c="22211">, like in a view function.</st> <st c="22238">Here is a view that uses</st> `<st c="22263">MethodView</st>` <st c="22273">to manage the contracts of the patients in</st> <st c="22317">the clinic:</st>

从 flask.views 导入 MethodView

class ContractView(MethodView): … … … … … … def get(self): return render_template("contract/ add_patient_contract.html") def post(self): pid = request.form['pid']

    approver = request.form['approver']

    … … … … … …

    result = insert_patient_contract(pid=int(pid), approved_by=approver, approved_date=approved_date, hcp=hcp, payment_mode=payment_mode, amount_paid=float(amount_paid), amount_due=float(amount_due))

    if result == False:

    return render_template("contract/ add_patient_contract.html")

    contracts = select_all_patient_contract()

    return render_template("contract/ list_patient_contract.html", contracts=contracts)

			<st c="22977">The</st> `<st c="22982">MethodView</st>` <st c="22992">class does not have a</st> `<st c="23015">methods</st>` <st c="23022">class variable to indicate the HTTP methods supported by the view.</st> <st c="23090">Instead, the subclass can select the appropriate HTTP hints from</st> `<st c="23155">MethodView</st>`<st c="23165">, which will then implement the required HTTP transactions of the custom</st> <st c="23238">view class.</st>
			<st c="23249">Since</st> `<st c="23256">MethodView</st>` <st c="23266">is a subclass of the</st> `<st c="23288">View</st>` <st c="23292">class, it also has an</st> `<st c="23315">as_view()</st>` <st c="23324">class method that creates a</st> `<st c="23353">view_func</st>` <st c="23362">name of the view.</st> <st c="23381">This is also necessary for</st> `<st c="23408">add_url_rule()</st>` <st c="23422">registration.</st>
			<st c="23436">Aside from</st> `<st c="23448">GET</st>` <st c="23451">and</st> `<st c="23456">POST</st>`<st c="23460">, the</st> `<st c="23466">MethodView</st>` <st c="23476">class also provides the</st> `<st c="23501">PUT</st>`<st c="23504">,</st> `<st c="23506">PATCH</st>`<st c="23511">, and</st> `<st c="23517">DELETE</st>` <st c="23523">method hints for API-based</st> <st c="23550">applications.</st> `<st c="23565">MethodView</st>` <st c="23575">is better than the</st> `<st c="23595">View</st>` <st c="23599">API because it organizes the transactions according to HTTP methods and checks and executes these HTTP methods by itself at runtime.</st> <st c="23733">In general, between the decorated view function and the class-based ones, the latter approach provides a complete Flask view component because of the attributes and built-in methods inherited by the view implementation from these API classes.</st> <st c="23976">Although the decorated view function can support a flexible and open-ended strategy for scalable applications, it cannot provide an organized base functionality that can supply baseline view features to other related views, unlike in a class-based approach.</st> <st c="24234">However, the choice still depends on the scope and requirements of</st> <st c="24301">the application.</st>
			<st c="24317">Now that we’ve created and registered the routes, let’s scrutinize these view implementations and identify the</st> <st c="24428">essential Flask components that</st> <st c="24461">compose them.</st>
			<st c="24474">Managing request and response data</st>
			<st c="24509">At this point, we already know that routing is a mechanism for mapping view functions to their URLs.</st> <st c="24611">But besides that, routing declares any valid functions to be view implementations that can manage the incoming request and</st> <st c="24734">outgoing response.</st>
			<st c="24752">Retrieving the request object</st>
			<st c="24782">Flask uses its</st> `<st c="24798">request</st>` <st c="24805">object to carry</st> <st c="24822">cookies, headers, parameters, form data, form objects, authorization data, and other request-related details.</st> <st c="24932">But the view function doesn’t need to declare a variable to auto-wire the request instance, just like in Django, because Flask has a built-in proxy object for it, the</st> `<st c="25099">request</st>` <st c="25106">object, which is part of the</st> `<st c="25136">flask</st>` <st c="25141">package.</st> <st c="25151">The following view function takes the</st> `<st c="25189">username</st>` <st c="25197">and</st> `<st c="25202">password</st>` <st c="25210">request parameters and checks if the credentials are in</st> <st c="25267">the database:</st>

main 导入 app

flask 导入 request, Response, render_template, redirect

从 repository.user 导入 validate_user

@app.route('/login/params')

def login_with_params(): username = request.args['username']password = request.args['password'] result = validate_user(username, password)

if result:

resp = Response(

response=render_template('/main.html'), 状态=200, 内容类型='text/html')

return resp

else:

    return redirect('/error')

			<st c="25728">For instance, running the URL pattern of the given view function,</st> `<st c="25795">http://localhost:5000/login/params?username=sjctrags&password=sjctrags2255</st>`<st c="25869">, will provide us with</st> `<st c="25892">sjctrags</st>` <st c="25900">and</st> `<st c="25905">sjctrags2255</st>` <st c="25917">as values when</st> `<st c="25933">request.args['username']</st>` <st c="25957">and</st> `<st c="25962">request.args['password']</st>` <st c="25986">are</st> <st c="25991">accessed, respectively.</st>
			<st c="26014">Here is the complete list of objects</st> <st c="26052">and details that we can retrieve from the</st> `<st c="26094">Request</st>` <st c="26101">object through its request</st> <st c="26129">instance proxy:</st>

				*   `<st c="26144">request.args</st>`<st c="26157">: Returns a</st> `<st c="26170">MultiDict</st>` <st c="26179">class that carries URL arguments or request parameters from the</st> <st c="26244">query string.</st>
				*   `<st c="26257">request.form</st>`<st c="26270">: Returns a</st> `<st c="26283">MultiDict</st>` <st c="26292">class that contains parameters from an HTML form or JavaScript’s</st> `<st c="26358">FormData</st>` <st c="26366">object.</st>
				*   `<st c="26374">request.data</st>`<st c="26387">: Returns request data in a byte stream that Flask couldn’t parse to form parameters and values due to an unrecognizable</st> <st c="26509">mime type.</st>
				*   `<st c="26519">request.files</st>`<st c="26533">: Returns a</st> `<st c="26546">MultiDict</st>` <st c="26555">class containing all file objects from a form</st> <st c="26602">with</st> `<st c="26607">enctype=multipart/form-data</st>`<st c="26634">.</st>
				*   `<st c="26635">request.get_data()</st>`<st c="26654">: This function returns the request data in byte streams before</st> <st c="26719">calling</st> `<st c="26727">request.data</st>`<st c="26739">.</st>
				*   `<st c="26740">request.json</st>`<st c="26753">: Returns parsed JSON data when the incoming request has a</st> `<st c="26813">Content-Type</st>` <st c="26825">header</st> <st c="26833">of</st> `<st c="26836">application/json</st>`<st c="26852">.</st>
				*   `<st c="26853">request.method</st>`<st c="26868">: Returns the HTTP</st> <st c="26888">method name.</st>
				*   `<st c="26900">request.values</st>`<st c="26915">: Returns the combined parameters of</st> `<st c="26953">args</st>` <st c="26957">and</st> `<st c="26962">form</st>` <st c="26966">and encounters collision problems when both</st> `<st c="27011">args</st>` <st c="27015">and</st> `<st c="27020">form</st>` <st c="27024">carry the same</st> <st c="27040">parameter name.</st>
				*   `<st c="27055">request.headers</st>`<st c="27071">: Returns request headers included in the</st> <st c="27114">incoming request.</st>
				*   `<st c="27131">request.cookies</st>`<st c="27147">: Returns all the cookies that are part of</st> <st c="27191">the request.</st>

			<st c="27203">The following view function utilizes some of the given request objects to perform an HTTP</st> `<st c="27294">GET</st>` <st c="27297">operation to fetch a user login</st> <st c="27330">application through an</st> `<st c="27353">ID</st>` <st c="27355">value and an HTTP</st> `<st c="27374">POST</st>` <st c="27378">operation to retrieve the user details, approve its preferred user role, and save the login details as new, valid</st> <st c="27493">user credentials:</st>

main 导入 app

从 flask 导入 render_template

从 model.candidates 导入 AdminUser, CounselorUser, PatientUser

从 urllib.parse 导入 parse_qsl

@app.route('/signup/approve', methods = ['POST'])

@app.route('/signup/approve/int:utype',methods = ['GET'])

def signup_approve(utype:int=None):

if (request.method == 'GET'): <st c="27848">id = request.args['id']</st> user = select_single_signup(id)

    … … … … … … …

else:

    utype = int(utype)

    if int(utype) == 1: <st c="27963">adm = request.get_data()</st> adm_dict = dict(parse_qsl(adm.decode('utf-8')))

        adm_model = AdminUser(**adm_dict)

        user_approval_service(int(utype), adm_model)

    elif int(utype) == 2: <st c="28137">cnsl = request.get_data()</st> cnsl_dict = dict(parse_qsl(

            cnsl.decode('utf-8')))

        cnsl_model = CounselorUser(**cnsl_dict)

        user_approval_service(int(utype), cnsl_model)

    elif int(utype) == 3: <st c="28322">pat = request.get_data()</st> pat_dict = dict(parse_qsl(pat.decode('utf-8')))

        pat_model = PatientUser(**pat_dict)

        user_approval_service(int(utype), pat_model)

    return render_template('approved_user.html', message='approved'), 200

			<st c="28545">Our application has a listing view that renders hyperlinks that can redirect users to this</st> `<st c="28637">signup_approve()</st>` <st c="28653">form page with a context variable</st> `<st c="28688">id</st>`<st c="28690">, a code for a user type.</st> <st c="28716">The view function retrieves the variable</st> `<st c="28757">id</st>` <st c="28759">through</st> `<st c="28768">request.args</st>`<st c="28780">, checks what the user type</st> `<st c="28808">id</st>` <st c="28810">is, and renders the appropriate page based on the user type detected.</st> <st c="28881">The function also uses</st> `<st c="28904">request.method</st>` <st c="28918">to check if the user request will pursue either the</st> `<st c="28971">GET</st>` <st c="28974">or</st> `<st c="28978">POST</st>` <st c="28982">transaction since the given view function caters to both HTTP methods, as defined in its</st> *<st c="29072">dual</st>* <st c="29076">route</st> <st c="29083">declaration.</st> <st c="29096">When clicking the</st> `<st c="29150">POST</st>` <st c="29154">transaction retrieves all the form parameters and values in a byte stream type via</st> `<st c="29238">request.get_data()</st>`<st c="29256">. It is decoded to a query string object and converted into a dictionary by</st> `<st c="29332">parse_sql</st>` <st c="29341">from the</st> `<st c="29351">urllib.parse</st>` <st c="29363">module.</st>
			<st c="29371">Now, if Flask can handle the request, it can also manage the outgoing response from the</st> <st c="29460">view functions.</st>
			<st c="29475">Creating the response object</st>
			<st c="29504">Flask uses</st> `<st c="29516">Response</st>` <st c="29524">to generate a</st> <st c="29538">client response for every request.</st> <st c="29574">The following view function renders a form page using the</st> `<st c="29632">Response</st>` <st c="29640">object:</st>

从 flask 导入 render_template, request, Response

@app.route('/admin/users/list')

def generate_admin_users():

users = select_admin_join_user()

user_list = [list(rec) for rec in users]

content = '''<html><head>

                <title>用户列表</title>

        </head><body>

                <h1>用户列表</h1>

                <p>{}

        </body></html>

    '''.format(user_list) <st c="29967">resp = Response(response=content, status=200,</st> <st c="30012">content_type='text/html')</st> return <st c="30050">Response</st> 是通过其所需的构造函数参数实例化,并由视图函数作为响应对象返回。以下是需要参数:

            +   `<st c="30215">response</st>`<st c="30224">: 包含需要渲染的内容,可以是字符串、字节流或这两种类型的可迭代对象。</st> <st c="30336">两种类型。</st>

            +   `<st c="30346">status</st>`<st c="30353">: 接受整数或字符串形式的 HTTP 状态码。</st> <st c="30399">或字符串。</st>

            +   `<st c="30409">content_type</st>`<st c="30422">: 接受需要渲染的响应对象的 MIME 类型。</st> <st c="30475">需要渲染。</st>

            +   `<st c="30491">headers</st>`<st c="30499">: 包含响应头(s)的字典,这些头对于渲染过程是必要的,例如</st> `<st c="30609">Access-Control-Allow-Origin</st>`<st c="30636">,</st> `<st c="30638">Content-Disposition</st>`<st c="30657">,</st> `<st c="30659">Origin</st>`<st c="30665">,</st> <st c="30667">和</st> `<st c="30671">Accept</st>`<st c="30677">.</st>

        <st c="30678">但如果是为了渲染 HTML 页面,Flask 有一个</st> `<st c="30735">render_template()</st>` <st c="30752">方法,该方法引用需要渲染的 HTML 模板文件。</st> <st c="30820">以下路由函数,</st> `<st c="30850">signup_users_form()</st>`<st c="30869">, 生成注册页面的内容——即,</st> `<st c="30918">add_signup.html</st>` <st c="30933">来自</st> <st c="30939">the</st> `<st c="30943">/pages</st>` <st c="30949">模板文件夹——供新</st> <st c="30976">用户申请人:</st>
 @app.route('/signup/form', methods= ['GET'])
def signup_users_form():
    resp = Response(  response=<st c="31089">render_template('add_signup.html')</st>, status=200, content_type="text/html")
    return resp
        `<st c="31175">render_template()</st>` <st c="31193">返回带有其上下文数据的 HTML 内容,如果有的话,作为一个字符串。</st> <st c="31268">为了简化语法,Flask 允许我们返回方法的结果和</st> *<st c="31346">状态码</st>* <st c="31357">而不是</st> `<st c="31373">Response</st>` <st c="31381">实例,因为框架可以从这些细节自动创建一个</st> `<st c="31438">Response</st>` <st c="31446">实例。</st> <st c="31476">像前面的例子一样,下面的</st> `<st c="31518">signup_list_users()</st>` <st c="31537">使用</st> `<st c="31543">render_template()</st>` <st c="31560">来显示需要管理员批准的新用户申请列表:</st>
 @app.route('/signup/list', methods = ['GET'])
def signup_list_users(): <st c="31845">render_template()</st> can accept and pass context data to the template page. The <st c="31922">candidates</st> variable in this snippet handles an extracted list of records from the database needed by the template for content generation using the <st c="32069">Jinja2</st> engine.
			<st c="32083">Jinja2</st>
			<st c="32090">Jinja2 is Python’s fast, flexible, robust, expressive, and extensive templating engine for creating HTML, XML, LaTeX, and other supported formats for Flask’s</st> <st c="32249">rendition purposes.</st>
			<st c="32268">On the other hand, Flask has a utility called</st> `<st c="32315">make_response()</st>` <st c="32330">that can modify the response by changing headers and cookies before sending them to the client.</st> <st c="32427">This method is suitable when the base response frequently undergoes some changes in its response headers and cookies.</st> <st c="32545">The following code modifies the content type of the original response to XLS with a</st> <st c="32628">given filename – in this</st> <st c="32654">case,</st> `<st c="32660">question.xls</st>`<st c="32672">:</st>

@app.route('/exam/details/list')

def report_exam_list():

exams = list_exam_details() <st c="32760">response = make_response(</st> <st c="32785">render_template('exam/list_exams.html',</st><st c="32825">exams=exams), 200)</st><st c="32844">headers = dict()</st><st c="32861">headers['Content-Type'] = 'application/vnd.ms-excel'</st><st c="32914">headers['Content-Disposition'] =</st> <st c="32947">'attachment;filename=questions.xls'</st><st c="32983">response.headers = headers</st> return response

			<st c="33026">Flask will require additional Python extensions when serializing and yielding PDF, XLSX, DOCX, RTF, and other complex content types.</st> <st c="33160">But for old and simple mime type values such as</st> `<st c="33208">application/msword</st>` <st c="33226">and</st> `<st c="33231">application/vnd.ms-excel</st>`<st c="33255">, Flask can easily and seamlessly serialize the content since Python has a built-in serializer for them.</st> <st c="33360">Other than mime types, Flask also supports adding web cookies for route functions.</st> <st c="33443">The following</st> `<st c="33457">assign_exam()</st>` <st c="33470">route shows how to add cookies to the</st> `<st c="33509">response</st>` <st c="33517">value that renders a form for scheduling and assigning counseling exams for patients with their</st> <st c="33614">respective counselors:</st>

@app.route('/exam/assign', methods=['GET', 'POST'])

def assign_exam():

if request.method == 'GET':

    cids = list_cid()

    pids = list_pid() <st c="33772">response = make_response( render_template('exam/assign_exam_form.html', pids=pids, cids=cids), 200)</st><st c="33871">response.set_cookie('exam_token', str(uuid4()))</st> return response, 200

else:

    id = int(request.form['id'])

    cid = request.form['cid']

    pid = int(request.form['pid'])

    exam_date = request.form['exam_date']

    duration = int(request.form['duration'])

    result = insert_question_details(id=id, cid=cid, pid=pid, exam_date=exam_date, duration=duration)

    if result: <st c="34221">task_token = request.cookies.get('exam_token')</st> task = "exam assignment (task id {})".format(task_token)

        return redirect(url_for('redirect_success_exam', message=task ))

    else:

        return redirect('/exam/task/error')

			<st c="34431">The</st> `<st c="34436">Response</st>` <st c="34444">instance has a</st> `<st c="34460">set_cookie()</st>` <st c="34472">method that creates cookies before the view dispatches the</st> <st c="34531">response to the client.</st> <st c="34556">It also has</st> `<st c="34568">delete_cookie()</st>`<st c="34583">, which deletes a particular cookie before yielding the response.</st> <st c="34649">To retrieve the cookies,</st> `<st c="34674">request.cookies</st>` <st c="34689">has a</st> `<st c="34696">get()</st>` <st c="34701">method that can retrieve the cookie value through its cookie name.</st> <st c="34769">The given</st> `<st c="34779">assign_exam()</st>` <st c="34792">route shows how the</st> `<st c="34813">get()</st>` <st c="34818">method</st> <st c="34825">retrieves</st> `<st c="34836">exam_cookie</st>` <st c="34847">in its</st> `<st c="34855">POST</st>` <st c="34859">transaction.</st>
			<st c="34872">Implementing page redirection</st>
			<st c="34902">Sometimes, it is ideal for the route transaction to redirect the user to another view page using the</st> `<st c="35004">redirect()</st>` <st c="35014">utility</st> <st c="35022">method instead of building its own</st> `<st c="35058">Response</st>` <st c="35066">instance.</st> <st c="35077">Flask redirection requires a URL pattern of the destination to where the view function will redirect.</st> <st c="35179">For instance, in the previous</st> `<st c="35209">assign_exam()</st>` <st c="35222">route, the output of its</st> `<st c="35248">POST</st>` <st c="35252">transaction is not a</st> `<st c="35274">Response</st>` <st c="35282">instance but a</st> `<st c="35298">redirect()</st>` <st c="35308">method:</st>

@app.route('/exam/assign', methods=['GET', 'POST'])

def assign_exam():

    … … … … … …

    if result:

        task_token = request.cookies.get('exam_token')

        task = "exam assignment (task id {})".format(task_token) <st c="35515">return redirect(url_for('redirect_success_exam',</st><st c="35563">message=task ))</st> else: <st c="35631">result</st> variable is <st c="35650">False</st>, redirection to an error view called <st c="35693">/exam/task/error</st> will occur. Otherwise, the route will redirect to an endpoint or view name called <st c="35792">redirect_success_exam</st>. Every <st c="35821">@route</st> has an endpoint equivalent, by default, to its view function name. So, <st c="35899">redirect_success_exam</st> is the function name of a route with the following implementation:
 @app.route('/exam/success', methods=['GET'])
def <st c="36037">redirect_success_exam</st>(): <st c="36063">message = request.args['message']</st> return render_template('exam/redirect_success_view.html', message=message)
        `<st c="36171">url_for()</st>`<st c="36181">, which is used in the</st> `<st c="36204">assign_exam()</st>` <st c="36217">view, is a route handler that allows us to pass the endpoint name of the destination view to</st> `<st c="36311">redirect()</st>` <st c="36321">instead of passing the actual URL pattern of the destination.</st> <st c="36384">It can also pass context data to the Jinja2 template of the redirected page or values to path variables if the view uses a dynamic URL pattern.</st> <st c="36528">The</st> `<st c="36532">redirect_success_exam()</st>` <st c="36555">function shows a perfect scenario of context data passing, where it uses</st> `<st c="36629">request.args</st>` <st c="36641">to access a message context passed from</st> `<st c="36682">assign_exam()</st>`<st c="36695">, which is where the redirection</st> <st c="36728">call originated.</st>

        <st c="36744">More content negotiations</st> <st c="36771">and how to serialize various mime types for responses will be showcased in the succeeding chapters, but in the meantime, let’s scrutinize the view templates of our route functions.</st> <st c="36952">View templates are essential for web-based applications because all form-handling transactions, report generation, and page generation depend on effective</st> <st c="37107">dynamic templates.</st>

        <st c="37125">实现视图模板</st>

        <st c="37153">Jinja2 是 Flask 框架的默认模板引擎,用于创建 HTML、XML、LaTeX 和标记</st> <st c="37268">文档。</st> <st c="37279">它是一种简单、功能丰富、快速且易于使用的模板方法,具有布局功能、内置编程结构、异步操作支持、上下文数据过滤和单元测试实用程序</st> <st c="37510">功能。</st>

        <st c="37523">首先,Flask 要求所有模板文件都必须位于</st> `<st c="37580">templates</st>` <st c="37589">主项目目录的</st> `<st c="37621">Flask()</st>` <st c="37656">构造函数有一个</st> `<st c="37675">template_folder</st>` <st c="37690">参数,可以设置和替换默认目录为另一个目录。</st> <st c="37766">例如,我们的原型具有以下 Flask 实例化,它使用更高级的</st> `<st c="37903">目录名称</st>` <st c="37910">覆盖默认模板目录:</st>
 from flask import Flask
app = Flask(__name__, <st c="38049">pages</st> directory when calling the template files through the <st c="38109">render_template()</st> method.
			<st c="38134">When it comes to syntax, Jinja2 has a placeholder (</st>`<st c="38186">{{ }}</st>`<st c="38192">) that renders dynamic content passed by the view functions to its template file.</st> <st c="38275">It also has a Jinja block (</st>`<st c="38302">{% %}</st>`<st c="38308">) that supports control structures such as loops, conditional statements, macros, and template inheritance.</st> <st c="38417">In the previous route function,</st> `<st c="38449">assign_exam()</st>`<st c="38462">, the</st> `<st c="38468">GET</st>` <st c="38471">transaction retrieves a list of counselor IDs (</st>`<st c="38519">cids</st>`<st c="38524">) and patient IDs (</st>`<st c="38544">pids</st>`<st c="38549">) from the database and passes them to the</st> `<st c="38593">assign_exam_form.html</st>` <st c="38614">template found in the</st> `<st c="38637">exam</st>` <st c="38641">subfolder</st> <st c="38651">of the</st> `<st c="38659">pages</st>` <st c="38664">directory.</st> <st c="38676">The following snippet shows the implementation of the</st> `<st c="38730">assign_exam_form.html</st>` <st c="38751">view template:</st>

<html lang="en"><head><title>患者评分表</title>
</head><body>

    <form action="/exam/score" method="POST">

    <h3>考试分数</h3>

    <label for="qid">输入问卷 ID:</label> <st c="38966"><select name="qid"></st><st c="38985">{% for id in qids %}</st><st c="39006"><option value="{{ id }}">{{ id }}</option></st><st c="39049">{% endfor %}</st><st c="39062"></select></st><br/>

    <label for="pid">输入患者 ID:</label> <st c="39122"><select name="pid"></st><st c="39141">{% for id in pids %}</st><st c="39162"><option value="{{ id }}">{{ id }}</option></st><st c="39205">{% endfor %}</st><st c="39218"></select></st><br/>

    … … … … … …

    <input type="submit" value="分配考试"/>

    </form></body>

			<st c="39311">This template uses the Jinja block to iterate all the IDs and embed each in the</st> `<st c="39392"><option></st>` <st c="39400">tag of the</st> `<st c="39412"><select></st>` <st c="39420">component with the</st> <st c="39440">placeholder operator.</st>
			<st c="39461">More about Jinja2 and Flask 3.x will be</st> <st c="39502">covered in</st> *<st c="39513">Chapter 2</st>*<st c="39522">, but for now, let’s delve into how Flask can implement the most common type of web-based transaction – that is, by capturing form data from</st> <st c="39663">the client.</st>
			<st c="39674">Creating web forms</st>
			<st c="39693">In Flask, we can</st> <st c="39710">choose from the following two approaches when implementing view functions for form</st> <st c="39794">data processing:</st>

				*   <st c="39810">Creating two separate routes, one for the</st> `<st c="39853">GET</st>` <st c="39856">operation and the other for the</st> `<st c="39889">POST</st>` <st c="39893">transaction, as shown for the following user</st> <st c="39939">signup transaction:</st>

    ```

    <st c="39958">@app.route('/signup/form', methods= ['GET'])</st>

    <st c="40003">def signup_users_form():</st> resp = Response(response= render_template('add_signup.html'), status=200, content_type="text/html")

        return resp <st c="40141">@app.route('/signup/submit', methods= ['POST'])</st>

    <st c="40188">def signup_users_submit():</st> username = request.form['username']

        password = request.form['password']

        user_type = request.form['utype']

        firstname = request.form['firstname']

        lastname = request.form['lastname']

        cid = request.form['cid']

        insert_signup(user=username, passw=password, utype=user_type, fname=firstname, lname=lastname, cid=cid)

        return render_template('add_signup_submit.html', message='添加新用户!'), 200

    ```py

    				*   <st c="40606">Utilizing only one view function for both the</st> `<st c="40653">GET</st>` <st c="40656">and</st> `<st c="40661">POST</st>` <st c="40665">transactions, as shown in the</st> <st c="40696">previous</st> `<st c="40705">signup_approve()</st>` <st c="40721">route and in the following</st> `<st c="40749">assign_exam()</st>` <st c="40762">view:</st>

    ```

    <st c="40768">@app.route('/exam/assign', methods=['GET', 'POST'])</st>

    <st c="40820">def assign_exam():</st> if request.method == 'GET':

        cids = list_cid()

        pids = list_pid()

        response = make_response(render_template('exam/assign_exam_form.html', pids=pids, cids=cids), 200)

        response.set_cookie('exam_token', str(uuid4()))

        return response, 200

        else:

        id = int(request.form['id'])

        … … … … … …

        duration = int(request.form['duration'])

        result = insert_question_details(id=id, cid=cid, pid=pid, exam_date=exam_date, duration=duration)

        if result:

            exam_token = request.cookies.get('exam_token')

            return redirect(url_for('introduce_exam', message=str(exam_token)))

        else:

            return redirect('/error')

    ```py

			<st c="41415">Compared to the first, the second approach needs</st> `<st c="41465">request.method</st>` <st c="41479">to separate</st> `<st c="41492">GET</st>` <st c="41495">from the</st> `<st c="41505">POST</st>` <st c="41509">transaction.</st>
			<st c="41522">In setting up the form template, binding context data to the form components through</st> `<st c="41608">render_template()</st>` <st c="41625">is a fast way to provide the form with parameters with default values.</st> <st c="41697">The form model must derive the names of its attributes from the form parameters to establish a</st> <st c="41792">successful mapping, such as in the</st> `<st c="41827">signup_approve()</st>` <st c="41843">route.</st> <st c="41851">When it comes to retrieving the form data, the</st> `<st c="41898">request</st>` <st c="41905">proxy has a</st> `<st c="41918">form</st>` <st c="41922">dictionary object that can store form parameters and their data while its</st> `<st c="41997">get_data()</st>` <st c="42007">function can access the entire query string in byte stream type.</st> <st c="42073">After a successful</st> `<st c="42092">POST</st>` <st c="42096">transaction, the view function can use</st> `<st c="42136">render_template()</st>` <st c="42153">to load a success page or go back to the form page.</st> <st c="42206">It may also apply redirection to bring the client to</st> <st c="42259">another view.</st>
			<st c="42272">But what happens to the form data after form submission?</st> <st c="42330">Usually, form parameter values are rendered as request attributes, stored as values of the session scope, or saved into a data store using a data persistency mechanism.</st> <st c="42499">Let’s explore how Flask can manage data from user requests using a relational database such</st> <st c="42591">as PostgreSQL.</st>
			<st c="42605">Building the data layer with PostgreSQL</st>
			`<st c="42795">psycopg2-binary</st>` <st c="42810">extension module.</st> <st c="42829">To install this extension module into the</st> `<st c="42871">venv</st>`<st c="42875">, run the</st> <st c="42884">following command:</st>

pip install psycopg2-binary


			<st c="42931">Now, we can write an approach to establish a connection to the</st> <st c="42995">PostgreSQL database.</st>
			<st c="43015">Setting up database connectivity</st>
			<st c="43048">There are multiple ways to create a connection to a database, but this chapter will showcase a Pythonic way to</st> <st c="43160">extract that connection using a custom decorator.</st> <st c="43210">In the project’s</st> `<st c="43227">/config</st>` <st c="43234">directory, there is a</st> `<st c="43257">connect_db</st>` <st c="43267">decorator that uses</st> `<st c="43288">psycopgy2.connect()</st>` <st c="43307">to establish connectivity to the</st> `<st c="43341">opcs</st>` <st c="43345">database of our prototype.</st> <st c="43373">Here is the implementation of this</st> <st c="43408">custom decorator:</st>

import psycopg2

import functools

from os import environ

def connect_db(func):

@functools.wraps(func) <st c="43527">def repo_function(*args, **kwargs):</st><st c="43562">conn = psycopg2.connect(</st><st c="43587">host=environ.get('DB_HOST'),</st><st c="43616">database=environ.get('DB_NAME'),</st><st c="43649">port=environ.get('DB_PORT'),</st><st c="43678">user = environ.get('DB_USER'),</st><st c="43709">password = environ.get('DB_PASS'))</st><st c="43744">resp = func(conn, *args, **kwargs)</st> conn.commit()

    conn.close()

    return resp

return <st c="43894">conn</st>,到一个存储库函数,并在事务成功执行后将所有更改提交到数据库。此外,它将在过程结束时关闭数据库连接。所有数据库详细信息,如<st c="44118">DB_HOST</st>、<st c="44127">DB_NAME</st>和<st c="44140">DB_PORT</st>,都存储在<st c="44194">.env</st>文件中的环境变量中。要使用<st c="44232">os</st>模块的<st c="44258">environ</st>字典检索它们,请运行以下命令以安装所需的扩展:
 pip install python-dotenv
        <st c="44355">然而,还有其他方法来管理这些自定义和内置配置变量,而不是将它们存储为</st> `<st c="44473">.env</st>` <st c="44477">变量。</st> <st c="44489">下一节将对此进行阐述,但首先,让我们将</st> `<st c="44549">@connect_db</st>` <st c="44560">应用于我们的</st> `<st c="44568">存储库层。</st>`

        <st c="44585">实现存储库层</st>

        <st c="44619">以下</st> `<st c="44634">insert_signup()</st>` <st c="44649">事务</st> <st c="44662">将新的用户注册记录添加到数据库。</st> <st c="44709">它从</st> `<st c="44721">conn</st>` <st c="44725">实例中获取</st> `<st c="44744">@connect_db</st>` <st c="44755">装饰器。</st> <st c="44767">我们的应用程序没有</st> `<st c="44845">psycopg2</st>` <st c="44853">驱动程序来执行</st> `<st c="44876">CRUD 操作。</st>` <st c="44892">该</st> `<st c="44896">cursor</st>` <st c="44902">实例由</st> `<st c="44923">conn</st>` <st c="44927">执行,并执行以下事务的*<st c="44941">INSERT</st>* <st c="44947">语句,其中包含其</st> `<st c="45018">view function</st>` <st c="45024">提供的表单数据:</st>
 from config.db import connect_db
from typing import Dict, Any, List <st c="45101">@connect_db</st> def insert_signup(<st c="45131">conn</st>, user:str, passw:str, utype:str, fname:str, lname:str, cid:str) -> bool:
    try: <st c="45215">cur = conn.cursor()</st> sql = 'INSERT INTO signup (username, password, user_type, firstname, lastname, cid) VALUES (%s, %s, %s, %s, %s, %s)'
        values = (user, passw, utype, fname, lname, cid) <st c="45401">cur.execute(sql, values)</st><st c="45425">cur.close()</st> return True
    except Exception as e:
        cur.close()
        print(e)
    return False
        `<st c="45506">游标</st>` <st c="45513">是从</st> `<st c="45540">conn</st>` <st c="45544">派生出来的对象,它使用数据库会话来执行插入、更新、删除和检索操作。</st> <st c="45631">因此,就像</st> `<st c="45645">insert_signup()</st>`<st c="45660">一样,以下事务</st> <st c="45687">再次使用</st> `<st c="45693">游标</st>` <st c="45699">来</st> <st c="45709">执行</st> *<st c="45721">UPDATE</st>* <st c="45727">语句:</st>
<st c="45738">@connect_db</st> def update_signup(<st c="45769">conn</st>, id:int, details:Dict[str, Any]) -> bool:
    try: <st c="45822">cur = conn.cursor()</st> params = ['{} = %s'.format(key) for key in details.keys()]
        values = tuple(details.values())
        sql = 'UPDATE signup SET {} where id = {}'.format(', '.join(params), id); <st c="46008">cur.execute(sql, values)</st><st c="46032">cur.close()</st> return True
    except Exception as e:
        cur.close()
        print(e)
    return False
        <st c="46113">为了完成对</st> `<st c="46154">注册</st>` <st c="46160">表的 CRUD 操作,以下是</st> *<st c="46180">DELETE</st>* <st c="46186">事务</st> 从 <st c="46204">我们的应用程序:</st>
<st c="46220">@connect_db</st> def delete_signup(conn, id) -> bool:
    try: <st c="46275">cur = conn.cursor()</st> sql = 'DELETE FROM signup WHERE id = %s'
        values = (id, ) <st c="46352">cur.execute(sql, values)</st><st c="46376">cur.close()</st> return True
    except Exception as e:
        cur.close()
        print(e)
    return False
        <st c="46457">使用 ORM 构建模型层</st> <st c="46501">将是</st> *<st c="46517">第二章</st>*<st c="46526">讨论的一部分。</st> <st c="46543">目前,我们应用程序的视图和服务依赖于一个直接通过</st> `<st c="46671">psycopg2</st>` <st c="46679">驱动程序管理 PostgreSQL 数据的存储库层。</st>

        <st c="46687">在创建存储库层之后,许多应用程序可以构建一个服务层,以在 CRUD 操作和</st> <st c="46827">视图之间提供松散耦合。</st>

        <st c="46837">创建服务层</st>

        <st c="46864">应用程序的服务层构建视图函数和存储库的业务逻辑。</st> <st c="46970">我们不是将事务相关的业务流程加载到视图函数中,而是通过创建所有顾问和患者 ID 的列表、验证新批准用户持久化的位置以及创建在考试中表现优异的患者列表,将这些实现放在服务层中。</st> <st c="47288">以下服务函数评估并记录</st> <st c="47341">患者</st> <st c="47351">的考试成绩:</st>
<st c="47363">def record_patient_exam(formdata:Dict[str, Any]) -> bool:</st> try:
        pct = round((<st c="47440">formdata['score']</st> / <st c="47461">formdata['total']</st>) * 100, 2)
        status = None
        if (pct >= 70):
            status = 'passed'
        elif (pct < 70) and (pct >= 55):
            status = 'conditional'
        else:
            status = 'failed' <st c="47619">insert_patient_score(pid=formdata['pid'],</st> <st c="47660">qid=formdata['qid'], score=formdata['score'],</st> <st c="47706">total=formdata['total'], status=status,</st> <st c="47746">percentage=pct)</st> return True
    except Exception as e:
        print(e)
    return False
        <st c="47819">而不是直接访问</st> `<st c="47850">insert_patient_score()</st>` <st c="47872">来保存患者考试成绩,</st> `<st c="47902">record_score()</st>` <st c="47916">访问</st> `<st c="47930">record_patient_exam()</st>` <st c="47951">服务来在调用</st> `<st c="48001">insert_patient_score()</st>` <st c="48023">从存储库层进行记录插入之前计算一些公式。</st> <st c="48072">该服务减少了数据库事务和视图层之间的摩擦。</st> <st c="48160">以下片段是访问</st> `<st c="48221">record_patient_exam()</st>` <st c="48242">服务进行记录考试</st> <st c="48267">记录插入的视图函数:</st>
<st c="48284">@app.route('/exam/score', methods=['GET', 'POST'])</st>
<st c="48335">def record_score():</st> if request.method == 'GET': <st c="48384">pids = list_pid()</st><st c="48401">qids = list_qid()</st> return render_template( 'exam/add_patient_score_form.html', pids=pids, qids=qids), 200
    else:
        params = dict()
        params['pid'] = int(request.form['pid'])
        params['qid'] = int(request.form['qid'])
        params['score'] = float(request.form['score'])
        params['total'] = float(request.form['total']) <st c="48705">result = record_patient_exam(params)</st> … … … … … … …
        else:
            return redirect('/exam/task/error')
        <st c="48796">除了调用</st> `<st c="48816">record_patient_exam()</st>`<st c="48837">之外,它还利用了</st> `<st c="48860">list_pid()</st>` <st c="48870">和</st> `<st c="48875">list_qid()</st>` <st c="48885">服务来检索 ID。</st> <st c="48916">使用服务可以帮助将抽象和用例与路由函数分离,这对路由的范畴、清洁编码和运行时</st> <st c="49080">性能有积极影响。</st> <st c="49107">此外,项目结构还可以有助于清晰的业务流程、可维护性、灵活性和适应性。</st>

        <st c="49230">管理项目结构</st>

        <st c="49261">Flask 为开发者</st> <st c="49288">提供了构建他们所需项目结构的便利。</st> <st c="49354">由于其 Python 特性,它对构建项目目录的设计模式和架构策略持开放态度。</st> <st c="49491">本讨论的重点是设置我们的</st> *<st c="49551">在线个人咨询系统</st>* <st c="49584">应用程序,采用简单且单一结构的项目方法,同时突出不同的配置</st> <st c="49700">变量设置。</st>

        <st c="49716">构建目录结构</st>

        <st c="49749">在构建项目结构时需要考虑的第一个方面是项目范畴的复杂程度。</st> <st c="49862">由于我们的项目仅关注小规模客户,典型的</st> *<st c="49929">单一结构</st>* <st c="49946">方法足以满足不太可扩展的应用。</st> <st c="50007">其次,我们必须确保从视图层到底层测试模块的各个项目组件的适当分层或分解,以便开发者可以确定哪些部分需要优先考虑、维护、修复错误和测试。</st> <st c="50229">以下是我们原型的目录结构截图:</st>

        ![图 1.4 – 单一结构的项目目录](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_01_004.jpg)

        <st c="50319">图 1.4 – 单一结构的项目目录</st>

        *<st c="50371">第二章</st>* <st c="50381">将讨论其他项目结构技术,特别是当应用程序可扩展且复杂时。</st>

        <st c="50485">设置开发环境</st>

        <st c="50522">Flask 应用程序默认情况下是生产就绪的,尽管其服务器,Werkzeug 的内置服务器,不是。</st> <st c="50641">我们需要用企业级服务器替换它,以便完全准备好生产设置。</st> <st c="50717">然而,我们的目标是设置一个具有开发环境的 Flask 项目,我们可以用它来尝试和测试各种功能和测试用例。</st> <st c="50888">有三种方法可以设置 Flask 3.x 项目用于开发和测试目的:</st>

            +   <st c="50976">使用</st> `<st c="51001">app.run(debug=True)</st>` <st c="51020">在</st> `<st c="51024">main.py</st>`<st c="51031">中运行服务器。</st>

            +   <st c="51032">将</st> `<st c="51045">FLASK_DEBUG</st>` <st c="51056">和</st> `<st c="51061">TESTING</st>` <st c="51068">内置配置变量设置为</st> `<st c="51105">true</st>` <st c="51109">在配置文件中。</st>

            +   <st c="51136">使用</st> `<st c="51170">flask run --</st>``<st c="51182">debug</st>` <st c="51188">命令运行应用程序。</st>

        <st c="51197">设置开发环境将同时启用自动重载和框架的默认调试器。</st> <st c="51314">但是,在将应用程序部署到生产环境后,请关闭调试模式以避免应用程序和软件日志的安全风险。</st> <st c="51469">以下截图显示了运行具有开发环境设置的 Flask 项目时的服务器日志:</st> <st c="51563">:</st>

        ![图 1.5 – Flask 内置服务器的服务器日志](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_01_005.jpg)

        <st c="51745">图 1.5 – Flask 内置服务器的服务器日志</st>

        *<st c="51799">图 1</st>**<st c="51808">.5</st>* <st c="51810">显示调试模式设置为</st> `<st c="51843">ON</st>` <st c="51845">,调试器已启用并分配了一个</st> `<st c="51891">PIN</st>` <st c="51894">值。</st>

        <st c="51901">实现 main.py 模块</st>

        <st c="51933">当创建一个简单的</st> <st c="51957">项目,如我们的样本,主模块通常包含 Flask 实例化和一些其参数(例如,</st> `<st c="52082">template_folder</st>` <st c="52097">用于 HTML 模板的新目录)以及下面视图所需的导入。</st> <st c="52175">以下是我们</st> `<st c="52233">main.py</st>` <st c="52240">文件的完整代码:</st>
 from flask import Flask
from converter.date_converter import DateConverter <st c="52322">app = Flask(__name__, template_folder='pages')</st>
<st c="52368">app.url_map.converters['date'] = DateConverter</st> @app.route('/', methods = ['GET'])
def index():
    return "This is an online … counseling system (OPCS)" <st c="52518">import views.index</st>
<st c="52536">import views.certificates</st>
<st c="52562">import views.signup</st>
<st c="52582">import views.examination</st>
<st c="52607">import views.reports</st>
<st c="52628">import views.admin</st>
<st c="52647">import views.login</st>
<st c="52666">import views.profile</st> app.add_url_rule('/certificate/terminate/<string:counselor>/<date:effective_date>/<string:patient>', 'show_honor_dissmisal', views.certificates.show_honor_dissmisal) <st c="53086">app</st> instance of the main module while the main module has the imports to the views declared at the beginning. This occurrence is called a circular dependency between two modules importing components from each other, which leads to some circular import issues. To avoid this problem with the main and view modules, the area below the Flask instantiation is where we place these view imports. The <st c="53481">if</st> statement at the bottom of <st c="53511">main.py</st>, on the other hand, verifies that only the main module can run the Flask server through the <st c="53611">app.run()</st> command.
			<st c="53629">The main module usually sets the configuration settings through its</st> `<st c="53698">app</st>` <st c="53701">instance to build the sessions and other context-based objects or integrate other custom components, such as the security and database modules.</st> <st c="53846">But the ideal setup doesn’t recommend including them there; instead, you should place them separately from the code, say using a configuration file, to seamlessly manage the environment variables when configuration blunders arise, to avoid performance degradation or congestion when the Flask</st> `<st c="54139">app</st>` <st c="54142">instance has several variables to load at server startup, and to replicate and back up the environment settings with less effort during project migration</st> <st c="54297">or replication.</st>
			<st c="54312">Creating environment variables</st>
			<st c="54343">Configuration variables will always be part of any project setup, and how the frameworks or platforms</st> <st c="54446">manage them gives an impression of the kind of framework they are.</st> <st c="54513">A good framework should be able to decouple both built-in and custom configuration variables from the implementation area while maintaining their easy access across the application.</st> <st c="54695">It can support having a configuration file that can do</st> <st c="54750">the following:</st>

				*   <st c="54764">Contain the variables in a structured and</st> <st c="54807">readable manner.</st>
				*   <st c="54823">Easily integrate with</st> <st c="54846">the application.</st>
				*   <st c="54862">Allow comments to be part of</st> <st c="54892">its content.</st>
				*   <st c="54904">Work even when deployed to other servers</st> <st c="54946">or containers.</st>
				*   <st c="54960">Decouple the variables from the</st> <st c="54993">implementation area.</st>

			<st c="55013">Aside from the</st> `<st c="55029">.env</st>` <st c="55033">file, Flask can also support configuration files in JSON, Python, and</st> `<st c="55284">config.json</st>` <st c="55295">file, which contains the database and Flask development</st> <st c="55352">environment settings:</st>

{

「数据库用户」:「postgres」,

「数据库密码」:「admin2255」,

「数据库端口」:5433,

"数据库主机地址" : "localhost",

"DB_NAME" : "opcs",

"FLASK_DEBUG" : true,

"TESTING": true

}


			<st c="55527">This next is a Python</st> `<st c="55550">config.py</st>` <st c="55559">file with the same variable settings</st> <st c="55597">in</st> `<st c="55600">config.json</st>`<st c="55611">:</st>

数据库用户:DB_USER = 「postgres」

数据库密码:DB_PASS = «admin2255»

数据库端口:DB_PORT = 5433

数据库主机地址:DB_HOST = "localhost"

数据库名称:DB_NAME = "opcs"

FLASK_DEBUG = True

测试模式:TESTING = True


			<st c="55744">The</st> `<st c="55749">app</st>` <st c="55752">instance has the</st> `<st c="55770">config</st>` <st c="55776">attribute with a</st> `<st c="55794">from_file()</st>` <st c="55805">method that can load the JSON  file, as shown in the</st> <st c="55858">following snippet:</st>

从文件“config.json”中加载配置:app.config.from_file("config.json", load=json.load)


			<st c="55928">On the other hand,</st> `<st c="55948">config</st>` <st c="55954">has a</st> `<st c="55961">from_pyfile()</st>` <st c="55974">method that can manage the Python config file when invoked, as shown in</st> <st c="56047">this snippet:</st>

从文件中加载配置:app.config.from_pyfile('myconfig.py')


			<st c="56098">The recent addition to the supported type,</st> `<st c="56178">toml</st>` <st c="56182">extension module before</st> <st c="56206">loading the</st> `<st c="56219">.toml</st>` <st c="56224">file into the platform.</st> <st c="56249">After running the</st> `<st c="56267">pip install toml</st>` <st c="56283">command, the</st> `<st c="56297">config</st>` <st c="56303">attribute’s</st> `<st c="56316">from_file()</st>` <st c="56327">method can now load the following settings of the</st> `<st c="56378">config.toml</st>` <st c="56389">file:</st>

数据库用户:DB_USER = 「postgres」

数据库密码:DB_PASS = «admin2255»

数据库端口:DB_PORT = 5433

数据库主机地址:DB_HOST = "localhost"

数据库名称:DB_NAME = "opcs"

FLASK_DEBUG = true

测试模式 = true


			<st c="56526">TOML, like JSON and Python, has data types.</st> <st c="56571">It supports arrays and tables and has structural patterns that may seem more complex than the JSON and Python configuration syntax.</st> <st c="56703">A TOML file will have the</st> `<st c="56729">.</st>``<st c="56730">toml</st>` <st c="56735">extension.</st>
			<st c="56746">When accessing variables from these file types, the Flask instance uses its</st> `<st c="56823">config</st>` <st c="56829">object to access each variable.</st> <st c="56862">This can be seen in the following version of our</st> `<st c="56911">db.py</st>` <st c="56916">module for database connectivity, which uses the</st> `<st c="56966">config.toml</st>` <st c="56977">file:</st>

main 导入 app import psycopg2

import functools

def connect_db(func):

@functools.wraps(func)

def repo_function(*args, **kwargs):

    conn = psycopg2.connect( <st c="57148">主机 = app.config['DB_HOST'],</st><st c="57175">数据库名 = app.config['DB_NAME'],</st><st c="57207">端口 = app.config['DB_PORT'],</st><st c="57235">用户 = app.config['DB_USER'],</st><st c="57263">密码 = app.config['DB_PASS'])</st> resp = func(conn, *args, **kwargs)

    conn.commit()

    conn.close()

    return resp

return repo_function

			<st c="57390">Summary</st>
			<st c="57398">This chapter has presented the initial requirements to set up a development environment for a single-structured Flask project.</st> <st c="57526">It provided the basic elements that are essential to creating a simple Flask prototype, such as the</st> `<st c="57626">main.py</st>` <st c="57633">module, routes, database connectivity, repository, services, and configuration files.</st> <st c="57720">The nuts and bolts of every procedure in building every aspect of the project describe Flask as a web framework.</st> <st c="57833">The many ways to store the configuration settings, the possibility of using custom decorators for database connectivity, and the many options to capture the form data are indicators of Flask being so flexible, extensible, handy, and Pythonic in many ways.</st> <st c="58089">The next chapter will focus on the core components and advanced features that Flask can provide in building a more</st> <st c="58204">scalable application.</st>





第三章:2

添加高级核心功能

在完成 Flask 网络应用的设置、配置和初步开发后 第一章,现在是时候包含 Flask 框架的其他基本组件,以完成一个网络应用。 这些组件,如 会话处理 闪存消息 错误处理 软件日志,可以监控和管理用户与内部事务之间的交互。 此外,Flask 还可以提供对系统如何应对运行时间、安全性、平稳性能以及适应不断变化的 生产环境变化的理解。

这些由 Flask 支持的构建企业级应用的主要网络组件将是本章的重点。 一旦这些核心组件成为应用的一部分,我们还将讨论设计项目结构的各种方法。 应用。

以下是本章我们将涉及的主题: 本章:

  • 构建庞大且 可扩展的项目

  • 应用对象关系映射(ORM)

  • 配置 日志机制

  • 创建 用户会话

  • 应用 闪存消息

  • 利用一些高级 Jinja2 功能

  • 实现 错误处理解决方案

  • 添加 静态资源

技术要求

本章将专注于订单和产品管理交易。 本章的应用原型,一个 在线货运管理系统,涵盖了某些通用产品库存、一个订单模块、一个基本的货运流程结构以及交付管理模块的一些部分。 这个原型提供了三种不同的实现方式,适合复杂且可扩展的 Flask 网络应用,具体如下: 以下:

  • 利用应用工厂设计模式的 <st c="1684">ch02-factory</st> 项目。

  • 使用 <st c="1763">ch02-blueprint</st> 项目的 Flask 蓝图。

  • 使用应用工厂和 <st c="1821">ch02-blueprint-factory</st> 项目同时使用蓝图结构。

就像在 *第一章中一样,该应用程序使用 *PostgreSQL 作为数据库,但这次使用了一个名为 SQLAlchemy 的 ORM。 所有这些项目都已上传至 github.com/PacktPublishing/Mastering-Flask-Web-Development/tree/main/ch02

构建大型且可扩展的项目

*简单 的 Flask 网络应用程序 创建目录结构非常方便且简单,尤其是当只有一个模块需要构建且软件功能较少时。 但对于复杂且可扩展的 企业级应用程序,其中需要支持的功能数量庞大,最常见的问题总是 *循环 *导入问题

重要提示

当两个或更多模块相互导入时,就会发生 *循环导入问题 ,在应用程序完全执行之前形成一个相互依赖的循环。 这种情况总是会导致意外的应用程序加载错误、缺少模块,甚至奇怪的 运行时问题

Flask 作为框架非常符合 Python 风格,这意味着开发者可以决定他们构建应用程序的结构方法。 不幸的是,并非所有目录结构设计都能因为循环导入问题而成功实施。 然而,三种设计模式可以为 Flask 项目 提供一个基本结构:即 *应用程序工厂设计 *蓝图方法 ,以及 *结合应用程序工厂和 *蓝图模板

使用应用程序工厂

*第一章中,我们应用程序中使用的项目结构由各种组件的模块和包组成,例如模型、存储库、服务、模板以及 <st c="3449">main.py</st> 文件。 代码组织并不符合 Flask 的标准,但被认为是一个干净的 目录结构

构建 Flask 项目的一种方法是用 应用工厂,这是一种由 Flask 实例的实例化和配置组成的方法。 它将配置文件加载到平台中,设置必要的扩展模块,如 SQLAlchemy,并在实例化app之前使用参数(如template_folder <st c="3912">static_folder</st>)初始化 Flask 构造函数。 使用工厂应用,在处理配置方面具有灵活性。 应用程序可能有一个单独的工厂方法用于 测试 开发 生产,具体取决于应用程序将经历的阶段。

但是这个方法定义应该放在哪里呢? 在实施此方法之前,将所有视图及其相关组件从通用应用程序组件(如异常类和错误页面)中分离出来。 将文件放在子文件夹中,但您也可以在下面添加更多文件夹以进一步组织模块。 之后,在这些子文件夹中的任何位置创建一个 <st c="4531">__init__.py</st> 文件以实现应用工厂方法。 在我们的案例中, <st c="4644">__init__.py</st> 文件位于 <st c="4668">app</st> 子文件夹中,我们在这里定义了应用工厂。 图 2**.1 显示了包含具有应用工厂的 <st c="4777">ch02-factory</st> 项目版本的原型目录结构:

图 2.1 – Flask 项目目录与应用工厂

图 2.1 – Flask 项目目录与应用工厂

__init__.py 文件将任何目录转换为包含可导入其他模块的文件和文件夹的包。 __init__.py 文件内部导入的任何模块脚本都暴露给包目录外部的导入。 同样,它还允许暴露由于相对路径问题而无法触及的来自其他包的模块。 另一方面,应用程序自动加载所有导入的模块并 执行__init__.py 文件内部的 方法调用。 因此,将我们的应用程序工厂放在<st c="5676">__init__.py</st> <st c="5687">文件中的<st c="5700">app</st> <st c="5703">包中,可以在 Flask 项目的任何地方暴露函数。 以下是我们<st c="5804">app/__init__.py</st> <st c="5819">文件的内容:

 from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import logging
import logging.config
import logging.handlers
import sys
import toml
# Extension modules initialization
db = SQLAlchemy()
def configure_func_logging(log_path):
    logging.getLogger("werkzeug").disabled = True
    console_handler =   logging.StreamHandler(stream=sys.stdout)
    console_handler.setLevel(logging.DEBUG)
    logging.basicConfig(level=logging.DEBUG,  format='%(asctime)s %(levelname)s %(module)s
          %(funcName)s %(message)s',
     datefmt='%Y-%m-%d %H:%M:%S',  handlers=[logging.handlers.RotatingFileHandler(
        log_path, backupCount=3, maxBytes=1024 ),
           console_handler])
def <st c="6464">create_app(config_file):</st> app = Flask(__name__, template_folder='../app/pages', static_folder='../app/resources')
    app.config.from_file(config_file, toml.load)
    db.init_app(app)
    configure_func_logging('log_msg.txt') <st c="6677">with app.app_context():</st> from app.views import login
        from app.views import menu
        from app.views import customer
        from app.views import admin
        from app.views import product
        from app.views import order
        from app.views import payment
        from app.views import shipping
    return app

分配给应用程序工厂函数的通用和标准名称是 <st c="7018">create_app()</st> <st c="7030">,但任何人都可以用适合他们项目的适当名称替换它。 在给定的代码片段中,我们的应用程序工厂创建了 Flask 的<st c="7170">app</st> <st c="7173">实例,调用了<st c="7190">db_init()</st> <st c="7199"><st c="7206">SQLAlchemy 的db 实例,以使用app 对象定义和配置 ORM,并设置了日志</st> <st c="7308">机制。</st> <st c="7320">由于它在init.py 中,所以main.py 文件必须导入工厂方法,最终通过调用app run() `方法来创建对象并运行应用程序。

为了使应用程序工厂方法灵活和可配置,向其中添加本地参数。 例如,它可以接受一个字符串参数作为文件名,以接受配置文件名,这样当应用程序以开发模式运行时,它可以接受 <st c="7745">config_dev.toml</st> 作为其配置文件。 当将部署转移到生产服务器时,它可以接受一个新的文件名,并用生产配置文件替换现有的配置,例如 <st c="7946">config_prod.toml</st>,以重新加载所有针对 生产服务器 的环境变量。

利用当前应用程序代理

使用应用程序工厂设计 模式来构建应用程序使得从 <st c="8182">app</st> 实例中访问 <st c="8200">main.py</st> 中的视图和其他需要它的组件变得不可能,否则会遇到 循环导入问题。我们不是在 <st c="8359">app</st> 目录的模块中导入 <st c="8326">app</st> 对象,而是在 <st c="8414">create_app()</st> 中建立应用程序上下文,以利用名为 <st c="8448">app</st> 的代理 <st c="8459">对象</st> <st c="8466">current_app</st>

在 Flask 中,应用程序上下文在请求期间管理配置变量、视图数据、记录器、数据库细节和其他自定义对象。 创建应用程序上下文有两种方法:

  • 显式地使用 <st c="8761">push()</st> 方法来推送应用程序上下文,允许在任何请求中从应用程序的任何地方访问 <st c="8799">current_app</st>

     app_ctx = <st c="8873">app.app_context()</st> app_ctx.<st c="8963">with</st>-block, setting a limit on where to access the application-level components, such as allowing access to <st c="9072">current_app</st> from the <st c="9093">views</st> module only, as depicted in the following example:
    
    

    使用 app.app_context():从 app.views 导入 login

            从 app.views 导入 menu
    
            从 app.views 导入 customer
    
            从 app.views 导入 admin
    
            从 app.views 导入 product
    
            从 app.views 导入 order
    
            从 app.views 导入 payment
    
            从 app.views 导入 shipping
    
    
    

不是从 <st c="9432">app</st> 实例中通过 <st c="9454">__init__.py</st> 文件来在 <st c="9497">views.shipping</st> 模块中实现视图,这肯定会导致由于 <st c="9583">current_app()</st> <st c="9614">views.shipping</st> 模块的导入而引起的循环导入问题,现在应用程序可以允许使用 <st c="9682">current_app</st> 代理来构建 <st c="9709">views.shipping</st> ,因为 <st c="9739">with</st>-context block 将模块脚本推送到应用程序上下文。 以下代码展示了在创建将配送员资料详情插入到 <st c="9882">add_delivery_officer</st> 视图函数时使用代理对象的方法:

<st c="9981">from flask import current_app</st> @<st c="10013">current_app</st>.route('/delivery/officer/add', methods = ['GET', 'POST'])
def add_delivery_officer():
    if request.method == 'POST': <st c="10140">current_app.logger.info('add_delivery_officer</st><st c="10185">POST view executed')</st> repo = DeliveryOfficerRepository(db)
        officer = DeliveryOfficer(id=int(request.form['id']), firstname=request.form['firstname'], middlename=request.form['middlename'],
          … … … … … …
        result = repo.insert(officer)
        return render_template(
         'shipping/add_delivery_officer_form.html'), 200 <st c="10488">current_app.logger.info('add_delivery_officer</st><st c="10533">GET view executed')</st> return render_template(
       'shipping/add_delivery_officer_form.html'), 200

current_app <st c="10657">flask</st> 模块的一部分,该模块可以提供构建视图函数和其他组件所需的所有必要的装饰器和实用工具,只要模块脚本位于 <st c="10873">create_app()</st>创建的应用程序上下文范围内 。在 <st c="10894">add_delivery_officer</st> 视图函数中, <st c="10934">current_app</st> 提供了 <st c="10959">route()</st> 装饰器和由 <st c="10985">logger</st> 实例配置的 <st c="11019">application factory</st>

将数据存储到应用程序上下文中

现在,Flask 中的应用程序上下文就像是在每个请求-响应事务之上创建的一个微型层。 每次请求都会有一个新的上下文,因此 Flask 应用程序中的所有应用程序级数据都是短暂的。 在这个时间段内,我们可以使用另一个应用程序级代理对象 <st c="11420">g</st> 组件来存储数据。 以下代码片段展示了如何创建和销毁应用程序上下文 <st c="11507">数据对象:</st>

<st c="11520">def get_database():</st><st c="11540">if 'db' not in g:</st><st c="11558">g.db = db</st> app.logger.info('storing … as context data') <st c="11614">@app.teardown_appcontext</st> def teardown_database(exception): <st c="11673">db = g.pop('db', None)</st> app.logger.info('removing … as context data')

The <st c="11746">get_database()</st> 方法将 <st c="11779">db</st> 实例存储到 <st c="11807">create_app()</st> 通过 <st c="11849">g</st> 代理创建的上下文中。 在存储数据之前,始终先验证对象是否已经在 <st c="11961">g</st>中,这是一个好的实践。 另一方面, <st c="11987">teardown_database()</st> 有一个 <st c="12013">@app.teardown_appcontext</st> 装饰器,允许在请求上下文结束时自动调用该方法。 在 Flask 销毁整个 应用程序上下文之前, <st c="12133">g</st> <st c="12149">pop()</st> 方法会从上下文中移除或释放数据。

访问 <st c="12263">g</st> 以存储数据 在应用程序中并不总是有效。 创建上下文数据或调用我们的 <st c="12384">get_database()</st> 方法适当的地点是在 <st c="12416">@before_request</st> 方法。 此方法在请求事务开始之前自动执行。 <st c="12534">g</st> 中的上下文数据将在 <st c="12608">@before_request</st> 事件方法执行后才能被任何视图函数访问。 换句话说,通过 <st c="12683">g</st> 共享的所有资源都只能在请求-响应范围内访问。 访问 <st c="12754">g</st> 上下文数据未在 <st c="12780">@before_request</st> 设置的情况下可能导致 <st c="12806">ValueError</st>。因此,我们在以下 <st c="12877">@</st>``<st c="12878">before_request</st> 实现中调用我们的 <st c="12838">get_database()</st> 方法:

<st c="12908">@app.before_request</st> def init_request(): <st c="12994">list_login</st> view function to access the database for query transactions:

@current_app.route('/login/list', methods=['GET'])

def list_login():

repo = LoginRepository(<st c="13158">g.db</st>)

users = repo.select_all()

session['sample'] = 'trial'

flash('用户凭证列表')

return render_template('login/login_list.html', users=users) , 200

			<st c="13320">Although it is an advantage to verify the existence of a context attribute in</st> `<st c="13399">g</st>` <st c="13400">before accessing it, sometimes accessing the data right away is inevitable, such as in our</st> `<st c="13492">list_login</st>` <st c="13502">view function</st> <st c="13517">that directly passes the</st> `<st c="13542">g.db</st>` <st c="13546">context object into</st> `<st c="13567">LoginRepository</st>`<st c="13582">. Another approach to accessing the context data is through the</st> `<st c="13646">g.set()</st>` <st c="13653">method that allows a default value if the data is non-existent, such as using</st> `<st c="13732">g.get('db', db)</st>` <st c="13747">instead of</st> `<st c="13759">g.db</st>`<st c="13763">, where</st> `<st c="13771">db</st>` <st c="13773">in the second parameter is a</st> <st c="13803">backup connection.</st>
			<st c="13821">Important note</st>
			<st c="13836">Both application and request contexts exist only during a request-response life cycle.</st> <st c="13924">The application context provides the</st> `<st c="13961">current_app</st>` <st c="13972">proxy and the</st> `<st c="13987">g</st>` <st c="13988">variable, while the request context contains the request variables and the view function details.</st> <st c="14087">Unlike in other web frameworks, Flask’s application context will not be valid for access after the life cycle destroys the</st> <st c="14210">request context.</st>
			<st c="14226">Aside from the application factory, we can also use Flask’s blueprints to establish a</st> <st c="14313">project directory.</st>
			<st c="14331">Using the Blueprint</st>
			`<st c="14351">Blueprints</st>` <st c="14362">are Flask’s built-in</st> <st c="14384">components from its</st> `<st c="14404">flask</st>` <st c="14409">module.</st> <st c="14418">Their core purpose is to organize all related views with the repository, services, templates, and other associated features to form a solid and self-contained structure.</st> <st c="14588">The strength of the Blueprint is that it can break down a single huge application into independent</st> <st c="14687">business units that can be considered sub-applications but are still dependent on the main application.</st> *<st c="14791">Figure 2</st>**<st c="14799">.2</st>* <st c="14801">shows the organization of the</st> `<st c="14832">ch02-blueprint</st>` <st c="14846">project, a version of our</st> *<st c="14873">Online Shipping Management System</st>* <st c="14906">that uses Blueprints in building its</st> <st c="14944">project structure:</st>
			![Figure 2.2 – Flask project directory with Blueprints](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_02_2.jpg)

			<st c="15287">Figure 2.2 – Flask project directory with Blueprints</st>
			<st c="15339">Defining the Blueprint</st>
			<st c="15362">Instead of placing all view files in</st> <st c="15400">one folder, the project’s related views are grouped based on business units, then assigned their respective Blueprint sub-projects, namely,</st> `<st c="15540">home</st>`<st c="15544">,</st> `<st c="15546">login</st>`<st c="15551">,</st> `<st c="15553">order</st>`<st c="15558">,</st> `<st c="15560">payment</st>`<st c="15567">,</st> `<st c="15569">product</st>`<st c="15576">, and</st> `<st c="15582">shipping</st>`<st c="15590">. Each Blueprint represents a section with the templates, static resources, repositories, services, and utilities needed to build</st> <st c="15720">a sub-application.</st>
			<st c="15738">Now, the</st> `<st c="15748">__init__.py</st>` <st c="15759">file of each sub-application is very important because this is where the Blueprint object is created and instantiated.</st> <st c="15879">The blueprint</st> `<st c="15893">home</st>`<st c="15897">’s</st> `<st c="15901">__init__.py</st>` <st c="15912">file, for instance, has the following</st> <st c="15951">Blueprint definition:</st>

从 flask 导入 Blueprint home_bp = Blueprint('home_bp', name,template_folder='pages',static_folder='resources', static_url_path='static') 导入 modules.home.views.menu


			<st c="16150">Meanwhile, the</st> `<st c="16166">login</st>` <st c="16171">section’s</st> <st c="16181">Blueprint has the</st> `<st c="16200">__init__py</st>` <st c="16210">file that</st> <st c="16221">contains this:</st>

从 flask 导入 Blueprint login_bp = Blueprint('login_bp', name,template_folder='pages',static_folder='resources', static_url_path='static') 导入 modules.login.views.login

导入 modules.login.views.admin

导入 modules.login.views.customer


			<st c="16486">The constructor of the</st> `<st c="16510">Blueprint</st>` <st c="16519">class requires two parameters for it to instantiate</st> <st c="16572">the class:</st>

				*   <st c="16582">The first parameter is the</st> *<st c="16610">name of Blueprint</st>*<st c="16627">, usually the name of the reference variable of</st> <st c="16675">its instance.</st>
				*   <st c="16688">The second parameter is</st> `<st c="16713">__name__</st>`<st c="16721">, which depicts the</st> *<st c="16741">current package</st>* <st c="16756">of</st> <st c="16760">the section.</st>

			<st c="16772">The name of the Blueprint must be unique to the sub-application because it is responsible for its internal routings, which must not have any collisions with the other Blueprints.</st> <st c="16952">The Blueprint package, on the other hand, will indicate the root path of</st> <st c="17025">the sub-application.</st>
			<st c="17045">Implementing the Blueprint’s routes</st>
			<st c="17081">One purpose of using a Blueprint is to avoid circular import issues in implementing the routes.</st> <st c="17178">Rather than accessing the</st> <st c="17204">app instance from</st> `<st c="17222">main.py</st>`<st c="17229">, sub-applications can now directly access their respective Blueprint instance to build their routes.</st> <st c="17331">The following code shows how the login Blueprint implements its route using its Blueprint instance, the</st> `<st c="17435">login_bp</st>` <st c="17443">object:</st>

从 modules.login 导入 login_bp

@login_bp.route('/admin/add', methods = ['GET', 'POST']) def add_admin():

if request.method == 'POST':

    app.logger.info('add_admin POST view executed')

    repo = AdminRepository(db_session)

    … … … … … …

    return render_template('admin_details_form.html', logins=logins), 200

app.logger.info('add_admin GET view executed')

logins = get_login_id(1, db_session)

return render_template('admin_details_form.html', logins=logins), 200

			<st c="17908">The</st> `<st c="17913">login_bp</st>` <st c="17921">object is instantiated from</st> `<st c="17950">__init__py</st>` <st c="17960">of the</st> `<st c="17968">login</st>` <st c="17973">directory, thus importing it from there.</st> <st c="18015">But this route and the rest of the views will only work after registering these Blueprints with the</st> `<st c="18115">app</st>` <st c="18118">instance.</st>
			<st c="18128">Registering the blueprints</st>
			<st c="18155">Blueprint registration happens in the</st> `<st c="18194">main.py</st>` <st c="18201">file, which is the location of the Flask app.</st> <st c="18248">The following snippet is part of the</st> `<st c="18285">main.py</st>` <st c="18292">file of our</st> `<st c="18305">ch02-blueprint</st>` <st c="18319">project that shows how to</st> <st c="18346">establish the registration</st> <st c="18373">procedure correctly:</st>

从 flask 导入 Flask

app = Flask(name, template_folder='pages')

app.config.from_file('config.toml', toml.load) 从 modules.home 导入 home_bp

从 modules.login 导入 login_bp

从 modules.order 导入 order_bp

从 modules.payment 导入 payment_bp

从 modules.shipping 导入 shipping_bp

从 modules.product 导入 product_bp

app.register_blueprint(home_bp, url_prefix='/ch02')

app.register_blueprint(login_bp, url_prefix='/ch02')

app.register_blueprint(order_bp, url_prefix='/ch02')

app.register_blueprint(payment_bp, url_prefix='/ch02')

app.register_blueprint(shipping_bp, url_prefix='/ch02')

app.register_blueprint(product_bp, url_prefix='/ch02') 从 modules.model.db 导入*

if name == 'main':

app.run()

			<st c="19125">The</st> `<st c="19130">register_blueprint()</st>` <st c="19150">method from the</st> `<st c="19167">app</st>` <st c="19170">instance has three parameters, namely,</st> <st c="19210">the following:</st>

				*   <st c="19224">The Blueprint object imported from</st> <st c="19260">the sub-application.</st>
				*   <st c="19280">The</st> `<st c="19285">url_prefix</st>`<st c="19295">, the assigned URL</st> <st c="19314">base route.</st>
				*   <st c="19325">The</st> `<st c="19330">url_defaults</st>`<st c="19342">, the dictionary of parameters required by the views linked to</st> <st c="19405">the Blueprint.</st>

			<st c="19419">Registering the Blueprints can also be</st> <st c="19459">considered a workaround in providing our Flask applications with a context root.</st> <st c="19540">The context root defines the application, and it serves as the base URL that can be used to access the application.</st> <st c="19656">In the given snippet, our application was assigned the</st> `<st c="19711">/ch02</st>` <st c="19716">context root through the</st> `<st c="19742">url_prefix</st>` <st c="19752">parameter of</st> `<st c="19766">register_blueprint()</st>`<st c="19786">. On the other hand, as shown from the given code, the imports to the Blueprints must be placed below the app instantiation to avoid circular</st> <st c="19928">import issues.</st>
			<st c="19942">Another way of building a clean Flask project is by combining the application factory design technique with the</st> <st c="20055">Blueprint approach.</st>
			<st c="20074">Utilizing both the application factory and the Blueprint</st>
			<st c="20131">To make the Blueprint structures</st> <st c="20165">flexible when managing configuration variables and more organized by utilizing the application context proxies</st> `<st c="20276">g</st>` <st c="20277">and</st> `<st c="20282">current_app</st>`<st c="20293">, add an</st> `<st c="20302">__init__.py</st>` <st c="20313">file in the</st> `<st c="20326">modules</st>` <st c="20333">folder.</st> *<st c="20342">Figure 2</st>**<st c="20350">.3</st>* <st c="20352">shows the project structure of</st> `<st c="20384">ch02-blueprint-factory</st>` <st c="20406">with the</st> `<st c="20416">__init__.py</st>` <st c="20427">file in place to implement the factory</st> <st c="20467">method definition:</st>
			![Figure 2.3 – Flask directory with Blueprints and application factory](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_02_3.jpg)

			<st c="20827">Figure 2.3 – Flask directory with Blueprints and application factory</st>
			<st c="20895">The</st> `<st c="20900">create_app()</st>` <st c="20912">factory method can now include the import of the Blueprints and their registration to the app.</st> <st c="21008">The rest of its setup is the same as the</st> `<st c="21049">ch01</st>` <st c="21053">project.</st> <st c="21063">The following code shows its</st> <st c="21092">entire</st> <st c="21098">implementation:</st>

导入 toml

从 flask 导入 Flask

从 flask_sqlalchemy 导入 SQLAlchemy

db = SQLAlchemy() def create_app(config_file): app = Flask(name, template_folder='../pages', static_folder='../resources')

app.config.from_file(config_file, toml.load)

… … … … … …

… … … … … … <st c="21387">使用 app.app_context():</st><st c="21410">从 modules.home 导入 home_bp</st><st c="21443">从 modules.login 导入 login_bp</st><st c="21478">从 modules.order 导入 order_bp</st> … … … … … …

… … … … … … <st c="21537">app.register_blueprint(home_bp, url_prefix='/ch02')</st><st c="21588">app.register_blueprint(login_bp, url_prefix='/ch02')</st><st c="21641">app.register_blueprint(order_bp, url_prefix='/ch02')</st> … … … … … …

返回 app

			<st c="21716">The application factory here uses the</st> `<st c="21755">with</st>`<st c="21759">-block to bind the application context only within the</st> <st c="21815">Blueprint components.</st>
			<st c="21836">Depending on the scope of the software requirements and the appropriate architecture, any of the given approaches</st> <st c="21950">will be reliable and applicable in building organized, enterprise-grade Flask applications.</st> <st c="22043">Adding more packages and other design patterns is possible, but the core structure emphasized in the previous discussions must remain intact to avoid</st> <st c="22193">cyclic imports.</st>
			<st c="22208">From project structuring, it is time to discuss the setup indicated in the application factory and</st> `<st c="22308">main.py</st>` <st c="22315">of the Blueprints, which is about the configuration of SQLAlchemy ORM and Flask’s</st> <st c="22398">logging mechanism.</st>
			<st c="22416">Applying object-relational mapping (ORM)</st>
			<st c="22457">The most used</st> **<st c="22472">object-relational mapping</st>** <st c="22497">(</st>**<st c="22499">ORM</st>**<st c="22502">) that can work perfectly with the Flask framework is SQLAlchemy.</st> <st c="22569">This</st> <st c="22574">ORM is a boilerplated interface that aims to create a database-agnostic data layer to connect to any database engine.</st> <st c="22692">But compared to other ORMs, SQLAlchemy has support in optimizing native SQL statements, which makes it popular with many database administrators.</st> <st c="22838">When formulating its queries, it only requires Python functions and expressions to pursue</st> <st c="22928">CRUD operations.</st>
			<st c="22944">Before using the ORM, the</st> `<st c="22971">flask-sqlalchemy</st>` <st c="22987">and</st> `<st c="22992">psycopg2-binary</st>` <st c="23007">extensions for the PostgreSQL database must be installed in the virtual environment using the</st> <st c="23102">following command:</st>

pip install psycopg2-binary flask-sqlalchemy


			<st c="23165">What follows next is</st> <st c="23186">the setup of the</st> <st c="23204">database connectivity.</st>
			<st c="23226">Setting up the database connectivity</st>
			<st c="23263">Now, we are ready to implement the configuration file for our database setup.</st> <st c="23342">Flask 3.x supports the declarative extension of SQLAlchemy, which is the commonly used approach in implementing</st> <st c="23453">SQLAlchemy ORM in most frameworks such</st> <st c="23493">as FastAPI.</st>
			<st c="23504">In this approach, the first step is to create the database connectivity by building the SQLAlchemy engine, which manages the connection pooling and the installed dialect.</st> <st c="23676">The</st> `<st c="23680">create_engine()</st>` <st c="23695">function from the</st> `<st c="23714">sqlalchemy</st>` <st c="23724">module derives the engine object with a</st> **<st c="23765">database URL</st>** <st c="23777">(</st>**<st c="23779">DB URL</st>**<st c="23785">) string as its</st> <st c="23802">main parameter.</st> <st c="23818">This URL string contains the database name, DB API driver, account credentials, IP address of the database server, and</st> <st c="23937">its port.</st>
			<st c="23946">Now, the engine is required to create the session factory through the</st> `<st c="24017">sessionmaker()</st>` <st c="24031">method.</st> <st c="24040">And this session factory becomes the essential parameter to the</st> `<st c="24104">session_scoped()</st>` <st c="24120">method in extracting the session registry, which provides the session to SQLAlchemy’s CRUD operations.</st> <st c="24224">The following is the database configuration found in the</st> `<st c="24281">/modules/model/config.py</st>` <st c="24305">module of the</st> `<st c="24320">ch02-blueprint</st>` <st c="24334">project:</st>

从 sqlalchemy 导入 create_engine

从 sqlalchemy.ext.declarative 导入 declarative_base

从 sqlalchemy.orm 导入 sessionmaker, scoped_session

DB_URL = "postgresql://:@localhost:5433/sms" engine = create_engine(DB_URL)

db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) Base = declarative_base()

def init_db():

导入 modules.model.db

			<st c="24743">When the sessions are all set, the derivation of the base object from the</st> `<st c="24818">declarative_base()</st>` <st c="24836">method is</st> <st c="24847">the next focus for the model layer implementation.</st> <st c="24898">The instance returned by this method will subclass all the SQLAlchemy entity or</st> <st c="24978">model classes.</st>
			<st c="24992">Building the model layer</st>
			<st c="25017">Each entity class needs to extend the</st> `<st c="25056">Base</st>` <st c="25060">instance to derive the necessary properties and methods in mapping the schema table to the ORM platform.</st> <st c="25166">It will allow the classes to use the</st> `<st c="25203">Column</st>` <st c="25209">helper class to</st> <st c="25225">build the properties of the actual column metadata.</st> <st c="25278">There are support classes that the models can utilize such as</st> `<st c="25340">Integer</st>`<st c="25347">,</st> `<st c="25349">String</st>`<st c="25355">,</st> `<st c="25357">Date</st>`<st c="25361">, and</st> `<st c="25367">DateTime</st>` <st c="25375">to define the data types and other constraints of the columns, and</st> `<st c="25443">ForeignKey</st>` <st c="25453">to establish parent-child table relationships.</st> <st c="25501">The following are some model classes from the</st> `<st c="25547">/</st>``<st c="25548">modules/model/db.py</st>` <st c="25567">module:</st>

从 sqlalchemy 导入 Time, Column, ForeignKey, Integer, String, Float, Date, Sequence

从 sqlalchemy.orm 导入关系

从 modules.model.config 导入 Base

class Login(Base):tablename = 'login' id = (Integer, Sequence('login_id_seq', increment=1), 主键 = True)

username = (字符串(45))

password = (字符串(45))

user_type = (整数)

admins = 关系('Admin', 反向填充="login", uselist=False)

customer = 关系('Customer', 反向填充="login", uselist=False)

def init(self, username, password, user_type, id = None):

self.id = id

self.username = username

self.password = password

self.user_type = user_type

def repr(self):

    返回 f"<Login {self.id} {self.username} {self.password} {self.user_type}>"

			<st c="26358">Now, the</st> `<st c="26368">relationship()</st>` <st c="26382">directive in the code links to model classes based on their actual reference and foreign keys.</st> <st c="26478">The</st> <st c="26482">model class invokes the method and configures it by setting up some parameters, beginning with the name of the entity it must establish a relationship with and the backreference specification.</st> <st c="26675">The</st> `<st c="26679">back_populates</st>` <st c="26693">parameter refers to the complementary attribute names of the related model classes, which express the rows needed to be queried based on some relationship loading technique, typically the lazy type.</st> <st c="26893">Using the</st> `<st c="26903">backref</st>` <st c="26910">parameter instead of</st> `<st c="26932">back_populates</st>` <st c="26946">is also acceptable.</st> <st c="26967">The following</st> `<st c="26981">Customer</st>` <st c="26989">model class shows its one-to-one relationship with</st> <st c="27040">the</st> `<st c="27045">Login</st>` <st c="27050">entity model as depicted in their respective calls to the</st> `<st c="27109">relationship()</st>` <st c="27123">directive:</st>

class Customer(Base): tablename = 'customer'

id = Column(Integer, ForeignKey('login.id'), primary_key = True)

firstname = Column(String(45))

lastname = Column(String(45))

middlename = Column(String(45))

… … … … … … login = relationship('Login', back_populates="customer") orders = relationship('Orders', back_populates="customer")

shippings = relationship('Shipping', back_populates="customer")

    …  … … … … …

			<st c="27545">The return value of the</st> `<st c="27570">relationship()</st>` <st c="27584">call in</st> `<st c="27593">Login</st>` <st c="27598">is the scalar object of the filtered</st> `<st c="27636">Customer</st>` <st c="27644">record.</st> <st c="27653">Likewise, the</st> `<st c="27667">Customer</st>` <st c="27675">model has the joined</st> `<st c="27697">Login</st>` <st c="27702">instance because of the directive.</st> <st c="27738">On the other hand, the method can also return either a</st> `<st c="27793">List</st>` <st c="27797">collection or scalar value if the relationship is a</st> *<st c="27850">one-to-many</st>* <st c="27861">or</st> *<st c="27865">many-to-one</st>* <st c="27876">type.</st> <st c="27883">When setting this setup in the parent model class, the</st> `<st c="27938">useList</st>` <st c="27945">parameter must be omitted or set to</st> `<st c="27982">True</st>` <st c="27986">to indicate that it will return a filtered list of records from its child class.</st> <st c="28068">However, if</st> `<st c="28080">useList</st>` <st c="28087">is set to</st> `<st c="28098">False</st>`<st c="28103">, the indicated relationship</st> <st c="28132">is</st> *<st c="28135">one-to-one</st>*<st c="28145">.</st>
			<st c="28146">The following</st> `<st c="28161">Orders</st>` <st c="28167">class creates a</st> *<st c="28184">many-to-one</st>* <st c="28195">relationship with the</st> `<st c="28218">Products</st>` <st c="28226">and</st> `<st c="28231">Customer</st>` <st c="28239">models but a</st> *<st c="28253">one-to-one</st>* <st c="28263">relationship</st> <st c="28277">with the</st> `<st c="28286">Payment</st>` <st c="28293">model:</st>

class Orders(Base):

tablename = 'orders'

id = Column(Integer, Sequence('orders_id_seq', increment=1), primary_key = True)

pid = Column(Integer, ForeignKey('products.id'), nullable = False)

… … … … … …

product = relationship('Products', back_populates="orders")

customer = relationship('Customer', back_populates="orders")

payment = relationship('Payment', back_populates="order", uselist=False)

… … … … … …

class Payment(Base):

tablename = 'payment'

id = Column(Integer, Sequence('payment_id_seq', increment=1), primary_key = True)

order_no = Column(String, ForeignKey('orders.order_no'), nullable = False)

… … … … … …

order = relationship('Orders', back_populates="payment")

payment_types = relationship('PaymentType', back_populates="payment")

shipping = relationship('Shipping', back_populates="payment", uselist=False)


			<st c="29131">The</st> `<st c="29136">main.py</st>` <st c="29143">module needs to call the custom method, the</st> `<st c="29188">init_db()</st>` <st c="29197">method found in the</st> `<st c="29218">config.py</st>` <st c="29227">module, to</st> <st c="29238">load, and register all these model classes for the</st> <st c="29290">repository classes.</st>
			<st c="29309">Implementing the repository layer</st>
			<st c="29343">Each repository class</st> <st c="29365">requires SQLAlchemy’s</st> `<st c="29388">Session</st>` <st c="29395">instance to implement its CRUD transactions.</st> <st c="29441">The following</st> `<st c="29455">ProductRepository</st>` <st c="29472">code is a sample repository class that manages the</st> `<st c="29524">product</st>` <st c="29531">table:</st>

from typing import List, Any, Dict

from modules.model.db import Products

from main import app from sqlalchemy.orm import Session class ProductRepository:

def __init__(self, <st c="29712">sess:Session</st>): <st c="29728">self.sess = sess</st> app.logger.info('产品仓库实例创建')

			`<st c="29798">ProductRepository</st>`<st c="29816">’s constructor is essential in accepting the</st> `<st c="29862">Session</st>` <st c="29869">instance from the view or service functions and preparing it for internal processing.</st> <st c="29956">The first transaction is the</st> `<st c="29985">INSERT</st>` <st c="29991">product record transaction that uses the</st> `<st c="30033">add()</st>` <st c="30038">method of the</st> `<st c="30053">Session</st>`<st c="30060">. SQLAlchemy always imposes transaction management in every CRUD operation.</st> <st c="30136">Thus, invoking</st> `<st c="30151">commit()</st>` <st c="30159">of its</st> `<st c="30167">Session</st>` <st c="30174">object is required after successfully executing the</st> `<st c="30227">add()</st>` <st c="30232">method.</st> <st c="30241">The following</st> `<st c="30255">insert()</st>` <st c="30263">method shows the correct implementation of an</st> `<st c="30310">INSERT</st>` <st c="30316">transaction</st> <st c="30329">in SQLAlchemy:</st>

def insert(self, prod:Products) -> bool:

    try: <st c="30391">self.sess.add(prod)</st><st c="30410">self.sess.commit()</st> app.logger.info('产品仓库插入记录')

        return True

    except Exception as e:

        app.logger.info(f'产品仓库插入错误: {e}')

    return False

			<st c="30586">The</st> `<st c="30591">Session</st>` <st c="30598">object has an</st> `<st c="30613">update</st>` <st c="30619">method that can perform an</st> `<st c="30647">UPDATE</st>` <st c="30653">transaction.</st> <st c="30667">The following</st> <st c="30681">is an</st> `<st c="30687">update()</st>` <st c="30695">implementation that updates a</st> `<st c="30726">product</st>` <st c="30733">record based on its primary</st> <st c="30762">key ID:</st>

def update(self, id:int, details:Dict[str, Any]) -> bool:

    try: <st c="30833">self.sess.query(Products).filter(Products.id ==</st> <st c="30880">id).update(details)</st><st c="30900">self.sess.commit()</st> app.logger.info('产品仓库更新记录')

        return True

    except Exception as e:

        app.logger.info(f'产品仓库更新错误: {e}')

    return False

			<st c="31075">The</st> `<st c="31080">Session</st>` <st c="31087">also has a</st> `<st c="31099">delete()</st>` <st c="31107">method that performs record deletion based on a constraint, usually by ID.</st> <st c="31183">The following is an SQLAlchemy way of deleting a</st> `<st c="31232">product</st>` <st c="31239">record based on</st> <st c="31256">its ID:</st>

def delete(self, id:int) -> bool:

    try: <st c="31303">login = self.sess.query(Products).filter(</st> <st c="31344">Products.id == id).delete()</st><st c="31372">self.sess.commit()</st> app.logger.info('产品仓库删除记录')

    return True

    except Exception as e:

        app.logger.info(f'产品仓库删除错误: {e}')

    return False

			<st c="31547">And lastly, the</st> `<st c="31564">Session</st>` <st c="31571">supports a query transaction implementation through its</st> `<st c="31628">query()</st>` <st c="31635">method.</st> <st c="31644">It can allow</st> <st c="31657">the filtering of records using some constraints that will result in retrieving a list or a single one.</st> <st c="31760">The following snippet shows a snapshot of these</st> <st c="31808">query implementations:</st>

def select_all(self) -> List[Any]:

    users = self.sess.query(Products).all()

    app.logger.info('产品仓库检索所有记录')

    return users

def select_one(self, id:int) -> Any:

    users =  self.sess.query(Products).filter( Products.id == id).one_or_none()

    app.logger.info('产品仓库检索一条记录')

    return users

def select_one_code(self, code:str) -> Any:

    users =  self.sess.query(Products).filter( Products.code == code).one_or_none()

    app.logger.info('ProductRepository 通过产品代码检索了一条记录')

    return users

			<st c="32369">Since the</st> `<st c="32380">selectall()</st>` <st c="32391">query transaction must return a list of</st> `<st c="32432">Product</st>` <st c="32439">records, it needs to call the</st> `<st c="32470">all()</st>` <st c="32475">method of the</st> `<st c="32490">Query</st>` <st c="32495">object.</st> <st c="32504">On the other hand, both</st> `<st c="32528">select_one()</st>` <st c="32540">and</st> `<st c="32545">select_one_code()</st>` <st c="32562">use</st> `<st c="32567">Query</st>`<st c="32572">’s</st> `<st c="32576">one_to_many()</st>` <st c="32589">method because they need to return only a single</st> `<st c="32639">Product</st>` <st c="32646">record based on</st> `<st c="32663">select_one()</st>`<st c="32675">’s primary key or</st> `<st c="32694">select_one_code()</st>`<st c="32711">’s unique</st> <st c="32722">key filter.</st>
			<st c="32733">In the</st> `<st c="32741">ch02-blueprint</st>` <st c="32755">project, each Blueprint module has its repository classes placed in their respective</st> `<st c="32841">/repository</st>` <st c="32852">directory.</st> <st c="32864">Whether these repository classes use the</st> `<st c="32905">SQLAlchemy</st>` <st c="32915">instance or</st> `<st c="32928">Session</st>` <st c="32935">of the declarative approach, Flask 3.x has no issues supporting either</st> <st c="33006">of these</st> <st c="33016">repository implementations.</st>
			<st c="33043">Service and repository layers are among the components that require a logging mechanism to audit all the process flows occurring within these two layers.</st> <st c="33198">Let us now explore how to employ software logging in Flask</st> <st c="33257">web applications.</st>
			<st c="33274">Configuring the logging mechanism</st>
			<st c="33308">Flask utilizes the standard logging modules of Python.</st> <st c="33364">The app instance has a built-in</st> `<st c="33396">logger()</st>` <st c="33404">method, which is</st> <st c="33421">pre-configured and can log views, repositories, services, and events.</st> <st c="33492">The only problem is that this default logger cannot perform info logging because the default severity level of the configuration is</st> `<st c="33624">WARNING</st>`<st c="33631">. By the way, turn off debug mode when running applications with a logger to avoid</st> <st c="33714">logging errors.</st>
			<st c="33729">The Python logging mechanism has the following</st> <st c="33777">severity levels:</st>

				*   `<st c="33793">Debug</st>`<st c="33799">: This level has a</st> *<st c="33819">severity value of 10</st>* <st c="33839">and can provide traces of results during the</st> <st c="33885">debugging process.</st>
				*   `<st c="33903">Info</st>`<st c="33908">: This level has a</st> *<st c="33928">severity value of 20</st>* <st c="33948">and can provide general details about</st> <st c="33987">execution flows.</st>
				*   `<st c="34003">Warning</st>`<st c="34011">: This level has a</st> *<st c="34031">severity value of 30</st>* <st c="34051">and can inform about areas of the application that may cause problems in the future due to some changes in the platform or</st> <st c="34175">API classes.</st>
				*   `<st c="34187">Error</st>`<st c="34193">: This level has a</st> *<st c="34213">severity value of 40</st>* <st c="34233">and can track down executions that encountered failures in performing the</st> <st c="34308">expected features.</st>
				*   `<st c="34326">Critical</st>`<st c="34335">: This level</st> <st c="34349">has a</st> *<st c="34355">severity value of 50</st>* <st c="34375">and can show audits of serious issues in</st> <st c="34417">the application.</st>

			<st c="34433">Important note</st>
			<st c="34448">The log level value or severity value provides a numerical weight on a logging level that signifies the importance of the audited log messages.</st> <st c="34593">Usually, the higher the value, the more critical the priority level or log</st> <st c="34668">message is.</st>
			<st c="34679">The logger can only log events with a severity level greater than or equal to the severity level of its configuration.</st> <st c="34799">For instance, if the logger has a severity level of</st> `<st c="34851">WARNING</st>`<st c="34858">, it can only log transactions with warnings, errors, and critical events.</st> <st c="34933">Thus, Flask requires a custom configuration of its</st> <st c="34984">logging setup.</st>
			<st c="34998">In all our three projects, we implemented the following ways to configure the</st> <st c="35077">Flask logger:</st>

				*   **<st c="35090">Approach 1</st>**<st c="35101">: Set up the logger, handlers, and formatter programmatically using the classes from Python’s logging module, as shown in the</st> <st c="35228">following method:</st>

    ```

    def configure_func_logging(log_path): <st c="35284">禁用 "werkzeug" 的日志记录器</st> console_handler =

            logging.<st c="35356">StreamHandler</st>(stream=sys.stdout)

        console_handler.<st c="35407">设置日志级别为 logging.DEBUG</st> logging.basicConfig(<st c="35452">日志级别为 logging.DEBUG</st>,

        format='%(asctime)s %(levelname)s %(module)s

                %(funcName)s %(message)s',

        datefmt='%Y-%m-%d %H:%M:%S', <st c="35702">使用 JSON 格式进行 dictConfig,如下面的片段所示:

    ```py
     def configure_logger(log_path):
               logging.config.dictConfig({
                 'version': 1,
                 'formatters': {
                  'default': {'format': '%(asctime)s
                    %(levelname)s %(module)s %(funcName)s
                    %(message)s', 'datefmt': '%Y-%m-%d
                            %H:%M:%S'}
                    },
                'handlers': {
                  '<st c="35998">console</st>': { <st c="36012">'level': 'DEBUG',</st><st c="36029">'class': 'logging.StreamHandler'</st>,
                    'formatter': 'default',
                    'stream': 'ext://sys.stdout'
                  },
                  '<st c="36121">file</st>': { <st c="36132">'level': 'DEBUG'</st>, <st c="36150">'class':</st><st c="36158">'logging.handlers .RotatingFileHandler'</st>,
                    'formatter': 'default',
                    'filename': log_path,
                    'maxBytes': 1024,
                    'backupCount': 3
                  }
              }, <st c="36286">'loggers'</st>: {
                'default': { <st c="36313">'level': 'DEBUG',</st><st c="36330">'handlers': ['console', 'file']</st> }
            }, <st c="36368">'disable_existing_loggers': False</st> })
    ```

    ```py

			<st c="36404">In maintaining clean logs, it is always a good practice to disable all default loggers, such as the</st> `<st c="36505">werkzeug</st>` <st c="36513">logger.</st> <st c="36522">In applying</st> *<st c="36534">Approach 1</st>*<st c="36544">, disable the server logging by explicitly deselecting the</st> `<st c="36603">werkzeug</st>` <st c="36611">logger from the working loggers.</st> <st c="36645">When using</st> *<st c="36656">Approach 2</st>*<st c="36666">, on the other</st> <st c="36680">hand, setting the</st> `<st c="36699">disable_existing_loggers</st>` <st c="36723">key to</st> `<st c="36731">False</st>` <st c="36736">disables the</st> `<st c="36750">werkzeug</st>` <st c="36758">logger and other</st> <st c="36776">unwanted ones.</st>
			<st c="36790">All in all, both of the given configurations produce a similar logging mechanism.</st> <st c="36873">The</st> `<st c="36877">ch02-factory</st>` <st c="36889">project of our</st> *<st c="36905">Online Shipping Management System</st>* <st c="36938">applied the programmatical approach, and its</st> `<st c="36984">add_payment()</st>` <st c="36997">view function has the following implementation</st> <st c="37045">with logging:</st>

@current_app.route('/payment/add', methods = ['GET', 'POST'])

def add_payment():

if request.method == 'POST': <st c="37169">当前应用日志器信息:add_payment POST 视图</st> <st c="37215">执行</st> repo_type = PaymentTypeRepository(db)

    ptypes = repo_type.select_all()

    orders = get_all_order_no(db) <st c="37327">repo = PaymentRepository(db)</st> payment = Payment(order_no=request.form['order_no'], mode_payment=int(request.form['mode']),

        ref_no=request.form['ref_no'], date_payment=request.form['date_payment'],

        amount=request.form['amount'])

    result = repo.insert(payment)

    if result == False:

        abort(500)

    return render_template('payment/add_payment_form.html', orders=orders, ptypes=ptypes), 200 <st c="37706">当前应用日志器信息:add_payment GET 视图</st> <st c="37751">执行</st> repo_type = PaymentTypeRepository(db)

ptypes = repo_type.select_all()

orders = get_all_order_no(db)

return render_template('payment/add_payment_form.html', orders=orders, ptypes=ptypes), 200

			<st c="37953">Regarding logging the repository layer, the following is a snapshot of</st> `<st c="38025">ShippingRepository</st>` <st c="38043">that</st> <st c="38048">manages shipment transactions and uses logging to audit all</st> <st c="38109">these transactions:</st>

class ShippingRepository:

def __init__(self, db):

    self.db = db <st c="38192">当前应用日志器信息:ShippingRepository</st> <st c="38235">实例创建</st> def insert(self, ship:Shipping) -> bool:

    try:

        self.db.session.add(ship)

        self.db.session.commit() <st c="38352">当前应用日志器信息:ShippingRepository</st> <st c="38395">已插入记录</st> return True

    except Exception as e: <st c="38449">当前应用日志器错误:ShippingRepository</st> <st c="38494">插入错误:{e}</st> return False

    … … … … … …

			<st c="38539">The given</st> `<st c="38550">insert()</st>` <st c="38558">method of the repository uses the</st> `<st c="38593">info()</st>` <st c="38599">method to log the insert transactions found in the</st> `<st c="38651">try</st>` <st c="38654">block, while the</st> `<st c="38672">error()</st>` <st c="38679">method logs the</st> `<st c="38696">exception</st>` <st c="38705">block.</st>
			<st c="38712">Now, every framework has its own way of managing session data, so let us learn how Flask enables its session</st> <st c="38822">handling mechanism.</st>
			<st c="38841">Creating user sessions</st>
			<st c="38864">Assigning an uncompromised</st> <st c="38892">value to Flask’s</st> `<st c="38909">SECRET_KEY</st>` <st c="38919">built-in configuration variable pushes the</st> `<st c="38963">Session</st>` <st c="38970">context into the platform.</st> <st c="38998">Here are the ways to generate the</st> <st c="39032">secret key:</st>

				*   <st c="39043">Apply the</st> `<st c="39054">uuid4()</st>` <st c="39061">method from the</st> `<st c="39078">uuid</st>` <st c="39082">module.</st>
				*   <st c="39090">Utilize any</st> `<st c="39103">openssl</st>` <st c="39110">utility.</st>
				*   <st c="39119">Use the</st> `<st c="39128">token_urlsafe()</st>` <st c="39143">method from the</st> `<st c="39160">secrets</st>` <st c="39167">module.</st>
				*   <st c="39175">Apply encryption tools such as AES, RSA,</st> <st c="39217">and SHA.</st>

			<st c="39225">Our three applications include a separate Python script that runs the</st> `<st c="39296">token_urlsafe()</st>` <st c="39311">method to generate a random key string with 16 random bytes for the</st> `<st c="39380">SECRET_KEY</st>` <st c="39390">environment variable.</st> <st c="39413">The following snippet shows how our applications set the secret key with the</st> `<st c="39490">app</st>` <st c="39493">instance:</st>

(config_dev.toml) SECRET_KEY = "SpOn1ZyV4KE2FTlAUrWRZ_h7o5s" (main.py)

app = Flask(name, template_folder='../app/pages', static_folder='../app/resources')

app.config.from_file("config_dev.toml 文件使用属性 config 的 from_file() 方法,在 TOML 文件中将 SECRET_KEY 环境变量与随机密钥字符串添加到配置文件中,将自动启用用户会话。通常,为任何 Flask 应用程序设置 SECRET_KEY 总是最佳实践。

        <st c="40053">管理会话数据</st>

        <st c="40075">在成功推送会话</st> <st c="40115">上下文后,我们的应用程序可以通过从</st> `<st c="40224">flask</st>` <st c="40229">模块导入的会话对象轻松地将数据存储在会话中。</st> <st c="40238">以下</st> `<st c="40252">login_db_ath()</st>` <st c="40266">视图函数在成功验证用户凭据后会将用户名存储在会话中:</st>
 @current_app.route('/login/auth', methods=['GET', 'POST'])
def login_db_auth():
    if request.method == 'POST':
        current_app.logger.info('add_db_auth POST view executed')
        repo = LoginRepository(db)
        username = request.form['username'].strip()
        password = request.form['password'].strip()
        user:Login = repo.select_one_username(username)
        if user == None:
          flash(f'User account { request.form["username"] } does not exist.', 'error')
          return render_template('login/login.html') , 200
        elif not user.password == password:
          flash('Invalid password.', 'error')
          return render_template('login/login.html') , 200
        else: <st c="40980">session['username'] = request.form['username']</st> return redirect('/menu')
    current_app.logger.info('add_db_auth GET view executed')
    return render_template('login/login.html') , 200
        <st c="41157">使用括号内的会话属性名称(例如,</st> `<st c="41251">session["username"]</st>`<st c="41270">)调用</st> `<st c="41170">session</st>` <st c="41177">对象可以在运行时检索会话数据。</st> <st c="41312">另一方面,删除会话需要调用会话对象的</st> `<st c="41373">pop()</st>` <st c="41378">方法。</st> <st c="41409">例如,删除用户名需要执行以下代码:</st>
 session.pop("username", None)
        <st c="41513">在删除它们或执行其他事务之前首先验证会话属性始终是一个</st> <st c="41620">建议,以下代码片段将展示我们如何验证</st> <st c="41691">会话属性:</st>
 @app.before_request
def init_request():
    get_database()
    if (( request.endpoint != 'login_db_auth' and  request.endpoint != 'index' and request.endpoint != 'static')  and <st c="41878">'username' not in session</st>):
        app.logger.info('a user is unauthenticated')
        return redirect('/login/auth')
    elif (( request.endpoint == 'login_db_auth' and  request.endpoint != 'index' and request.endpoint != 'static')  and <st c="42097">'username' in session</st>):
        app.logger.info('a user is already logged in')
        return redirect('/menu')
        <st c="42193">如前所述,带有</st> `<st c="42239">@before_request</st>` <st c="42254">装饰器的方法定总是在任何路由函数执行之前首先执行。</st> <st c="42323">它在请求到达路由之前处理一些前置事务。</st> <st c="42406">在给定的代码片段中,</st> `<st c="42428">@before_request</st>` <st c="42443">执行了</st> `<st c="42457">get_database()</st>` <st c="42471">方法,并检查是否有认证用户已经登录到应用程序中。</st> <st c="42562">如果有已登录用户,则除了索引和静态资源之外,对任何端点的访问都将始终重定向用户到菜单页面。</st> <st c="42700">否则,它将始终重定向用户到</st> <st c="42751">登录页面。</st>

        <st c="42762">清除所有会话数据</st>

        <st c="42788">而不是删除每个会话属性,会话对象有一个</st> `<st c="42838">clear()</st>` <st c="42845">方法,只需一次调用即可删除所有会话数据。</st> <st c="42922">以下是一个</st> `<st c="42941">logout</st>` <st c="42947">路由,在将用户重定向到</st> <st c="43021">登录页面</st>之前删除所有会话数据:</st>
 @current_app.route('/logout', methods=['GET'])
def logout(): <st c="43094">session.clear()</st> current_app.logger.info('logout view executed')
    return redirect('/login/auth')
        <st c="43188">在 Flask 中没有简单的方法来使会话无效,但</st> `<st c="43250">clear()</st>` <st c="43257">可以帮助为另一个用户访问会话做准备。</st>

        <st c="43317">现在,另一个很大程度上依赖于会话处理的组件是闪存消息,它将字符串类型的消息存储在</st> <st c="43439">会话中。</st>

        <st c="43449">应用闪存消息</st>

        <st c="43473">闪存消息通常在经过验证的表单上显示,为每个具有无效输入值的文本字段提供错误消息。</st> <st c="43592">有时,闪存消息是标题或重要通知,以全大写形式打印在网页上。</st>

        <st c="43700">Flask 有一个闪存方法,任何视图函数都可以导入以创建闪存消息。</st> <st c="43784">以下认证过程在从</st> <st c="43888">数据库验证用户凭据后创建一个闪存消息:</st>
 @current_app.route('/login/add', methods=['GET', 'POST'])
def add_login():
    if request.method == 'POST':
        current_app.logger.info('add_login POST view executed')
        login = Login(username=request.form['username'], password=request.form['password'], user_type=int(request.form['user_type']) )
        repo = LoginRepository(db)
        result = repo.insert(login)
        if result == True: <st c="44263">flash('Successully added a user', 'success')</st> else: <st c="44314">flash(f'Error adding { request.form["username"]</st> <st c="44361">}', 'error')</st> return render_template('login/login_add.html') , 200
    current_app.logger.info('add_login GET view executed')
    return render_template('login/login_add.html') , 200
        <st c="44535">给定的</st> `<st c="44546">add_login()</st>` <st c="44557">视图函数使用</st> `<st c="44577">flash()</st>` <st c="44584">在路由接受的凭据已在数据库中时创建错误消息。</st> <st c="44682">但它也通过</st> `<st c="44723">flash()</st>` <st c="44730">发送通知,如果</st> `<st c="44738">插入</st>` <st c="44744">事务</st> <st c="44757">成功。</st>

        <st c="44771">重要提示</st>

        <st c="44786">Flask 的闪存系统在每个请求结束时将消息记录到用户会话中,并在随后的立即请求事务中检索它们。</st> <st c="44924">请求事务。</st>

        *<st c="44944">图 2</st>**<st c="44953">.4</st>* <st c="44955">显示了添加现有用户名和密码后的样本屏幕结果:</st>

        ![图 2.4 – 一个无效插入事务的闪存消息](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_02_4.jpg)

        <st c="45153">图 2.4 – 一个无效插入事务的闪存消息</st>

        <st c="45215">Jinja2 模板可以访问 Flask 的</st> `<st c="45240">get_flashed_messages()</st>` <st c="45280">方法,该方法检索所有闪存消息或只是分类的闪存消息。</st> <st c="45356">以下</st> `<st c="45390">/login/login_add.html</st>` <st c="45411">模板的 Jinja2 宏在</st> *<st c="45456">图 2</st>**<st c="45464">.4</st>*<st c="45466">中渲染错误闪存消息:</st>
 {% macro render_error_flash(class_id) %} <st c="45510">{% with errors =</st> <st c="45526">get_flashed_messages(category_filter=["error"]) %}</st> {% if errors %}
            <p id="{{class_id}}" class="w-lg-50">
            {% for msg in errors %}
                {{ msg }}
            {% endfor %}
            </p>
        {% endif %}
    {% endwith %}
{% endmacro %}
        <st c="45724">The</st> `<st c="45729">with</st>`<st c="45733">-block 提供检查是否需要渲染错误类型闪存消息的上下文。</st> <st c="45841">如果有,一个</st> `<st c="45857">for</st>`<st c="45860">-block 将检索所有这些检索到的</st> <st c="45902">闪存消息。</st>

        <st c="45917">另一方面,Jinja2</st> <st c="45944">也可以从视图函数中检索未分类或通用的闪存消息。</st> <st c="46027">以下宏从</st> `<st c="46082">list_login()</st>` <st c="46094">路由中检索闪存消息:</st>
 {%macro render_list_flash()%} <st c="46132">{% with messages = get_flashed_messages() %}</st> {% if messages %}
            <h1 class="display-4 ">
                {% for message in messages %}
                    {{ message }}
                {% endfor %}
            </h1>
        {% endif %}
    {% endwith %}
{%endmacro%}
        <st c="46320">鉴于宏在渲染闪存消息中的应用,让我们探索我们的应用程序中 Jinja2 模板的其他高级功能,这些功能可以提供更好的</st> <st c="46477">模板实现。</st>

        <st c="46501">利用一些高级 Jinja2 功能</st>

        *<st c="46541">第一章</st>* <st c="46551">介绍了 Jinja2 引擎和</st> <st c="46585">模板,并将其中一些 Jinja 构造应用于渲染</st> <st c="46655">HTML 内容:</st>

            +   `<st c="46669">{{ variable }}</st>`<st c="46684">:这是一个占位符表达式,用于从</st> <st c="46755">视图函数中渲染单值对象。</st>

            +   `<st c="46770">{% statement %</st>`<st c="46785">》:实现</st> `<st c="46820">if</st>`<st c="46822">-</st>`<st c="46824">else</st>`<st c="46828">-条件、</st> `<st c="46842">for</st>`<st c="46845">-循环、</st> `<st c="46854">block</st>`<st c="46859">-表达式</st> <st c="46873">用于调用布局片段、</st> `<st c="46903">with</st>`<st c="46907">-块用于管理上下文,以及</st> <st c="46942">宏调用。</st>

        <st c="46954">但是,一些 Jinja2 功能,如应用</st> `<st c="47002">with</st>`<st c="47006">-语句、宏、过滤器以及注释,可以帮助我们为</st> <st c="47085">路由生成更好的视图。</st>

        <st c="47096">应用 with 块和宏</st>

        <st c="47128">在</st> *<st c="47136">应用闪存消息</st>* <st c="47159">部分,模板使用了</st> `<st c="47188">{% with %}</st>` <st c="47198">语句从视图</st> <st c="47252">函数中提取闪存消息,并在</st> `<st c="47267">{% macro %}</st>` <st c="47278">中优化我们的 Jinja2 事务。</st> <st c="47318">`<st c="47322">{% with %}</st>` <st c="47332">语句设置一个上下文,以限制</st> `<st c="47416">with</st><st c="47420">-block</st>` <st c="47429">中某些变量的访问或作用域。</st> <st c="47465">在块外部访问会产生一个</st> <st c="47429">Jinja2 错误。</st>

        <st c="47478">另一方面,</st> `<st c="47483">{% macro %}</st>` <st c="47494">块在 Jinja2 模板中追求模块化编程。</st> <st c="47571">每个宏都有一个名称,并且可以具有用于重用的局部参数,任何模板都可以像典型方法一样导入和调用它们。</st> <st c="47706">以下</st> `<st c="47720">/login/login_list.html</st>` <st c="47742">模板通过调用输出未分类</st> <st c="47842">闪存消息的宏来渲染用户凭据列表:</st>
<st c="47856">{% from "macros/flask_segment.html" import</st> <st c="47899">render_list_flash with context</st> %}
<!DOCTYPE html>
<html lang="en">
    <head>
        <title>List Login Accounts</title>
        <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css')}}">
        <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css')}}">
        <script src="img/jquery-3.6.4.js') }}"></script>
        <script src="img/bootstrap.bundle.min.js') }}"></script>
    </head>
    <body>
        <section class="position-relative py-4 py-xl-5">
            <div class="container position-relative">
                <div class="row d-flex">
                    <div class="col-md-8 col-xl-6 text-center mx-auto"> <st c="48527">{{render_list_flash()}}</st> … … … … … …
                     </div>
                </div>
                … … … … … …
        </section>
    </body>
</html>
        <st c="48614">所有宏都放置在一个</st> <st c="48642">模板文件中,就像任何 Jinja2 表达式一样。</st> <st c="48686">在我们的应用程序中,宏位于</st> `<st c="48730">/macros/flask_segment.html</st>`<st c="48756">,并且任何模板都必须使用</st> `<st c="48817">{% from ...</st> <st c="48829">import ...</st> <st c="48840">with context %}</st>` <st c="48855">语句从该文件导入它们,在使用之前。</st> <st c="48889">在给定的模板中,</st> `<st c="48912">render_list_flash()</st>` <st c="48931">首先导入,然后像方法一样使用</st> `<st c="48992">{{}}</st>` <st c="48996">占位符表达式调用它。</st>

        <st c="49020">应用过滤器</st>

        <st c="49037">为了提高渲染数据的视觉效果、清晰度和可读性,Jinja2 提供了几个过滤器操作,可以提供额外的</st> <st c="49166">美学,使渲染结果更吸引用户。</st> <st c="49234">这个过程被称为</st> `<st c="49296">|</st>`<st c="49298">) 以将这些值传递给这些操作。</st> <st c="49339">以下</st> `<st c="49353">product/list_product.html</st>` <st c="49378">页面在渲染产品列表时使用了过滤器方法:</st>
 <!DOCTYPE html>
<html lang="en">
    <head>
        <title>List of Products</title>
        <link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css')}}">
        <link rel="stylesheet" href="{{ url_for('static', filename='css/bootstrap.min.css')}}">
        <script src="img/jquery-3.6.4.js') }}"></script>
        <script src="img/bootstrap.bundle.min.js') }}"></script>
    </head>
    <body>
        <table>
        {% for p in prods %}
            <tr>
                <td>{{p.id}}</td>
                <td>{{p.name|<st c="49926">trim</st>|<st c="49933">upper</st>}}</td>
                <td>{<st c="49952">{"\u20B1%.2f"|format(</st>p.price<st c="49982">)</st> }}</td>
                <td>{{p.code}}</td>
            </tr>
        {% endfor %}
        </table>
    </body>
</html>
        <st c="50056">给定的模板使用 trim 过滤器去除名称数据的前后空白,并使用 upper 过滤器将名称转换为大写。</st> <st c="50213">通过格式过滤器,所有价格数据现在</st> <st c="50263">都包含带有两位小数的菲律宾比索货币符号。</st> <st c="50329">Jinja2 支持几个内置过滤器,可以帮助从视图函数中派生其他功能、计算、操作、修改、压缩、扩展和清理原始数据,以便以更</st> <st c="50547">可展示的结果渲染所有这些细节。</st>

        <st c="50567">添加注释</st>

        <st c="50583">在模板中添加注释始终是最好的实践,使用</st> `<st c="50655">{# comment #}</st>` <st c="50668">表达式进行分节和内部文档目的。</st> <st c="50732">这些注释不是由 Jinja2</st> <st c="50800">模板引擎提供的渲染的一部分。</st>

        <st c="50816">Jinja2 表达式不仅应用于路由视图,也应用于错误页面。</st> <st c="50897">现在让我们学习如何在 Flask</st> <st c="50953">3.x 框架中渲染错误页面。</st>

        <st c="50967">实现错误处理解决方案</st>

        *<st c="51005">第一章</st>* <st c="51015">展示了在给定状态码(如状态码</st> `<st c="51129">500</st>`<st c="51132">)的情况下渲染错误页面时使用 redirect()方法的使用。我们现在将讨论一种更好的管理异常</st> <st c="51189">和状态码的方法,包括根据</st> <st c="51245">状态码触发错误页面。</st>

        <st c="51257">Flask 应用程序必须始终实现一个错误处理机制,使用以下任何一种策略:</st>

            +   <st c="51365">使用应用中的 register_error_handler()方法注册一个自定义错误函数。</st>

            +   <st c="51448">使用应用中的 errorhandler 装饰器创建一个错误处理器。</st>

            +   <st c="51513">抛出一个</st> <st c="51523">自定义</st> `<st c="51530">异常</st>` <st c="51539">类。</st>

        <st c="51546">使用 register_error_handler 方法</st>

        <st c="51586">实现错误处理器的声明式方法是创建一个自定义函数</st> <st c="51623">并将其注册到</st> `<st c="51691">app</st>`<st c="51694">的</st> `<st c="51698">register_error_handler()</st>` <st c="51722">方法。</st> <st c="51731">自定义函数必须有一个局部参数,该参数将接受来自平台的注入的错误消息。</st> <st c="51849">它还必须使用</st> `<st c="51903">make_response()</st>` <st c="51918">和</st> `<st c="51923">render_template()</st>` <st c="51940">方法返回其分配的错误页面,并且可以选择将错误消息作为上下文数据传递给模板进行渲染。</st> <st c="52041">以下是一个示例代码片段,展示了</st> <st c="52079">以下步骤:</st>
 def server_error(<st c="52107">e</st>):
    print(e)
    return make_response(render_template("error/500.html", title="Internal server error"), 500)
app.<st c="52265">register_error_handler()</st> method has two parameters:

				*   <st c="52316">The status code that will trigger the</st> <st c="52355">error handling.</st>
				*   <st c="52370">The function name of the custom</st> <st c="52403">error handler.</st>

			<st c="52417">There should only be one registered custom error handler per</st> <st c="52479">status code.</st>
			<st c="52491">Applying the @errorhandler decorator</st>
			<st c="52528">The easiest way to</st> <st c="52547">implement error handlers is to</st> <st c="52578">decorate customer error handlers with the app’s</st> `<st c="52627">errorhandler()</st>` <st c="52641">decorator.</st> <st c="52653">The structure and behavior of the custom method are the same as the previous approach except that it has an</st> `<st c="52761">errorhandler</st>` <st c="52773">decorator with the assigned status code.</st> <st c="52815">The following shows the error handlers implemented</st> <st c="52865">using</st> <st c="52872">the decorator:</st>

@app.errorhandler(404) def not_found(e):

return make_response(render_template("error/404.html", title="页面未找到"), 404) <st c="53013">@app.errorhandler(400)</st> def bad_request(e):

return make_response(render_template("error/400.html", title="请求错误"), 400)

			<st c="53137">Accessing an invalid URL path</st> <st c="53168">will auto-render the error page in</st> *<st c="53203">Figure 2</st>**<st c="53211">.5</st>* <st c="53213">because of the given error handler for HTTP status</st> <st c="53265">code</st> `<st c="53270">404</st>`<st c="53273">:</st>
			![Figure 2.5 – An error page rendered by the not_found() error handler](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_02_5.jpg)

			<st c="53321">Figure 2.5 – An error page rendered by the not_found() error handler</st>
			<st c="53389">Creating custom exceptions</st>
			<st c="53416">Another wise approach is</st> <st c="53441">assigning custom exceptions to error handlers.</st> <st c="53489">First, create a custom exception by subclassing</st> `<st c="53537">HttpException</st>` <st c="53550">from the</st> `<st c="53560">werkzeug.exceptions</st>` <st c="53579">module.</st> <st c="53588">The following shows how to create</st> <st c="53621">custom exceptions for</st> <st c="53644">Flask transactions:</st>

from werkzeug.exceptions import HTTPException from flask import render_template, Response

class DuplicateRecordException(HTTPException):

code = 500

description = '记录已存在。' def get_response(self, environ=None):

    resp = Response()

    resp.response = render_template('error/generic.html',

        ex_message=self.description)

    return resp

			<st c="54001">View functions and repository methods can throw this custom</st> `<st c="54062">DuplicateRecordException</st>` <st c="54086">class when an</st> `<st c="54101">INSERT</st>` <st c="54107">record transaction encounters a primary or unique key duplicate error.</st> <st c="54179">It requires setting the two inherited fields from the parent</st> `<st c="54240">HTTPException</st>` <st c="54253">class, namely the</st> `<st c="54272">code</st>` <st c="54276">and</st> `<st c="54281">description</st>` <st c="54292">fields.</st> <st c="54301">Once triggered, the exception class can auto-render its error page when it has an overridden</st> `<st c="54394">get_response()</st>` <st c="54408">method that creates a custom</st> `<st c="54438">Response</st>` <st c="54446">object to make way for the rendering of its error page with the</st> <st c="54511">exception message.</st>
			<st c="54529">But overriding the</st> `<st c="54549">get_response()</st>` <st c="54563">instance method of the custom exception is just an option.</st> <st c="54623">Sometimes, assigning values to the code and description fields is enough, and then we map them to a custom error handler for the rendition of its error page, either through the</st> `<st c="54800">@errorhandler</st>` <st c="54813">decorator or</st> `<st c="54827">register_error_handler()</st>`<st c="54851">. The following code shows this kind</st> <st c="54888">of approach:</st>

@app.errorhandler(DuplicateRecordException) def insert_record_exception(e): DuplicateRecordException 类,事件处理器将返回其重写的 get_response() 方法,该方法带有映射的 Jinja2 错误页面和 HTTP 状态码 500。但如果触发的异常是 Python 类型的呢?

        <st c="55272">管理内置异常</st>

        <st c="55301">所有之前</st> <st c="55330">展示的处理程序仅管理 Flask 异常,而不是 Python 特定的异常。</st> <st c="55410">为了包括处理由某些 Python 运行时问题生成的异常,创建一个专门的定制方法处理程序,它监听所有这些异常,例如在以下实现中:</st> <st c="55585">以下是一个示例实现:</st>
 from werkzeug.exceptions import HTTPException <st c="55657">@app.errorhandler(Exception)</st> def handle_built_exception(e):
    if isinstance(e, HTTPException):
        return e <st c="55759">return render_template("error/generic.html",</st> <st c="55803">title="Internal server error", e=e), 500</st>
        <st c="55844">给定的错误处理器过滤掉所有与 Flask 相关的异常,并将它们抛给 Flask 处理器处理,但对于任何 Python</st> <st c="56007">运行时异常,它将渲染自定义错误页面。</st>

        <st c="56025">触发错误处理器</st>

        `<st c="56055">有时建议显式触发错误处理程序,尤其是在使用 Blueprints 作为其应用程序构建块的项目中。</st>` `<st c="56174">Blueprint 模块不是一个独立的子应用程序,它不能拥有一个 URL 上下文,该上下文可以监听并直接调用精确的错误处理程序。</st>` `<st c="56361">因此,为了避免调用精确错误处理程序时出现的一些问题,事务可以调用`<st c="56453">abort()</st>` `<st c="56460">方法,并使用适当的 HTTP 状态码,如下面的片段所示:</st>`
 @current_app.route('/payment/add', methods = ['GET', 'POST'])
def add_payment():
    if request.method == 'POST':
        current_app.logger.info('add_payment POST view executed')
        … … … … … …
        result = repo.insert(payment)
        if result == False: <st c="56766">abort(500)</st> return render_template('payment/add_payment_form.html', orders=orders, ptypes=ptypes), 200
    current_app.logger.info('add_payment GET view executed')
    … … … … … …
    return render_template('payment/add_payment_form.html', orders=orders, ptypes=ptypes), 200
        `<st c="57027">对于自定义或内置异常,事务可以调用`<st c="57089">raise()</st>` `<st c="57096">方法来触发抛出异常的错误处理程序。</st>` `<st c="57159">以下视图函数在订单记录插入期间出现问题时抛出`<st c="57198">DuplicateRecordException</st>` `<st c="57222">类:</st>`
 @current_app.route('/orders/add', methods=['GET', 'POST'])
def add_order():
    if request.method == 'POST':
        current_app.logger.info('add_order POST view executed')
        repo = OrderRepository(db)
        … … … … … …
        result = repo.insert(order)
        if result == False: <st c="57529">raise DatabaseException()</st> customers = get_all_cid(db)
        products = get_all_pid(db)
        return render_template('order/add_order_form.html', customers=customers, products=products), 200
    current_app.logger.info('add_order GET view executed')
    customers = get_all_cid(db)
    products = get_all_pid(db)
    return render_template('order/add_order_form.html', customers=customers, products=products), 200
        所有通用的错误处理程序都放置在`<st c="57963">main.py</st>` `<st c="57970">模块中,而自定义和组件特定的异常类则放在单独的`<st c="58053">模块中,以符合编码标准并便于调试。</st>`

        `<st c="58135">现在,错误页面和其他 Jinja2 模板也可以使用`*<st c="58203">CSS</st>` `<st c="58206">、`<st c="58208">JavaScript</st>` `<st c="58218">、`<st c="58220">图像</st>` `<st c="58226">和其他静态资源,为它们的内容添加外观和感觉功能。</st>`

        `<st c="58302">添加静态资源</st>`

        `<st c="58326">静态资源为 Flask Web 应用程序提供用户体验。</st>` `<st c="58400">这些静态资源包括一些模板页面所需的 CSS、JavaScript、图像和视频文件。</st>` `<st c="58518">现在,Flask 不允许在项目的任何地方添加这些文件。</st>` `<st c="58588">通常,Flask 构造函数有一个`<st c="58627">static_folder</st>` `<st c="58640">参数,它接受一个专用目录的相对路径,用于存放这些文件。</st>`

        在`<st c="58725">ch02-factory</st>` `<st c="58737">中,`<st c="58739">create_app()</st>` `<st c="58751">配置了 Flask 实例,允许将资源放置在主项目目录的`<st c="58820">/resources</st>` `<st c="58830">文件夹中。</st>` `<st c="58869">以下`<st c="58894">create_app()</st>` `<st c="58906">的片段显示了使用`<st c="58946">resource</st>` `<st c="58954">文件夹设置的 Flask 实例化:</st>`
 def create_app(config_file):
    app = Flask(__name__, template_folder='../app/pages', <st c="59052">static_folder='../app/resources'</st>)
    app.config.from_file(config_file, toml.load)
    db.init_app(app)
    configure_func_logging('log_msg.txt')
    … … … … … …
        同时,在`<st c="59198">ch02-blueprint</st>` `<st c="59217">项目</st>`中,主项目和其蓝图可以分别拥有各自的`<st c="59303">/resources</st>` `<st c="59313">目录</st>`。以下片段展示了一个带有自己`<st c="59392">resources</st>` `<st c="59401">文件夹设置</st>`的蓝图配置:
 shipping_bp = Blueprint('shipping_bp', __name__,
    template_folder='pages', <st c="59596">/resources</st> folder of the main application, while *<st c="59645">Figure 2</st>**<st c="59653">.7</st>* shows the <st c="59666">/resources</st> folder of the shipping Blueprint package:
			![Figure 2.6 – The location of /resources in the main application](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_02_6.jpg)

			<st c="59837">Figure 2.6 – The location of /resources in the main application</st>
			![Figure 2.7 – The location of /resources in the shipping Blueprint](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_02_7.jpg)

			<st c="59985">Figure 2.7 – The location of /resources in the shipping Blueprint</st>
			<st c="60050">The</st> `<st c="60055">static</st>` <st c="60061">directory is the default and common folder name used to contain the Flask application’s web assets.</st> <st c="60162">But in the</st> <st c="60173">succeeding chapters, we will use</st> `<st c="60206">/resources</st>` <st c="60216">instead of</st> `<st c="60228">/static</st>` <st c="60235">for naming</st> <st c="60247">convention purposes.</st>
			<st c="60267">Accessing the assets in the templates</st>
			<st c="60305">To avoid accessing relative paths, Flask manages, accesses, and loads static files or web assets in template pages using</st> `<st c="60427">static_url_path</st>`<st c="60442">, a logical path name used to access web resources from</st> <st c="60497">the</st> `<st c="60502">static</st>` <st c="60508">folder.</st> <st c="60517">Its default path</st> <st c="60534">value is</st> `<st c="60543">static</st>`<st c="60549">, but applications can set an appropriate value</st> <st c="60597">if needed.</st>
			<st c="60607">Our application uses the Bootstrap 4 framework to apply responsive web design.</st> <st c="60687">All its assets are in the</st> `<st c="60713">/resources</st>` <st c="60723">folder, and the following</st> `<st c="60750">menu.html</st>` <st c="60759">template shows how to access these assets from</st> <st c="60807">the folder:</st>

<head>

    <title>主菜单</title>

    <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">

    <link rel="stylesheet" href="<st c="61002">{{ url_for('static', filename='css/styles.css')}}</st>">

    <link rel="stylesheet" href="<st c="61085">{{ url_for('static', filename='css/bootstrap.min.css')}}</st>">

    <script src="img/st>**<st c="61159">{{ url_for('static', filename='js/jquery-3.6.4.js') }}</st>**<st c="61214">"></script>

    <script src="img/st>**<st c="61240">{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}</st>**<st c="61303">"></script>

</head>

<body>

    <div class="container py-4 py-xl-5">

        <div class="row mb-5">

            <div class="col-md-8 col-xl-6 text-center mx-auto">

                <h2 class="display-4">供应管理系统菜单</h2>

                <p class="w-lg-50"><strong><em>{{ session['username']}}</em></strong> 已登录。</p>

            </div>

        </div>

        <div class="row row-cols-1 row-cols-md-2 row-cols-xl-3">

            <div class="col">

                <div class="d-flex p-3">

                    <div class="px-2">

                        <h5 class="mb-0 mt-1"><a href="#">添加配送员</a></h5>

                    </div>

                </div>

            </div>

            <div class="col">

                <div class="d-flex p-3">

        … … … … … …

</body>

			<st c="61876">The</st> `<st c="61881">url_for()</st>` <st c="61890">function, used to</st> <st c="61908">access view endpoints in the</st> <st c="61938">templates, is the way to access the static resources from the</st> `<st c="62000">/resources</st>` <st c="62010">folder using</st> `<st c="62024">static_url_path</st>` <st c="62039">as the</st> <st c="62046">directory name.</st>
			<st c="62062">Summary</st>
			<st c="62070">This chapter provided information on additional features of Flask that can support building complete, enterprise-grade, scalable, and complex but manageable Flask web applications.</st> <st c="62252">The details about adding error handling in many ways, integrating the Bootstrap framework to the application without using extensions, implementing SQLAlchemy using the declarative and standard approaches, and optimizing the Jinja2 templates using macros, indicate that the Flask framework is a lightweight but powerful solution to building</st> <st c="62593">web applications.</st>
			<st c="62610">After learning about creating full-blown web applications with Flask, let us discuss and highlight, in the next chapter, the components and procedures for building API-based applications using the Flask</st> <st c="62814">3.x framework.</st>



第四章:3

创建 RESTful 网络服务

尽管 Flask 是一个流行的轻量级网络框架,但它也可以支持 RESTful 网络服务的实现。 它有自己的 JSON 编码器和解码器,内置的 JSON 支持机制用于响应生成和错误处理,易于管理的 RESTful 请求分发,以及精简的配置方法。 与其他 API 框架不同,Flask 由于需要维护的项目结构,因此使用更多的模块和包。 然而,在相应地设置目录结构后,后续步骤将无缝、轻量级且直接。

本章将介绍 Flask 框架中处理构建 API 端点以向子模块或其他应用程序提供数据和服务的部分。 目标是了解 Flask 如何管理在其环境中运行的 RESTful 端点的传入请求和传出响应。 此外,本章还将讨论将构成 Flask API 端点实现的各种组件。 端点实现。

以下是本章将涵盖的主题,以了解使用 Flask 开发 API:

  • 设置 RESTful 应用程序

  • 实现 API 端点

  • 管理请求 和响应

  • 利用响应编码器 和解码器

  • 过滤 API 请求 和响应

  • 处理异常

  • 消费 API 端点

技术要求

本章使用一个简单的在线披萨订购系统来展示 Flask 框架在开发 RESTful 网络服务方面的能力。 <st c="1508">3</st> 个应用程序具有登录、产品库存、订单和支付模块,其业务范围是确定开发所需的 Flask 组件和实用工具。 此外,还包含一个客户端应用程序 <st c="1724">ch03-client</st>,以展示如何消费 Flask API 端点。 这两个应用程序都使用 PostgreSQL 作为其数据库管理系统,并使用 SQLAlchemy 作为其 ORM。 所有这些项目都已上传至 https://github.com/PacktPublishing/Mastering-Flask-Web-Development/tree/main/ch03

设置 RESTful 应用程序

首先,创建 项目的虚拟环境,它将作为所需模块扩展的本地仓库。 接下来,打开 VSCode 编辑器,创建主项目文件夹,并使用 VSCode 的命令行解释器通过 <st c="2273">flask</st> 扩展模块使用 <st c="2306">pip</st> 命令进行安装。

之后,管理目录结构,例如为 第二章 项目 所做的安排。 从三种方法中,即应用工厂设计, <st c="2527">蓝图</st>,以及这两种方法的混合,我们的在线披萨订购应用将使用应用工厂方法来放置其自定义异常类、模型、仓库、服务、实用工具、API 和数据库配置在一个 <st c="2772">app</st> 文件夹中,并使用 <st c="2827">create_app()</st> 方法注册所有这些组件。 图 3**.1 显示了我们的 原型应用 的项目目录结构。

图 3.1 – RESTful 应用的目录结构

图 3.1 – RESTful 应用的目录结构

日志设置、 SQLAlchemy 声明性配置,以及在第 第二章 中创建的会话都保留并用于此应用。 另一方面,仍然放置在 <st c="3293">create_app()</st>,位于 <st c="3327">__init__.py</st> <st c="3346">app</st> 包中,实现如下:

 def create_app(config_file):
    app = Flask(__name__)
    app.config.from_file(config_file, toml.load)
    init_db()
    configure_logger('log_msg.txt')
    with app.app_context():
        from app.api import index
        … … … … … … …
        from app.api import orders
    return app

<st c="3630">main.py</st> 仍然包含错误处理程序和服务器启动的 <st c="3675">app.run()</st> 方法。 相同的命令, <st c="3734">python main.py</st>,将运行应用。 然而, <st c="3789">ch03</st> 应用 将不是基于 Web 的 而是基于 API 的。

让我们剖析我们的应用,并确定用于构建 REST 服务 的 Flask 组件。

实现 API 端点

The implementation of API endpoints uses the same bolts and knots applied in creating web-based components in Chapters 1 and 2, such as declaring path variables, accessing the request through the <st c="4160">request</st> proxy object, returning the same <st c="4201">Response</st> object, and using the same <st c="4237">@route()</st> decorator. A GET API endpoint that returns a JSON response is as follows:

<st c="4319">@current_app.route("/index", methods = ['GET'])</st> def index():
   response = <st c="4392">make_response</st>(<st c="4407">jsonify</st>(message='This is an Online Pizza Ordering System.', today=date.today()), 200) <st c="4515">index()</st> function, found in the <st c="4546">app/api/index.py</st> module, is exactly similar to the web-based view function, except that <st c="4634">make_response()</st> requires the <st c="4663">jsonify()</st> instead of the <st c="4688">render_template()</st> method.
			<st c="4713">The</st> `<st c="4718">jsonify()</st>` <st c="4727">is a Flask utility method that serializes any data to produce an</st> `<st c="4793">application/json</st>` <st c="4809">response.</st> <st c="4820">It converts</st> *<st c="4832">multiple values</st>* <st c="4847">into an</st> *<st c="4856">array of data</st>* <st c="4869">and</st> *<st c="4874">key-value pairs</st>* <st c="4889">to a</st> *<st c="4895">dictionary</st>*<st c="4905">. It can also accept a</st> *<st c="4928">single-valued</st>* <st c="4941">entry.</st> <st c="4949">The</st> `<st c="4953">jsonify()</st>` <st c="4962">in the given</st> `<st c="4976">index()</st>` <st c="4983">function converts its arguments into a dictionary before calling Python’s</st> `<st c="5058">json.dumps()</st>` <st c="5070">method.</st> <st c="5079">After</st> `<st c="5085">json.dumps()</st>`<st c="5097">’s JSON serialization,</st> `<st c="5121">jsonify()</st>` <st c="5130">will contain and render the result as part of</st> `<st c="5177">Response</st>` <st c="5185">with a mime-type of</st> `<st c="5206">application/json</st>` <st c="5222">instead of a plain JSON string.</st> <st c="5255">Thus, running the given</st> `<st c="5279">/index</st>` <st c="5285">endpoint with the</st> `<st c="5304">curl -i</st>` <st c="5311">command will generate the following request</st> <st c="5356">header result:</st>
			![Figure 3.2 – Running the /index endpoint using cURL](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_03_2.jpg)

			<st c="5653">Figure 3.2 – Running the /index endpoint using cURL</st>
			<st c="5704">The response body</st> <st c="5723">provided by running the curl command against</st> `<st c="5768">/index</st>` <st c="5774">has a message body and response headers composed of</st> `<st c="5827">Server</st>`<st c="5833">,</st> `<st c="5835">Date</st>`<st c="5839">,</st> `<st c="5841">Content-Type</st>`<st c="5853">,</st> `<st c="5855">Content-Length</st>`<st c="5869">, and</st> `<st c="5875">Connection</st>`<st c="5885">.</st> `<st c="5887">Content-Type</st>` <st c="5899">indicates the resource type the</st> `<st c="5932">/index</st>` <st c="5938">API will return to the client.</st> <st c="5970">Aside from strings, the</st> `<st c="5994">jsonify()</st>` <st c="6003">method can also serialize and render an array of objects like in the following API function that returns an array of string data and some</st> <st c="6142">single-valued objects:</st>

@current_app.route("/introduction", methods = ['GET']) def introduction():

response = make_response(jsonify('This is an application that … … … order requests, and provides payment receipts.'), 200)

return response @current_app.route("/company/trademarks", methods = ['GET']) def list_goals():

response = make_response(jsonify(['Eat', 'Live', 'Happy']), 200)

return response


			<st c="6540">When the response data is not serializable,</st> `<st c="6585">jsonify()</st>` <st c="6594">can throw an exception, so it is advisable to enable error handlers.</st> <st c="6664">Now, it is customary to exclude</st> `<st c="6696">make_response</st>` <st c="6709">in returning the response data since</st> `<st c="6747">jsonify()</st>` <st c="6756">can already manage the</st> `<st c="6780">Response</st>` <st c="6788">generation alone for the endpoint function.</st> <st c="6833">Thus, the following versions of the</st> `<st c="6869">index()</st>`<st c="6876">,</st> `<st c="6878">introduction()</st>`<st c="6892">, and</st> `<st c="6898">list_goals()</st>` <st c="6910">endpoint functions</st> <st c="6930">are acceptable:</st>

@current_app.route("/index", methods = ['GET']) def index():

response = jsonify(message='This is an Online Pizza Ordering System.', today=date.today()), 200

return response @current_app.route("/introduction", methods = ['GET']) def introduction():

response = jsonify('This is an application that … … … order requests, and provides payment receipts.'), 200

return response @current_app.route("/company/trademarks", methods = ['GET']) def list_goals():

response = jsonify(['Eat', 'Live', 'Happy']), 200

return response


			<st c="7462">Using the</st> `<st c="7473">@app.route()</st>` <st c="7485">decorator</st> <st c="7495">to bind the URL pattern to the function and define the HTTP request is always valid.</st> <st c="7581">But Flask 3.x had released some decorator shortcuts that can assign one HTTP request per endpoint function, unlike the</st> `<st c="7700">@app.route()</st>`<st c="7712">, which can bind more than one HTTP request.</st> <st c="7757">These shortcuts are</st> <st c="7777">the following:</st>

				*   `<st c="7791">get()</st>`<st c="7797">: This defines an endpoint function that will listen to incoming</st> *<st c="7863">HTTP</st>* <st c="7868">GET requests, such as</st> <st c="7890">retrieving data from the</st> <st c="7915">database servers.</st>
				*   `<st c="7932">post()</st>`<st c="7939">: This</st> <st c="7947">defines an endpoint function to process an</st> *<st c="7990">HTTP POST</st>* <st c="7999">request, such as receiving a body of data for</st> <st c="8046">internal processing.</st>
				*   `<st c="8066">put()</st>`<st c="8072">: This defines an endpoint function to cater to</st> <st c="8121">any</st> *<st c="8125">HTTP PUT</st>* <st c="8133">requests, such as receiving a body of data containing updated details for the</st> <st c="8212">database server.</st>
				*   `<st c="8228">patch()</st>`<st c="8236">: This defines an endpoint to listen to an</st> *<st c="8280">HTTP PATCH</st>* <st c="8290">request</st> <st c="8298">that aims to modify some</st> <st c="8324">backend resources.</st>
				*   `<st c="8342">delete()</st>`<st c="8351">: This defines an</st> *<st c="8370">HTTP DELETE</st>* <st c="8381">endpoint function</st> <st c="8399">that will delete some</st> <st c="8422">server resources.</st>

			<st c="8439">The following</st> <st c="8454">employee-related transactions of our</st> `<st c="8491">ch03</st>` <st c="8495">application are all implemented using the shortcut</st> <st c="8547">routing decorators:</st>

@current_app.post('/employee/add') def add_employee():

emp_json = request.get_json()

repo = EmployeeRepository(db_session)

employee = Employee(**emp_json)

result = repo.insert(employee)

if result:

    content = jsonify(emp_json)

    current_app.logger.info('insert employee record successful')

    return make_response(content, 201)

else:

    raise DuplicateRecordException("insert employee record encountered a problem", status_code=500)

			<st c="8989">The given</st> `<st c="9000">add_employee()</st>` <st c="9014">endpoint</st> <st c="9023">function performs a database INSERT transaction of a record of employee details received from the client.</st> <st c="9130">The decorated</st> `<st c="9144">@current_app.post()</st>` <st c="9163">makes the API function an HTTP POST request method.</st> <st c="9216">On the other hand, the following is an API function that responds to an HTTP GET</st> <st c="9297">client request:</st>

@current_app.get('/employee/list/all') def list_all_employee():

repo = EmployeeRepository(db_session)

records = repo.select_all()

emp_rec = [rec.to_json() for rec in records]

current_app.logger.info('retrieved a list of employees successfully')

return jsonify(emp_rec)

			<st c="9581">The</st> `<st c="9586">list_all_employee()</st>`<st c="9605">, defined by the</st> `<st c="9622">@current_app.get()</st>` <st c="9640">decorator, processes the incoming HTTP GET requests for retrieving a list of employee records from the database server.</st> <st c="9761">For an HTTP PUT transaction, here is an API that updates</st> <st c="9818">employee details:</st>

@current_app.put('/employee/update') def update_employee():

emp_json = request.get_json()

repo = EmployeeRepository(db_session)

result = repo.update(emp_json['empid'], emp_json)

if result:

    content = jsonify(emp_json)

    current_app.logger.info('update employee record successful')

    return make_response(content, 201)

else:

    raise NoRecordException("update employee record encountered a problem", status_code=500)

			<st c="10243">The given API endpoint requires an</st> `<st c="10279">empid</st>` <st c="10284">path variable, which will serve as the key to search for the employee record that needs updating.</st> <st c="10383">Since this is an HTTP PUT request, the transaction requires all the new employee details to be replaced by their new values.</st> <st c="10508">But the following is another version of the update transaction that does not need a complete</st> <st c="10600">employee</st> <st c="10610">detail update:</st>

@current_app.patch('/employee/update/string:empid') def update_employee_name(empid:str):

emp_json = request.get_json()

repo = EmployeeRepository(db_session)

result = repo.update(empid, emp_json)

if result:

    content = jsonify(emp_json)

    current_app.logger.info('update employee firstname, middlename, and lastname successful')

    return make_response(content, 201)

else:

    raise NoRecordException("update employee firstname, middlename, and lastname encountered a problem", status_code=500)

			`<st c="11111">update_employee()</st>`<st c="11129">, decorated by</st> `<st c="11144">@current_app.patch()</st>`<st c="11164">, only updates the first name, middle name, and last name of the employee identified by the given employee ID using its path variable</st> `<st c="11298">empid</st>`<st c="11303">. Now, the following API function deletes an employee record based on the</st> `<st c="11377">empid</st>` <st c="11382">path variable:</st>

@current_app.delete('/employee/delete/string:empid') def delete_employee(empid:str):

repo = EmployeeRepository(db_session)

result = repo.delete(empid)

if result:

    content = jsonify(message=f'employee {empid} deleted')

    current_app.logger.info('delete employee record successful')

    return make_response(content, 201)

else:

    raise NoRecordException("delete employee record encountered a problem", status_code=500)

			`<st c="11809">delete_employee()</st>`<st c="11827">, decorated by</st> `<st c="11842">@current_app.delete()</st>`<st c="11863">, is an HTTP</st> `<st c="11876">DELETE</st>` <st c="11882">request method</st> <st c="11898">with the path variable</st> `<st c="11921">empid</st>`<st c="11926">, used for searching employee records</st> <st c="11964">for deletion.</st>
			<st c="11977">These shortcuts of binding HTTP requests to their respective request handler methods are appropriate for implementing REST services because of their definite, simple, and straightforward one-route approach to managing incoming requests and serializing the</st> <st c="12234">required responses.</st>
			<st c="12253">Let us now explore how Flask API captures the incoming body of data for POST, PUT, and PATCH requests and, aside from</st> `<st c="12372">make_response()</st>`<st c="12387">, what other ways the API can generate</st> <st c="12426">JSON responses.</st>
			<st c="12441">Managing requests and responses</st>
			<st c="12473">Unlike in other frameworks, it is</st> <st c="12508">easy to capture the request body of the incoming POST, PUT, and</st> <st c="12571">PATCH request in Flask, which is through the</st> `<st c="12617">get_json()</st>` <st c="12627">method from the</st> `<st c="12644">request</st>` <st c="12651">proxy object.</st> <st c="12666">This utility method receives the incoming JSON data, parses the data using</st> `<st c="12741">json.loads()</st>`<st c="12753">, and returns the data in a Python dictionary format.</st> <st c="12807">As seen in the following</st> `<st c="12832">add_customer()</st>` <st c="12846">API, the value of</st> `<st c="12865">get_json()</st>` <st c="12875">is converted into a</st> `<st c="12896">kwargs</st>` <st c="12902">argument by Python’s</st> `<st c="12924">**</st>` <st c="12926">operator before passing the request data to the model class’s constructor, an indication that the captured request data is a</st> `<st c="13052">dict</st>` <st c="13056">convertible</st> <st c="13069">into</st> `<st c="13074">kwargs</st>`<st c="13080">:</st>

@current_app.post('/customer/add')

def add_customer(): cust_json = request.get_json() repo = CustomerRepository(db_session) customer = Customer(**cust_json) result = repo.insert(customer)

    if result:

        content = jsonify(cust_json)

        current_app.logger.info('insert customer record successful')

        return make_response(content, 201)

    else:

        content = jsonify(message="insert customer record encountered a problem")

        return make_response(content, 500)

			<st c="13521">Another</st> <st c="13529">common approach is to use the</st> `<st c="13560">request.json</st>` <st c="13572">property to</st> <st c="13585">capture the incoming message body, which is raw and with the mime-type</st> `<st c="13656">application/json</st>`<st c="13672">. The following endpoint function captures the incoming request through</st> `<st c="13744">request.json</st>` <st c="13756">and stores the data in the database as</st> `<st c="13796">category</st>` <st c="13804">information:</st>

@current_app.post('/category/add')

def add_category():

if <st c="13876">request.is_json</st>: <st c="13894">cat_json = request.json</st> cat = Category(<st c="13933">**cat_json</st>)

    repo = CategoryRepository(db_session)

    result = repo.insert(cat)

    … … … … … …

else:

    abort(500)

			<st c="14039">Unlike</st> `<st c="14047">request.get_json()</st>`<st c="14065">, which uses serialization, validation, and other utilities to transform and return incoming data to JSON, the</st> `<st c="14176">request.json</st>` <st c="14188">property has no validation support other than raising an</st> `<st c="14246">HTTP status 400</st>` <st c="14261">or</st> `<st c="14265">Bad Data</st>` <st c="14273">error if the data is not JSON serializable.</st> <st c="14318">The</st> `<st c="14322">request.get_json()</st>` <st c="14340">returns</st> `<st c="14349">None</st>` <st c="14353">if the request data is not parsable.</st> <st c="14391">That is why it is best to pair the</st> `<st c="14426">request.is_json</st>` <st c="14441">Boolean property with</st> `<st c="14464">request.json</st>` <st c="14476">to verify the incoming request and filter the non-JSON</st> <st c="14532">message body to avoid</st> `<st c="14554">HTTP Status Code 500</st>`<st c="14574">. Another</st> <st c="14584">option is to check if the</st> `<st c="14610">Content-Type</st>` <st c="14622">request header of the incoming request is</st> `<st c="14665">application/json</st>`<st c="14681">, as showcased by the following</st> <st c="14713">API function:</st>

@current_app.post('/nonpizza/add')

def add_nonpizza(): content_type = request.headers.get('Content-Type')if content_type == 'application/json': nonpizza_json = request.json nonpizza = NonPizza(**nonpizza_json)

    … … … … … …

else:

    abort(500)

			<st c="14967">This</st> `<st c="14973">add_nonpizza()</st>` <st c="14987">function inserts a new record for the non-pizza menu options for the application, and it uses</st> `<st c="15082">request.json</st>` <st c="15094">to access the JSON-formatted input from the client.</st> <st c="15147">Both</st> `<st c="15152">request.json</st>` <st c="15164">and</st> `<st c="15169">request.get_json()</st>` <st c="15187">yield a dictionary object that makes the instantiation of model objects in the</st> `<st c="15267">add_category()</st>` <st c="15281">and</st> `<st c="15286">add_non_pizza()</st>` <st c="15301">API functions easier because</st> `<st c="15331">kwargs</st>` <st c="15337">transformation from these JSON data</st> <st c="15374">is straightforward.</st>
			<st c="15393">On the other hand, validation of incoming requests using</st> `<st c="15451">request.is_json</st>` <st c="15466">and</st> `<st c="15471">Content-Type</st>` <st c="15483">headers is also applicable to the POST, PUT, and DELETE message body retrieval through</st> `<st c="15571">request.get_json()</st>`<st c="15589">. Now, another approach to accessing the message body that requires</st> `<st c="15657">request.is_json</st>` <st c="15672">validation is through</st> `<st c="15695">request.data</st>`<st c="15707">. This property captures POST, PUT, or PATCH message bodies regardless of any</st> `<st c="15785">Content-Type</st>`<st c="15797">, thus requiring a thorough validation mechanism.</st> <st c="15847">The following API function captures user credentials through</st> `<st c="15908">request.data</st>` <st c="15920">and inserts the</st> <st c="15936">login details</st> <st c="15950">in</st> <st c="15954">the database:</st>

@current_app.route('/login/add', methods = ['POST'])

def add_login(): if request.is_json:login_json = loads(request.data) login = Login(**login_json)

    … … … … … …

else:

    abort(500)

			<st c="16147">It is always feasible to use</st> `<st c="16177">request.data</st>` <st c="16189">for HTTP POST transactions, such as in the given</st> `<st c="16239">add_login()</st>` <st c="16250">function, but the API needs to parse and serialize the</st> `<st c="16306">request.data</st>` <st c="16318">using Flask’s built-in</st> `<st c="16342">loads()</st>` <st c="16349">decoder from the</st> `<st c="16367">flask.json</st>` <st c="16377">module extension because the request data is not yet JSON-formatted.</st> <st c="16447">Additionally, the process needs tight data type validation for each JSON object in the captured request data before using it in</st> <st c="16575">the transactions.</st>
			<st c="16592">Aside from these variations of managing the incoming requests, Flask also has approaches to dealing with outgoing JSON responses.</st> <st c="16723">Instead of</st> `<st c="16734">jsonify()</st>`<st c="16743">, another way to render a JSON response is by instantiating and returning</st> `<st c="16817">Response</st>` <st c="16825">to the client.</st> <st c="16841">The following is a</st> `<st c="16860">list_login()</st>` <st c="16872">endpoint function that retrieves a list of</st> `<st c="16916">Login</st>` <st c="16921">records from the database using the</st> `<st c="16958">Response</st>` <st c="16966">class:</st>

@current_app.route('/login/list/all', methods = ['GET'])

def list_all_login():

repo = LoginRepository(db_session)

records = repo.select_all()

login_rec = [rec.to_json() for rec in records]

current_app.logger.info('retrieved a list of login successfully') <st c="17229">resp = Response(response = dumps(login_rec),</st> <st c="17273">status=200, mimetype="application/json" )</st> return resp

			<st c="17327">When</st> <st c="17333">using</st> `<st c="17339">Response</st>`<st c="17347">, an encoder such as</st> `<st c="17368">dumps()</st>` <st c="17375">of the</st> `<st c="17383">flask.json</st>` <st c="17393">module</st> <st c="17400">can be used to create a JSONable object from an object, list, or dictionary.</st> <st c="17478">And the</st> `<st c="17486">mime-type</st>` <st c="17495">should always be</st> `<st c="17513">application/json</st>` <st c="17529">to force the object to</st> <st c="17553">become JSON.</st>
			<st c="17565">Let us focus now on Flask’s built-in support for JSON types and the serialization and de-serialization utilities it has to process</st> <st c="17697">JSON objects.</st>
			<st c="17710">Utilizing response encoders and decoders</st>
			<st c="17751">Flask framework</st> <st c="17768">supports the built Python</st> `<st c="17794">json</st>` <st c="17798">module by default.</st> <st c="17818">The built-in encoders,</st> `<st c="17841">dumps()</st>`<st c="17848">, and</st> `<st c="17854">loads()</st>`<st c="17861">, are found in the</st> `<st c="17880">flask.json</st>` <st c="17890">module.</st> <st c="17899">In the</st> *<st c="17906">Managing the requests and responses</st>* <st c="17941">section, the</st> `<st c="17955">add_login()</st>` <st c="17966">endpoint function uses the</st> `<st c="17994">flask.json.loads()</st>` <st c="18012">to de-serialize and transform the</st> `<st c="18047">request.data</st>` <st c="18059">into a JSONable dictionary.</st> <st c="18088">Meanwhile, the</st> `<st c="18103">flask.json.dumps()</st>` <st c="18121">provided the</st> `<st c="18135">Response</st>` <st c="18143">class with a JSONable object for some JSON response output, as previously highlighted in the</st> `<st c="18237">list_all_login()</st>` <st c="18253">endpoint.</st>
			<st c="18263">But any application can override these default encoding and decoding processes to solve some custom requirements.</st> <st c="18378">Customizing an appropriate JSON provider by sub-classing Flask’s</st> `<st c="18443">JSONProvider</st>`<st c="18455">, found in the</st> `<st c="18470">flask.json.provider</st>`<st c="18489">, can allow the overriding of these JSON processes.</st> <st c="18541">The following is a custom implementation of a</st> `<st c="18587">JSONProvider</st>` <st c="18599">with some modifications to the</st> `<st c="18631">dumps()</st>` <st c="18638">and</st> `<st c="18643">loads()</st>` <st c="18650">algorithms:</st>

from flask.json.provider import JSONProvider

import json class ImprovedJsonProvider(JSONProvider):

def __init__(self, *args, **kwargs):

    self.options = kwargs

    super().__init__(*args, **kwargs) <st c="18857">def default(self, o):</st>

if isinstance(o, date):

返回 o.strftime("%m/%d/%Y")

elif isinstance(o, datetime):

返回 o.strftime("%m/%d/%Y, %H:%M:%S")

返回 super().default(self, o)

def dumps(self, obj, **kwargs):

kwargs.setdefault("default", self.default)kwargs.setdefault("ensure_ascii", True)kwargs.setdefault("sort_keys", True)

返回 json.dumps(obj, **kwargs) def loads(self, s: str | bytes, **kwargs): s_dict:dict = json.loads(s.decode('utf-8'))s_sanitized = dict((k, v) for k, v in s_dict.items() if v)s_str = json.dumps(s_sanitized) return json.loads(main.py 模块并将 appjson 属性设置为自定义提供者的实例,其构造参数为 app 对象。以下是在线披萨订购原型中自定义 ImprovedJsonprovider 的设置:

 app = create_app('../config_dev.toml') <st c="19835">JSONProvider</st> requires overriding its <st c="19872">dump()</st> and <st c="19883">loads()</st> methods. Additional custom features, such as formatting encoded dates, filtering empty JSON properties, and validating key and value types, can be helpful to custom implementation. For the serializer and de-serializer, the preferred JSON utility in customizing the <st c="20156">JSONProvider</st> is Python’s built-in <st c="20190">json</st> module.
			<st c="20202">The</st> `<st c="20207">ImprovedJsonprovider</st>` <st c="20227">class includes a custom</st> `<st c="20252">default()</st>` <st c="20261">method that validates the property value types during encoding.</st> <st c="20326">It coerces the</st> `<st c="20341">date</st>` <st c="20345">or</st> `<st c="20349">datetime</st>` <st c="20357">objects to have a defined format.</st> <st c="20392">For the application to utilize this method during encoding, the overridden</st> `<st c="20467">dumps()</st>` <st c="20474">must pass this</st> `<st c="20490">default()</st>` <st c="20499">to Python’s</st> `<st c="20512">json.dumps()</st>` <st c="20524">as the</st> `<st c="20532">kwargs["default"]</st>` <st c="20549">value.</st> <st c="20557">In addition, there are also other keyword arguments that can smoothen the encoding process, such as</st> `<st c="20657">ensure_scii</st>`<st c="20668">, which enables the replacement of non-ASCII characters with whitespaces, and</st> `<st c="20746">sort_keys</st>`<st c="20755">, which sorts the keys of the resulting dictionary in</st> <st c="20809">ascending order.</st>
			<st c="20825">On the other</st> <st c="20839">hand,</st> `<st c="20845">ImprovedJsonprovider</st>` <st c="20865">‘s overridden</st> `<st c="20880">loads()</st>` <st c="20887">method initially converts the string request data into a dictionary using Python’s</st> `<st c="20971">json.loads()</st>` <st c="20983">before removing all the key-value pairs with empty values.</st> <st c="21043">Afterward,</st> `<st c="21054">json.dumps()</st>` <st c="21066">serializes the sanitized dictionary back to its string type before submitting it for JSON de-serialization.</st> <st c="21175">Thus, running the</st> `<st c="21193">add_category()</st>` <st c="21207">endpoint with a message body that has an empty description value will lead to</st> *<st c="21286">HTTP Status Code 500</st>*<st c="21306">, as shown in</st> *<st c="21320">Figure 3</st>**<st c="21328">.3</st>*<st c="21330">:</st>
			![Figure 3.3 – Applying the overridden flask.json.loads() decoder](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_03_3.jpg)

			<st c="21744">Figure 3.3 – Applying the overridden flask.json.loads() decoder</st>
			<st c="21807">The removal of the</st> `<st c="21827">description</st>` <st c="21838">property by the custom</st> `<st c="21862">loads()</st>` <st c="21869">caused the constructor error flagged in the</st> `<st c="21914">cURL</st>` <st c="21918">command execution in</st> *<st c="21940">Figure 3</st>**<st c="21948">.3</st>*<st c="21950">.</st>
			<st c="21951">Now, the following are</st> <st c="21975">the deprecated features that will not work anymore in Flask 3.x</st> <st c="22039">and onwards:</st>

				*   `<st c="22051">JSONEncoder</st>` <st c="22063">and</st> `<st c="22068">JSONDecoder</st>` <st c="22079">APIs customize</st> `<st c="22095">flask.json.dumps()</st>` <st c="22113">and</st> `<st c="22118">flask.json.loads()</st>`<st c="22136">, respectively.</st>
				*   `<st c="22151">json_encoder</st>` <st c="22164">and</st> `<st c="22169">json_decoder</st>` <st c="22181">attributes set up</st> `<st c="22200">JSONEncoder</st>` <st c="22211">and</st> `<st c="22216">JSONDecoder</st>`<st c="22227">, respectively.</st>

			<st c="22242">Also, the following setup applied in Python’s</st> `<st c="22289">json</st>` <st c="22293">encoder and decoder during customization will not work here in the</st> <st c="22361">Flask framework:</st>

				*   <st c="22377">Specifying the</st> `<st c="22393">flask.json.loads()</st>` <st c="22411">encoder directly with</st> <st c="22434">the custom.</st>
				*   <st c="22445">Specifying the</st> `<st c="22461">flask.json.dumps()</st>` <st c="22479">decoder directly with the custom implementation class using the non-existent</st> `<st c="22557">cls</st>` <st c="22560">kwarg.</st>

			<st c="22567">Since</st> `<st c="22574">JSONEcoder</st>` <st c="22584">and</st> `<st c="22589">JSONDecoder</st>` <st c="22600">will be obsolete soon, there will be no other means to customize these JSON utilities but through</st> <st c="22699">the</st> `<st c="22703">JSONProvider</st>`<st c="22715">.</st>
			<st c="22716">However, there are instances where the incoming message body or the outgoing JSON responses are complex and huge, which cannot be handled optimally by the built-in JSON provider.</st> <st c="22896">In this case, Flask allows replacing the existing provider with a fast, accurate, and flexible provider, such as</st> `<st c="23009">ujson</st>` <st c="23014">and</st> `<st c="23019">orjson</st>`<st c="23025">. The following class is a sub-class of the</st> `<st c="23069">JSONProvider</st>` <st c="23081">that uses the</st> `<st c="23096">orjson</st>` <st c="23102">encoder</st> <st c="23111">and decoder.</st>

从 flask.json.provider 模块导入 JSONProvider 导入 orjson 类 OrjsonJsonProvider(JSONProvider):

def __init__(self, *args, **kwargs):

    self.options = kwargs

    super().__init__(*args, **kwargs)

def dumps(self, obj, **kwargs): <st c="23348">返回 orjson.dumps(obj,</st> <st c="23372">option=orjson.OPT_NON_STR_KEYS).decode('utf-8')</st> def loads(self, s, **kwargs): <st c="23478">OrjsonJsonProvider</st> 实现了一个自定义 JSON 提供者,它使用 <st c="23541">orjson</st>,这是一个支持多种类型(如 <st c="23620">datetime</st>,<st c="23630">dataclass</st>,<st c="23641">numpy</st> 类型,以及 <st c="23658">通用唯一</st> <st c="23677">标识符</st> (<st c="23690">UUID</st>))的最快 JSON 库之一。

        <st c="23697">另一个可以进一步改进我们的 RESTful 应用程序验证和处理传入请求体和传出响应的必要附加组件是</st> *<st c="23850">路由过滤器</st>*<st c="23863">。</st>

        <st c="23864">过滤 API 请求和响应</st>

        <st c="23901">在</st> *<st c="23905">第一章</st>*<st c="23914">中,由于自定义装饰器</st> `<st c="23986">@connect_db</st>`<st c="24021">,每个视图函数的 CRUD 操作成为可能,无需 ORM。该装饰器负责视图函数每次执行时的数据库连接和关闭。</st> <st c="24144">与任何 Python 装饰器一样,</st> `<st c="24178">@connect_db</st>` <st c="24189">在视图函数开始接收客户端请求之前执行,并在视图生成响应之后执行。</st>

        <st c="24327">另一方面,</st> *<st c="24347">第二章</st>* <st c="24356">介绍了使用</st> `<st c="24379">@before_request</st>` <st c="24394">和</st> `<st c="24399">@after_request</st>` <st c="24413">装饰器来管理视图函数的应用程序上下文。</st> <st c="24484">我们的应用程序使用它们来访问</st> `<st c="24533">db</st>` <st c="24535">对象以实现 SQLAlchemy 的数据库连接,实施用户身份验证,并执行</st> `<st c="24623">软件日志记录</st>`。</st>

        <st c="24640">使用装饰器来管理视图或 API 函数的请求和响应被称为路由过滤。</st> <st c="24749">以下是实现 Flask 的</st> `<st c="24794">before_request</st>` <st c="24808">和</st> `<st c="24813">after_request</st>` <st c="24826">方法,这些方法由</st> `<st c="24847">ch03</st>` <st c="24851">应用程序用于过滤</st> <st c="24878">请求-响应握手:</st>
<st c="24905">from flask import request, abort, Response</st>
<st c="24948">@app.before_request</st> def before_request_func():
    api_method = request.method
    if api_method in ['POST', 'PUT', 'PATCH']:
        if request.json == '' or request.json == None:
            abort(500, description="request body is empty")
    api_endpoint_func = request.endpoint
    api_path = request.path
    app.logger.info(f'accessing URL endpoint: {api_path}, function name: {api_endpoint_func} ') <st c="25315">@app.after_request</st> def after_request_func(response:Response):
    api_endpoint_func = request.endpoint
    api_path = request.path
    resp_allow_origin = response.access_control_allow_origin
    app.logger.info(f"access_control_allow_origin header: {resp_allow_origin}")
    app.logger.info(f'exiting URL endpoint: {api_path}, function name: {api_endpoint_func} ')
    return response
        <st c="25676">在这个</st> <st c="25685">应用程序中,</st> `<st c="25698">before_request</st>` <st c="25712">检查传入的 HTTP POST、PUT 或 PATCH 事务的请求体是否为空或</st> `<st c="25752">None</st>`<st c="25805">。否则,它将引发一个</st> `<st c="25835">HTTP 状态码 500</st>` <st c="25855">,并带有错误消息</st> `<st c="25879">请求体为空</st>`<st c="25900">。它还执行日志记录以供审计目的。</st> <st c="25947">另一方面,</st> `<st c="25951">after_request</st>` <st c="25964">方法,另一方面,记录 API 的基本详细信息以供跟踪目的,并检查</st> `<st c="26062">access_control_allow_origin</st>` <st c="26089">响应头。</st> <st c="26107">强制参数 response 允许我们在软件需求给出时修改响应头。</st> <st c="26236">此外,这也是创建 cookie 和执行最后数据库提交的最佳位置,因为这是在</st> `<st c="26394">after_request</st>` <st c="26407">方法将其发送到</st> `<st c="26427">客户端</st>`之前的最后时刻访问响应对象。</st>

        <st c="26438">与 FastAPI 一样,Flask 框架也有其创建类似中间件组件的版本,这些组件可以作为全局路由过滤器。</st> <st c="26569">我们的应用程序有以下实现,它作为 API 端点的中间件:</st>
<st c="26669">import werkzeug.wrappers</st>
<st c="26694">import werkzeug.wsgi</st> class AppMiddleware:
    def __init__(self, app):
        self.app = app
    def __call__(self, environ, start_response):
        request = <st c="26832">werkzeug.wrappers.Request</st>(environ)
        api_path = request.url
        app.logger.info(f'accessing URL endpoint: {api_path} ')
        iterator:<st c="26956">werkzeug.wsgi.ClosingIterator</st> = self.app(environ, start_response)
        app.logger.info(f'exiting URL …: {api_path} ') <st c="27145">AppleMiddleware</st>, the involved <st c="27175">Request</st> API class is from the <st c="27205">werkzeug</st> module or the core platform itself since the implementation is server-level. Instantiating the <st c="27309">werkzeug.wrappers.Request</st> with the <st c="27344">environ</st> parameter as its constructor argument will give us access to the details of the incoming request of the API endpoint. Unfortunately, there is no direct way of accessing the response object within the filter class. Some implementations require the creation of hook methods by registering custom decorators to Flask through the custom middleware, and others use external modules to implement a middleware that acts like a URL dispatcher. Now, our custom middleware must be a callable class type, so all the implementations must be in its overridden <st c="27899">__call__()</st> method.
			<st c="27917">Moreover, we can</st> <st c="27935">also associate</st> `<st c="27950">Blueprint</st>` <st c="27959">modules with their respective</st> <st c="27990">custom before and after filter methods, if required.</st> <st c="28043">The following</st> `<st c="28057">app</st>` <st c="28060">configuration assigns filter methods to the</st> `<st c="28105">order_cient_bp</st>` <st c="28119">and</st> `<st c="28124">pizza_client_bp</st>` `<st c="28139">Blueprint</st>`<st c="28149">s of the</st> `<st c="28159">ch03-client</st>` <st c="28170">application:</st>

app.before_request_funcs = {

'orders_client_bp': [before_check_api_server],

'pizza_client_bp': [before_log_pizza_bp]

}

app.after_request_funcs = {

'orders_client_bp': [after_check_api_server],

'pizza_client_bp': [after_log_pizza_bp]

}


			<st c="28420">Both</st> `<st c="28426">before_request_funcs</st>` <st c="28446">and</st> `<st c="28451">after_request_funcs</st>` <st c="28470">contain the concerned</st> `<st c="28493">Blueprint</st>` <st c="28502">names and their corresponding lists of implemented filter</st> <st c="28561">method names.</st>
			<st c="28574">Can we also apply the same exception-handling directives used in the web-based applications of</st> *<st c="28670">Chapters 1</st>* <st c="28680">and</st> *<st c="28685">2</st>*<st c="28686">? Let us find out in the</st> <st c="28711">following discussion.</st>
			<st c="28732">Handling exceptions</st>
			<st c="28752">In RESTful applications, Flask</st> <st c="28784">allows the endpoint function to trigger error handlers that return error messages in JSON format.</st> <st c="28882">The following snippets are the error handlers of our</st> `<st c="28935">ch03</st>` <st c="28939">application:</st>

@app.errorhandler(404)

def not_found(e): return jsonify(error=str(e)), 404 @app.errorhandler(400)

def bad_request(e): return jsonify(error=str(e)), 400 def server_error(e):

print(e) <st c="29135">return jsonify(error=str(e)), 500</st> app.register_error_handler(500, server_error)

			<st c="29214">Error handlers can also return the JSON response through the</st> `<st c="29276">jsonify()</st>`<st c="29285">,</st> `<st c="29287">make_response()</st>`<st c="29302">, or</st> `<st c="29307">Response</st>` <st c="29315">class.</st> <st c="29323">As shown in the given error handlers, the implementation is the same with the web-based error handlers except for the</st> `<st c="29441">jsonify()</st>` <st c="29450">method, which serializes the captured error message to the JSON type instead of</st> <st c="29531">using</st> `<st c="29537">render_template()</st>`<st c="29554">.</st>
			<st c="29555">Custom exception</st> <st c="29573">classes must include both the</st> *<st c="29603">HTTP Status Code</st>* <st c="29619">and error message in the JSON message.</st> <st c="29659">The customization must include a</st> `<st c="29692">to_dict()</st>` <st c="29701">method that will convert the payload and other external parameters to a dictionary object for the</st> `<st c="29800">jsonify()</st>` <st c="29809">to serialize.</st> <st c="29824">The following is a custom exception class raised by our</st> `<st c="29880">INSERT</st>` <st c="29886">repository transactions and</st> <st c="29915">endpoint functions:</st>

class DuplicateRecordException(HTTPException):

status_code = 500

def __init__(self, message, status_code=None, payload=None):

    super().__init__()

    self.message = message

    if status_code is not None:

        self.status_code = status_code

    self.payload = payload <st c="30292">DuplicateRecordException</st>,以下错误处理程序将访问其<st c="30362">to_dict()</st>实例方法并通过<st c="30419">jsonify()</st>将其转换为 JSON。它还将访问响应的<st c="30454">status_code</st>:
 @app.errorhandler(<st c="30502">DuplicateRecordException</st>)
def insert_record_exception(e):
    return <st c="30686">Database</st> <st c="30694">RecordException</st>, triggering this <st c="30728">insert_record_exception()</st> handler. But for Python-related exceptions, the following error handler will also render the built-in exception messages in JSON format:

@app.errorhandler(Exception)

def handle_built_exception(e):

if isinstance(e, HTTPException):

    return e <st c="31032">handle_built_exception()</st>处理程序将始终返回一个 JSON 格式的错误消息,并为其他自定义处理程序抛出 Werkzeug 特定的异常。但对于抛出的 Python 特定异常,<st c="31238">handle_built_exception()</st>将直接渲染 JSON 错误消息。

        `<st c="31307">在构建我们的 RESTful 应用程序所需组件完成后,是时候使用</st>` `<st c="31434">客户端应用程序</st>` `<st c="31434">消耗这些 API 端点了。</st>`

        <st c="31453">消耗 API 端点</st>

        `<st c="31477">我们的</st>` `<st c="31482">ch03-client</st>` `<st c="31493">项目是一个</st>` `<st c="31505">基于 Web 的 Flask 应用程序,它利用了在</st> `<st c="31582">ch03</st>` `<st c="31586">应用程序中创建的 API 端点。</st>` `<st c="31600">到目前为止,使用</st>` `<st c="31670">requests</st>` `<st c="31678">扩展模块是消耗 Flask API 端点最简单的方法。</st>` `<st c="31697">要安装</st>` `<st c="31712">requests</st>` `<st c="31720">库,请运行以下命令:</st>
 pip install requests
        <st c="31777">此</st> `<st c="31783">requests</st>` `<st c="31791">模块有一个</st> `<st c="31805">get()</st>` `<st c="31810">辅助方法,用于向 URL 发送 HTTP GET 请求以检索一些服务器资源。</st>` `<st c="31897">以下来自</st> `<st c="31934">ch03-client</st>` `<st c="31945">项目` `<st c="31934">的视图函数从</st> `<st c="32007">ch03</st>` `<st c="32011">应用程序中检索客户和员工列表,并将它们作为上下文数据传递给</st> `<st c="32063">add_order.html</st>` `<st c="32077">模板:</st>`
 @current_app.route('/client/order/add', methods = ['GET', 'POST'])
def add_order():
    if request.method == 'POST':
        order_dict = request.form.to_dict(flat=True) <st c="32246">order_add_api = "http://localhost:5000/order/add"</st><st c="32295">response: requests.Response =</st> <st c="32325">requests.post(order_add_api, json=order_dict)</st><st c="32371">customers_list_api =</st> <st c="32392">"http://localhost:5000/customer/list/all"</st><st c="32434">employees_list_api =</st> <st c="32455">"http://localhost:5000/employee/list/all"</st><st c="32497">resp_customers:requests.Response = requests.get(customers_list_api)</st><st c="32565">resp_employees:requests.Response = requests.get(employees_list_api)</st> return render_template('add_order.html', customers=<st c="32747">get()</st> method returns a <st c="32770">requests.Response</st> object that contains essential details, such as <st c="32836">content</st>, <st c="32845">url</st>, <st c="32850">status_code</st>, <st c="32863">json()</st>, <st c="32871">encoding</st>, and other headers from the API’s server. Our <st c="32926">add_order()</st> calls the <st c="32948">json()</st> for each GET response to serialize the result in JSON format.
			<st c="33016">For the HTTP POST transaction, the</st> `<st c="33052">request</st>` <st c="33059">module has a</st> `<st c="33073">post()</st>` <st c="33079">method to send an HTTP POST request to</st> `<st c="33119">http://localhost:5000/order/add</st>` <st c="33151">API.</st> <st c="33156">For a successful POST request handshake, the</st> `<st c="33201">post()</st>` <st c="33207">requires the URL of the API service and the record or object as the request body in</st> <st c="33292">dictionary format.</st>
			<st c="33310">Aside from the dictionary type, the</st> `<st c="33347">post()</st>` <st c="33353">method can also allow the submission of a list of</st> *<st c="33404">tuples</st>*<st c="33410">,</st> *<st c="33412">bytes</st>*<st c="33417">, or</st> *<st c="33422">file entity types</st>*<st c="33439">. It also has various parameter options such as</st> `<st c="33487">data</st>`<st c="33491">,</st> `<st c="33493">json</st>`<st c="33497">, or</st> `<st c="33502">files</st>` <st c="33507">that can accept the appropriate</st> <st c="33539">request</st> <st c="33548">body types.</st>
			<st c="33559">Now, other than</st> `<st c="33576">get()</st>` <st c="33581">and</st> `<st c="33586">post()</st>` <st c="33592">methods, the</st> `<st c="33606">requests</st>` <st c="33614">library has other helper methods that can also send other HTTP requests to the server, such as the</st> `<st c="33714">put()</st>` <st c="33719">that calls the PUT API service,</st> `<st c="33752">delete()</st>` <st c="33760">that calls DELETE API service, and</st> `<st c="33796">patch()</st>` <st c="33803">for the PATCH</st> <st c="33818">API service.</st>
			<st c="33830">Summary</st>
			<st c="33838">This chapter has proven to us that some components apply to both API-based and web-based applications, but there are specific components that fit better in API transactions than in web-based ones.</st> <st c="34036">It provided details on Flask’s JSON de-serialization applied to request bodies and serialization of outgoing objects to be part of the API responses.</st> <st c="34186">The many options of capturing the request body through</st> `<st c="34241">request.json</st>`<st c="34253">,</st> `<st c="34255">request.data</st>`<st c="34267">, and</st> `<st c="34273">request.get_json()</st>` <st c="34291">and generating responses through its</st> `<st c="34329">jsonify()</st>` <st c="34338">or</st> `<st c="34342">make_response()</st>` <st c="34357">and</st> `<st c="34362">Response</st>` <st c="34370">class with</st> `<st c="34382">application/json</st>` <st c="34398">as a mime-type show Flask’s flexibility as</st> <st c="34442">a framework.</st>
			<st c="34454">The chapter also showcased Flask’s ability to adapt to different third-party JSON providers through sub-classing its</st> `<st c="34572">JSONProvider</st>` <st c="34584">class.</st> <st c="34592">Moreover, the many options for providing our API endpoints with route filtering mechanisms also show that the platform can manage the application’s incoming requests and outgoing responses like any good framework.</st> <st c="34806">Regarding error handling mechanisms, the framework can provide error handlers for web-based applications that render templates and those that send JSON responses for</st> <st c="34972">RESTful applications.</st>
			<st c="34993">When consuming the API endpoints, this chapter exhibited that Flask could support typical Python REST client modules, such as</st> `<st c="35120">requests</st>`<st c="35128">, without any</st> <st c="35142">additional workaround.</st>
			<st c="35164">So, we have seen that Flask can support building web-based and API-based applications even though it is lightweight and</st> <st c="35285">a microframework.</st>
			<st c="35302">The next chapter will discuss simplifying and organizing Flask implementations using popular third-party Flask</st> <st c="35414">module extensions.</st>





第五章:利用 Flask 扩展

Flask 因其扩展而流行,这些扩展是可安装的外部或第三方模块或插件,它们增加了支持并甚至增强了可能看起来重复创建的一些内置功能,例如表单处理、会话处理、认证过程,以及缓存。

将 Flask 扩展应用于项目开发可以节省时间和精力,与重新创建相同功能相比。此外,这些模块可以与其他必要的 Python 和 Flask 模块具有相互依赖性,而无需太多配置,这对于向基线项目添加新功能来说很方便。然而,尽管有积极的因素,但安装 Flask 应用的扩展也有一些副作用,例如与某些已安装模块发生冲突以及与当前 Flask 版本存在版本问题,这导致我们必须降级某些 Flask 扩展或 Flask 版本本身。版本冲突、弃用和非支持是利用 Flask 扩展时的核心关注点;因此,在平台上安装每个 Flask 扩展之前,建议阅读每个 Flask 扩展的文档。

本章将展示与第一章到第三章中创建的相同项目组件,包括网页表单、REST 服务、后端数据库、网页会话和外观,但使用它们各自的 Flask 扩展模块。此外,本章还将向您展示如何应用缓存并将邮件功能集成到应用程序中。

本章将涵盖以下主题:

  • 使用 Flask-Migrate 应用数据库迁移

  • 使用 Bootstrap-Flask 设计 UI

  • 使用 Flask-WTF 构建 Flask 表单

  • 使用 Flask-RESTful 构建 RESTful 服务

  • 使用 Flask-Session 实现会话处理

  • 使用 Flask-Caching 应用缓存

  • 使用 Flask-Mail 添加邮件功能

技术要求

本章将突出介绍两个用于 *在线投诉管理系统 的原型,该系统利用了不同的流行 Flask 3.0 扩展。 这些扩展将构建投诉、管理、登录和报告模块。 <st c="2114">ch04-web</st> 项目将包括基于表单的一侧,而 <st c="2178">ch04-api</st> 项目包含 RESTful 服务,以适应各种投诉细节。 这两个应用程序都将利用 <st c="2295">Blueprints</st> 来组织它们的目录结构,并使用 SQLAlchemy 与它们的 PostgreSQL 数据库执行 CRUD 事务。 所有这些项目都已上传到 https://github.com/PacktPublishing/Mastering-Flask-Web-Development/tree/main/ch04

应用数据库迁移

在构建应用程序时,要使用的最重要的第三方 Flask 模块是用于管理数据模型层的模块,那就是 Flask-Migrate 扩展。 尽管有时使用 Alembic 来定制数据库迁移是合适的,但 Flask-Migrate 提供了更少的编码和更快的配置设置。

重要注意事项

Alembic 是一个轻量级且快速的 SQLAlchemy 数据库迁移工具,可以定制以支持各种 数据库后端。

数据库迁移 是从 Flask 模型类推导和生成数据库架构的一种方式,并允许在整个应用程序的生命周期中监控和审计这些架构中的更改,例如添加和删除表列、修改表约束以及重命名列,而不会破坏当前数据。 所有这些机制都由 Flask-Migrate 管理。

现在,让我们了解如何使用 *Flask-Migrate 来设置我们应用程序的数据库后端,而不是手动创建 表架构。

使用 Flask-Migrate 应用数据库迁移

首先,由于我们的应用程序将使用 *SQLAlchemy 作为 ORM 选项,请通过 <st c="3742">flask-sqlalchemy</st> 通过 <st c="3771">pip</st> 命令安装:

 pip install flask-sqlalchemy

其次,通过创建 <st c="3855">engine</st>, <st c="3863">db_session</st>, 和 <st c="3879">Base</st> 类来启用 <st c="3828">SQLAlchemy</st> ,因为我们的原型将利用 声明式方法 进行数据库连接。 此设置可在 <st c="4008">/model/config.py</st> 模块的 `两个应用程序中找到。

现在,使用 <st c="4069">Base</st> 类创建模型类,这些类将成为数据库迁移的基础。 以下代码片段展示了如何使用 SQLAlchemy 的 <st c="4245">Base</st> 类实现模型类:

 class Complaint(<st c="4273">Base</st>):
   __tablename__ = 'complaint'
   id = Column(Integer, <st c="4331">Sequence('complaint_id_seq', increment=1)</st>, <st c="4374">primary_key = True</st>)
   cid = Column(Integer, <st c="4417">ForeignKey('complainant.id')</st>, nullable = False)
   catid = Column(Integer, <st c="4489">ForeignKey('category.id')</st>, nullable = False)
   ctype = Column(Integer, <st c="4558">ForeignKey('complaint_type.id')</st>, nullable = False) <st c="4609">category = relationship('Category', back_populates="complaints")</st><st c="4673">complainants = relationship('Complainant', back_populates="complaints")</st>
 <st c="4745">complaint_type = relationship('ComplaintType', back_populates="complaints")</st>
<st c="4952">Base</st> class to create an SQLAlchemy model class that will depict the schema of its corresponding table. For instance, a given <st c="5077">Complaint</st> class corresponds to the <st c="5112">complaint</st> table with the <st c="5137">id</st>, <st c="5141">cid</st>, <st c="5146">catid</st>, and <st c="5157">ctype</st> columns, as defined by the <st c="5190">Column</st> helper class with the matching column type classes. All column metadata must be correct since *<st c="5291">Flask-Migrate</st>* will derive the table schema details from this metadata during migration. All column metadata, including the *<st c="5414">primary</st>*, *<st c="5423">unique</st>*, and *<st c="5435">foreign key constraints</st>*, will be part of this database migration. After migration, the following model classes will generate sub-tables for the <st c="5579">complaint</st> table:

class Category(Base):

tablename = 'category'

id = Column(Integer, Sequence('category_id_seq', increment=1), primary_key = True)

name = Column(String(45), nullable = False) complaints = relationship('Complaint', back_populates="category") … … … … … …

class ComplaintType(Base):

tablename = 'complaint_type'

id = Column(Integer, Sequence('complaint_type_id_seq', increment=1), primary_key = True)

name = Column(String(45), nullable = False) complaints = relationship('Complaint', back_populates="complaint_type") … … … … … …

class ComplaintDetails(Base):

tablename = 'complaint_details'

id = Column(Integer, Sequence('complaint_details_id_seq', increment=1), primary_key = True)

compid = Column(Integer, ForeignKey('complaint.id'), nullable = False, unique=True)

statement = Column(String(100), nullable = False)

status = Column(String(50))

resolution = Column(String(100))

date_resolved = Column(Date) complaint = relationship('Complaint', back_populates="complaint_details") … … … … … …


			<st c="6599">The</st> `<st c="6604">Category</st>`<st c="6612">,</st> `<st c="6614">ComplaintType</st>`<st c="6627">, and</st> `<st c="6633">ComplaintDetails</st>` <st c="6649">classes all reference the parent</st> `<st c="6683">Complaint</st>`<st c="6692">, as depicted by</st> <st c="6709">their respective</st> `<st c="6726">relationship()</st>` <st c="6740">parameters.</st>
			<st c="6752">With SQLAlchemy set up, install the</st> `<st c="6789">flask-migrate</st>` <st c="6802">extension module:</st>

pip install flask-migrate


			<st c="6846">Before running the migration commands from the extension module, create a module file (not</st> `<st c="6938">main.py</st>`<st c="6945">) to provide the</st> <st c="6962">necessary helper classes to run the migration commands locally.</st> <st c="7027">The following</st> `<st c="7041">manage.py</st>` <st c="7050">file of our prototypes will run the module’s</st> `<st c="7096">install</st>`<st c="7103">,</st> `<st c="7105">manage</st>`<st c="7111">, and</st> `<st c="7117">upgrade</st>` <st c="7124">CLI commands:</st>

从 flask_migrate 导入 Migrate

从 flask_sqlalchemy 导入 SQLAlchemy

从 model.config 导入 Base from main import app

import toml

app.config.from_file('config-dev.toml', toml.load) db = SQLAlchemy(app, metadata=Base.metadata)

flask-sqlalchemy方法,其中 SQLAlchemy 类的实例创建模型类,实例化 <st c="7553">Migrate</st> 类只需传递 <st c="7593">app</st>实例和 SQLAlchemy 实例。在这种方法中,SQLAlchemy 仍然是迁移过程的重要组成部分,但其显式实例化将取决于 <st c="7755">Base.metadata</st>构造函数参数,除了 <st c="7806">app</st>实例之外。 <st c="7845">Migration</st> 类的实例化也需要 <st c="7879">app</st>实例和派生的 SQLAlchemy 实例,如给定的模块脚本所示。

        <st c="7965">现在,如果迁移</st> `<st c="7988">设置准备就绪且正确,</st> `<st c="8020">migrate</st>` <st c="8027">实例由</st> `<st c="8049">manage.py</st>` <st c="8058">提供,可以运行</st> `<st c="8071">init</st>` <st c="8075">CLI 命令。</st> <st c="8089">这次执行将生成迁移过程所需的 Alembic 文件。</st>

        <st c="8169">设置 Alembic 配置</st>

        <st c="8206">Flask-Migrate 使用 Alembic 来</st> <st c="8246">建立和管理数据库迁移。</st> <st c="8279">从</st> `<st c="8291">migrate</st>` <st c="8295">实例运行</st> `<st c="8317">init</st>` <st c="8324">CLI 命令将在项目目录内生成 Alembic 配置文件。</st> <st c="8410">以下 Python 命令运行 Flask-Migrate 的</st> `<st c="8460">init</st>` <st c="8464">CLI 命令,使用我们的</st> `<st c="8487">manage.py</st>` <st c="8496">文件:</st>
 python -m flask --app manage.py db init
        <st c="8542">在前面的命令中,</st> `<st c="8569">db</st>` <st c="8571">指定了传递给</st> `<st c="8630">migrate</st>` <st c="8637">实例的 SQLAlchemy</st> `<st c="8597">db</st>` <st c="8599">实例,而</st> `<st c="8654">init</st>` <st c="8658">是</st> `<st c="8698">flask_migrate</st>` <st c="8711">模块的一部分。</st> <st c="8720">运行前面的命令将创建日志,列出由</st> `<st c="8820">init</st>` <st c="8824">命令生成的所有文件夹和文件,如图</st> *<st c="8849">图 4</st>**<st c="8857">.1</st>*<st c="8859">所示:</st>

        ![图 4.1 – init CLI 命令日志](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_04_1.jpg)

        <st c="9611">图 4.1 – init CLI 命令日志</st>

        <st c="9649">所有的 Alembic 文件都在</st> `<st c="9687">migrations</st>` <st c="9697">文件夹内,并且由前面的命令自动生成。</st> <st c="9754">该</st> `<st c="9758">migrations</st>` <st c="9768">文件夹包含主要的 Alembic 文件,</st> `<st c="9808">env.py</st>`<st c="9814">,它可以进行调整或进一步配置以支持一些额外的迁移需求。</st> *<st c="9910">图 4</st>**<st c="9918">.2</st>* <st c="9920">显示了</st> `<st c="9946">migrations</st>` <st c="9956">文件夹的内容:</st>

        ![图 4.2 – 迁移文件夹](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_04_2.jpg)

        <st c="10027">图 4.2 – 迁移文件夹</st>

        <st c="10061">除了</st> `<st c="10073">env.py</st>`<st c="10079">之外,以下文件也包含在</st> `<st c="10126">migrations</st>` <st c="10136">文件夹中:</st>

            +   <st c="10144">该</st> `<st c="10149">alembic.ini</st>` <st c="10160">文件,其中包含默认的 Alembic</st> <st c="10202">配置变量。</st>

            +   <st c="10226">该</st> `<st c="10231">script.py.mako</st>` <st c="10245">文件,作为迁移文件的模板文件。</st>

        <st c="10310">还将有一个</st> `<st c="10332">versions</st>` <st c="10340">文件夹,其中将包含运行</st> `<st c="10406">migrate</st>` <st c="10413">命令后的迁移脚本。</st>

        <st c="10422">创建迁移</st>

        <st c="10446">在生成 Alembic 文件后,</st> `<st c="10487">migrate</st>` <st c="10494">CLI 命令将准备好开始</st> *<st c="10534">初始迁移</st>*<st c="10551">。首次运行</st> `<st c="10565">migrate</st>` <st c="10572">命令将根据 SQLAlchemy 模型类从头开始生成所有表。</st> <st c="10683">运行</st> `<st c="10713">migrate</st>` <st c="10720">CLI 命令的 Python 命令如下:</st>
 python -m flask --app manage.py db migrate -m "Initial"
        *<st c="10803">图 4</st>**<st c="10812">.3</st>* <st c="10814">显示了运行此</st> <st c="10857">初始迁移后的日志消息:</st>

        ![图 4.3 – migrate CLI 命令日志](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_04_3.jpg)

        <st c="11707">图 4.3 – migrate CLI 命令日志</st>

        成功的初始迁移将<st c="11748">创建一个</st> <st c="11784">数据库中的</st> `<st c="11795">alembic_version</st>` <st c="11810">表。</st> *<st c="11834">图 4</st>**<st c="11842">.4</st>* <st c="11844">显示了初始数据库迁移后</st> `<st c="11870">ocms</st>` <st c="11874">数据库的内容:</st>

        ![图 4.4 – alembic_version 表](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_04_4.jpg)

        <st c="12131">图 4.4 – alembic_version 表</st>

        <st c="12169">每次执行</st> `<st c="12193">migrate</st>` <st c="12200">命令都会创建一个与分配的唯一</st> *<st c="12283">版本号</st>*<st c="12297">相似的迁移脚本文件名。</st> Flask-Migrate 在</st> `<st c="12347">alembic_version</st>` <st c="12362">表中记录这些版本号,并将所有迁移脚本放置在</st> `<st c="12413">migrations</st>` <st c="12423">文件夹中,该文件夹位于</st> `<st c="12441">/versions</st>` <st c="12450">子目录下。</st> <st c="12466">以下是一个此类</st> <st c="12500">迁移脚本的示例:</st>
 """empty message
Revision ID: 9eafa601a7db
Revises:
Create Date: 2023-06-08 06:51:46.327352
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic. <st c="12702">revision = '9eafa601a7db'</st> down_revision = None
branch_labels = None
depends_on = None <st c="12788">def upgrade():</st> # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('category',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('name', sa.String(length=45), nullable=False),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_table('complaint_type',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('name', sa.String(length=45), nullable=False),
    sa.PrimaryKeyConstraint('id')
    )
  … … … … … … …
        <st c="13212">这些自动生成的迁移脚本有时需要验证、编辑和重新编码,因为它们并不总是对 SQLAlchemy 模型类的精确描述。</st> <st c="13385">有时,这些脚本没有</st> <st c="13417">捕捉到应用于</st> <st c="13504">模型的关系表和元数据中的所需更改。</st>

        <st c="13515">现在,为了实现最终的迁移脚本,需要执行</st> `<st c="13566">升级</st>` <st c="13573">CLI 命令</st>。</st>

        <st c="13607">应用数据库更改</st>

        <st c="13637">运行</st> <st c="13673">升级</st> `<st c="13677">CLI 命令</st>` 的完整 Python 命令如下:</st>
 python -m flask --app manage.py db upgrade
        *<st c="13754">图 4</st>**<st c="13763">.6</st>* 展示了运行 `<st c="13807">升级</st>` <st c="13814">CLI 命令</st>` 后的日志消息:

        ![图 4.5 – 升级 CLI 命令日志](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_04_5.jpg)

        <st c="14141">图 4.5 – 升级 CLI 命令日志</st>

        <st c="14182">初始升级执行会生成初始迁移脚本中定义的所有表。</st> <st c="14282">此外,后续的脚本将始终根据应用于模型类的更改修改模式。</st> <st c="14410">另一方面,</st> `<st c="14512">降级</st>` <st c="14521">CLI 命令。</st> <st c="14535">此命令将恢复数据库的先前版本。</st>

        <st c="14594">在 Flask 项目中,没有 Flask-Migrate,数据库迁移将不会直接且无缝。</st> <st c="14696">从头开始编写迁移设置和流程将非常耗时且相当严格。</st>

        <st c="14802">下一个可以帮助开发团队节省处理 Bootstrap 静态文件并将其导入 Jinja2 模板时间的扩展是</st> *<st c="14945">Bootstrap-Flask</st>*<st c="14960">。</st>

        <st c="14961">使用 Bootstrap-Flask 设计 UI</st>

        <st c="15000">有几种方法可以将</st> <st c="15026">具有外观和感觉的上下文数据渲染到 Jinja2 模板中</st> <st c="15089">而不必过于担心下载资源文件或从</st> **<st c="15196">内容分发网络</st>** <st c="15220">(</st>**<st c="15222">CDN</st>**<st c="15225">) 存储库</st> <st c="15239">中引用静态文件并将它们导入模板页面以管理最佳的 UI 设计来呈现。</st> <st c="15331">其中最理想和最及时</st> <st c="15368">的选项是</st> **<st c="15379">Bootstrap-Flask</st>**<st c="15394">,这是一个与</st> *<st c="15428">Flask-Bootstrap</st>* <st c="15443">扩展模块</st> <st c="15462">截然不同的模块。</st> <st c="15462">后者仅使用 Bootstrap 版本 3.0,而</st> *<st c="15512">Bootstrap-Flask</st>* <st c="15527">可以支持高达</st> *<st c="15546">Bootstrap 5.0</st>*<st c="15559">。因此,建议在设置 Flask-Bootstrap 之前先卸载 Flask-Bootstrap 和其他 UI 相关模块,以避免意外冲突。</st> <st c="15683">仅允许 Bootstrap-Flask 管理 UI 设计可以提供</st> <st c="15779">更好的结果。</st>

        <st c="15794">但首先,让我们通过运行以下</st> *<st c="15820">Bootstrap-Flask</st>* <st c="15835">命令来安装它:</st> `<st c="15861">pip</st>` <st c="15864">命令:</st>
 pip install bootstrap-flask
        <st c="15901">接下来,我们将设置</st> <st c="15942">具有所需 Bootstrap</st> <st c="15969">框架发行版的</st> Bootstrap 模块。</st>

        <st c="15992">设置 UI 模块</st>

        <st c="16017">为了使模块与 Flask 平台协同工作,必须在</st> `<st c="16091">main.py</st>` <st c="16098">模块中</st> <st c="16107">设置。</st> `<st c="16111">bootstrap_flask</st>` <st c="16126">模块具有</st> `<st c="16138">Bootstrap4</st>` <st c="16148">和</st> `<st c="16153">Bootstrap5</st>` <st c="16163">核心类,必须在将框架的资产应用之前将它们连接到 Flask 实例。</st> <st c="16262">应用程序只能使用一个</st> <st c="16294">Bootstrap 发行版:我们的</st> `<st c="16322">ch04-web</st>` <st c="16330">应用程序使用</st> `<st c="16356">Bootstrap4</st>` <st c="16366">类来保持与</st> *<st c="16402">第三章</st>*<st c="16411">的 Bootstrap 偏好的一致性。</st> `<st c="16437">以下</st>` `<st c="16451">main.py</st>` <st c="16458">模块实例化</st> `<st c="16479">Bootstrap4</st>`<st c="16489">,这启用了</st> <st c="16509">扩展模块:</st>
 from flask import Flask <st c="16551">from flask_bootstrap import Bootstrap4</st> import toml
from model.config import init_db
init_db()
app = Flask(__name__, template_folder='pages', static_folder="resources")
app.config.from_file('config-dev.toml', toml.load) <st c="16802">Bootstrap4</st> class requires the <st c="16832">app</st> instance as its constructor argument to proceed with the instantiation. After this setup, the Jinja2 templates can now load the necessary built-in resource files and start the web design process.
			<st c="17031">Applying the Bootstrap files and assets</st>
			<st c="17071">Bootstrap-Flask has a</st> `<st c="17094">bootstrap.load_css()</st>` <st c="17114">helper function that loads the CSS resources into the Jinja2 template and a</st> `<st c="17191">bootstrap.load_js()</st>` <st c="17210">helper function that loads all Bootstrap</st> <st c="17251">JavaScript files.</st> <st c="17270">The following is the</st> `<st c="17291">login.html</st>` <st c="17301">template of the</st> `<st c="17318">ch04-web</st>` <st c="17326">application with the preceding two</st> <st c="17362">helper functions:</st>

<head>
<meta charset="utf-8" />

<meta http-equiv="x-ua-compatible" content="ie=edge" />

<meta name="viewport" content="width=device-width, initial-scale=1" />

<title>在线投诉管理系统</title> <st c="17622">{{ bootstrap.load_css() }}</st> </head>
<body>
… … … … … … <st c="17675">{{ bootstrap.load_js() }}</st> </body>

			<st c="17716">It is always the standard to call</st> `<st c="17751">bootstrap.load_css()</st>` <st c="17771">in</st> `<st c="17775"><head></st>`<st c="17781">, which is the appropriate markup to call the</st> `<st c="17827"><style></st>` <st c="17834">tag.</st> <st c="17840">Calling</st> `<st c="17848">bootstrap.load_js()</st>` <st c="17867">in</st> `<st c="17871"><head></st>` <st c="17877">is also feasible, but for many, the custom is to load all the JavaScript files in the last part of the</st> `<st c="17981"><body></st>` <st c="17987">content, which is why</st> `<st c="18010">bootstrap.load_css()</st>` <st c="18030">is present there.</st> <st c="18049">On the other hand, if there are custom</st> `<st c="18088">styles.css</st>` <st c="18098">or JavaScript files for the applications, the module can allow their imports in the Jinja2 templates, so long as there are no conflicts with the</st> <st c="18244">Bootstrap resources.</st>
			<st c="18264">After loading the CSS and</st> <st c="18291">JavaScript, we can start designing the pages with the Bootstrap components.</st> <st c="18367">The following code shows the content of the given</st> `<st c="18417">login.html</st>` <st c="18427">page with all the needed</st> *<st c="18453">Bootstrap</st>* *<st c="18463">4</st>* <st c="18464">components:</st>

<body>
<section class="<st c="18500">position-relative py-4 py-xl-5</st>">

    <div class="<st c="18547">container position-relative</st>">

        <div class="<st c="18591">row mb-5</st>">

            <div class="<st c="18616">col-md-8 col-xl-6 text-center</st> <st c="18646">mx-auto</st>">

                <h2 class="<st c="18669">display-3</st>">用户登录</h2>

            </div>

        </div>

        <div class="<st c="18724">row d-flex justify-content-center</st>">

            <div class="<st c="18774">col-md-6 col-xl-4</st>">

                <div class="<st c="18808">card</st>">

                    <div class="<st c="18829">card-body text-center</st> <st c="18851">d-flex flex-column align-items-center</st>">

                    <form action="{{ request.path }}" method = "post">

                        … … … … … …

                    </form>

                    </div>

                </div>

            </div>

        </div>

    </div>

</section>

… … … … … …

</body>

			*<st c="19029">Figure 4</st>**<st c="19038">.6</st>* <st c="19040">shows the</st> <st c="19051">published version of the given</st> `<st c="19082">login.html</st>` <st c="19092">web design:</st>
			![Figure 4.6 – The login.html page using Bootstrap 4](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_04_6.jpg)

			<st c="19143">Figure 4.6 – The login.html page using Bootstrap 4</st>
			<st c="19193">Aside from the updated Bootstrap support, the Bootstrap-Flask module has macros and built-in configurations that applications can use to create a better</st> <st c="19347">UI design.</st>
			<st c="19357">Utilizing built-in features</st>
			<st c="19385">The extension module</st> <st c="19406">has five built-in</st> *<st c="19425">macros</st>* <st c="19431">that Jinja2 templates can import to create fewer HTML codes and manageable components.</st> <st c="19519">These built-in macros are</st> <st c="19545">as follows:</st>

				*   `<st c="19556">bootstrap4/form.html</st>`<st c="19577">: This can render</st> *<st c="19596">Flask-WTF</st>* <st c="19605">forms or form components and their hidden</st> <st c="19648">error messages.</st>
				*   `<st c="19663">bootstrap4/nav.html</st>`<st c="19683">: This can render navigations</st> <st c="19714">and breadcrumbs.</st>
				*   `<st c="19730">bootstrap4/pagination.html</st>`<st c="19757">: This can provide paginations to</st> *<st c="19792">Flask-SQLAlchemy</st>* <st c="19808">data.</st>
				*   `<st c="19814">bootstrap4/table.html</st>`<st c="19836">: This can render table-formatted</st> <st c="19871">context data.</st>
				*   `<st c="19884">bootstrap4/utils.html</st>`<st c="19906">: This can provide other utilities, such as rendering flash messages, icons, and resource</st> <st c="19997">reference code.</st>

			<st c="20012">The module also has built-in</st> *<st c="20042">configuration variables</st>* <st c="20065">to enable and disable some features and customize Bootstrap components.</st> <st c="20138">For instance,</st> `<st c="20152">BOOTSTRAP_SERVE_LOCAL</st>` <st c="20173">disables the process of loading built-in CSS and JavaScript when set to</st> `<st c="20246">false</st>` <st c="20251">and allows us to refer to CDN or local resources in</st> `<st c="20304">/static</st>` <st c="20311">instead.</st> <st c="20321">In addition,</st> `<st c="20334">BOOTSTRAP_BTN_SIZE</st>` <st c="20352">and</st> `<st c="20357">BOOTSTRAP_BTN_STYLE</st>` <st c="20376">can</st> <st c="20380">customize buttons.</st> <st c="20400">The</st> **<st c="20404">TOML</st>** <st c="20408">or any configuration file is where all these</st> <st c="20454">configuration variables are registered</st> <st c="20493">and set.</st>
			<st c="20501">Next, we’ll focus on</st> *<st c="20523">Flask-WTF</st>*<st c="20532">, a module supported</st> <st c="20553">by</st> *<st c="20556">Bootstrap-Flask</st>*<st c="20571">.</st>
			<st c="20572">Building Flask forms with Flask-WTF</st>
			`<st c="20750">WTForms</st>` <st c="20757">library to enhance form handling in Flask applications.</st> <st c="20814">Instead of using HTML markup, Flask-WTF provides</st> <st c="20863">the necessary utilities</st> <st c="20886">to manage the web forms in a Pythonic way through</st> <st c="20937">form models.</st>
			<st c="20949">Creating the form models</st>
			<st c="20974">Form models must extend the</st> `<st c="21003">FlaskForm</st>` <st c="21012">core class to create and render the</st> `<st c="21049"><form></st>` <st c="21055">tag.</st> <st c="21061">Its attributes</st> <st c="21076">correspond to the form fields defined by the following</st> <st c="21131">helper classes:</st>

				*   `<st c="21146">StringField</st>`<st c="21158">: Defines and creates a text input field that accepts</st> <st c="21213">string-typed data.</st>
				*   `<st c="21231">IntegerField</st>`<st c="21244">: Defines and creates a text input field that</st> <st c="21291">accepts integers.</st>
				*   `<st c="21308">DecimalField</st>`<st c="21321">: Defines and creates a text input field that asks for</st> <st c="21377">decimal values.</st>
				*   `<st c="21392">DateField</st>`<st c="21402">: Defines and creates a text input field that supports</st> `<st c="21458">Date</st>` <st c="21462">types with the default format</st> <st c="21493">of</st> `<st c="21496">yyyy-mm-dd</st>`<st c="21506">.</st>
				*   `<st c="21507">EmailField</st>`<st c="21518">: Defines and creates a text input that uses a regular expression to manage</st> <st c="21595">email-formatted</st> <st c="21611">values.</st>
				*   `<st c="21618">SelectField</st>`<st c="21630">: Defines and creates a</st> <st c="21655">combo box.</st>
				*   `<st c="21665">SelectMultipleField</st>`<st c="21685">: Defines and creates a combo box with a list</st> <st c="21732">of options.</st>
				*   `<st c="21743">TextAreaField</st>`<st c="21757">: Defines a text area for multi-line</st> <st c="21795">text input.</st>
				*   `<st c="21806">FileField</st>`<st c="21816">: Defines and creates a file upload field for</st> <st c="21863">uploading files.</st>
				*   `<st c="21879">PasswordField</st>`<st c="21893">: Defines a password</st> <st c="21915">input field.</st>

			<st c="21927">The following code shows a form model that utilizes the given helper classes to build the</st> `<st c="22018">Complainant</st>` <st c="22029">form</st> *<st c="22035">widgets</st>* <st c="22042">for the</st> `<st c="22051">add_complainant()</st>` <st c="22068">view:</st>

from flask_wtf import FlaskForm

from wtforms import StringField, IntegerField, SelectField, DateField, EmailField

from wtforms.validators import InputRequired, Length, Regexp, Email class ComplainantForm(FlaskForm):

id = <st c="22298">SelectField</st>('选择登录 ID: ', validators=[InputRequired()])

firstname = <st c="22374">StringField</st>('输入名字:', validators=[InputRequired(), Length(max=50)])

middlename = <st c="22466">StringField</st>('输入中间名:', validators=[InputRequired(), Length(max=50)])

lastname = <st c="22557">StringField</st>('输入姓氏:', validators=[InputRequired(), Length(max=50)])

email = <st c="22643">EmailField</st>('输入邮箱:', validators=[InputRequired(), Length(max=20), Email()])

mobile = <st c="22735">StringField</st>('输入手机:', validators=[InputRequired(), Length(max=20), Regexp(regex=r"^(\+63)[-]{1}\d{3}

            [-]{1}\d{3}[-]{1}\d{4}$", message="有效的电话号码格式为 +63-xxx-xxx-xxxx")])

address = <st c="22939">StringField</st>('输入地址:', validators=[InputRequired(), Length(max=100)])

zipcode = <st c="23027">IntegerField</st>('输入邮编:',  validators=[InputRequired()])

status = <st c="23099">SelectField</st>('输入状态:', choices=[('active', 'ACTIVE'),

    ('无效', 'INACTIVE'), ('已阻止', 'BLOCKED')], validators=[InputRequired()])

date_registered = <st c="23348">firstname</st>,<st c="23359">middlename</st>,<st c="23371">lastname</st>和<st c="23385">address</st>是具有不同长度的输入类型文本框,是必填表单参数。对于特定的输入类型,<st c="23500">date_registered</st>是<st c="23552">Date</st>类型的必填表单参数,日期格式为<st c="23584">yyyy-mm-dd</st>,而<st c="23602">email</st>是电子邮件类型文本框。另一方面,<st c="23658">status</st>和<st c="23669">id</st>表单参数是组合框,但不同的是<st c="23753">id</st>中没有选项。<st c="23761">status</st>表单参数的<st c="23791">choices</st>选项已经在表单中定义,而在<st c="23849">id</st>表单参数中,视图函数将在运行时填充这些字段。以下是一个管理<st c="23959">add_complainant()</st>视图的<st c="23999">id</st>参数选项的代码片段:
 @complainant_bp.route('/complainant/add', methods=['GET', 'POST'])
def add_complainant(): <st c="24113">form:ComplainantForm = ComplainantForm()</st> login_repo = LoginRepository(db_session)
    users = login_repo.select_all() <st c="24227">form.id.choices = [(f"{u.id}", f"{u.username}") for u</st> <st c="24280">in users]</st><st c="24290">if request.method == 'GET':</st><st c="24318">return render_template('complainant_add.html',</st> <st c="24365">form=form), 200</st><st c="24381">else:</st><st c="24387">if form.validate_on_submit():</st><st c="24417">details = dict()</st><st c="24434">details["id"] = int(form.id.data)</st><st c="24468">details["firstname"] = form.firstname.data</st><st c="24511">details["lastname"]  = form.lastname.data</st> … … … … … … <st c="24564">complainant:Complainant =</st> <st c="24589">Complainant(**details)</st><st c="24612">complainant_repo:ComplainantRepository =</st> <st c="24653">ComplainantRepository(db_session)</st><st c="24687">result = complainant_repo.insert(complainant)</st> if result:
                records = complainant_repo.select_all()
                return render_template( 'complainant_list_all.html', records=records), 200 <st c="24860">else:</st><st c="24865">return render_template('complainant_add.html',</st><st c="24912">form=form), 500</st> else:
            return render_template('complainant_add.html', form=form), 500
        <st c="24997">前面的视图访问了</st> `<st c="25030">choices</st>` <st c="25037">参数的</st> `<st c="25055">id</st>` <st c="25057">表单参数,将其分配给一个包含</st> `<st c="25101">(id, username) Tuple</st>`<st c="25121">的列表,其中<st c="25128">username</st>作为标签,<st c="25136">id</st>作为其值。

        <st c="25170">另一方面,<st c="25223">add_complainant()</st> <st c="25240">视图的 HTTP POST 事务将在提交后通过</st> `<st c="25310">form</st>` <st c="25314">参数的</st> `<st c="25327">validate_on_submit()</st>`<st c="25347">验证表单验证错误。如果没有错误,视图函数将提取所有表单数据从</st> `<st c="25421">form</st>` <st c="25425">对象,将</st> <st c="25444">投诉详情插入数据库,并渲染所有投诉者的列表。</st> <st c="25521">否则,它将返回带有提交的</st> `<st c="25580">ComplainantForm</st>` <st c="25595">实例和表单数据值的表单页面。</st> <st c="25628">现在我们已经实现了表单模型和管理的视图函数,我们可以专注于如何使用 Jinja2 模板将这些模型映射到它们各自的</st> `<st c="25772"><form></st>` <st c="25778">标签。

        <st c="25807">渲染表单</st>

        在将 WTF 表单模型返回到 Jinja2 模板之前,视图函数必须访问并实例化 FlaskForm 子类,甚至用值填充一些字段以准备</st> `<st c="26024"><form></st>` <st c="26030">映射。</st> <st c="26040">将模型表单与适当的值关联可以避免在</st> `<st c="26124"><</st>``<st c="26125">form></st>` <st c="26130">加载时出现 Jinja2 错误。

        <st c="26139">现在,表单渲染仅在存在</st> <st c="26172">HTTP GET</st> <st c="26202">请求以加载表单或当 HTTP POST 在提交过程中遇到验证错误时发生,需要重新加载显示当前值和错误状态的表单页面。</st> <st c="26392">HTTP 请求的类型决定了在渲染表单之前将哪些值分配给表单模型的字段。</st> <st c="26504">因此,在给定的</st> `<st c="26523">add_complainant()</st>` <st c="26540">视图中,检查</st> `<st c="26559">request.method</st>` <st c="26573">是否为</st> *<st c="26579">GET</st> <st c="26582">请求意味着验证何时使用具有基础或初始化值的</st> `<st c="26626">complainant_add.html</st>` <st c="26646">表单模板和</st> `<st c="26670">ComplainantForm</st>` <st c="26685">实例进行渲染。</st> <st c="26728">否则,它将是一个渲染当前表单值和</st> <st c="26810">验证错误的表单页面。</st>

        <st c="26828">以下</st> `<st c="26843">complainant_add.html</st>` <st c="26863">页面将</st> `<st c="26878">ComplainantForm</st>` <st c="26893">字段,包括基础或当前值,映射到</st> `<st c="26938"><</st>``<st c="26939">form></st>` <st c="26944">标签:
 <form action = "{{ request.path }}" method = "post"> <st c="27003">{{ form.csrf_token }}</st> <div class="mb-3">{{ <st c="27046">form.id</st>(<st c="27055">size</st>=1, <st c="27064">class</st>="form-control") }}</div>
  <div class="mb-3"> {{ <st c="27118">form.firstname</st>(<st c="27134">size</st>=50, <st c="27144">placeholder</st>='Firstname', <st c="27170">class</st>="form-control") }}
  </div> <st c="27203">{% if form.firstname.errors %}</st><st c="27233"><ul></st><st c="27238">{% for error in form.username.errors %}</st><st c="27278"><li>{{ error }}</li></st><st c="27299">{% endfor %}</st><st c="27312"></ul></st><st c="27318">{% endif %}</st> … … … … … …
  <div class="mb-3"> {{ <st c="27364">form.mobile</st>(<st c="27377">size</st>=50, <st c="27387">placeholder</st>='+63-XXX-XXX-XXXX', <st c="27420">class</st>="form-control") }}
  </div>
  … … … … … …
  <div class="mb-3"> {{ <st c="27487">form.email</st>(<st c="27499">size</st>=50,<st c="27508">placeholder</st>='xxxxxxx@xxxx.xxx', <st c="27542">class</st>="form-control") }}
  </div>
  … … … … … …
   <div class="mb-3"> {{ <st c="27609">form.zipcode</st>(<st c="27623">size</st>=50, <st c="27633">placeholder</st>='Zip Code', <st c="27658">class</st>="form-control") }}
   </div>
   … … … … … …
</form>
        <st c="27710">将单个模型字段绑定到</st> `<st c="27749"><form></st>` <st c="27755">需要调用字段的属性 - 例如,</st> `<st c="27814">context_name.field()</st>`<st c="27834">。因此,要渲染</st> `<st c="27854">firstname</st>` <st c="27863">表单字段</st> `<st c="27878">ComplainantForm</st>`<st c="27893">,例如,Jinja2 模板必须在</st> `<st c="27939">form.firstname()</st>` <st c="27955">内部调用</st> `<st c="27967">{{}}</st>` <st c="27971">语句。</st> <st c="27983">方法调用还可以包括其</st> `<st c="28020">kwargs</st>` <st c="28026">或</st> *<st c="28030">关键字参数</st>* <st c="28047">的小部件属性,例如</st> `<st c="28078">size</st>`<st c="28082">,</st> `<st c="28084">placeholder</st>`<st c="28095">,和</st> `<st c="28101">class</st>`<st c="28106">,如果在渲染过程中小部件的默认设置发生了变化。</st> <st c="28182">如模板所示,</st> *<st c="28212">Flask-WTF</st>* <st c="28221">小部件支持由</st> *<st c="28279">Bootstrap-Flask</st>* <st c="28294">模块扩展提供的 Bootstrap 组件。</st> <st c="28313">使用小部件添加自定义 CSS 样式也是可行的,只要 CSS 属性设置在小部件的</st> <st c="28411">kwargs</st><st c="28426">中。</st>

        现在,让我们探讨 Flask 是否可以像 Django 的表单一样,防止来自**跨站请求伪造** <st c="28427">(CSRF)问题。</st>

        应用 CSRF

        Flask-WTF 通过其`<st c="28571">csrf_token</st>` <st c="28599">生成内置了 CSRF 支持。</st> <st c="28643">要</st> <st c="28646">通过 Flask-WTF 启用 CSRF,在`<st c="28689">CSRFProtect</st>` <st c="28700">从`<st c="28710">flask_wtf</st>` <st c="28719">模块中实例化`<st c="28730">main.py</st>`<st c="28737">,如下面的代码片段所示:</st>
<st c="28773">from flask_wtf import CSRFProtect</st> app = Flask(__name__, template_folder='pages', static_folder="resources")
app.config.from_file('config-dev.toml', toml.load)
bootstrap = Bootstrap4(app) <st c="29019">csrf_token</st>) using the WTF form context inside the <st c="29071">{{}}</st> statement enables the token generation per-user access, as shown in the given <st c="29154">complainant_add.html</st> template. Flask-WTF generates a unique token for every rendition of the form fields. Note that CRSF protection is only possible with Flask-WTF if the <st c="29325">SECRET_KEY</st> configuration variable is part of the configuration file and has the appropriate hash value.
			<st c="29428">CRSF protection occurs in every form submission that involves the form model instance.</st> <st c="29516">Now, let’s discuss the general flow of form submission with the</st> <st c="29580">Flask-WTF module.</st>
			<st c="29597">Submitting the form</st>
			<st c="29617">After clicking the submit button, the</st> *<st c="29656">HTTP POST</st>* <st c="29665">request transaction of</st> `<st c="29689">add_complainant()</st>` <st c="29706">retrieves the</st> <st c="29720">form values after the validation, as shown in the preceding snippet.</st> <st c="29790">Flask-WTF sends the form data to the view function through the</st> *<st c="29853">HTTP POST</st>* <st c="29862">request method, requiring the view function to have validation for the incoming</st> `<st c="29943">POST</st>` <st c="29947">requests.</st> <st c="29958">If</st> `<st c="29961">request.method</st>` <st c="29975">is</st> `<st c="29979">POST</st>`<st c="29983">, the view must perform another evaluation on the extension’s</st> `<st c="30045">validate_on_submit()</st>` <st c="30065">to check for violation of some form constraints.</st> <st c="30115">If the results for all these evaluations are</st> `<st c="30160">True</st>`<st c="30164">, the view function can access all the form data in</st> `<st c="30216">form_object.<field_name>.data</st>`<st c="30245">. Otherwise, the view function will redirect the users to the form page with the corresponding error message(s) and</st> *<st c="30361">HTTP Status</st>* *<st c="30373">Code 500</st>*<st c="30381">.</st>
			<st c="30382">But what comprises Flask-WTF’s form</st> <st c="30419">validation framework, or what criteria are the basis of the</st> `<st c="30479">validate_on_submit()</st>` <st c="30499">result after</st> <st c="30513">form submission?</st>
			<st c="30529">Validating form fields</st>
			<st c="30552">Flask-WTF has a list of useful built-in</st> <st c="30593">validator classes that support core validation rules such as</st> `<st c="30654">InputRequired()</st>`<st c="30669">, which imposes the HTML-required constraint.</st> <st c="30715">Some constraints are specific to widgets, such as the</st> `<st c="30769">Length()</st>` <st c="30777">validator, which can restrict the input length of the</st> `<st c="30832">StringField</st>` <st c="30843">values, and</st> `<st c="30856">RegExp()</st>`<st c="30864">, which can impose regular expressions for mobile and telephone data formats.</st> <st c="30942">Moreover, some validators require dependency modules to be installed, such as</st> `<st c="31020">Email()</st>`<st c="31027">, which needs an email-validator external library.</st> <st c="31078">All these built-in validators are ready to be imported from</st> *<st c="31138">Flask-WTF</st>*<st c="31147">’s</st> `<st c="31151">wtforms.validators</st>` <st c="31169">module.</st> <st c="31178">The</st> `<st c="31182">validators</st>` <st c="31192">parameter of every</st> *<st c="31212">FlaskForm</st>* <st c="31221">attribute can accept any callables from validator classes to impose</st> <st c="31290">constraint rules.</st>
			<st c="31307">Violations are added to the field’s errors list that can trigger</st> `<st c="31373">validate_on_submit()</st>` <st c="31393">to return</st> `<st c="31404">False</st>`<st c="31409">. The form template must render all such error messages per field during redirection after an</st> *<st c="31503">HTTP Status Code</st>* *<st c="31520">500</st>* <st c="31523">error.</st>
			<st c="31530">The module can also support</st> <st c="31559">custom validation for some constraints that are not typical.</st> <st c="31620">There are many ways to implement custom validations, and one is through the</st> *<st c="31696">in-line validator approach</st>* <st c="31722">exhibited in the</st> <st c="31740">following snippet:</st>

class ComplainantForm(FlaskForm):

… … … … … …

zipcode = IntegerField('输入邮编:', validators=[InputRequired()])

… … … … … …

def validate_<st c="31902">zipcode</st>(self, <st c="31918">zipcode</st>):

    如果`zipcode.data`字符串的长度不等于 4: FlaskForm 函数,其方法名以`validate_`为前缀,后跟它验证的表单字段名称,该字段作为参数传递。给定的`validate_zipcode()`检查表单字段的`zipcode`是否为四位数字值。如果不是,它将抛出一个异常类。另一种方法是实现验证器作为*典型 FlaskForm 函数*,但验证器函数需要显式地注入到它验证的字段的`validators`参数中。

        最后,*类似闭包或可调用的方法* <st c="32543">用于验证器的实现也是可能的。</st> <st c="32634">在这里</st> `<st c="32639">disallow_invalid_dates()</st>` <st c="32663">是一个不允许在给定</st> `<st c="32734">date_after</st>` <st c="32750">之前的日期输入的闭包类型验证器。</st>
<st c="32752">from wtforms.validators ValidationError</st> class ComplainantForm(FlaskForm): <st c="32826">def disallow_invalid_dates(date_after):</st> message = 'Must be after %s.' % (date_after) <st c="32911">def _disallow_invalid_dates(form, field):</st> base_date = datetime.strptime(date_after, '%Y-%m-%d').date()
          if field.data < base_date: <st c="33041">raise ValidationError(message)</st><st c="33071">return _disallow_invalid_dates</st> … … … … … …
     … … … … … …
     date_registered = DateField('Enter date registered', format='%Y-%m-%d', validators=[InputRequired(), <st c="33409">disallow_invalid_dates()</st>, the closure is <st c="33450">_disallow_invalid_dates(form, field)</st>, which raises <st c="33501">ValidationError</st> when the <st c="33526">field.data</st> or <st c="33540">date_registered</st> form value is before the specified boundary date’s <st c="33607">date_after</st> provided by the validator function. To apply validators, you can call them just like a typical method – that is, with parameter values in the <st c="33760">validators</st> parameter of the field class.
			<st c="33800">Another extension module that is popular nowadays and supports the recent Flask framework is the</st> *<st c="33898">Flask-RESTful</st>* <st c="33911">module.</st> <st c="33920">We’ll take a look at it in the</st> <st c="33951">next section.</st>
			<st c="33964">Building RESTful services with Flask-RESTful</st>
			<st c="34009">The</st> `<st c="34014">flask-RESTful</st>` <st c="34027">module uses the</st> *<st c="34044">class-based view strategy</st>* <st c="34069">of Flask to build RESTful services.</st> <st c="34106">It provides a</st> `<st c="34120">Resource</st>` <st c="34128">class to create custom resources to build from the ground up HTTP-based services instead of</st> <st c="34221">endpoint-based routes.</st>
			<st c="34243">This chapter specifies another</st> <st c="34275">application,</st> `<st c="34288">ch04-api</st>`<st c="34296">, that implements</st> <st c="34314">RESTful endpoints for managing user complaints and related details.</st> <st c="34382">Here’s one of the resource-based implementations of our application’s</st> <st c="34452">API endpoints:</st>

from flask_restful import Resource class ListComplaintRestAPI(资源): def get(self): repo = ComplaintRepository(db_session)

    records = repo.select_all()

    complaint_rec = [rec.to_json() for rec in records]

    return make_response(jsonify(complaint_rec), 201)

			<st c="34724">Here,</st> `<st c="34731">flask_restful</st>` <st c="34744">provides the</st> `<st c="34758">Resource</st>` <st c="34766">class that creates resources for views.</st> <st c="34807">In this case,</st> `<st c="34821">ListComplaintRestAPI</st>` <st c="34841">sub-classes the</st> `<st c="34858">Resource</st>` <st c="34866">class to override its</st> `<st c="34889">get()</st>` <st c="34894">instance method, which will retrieve all complaints from the database through an HTTP</st> *<st c="34981">GET</st>* <st c="34984">request.</st> <st c="34994">On the other hand,</st> `<st c="35013">AddComplaintRestAPI</st>` <st c="35032">implements the</st> *<st c="35048">INSERT</st>* <st c="35054">complaint transaction through an HTTP</st> *<st c="35093">POST</st>* <st c="35097">request:</st>

class AddComplaintRestAPI(资源): def post(self): complaint_json = request.get_json()

    repo = ComplaintRepository(db_session)

    complaint = Complaint(**complaint_json)

    result = repo.insert(complaint)

    if result:

        content = jsonify(complaint_json)

        return make_response(content, 201)

    else:

        content = jsonify(message="插入投诉记录时遇到问题")

        return make_response(content, 500)

			<st c="35504">The</st> `<st c="35509">Resource</st>` <st c="35517">class</st> <st c="35523">has a</st> `<st c="35530">post()</st>` <st c="35536">method that needs to be overridden to</st> <st c="35575">create</st> *<st c="35582">POST</st>* <st c="35586">transactions.</st> <st c="35601">The following</st> `<st c="35615">UpdateComplainantRestAPI</st>`<st c="35639">,</st> `<st c="35641">DeleteComplaintRestAPI</st>`<st c="35663">, and</st> `<st c="35669">UpdateComplaintRestAPI</st>` <st c="35691">resources implement HTTP</st> *<st c="35717">PATCH</st>*<st c="35722">,</st> *<st c="35724">DELETE</st>*<st c="35730">, and</st> *<st c="35736">PUT</st>*<st c="35739">, respectively:</st>

class UpdateComplainantRestAPI(资源): def patch(self, id): complaint_json = request.get_json()

    repo = ComplaintRepository(db_session)

    result = repo.update(id, complaint_json)

    if result:

        `content = jsonify(complaint_json)`

        返回`make_response(content, 201)`

    否则:

        `content = jsonify(message="更新投诉人 ID 时遇到问题")`

        返回`make_response(content, 500)`

			<st c="36129">The</st> `<st c="36134">Resource</st>` <st c="36142">class’</st> `<st c="36150">patch()</st>` <st c="36157">method implements the HTTP</st> *<st c="36185">PATCH</st>* <st c="36190">request when overridden.</st> <st c="36216">Like HTTP</st> *<st c="36226">GET</st>*<st c="36229">,</st> `<st c="36231">patch()</st>` <st c="36238">can also accept path variables or request parameters by declaring local parameters to the override.</st> <st c="36339">The</st> `<st c="36343">id</st>` <st c="36345">parameter in the</st> `<st c="36363">patch()</st>` <st c="36370">method of</st> `<st c="36381">UpdateComplaintRestAPI</st>` <st c="36403">is a path variable for a complainant ID.</st> <st c="36445">This is</st> <st c="36452">required to retrieve the</st> <st c="36478">complainant’s profile:</st>

class DeleteComplaintRestAPI(<st c="36530">Resource</st>): <st c="36543">def delete(self, id):</st> repo = ComplaintRepository(db_session)

    `result = repo.delete(id)`

    如果`result`:

        `content = jsonify(message=f'投诉 {id} 已删除')`

        返回`make_response(content, 201)`

    否则:

        `content = jsonify(message="删除投诉记录时遇到问题")`

        返回`make_response(content, 500)`

			<st c="36843">The</st> `<st c="36848">delete()</st>` <st c="36856">override of</st> `<st c="36869">DeleteComplaintRestAPI</st>` <st c="36891">also has an</st> `<st c="36904">id</st>` <st c="36906">parameter that’s used to delete</st> <st c="36939">the</st> <st c="36942">complaint:</st>

class UpdateComplaintRestAPI(<st c="36983">Resource</st>): <st c="36996">def put(self):</st> complaint_json = request.get_json()

    `repo = ComplaintRepository(db_session)`

    `result = repo.update(complaint_json['id'], complaint_json)`

    如果`result`:

        `content = jsonify(complaint_json)`

        返回`make_response(content, 201)`

    否则:

        `content = jsonify(message="更新投诉记录时遇到问题")`

        返回`make_response(content, 500)`

			<st c="37340">To utilize all the preceding resources, the Flask-RESTful extension module has an</st> `<st c="37423">Api</st>` <st c="37426">class that must be instantiated with</st> <st c="37463">the</st> `<st c="37468">Flask</st>` <st c="37473">or</st> `<st c="37477">Blueprint</st>` <st c="37486">instance as its constructor argument.</st> <st c="37525">Since</st> <st c="37530">the</st> `<st c="37535">ch04-api</st>` <st c="37543">project uses blueprints, the following</st> `<st c="37583">__init__.py</st>` <st c="37594">file of the complaint blueprint module highlights how to create the</st> `<st c="37663">Api</st>` <st c="37666">instance and map all these resources with their respective</st> <st c="37726">URL patterns:</st>

<st c="37739">从 flask 导入 Blueprint</st>

<st c="37767">从 flask_restful 导入 Api</st>

<st c="37797">complaint_bp</st> = Blueprint('complaint_bp', __name__)

modules.complaint.api.complaint导入AddComplaintRestAPI, ListComplaintRestAPI, UpdateComplainantRestAPI, UpdateComplaintRestAPI, DeleteComplaintRestAPI

… … … … … … <st c="38021">api = Api(complaint_bp)</st> api.<st c="38049">add_resource</st>(<st c="38064">AddComplaintRestAPI</st>, '/complaint/add', endpoint='add_complaint')

api.<st c="38133">add_resource</st>(<st c="38148">ListComplaintRestAPI</st>, '/complaint/list/all', endpoint='list_all_complaint')

api.<st c="38228">add_resource</st>(<st c="38243">UpdateComplainantRestAPI</st>, '/complaint/update/complainant/<int:id>', endpoint='update_complainant')

api.<st c="38346">add_resource</st>(<st c="38361">UpdateComplaintRestAPI</st>, '/complaint/update', endpoint='update_complaint')

api.<st c="38541">Api</st> 类有一个 <st c="38558">add_resource()</st> 方法,它将每个资源映射到其 *<st c="38612">URL 模式</st>* 和 *<st c="38628">端点</st>* 或 *<st c="38640">视图函数名称</st>*。此脚本展示了如何将投诉模块的所有资源类注入平台作为完整的 API 端点。蓝图模块内外的端点名称和 URL 冲突会导致编译时错误,因此每个资源都必须具有唯一详细信息。

        `<st c="38945">下一个模块扩展,</st> *<st c="38973">Flask-Session</st>*<st c="38986">,为 Flask 提供了一个比其</st> <st c="39052">内置实现</st>更好的会话处理解决方案。</st>`

        `<st c="39076">使用 Flask-Session 实现会话处理</st>`

        `<st c="39125">The</st> **<st c="39130">Flask-Session</st>** <st c="39143">模块,就像 Flask 的内置会话一样,易于配置和使用,除了模块扩展不会</st> <st c="39249">在</st> <st c="39263">浏览器</st>中存储会话数据。</st>`

        <st c="39288">Before you can configure this module, you must install it using the</st> `<st c="39357">pip</st>` <st c="39360">command:</st>
 pip install flask-session
        <st c="39395">Then, import the</st> `<st c="39413">Session</st>` <st c="39420">class into the</st> `<st c="39436">main.py</st>` <st c="39443">module to instantiate and integrate the extension module into the Flask platform.</st> <st c="39526">The following</st> `<st c="39540">main.py</st>` <st c="39547">snippet shows the configuration</st> <st c="39580">of</st> *<st c="39583">Flask-Session</st>*<st c="39596">:</st>
<st c="39598">from flask_session import Session</st> app = Flask(__name__)
app.config.from_file('config-dev.toml', toml.load) <st c="39705">sess = Session()</st>
<st c="39745">Session</st> instance is only used for configuration and not for session handling per se. Flask’s <st c="39838">session</st> proxy object should always directly access the session data for storage and retrieval.
			<st c="39932">Afterward, set some Flask-Session configuration variables, such as</st> `<st c="40000">SESSION_FILE_THRESHOLD</st>`<st c="40022">, which sets the maximum number of data the session stores before deletion, and</st> `<st c="40102">SESSION_TYPE</st>`<st c="40114">, which determines the kind of data storage for the session data.</st> <st c="40180">The following are some</st> `<st c="40203">SESSION_TYPE</st>` <st c="40215">options:</st>

				*   `<st c="40224">null</st>` <st c="40229">(default): This utilizes</st> `<st c="40255">NullSessionInterface</st>`<st c="40275">, which triggers an</st> `<st c="40295">Exception</st>` <st c="40304">error.</st>
				*   `<st c="40311">redis</st>`<st c="40317">: This utilizes</st> `<st c="40334">RedisSessionInterface</st>` <st c="40355">to use</st> *<st c="40363">Redis</st>* <st c="40368">as a</st> <st c="40374">data store.</st>
				*   `<st c="40385">memcached</st>`<st c="40395">: This utilizes</st> `<st c="40412">MemcachedSessionInterface</st>` <st c="40437">to</st> <st c="40441">use</st> *<st c="40445">memcached</st>*<st c="40454">.</st>
				*   `<st c="40455">filesystem</st>`<st c="40466">: This utilizes</st> `<st c="40483">FileSystemSessionInterface</st>` <st c="40509">to use the</st> *<st c="40521">filesystem</st>* <st c="40531">as</st> <st c="40535">the datastore.</st>
				*   `<st c="40549">mongodb</st>`<st c="40557">: This utilizes</st> `<st c="40574">MongoDBSessionInterface</st>` <st c="40597">to use the</st> <st c="40609">MongoDB database.</st>
				*   `<st c="40626">sqlalchemy</st>`<st c="40637">: This uses</st> `<st c="40650">SqlAlchemySessionInterface</st>` <st c="40676">to apply the SQLAlchemy ORM for a</st> <st c="40710">relational database as</st> <st c="40734">session</st> <st c="40741">storage.</st>

			<st c="40750">The module can also recognize Flask session config variables such</st> <st c="40817">as</st> `<st c="40820">SESSION_LIFETIME</st>`<st c="40836">.</st>
			<st c="40837">The following configuration variables are registered in the</st> `<st c="40898">config-dev.toml</st>` <st c="40913">file for</st> <st c="40923">both applications:</st>

SESSION_LIFETIME = true SESSION_TYPE = "filesystem" SESSION_FILE_THRESHOLD = 600

SESSION_PERMANENT = true


			<st c="41047">Lastly, start the Flask server to load all the configurations and check the module’s integration.</st> <st c="41146">The module will establish database connectivity to the specified data storage at server startup.</st> <st c="41243">In our case, the</st> *<st c="41260">Flask-Session</st>* <st c="41273">module will create a</st> `<st c="41295">flask_session</st>` <st c="41308">directory inside the project</st> <st c="41338">directory when the</st> <st c="41357">application starts.</st>
			*<st c="41376">Figure 4</st>**<st c="41385">.7</st>* <st c="41387">shows the</st> `<st c="41398">flask_session</st>` <st c="41411">folder and</st> <st c="41423">its content:</st>
			![Figure 4.7 – The session files inside the flask_session folder](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_04_7.jpg)

			<st c="41502">Figure 4.7 – The session files inside the flask_session folder</st>
			<st c="41564">With everything set up, utilize</st> <st c="41596">Flask’s</st> `<st c="41605">session</st>` <st c="41612">to handle session data.</st> <st c="41637">This can be seen in</st> `<st c="41657">login_db_auth()</st>`<st c="41672">, which stores</st> `<st c="41687">username</st>` <st c="41695">as a session attribute for other</st> <st c="41729">views’ reach:</st>

from flask import session

@login_bp.route('/login/auth', methods=['GET', 'POST'])

def login_db_auth():

authForm:LoginAuthForm = LoginAuthForm()

… … … … … …

if authForm.validate_on_submit():

    repo = LoginRepository(db_session)

    username = authForm.username.data

    … … … … … …

    if user == None:

        return render_template('login.html', form=authForm), 500

    elif not user.password == password:

        return render_template('login.html', form=authForm), 500

    else: <st c="42189">session['username'] = request.form['username']</st> return redirect('/ch04/login/add')

else:

return render_template('login.html', form=authForm), 500

			<st c="42334">Similar to</st> *<st c="42346">Flask-Session</st>*<st c="42359">, another</st> <st c="42369">extension module that can help</st> <st c="42399">build a better enterprise-grade Flask application is the</st> *<st c="42457">Flask-Caching</st>* <st c="42470">module.</st>
			<st c="42478">Applying caching using Flask-Caching</st>
			`<st c="42794">BaseCache</st>` <st c="42803">class from its</st> `<st c="42819">flask_caching.backends.base</st>` <st c="42846">module.</st>
			<st c="42854">Before we can configure Flask-Caching, we must install the</st> `<st c="42914">flask-caching</st>` <st c="42927">module via the</st> `<st c="42943">pip</st>` <st c="42946">command:</st>

pip install flask-caching


			<st c="42981">Then, we must register some of its configuration variables in the configuration file, such as</st> `<st c="43076">CACHE_TYPE</st>`<st c="43086">, which sets the cache type suited for the application, and</st> `<st c="43146">CACHE_DEFAULT_TIMEOUT</st>`<st c="43167">, which sets the caching timeout.</st> <st c="43201">The following are the applications’ caching configuration variables declared in their respective</st> `<st c="43298">config-dev.toml</st>` <st c="43313">files:</st>

CACHE_TYPE = "FileSystemCache" CACHE_DEFAULT_TIMEOUT = 300

CACHE_DIR = "./cache_dir/"

CACHE_THRESHOLD = 800


			<st c="43428">Here,</st> `<st c="43435">CACHE_DIR</st>` <st c="43444">sets the cache folder for the filesystem cache type, while</st> `<st c="43504">CACHE_THRESHOLD</st>` <st c="43519">sets the maximum number of cached items before it starts</st> <st c="43577">deleting some.</st>
			<st c="43591">Afterward, to avoid cyclic collisions, create a</st> <st c="43639">separate module file, such as</st> `<st c="43670">main_cache.py</st>`<st c="43683">, to instantiate</st> <st c="43699">the</st> `<st c="43704">Cache</st>` <st c="43709">class from the</st> `<st c="43725">flask_caching</st>` <st c="43738">module.</st> <st c="43747">Access to the</st> `<st c="43761">cache</st>` <st c="43766">instance must be done from</st> `<st c="43794">main_cache.py</st>`<st c="43807">, not</st> `<st c="43813">main.py</st>`<st c="43820">, even though the final setup of the extension module occurs in</st> `<st c="43884">main.py</st>`<st c="43891">. The following snippet integrates the</st> *<st c="43930">Flask-Caching</st>* <st c="43943">module into the</st> <st c="43960">Flask platform:</st>

from main_cache import cache app = Flask(name, template_folder='pages', static_folder="resources")

app.config.from_file('config-dev.toml', toml.load) cache_dir must be created inside the main folder, as shown in Figure 4**.8:

        ![Figure 4.8 – The cache files in cache_dir](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_04_8.jpg)

        <st c="44449">Figure 4.8 – The cache files in cache_dir</st>

        <st c="44490">If the setup is successful, components can now access</st> `<st c="44545">main_cache.py</st>` <st c="44558">for the cache instance.</st> <st c="44583">It has a</st> `<st c="44592">cached()</st>` <st c="44600">decorator that can provide caching for various functions.</st> <st c="44659">First, it can cache views, usually with an HTTP</st> *<st c="44707">GET</st>* <st c="44710">request to retrieve bulk records from the database.</st> <st c="44763">The following view function from the</st> `<st c="44800">complainant.py</st>` <st c="44814">module of the</st> `<st c="44829">ch04-web</st>` <st c="44837">application caches all its results to</st> `<st c="44876">cache_dir</st>` <st c="44885">for</st> <st c="44890">optimal performance:</st>
<st c="44910">from main_cache import cache</st> @complainant_bp.route('/complainant/list/all', methods=['GET']) <st c="45004">@cache.cached(timeout=50, key_prefix="all_complaints")</st> def list_all_complainant():
     repo:ComplainantRepository = ComplainantRepository(db_session)
     records = repo.select_all()
     return render_template('complainant_list_all.html', records=records), 200
        <st c="45251">要装饰</st> `<st c="45283">cached()</st>` <st c="45291">装饰器的确切位置</st> <st c="45316">在函数定义和视图函数的路由装饰器之间。</st> <st c="45383">装饰器需要</st> `<st c="45403">key_prefix</st>` <st c="45413">来生成</st> `<st c="45426">cache_key</st>`<st c="45435">。如果没有指定,*<st c="45455">Flask-Caching</st>* <st c="45468">将使用默认的</st> `<st c="45490">request.path</st>` <st c="45502">作为</st> `<st c="45510">cache_key</st>` <st c="45519">值。</st> <st c="45527">请注意</st> `<st c="45537">cache_key</st>` <st c="45546">是用于访问函数缓存的值的键,并且仅用于模块访问。</st> <st c="45653">给定的</st> `<st c="45663">list_all_complainant()</st>` <st c="45685">使用</st> `<st c="45730">prefix_key</st>` <st c="45740">设置为</st> `<st c="45748">all_complaints</st>`<st c="45762">来缓存渲染的投诉列表。</st>

        <st c="45763">此外,由 Flask-RESTful 创建的资源型 API 的端点函数也可以通过</st> `<st c="45897">@cached()</st>` <st c="45906">装饰器</st> <st c="45918">缓存它们的返回值。</st> <st c="45918">以下代码展示了</st> `<st c="45943">ListComplaintDetailsRestAPI</st>` <st c="45970">来自</st> `<st c="45980">ch04-api</st>` <st c="45988">应用程序,它将</st> `<st c="46027">ComplaintDetails</st>` <st c="46043">记录的列表</st> <st c="46052">缓存到</st> `<st c="46057">cache_dir</st>`<st c="46066">:</st>
 class ListComplaintDetailsRestAPI(Resource): <st c="46114">@cache.cached(timeout=50)</st> def get(self):
        repo = ComplaintDetailsRepository(db_session)
        records = repo.select_all()
        compdetails_rec = [rec.to_json() for rec in records]
        return make_response(jsonify(compdetails_rec), 201)
        <st c="46333">如前述代码片段所示,装饰器放置在</st> `<st c="46433">Resource</st>` <st c="46441">类的覆盖方法之上。</st> <st c="46449">此规则也适用于其他</st> <st c="46488">基于类的视图。</st>

        <st c="46506">该模块还可以缓存在用户访问期间检索大量数据的存储库和</st> <st c="46547">服务函数。</st> <st c="46622">以下代码展示了</st> `<st c="46649">select_all()</st>` <st c="46661">函数,该函数从</st> `<st c="46700">login</st>` <st c="46705">表中检索数据:</st>
<st c="46712">from main_cache import cache</st> class LoginRepository:
    def __init__(self, sess:Session):
        self.sess = sess
    … … … … … … <st c="46828">@cache.cached(timeout=50, key_prefix='all_login')</st> def select_all(self) -> List[Any]:
        users = self.sess.query(Login).all()
        return users
    … … … … … …
        此外,该模块还支持<st c="46974">存储值的过程</st> *<st c="47014">记忆化</st>* <st c="47025">,类似于缓存,但用于频繁访问的自定义函数。</st> <st c="47126">缓存</st> <st c="47130">实例有一个</st> <st c="47135">memoize()</st> <st c="47151">装饰器</st> <st c="47160">来管理这些函数以提高性能。</st> <st c="47224">以下代码展示了</st> `<st c="47253">@memoize</st>` <st c="47261">装饰的</st> `<st c="47279">方法</st> <st c="47282">ComplaintRepository</st>`<st c="47301">:</st>
<st c="47303">from main_cache import cache</st> class ComplaintRepository:
    def __init__(self, sess:Session):
        self.sess = sess <st c="47410">@cache.memoize(timeout=50)</st> def select_all(self) -> List[Any]:
        complaint = self.sess.query(Complaint).all()
        return complaint
        <st c="47533">给定的</st> `<st c="47544">select_all()</st>` <st c="47556">方法将缓存所有查询记录 50 秒以提高其数据检索性能。</st> <st c="47657">为了在服务器启动后清除缓存,始终在</st> `<st c="47711">cache.clear()</st>` <st c="47724">在</st> `<st c="47732">main.py</st>` <st c="47739">模块</st> <st c="47746">在</st> `<st c="47753">蓝图注册</st>》之后调用。</st>

        <st c="47776">为了能够通过电子邮件发送投诉,让我们展示一个流行的扩展模块</st> <st c="47865">称为</st> *<st c="47872">Flask-Mail</st>*<st c="47882">。</st>

        <st c="47883">使用 Flask-Mail 添加邮件功能</st>

        **<st c="47921">Flask-Mail</st>** <st c="47932">是一个扩展模块,用于在不进行太多配置的情况下向电子邮件服务器发送邮件。</st> <st c="48015">它处理发送邮件到电子邮件服务器,无需太多配置。</st>

        <st c="48034">首先,使用</st> `<st c="48054">flask-mail</st>` <st c="48064">模块通过</st> `<st c="48082">pip</st>` <st c="48085">命令安装:</st>
 pip install flask-mail
        <st c="48117">然后,创建一个单独的模块脚本,例如</st> `<st c="48165">mail_config.py</st>`<st c="48179">,以实例化</st> `<st c="48200">Mail</st>` <st c="48204">类。</st> <st c="48212">这种方法解决了当视图或端点函数访问</st> `<st c="48310">mail</st>` <st c="48314">实例以进行实用方法时发生的循环冲突。</st>

        <st c="48348">尽管有一个独立的模块,但</st> `<st c="48382">main.py</st>` <st c="48389">模块仍然需要访问</st> `<st c="48423">mail</st>` <st c="48427">实例,以便将模块集成到 Flask 平台。</st> <st c="48486">以下</st> `<st c="48500">main.py</st>` <st c="48507">代码片段显示了如何使用 Flask 设置</st> *<st c="48540">Flask-Mail</st>* <st c="48550">模块</st> <st c="48558">:</st>
<st c="48569">from mail_config import mail</st> app = Flask(__name__, template_folder='pages', static_folder="resources")
app.config.from_file('config-dev.toml', toml.load) <st c="48724">mail.init_app(app)</st>
        <st c="48742">之后,设置需要将一些配置变量设置在配置文件中。</st> <st c="48832">以下配置</st> <st c="48860">变量是本章中我们应用程序的最基本设置:</st>
 MAIL_SERVER ="smtp.gmail.com"
MAIL_PORT = 465 <st c="48986">MAIL_USERNAME = "your_email@gmail.com"</st>
<st c="49024">MAIL_PASSWORD = "xxxxxxxxxxxxxxxx"</st> MAIL_USE_TLS = false
MAIL_USE_SSL = true
        <st c="49100">这些详细信息适用于一个</st> *<st c="49128">Gmail</st>* <st c="49133">账户,但您可以用</st> *<st c="49173">Yahoo!</st>* <st c="49179">账户的详细信息替换它们。</st> <st c="49197">最好</st> `<st c="49209">MAIL_USE_SSL</st>` <st c="49221">设置为</st> `<st c="49237">true</st>`<st c="49241">。请注意,</st> `<st c="49253">MAIL_PASSWORD</st>` <st c="49266">是从电子邮件账户的</st> *<st c="49304">应用密码</st>* <st c="49316">生成的令牌,而不是实际密码,以建立与邮件服务器的安全连接。</st> <st c="49414">服务器启动后,如果没有</st> <st c="49479">编译错误,Flask-Mail 即可使用。</st>

        <st c="49495">以下</st> `<st c="49510">email_complaint()</st>` <st c="49527">视图函数使用</st> `<st c="49551">mail</st>` <st c="49555">实例通过</st> *<st c="49597">Flask-WTF</st>* <st c="49606">和</st> *<st c="49611">Flask-Mail</st>* <st c="49621">扩展模块发送投诉:</st>
<st c="49640">from flask_mail import Message</st>
<st c="49671">from mail_config import mail</st> @complaint_bp.route("/complaint/email")
def email_complaint(): <st c="49764">form:EmailComplaintForm = EmailComplaintForm()</st> if request.method == 'GET':
        return render_template('email_form.html', form=form), 200
    if form.validate_on_submit():
       try:
           recipients = [rec for rec in str(form.to.data).split(';')]
           msg = <st c="49997">Message(form.subject, sender = 'your_email@gmail.com', recipients = recipients)</st><st c="50076">msg.body = form.message.data</st><st c="50105">mail.send(msg)</st> form:EmailComplaintForm = EmailComplaintForm()
           return render_template('email_.html', form=form, message='Email sent.'), 200
       except:
         return render_template('email_.html', form=form), 500
        <st c="50306">在此处,</st> `<st c="50313">EmailComplaintForm</st>` <st c="50331">为</st> `<st c="50357">Message()</st>` <st c="50366">属性提供详细信息,除了发送者,这是分配给</st> `<st c="50452">MAIL_USERNAME</st>` <st c="50465">配置变量的电子邮件地址。</st> <st c="50490">`<st c="50494">mail</st>` <st c="50498">实例提供了</st> `<st c="50521">send()</st>` <st c="50527">实用方法,用于将消息发送给</st> `<st c="50566">收件人</st>`。</st>

        <st c="50583">摘要</st>

        <st c="50591">本章解释了在构建应用程序时如何利用 Flask 扩展模块。</st> <st c="50682">大多数扩展模块将让我们专注于需求,而不是配置和设置的复杂性,例如</st> *<st c="50814">Flask-WTF</st>* <st c="50823">和</st> *<st c="50828">Bootstrap-Flask</st>* <st c="50843">模块。</st> <st c="50853">一些可以缩短开发时间,而不是反复编写代码片段或再次处理所有细节,例如</st> *<st c="50983">Flask-Migrate</st>* <st c="50996">在数据库迁移上和</st> *<st c="51024">Flask-Mail</st>* <st c="51034">用于向电子邮件服务器发送消息。</st> <st c="51074">一些模块可以增强 Flask 框架的内置功能并提供更好的配置选项,例如</st> *<st c="51194">Flask_Caching</st>* <st c="51207">和</st> *<st c="51212">Flask-Session</st>*<st c="51225">。最后,一些模块会组织概念和组件的实现,例如</st> *<st c="51317">Flask-RESTful</st>*<st c="51333">。</st>

        <st c="51334">尽管可能会出现版本冲突和过时模块的问题,但扩展模块提供的许多优势超过了所有缺点。</st> <st c="51492">但最重要的是,在应用这些扩展模块时,软件需求必须始终放在首位。</st> <st c="51595">你应该始终能够从这些模块中选择最合适的选项,以最好地满足所需的需求,而不仅仅是因为它们可以</st> <st c="51744">提供捷径。</st>

        <st c="51762">下一章将介绍 Flask 框架和</st> <st c="51818">异步编程</st>。





第二部分:构建高级 Flask 3.x 应用程序

在本部分,您将学习如何扩展您的 Flask 技能,使用异步视图和 API 端点构建具有增强性能的企业应用程序。 您将学习如何使用异步 SQLAlchemy 构建异步存储库事务和非关系型数据库来管理非结构化或半结构化大数据。 总体而言,本部分将引导您实现利用异步后台任务、上传 XLSX 和 CSV 文件以生成图表、图形和表格数据、生成 PDF 文档、使用 WebSockets 和 服务器端事件 (SSE),实现非 BPMN 和 BPMN 工作流程,并保护应用程序免受 网络攻击。

本部分包括以下章节:

  • 第五章, 构建异步事务

  • 第六章, 开发计算和科学应用程序

  • 第七章, 使用非关系型数据存储

  • 第八章, 使用 Flask 构建工作流程

  • 第九章, 保护 Flask 应用程序

第六章:5

构建异步事务

在详细讨论了 Flask 3.0 框架的核心组件和高级特性之后,本章将探讨 Flask 管理请求和响应的异步能力以及其执行异步服务和 存储库事务的能力。

Flask 最初是一个在 <st c="589">asyncio</st> 工具上运行的 <st c="659">SQLAlchemy 2.x</st> 构建异步存储库事务的标准 Python 框架。

本章还将探讨其他途径,帮助 Flask 应用程序通过异步机制获得最快的性能,例如 Celery 任务、任务队列、WebSocket 和服务器推送。 本章还将介绍 Quart,这是一个基于异步的 Flask 平台,可以与原始的 <st c="1035">Flask 框架</st> 相比,构建和运行所有组件都是异步的。

本章将重点介绍以下主题:

  • 创建异步 Flask 组件

  • 构建异步 SQLAlchemy 存储层

  • 使用 asyncio 实现异步事务

  • 利用异步 信号通知

  • 使用 Celery 和 Redis 构建后台任务

  • 使用异步事务构建 WebSocket

  • 实现异步 服务器端发送 事件 (SSE)

  • 使用 RxPy 应用响应式编程

  • 选择 Quart 而非 Flask 2.x

技术要求

本章将突出介绍一个具有一些异步任务和后台进程的《在线投票系统》原型,这些进程用于管理来自不同地区的候选人申请和与选举相关的提交的高带宽,以及满足从各个政党同时检索投票计数的需要。该系统由三个独立的项目组成,分别是 <st c="1900">ch05-api</st>,它包含 API 端点, <st c="1939">ch05-web</st>,它实现了 SSE、WebSocket 和基于模板的结果,以及 <st c="2018">ch05-quart</st>,它为应用程序提供了使用 Quart 框架的另一个平台。 <st c="2101">所有这些项目都使用了</st> 《应用工厂设计模式》 <st c="2128">,并且可在</st> [<st c="2184">https://github.com/PacktPublishing/Mastering-Flask-Web-Development/tree/main/ch05</st>](https://github.com/PacktPublishing/Mastering-Flask-Web-Development/tree/main/ch05)<st c="2265">找到。</st>

创建异步 Flask 组件

<st c="2305">Flask 2.3 及以上版本</st> <st c="2322">到当前版本支持在其基于 WSGI 的平台上运行异步 API 端点和基于 Web 的视图函数。</st> <st c="2447">然而,要完全使用此功能,请使用以下</st> <st c="2535">pip</st> <st c="2538">命令安装</st> <st c="2495">flask[async]</st> <st c="2507">模块:</st>

 pip install flask[async]

在安装了 <st c="2572">flask[async]</st> <st c="2606">模块后,现在可以使用</st> <st c="2656">async</st>/<st c="2663">await</st> <st c="2668">设计模式来实现同步视图。</st> 现在这变得可行了。`

实现异步视图和端点

<st c="2750">Django 或 FastAPI</st> 类似,在 Flask 框架中创建异步视图和端点涉及应用 <st c="2862">async</st>/<st c="2869">await</st> <st c="2874">关键字。</st> 以下来自 ch05-web 的网页视图向用户显示欢迎问候信息,并描述了我们的**《在线》** **《投票》**应用程序:`

 @current_app.route('/ch05/web/index') <st c="3063">async</st> def welcome():
    return render_template('index.html'), 200

<st c="3125">来自另一个应用程序的另一个异步视图函数</st> <st c="3189">ch05-api</st>,在以下 API 端点中展示,该端点向 《数据库》 <st c="3281">(</st>**《DB》**<st c="3291">)**<st c="3293">)**中添加新的登录凭证:</st>

 @current_app.post('/ch05/login/add') <st c="3334">async</st> def add_login():
   async with db_session() as sess:
            repo = LoginRepository(sess)
            login_json = request.get_json()
            login = Login(**login_json)
            result = await repo.insert(login)
            if result:
                content = jsonify(login_json)
                return make_response(content, 201)
            else:
                raise DuplicateRecordException("add login credential has failed")

给定的视图和 API 函数都使用 <st c="3703">async</st> <st c="3708">路由</st> 来管理它们各自的请求和响应对象。使用 async 定义这些路由创建协程,Flask 2.x 可以使用run() 工具asyncio 模块` 意外地运行。但是,Flask 框架如何在 WSGI 平台的缺陷中管理协程执行呢?

Flask 创建一个 <st c="4084">async</st>,框架从工作线程创建一个 子线程 以创建 <st c="4215">asyncio</st> <st c="4222">工具。</st> 尽管存在异步过程,但由于环境仍然在 WSGI,一个同步平台中,因此 <st c="4309">async</st> <st c="4314">可以推进的极限仍然有限。</st> 然而,对于不太复杂的非阻塞事务,<st c="4463">flask[async]</st> <st c="4475">框架足以提高软件质量和性能。</st>

<st c="4540">异步组件</st> <st c="4566">不仅限于视图和 API 函数,还有 Flask 事件处理器`。

实现异步的 before_requestafter_request 处理器

除了视图和端点之外,Flask 3.0 框架允许实现异步的 <st c="4808">before_request</st><st c="4827">after_request</st> 处理器,就像以下 <st c="4875">ch05-api</st> <st c="4883">处理器</st> 那样记录每个 API <st c="4912">请求事务</st>

<st c="4932">@app.before_request</st>
<st c="4952">async</st> def init_request():
    app.logger.info('executing ' + request.endpoint + ' starts') <st c="5040">@app.after_request</st>
<st c="5058">async</st> def return_response(response):
    app.logger.info('executing ' + request.endpoint + ' stops')
    return response

这些事件处理器仍然使用在 <st c="5231">main.py</st> <st c="5238">中创建的 app 实例,使用内置的 logging 模块` 创建日志文件。

另一方面,<st c="5321">flask[async]</st> <st c="5333">模块可以允许创建异步的错误处理器。</st>

创建异步错误处理器

Flask 2.x 可以使用 <st c="5472">@errorhandler</st> <st c="5485">装饰器</st> 装饰协程,以管理引发的异常和 HTTP 状态码。ch05-api 项目的以下异步错误处理器放置在 main.py :`

<st c="5630">@app.errorhandler(404)</st>
<st c="5652">async</st> def not_found(e):
    return jsonify(error=str(e)), 404 <st c="5711">@app.errorhandler(400)</st>
<st c="5733">async</st> def bad_request(e):
    return jsonify(error=str(e)), 400 <st c="5794">@app.errorhandler(DuplicateRecordException)</st>
<st c="5837">async</st> def insert_record_exception(e):
    return jsonify(e.to_dict()), e.code <st c="5912">async</st> def server_error(e):
    return jsonify(error=str(e)), 500
app.register_error_handler(500, server_error)

现在,所有这些 异步 Flask 组件也可以等待其他异步操作,例如应用程序的存储层。 实际上,异步 Flask 环境对集成异步第三方扩展模块,如 async SQLAlchemy 2.x,持开放态度。

构建异步 SQLAlchemy 存储层

更新的 flask-sqlalchemy 扩展模块支持 SQLAlchemy 2.x,它提供了 API 工具,这些工具使用 <st c="6472">asyncio</st> 环境,以 <st c="6497">greenlet</st> 作为主要库,允许在 API 的内部过程中传播 <st c="6555">await</st> 关键字。 我们的 <st c="6606">ch05-web</st> <st c="6619">ch05-api</st> 项目具有异步事务,这些事务调用这些等待的 SQLAlchemy <st c="6791">/models/config.py</st> 文件,该文件利用一个 <st c="6831">asyncpg</st> 驱动程序来构建一个用于非阻塞 存储层事务的会话。

设置数据库连接

要开始 配置,请安装 <st c="6983">asyncpg</st> 数据库驱动程序或方言,该驱动程序或方言是 <st c="7021">asyncio</st>-驱动的 SQLAlchemy 模块所必需的,使用 <st c="7074">pip</st> 命令:

 pip install asyncpg

此外,如果绿色库尚未成为虚拟环境的一部分,请将其包含在安装中:

 pip install greenlet

设置还需要 一个 <st c="7339">asyncpg</st> 协议,用户凭据,数据库服务器的主机地址,端口号以及模式名称。 我们的项目使用连接字符串 <st c="7470">postgresql+asyncpg://postgres:admin2255@localhost:5433/ovs</st>,来生成 <st c="7546">AsyncEngine</st> 实例,该实例使用 SQLAlchemy 框架的 <st c="7591">create_async_engine()</st> 函数,这是其 <st c="7674">create_engine()</st> 实用工具的异步版本。 除了数据库 URL 之外,该方法还需要将它的 <st c="7746">future</st> 参数设置为 <st c="7776">True</st>,将 <st c="7782">pool_pre_pring</st> 设置为 <st c="7810">True</st>,并将 <st c="7820">poolclass</st> 设置为连接池策略,例如 <st c="7881">NullPool</st> <st c="7891">poolclass</st> 管理 SQLAlchemy 在 CRUD 操作期间使用的线程,将其设置为 <st c="7995">NullPool</st> 将限制一个 Python 线程仅运行一个事件循环以执行一个 CRUD 操作。 <st c="8087">pool_pre_ping</st>,另一方面,有助于处理断开连接的悲观方法的连接池。 如果它确定数据库连接不可用或无效,那么在执行新操作之前,该连接及其之前的连接将立即回收。 最重要的是,必须将 <st c="8386">future</st> 参数设置为 <st c="8418">True</st> 以启用 SQLAlchemy 2.x 的异步功能,否则异步 SQLAlchemy 设置将 无法工作。

在成功创建之后, <st c="8566">sessionmaker</st> 可调用对象将需要 <st c="8602">AsyncEngine</st> 实例来实例化每个 CRUD 操作所需的会话。 然而,这次,会话将是 <st c="8733">AsyncSession</st> 类型,并且 <st c="8760">async_scoped_session</st> 可调用对象将帮助通过其提供的 <st c="8836">scopefunc</st> 参数来管理仓库层中的轻量级线程局部会话操作。 每个仓库类都需要这个 <st c="8982">AsyncSession</st> 实例来实现异步 Flask 平台的每个必要的数据库事务。

Now, there is nothing new with the <st c="9119">declarative_base()</st> method, for it will still provide the needed helper classes to generate the model classes for the repository layer, like in the standard SQLAlchemy setup. The following is the complete module script of the specified SQLAlchemy setup:

<st c="9371">from sqlalchemy.ext.asyncio</st> import <st c="9407">create_async_engine</st>, <st c="9428">AsyncSession, async_scoped_session</st>
<st c="9462">from sqlalchemy.orm</st> import declarative_base, <st c="9508">sessionmaker</st>
<st c="9520">from sqlalchemy.pool import NullPool</st>
<st c="9557">from asyncio import current_task</st> DB_URL = "postgresql+<st c="9612">asyncpg</st>:// postgres:admin2255@localhost:5433/ovs"
engine = <st c="9673">create_async_engine</st>(DB_URL, <st c="9702">future=True</st>, echo=True, <st c="9726">pool_pre_ping=True</st>, <st c="9746">poolclass=NullPool</st>)
db_session = <st c="9780">async_scoped_session</st>(<st c="9802">sessionmaker</st>(engine, expire_on_commit=False, class_=AsyncSession), <st c="9870">scopefunc=current_task</st>)
Base = declarative_base() <st c="9921">def init_db():</st> import app.model.db

The <st c="9960">echo</st> parameter of the given <st c="9988">create_async_engine</st> enables logging for the <st c="10032">AsyncEngine</st>-related transactions. Now, the <st c="10076">init_db()</st> method from the preceding configuration exposes the model classes to the different areas of the application. These model classes, built using the <st c="10232">Base</st> instance, help auto-generate the table schemas of our DB through the Flask-Migrate extension module, which still works with <st c="10361">flask[async]</st> and <st c="10378">flask-sqlalchemy</st> integration.

Let us now use the derived <st c="10435">async_scoped_session()</st> to build repository classes.

Building the asynchronous repository layer

The asynchronous repository layer of the application requires the <st c="10596">AsyncSession</st> and the model classes to be created in the setup. The following is a <st c="10678">VoterRepository</st> class implementation that provides CRUD transactions for managing the <st c="10764">Voter</st> records:

<st c="10778">from sqlalchemy import update, delete, insert</st>
<st c="10824">from sqlalchemy.future import select</st>
<st c="10861">from sqlalchemy.orm import Session</st>
<st c="10896">from app.model.db import Voter</st> from datetime import datetime
class <st c="10964">VoterRepository</st>: <st c="11128">Session</st> object is always part of the constructor parameters of the repository class, like in the preceding <st c="11235">VoterRepository</st>.
			<st c="11251">Every operation under the</st> `<st c="11278">AsyncSession</st>` <st c="11290">scope requires an</st> `<st c="11309">await</st>` <st c="11314">process to finish its execution, which means every repository transaction must be</st> *<st c="11397">coroutines</st>*<st c="11407">. Every repository transaction requires an event loop to pursue its execution because of the</st> `<st c="11500">async</st>`<st c="11505">/</st>`<st c="11507">await</st>` <st c="11512">design pattern delegated</st> <st c="11538">by</st> `<st c="11541">AsyncSession</st>`<st c="11553">.</st>
			<st c="11554">The best-fit approach to applying the asynchronous</st> *<st c="11606">INSERT</st>* <st c="11612">operation is to utilize the</st> `<st c="11641">insert()</st>` <st c="11649">method from SQLAlchemy utilities.</st> <st c="11684">The</st> `<st c="11688">insert()</st>` <st c="11696">method will establish the</st> *<st c="11723">INSERT</st>* <st c="11729">command, which</st> `<st c="11745">AsyncSession</st>` <st c="11757">will</st> *<st c="11763">execute</st>*<st c="11770">,</st> *<st c="11772">commit</st>*<st c="11778">, or</st> *<st c="11783">roll back</st>* <st c="11792">asynchronously.</st> <st c="11809">The following is</st> `<st c="11826">VoterRepository</st>`<st c="11841">’s</st> <st c="11845">INSERT transaction:</st>

async def insert_voter(self, voter: Voter) -> bool:

    try: <st c="11922">sql = insert(Voter).values(mid=voter.mid,</st> <st c="11963">precinct=voter.precinct,</st> <st c="11988">voter_id=voter.voter_id,</st> <st c="12013">last_vote_date=datetime.strptime(</st><st c="12047">voter.last_vote_date, '%Y-%m-%d').date())</st><st c="12089">await</st> self.sess.execute(sql) <st c="12119">await</st> self.sess.commit() <st c="12144">await</st> self.sess.close()

        return True

    except Exception as e:

        print(e)

    return False

			<st c="12224">As depicted in the preceding snippet, the transaction awaits the</st> `<st c="12290">execute()</st>`<st c="12299">,</st> `<st c="12301">commit()</st>`<st c="12309">, and</st> `<st c="12315">close()</st>` <st c="12322">methods to finish their respective tasks, which is a clear indicator that a repository operation needs to be a coroutine before executing these</st> `<st c="12467">AsyncSession</st>` <st c="12479">member methods.</st> <st c="12496">The same applies to the following UPDATE transaction of</st> <st c="12552">the</st> <st c="12555">repository:</st>

async def update_voter(self, id:int, details:Dict[str, Any]) -> bool:

try: <st c="12643">sql = update(Voter).where(Voter.id ==</st> <st c="12680">id).values(**details)</st><st c="12702">await</st> self.sess.execute(sql) <st c="12732">await</st> self.sess.commit() <st c="12757">await</st> self.sess.close()

    return True

except Exception as e:

    print(e)

return False

			<st c="12837">The preceding</st> `<st c="12852">update_voter()</st>` <st c="12866">also uses the same asynchronous approach as</st> `<st c="12911">insert_voter()</st>` <st c="12925">using the</st> `<st c="12936">AsyncSession</st>` <st c="12948">methods.</st> `<st c="12958">update_voter()</st>` <st c="12972">also needs an event loop from</st> <st c="13003">Flask to run successfully as an</st> <st c="13035">asynchronous task:</st>

async def delete_voter(self, id:int) -> bool:

    try: <st c="13105">sql = delete(Voter).where(Voter.id == id)</st><st c="13146">等待</st> self.sess.execute(sql) <st c="13176">等待</st> self.sess.commit() <st c="13201">等待</st> self.sess.close()

    return True

    except Exception as e:

        print(e)

    return False

			<st c="13281">For the query transactions, the following are the repository’s coroutines that implement its SELECT</st> <st c="13382">operations:</st>

异步 def select_all_voter(self): sql = select(Voter) q = 等待 self.sess.execute(sql)

    records = q.scalars().all() <st c="13509">等待</st> self.sess.close()

    return records

			<st c="13547">Both</st> `<st c="13553">select_all_voter()</st>` <st c="13571">and</st> `<st c="13576">select_voter()</st>` <st c="13590">use the</st> `<st c="13599">select()</st>` <st c="13607">method from the</st> `<st c="13624">sqlalchemy</st>` <st c="13634">or</st> `<st c="13638">sqlalchemy.future</st>` <st c="13655">module.</st> <st c="13664">With the same objective as the</st> `<st c="13695">insert()</st>`<st c="13703">,</st> `<st c="13705">update()</st>`<st c="13713">, and</st> `<st c="13719">delete()</st>` <st c="13727">utilities, the</st> `<st c="13743">select()</st>` <st c="13751">method establishes a</st> *<st c="13773">SELECT</st>* <st c="13779">command object, which requires the asynchronous</st> `<st c="13828">execute()</st>` <st c="13837">utility for its execution.</st> <st c="13865">Thus, both query implementations are</st> <st c="13902">also coroutines:</st>

异步 def select_voter(self, id:int): sql = select(Voter).where(Voter.id == id) q = 等待 self.sess.execute(sql)

    record = q.scalars().all() <st c="14059">等待</st> self.sess.close()

    return record

			<st c="14096">In SQLAlchemy, the</st> *<st c="14116">INSERT</st>*<st c="14122">,</st> *<st c="14124">UPDATE</st>*<st c="14130">, and</st> *<st c="14136">DELETE</st>* <st c="14142">transactions technically utilize the model attributes that refer to the primary keys of the models’ corresponding DB tables, such as</st> `<st c="14276">id</st>`<st c="14278">. Conventionally, SQLAlchemy recommends updating and removing retrieved records based on</st> <st c="14366">their</st> `<st c="14546">update_precinct()</st>` <st c="14563">and</st> `<st c="14568">delete_voter_by_precinct()</st>` <st c="14594">of</st> <st c="14598">the repository:</st>

异步 def update_precinct(self, old_prec:str, new_prec:str) -> bool:

try:

    sql = update(Voter).<st c="14708">where(Voter.precinct == old_prec).values(precint=new_prec)</st><st c="14767">sql.execution_options(synchronize_session=</st> <st c="14810">"fetch")</st> await self.sess.execute(sql)

    await self.sess.commit()

    await self.sess.close()

    return True

except Exception as e:

    print(e)

return False

			`<st c="14954">update_precinct()</st>` <st c="14972">searches a</st> `<st c="14984">Voter</st>` <st c="14989">record with an existing</st> `<st c="15014">old_prec</st>` <st c="15022">(old precinct) and replaces it with</st> `<st c="15059">new_prec</st>` <st c="15067">(new precinct).</st> <st c="15084">There is no</st> `<st c="15096">id</st>` <st c="15098">primary key used to search the records for updating.</st> <st c="15152">The same scenario is also depicted in</st> `<st c="15190">delete_voter_by_precinct()</st>`<st c="15216">, which uses the</st> `<st c="15233">precinct</st>` <st c="15241">non-primary key value for record removal.</st> <st c="15284">Both</st> <st c="15288">transactions do not conform with the</st> <st c="15326">ideal</st> **<st c="15332">object-relational</st>** **<st c="15350">mapper</st>** <st c="15356">persistence:</st>

异步 def delete_voter_by_precinct(self, precinct:str) -> bool:

    try:

    sql = delete(Voter).<st c="15458">where(Voter.precinct == precinct)</st><st c="15491">sql.execution_options(synchronize_session=</st> <st c="15534">"fetch")</st> await self.sess.execute(sql)

    await self.sess.commit()

    await self.sess.close()

    return True

    except Exception as e:

        print(e)

    return False

			<st c="15678">In this regard, it is mandatory to perform</st> `<st c="15722">execution_options()</st>` <st c="15741">to apply the necessary synchronization strategy, preferably the</st> `<st c="15806">fetch</st>` <st c="15811">strategy, before executing the</st> *<st c="15843">UPDATE</st>* <st c="15849">and</st> *<st c="15854">DELETE</st>* <st c="15861">operations that do not conform with the ORM persistence.</st> <st c="15918">This mechanism provides the session with the resolution to manage the changes reflected by these two operations.</st> <st c="16031">For instance, the</st> `<st c="16049">fetch</st>` <st c="16054">strategy will let the session retrieve the primary keys of those records retrieved through the arbitrary values and will eventually update the in-memory objects or records affected by the operations and merge them into the actual table records.</st> <st c="16300">This setup is essential for the asynchronous</st> <st c="16345">SQLAlchemy operations.</st>
			<st c="16367">After</st> <st c="16374">building the repository layer, let us call these CRUD transactions in our view or</st> <st c="16456">API functions.</st>
			<st c="16470">Utilizing the asynchronous DB transactions</st>
			<st c="16513">To call the</st> <st c="16526">repository transactions, the asynchronous view and endpoint functions require an asynchronous context manager to create and manage</st> `<st c="16657">AsyncSession</st>` <st c="16669">for the repository class.</st> <st c="16696">The following is an</st> `<st c="16716">add_login()</st>` <st c="16727">API function that adds a new</st> `<st c="16757">Login</st>` <st c="16762">credential to</st> <st c="16777">the DB:</st>

from app.model.db import Login

from app.repository.login import LoginRepository from app.model.config import db_session @current_app.post('/ch05/login/add') 异步 def add_login(): 异步 with db_session() as sess:异步 with sess.begin():repo = LoginRepository(sess) login_json = request.get_json()

        login = Login(**login_json) <st c="17112">结果 = 等待(repo.insert_login(login))</st> if 结果:

            content = jsonify(login_json)

            return make_response(content, 201)

        else:

            abort(500)

			<st c="17244">The view function uses the</st> `<st c="17272">async with</st>` <st c="17282">context manager to localize the session for the coroutine or task execution.</st> <st c="17360">It opens the session for that specific task that will run the</st> `<st c="17422">insert_login()</st>` <st c="17436">transaction of</st> `<st c="17452">LoginRepository</st>`<st c="17467">. Then, eventually, the session will be closed by</st> <st c="17516">the repository or the context</st> <st c="17547">manager itself.</st>
			<st c="17562">Now, let us focus on another way of running asynchronous transactions using the</st> `<st c="17643">asyncio</st>` <st c="17650">library.</st>
			<st c="17659">Implementing async transactions with asyncio</st>
			<st c="17704">The</st> `<st c="17709">asyncio</st>` <st c="17716">module</st> <st c="17723">is an easy-to-use library for implementing asynchronous tasks.</st> <st c="17787">Compared to the</st> `<st c="17803">threading</st>` <st c="17812">module, the</st> `<st c="17825">asyncio</st>` <st c="17832">utilities use an event loop to execute each task, which is lightweight and easier to control.</st> <st c="17927">Threading uses one whole thread to run one specific operation, while</st> `<st c="17996">asyncio</st>` <st c="18003">utilizes only a single event loop to run all registered tasks concurrently.</st> <st c="18080">Thus, constructing an event loop is more resource friendly than running multiple threads to build</st> <st c="18178">concurrent transactions.</st>
			`<st c="18202">asyncio</st>` <st c="18210">is seamlessly compatible with</st> `<st c="18241">flask[async]</st>`<st c="18253">, and the clear proof is the following API function that adds a new voter to the DB using the task created by the</st> `<st c="18367">create_task()</st>` <st c="18380">method:</st>

from app.model.db import Member

from app.repository.member import MemberRepository

from app.model.config import db_session from asyncio import create_task, ensure_future, InvalidStateError from app.exceptions.db import DuplicateRecordException @current_app.post("/ch05/member/add") async def add_member():

async with db_session() as sess:

    async with sess.begin():

        repo = MemberRepository(sess)

        member_json = request.get_json()

        member = Member(**member_json)

        try: <st c="18852">插入任务 =</st> <st c="18865">创建任务(repo.insert(member))</st><st c="18898">等待插入任务</st><st c="18916">结果 = 插入任务的结果</st> if 结果:

                content = jsonify(member_json)

                return make_response(content, 201)

            else:

                raise DuplicateRecordException("插入成员记录失败")

        except InvalidStateError:

            abort(500)

			<st c="19128">The</st> `<st c="19133">create_task()</st>` <st c="19146">method</st> <st c="19153">requires a coroutine to create a task and schedule its execution in an event loop.</st> <st c="19237">So, coroutines are not tasks at all, but they are the core inputs for generating these tasks.</st> <st c="19331">Running the scheduled task requires the</st> `<st c="19371">await</st>` <st c="19376">keyword.</st> <st c="19386">After its execution, the task returns a</st> `<st c="19426">Future</st>` <st c="19432">object that requires the task’s</st> `<st c="19465">result()</st>` <st c="19473">built-in method to retrieve its actual returned value.</st> <st c="19529">The given API transaction creates an</st> *<st c="19566">INSERT</st>* <st c="19572">task from the</st> `<st c="19587">insert_login()</st>` <st c="19601">coroutine and retrieves a</st> `<st c="19628">bool</st>` <st c="19632">result</st> <st c="19640">after execution.</st>
			<st c="19656">Now,</st> `<st c="19662">create_task()</st>` <st c="19675">automatically utilizes Flask’s internal event loop in running its tasks.</st> <st c="19749">However, for complex cases such as executing scheduled tasks,</st> `<st c="19811">get_event_loop()</st>` <st c="19827">or</st> `<st c="19831">get_running_loop()</st>` <st c="19849">are more applicable to utilize than</st> `<st c="19886">create_task()</st>` <st c="19899">due to their flexible settings.</st> `<st c="19932">get_event_loop()</st>` <st c="19948">gets the current running event loop, while</st> `<st c="19992">get_running_loop()</st>` <st c="20010">uses the running event in the current</st> <st c="20049">system’s thread.</st>
			<st c="20065">Another way of creating tasks from the coroutine is through</st> `<st c="20126">asyncio</st>`<st c="20133">’s</st> `<st c="20137">ensure_future()</st>`<st c="20152">. The following API uses this utility to spawn a task that lists all</st> <st c="20221">user accounts:</st>

@current_app.get("/ch05/member/list/all")

async def list_all_member():

async with db_session() as sess:

    async with sess.begin():

        repo = MemberRepository(sess) <st c="20395">list_member_task =</st> <st c="20413">ensure_future(repo.select_all_member())</st><st c="20453">await list_member_task</st><st c="20476">records = list_member_task.result()</st> member_rec = [rec.to_json() for rec in records]

        return make_response(member_rec, 201)

			<st c="20598">The only difference</st> <st c="20618">between</st> `<st c="20627">create_task()</st>` <st c="20640">and</st> `<st c="20645">ensure_future()</st>` <st c="20660">is that the former strictly requires coroutines, while the latter can accept coroutines,</st> `<st c="20750">Future</st>`<st c="20756">, or any awaitable objects.</st> `<st c="20784">ensure_future()</st>` <st c="20799">also invokes</st> `<st c="20813">create_task()</st>` <st c="20826">to wrap a</st> `<st c="20837">coroutine()</st>` <st c="20848">argument or directly return a</st> `<st c="20879">Future</st>` <st c="20885">result from a</st> `<st c="20900">Future</st>` <st c="20906">parameter object.</st>
			<st c="20924">On the other hand,</st> `<st c="20944">flask[async]</st>` <st c="20956">supports creating and running multiple tasks concurrently using</st> `<st c="21021">asyncio</st>`<st c="21028">. Its</st> `<st c="21034">gather()</st>` <st c="21042">method has</st> <st c="21054">two parameters:</st>

				*   <st c="21069">The first parameter is the sequence of coroutines,</st> `<st c="21121">Future</st>`<st c="21127">, or any</st> <st c="21136">awaitable objects.</st>
				*   <st c="21154">The second parameter is</st> `<st c="21179">return_exceptions</st>`<st c="21196">, which is set to</st> `<st c="21214">False</st>` <st c="21219">by default.</st>

			<st c="21231">The following is an endpoint function that inserts multiple profiles of candidates using</st> <st c="21321">concurrent tasks:</st>

@current_app.post('/ch05/candidates/party')

async def add_list_candidates():

candidates = request.get_json()

count_rec_added = 0 <st c="21468">results = await gather( *[insert_candidate_task(data)</st> <st c="21521">for data in candidates])</st> for success in results:

    如果成功:

        count_rec_added = count_rec_added + 1

return jsonify(message=f'there are {count_rec_added} newly added candidates'), 201

			<st c="21703">The given API</st> <st c="21717">expects a list of candidate profile details from</st> `<st c="21767">request</st>`<st c="21774">. A service named</st> `<st c="21792">insert_candidate_task()</st>` <st c="21815">will create a task that will convert the dictionary of objects to a</st> `<st c="21884">Candidate</st>` <st c="21893">instance and add the model instance to the DB through the</st> `<st c="21952">insert_candidate()</st>` <st c="21970">transaction of</st> `<st c="21986">CandidateRepository</st>`<st c="22005">. The following code showcases the complete implementation of this</st> <st c="22072">service task:</st>

from asyncio import create_task … … … … … … async def insert_candidate_task(data): async with db_session() as sess:async with sess.begin(): repo = CandidateRepository(sess) insert_task = create_task(repo.insert_candidate(Candidate(**data)))await insert_taskresult = insert_task.result() return result


			<st c="22389">Since our SQLAlchemy connection pooling is</st> `<st c="22433">NullPool</st>`<st c="22441">, which means connection pooling is disabled, we cannot utilize the same</st> `<st c="22514">AsyncSession</st>` <st c="22526">for all the</st> `<st c="22539">insert_candidate()</st>` <st c="22557">transactions.</st> <st c="22572">Otherwise,</st> `<st c="22583">gather()</st>` <st c="22591">will throw</st> `<st c="22603">RuntimeError</st>` <st c="22615">object.</st> <st c="22624">Thus, each</st> `<st c="22635">insert_candidate_task()</st>` <st c="22658">will open a new localized session for every</st> `<st c="22703">insert_candidate()</st>` <st c="22721">task execution.</st> <st c="22738">To add connection pooling, replace</st> `<st c="22773">NullPool</st>` <st c="22781">with</st> `<st c="22787">QueuePool</st>`<st c="22796">,</st> `<st c="22798">AsyncAdaptedQueuePool</st>`<st c="22819">,</st> <st c="22821">or</st> `<st c="22824">SingletonThreadPool</st>`<st c="22843">.</st>
			<st c="22844">Now, the</st> `<st c="22854">await</st>` <st c="22859">keyword will concurrently run the sequence of tasks registered in</st> `<st c="22926">gather()</st>` <st c="22934">and propagate all results in the resulting</st> `<st c="22978">tuple</st>` <st c="22983">of</st> `<st c="22987">Future</st>` <st c="22993">once these tasks have finished their execution successfully.</st> <st c="23055">The order of these</st> `<st c="23074">Future</st>` <st c="23080">objects is the same as the sequence of the awaitable objects provided in</st> `<st c="23154">gather()</st>`<st c="23162">. If a task has encountered failure or exception, it will not throw any exception and pre-empt the other task execution because</st> `<st c="23290">return_exceptions</st>` <st c="23307">of</st> `<st c="23311">gather()</st>` <st c="23319">is</st> `<st c="23323">False</st>`<st c="23328">. Instead, the failed task will join as a typical awaitable object in the</st> <st c="23402">resulting</st> `<st c="23412">tuple</st>`<st c="23417">.</st>
			<st c="23418">By the way, the</st> <st c="23434">given</st> `<st c="23441">add_list_candidates()</st>` <st c="23462">API function will return the number of successful INSERT tasks that persisted in the</st> <st c="23548">candidate profiles.</st>
			<st c="23567">The next section will discuss how to de-couple Flask components using the event-driven behavior of</st> <st c="23667">Flask</st> **<st c="23673">signals</st>**<st c="23680">.</st>
			<st c="23681">Utilizing asynchronous signal notifications</st>
			<st c="23725">Flask has a</st> <st c="23737">built-in lightweight event-driven mechanism called signals that can establish a loosely coupled software architecture using subscription-based event handling.</st> <st c="23897">It can trigger single or multiple transactions depending on the purpose.</st> <st c="23970">The</st> `<st c="23974">blinker</st>` <st c="23981">module provides the building blocks for Flask signal utilities, so install</st> `<st c="24057">blinker</st>` <st c="24064">using the</st> `<st c="24075">pip</st>` <st c="24078">command if it is not yet in the</st> <st c="24111">virtual environment.</st>
			<st c="24131">Flask has built-in signals and listens to many Flask events and callbacks such as</st> `<st c="24214">render_template()</st>`<st c="24231">,</st> `<st c="24233">before_request()</st>`<st c="24249">, and</st> `<st c="24255">after_request()</st>`<st c="24270">. These signals, such as</st> `<st c="24295">request_started</st>`<st c="24310">,</st> `<st c="24312">request_finished</st>`<st c="24328">,</st> `<st c="24330">message_flashed</st>`<st c="24345">, and</st> `<st c="24351">template_rendered</st>`<st c="24368">, are found in the</st> `<st c="24387">flask</st>` <st c="24392">module.</st> <st c="24401">For instance, once a component connects to</st> `<st c="24444">template_rendered</st>`<st c="24461">, it will run its callback method after</st> `<st c="24501">render_template()</st>` <st c="24518">finishes posting a Jinja template.</st> <st c="24554">However, our target is to create custom</st> *<st c="24594">asynchronous signals</st>*<st c="24614">.</st>
			<st c="24615">To create custom signals, import the</st> `<st c="24653">Namespace</st>` <st c="24662">class from the</st> `<st c="24678">flask.signals</st>` <st c="24691">module and instantiate it.</st> <st c="24719">Use its instance to define and instantiate specific custom signals, each having a unique name.</st> <st c="24814">The following is a snippet from our applications that creates an event signal for election date verification and another for retrieving all the</st> <st c="24958">election details:</st>

from flask.signals import Namespace

election_ns = Namespace() check_election = election_ns.signal('check_election') list_elections = election_ns.check_election_event,例如,它具有以下使用ElectionRepository验证选举日期的异步方法:

<st c="25411">@check_election.connect</st>
<st c="25435">async</st> def check_election_event(<st c="25467">app</st>, election_date):
    async with db_session() as sess:
        async with sess.begin():
            repo = ElectionRepository(sess)
            records = await repo.select_all_election()
            election_rec = [rec.to_json() for rec in records if rec.election_date == datetime.strptime(election_date, '%Y-%m-%d').date()]
            if len(election_rec) > 0:
                return True
            return False
        同时,我们的<st c="25798">`list_all_election()`</st> <st c="25814">API 端点具有以下</st> <st c="25833">`list_elections_event()`</st> <st c="25865">,它返回 JSON 格式的记录列表:</st> <st c="25922">:
<st c="25934">@list_elections.connect</st>
<st c="25958">async</st> def list_elections_event(app):
    async with db_session() as sess:
        async with sess.begin():
            repo = ElectionRepository(sess)
            records = await repo.select_all_election()
            election_rec = [rec.to_json() for rec in records]
            return election_rec
        <st c="26198">事件或</st> <st c="26207">信号函数必须接受一个</st> *<st c="26239">发送者</st> <st c="26245">或</st> *<st c="26249">监听器</st> <st c="26257">作为第一个局部参数,后面跟着其他对事件事务至关重要的自定义</st> `<st c="26326">args</st>` <st c="26330">对象。</st> <st c="26375">如果事件机制是类作用域的一部分,函数的值必须是</st> `<st c="26460">self</st>` <st c="26464">或类实例本身。</st> <st c="26495">否则,如果信号用于全局事件处理,其第一个参数必须是 Flask</st> `<st c="26589">app</st>` <st c="26592">实例。</st>

        信号有一个 `<st c="26618">connect()</st>` 函数或装饰器,用于注册事件或函数作为其实现。这些事件将在调用者发出信号时执行一次。Flask 组件可以通过调用信号的 `<st c="26827">send()</st>` 或 `<st c="26833">send_async()</st>` 工具函数,并传递事件函数参数来发出信号。以下 `<st c="26907">verify_election()</st>` 端点通过 `<st c="26965">check_election</st>` 信号从数据库检查特定日期是否发生了选举:
<st c="27031">@current_app.post('/ch05/election/verify')</st>
<st c="27074">async</st> def verify_election():
    election_json = request.get_json()
    election_date = election_json['election_date'] <st c="27186">result_tuple = await</st> <st c="27206">check_election.send_async(current_app,</st> <st c="27245">election_date=election_date)</st> isApproved = result_tuple[0][1]
    if isApproved:
        return jsonify(message=f'election for {election_date} is approved'), 201
    else:
        return jsonify(message=f'election for {election_date} is disabled'), 201
        如果事件函数是一个标准的 Python 函数,则通过信号 `<st c="27588">send()</st>` 方法发送其执行的通知。然而,如果它是一个异步方法,就像我们的情况一样,使用 `<st c="27667">send_async()</st>` 创建并运行协程的任务,并使用 `<st c="27730">await</st>` 提取其 `<st c="27751">Future</st>` 值。

        通常,信号可以采用可扩展应用程序中组件的解耦,以减少依赖关系并提高模块化和可维护性。这也有助于构建具有分布式架构设计的应用程序。然而,随着需求的复杂化和信号订阅者的数量增加,通知可能会降低整个应用程序的性能。因此,如果调用者和事件函数可以减少对彼此参数、返回值和条件的依赖,那么这是一个良好的设计。订阅者必须对事件函数有独立的范围。此外,创建一个灵活且目标不太狭窄的事件函数是一种良好的编程方法,这样许多 `<st c="28532">组件</st>` 可以订阅它。

        在探索了 Flask 如何使用其信号支持事件处理之后,现在让我们学习如何使用其平台创建后台进程。

        使用 Celery 和 Redis 构建后台任务

        <st c="28756">在 Flask 中使用其</st> <st c="28763">flask[async]</st> <st c="28804">平台</st> <st c="28807">创建后台进程</st> <st c="28823">或</st> <st c="28840">事务</st> <st c="28852">是不可能的。</st> <st c="28863">运行异步视图或端点的任务的事件循环不允许启动另一个事件循环来处理后台任务,因为它不能等待视图或端点完成其处理后再结束后台进程。</st> <st c="29131">然而,通过一些第三方组件,如任务队列,对于 Flask 平台来说,后台处理是可行的。</st>

        <st c="29252">其中一个解决方案是使用 Celery,它是一个异步任务队列,可以在应用程序上下文之外运行进程。</st> <st c="29391">因此,当事件循环正在运行视图或端点的协程时,它们可以将后台事务的管理委托给 Celery。</st>

        <st c="29533">设置 Celery 任务队列</st>

        <st c="29566">在用 Celery 编写后台进程时,有一些</st> <st c="29583">考虑因素,首先是使用</st> <st c="29681">celery</st> <st c="29687">扩展模块通过</st> <st c="29715">pip</st> <st c="29718">命令进行安装:</st>
 pip install celery
        <st c="29746">然后,我们在 WSGI 服务器中指定一些本地工作者来运行 Celery 队列中的后台任务,但在我们的应用程序中,我们的 Flask 服务器将只使用一个工作者来运行所有</st> <st c="29945">进程。</st>

        <st c="29959">现在让我们安装 Redis 服务器,它将作为消息代理为 Celery 服务。</st>

        <st c="30050">安装 Redis 数据库</st>

        <st c="30074">指定工作者后,Celery 需要一个消息代理来让工作者与客户端应用程序通信,以便运行后台任务。</st> <st c="30232">我们的应用程序使用 Redis 数据库作为代理。</st> <st c="30281">因此,在 Windows 上使用</st> <st c="30315">**<st c="30320">Windows Subsystem for Linux</st>** <st c="30347">(</st>**<st c="30349">WSL2</st>**<st c="30353">) shell</st> <st c="30347">或通过在</st> [<st c="30405">https://github.com/microsoftarchive/redis/releases</st>](https://github.com/microsoftarchive/redis/releases)<st c="30455">下载 Windows 安装程序</st> <st c="30402">来安装 Redis。</st>

        <st c="30456">下一步是向</st> <st c="30563">app</st> <st c="30566">实例</st> <st c="30563">添加必要的 Celery 配置变量,包括</st> `<st c="30537">CELERY_BROKER_URL</st>`<st c="30554">。</st>

        <st c="30576">设置 Celery 客户端配置</st>

        `<st c="30619">由于我们的项目使用</st>` `<st c="30642">TOML</st>` `<st c="30645">文件来设置配置环境变量,Celery 将从这些文件中作为 TOML 变量获取所有配置详细信息。</st> `<st c="30791">以下是对</st>` `<st c="30826">config_dev.toml</st>` `<st c="30841">文件的快照,该文件包含 Celery</st>` `<st c="30868">设置变量:</st>`
 CELERY_BROKER_URL = "redis://127.0.0.1:6379/0"
CELERY_RESULT_BACKEND = "redis://127.0.0.1:6379/0 <st c="30982">[CELERY]</st> celery_store_errors_even_if_ignored = true
task_create_missing_queues = true
task_store_errors_even_if_ignored = true
task_ignore_result = false
broker_connection_retry_on_startup = true
celery_task_serializer = "pickle"
celery_result_serializer = "pickle"
celery_event_serializer = "json"
celery_accept_content = ["pickle", "application/json", "application/x-python-serialize"]
celery_result_accept_content = ["pickle", "application/json", "application/x-python-serialize"]
        Celery 客户端模块需要的两个最重要的变量是 `<st c="31465">CELERY_BROKER_URL</st>` `<st c="31478">和</st> `<st c="31483">CELERY_RESULT_BACKEND</st>` `<st c="31505">,它们分别提供 Redis 代理和后端服务器的地址、端口和 DB 名称。</st> `<st c="31682">Redis 有 DB</st>` `<st c="31696">0</st>` `<st c="31697">到</st>` `<st c="31701">15</st>` `<st c="31703">,但我们的应用程序仅使用 DB</st>` `<st c="31742">0</st>` `<st c="31743">作为默认用途。</st> `<st c="31766">由于</st>` `<st c="31776">CELERY_RESULT_BACKEND</st>` `<st c="31797">在此配置中不是那么重要,因此将</st>` `<st c="31843">CELERY_RESULT_BACKEND</st>` `<st c="31864">设置为定义的代理 URL 或从配置中删除它是可接受的。</st>`

        首先,创建包含 Celery 实例在管理后台任务执行所需详细信息的 `<st c="31943">CELERY</st>` `<st c="31961">TOML</st>` 字典。<st c="32081">首先,</st> `<st c="32088">celery_store_errors_even_if_ignored</st>` `<st c="32123">和</st> `<st c="32128">task_store_errors_even_if_ignored</st>` `<st c="32161">必须</st> `<st c="32170">设置为</st>` `<st c="32174">True</st>` `<st c="32178">以启用 Celery 执行期间的错误审计跟踪功能。</st> `<st c="32250">broker_connection_retry_on_startup</st>` `<st c="32284">应该</st> `<st c="32295">设置为</st>` `<st c="32299">True</st>` `<st c="32303">以防 Redis 仍在关闭模式。</st> `<st c="32341">另一方面,</st>` `<st c="32360">task_ignore_result</st>` `<st c="32378">必须</st> `<st c="32387">设置为</st>` `<st c="32392">False</st>` `<st c="32396">因为我们的一些协程作业将返回一些值给调用者。</st> `<st c="32471">此外,</st>` `<st c="32481">task_create_missing_queues</st>` `<st c="32507">设置为</st>` `<st c="32518">True</st>` `<st c="32522">以防在流量期间有未定义的任务队列供应用程序使用。</st> `<st c="32612">顺便说一下,默认任务队列的名称</st>` `<st c="32654">是</st>` `<st c="32657">celery</st>` `<st c="32663">。</st>`

        <st c="32664">其他细节包括任务可以接受用于其协程的资源 mime-type(</st>`<st c="32760">celery_accept_content</st>`<st c="32782">)以及这些后台进程可以向调用者返回的返回值(</st>`<st c="32868">celery_result_accept_content</st>`<st c="32897">)。</st> <st c="32901">任务序列化器也是细节的一部分,因为它们是将任务的传入参数和返回</st> <st c="33039">值转换为可接受状态和有效</st> <st c="33089">mime-type 类型</st>的机制。

        <st c="33105">现在,让我们专注于构建我们项目的 Celery 客户端模块,从创建</st> <st c="33218">Celery 实例</st>开始。

        <st c="33234">创建客户端实例</st>

        <st c="33263">由于本章中的所有</st> <st c="33274">项目都使用应用程序工厂方法,识别应用程序作为 Celery 客户端的设置发生在</st> `<st c="33405">app/__init__.py</st>`<st c="33420">中。</st> 然而,确切的</st> `<st c="33441">Celery</st>` <st c="33447">类实例化发生在另一个模块中,</st> `<st c="33494">celery_config.py</st>`<st c="33510">,以避免循环导入错误。</st> <st c="33545">以下代码片段显示了在</st> `<st c="33614">celery_config.py</st>`<st c="33630">中创建</st> `<st c="33598">Celery</st>` <st c="33604">类的实例:</st>
<st c="33632">from celery import Celery, Task</st> from flask import Flask
def <st c="33692">celery_init_app</st>(app: Flask) -> Celery:
    class FlaskTask(Task):
        def __call__(self, *args: object, **kwargs: object) -> object: <st c="33818">with app.app_context():</st> return self.run(*args, **kwargs)
    celery_app = <st c="33888">Celery(app.name, task_cls=FlaskTask,</st> <st c="33924">broker=app.config["CELERY_BROKER_URL"],</st> <st c="33964">backend=app.config["CELERY_RESULT_BACKEND"])</st><st c="34009">celery_app.config_from_object(app.config["CELERY"])</st><st c="34061">celery_app.set_default()</st> return celery_app
        <st c="34104">从前面的代码片段中,创建</st> `<st c="34158">Celery</st>` <st c="34164">类的实例严格需要 Celery 应用程序名称,</st> `<st c="34218">CELERY_BROKER_URL</st>`<st c="34235">,以及工作任务。</st> <st c="34258">第一个参数,Celery 应用程序名称,可以有任何指定的名称,或者直接使用 Flask 应用程序的名称,因为 Celery 客户端模块将在应用程序的线程中运行后台作业(</st>`<st c="34423">FlaskTask</st>`<st c="34433">)。</st>

        <st c="34456">在</st> <st c="34462">实例化 Celery 之后,Celery 实例,</st> `<st c="34510">celery_app</st>`<st c="34520">,需要从 Flask</st> `<st c="34578">app</st>` <st c="34581">加载</st> `<st c="34540">CELERY</st>` <st c="34546">TOML 字典来配置任务队列及其消息代理。</st> <st c="34634">最后,</st> `<st c="34642">celery_app</st>` <st c="34652">必须调用</st> `<st c="34665">set_default()</st>` <st c="34678">来封闭配置。</st> <st c="34706">现在,</st> `<st c="34711">app/__init__.py</st>` <st c="34726">将导入</st> `<st c="34743">celery_init_app()</st>` <st c="34760">工厂,以最终从 Flask 应用程序中创建 Celery 客户端。</st>

        <st c="34853">现在,让我们使用自定义任务来构建 Celery 客户端模块。</st>

        <st c="34914">实现 Celery 任务</st>

        <st c="34944">To avoid circular</st> <st c="34963">import problems, it is not advisable to import</st> `<st c="35010">celery_app</st>` <st c="35020">and use it to decorate functions with the</st> `<st c="35063">task()</st>` <st c="35069">decorator.</st> <st c="35081">The</st> `<st c="35085">shared_task()</st>` <st c="35098">decorator from the</st> `<st c="35118">celery</st>` <st c="35124">module is enough proxy to define functions as Celery tasks.</st> <st c="35185">Here is a Celery task that adds a new vote to</st> <st c="35231">a candidate:</st>
<st c="35243">from celery import shared_task</st>
<st c="35274">from asyncio import run</st>
<st c="35298">@shared_task</st> def add_vote_task_wrapper(details): <st c="35348">async</st> def add_vote_task(details):
        try: <st c="35387">async</st> with db_session() as sess: <st c="35420">async</st> with sess.begin():
                repo = VoteRepository(sess)
                details_dict = loads(details)
                print(details_dict)
                election = Vote(**details_dict)
                result = await repo.insert(election)
                if result: <st c="35603">return str(True)</st> else: <st c="35626">return str(False)</st> except Exception as e:
            print(e) <st c="35676">return str(False)</st> return <st c="35776">add_vote_task_wrapper()</st>, must not be a coroutine. A Celery task is a class generated by any callable decorated by <st c="35890">@shared_task</st>, which means it cannot propagate the <st c="35940">await</st> keyword outwards with the <st c="35972">async</st> function call. However, it can enclose an asynchronous local method to handle all the operations asynchronously, such as <st c="36099">add_vote_task()</st>, which wraps and executes the INSERT transactions for new vote details. The Celery task can apply the <st c="36217">asyncio</st>’s <st c="36228">run()</st> utility method to run its async local function.
			<st c="36281">Since our Celery app does not ignore the result, our task returns a Boolean value converted into a string, a safe object type that a task can return to the caller.</st> <st c="36446">Although it is feasible to use pickling, through the</st> `<st c="36499">pickle</st>` <st c="36505">module, to pass an argument to or transport return values from Celery tasks to the callers, it might open vulnerabilities that can pose security risks to the application, such as accidentally exposing confidential information stored in the pickled object or unpickling/de-serializing</st> <st c="36790">malicious objects.</st>
			<st c="36808">Another approach to manage the Celery task’s input arguments and returned values, especially if they are collection types, is through the</st> `<st c="36947">loads()</st>` <st c="36954">and</st> `<st c="36959">dumps()</st>` <st c="36966">utilities of the</st> `<st c="36984">json</st>` <st c="36988">module.</st> <st c="36997">This</st> `<st c="37002">loads()</st>` <st c="37009">function deserializes a JSON string into a Python object while</st> `<st c="37073">dumps()</st>` <st c="37080">serializes Python objects (e.g., dictionaries, lists, etc.) into a JSON formatted string.</st> <st c="37171">However, sometimes, using</st> `<st c="37197">dumps()</st>` <st c="37204">to convert these objects to strings is not certain.</st> <st c="37257">There are data in the string payload that can cause serialization error, because Celery does not support their default format, such as</st> `<st c="37392">time</st>`<st c="37396">,</st> `<st c="37398">date</st>`<st c="37402">, and</st> `<st c="37408">datetime</st>`<st c="37416">. In this</st> <st c="37425">scenario, the</st> `<st c="37440">dumps()</st>` <st c="37447">method needs a custom serializer to convert these temporal data types to their equivalent</st> *<st c="37538">ISO 8601</st>* <st c="37546">formats.</st> <st c="37556">The following Celery task has the same problem, thus the presence of a</st> <st c="37627">custom</st> `<st c="37634">json_date_serializer()</st>`<st c="37656">:</st>

@shared_task def list_all_votes_task_wrapper():

async def list_all_votes_task():

async with db_session() as sess:

    async with sess.begin():

        repo = VoteRepository(sess)

        records = await repo.select_all_vote()

        vote_rec = [rec.to_json() for rec in records]

        return <st c="37917">dumps(vote_rec,</st> <st c="37932">default=json_date_serializer)</st> return <st c="37970">run(list_all_votes_task())</st>

def json_date_serializer(obj): if isinstance(obj, time):

    return obj.isoformat()

raise TypeError ("Type %s not …" % type(obj))

			<st c="38122">Among the many</st> <st c="38137">ways to implement a date serializer,</st> `<st c="38175">json_date_serializer()</st>` <st c="38197">uses the</st> `<st c="38207">time</st>`<st c="38211">’s</st> `<st c="38215">isoformat()</st>` <st c="38226">method to convert the time object to an</st> *<st c="38267">ISO 8601</st>* <st c="38275">or</st> *<st c="38279">HH:MM:SS:ssssss</st>* <st c="38294">formatted string value so that the task can return the list of vote records without conflicts on the</st> `<st c="38396">date</st>` <st c="38400">types.</st>
			<st c="38407">Running the Celery worker server</st>
			<st c="38440">After creating the</st> <st c="38460">Celery tasks, the next step is to run the built-in Celery server through the following command to check whether the server can</st> <st c="38587">recognize them:</st>

celery -A main.celery_app worker --loglevel=info -P solo


			`<st c="38659">main</st>` <st c="38664">in the command is the</st> `<st c="38687">main.py</st>` <st c="38694">module, and</st> `<st c="38707">celery_app</st>` <st c="38717">is the Celery instance found in the</st> `<st c="38754">main.py</st>` <st c="38761">module.</st> <st c="38770">The</st> `<st c="38774">loglevel</st>` <st c="38782">option creates a console logger for the server, and the</st> `<st c="38839">P</st>` <st c="38840">option indicates the</st> *<st c="38862">concurrency pool</st>*<st c="38878">, which is</st> `<st c="38889">solo</st>` <st c="38893">in the given command.</st> *<st c="38916">Figure 5</st>**<st c="38924">.1</st>* <st c="38926">shows the screen details after the</st> <st c="38962">server started.</st>
			![Figure 5.1 – Server details after Celery server startup](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_05_001.jpg)

			<st c="39751">Figure 5.1 – Server details after Celery server startup</st>
			<st c="39806">Celery server fetched the</st> `<st c="39833">add_vote_task_wrapper()</st>` <st c="39856">and</st> `<st c="39861">list_all_votes_task_wrapper()</st>` <st c="39890">tasks, as indicated in</st> *<st c="39914">Figure 5</st>**<st c="39922">.1</st>*<st c="39924">. Thus, Flask views and endpoints can now use these tasks to cast and view the votes from users.</st> <st c="40021">Aside from the list of ready-to-use tasks, the server logs also show details of the default task queue,</st> `<st c="40125">celery</st>`<st c="40131">. Also, it indicates the concurrency pool type, which is</st> `<st c="40188">solo</st>`<st c="40192">, and has a concurrency worker limit of</st> `<st c="40232">8</st>`<st c="40233">. Among the</st> `<st c="40245">prefork</st>`<st c="40252">,</st> `<st c="40254">eventlet</st>`<st c="40262">,</st> `<st c="40264">gevent</st>`<st c="40270">, and</st> `<st c="40276">solo</st>` <st c="40280">concurrency options, our applications use</st> `<st c="40323">solo</st>` <st c="40327">and</st> `<st c="40332">eventlet</st>`<st c="40340">. However, to use</st> `<st c="40358">eventlet</st>`<st c="40366">, install the</st> `<st c="40380">eventlet</st>` <st c="40388">module using the</st> `<st c="40406">pip</st>` <st c="40409">command:</st>

pip install eventlet


			<st c="40439">Our application uses the solo Celery execution pool because it runs within the worker process, which makes a task’s performance fast.</st> <st c="40574">This pool is fit for running resource-intensive tasks.</st> <st c="40629">Other better options are</st> `<st c="40654">eventlet</st>` <st c="40662">and</st> `<st c="40667">gevent</st>`<st c="40673">, which spawn greenlets, sometimes called green threads, cooperative threads, or coroutines.</st> <st c="40766">Most Input/Output-bound tasks run better with</st> `<st c="40812">eventlet</st>` <st c="40820">or</st> `<st c="40824">gevent</st>` <st c="40830">because they generate more threads and emulate a multi-threading environment</st> <st c="40908">for efficiency.</st>
			<st c="40923">Once the Celery</st> <st c="40940">server loads and recognizes the tasks with a worker managing the message queues, Flask view and endpoint functions can invoke the tasks now using Celery</st> <st c="41093">utility methods.</st>
			<st c="41109">Utilizing the Celery tasks</st>
			<st c="41136">Once the</st> <st c="41146">Celery worker server runs with the list of tasks, Flask’s</st> `<st c="41204">async</st>` <st c="41209">views and endpoints can now access and run these tasks like signals.</st> <st c="41279">These tasks will execute only when the caller invokes their built-in</st> `<st c="41348">delay()</st>` <st c="41355">or</st> `<st c="41359">apply_async()</st>` <st c="41372">methods.</st> <st c="41382">The following endpoint function runs</st> `<st c="41419">add_vote_task_wrapper()</st>` <st c="41442">to cast a vote for</st> <st c="41462">a user:</st>

@current_app.post('/ch05/vote/add')

async def add_vote():

vote_json = request.get_json() <st c="41559">vote_str = dumps(vote_json)</st><st c="41586">task =</st> <st c="41593">add_vote_task_wrapper.apply_async(args=[vote_str])</st><st c="41644">result = task.get()</st> return jsonify(message=result), 201

			<st c="41700">The given</st> `<st c="41711">add_vote()</st>` <st c="41721">endpoint retrieves the request JSON data and converts it to a string before passing it as an argument to</st> `<st c="41827">add_vote_task_wrapper()</st>`<st c="41850">. Without using the</st> `<st c="41870">await</st>` <st c="41875">keyword, the Celery task has</st> `<st c="41905">apply_async()</st>`<st c="41918">, which the invoker can use to trigger its execution with the argument.</st> `<st c="41990">apply_async()</st>` <st c="42003">returns an</st> `<st c="42015">AsyncResult</st>` <st c="42026">object with a</st> `<st c="42041">get()</st>` <st c="42046">method that returns the returned value, if any.</st> <st c="42095">It also has a</st> `<st c="42109">traceback</st>` <st c="42118">variable</st> <st c="42127">that retrieves an exception stack trace when the execution raises</st> <st c="42194">an exception.</st>
			<st c="42207">From creating asynchronous background tasks, let us move on to WebSocket implementation with</st> <st c="42301">asynchronous transactions.</st>
			<st c="42327">Building WebSockets with asynchronous transactions</st>
			<st c="42378">WebSocket is</st> <st c="42391">a well-known bi-directional communication between a server and browser-based clients.</st> <st c="42478">Many popular frameworks such as Spring, JSF, Jakarta EE, Django, FastAPI, Angular, and React support this technology, and Flask is one of them.</st> <st c="42622">However, this chapter will focus on implementing WebSocket and its client applications using the</st> <st c="42719">asynchronous paradigm.</st>
			<st c="42741">Creating the client-side application</st>
			<st c="42778">Our WebSocket implementation with the</st> <st c="42817">client-side application is in the</st> `<st c="42851">ch05-web</st>` <st c="42859">project.</st> <st c="42869">Calling</st> `<st c="42877">/ch05/votecount/add</st>` <st c="42896">from the</st> `<st c="42906">vote_count.py</st>` <st c="42919">view module will give us the following HTML form in</st> *<st c="42972">Figure 5</st>**<st c="42980">.2</st>*<st c="42982">, which handles the data entry for the final vote tally per precinct or</st> <st c="43054">election district:</st>
			![Figure 5.2 – Client-side application for adding final vote counts](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_05_002.jpg)

			<st c="43230">Figure 5.2 – Client-side application for adding final vote counts</st>
			<st c="43295">Our</st> <st c="43299">WebSocket captures election data from officers and then updates DB records in real time.</st> <st c="43389">It retrieves a string message from the server as a response.</st> <st c="43450">The HTML form and the</st> `<st c="43507">WebSocket</st>` <st c="43516">are in</st> `<st c="43524">pages/vote_count_add.html</st>` <st c="43549">of</st> `<st c="43553">ch05-web</st>`<st c="43561">. The following snippet is the JS code that communicates with our</st> <st c="43627">server-side</st> `<st c="43639">WebSocket</st>`<st c="43648">:</st>


			<st c="44681">The preceding JS script will connect to the Flask server through</st> `<st c="44747">ws://localhost:5001/ch05/vote/save/ws</st>` <st c="44784">by instantiating the</st> `<st c="44806">WebSocket</st>` <st c="44815">API.</st> <st c="44821">When the connection is ready, the client can ask for vote details from the client through the form components.</st> <st c="44932">Submitting the data will create a JSON object out of the form data before sending the JSON formatted details to the server through the</st> `<st c="45067">WebSocket</st>` <st c="45076">connection.</st>
			<st c="45088">On the other</st> <st c="45102">hand, to capture the message from the server, the client must create a listener to the message emitter by calling the WebSocket’s</st> `<st c="45232">addEventListener()</st>`<st c="45250">, which will watch and retrieve any JSON message from the Flask server.</st> <st c="45322">The custom</st> `<st c="45333">add_log()</st>` <st c="45342">function will render the message to the front end using the</st> `<st c="45403"><</st>``<st c="45404">span></st>` <st c="45409">tag.</st>
			<st c="45414">Next, let us focus on the WebSocket implementation per se using the</st> `<st c="45483">flask-sock</st>` <st c="45493">module.</st>
			<st c="45501">Creating server-side transactions</st>
			<st c="45535">There are many ways to implement a</st> <st c="45571">server-side message emitter, such as</st> `<st c="45608">WebSocket</st>`<st c="45617">, in Flask, and many Flask extensions can provide support for it, such as</st> `<st c="45691">flask-socketio</st>`<st c="45705">,</st> `<st c="45707">flask-sockets</st>`<st c="45720">, and</st> `<st c="45726">flask-sock</st>`<st c="45736">. This chapter will use the</st> `<st c="45764">flask-sock</st>` <st c="45774">module to create WebSocket routes because it can implement WebSocket communication with minimal configuration and setup.</st> <st c="45896">So, to start, install the</st> `<st c="45922">flask-sock</st>` <st c="45932">extension using the</st> `<st c="45953">pip</st>` <st c="45956">command:</st>

pip install flask-sock


			<st c="45988">Then, integrate the extension to Flask by instantiating the</st> `<st c="46049">Sock</st>` <st c="46053">class with the</st> `<st c="46069">app</st>` <st c="46072">instance as its required argument.</st> <st c="46108">The following</st> `<st c="46122">app/__init__.py</st>` <st c="46137">snippet shows the</st> `<st c="46156">flask-sock</st>` <st c="46166">setup:</st>

from flask_sock import Sock

sock = Sock() def create_app(config_file):

app = Flask(__name__, template_folder='../app/pages', static_folder="../app/resources")

app.config.from_file(config_file, toml.load)

在<st c="46468">/api/ votecount_websocket.py 模块</st>中初始化<st c="46451">sock</st>实例以定义 WebSocket 路由。 <st c="46536">ws://localhost:5001/ch05/vote/save/ws</st>,这是由前面的 JS 代码调用的,具有以下路由实现:
 from app import sock <st c="46680">@sock.route('/ch05/vote/save/ws')</st> def add_vote_count_server(<st c="46740">ws</st>):
    async def add_vote_count():
        while True:
            vote_count_json = ws.receive()
            vote_count_dict = loads(vote_count_json)
            async with db_session() as sess:
                repo = VoteCountRepository(sess)
                vote_count = VoteCount(**vote_count_dict)
                result = await repo.insert(vote_count)
                if result:
                    ws.send("data added")
                else:
                    ws.send("data not added")
    run(add_vote_count())
        <st c="47092">Sock</st> <st c="47101">实例有一个</st> `<st c="47117">route()</st>` <st c="47124">装饰器,用于定义 WebSocket 实现。</st> <st c="47174">WebSocket 路由函数或处理程序始终是非异步的,并需要一个接受从</st> `<st c="47310">Sock</st>`<st c="47314">注入的 WebSocket 对象的必需参数。</st> 这个<st c="47321">ws</st> <st c="47323">对象有一个<st c="47337">send()</st> <st c="47343">方法,用于向客户端应用程序发送数据,一个<st c="47396">receive()</st> <st c="47405">实用工具,用于接受来自客户端的消息,以及</st> `<st c="47457">close()</st>` <st c="47464">用于在运行时异常或与服务器相关的问题发生时强制断开双向通信。</st>

        <st c="47582">WebSocket 处理程序通常保持一个</st> *<st c="47622">开环过程</st> <st c="47639">,其中它可以首先通过</st> `<st c="47685">receive()</st>` <st c="47694">接收消息,然后使用</st> `<st c="47727">send()</st>` <st c="47733">连续地发出其消息,具体取决于消息的目的。</st>

        <st c="47790">在<st c="47806">add_vote_count_server()</st> <st c="47829">的情况下,它需要等待异步的</st> `<st c="47865">VoteCountRepository</st>` <st c="47884">的 INSERT 事务,WebSocket 路由函数内部必须存在一个类似于 Celery 任务的</st> `<st c="47911">async</st>` <st c="47916">本地方法。</st> <st c="48010">这个本地方法将封装异步操作,并且</st> `<st c="48077">asyncio</st>` <st c="48084">的</st> `<st c="48088">run()</st>` <st c="48093">将在路由函数内部执行它。</st>

        <st c="48136">现在,为了见证消息交换,</st> *<st c="48179">图 5</st>**<st c="48187">.3</st>* <st c="48189">显示了我们的 JS 客户端与运行时的</st> `<st c="48258">add_vote_count_server()</st>` <st c="48281">处理程序</st> `<st c="48290">之间的通信快照:</st>

        ![图 5.3 – 一个 JS 客户端与 flask-sock WebSocket 之间的消息交换](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_05_003.jpg)

        <st c="48668">图 5.3 – 一个 JS 客户端与 flask-sock WebSocket 之间的消息交换</st>

        <st c="48744">除了基于 Web 的客户端之外,WebSocket 还可以将数据传播或发送到</st> <st c="48820">API 客户端。</st>

        <st c="48832">创建 Flask API 客户端应用程序</st>

        <st c="48872">另一种通过 WebSocket 发射器连接的方式是通过 Flask 组件,而不是 JS 代码。</st> <st c="48888">有时,客户端应用程序不是由 HTML、CSS 和前端 JS 框架组成的支持 WebSocket 通信的 Web 组件。</st> <st c="49112">例如,在我们的</st> `<st c="49133">ch05-api</st>` <st c="49141">项目中,一个 POST API 函数</st> `<st c="49172">bulk_check_vote_count()</st>`<st c="49195">,要求列出候选人以计算他们在选举期间获得的选票。</st> <st c="49277">API 的输入是一个 JSON 字符串,如下所示</st> <st c="49338">样本数据:</st>
 [
    {
        "election_id": 1,
        "cand_id": "PHL-101"
    },
    {
        "election_id": 1,
        "cand_id": "PHL-111"
    },
    {
        "election_id": 1,
        "cand_id": "PHL-005"
    }
]
        <st c="49485">然后,API 函数将此 JSON 输入转换为包含候选人和选举 ID 的字典列表。</st> <st c="49603">以下是实现此 API 函数的代码,该函数作为 WebSocket 的客户端:</st> <st c="49678">客户端:</st>
<st c="49690">from simple_websocket import Client</st>
<st c="49726">from json import dumps</st>
<st c="49749">@current_app.post("/ch05/check/vote/counts/client")</st> def bulk_check_vote_count(): <st c="49831">ws =</st> <st c="49835">Client('ws://127.0.0.1:5000/ch05/check/vote/counts/ws',</st><st c="49891">headers={"Access-Control-Allow-Origin": "*"})</st><st c="49937">candidates = request.get_json()</st> for candidate in candidates:
            try:
                print(f'client sent: {candidate}') <st c="50039">ws.send(dumps(candidate))</st><st c="50064">vote_count = ws.receive()</st> print(f'client recieved: {vote_count}')
            except Exception as e:
                print(e)
    return jsonify(message="done client transaction"), 201
        <st c="50217">对于与</st> `<st c="50275">flask-sock</st>` <st c="50285">最兼容的 WebSocket 客户端扩展是</st> `<st c="50289">simple-websocket</st>`<st c="50305">,请使用以下</st> `<st c="50337">pip</st>` <st c="50340">命令安装此模块:</st>
 pip install simple-websocket
        <st c="50378">从</st> `<st c="50417">simple-websocket</st>` <st c="50433">模块实例化</st> `<st c="50495">Client</st>` <st c="50401">类</st> <st c="50407">以连接到具有</st> `<st c="50459">flask-sock</st>` <st c="50469">WebSocket 发射器,并使用</st> `<st c="50493">Access-Control-Allow-Origin</st>` <st c="50520">允许跨域访问。</st> <st c="50551">然后,API 将通过</st> `<st c="50643">Client</st>`<st c="50649">的</st> `<st c="50653">send()</st>` <st c="50659">方法将转换为字符串的字典详情发送到发射器。</st>

        <st c="50667">另一方面,将接收来自</st> `<st c="50755">bulk_check_vote_count()</st>` <st c="50778">客户端 API 的选举详情的 WebSocket 路由具有以下实现:</st>
<st c="50823">@sock.route("/ch05/check/vote/counts/ws")</st> def bulk_check_vote_count_ws(<st c="50895">websocket</st>): <st c="50909">async</st> def vote_count():
      While True:
        try: <st c="50950">candidate = websocket.receive()</st> candidate_map = loads(candidate)
          print(f'server received: {candidate_map}')
          async with db_session() as sess:
            async with sess.begin():
               repo = VoteRepository(sess) <st c="51144">count = await repo.count_votes_by_candidate(</st> <st c="51188">candidate_map["cand_id"],</st> <st c="51214">int(candidate_map["election_id"]))</st> vote_count_data = {"cand_id": candidate_map["cand_id"], "vote_count": count} <st c="51327">websocket.send(dumps(vote_count_data))</st> print(f'server sent: {candidate_map}')
        except  Exception as e:
          print(e)
          break <st c="51527">run()</st> from <st c="51538">asyncio</st> to execute asynchronous query transactions from <st c="51594">VoteRepository</st> and extract the total number of votes for each candidate sent by the API client. The emitter will send a newly formed dictionary containing the candidate’s ID and counted votes back to the client API in string format. So, the handshake in this setup is between two Flask components, the WebSocket route and an async Flask API.
			<st c="51935">There are other client-server interactions that</st> `<st c="51984">flask[async]</st>` <st c="51996">can build, and one of these is</st> <st c="52028">the SSE.</st>
			<st c="52036">Implementing asynchronous SSE</st>
			<st c="52066">Like the</st> <st c="52075">WebSocket, the SSE is a real-time mechanism for sending messages from the server to client applications.</st> <st c="52181">However, unlike the WebSocket, it establishes unidirectional communication between the server and</st> <st c="52279">client applications.</st>
			<st c="52299">There are many ways to build server push solutions in Flask, but our applications prefer using the built-in</st> <st c="52408">response’s</st> `<st c="52419">text/event-stream</st>`<st c="52436">.</st>
			<st c="52437">Implementing the message publisher</st>
			<st c="52472">SSE is a</st> *<st c="52482">server push</st>* <st c="52493">solution</st> <st c="52503">that requires an input source where it can listen for incoming data or messages in real time and push that data to its client applications.</st> <st c="52643">One of the reliable sources that will work with SSE is</st> <st c="52698">a</st> **<st c="52700">message broker</st>**<st c="52714">, which can store messages from various resources.</st> <st c="52765">It can also help the SSE generator function to listen for incoming messages before yielding them to</st> <st c="52865">the clients.</st>
			<st c="52877">In this chapter, our</st> `<st c="52899">ch05-web</st>` <st c="52907">application utilizes Redis as the broker, which our</st> `<st c="52960">ch05-api</st>` <st c="52968">project used for invoking the Celery background tasks.</st> <st c="53024">However, in this scenario, there is a need to create a Redis client application that will implement its publisher-subscribe pattern.</st> <st c="53157">So, install the</st> *<st c="53173">redis-py</st>* <st c="53181">extension by using the</st> `<st c="53205">pip</st>` <st c="53208">command:</st>

pip install redis


			<st c="53235">This extension will provide us with the</st> `<st c="53276">Redis</st>` <st c="53281">client that will connect to the Redis server once instantiated in the</st> `<st c="53352">main</st>` <st c="53356">module.</st> <st c="53365">The following</st> `<st c="53379">main.py</st>` <st c="53386">snippet shows the setup of the Redis</st> <st c="53424">client application:</st>

from app import create_app from redis import Redis app = create_app('../config_dev.toml') redis_conn = Redis(db = 0,host='127.0.0.1',port=6379,decode_responses=True )


			<st c="53614">The Redis callable requires details about the DB (</st>`<st c="53664">db</st>`<st c="53667">), port, and host address of the installed Redis server as its parameters for setup.</st> <st c="53753">Since Celery tasks can return bytes, the</st> `<st c="53794">Redis</st>` <st c="53799">constructor should set its</st> `<st c="53827">decode_response</st>` <st c="53842">parameter to</st> `<st c="53856">True</st>` <st c="53860">to enable binary message data decoding mechanism and receive decoded strings.</st> <st c="53939">The instance,</st> `<st c="53953">redis_conn</st>`<st c="53963">, will be the key to the message publisher implementation needed by the SSE.</st> <st c="54040">In the complaint module of the application, our input source is a form view function that requests the user its statement and voter’s ID before pushing these details to the Redis</st> <st c="54219">broker.</st> <st c="54227">The following is the view that publishes data to the</st> <st c="54280">Redis server:</st>

from main import redis_conn from json import dumps @current_app.route('/ch05/election/complaint/form', methods = ['GET','POST'])

async def create_complaint():

if request.method == "GET":

    return render_template('complaint_form.html')

else:

    voter_id = request.form['voter_id']

    complaint = request.form['complaint']

    record = {'voter_id': voter_id, 'complaint': complaint} <st c="54663">redis_conn.publish("complaint_channel",</st> <st c="54702">dumps(record))</st> return render_template('complaint_form.html')

			<st c="54763">The</st> `<st c="54768">Redis</st>` <st c="54773">client</st> <st c="54780">instance,</st> `<st c="54791">redis_conn</st>`<st c="54801">, has a</st> `<st c="54809">publish()</st>` <st c="54818">method that stores a message to Redis under a specific topic or channel, a point where a subscriber will fetch the message from the broker.</st> <st c="54959">The name of our Redis channel</st> <st c="54989">is</st> `<st c="54992">complaint_channel</st>`<st c="55009">.</st>
			<st c="55010">Building the server push</st>
			<st c="55035">Our SSE will be</st> <st c="55051">the subscriber to</st> `<st c="55070">complaint_channel</st>`<st c="55087">. It will create a subscriber object first, through</st> `<st c="55139">redis_conn</st>`<st c="55149">’s</st> `<st c="55153">pubsub()</st>` <st c="55161">method, to connect to Redis and eventually use the broker to listen for any published message from the form view.</st> <st c="55276">The following is our SSE implementation using the</st> `<st c="55326">async</st>` <st c="55331">Flask route:</st>

@current_app.route('/ch05/elec/comaplaint/stream')

async def elec_complaint_sse():

def process_complaint_event(): <st c="55459">connection = redis_conn.pubsub()</st><st c="55491">connection.subscribe('complaint_channel')</st> for message in <st c="55549">connection.listen()</st>:

        time.sleep(1)

        if message is not None and message['type'] == 'message': <st c="55642">data = message['data']</st><st c="55664">yield 'data: %s\n\n' % data</st> return <st c="55765">process_complaint_event()</st> 在给定的 SSE 路由中是 *<st c="55822">生成器函数</st>*,它创建订阅者对象 (<st c="55877">connection</st>),通过调用 <st c="55926">subscribe()</st> 方法连接到 Redis,并构建一个开放循环事务,该事务将连续从代理监听当前发布的消息。它从订阅者对象的 <st c="56096">listen()</st> 实用程序检索到的消息是一个包含有关消息类型、通道和表单视图发布者发布的 <st c="56215">数据</st> 的 JSON 实体。<st c="56258">elec_complaint_sse()</st> 只需要产生消息的 <st c="56303">数据</st> 部分。现在,运行 <st c="56349">process_complaint_event()</st> 生成器需要 SSE 路由返回 Flask 的 <st c="56426">Response</st>,这将执行并渲染它为一个 <st c="56474">text/event-stream</st> 类型的对象。*<st c="56505">图 5</st>**<st c="56513">.4</st>* 展示了为投票者提供投诉的表单视图:

        ![图 5.4 – 从 Redis 发布的数据的投诉表单视图](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_05_004.jpg)

        <st c="56748">图 5.4 – 发布到 Redis 的数据投诉表单视图</st>

        *<st c="56813">图 5</st>**<st c="56822">.5</st>* 提供了包含从 Redis 代理推送的消息的 SSE 客户端页面的快照。

        ![图 5.5 – 从 Redis 推送数据渲染的 SSE 客户端页面](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_05_005.jpg)

        <st c="57234">图 5.5 – 从 Redis 推送数据渲染的 SSE 客户端页面</st>

        <st c="57298">除了代理消息之外,Flask 还支持其他库,这些库在创建其组件时使用发布者-订阅者设计模式。<st c="57433">下一个主题将展示其中之一,即</st> `<st c="57481">reactivex</st> <st c="57490">模块。</st>`

        <st c="57498">使用 RxPy 进行响应式编程</st>

        **<st c="57538">响应式编程</st>** <st c="57559">是当今兴起的一种流行编程范式之一,它侧重于异步数据流和可以管理执行、事件、存储库和异常传播的操作。<st c="57757">它利用发布者-订阅者编程方法,该方法在软件组件和事务之间建立异步交互。</st> <st c="57883">它支持 Flask 的其他库,这些库在创建其组件时使用发布者-订阅者设计模式。</st>

        <st c="57900">The</st> <st c="57904">library used to apply reactive streams to build services transactions and API functions in this chapter is</st> `<st c="58012">reactivex</st>`<st c="58021">, so install the module using the</st> `<st c="58055">pip</st>` <st c="58058">command:</st>
 pip install reactivex
        <st c="58089">The</st> `<st c="58094">reactivex</st>` <st c="58103">module has an</st> `<st c="58118">Observable</st>` <st c="58128">class that generates data sources for the subscribers to consume.</st> `<st c="58195">Observer</st>` <st c="58203">is another API class that pertains to the subscriber entities.</st> `<st c="58267">reactivex</st>` <st c="58276">will not be a complete reactive programming library without its</st> *<st c="58341">operators</st>*<st c="58350">. The following is a vote-counting service implementation that uses the</st> `<st c="58422">reactivex</st>` <st c="58431">utilities:</st>
<st c="58442">from reactivex import Observable, Observer, create</st>
<st c="58493">from reactivex.disposable import Disposable</st>
<st c="58537">from asyncio import ensure_future</st>
<st c="58571">async</st> def extract_precinct_tally(rec_dict):
    del rec_dict['id']
    del rec_dict['election_id']
    del rec_dict['approved_date']
    return str(rec_dict) <st c="58714">async</st> def create_tally_data(<st c="58742">observer</st>):
    async with db_session() as sess:
          async with sess.begin():
            repo = VoteCountRepository(sess)
            records = await repo.select_all_votecount()
            votecount_rec = [rec.to_json() for rec in records]
            print(votecount_rec)
            for vc in votecount_rec:
                rec_str = await extract_precinct_tally(vc) <st c="59030">observer.on_next(rec_str)</st><st c="59055">observer.on_completed()</st>
<st c="59388">create_tally_data()</st> and <st c="59412">extract_precinct_tally()</st> service operations that utilize these <st c="59475">async</st> queries also asynchronous. The objective is not to call these <st c="59543">async</st> services directly from the API layer but to wrap these service transactions in one <st c="59632">Observable</st> object through <st c="59658">create_observable()</st> and let the API functions subscribe to it. However, the problem is that <st c="59750">create_observable()</st> can’t be <st c="59779">async</st> because <st c="59793">reactivex</st> does not allow <st c="59818">async</st> to deal with its operators such as <st c="59859">create()</st>, <st c="59869">from_iterable()</st>, and <st c="59890">from_list()</st>.
			<st c="59902">With that, the</st> `<st c="59918">create_observable()</st>` <st c="59937">custom function needs a local-scoped subscriber function,</st> `<st c="59996">on_subscribe()</st>`<st c="60010">, that will invoke</st> `<st c="60029">create_task()</st>` <st c="60042">or</st> `<st c="60046">ensure_future()</st>` <st c="60061">with an event loop to create a task for the</st> `<st c="60106">create_tally_data()</st>` <st c="60125">coroutine and return it as a</st> `<st c="60155">Disposable</st>` <st c="60165">resource object.</st> <st c="60183">A disposable resource link allows for the cleaning up of the resources used by the observable operators during the subscription.</st> <st c="60312">Creating the</st> `<st c="60325">async</st>` <st c="60330">subscriber disposable will help manage the</st> <st c="60374">Flask resources.</st>
			<st c="60390">In connection with this setup,</st> `<st c="60422">create_tally_data()</st>` <st c="60441">will now emit the vote counts from the repository to the observer or subscriber.</st> <st c="60523">The only goal now of</st> `<st c="60544">create_observable()</st>` <st c="60563">is to return its created</st> `<st c="60589">Observable</st>` <st c="60599">based on the</st> `<st c="60613">on_subscribe()</st>` <st c="60627">emissions.</st>
			<st c="60638">The API transaction needs</st> <st c="60665">to run the</st> `<st c="60676">create_tally_date()</st>` <st c="60695">service and extract all the emitted vote counts by invoking</st> `<st c="60756">create_observable()</st>` <st c="60775">and subscribing to its returned</st> `<st c="60808">Observable</st>` <st c="60818">through the</st> `<st c="60831">subscribe()</st>` <st c="60842">method.</st> <st c="60851">The following is the</st> `<st c="60872">list_votecount_tally()</st>` <st c="60894">endpoint function that creates a subscription to the</st> <st c="60948">returned</st> `<st c="60957">Observable</st>`<st c="60967">:</st>

from app.services.vote_count import create_observable

from asyncio import get_event_loop, Future @current_app.get("/ch05/votecount/tally") async def list_votecount_tally():

finished = Future() <st c="61163">loop = get_event_loop()</st> def on_completed():

    finished.set_result(0)

tally = [] <st c="61241">disposable</st> = <st c="61254">create_observable(loop).subscribe(</st><st c="61288">on_next = lambda i: tally.append(i),</st><st c="61325">on_error = lambda e: print("Error</st> <st c="61359">Occurred: {0}".format(e)),</st><st c="61386">on_completed = on_completed)</st><st c="61415">await finished</st><st c="61430">disposable.dispose()</st> return jsonify(tally=tally), 201

			`<st c="61484">subscribe()</st>` <st c="61496">has three</st> <st c="61506">callback methods that are all active and ready to run anytime</st> <st c="61569">when triggered:</st>

				*   `<st c="61584">on_next()</st>`<st c="61594">: This executes when</st> `<st c="61616">Observer</st>` <st c="61624">receives</st> <st c="61634">emitted data.</st>
				*   `<st c="61647">on_error()</st>`<st c="61658">: This executes when</st> `<st c="61680">Observable</st>` <st c="61690">encounters an exception along</st> <st c="61721">its operators.</st>
				*   `<st c="61735">on_completed()</st>`<st c="61750">: This runs when</st> `<st c="61768">Observable</st>` <st c="61778">completes</st> <st c="61789">its task.</st>

			<st c="61798">Our</st> `<st c="61803">on_next()</st>` <st c="61812">callback adds all the emitted data to the</st> <st c="61855">tally list.</st>
			<st c="61866">Now, the execution of the</st> `<st c="61893">Observable</st>` <st c="61903">operations will not be possible without the event loop.</st> <st c="61960">The API function needs the currently running event loop for the</st> `<st c="62024">create_tally_data()</st>` <st c="62043">coroutine execution, and thus its</st> `<st c="62078">get_event_loop()</st>` <st c="62094">invocation.</st> <st c="62107">The API will return the tally list once it disposes of the task running</st> <st c="62179">in</st> `<st c="62182">Observable</st>`<st c="62192">.</st>
			<st c="62193">Even though our framework is asynchronous Flask or the solutions applied to our applications are reactive and asynchronous, Flask will remain a WSGI-based framework, unlike FastAPI.</st> <st c="62376">The platform is still not 100% asynchronous friendly.</st> <st c="62430">However, if the application requires a 100% Flask environment, replace Flask with one of its variations called the</st> *<st c="62545">Quart</st>* <st c="62550">framework.</st>
			<st c="62561">Choosing Quart over Flask 2.x</st>
			<st c="62591">Quart</st> <st c="62597">is a Flask framework in and out but with a platform that runs entirely on</st> `<st c="62672">asyncio</st>`<st c="62679">. Many of the core features from Flask are part of the Quart framework, except for the main application class.</st> <st c="62790">The framework has its</st> `<st c="62812">Quart</st>` <st c="62817">class to set up</st> <st c="62834">an application.</st>
			<st c="62849">Moreover, Quart supports the</st> `<st c="63016">hypercorn</st>` <st c="63025">server, which supports HTTP/2</st> <st c="63056">request-response transactions.</st>
			<st c="63086">Since Quart and</st> <st c="63102">Flask are almost the same, migration of Flask applications to Quart is seamless and straightforward.</st> `<st c="63204">ch05-quart</st>` <st c="63214">is a product of migrating our</st> `<st c="63245">ch05-web</st>` <st c="63253">and</st> `<st c="63258">ch05-api</st>` <st c="63266">projects into using the Quart platform.</st> <st c="63307">The following is the</st> `<st c="63328">app/__init__.py</st>` <st c="63343">configuration of</st> <st c="63361">that project:</st>

from quart import Quart import toml

from app.model.config import init_db

from app.api.home import home, welcome

from app.api.login import add_login, list_all_login

def create_app(config_file): app = Quart(name, template_folder='../app/pages', static_folder="../app/resources")app.config.from_file(config_file, toml.load) init_db()

app.<st c="63715">add_url_rule</st>('/ch05/home', view_func=home, endpoint='home')

app.<st c="63781">add_url_rule</st>('/ch05/welcome', view_func=welcome, endpoint='welcome')

app.<st c="63856">add_url_rule</st>('/ch05/login/add', view_func=add_login, endpoint='add_login')

app.<st c="63937">add_url_rule</st>('/ch05/login/list/all', view_func=list_all_login, endpoint='list_all_login')

return app

			<st c="64039">The</st> <st c="64044">Quart framework has a</st> `<st c="64066">Quart</st>` <st c="64071">class to build the application.</st> <st c="64104">Its constructor parameters, such as</st> `<st c="64140">template_folder</st>` <st c="64155">and</st> `<st c="64160">static_folder</st>`<st c="64173">, are the same as those of Flask.</st> <st c="64207">The framework can also recognize TOML</st> <st c="64245">configuration files.</st>
			<st c="64265">On the repository layer, the framework has a</st> `<st c="64311">quart-sqlalchemy</st>` <st c="64327">extension module that supports asynchronous ORM operations for Quart applications.</st> <st c="64411">There is no need to rewrite the model and repository classes during the migration because all the helper classes and utilities are the same as the</st> `<st c="64558">flask-sqlalchemy</st>` <st c="64574">extension.</st> <st c="64586">The same</st> `<st c="64595">init_db()</st>` <st c="64604">from the project’s application factory will set up and load the helper functions, methods, and model classes of the</st> `<st c="64721">quart-sqlalchemy</st>` <st c="64737">ORM.</st>
			<st c="64742">Quart also supports blueprint, application factory design, or even the hybrid approach in building the application.</st> <st c="64859">However, the current version,</st> *<st c="64889">Quart 0.18.4</st>*<st c="64901">, does not have an easy way to manage the asynchronous request context so that modules inside the application can access the</st> `<st c="65026">current_app</st>` <st c="65037">proxy for view or API implementation.</st> <st c="65076">That’s why, from the given configuration, the views and endpoints can be defined inside</st> `<st c="65164">create_app()</st>` <st c="65176">using</st> `<st c="65183">add_url_rule()</st>`<st c="65197">. Decorating them with</st> `<st c="65220">route()</st>` <st c="65227">in their respective module script using the</st> `<st c="65272">app</st>` <st c="65275">object or</st> `<st c="65286">current_app</st>` <st c="65297">raises an exception.</st> <st c="65319">Now, the following are the view and endpoint implementations in the</st> <st c="65387">Quart platform:</st>

from quart import jsonify, render_template, request, make_response

async def add_login(): async with db_session() as sess:

        repo = LoginRepository(sess)

        login_json = request.get_json()

        login = Login(**login_json)

        result = await repo.insert(login)

        if result: <st c="65660">content = jsonify(login_json)</st><st c="65689">return await make_response(content, 201)</st> else:

            content = jsonify(message="insert complaint details record encountered a problem") <st c="65820">return await make_response(content, 500)</st>

async def welcome(): render_template()make_response() 在 Quart 中需要使用 await 关键字。另一个区别是 Quart 使用 hypercorn 来运行其应用程序而不是 Werkzeug 服务器。因此,使用 hypercorn 命令安装 pip

 pip install hypercorn
        <st c="66194">然后,使用</st> `<st c="66230">hypercorn</st>` `<st c="66240">main:app</st>` <st c="66248">命令运行应用程序。</st>

        <st c="66257">到目前为止,在</st> <st c="66269">一般情况下,Quart 已经是一个有前途的异步框架。</st> <st c="66329">让我们希望与创建者、支持组和爱好者的合作能够帮助在不久的将来升级和扩展这个框架。</st>

        <st c="66472">摘要</st>

        <st c="66480">Flask 2.2 现在与其他支持并利用异步解决方案来提高应用程序运行性能的框架相当。</st> <st c="66629">它的视图和 API 函数现在可以是</st> `<st c="66667">async</st>` <st c="66672">并且可以在 Flask 创建的事件循环上运行。</st> <st c="66721">异步服务和事务现在可以在 Flask 平台上作为由</st> `<st c="66834">create_task()</st>` <st c="66847">和</st> `<st c="66852">ensure_future()</st>`<st c="66867">创建的任务执行和等待。</st>

        <st c="66868">最新的</st> *<st c="66880">SQLAlchemy[async]</st>* <st c="66897">可以轻松集成到 Flask 应用程序中,以提供异步 CRUD 事务。</st> <st c="66989">此外,使用 Flask 2.2 创建异步任务以分解 Celery 后台进程中的阻塞事务序列、WebSocket 消息传递和可观察操作现在成为可能。</st>

        <st c="67186">此外,通过内置的</st> <st c="67363">异步信号</st>,现在可以使用 Flask 2.2 设计松散耦合的组件、应用程序范围的跨切关注点解决方案和一些分布式设置。</st>

        <st c="67384">甚至有一个 100%异步的 Flask 框架叫做 Quart,可以构建快速响应的</st> <st c="67479">请求-响应事务。</st>

        <st c="67509">尽管 Flask 中异步支持的目的在于性能,但它在我们的应用程序中可以成为一部分的边界仍然存在。</st> <st c="67654">当与 <st c="67733">asyncio</st> <st c="67740">一起使用时,某些组件或实用程序将降低其运行时间。</st> <st c="67873">其他,如 CRUD 操作,由于数据库规范不符合异步设置,将减慢数据库访问速度。</st> <st c="67988">因此,异步编程的效果仍然取决于项目的需求和应用程序使用的资源。</st>

        <st c="68005">下一章将带我们进入 Flask 的计算世界,它涉及</st> `<st c="68091">numpy</st>`<st c="68096">,</st> `<st c="68098">pandas</st>`<st c="68104">, 图表,统计,文件序列化以及其他 Flask</st> <st c="68197">可以提供的科学解决方案。</st>




第七章:6

开发计算和科学应用

计算科学家总是选择易于使用、有效和准确的基于 GUI 的应用程序来进行他们的发现、分析、综合、数据挖掘和数值计算,以节省时间和精力,为他们的研究得出结论。 尽管市场上提供了强大的计算工具,如 Maple、Matlab、MathCAD 和 Mathematica,但科学家仍然更喜欢可以提供进一步定制的机制,以便应用他们所需的精度、准确性和校准到他们的数学和统计模型。 换句话说,他们仍然更喜欢可以与他们的实验室设置 和参数相匹配的定制应用程序。

由于最高优先级是提供科学家在给定无限数据的情况下准确的结果,因此选择什么应用程序框架来构建适合他们需求的可扩展、实时和快速模块始终是一个挑战。 最终要求是创建和运行异步事务以执行复杂的数值算法,这是异步 Flask 可以提供的。

Flask 有异步组件,可以为科学家构建复杂、快速和实时的应用程序。 由于其灵活性、异步特性和广泛的支持,这个框架提供了完整的构建模块,可以为科学家提供定制的 科学软件。

本章将介绍 Flask[async] 可以提供的以下计算构建模块:

  • 上传 逗号分隔值 (CSV) 和 Microsoft Excel 工作表 (XLSX) 文档 用于计算

  • 实现符号计算 与可视化

  • 使用 <st c="1691">pandas</st> 模块进行数据和 图形分析

  • 创建和渲染 LaTeX 文档

  • 使用 前端库 构建图形图表

  • 使用 WebSocket 和 服务器端事件 (SSE) (SSE**)

  • 使用异步后台任务进行 资源密集型计算

  • 将 Julia 包与 Flask 集成

技术要求

本章将重点介绍一个 在线房价预测与分析 软件原型,该原型预计将在许多科学应用中出现。 首先,它具有简单而正式的图形用户界面,通过表单捕获用户数据。 使用的表单将要求输入公式、变量值和常数,并具有提供图形图表的能力,无论是实时还是计算后立即显示。 其次,它是一个可以在团队或组织中访问的 Web 应用程序。 最后,该应用程序可以使用 Flask 平台异步运行高度计算的任务。

本章中使用的测试数据来自 https://www.kaggle.com/datasets/yasserh/housing-prices-dataset https://data.world/finance/international-house-price-database。另一方面,本项目采用 <st c="2871">蓝图</st> 方法来管理模块和组件。 所有文件均可在 以下链接 https://github.com/PacktPublishing/Mastering-Flask-Web-Development/tree/main/ch06找到。

上传 CSV 和 XLSX 文档进行计算

该应用程序 将处理 包含影响全球房价的数值数据的 XLSX 和 CSV 文件,例如每个国家的周期性实际和 名义 房价指数 (HPI) 以及客户的 名义和实际 个人可支配收入 (PDI)。 此外,一些文档将展示诸如房屋面积、装修状况、主要道路偏好以及卧室和浴室数量等因素如何影响一个国家的房价。 我们的应用程序将把这些文档上传到服务器进行 数据分析。

Flask 通过一个具有 <st c="3726"><form></st> <st c="3738">enctype</st> <st c="3749">multipart/form-data</st>的 HTML <st c="3726"><form></st> 表单支持单个或多个文件的上传过程。它将所有上传的文件存储在 <st c="3806">request.files</st> 字典中,作为 <st c="3834">FileStorage</st> 实例。 <st c="3857">FileStorage</st> 是 Flask 使用的 Werkzeug 模块中的一个薄包装类,用于表示传入的文件。 <st c="3963">以下是一个 HTML 脚本,它使用</st> pandas` 模块上传 XLSX 文档以进行数据分析:

 <!DOCTYPE html>
<html lang="en">
  … … … … … …
  <body>
    <h1>Data Analysis … Actual House Price Index (HPI)</h1>
    <form action="{{request.path}}" method="POST" <st c="4221">enctype="multipart/form-data"</st>>
        Upload XLSX file: <st c="4271"><input type="file" name="data_file"/></st><br/>
        <input type="submit" value="Upload File"/>
    </form>
  </body><br/> <st c="4379">{%if df_table == None %}</st><st c="4403"><p>No analysis.</p></st><st c="4423">{% else %}</st><st c="4434">{{ table | safe}}</st><st c="4452">{% endif %}</st> </html>

以下代码片段显示了 <st c="4505">view</st> 函数的实现,该函数渲染给定的页面并接受 <st c="4577"> incoming</st> XLSX 文档:

 from modules.upload import upload_bp
from flask import render_template, request, current_app <st c="4695">from werkzeug.utils import secure_filename</st>
<st c="4737">from werkzeug.datastructures import FileStorage</st> import os <st c="4796">from pandas import read_excel</st>
<st c="4825">from exceptions.custom import (NoneFilenameException, InvalidTypeException, MissingFileException,</st> <st c="4923">FileSavingException)</st> @upload_bp.route('/upload/xlsx/analysis', methods = ["GET", "POST"])
async def show_analysis():
    if request.method == 'GET':
        df_tbl = None
    else:
        uploaded_file:FileStorage = request.files['data_file']
        filename = secure_filename(uploaded_file.filename) <st c="5195">if filename == '':</st><st c="5213">raise NoneFilenameException()</st> file_ext = os.path.splitext(filename)[1]
        if file_ext not in current_app.config['UPLOAD_FILE_TYPES']: <st c="5345">raise InvalidTypeException()</st> if  uploaded_file.filename == '' or uploaded_file == None: <st c="5432">raise MissingFileException()</st> try:
            df_xlsx = read_excel(uploaded_file, sheet_name=2, skiprows=[1])
            df_tbl = df_xlsx.loc[: , 'Australia':'US'].describe().to_html()
        except: <st c="5602">raise FileSavingException()</st> return render_template("file_upload_pandas_xlsx.html", table=df_tbl), 200

与任何表单参数一样,视图函数通过表单字段的名称从 <st c="5777">request.files</st> 访问文件对象。 文件对象,封装在一个 <st c="5857">FileStorage</st> 包装器中,提供了以下属性:

  • <st c="5912">filename</st>: 这提供了文件对象的原始文件名。

  • <st c="5974">stream</st>: 这提供了输入流对象,该对象可以发出输入/输出方法,例如 <st c="6062">read()</st>, <st c="6070">write()</st>, <st c="6079">readline()</st>, <st c="6091">writelines()</st>, <st c="6109">seek()</st>.

  • <st c="6116">headers</st>: 这包含文件的 <st c="6152">header</st>信息。

  • <st c="6171">content-length</st>: 这与文件的 <st c="6235"> content-length</st>头有关。

  • <st c="6244">content-type</st>: 这与文件的 <st c="6304">content-type</st>头有关。

它还包含以下可以在运行时管理文件的方法:

  • <st c="6389">save(destination)</st>: 这会将文件放置在 <st c="6434">目的地</st>

  • <st c="6448">close()</st>: 如果需要,这会关闭文件。

在访问文件进行读取、写入、转换或保存之前,视图函数必须对收到的文件对象应用验证和限制。 <st c="6655">以下是需要注意的以下区域,以设置</st> <st c="6711">红旗:</st>

  • 实际 `上传的文件

  • 一个 `清理过的文件名

  • <st c="6784">接受的文件有效扩展名</st> <st c="6817">为</st>

  • <st c="6825">接受的</st> <st c="6839">文件大小</st>

给定的<st c="6848">show_analysis()</st> <st c="6859">视图函数</st> <st c="6874">在遇到前一个</st> <st c="6981">红旗</st> <st c="6981">问题</st>时引发以下自定义异常类:

  • <st c="6991">NoneFilenameException</st> <st c="7013">: 当请求中没有文件名时引发。</st>

  • <st c="7072">InvalidTypeException</st> <st c="7093">: 当清理后的文件名给出空值时引发。</st>

  • <st c="7160">InvalidTypeException</st> <st c="7181">: 当上传的文件扩展名不被</st> <st c="7256">应用程序</st> <st c="7256">支持</st>时引发。

此外,部分关注点是,在利用任何文件事务之前,对多部分对象的文件名进行清理。立即使用 filename 属性 FileStorage 实例可能会使应用程序暴露于多个漏洞,因为filename 可能包含与恶意软件相关的符号,一些可疑的特殊字符,以及表示文件路径的字符,例如<st c="7671">../../</st> <st c="7677">,这可能会与<st c="7712">save()</st> <st c="7718">方法</st> <st c="7718">产生问题。</st> <st c="7727">要执行文件名清理,请使用</st> <st c="7767">secure_filename()</st> <st c="7784">实用方法</st> <st c="7784">的</st> <st c="7807">werkzeug.utils</st> <st c="7821">模块。</st> <st c="7830">另一方面,我们的一些应用程序视图函数将上传的文件保存在我们项目的文件夹中,但将它们存储在项目目录之外仍然是</st> <st c="8005">最佳实践。</st>

最后,始终使用<st c="8019">try-except</st> <st c="8043">子句</st> <st c="8046">包围</st> <st c="8046">视图函数的全部文件事务</st>,并引发必要的异常类以记录运行时可能出现的所有底层问题。现在,让我们讨论使用 pandas 模块 上传文件后的过程。`

使用<st c="8305">pandas 模块进行数据和分析</st>

pandas 模块是一个 流行的 Python 数据分析库,因为它易于应用的功能函数和名为 <st c="8583">numpy</st> 模块的高性能表格数据结构,一个支持多维数组对象和其数学运算的低级库,称为 <st c="8670">ndarray</st> ,以及 <st c="8715">matplotlib</st>,一个用于可视化的库。 因此,首先安装这两个 模块:

 pip install numpy matplotlib

然后,安装 <st c="8841">pandas</st> 模块:

 pip install pandas

由于我们的数据将来自 XLSX 工作表,请安装处理读取和写入 <st c="8935">openpyxl</st> 依赖模块的 <st c="8965">pandas</st> 模块,该模块处理读取和写入 `XLSX 文档:

 pip install openpyxl

安装所有依赖模块后,我们可以开始创建 <st c="9116">DataFrame</st> 对象。

利用 DataFrame

要读取一个 XLSX 文档,pandas 模块有一个 <st c="9208">read_excel()</st> 方法,具有 <st c="9252">usecols</st>等参数,该参数表示要包含的列或列的范围, <st c="9321">skiprows</st>,它从列行开始选择要跳过的行,以及 <st c="9396">sheet_name</st>,它从工作表 <st c="9460">0</st>开始选择要读取的工作表。以下是从之前的 <st c="9495">show_analysis()</st> 视图中的数据检索,包括工作表 <st c="9554">2</st> ,排除 <st c="9583">行</st> <st c="9587">1</st>

 df_xlsx = read_excel(uploaded_file, sheet_name=2, skiprows=[1])

此结果将与以下从样本 上传文件 的快照相似:

图 6.1 – 包含 HPI 和 PDI 数据的样本 XLSX 文档

图 6.1 – 包含 HPI 和 PDI 数据的样本 XLSX 文档

图 6**.2 显示了一个 样本 <st c="11441">DataFrame</st> 对象,该对象是从上传的房价数据集中提取到 <st c="11515">show_analysis()</st> 视图函数 的。

图 6.2 – 从上传文件中提取的样本 DataFrame

图 6.2 – 从上传文件中提取的样本 DataFrame

DataFrame对象具有易于使用的属性,可以提取表格的一部分,例如DataFrame 对象有易于使用的属性,可以提取表格的一部分,例如 shape size axes at columns indexes ndim iloc loc 。如果目标是仅提取从澳大利亚到美国的列,则 loc 属性应指示 DataFrame 对象将从中筛选分析列的范围,如下面的代码片段所示:

 df_tbl = df_xlsx.loc[: , 'Australia':'US'].describe().to_html()

loc 属性通过选择列标签或范围访问数据值,而其 iloc 对应属性使用列索引来切片 DataFrame 实例,如 df_tbl 。这两个属性都发出数学方法,如 count() mean() sum() mode() std() var() 。然而,给定的视图函数利用 describe() 方法从 1975 年到当前年度的实际 HPI 值季度数据中提取从澳大利亚到美国的列数据。 以下是上传有效的住房数据集 XLSX 文档时我们视图的实际输出:

图 6.3 – show_analysis()视图的示例输出

图 6.3 – show_analysis()视图的示例输出

当使用 Flask 渲染数据值时,DataFrame 对象有三个实用方法,可以提供格式化后的结果。 以下是这三个方法:

  • <st c="15228">to_html()</st> :这生成一个带有数据集的 HTML 表格格式。

  • <st c="15295">to_latex()</st> :这创建了一个 LaTeX 格式的结果,数据已准备好进行 PDF 转换。

  • <st c="15390">to_markdown()</st> :这生成一个带有数据值的 Markdown 模板。

show_analysis()的情况下,它使用 <st c="15510">to_html()</st> 将所有捕获的数据集通过 <st c="15581">to_html()</st>渲染为 HTML 表格。然而,这种渲染方式仅适用于 <st c="15639">safe</st> Jinja2 过滤器,因为出于安全目的,Jinja2 不会自动对 <st c="15739">to_html()</st> 提供的所有字符进行 HTML 转义。 图 6**.3 显示了从包含值的 <st c="15839">DataFrame</st> 实例使用其 <st c="15890">to_html()</st> 方法渲染的表格值的原始结果。

使用 matplotlib 绘制图表和图表

当数据包含在 <st c="15997">DataFrame</st> 对象的两维数据结构中时,绘制数据很容易。 matplotlib 内置支持将表格值渲染为 线条形图饼图或其他图形或图表类型。 由于我们的应用程序是一个 Web 应用程序,我们的视图函数必须将这些视觉元素渲染为图像,这与返回 JSON 资源的 REST 应用程序不同,后者为前端框架提供资源。

现在,第一步是创建一个 <st c="16377">Figure</st> 对象。 一个 <st c="16394">Figure</st> 对象根据可视化方法作为图表或子图的画布。 它是由 <st c="16532">figure()</st> 方法创建的一个普通空白对象,该方法属于 <st c="16555">matplotlib</st> 模块或 <st c="16580">Figure</st> 辅助类,该类属于 <st c="16607">matplotlib.figure</st> 模块。 在最终确定绘图之前,它具有以下需要配置的基本属性:

  • <st c="16726">figsize</st>: 这用于测量画布尺寸的 x 轴和 y 轴。

  • <st c="16800">dpi</st>: 这用于测量绘图每英寸的点数。

  • <st c="16855">linewidth</st>: 这用于测量画布的边框线。

  • <st c="16911">edgecolor</st>: 这应用于画布边框的颜色。

  • <st c="16973">facecolor</st>: 这将应用指定的颜色到画布边框和坐标轴之间的边界区域 绘图边框。

以下视图实现上传文件,从上传的 XLSX 文档创建一个 <st c="17158">数据框</st> 对象,并从表格值渲染线图:

 from pandas import read_excel <st c="17288">from numpy import arange</st>
<st c="17312">from matplotlib.figure import Figure</st>
<st c="17349">from io import BytesIO</st>
<st c="17372">import base64</st> @upload_bp.route("/upload/xlsx/rhpi/plot/belgium", methods = ['GET', 'POST'])
async def upload_xlsx_hpi_belgium_plot():
    if request.method == 'GET':
        data = None
    else:
        … … … … …
        try:
            df_rhpi = read_excel(uploaded_file, sheet_name=2, <st c="17618">usecols='C'</st>, skiprows=[1])
            array_rhpi = df_rhpi.to_numpy().flatten()
            array_hpi_index = arange(0, array_rhpi.size ) <st c="17733">fig = Figure(figsize=(6, 6), dpi=72,</st> <st c="17769">edgecolor='r', linewidth=2, facecolor='y')</st><st c="17812">axis = fig.subplots()</st> axis.<st c="17840">plot</st>(array_hpi_index, array_rhpi)
            axis.<st c="17881">set_xlabel</st>('Quarterly Duration')
            axis.<st c="17921">set_ylabel</st>('House Price Index')
            axis.<st c="17960">set_title</st>("Belgium's HPI versus RHPI")
            … … … … … … <st c="18013">output = BytesIO()</st><st c="18031">fig.savefig(output, format="png")</st><st c="18065">data = base64.b64encode(output.getbuffer())</st> <st c="18109">.decode("ascii")</st> except:
            raise FileSavingException()
    return render_template("file_upload_xlsx_form.html", <st c="18271">Figure</st> canvas is now 6 inches x 6 inches in dimension, as managed by its <st c="18344">figsize</st> parameter. By default, a <st c="18377">Figure</st> canvas is 6.4 and 4.8 inches. Also, the borderline has an added 2units in thickness, with an <st c="18477">edgecolor</st> value of ‘<st c="18497">r</st>’, a single character shorthand for color red, and a <st c="18552">facecolor</st> value of ‘<st c="18572">y</st>’ character notation, which means color yellow. *<st c="18622">Figure 6</st>**<st c="18630">.4</st>* shows the outcome of the given details of the canvas:
			![Figure 6.4 – A line graph with a customized Figure instance](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_004.jpg)

			<st c="18806">Figure 6.4 – A line graph with a customized Figure instance</st>
			<st c="18865">The next step is to draw up the data values from the</st> `<st c="18919">DataFrame</st>` <st c="18928">object using</st> `<st c="18942">Axes</st>` <st c="18946">or the plot of the</st> `<st c="18966">Figure</st>`<st c="18972">.</st> `<st c="18974">Axes</st>`<st c="18978">, not the x-axis and y-axis, is the area on the</st> `<st c="19026">Figure</st>` <st c="19033">canvas where the visualization will happen.</st> <st c="19077">There are two ways to create an</st> `<st c="19109">Axes</st>` <st c="19113">instance:</st>

				*   <st c="19123">Using the</st> `<st c="19134">subplots()</st>` <st c="19144">method of</st> <st c="19155">the</st> `<st c="19159">Figure</st>`<st c="19165">.</st>
				*   <st c="19166">Using the</st> `<st c="19177">subplots()</st>` <st c="19187">method of the</st> `<st c="19202">matplotlib</st>` <st c="19212">module.</st>

			<st c="19220">Since there is already an existing</st> `<st c="19256">Figure</st>` <st c="19262">instance, the former is the appropriate approach to create the plotting area.</st> <st c="19341">The latter returns a tuple containing a new</st> `<st c="19385">Figure</st>` <st c="19391">instance, with</st> `<st c="19407">Axes</st>` <st c="19411">all in with one</st> <st c="19428">method call.</st>
			<st c="19440">Now, an</st> `<st c="19449">Axes</st>` <st c="19453">instance has almost all the necessary utilities for setting up any</st> `<st c="19521">Figure</st>` <st c="19527">component, such as</st> `<st c="19547">plot()</st>`<st c="19553">,</st> `<st c="19555">axis()</st>`<st c="19561">,</st> `<st c="19563">bar()</st>`<st c="19568">,</st> `<st c="19570">pie()</st>`<st c="19575">, and</st> `<st c="19581">tick_params()</st>`<st c="19594">. In the given</st> `<st c="19609">upload_xlsx_hpi_belgium_plot()</st>`<st c="19639">, the goal is to create a Line2D graph of the actual HPI values of Belgium by using the</st> `<st c="19727">plot()</st>` <st c="19733">method.</st> <st c="19742">The extracted DataFrame tabular data focuses only on the</st> `<st c="19799">Belgium</st>` <st c="19806">column (column C), as indicated by the</st> `<st c="19846">usecols</st>` <st c="19853">parameter of the</st> `<st c="19871">read_excel()</st>` <st c="19883">statement:</st>

df_rhpi = read_excel(uploaded_file, sheet_name=2, plot()的 x-值或 scalex 将有从 0 到捕获的 HPI 值最大数的 ndarray,其 y-值或 scaley 将有比利时的 HPI 值。其颜色参数设置为 #fc0366 以改变线图的默认蓝色。除了 plot()坐标轴 还具有 set_title() 以添加图像的标题,set_xlabel() 以添加 x-值的描述,set_ylabel() 以添加 y-值的描述,set_facecolor() 以改变文本的字体颜色,以及 tick_params() 以更新 x 和 y 刻度值的颜色。 坐标轴 还具有如 xaxisyaxis 之类的属性,以将新的颜色应用到 x 和 y 轴描述和脊上,并调整绘图 linewidthedgecolor

        <st c="20690">在完成绘图细节后,创建一个</st> `<st c="20735">BytesIO</st>` <st c="20742">缓冲对象来包含</st> `<st c="20772">图</st>` <st c="20778">实例。</st> <st c="20789">将</st> `<st c="20800">图</st>` <st c="20806">保存到</st> `<st c="20810">BytesIO</st>` <st c="20817">是解码为内联图像所必需的。</st> <st c="20873">视图必须将</st> `<st c="20896">base64</st>`<st c="20902">-编码的图像传递给其 Jinja2 模板以进行渲染。</st> <st c="20956">通过</st> `<st c="20994"><url></st>` <st c="20999">标签渲染内联图像是一种快速显示图像的方法。</st> *<st c="21040">图 6</st>**<st c="21048">.5</st>* <st c="21050">显示了比利时样本实际 HPI 数据集的更新线图。</st> <st c="21112">。</st>

        ![图 6.5 – 比利时样本实际 HPI 数据集的最终线图](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_005.jpg)

        <st c="21401">图 6.5 – 比利时样本实际 HPI 数据集的最终线图</st>

        <st c="21477">如果我们</st> <st c="21491">有一个</st> <st c="21494">多个</st> <st c="21499">图表</st> <st c="21508">在一个</st> `<st c="21522">坐标轴</st>` <st c="21526">绘图</st>中呢?</st>

        <st c="21532">渲染多个线图</st>

        <st c="21563">根据</st> <st c="21580">可视化的</st> <st c="21589">目标,</st> `<st c="21612">pandas</st>` <st c="21618">模块与</st> `<st c="21631">matplotlib</st>` <st c="21641">可以处理 DataFrame 对象数据值的复杂图形渲染。</st> <st c="21717">以下视图函数创建两个线形图,可以比较基于样本数据集的比利时实际和名义 HPI 值:</st>
 @upload_bp.route("/upload/xlsx/rhpi/hpi/plot/belgium", methods = ['GET', 'POST'])
async def upload_xlsx_belgium_hpi_rhpi_plot():
    if request.method == 'GET':
        data = None
    else:
        … … … … … …
        try: <st c="22045">df_hpi = read_excel(uploaded_file,</st> <st c="22079">sheet_name=1, usecols='C', skiprows=[1])</st><st c="22120">df_rhpi = read_excel(uploaded_file,</st> <st c="22156">sheet_name=2, usecols='C', skiprows=[1])</st><st c="22197">array_hpi = df_hpi.to_numpy().flatten()</st> array_hpi_index = arange(0, df_rhpi.size ) <st c="22281">array_rhpi = df_rhpi.to_numpy().flatten()</st> array_rhpi_index = arange(0, df_rhpi.size )
            fig = Figure(figsize=(7, 7), dpi=72, edgecolor='#140dde', linewidth=2, facecolor='#b7b6d4')
            axes = fig.subplots() <st c="22481">lbl1,</st> = axes.plot(array_hpi_index ,array_hpi, color="#32a8a2") <st c="22544">lbl2</st>, = axes.plot(array_rhpi_index ,array_rhpi, color="#bf8a26")
            axes.set_xlabel('Quarterly Duration')
            axes.set_ylabel('House Price Index') <st c="22684">axes.legend([lbl1, lbl2], ["HPI", "RHPI"])</st> axes.set_title("Belgium's HPI versus RHPI")
            … … … … … …
        except:
            raise FileSavingException()
    return render_template("file_upload_xlsx_sheets_form.html", data=data), 200
        <st c="22894">与之前的</st> `<st c="22920">upload_xlsx_hpi_belgium_plot()</st>` <st c="22950">视图相比,</st> `<st c="22957">upload_xlsx_belgium_hpi_rhpi_plot()</st>` <st c="22992">利用上传文件的工作簿中的两个工作表,即</st> `<st c="23059">sheet[1]</st>` <st c="23067">用于名义 HPI 和</st> `<st c="23092">sheet[2]</st>` <st c="23100">用于比利时实际 HPI 值。</st> <st c="23139">它从每个工作表导出单独的</st> `<st c="23159">DataFrame</st>` <st c="23168">对象的表格值,并绘制一个 Line2D 图来比较两个数据集之间的趋势。</st> <st c="23285">与本章中之前的向量变换类似,这个视图仍然使用</st> `<st c="23369">numpy</st>` <st c="23374">来展平从 DataFrame 的</st> `<st c="23437">to_numpy()</st>` <st c="23447">实用方法中提取的垂直向量。</st> <st c="23464">顺便说一下,视图函数只为两个图表使用一个</st> `<st c="23508">Axes</st>` <st c="23512">绘图。</st>

        <st c="23534">此外,视图还展示了包含一个</st> `<st c="23767">Axes</st>`<st c="23771">,但这个视图捕获了从</st> `<st c="23824">plot()</st>` <st c="23830">方法调用中的 Line2D 对象,并使用</st> `<st c="23893">Axes</st>`<st c="23897">的</st> `<st c="23900">legend()</st>` <st c="23908">方法将每个图表映射到一个字符串标签。</st> *<st c="23917">图 6</st>**<st c="23925">.6</st>* <st c="23927">显示了运行</st> `<st c="23956">upload_xlsx_belgium_hpi_rhpi_plot()</st>` <st c="23991">并上传一个</st> <st c="24009">XLSX 文档的结果。</st>

        ![图 6.6 – 一个 Axes 图中两个线形图](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_006.jpg)

        <st c="24301">图 6.6 – 一个 Axes 图中两个线形图</st>

        下一个,我们将<st c="24346">了解如何</st> <st c="24364">使用 Flask 绘制饼图</st> <st c="24392">。</st>

        <st c="24403">从 CSV 文件渲染饼图</st>

        <st c="24441">The</st> `<st c="24446">pandas</st>` <st c="24452">模块</st> <st c="24460">也可以通过其</st> `<st c="24506">read_csv()</st>` <st c="24516">方法</st> <st c="24525">从 CSV 文件中读取数据。</st> <st c="24525">与</st> `<st c="24535">read_excel()</st>`<st c="24547">不同,</st> `<st c="24553">pandas</st>` <st c="24559">模块读取有效的 CSV 文件不需要任何依赖。</st> <st c="24621">以下视图使用</st> `<st c="24645">read_csv()</st>` <st c="24655">创建用于绘制饼图的值的 DataFrame:</st>
<st c="24713">from pandas import read_csv</st> @upload_bp.route("/upload/csv/pie", methods = ['GET', 'POST'])
async def upload_csv_pie():
    if request.method == 'GET':
        data = None
    else:
        … … … … … …
        try: <st c="24896">df_csv = read_csv(uploaded_file)</st><st c="24928">matplotlib.use('agg')</st> fig = plt.figure()
            axes = fig.add_subplot(1, 1, 1) <st c="25002">explode = (0.1, 0, 0)</st><st c="25023">axes.pie(df_csv.groupby(['FurnishingStatus'])</st><st c="25069">['Price'].count(), colors=['#bfe089', '#ebd05b', '#e67eab'],</st><st c="25130">labels =["Furnished","Semi-Furnished", "Unfurnished"], autopct ='% 1.1f %%',</st><st c="25207">shadow = True, startangle = 90,</st> <st c="25239">explode=explode)</st><st c="25256">axes.axis('equal')</st><st c="25275">axes.legend(loc='lower right',fontsize=7,</st> <st c="25317">bbox_to_anchor = (0.75, -01.0) )</st> … … … … … …
        except:
            raise FileSavingException()
    return render_template("file_upload_csv_pie_form.html", data=data), 200
        <st c="25469">The</st> `<st c="25474">pandas</st>` <st c="25480">模块</st> <st c="25488">也可以通过其</st> `<st c="25534">read_csv()</st>` <st c="25544">方法</st> <st c="25553">从 CSV 文件中读取数据。</st> <st c="25553">与</st> `<st c="25563">read_excel()</st>`<st c="25575">不同,</st> `<st c="25581">pandas</st>` <st c="25587">模块读取有效的 CSV 文件不需要任何依赖。</st>

        <st c="25648">另一方面,</st> `<st c="25672">Axes</st>`<st c="25676">’</st> `<st c="25679">pie()</st>` <st c="25684">方法在达到适合数据值的适当饼图之前需要考虑几个参数。</st> <st c="25792">以下是</st> `<st c="25836">upload_csv_pie()</st>` <st c="25852">视图函数使用的部分参数:</st>

            +   `<st c="25867">explode</st>`<st c="25875">: 这提供了一个分数数字列表,指示围绕扇区的空间,使它们突出。</st>

            +   `<st c="25987">colors</st>`<st c="25994">: 这提供了一个颜色列表,可以是</st> `<st c="26021">matplotlib</st>`<st c="26031">的内置命名颜色或设置为每个小部件的十六进制格式化颜色代码。</st>

            +   `<st c="26120">labels</st>`<st c="26127">: 这提供了分配给每个小部件的字符串值列表。</st>

            +   `<st c="26192">autopct</st>`<st c="26200">: 这提供了每个小部件的字符串格式化百分比值。</st>

            +   `<st c="26268">shadow</st>`<st c="26275">: 这允许在饼图周围添加阴影。</st>

            +   `<st c="26327">startangle</st>`<st c="26338">: 这提供了一个旋转角度,用于饼图从其第一个扇区开始。</st>

        给定的`<st c="26447">upload_csv_pie()</st>` `<st c="26463">is to</st>` `<st c="26469">generate</st>` `<st c="26478">a pie chart based on the number of projected house prices (</st>` `<st c="26538">Price</st>` `<st c="26544">) per furnishing status (</st>` `<st c="26570">FurnishingStatus</st>` `<st c="26587">), namely the</st>` `<st c="26602">Furnished</st>` `<st c="26611">,</st>` `<st c="26613">Semi-furnished</st>` `<st c="26627">, and</st>` `<st c="26633">Fully-furnished</st>` `<st c="26648">houses.</st>` `<st c="26657">The</st>` `<st c="26661">groupby()</st>` `<st c="26670">method of the</st>` `<st c="26685">df_csv</st>` `<st c="26691">DataFrame extracts the needed data values for the</st>` `<st c="26742">pie()</st>` `<st c="26747">method.</st>` `<st c="26756">Now, running this view function will render the</st>` `<st c="26804">following chart:</st>`

        ![Figure 6.7 – 饰面状态偏好饼图](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_007.jpg)

        `<st c="26951">Figure 6.7 – 饰面状态偏好饼图</st>`

        如果保存饼图图形时产生以下警告信息,`<st c="27077">UserWarning: Starting a Matplotlib GUI outside of the main thread will likely fail.</st>`,则添加`<st c="27166">matplotlib.use('agg')</st>`在任何创建`<st c="27217">Figure</st>` `<st c="27223">instance</st>` `<st c="27233">to</st>` `<st c="27235">enable</st>` `<st c="27242">the non-interactive backend mode for writing files outside the</st>` `<st c="27306">main thread.</st>`之前,以启用非交互式后端模式,以便在主线程外写入文件。

        如果我们在一个`<st c="27367">Figure</st>` `<st c="27373">>`中有多多个`<st c="27349">Axes</st>` `<st c="27353">plots`呢?

        `<st c="27374">Rendering multiple Axes plots</st>`

        `<st c="27403">一个 Figure 可以</st>` `<st c="27416">包含</st>` `<st c="27425">多个不同图表和图形的 plot。</st>` `<st c="27476">科学应用通常具有 GUI,可以渲染不同数据校准、转换和分析的多个图表。</st>` `<st c="27604">以下视图函数上传一个 XLSX 文档,并在一个</st>` `<st c="27685">Figure</st>` `<st c="27691">>`上创建四个 plot,以创建从文档中提取的 DataFrame 数据值的不同的图形:</st>`
 @upload_bp.route("/upload/xlsx/multi/subplot", methods = ['GET', 'POST'])
async def upload_xlsx_multi_subplots():
    if request.method == 'GET':
        data = None
    else:
        … … … … … …
        try:
            df_xlsx = read_excel(uploaded_file, sheet_name=2, skiprows=[1]) <st c="28201">axes1</st>, creates two line graphs of the actual HPI values of Australia and Belgium for all the quarterly periods, as indicated in the following code block:

<st c="28354">axes1.plot(df_xlsx.index.values,</st><st c="28387">df_xlsx['Australia'], 'green',</st><st c="28418">df_xlsx.index.values,</st><st c="28440">df_xlsx['Belgium'], 'red',)</st> axes1.set_xlabel('季度持续时间')

    `axes1.set_ylabel('House Price Index')`

    `axes1.set_title('澳大利亚……之间的 RHPI')`

			<st c="28591">The second plot,</st> `<st c="28609">axes2</st>`<st c="28614">, generates a bar chart depicting the mean HPI values of all countries in the tabular values, as shown in the following</st> <st c="28734">code block:</st>

index = arange(df_xlsx.loc[: , 'Australia':'US'].shape[1]) <st c="28805">axes2.bar(index, df_xlsx.loc[: ,</st> 'Australia':'US'].mean(),color=(0.1, 0.1, 0.1, 0.1), edgecolor='blue') axes2.set_xlabel('国家 ID')`

    `axes2.set_ylabel('Mean HPI')`

    `axes2.set_xticks(index)`

    `axes2.set_title('Mean RHPI among countries')`

			<st c="29038">The third plot,</st> `<st c="29055">axes3</st>`<st c="29060">, plots all HPI values of each country in the tabular values from 1975 to the current year, creating multiple</st> <st c="29170">line graphs:</st>

axes3.plot(df_xlsx.loc[: , 'Australia':'US']) axes3.set_xlabel('季度持续时间')

        axes3.set_ylabel('房价指数')

        axes3.set_title('各国 RHPI 趋势')

			<st c="29351">The last</st> <st c="29361">plot,</st> `<st c="29367">axes4</st>`<st c="29372">, builds a</st> <st c="29383">grouped bar chart showing the HPI values of Japan, South Korea, and New Zealand quarterly</st> <st c="29473">in 1975:</st>

width = 0.3 axes4.bar(df_xlsx.loc[0:3, 'Japan'].index.values-width, df_xlsx.loc[0:3, 'Japan'], width=width, color='#d9182b', label="JP")axes4.bar(df_xlsx.loc[0:3, 'S. Korea'].index.values, df_xlsx.loc[0:3, 'S. Korea'], width=width, color='#f09ec1', label="SK")axes4.bar(df_xlsx.loc[0:3, 'New Zealand'].index.values+width, df_xlsx.loc[0:3, 'New Zealand'], width=width, color='#000', label="NZ") axes4.set_xlabel('季度持续时间')

        … … … … … …

        axes4.legend()

			<st c="29943">The given</st> `<st c="29954">axes4</st>` <st c="29959">setup uses the</st> `<st c="29975">plot()</st>` <st c="29981">label parameter to assign codes for each bar plot needed by its</st> `<st c="30046">legend()</st>` <st c="30054">method in forming the diagram’s legends.</st> <st c="30096">Running the view function</st> <st c="30122">will</st> <st c="30127">give us the following</st> <st c="30149">multiple graphs:</st>
			![Figure 6.8 – A Figure with multiple plots](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_008.jpg)

			<st c="30688">Figure 6.8 – A Figure with multiple plots</st>
			<st c="30729">Flask’s asynchronous components can also support more advanced, informative, and complex mathematical and statistical graphs plotted on a</st> `<st c="30868">Figure</st>` <st c="30874">with the</st> `<st c="30884">seaborn</st>` <st c="30891">module.</st> <st c="30900">Also, it can create regression plots using various regression techniques using the</st> `<st c="30983">statsmodels</st>` <st c="30994">module.</st> <st c="31003">The next topic will highlight the solving of nonlinear and linear equations</st> <st c="31079">with</st> <st c="31083">the</st> `<st c="31088">sympy</st>` <st c="31093">module.</st>
			<st c="31101">Implementing symbolic computation with visualization</st>
			`<st c="31682">matplotlib</st>` <st c="31692">and</st> `<st c="31697">numpy</st>` <st c="31702">modules.</st>
			<st c="31711">For Flask to recognize symbolic expressions and formulas in a string expression, install the</st> `<st c="31805">sympy</st>` <st c="31810">module using the</st> `<st c="31828">pip</st>` <st c="31831">command:</st>

pip install sympy


			<st c="31858">Then, install the</st> `<st c="31877">mpmath</st>` <st c="31883">module, a prerequisite of the</st> `<st c="31914">sympy</st>` <st c="31919">module:</st>

pip install mpmath


			<st c="31946">After these installations, we can start</st> <st c="31987">problem solving.</st>
			<st c="32003">Solving linear equations</st>
			<st c="32028">Let us begin</st> <st c="32042">with the following asynchronous</st> <st c="32074">route implementation that asks for any linear equation with x and y</st> <st c="32142">variables only:</st>

from modules.equations import eqn_bp

from flask import render_template, request from sympy import sympify

import gladiator as gl @eqn_bp.route('/eqn/simple/bivar', methods = ['GET', 'POST'])

async def solve_multivariate_linear():

if request.method == 'GET':

    soln = None

else: <st c="32434">field_validations</st> = (

        ('lineqn', gl.required, gl.type_(str),    gl.regex_('[+\-]?(([0-9]+\.[0-9]+)|([0-9]+\.?)|(\.?[0-9]+))[+\-/*]xy|([0-9]+\.?)|(\.?[0-9]+))[+\-/*][xy])*(+\-/*|([0-9]+\.?)|(\.?[0-9]+)))*')),

        ('xvar', gl.required, gl.type_(str), gl.regex_('[0-9]+')),

        ('yvar', gl.required, gl.type_(str), gl.regex_('[0-9]+'))

    )

    form_data = request.form.to_dict() <st c="32839">result = gl.validate(field_validations, form_data )</st> if bool(result): <st c="32908">xval = float(form_data['xvar'])</st><st c="32939">yval = float(form_data['yvar'])</st><st c="32971">eqn = sympify(form_data['lineqn'], {'x': xval,</st> <st c="33018">'y': yval})</st><st c="33030">soln = eqn.evalf()</st> else:

        soln = None

return render_template('simple_linear_mv_form.html', soln=soln), 200

			<st c="33136">Assuming that</st> `<st c="33151">xvar</st>` <st c="33155">and</st> `<st c="33160">yvar</st>` <st c="33164">are valid form parameter values convertible to</st> `<st c="33212">float</st>` <st c="33217">and</st> `<st c="33222">lineqn</st>` <st c="33228">is a valid two-variate string expression with x and y variables, the</st> `<st c="33298">sympify()</st>` <st c="33307">method of the</st> `<st c="33322">sympy</st>` <st c="33327">module can convert</st> `<st c="33347">lineqn</st>` <st c="33353">to a symbolic formula with</st> `<st c="33381">xvar</st>` <st c="33385">and</st> `<st c="33390">yvar</st>` <st c="33394">values assigned to the x and y symbols and compute the solution.</st> <st c="33460">To extract the exact value of the sympification, the resulting symbolic formula has a method such as</st> `<st c="33561">evalf()</st>` <st c="33568">that returns a floating-point value of the solution.</st> <st c="33622">Now, the</st> `<st c="33631">sympify()</st>` <st c="33640">method uses the risky</st> `<st c="33663">eval()</st>` <st c="33669">function, so the mathematical expression, such as</st> `<st c="33720">lineqn</st>`<st c="33726">, requires sanitation by popular validation tools such as</st> `<st c="33784">gladiator</st>` <st c="33793">before performing sympification.</st> *<st c="33827">Figure 6</st>**<st c="33835">.9</st>* <st c="33837">shows a sample execution of</st> `<st c="33866">solve_multivariate_linear()</st>` <st c="33893">with a sample linear equation and the corresponding values for its x</st> <st c="33963">and y:</st>
			![Figure 6.9 – Solving a linear equation with x and y variables](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_009.jpg)

			<st c="34065">Figure 6.9 – Solving a linear equation with x and y variables</st>
			<st c="34126">Now, not all</st> <st c="34139">real-world problems are solvable</st> <st c="34172">using linear models.</st> <st c="34194">Some require non-linear models to derive</st> <st c="34235">their solutions.</st>
			<st c="34251">Solving non-linear formulas</st>
			<st c="34279">Flask</st> `<st c="34286">async</st>` <st c="34291">and</st> `<st c="34296">sympy</st>` <st c="34301">can</st> <st c="34305">also implement a</st> <st c="34322">view function for solving non-linear equations.</st> <st c="34371">The</st> `<st c="34375">sympify()</st>` <st c="34384">method can recognize Python mathematical functions such as</st> `<st c="34444">exp(x)</st>`<st c="34450">,</st> `<st c="34452">log(x)</st>`<st c="34458">,</st> `<st c="34460">sqrt(x)</st>`<st c="34467">,</st> `<st c="34469">cos(x)</st>`<st c="34475">,</st> `<st c="34477">sin(x)</st>`<st c="34483">, and</st> `<st c="34489">pow(x)</st>`<st c="34495">. Thus, creating mathematical expressions with the inclusion of these Python functions is feasible with</st> `<st c="34599">sympy</st>`<st c="34604">.</st> *<st c="34606">Figure 6</st>**<st c="34614">.10</st>* <st c="34617">shows a view function that computes a solution of a univariate non-linear equation with</st> <st c="34706">one variable.</st>
			![Figure 6.10 – Solving a non-linear equation with Python functions](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_010.jpg)

			<st c="34811">Figure 6.10 – Solving a non-linear equation with Python functions</st>
			<st c="34876">The strength of the</st> `<st c="34897">sympy</st>` <st c="34902">module is to extract the parameter values of an equation or equations</st> <st c="34973">based on a given result</st> <st c="34997">or</st> <st c="34999">solution.</st>
			<st c="35009">Finding solutions for a linear system</st>
			<st c="35047">The</st> `<st c="35052">sympy</st>` <st c="35057">module</st> <st c="35065">has a</st> `<st c="35071">solve()</st>` <st c="35078">method</st> <st c="35086">that can solve systems of linear or polynomial equations.</st> <st c="35144">The following implementation can find a solution for a system of two</st> <st c="35213">polynomial equations:</st>

from modules.equations import eqn_bp

from flask import render_template, request from sympy import symbols, sympify, solve @eqn_bp.route('/eqn/eqnsystem/solve', methods = ['GET', 'POST'])

async def solve_multiple_eqns():

if request.method == 'GET':

    soln = None

else:

    field_validations = (

        ('polyeqn1', gl.required, gl.type_(str)),

        ('polyeqn2', gl.required, gl.type_(str))

    )

    form_data = request.form.to_dict()

    result = gl.validate(field_validations, form_data )

    if bool(result): <st c="35712">x, y = symbols('x y')</st><st c="35733">eqn1 = sympify(form_data['polyeqn1'])</st><st c="35771">eqn2 = sympify(form_data['polyeqn2'])</st><st c="35809">soln = solve((eqn1, eqn2),(x, y))</st> else:

        soln = None

return  render_template('complex_multiple_eqns_form.html',   soln=soln), 200y

			<st c="35936">After the retrieval from</st> `<st c="35962">request.form</st>` <st c="35974">and a successful validation using</st> `<st c="36009">gladiator</st>`<st c="36018">, the</st> `<st c="36024">polyeqn1</st>` <st c="36032">and</st> `<st c="36037">polyeqn2</st>` <st c="36045">string expressions must undergo sympification</st> <st c="36091">through the</st> `<st c="36104">sympify()</st>` <st c="36113">method</st> <st c="36121">to derive their symbolic equations or</st> `<st c="36159">sympy</st>` <st c="36164">expressions.</st> <st c="36178">The function variables, x and y, of these mathematical expressions must have their corresponding</st> `<st c="36275">Symbol</st>`<st c="36281">-type variables utilizing the</st> `<st c="36312">symbols()</st>` <st c="36321">function of</st> `<st c="36334">sympy</st>`<st c="36339">, a vital mechanism for creating</st> `<st c="36372">Symbol</st>` <st c="36378">variables out of string variables.</st> <st c="36414">The</st> `<st c="36418">solve()</st>` <st c="36425">method requires a tuple of these symbolic equations in its first parameter and a tuple of</st> `<st c="36516">Symbols</st>` <st c="36523">in its second parameter to find the solutions of the linear system.</st> <st c="36592">If the linear equations are not parallel to each other, the</st> `<st c="36652">solve()</st>` <st c="36659">method will return a feasible solution in a dictionary format with</st> `<st c="36727">sympy</st>` <st c="36732">variables</st> <st c="36743">as keys.</st>
			<st c="36751">If we execute</st> `<st c="36766">solve_multiple_eqns()</st>` <st c="36787">with a simple linear system, such as passing the</st> `<st c="36837">5*x-3*y-9</st>` <st c="36846">equation to</st> `<st c="36859">polyeqn1</st>` <st c="36867">and the</st> `<st c="36876">15*x+3*y+12</st>` <st c="36887">equation to</st> `<st c="36900">polyeqn2</st>`<st c="36908">,</st> `<st c="36910">solve()</st>` <st c="36917">will provide us with numerical results, as shown in</st> *<st c="36970">Figure 6</st>**<st c="36978">.11</st>*<st c="36981">.</st>
			![Figure 6.11 – Solving simple linear equations](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_011.jpg)

			<st c="37122">Figure 6.11 – Solving simple linear equations</st>
			<st c="37167">However, if we have polynomials or non-linear equations such as passing the</st> `<st c="37244">x**2-10*y+10</st>` <st c="37256">quadratic</st> <st c="37267">formula to</st> `<st c="37278">polyeqn1</st>` <st c="37286">and the</st> `<st c="37295">10*x+5*y-3</st>` <st c="37305">linear expression to</st> `<st c="37327">polyeqn2</st>`<st c="37335">, the resulting non-linear solutions will be rational values with square roots, as shown in</st> *<st c="37427">Figure 6</st>**<st c="37435">.12</st>*<st c="37438">.</st>
			![Figure 6.12 – Solving polynomial system of equations](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_012.jpg)

			<st c="37623">Figure 6.12 – Solving polynomial system of equations</st>
			<st c="37675">There are many</st> <st c="37691">possible symbolic computations, formulas, and algorithms that Flask can implement with</st> `<st c="37778">sympy</st>`<st c="37783">. Sometimes, the</st> `<st c="37800">scipy</st>` <st c="37805">module can help</st> `<st c="37822">sympy</st>` <st c="37827">solve other mathematical algorithms that are very</st> <st c="37878">tedious and complicated, such as</st> <st c="37911">approximation problems.</st>
			<st c="37934">The</st> `<st c="37939">sympy</st>` <st c="37944">module is also capable of providing graphical analysis</st> <st c="38000">through plots.</st>
			<st c="38014">Plotting mathematical expressions</st>
			<st c="38048">When it comes</st> <st c="38062">to visualization,</st> `<st c="38081">sympy</st>` <st c="38086">is capable</st> <st c="38098">of rendering graphs and charts created by its built-in</st> `<st c="38153">matplotlib</st>` <st c="38163">library.</st> <st c="38173">The following view function accepts two equations from the user and creates a graphical plot for the equations within the specified range of values</st> <st c="38321">for x:</st>

from sympy import symbols, sympify

from sympy.plotting import plot

import matplotlib

import base64

from io import BytesIO

from PIL import Image @eqn_bp.route('/eqn/multi/plot', methods = ['GET', 'POST'])

async def plot_two_equations():

if request.method == 'GET':

    data = None

else:

    … … … … … …

    form_data = request.form.to_dict()

    result = gl.validate(field_validations, form_data )

    eqn1_upper = float(form_data['eqn1_maxval'])

    eqn1_lower = float(form_data['eqn1_minval'])

    eqn2_upper = float(form_data['eqn2_maxval'])

    eqn2_lower = float(form_data['eqn2_minval'])

    data = None

    if bool(result) and (eqn1_lower <= eqn1_upper) and (eqn2_lower <= eqn2_upper): <st c="38980">matplotlib.use('agg')</st> x = symbols('x')

        eqn1 = sympify(form_data['equation1'])

        eqn2 = sympify(form_data['equation2']) <st c="39097">graph = plot(eqn1, (x, eqn1_lower, eqn1_upper), line_color='red', show=False)</st><st c="39174">graph.extend(plot(eqn2, (x, eqn2_lower, eqn2_upper), line_color='blue', show=False))</st> filename = "./files/img/multi_plot.png" <st c="39300">graph.save(filename)</st><st c="39320">img = Image.open(filename)</st><st c="39347">image_io = BytesIO()</st><st c="39368">img.save(image_io, 'PNG')</st> data = base64.b64encode(image_io.getbuffer()) .decode("ascii")

return render_template('plot_two_eqns_form.html', data=data), 200

			<st c="39523">After sanitizing the</st> <st c="39544">string equations</st> <st c="39562">and deriving the</st> `<st c="39579">sympy</st>` <st c="39584">formulas, the view can directly create a plot for each formula using the</st> `<st c="39658">plot()</st>` <st c="39664">method in the</st> `<st c="39679">sympy.plotting</st>` <st c="39693">module, which is almost similar to that in the</st> `<st c="39741">matplotlib</st>` <st c="39751">module but within the context of</st> `<st c="39785">sympy</st>`<st c="39790">. The method returns a</st> `<st c="39813">Plot</st>` <st c="39817">instance that can combine with another</st> `<st c="39857">Plot</st>` <st c="39861">using its</st> `<st c="39872">extend()</st>` <st c="39880">method to create multiple plots in one frame.</st> <st c="39927">Running the</st> `<st c="39939">plot_two_equations()</st>` <st c="39959">view will yield line graphs of both</st> `<st c="39996">equation1</st>` <st c="40005">and</st> `<st c="40010">equation2</st>`<st c="40019">, as shown in</st> *<st c="40033">Figure 6</st>**<st c="40041">.13</st>*<st c="40044">.</st>
			![Figure 6.13 – Plotting the two sympy equations](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_013.jpg)

			<st c="40186">Figure 6.13 – Plotting the two sympy equations</st>
			<st c="40232">On the other</st> <st c="40245">hand, the</st> `<st c="40256">Plot</st>` <st c="40260">instance</st> <st c="40269">has a</st> `<st c="40276">save()</st>` <st c="40282">method that can store the graphical plot as an image.</st> <st c="40337">However, to create an inline image for a Jinja2 rendition, the view needs the</st> `<st c="40415">Image</st>` <st c="40420">class from</st> `<st c="40519">BytesIO</st>` <st c="40526">for</st> `<st c="40531">base64</st>` <st c="40537">encoding.</st>
			<st c="40547">Let us examine now how asynchronous Flask can manage those scientific data that need LaTeX serialization or</st> <st c="40656">PDF renditions.</st>
			<st c="40671">Creating and rendering LaTeX documents</st>
			**<st c="40710">LaTex</st>** <st c="40716">is a high-standard</st> <st c="40735">typesetting system used in publishing and packaging technical and scientific papers and literature, especially those documents with charts, graphs, equations, and tabular data.</st> <st c="40913">When creating scientific applications, there should be a mechanism for the application to write LaTeX content, save it in a repository, and render it as</st> <st c="41066">a response.</st>
			<st c="41077">But first, our applications will require a LaTeX compiler that assembles and compiles newly created LaTeX documents.</st> <st c="41195">Here are two popular tools that offer various</st> <st c="41241">LaTeX compilers:</st>

				*   **<st c="41257">TeX Live</st>**<st c="41266">: This is an open-source</st> <st c="41292">LaTeX tool most suitable for creating secured</st> <st c="41338">LaTeX documents.</st>
				*   **<st c="41354">MikTeX</st>**<st c="41361">: This is an open-source LaTeX tool popular for its on-the-fly libraries and</st> <st c="41439">up-to-date releases.</st>

			<st c="41459">Our application will be utilizing MikTeX for its LaTeX compilers.</st> <st c="41526">Do not forget to update MikTex for the latest plugins using the console, as shown in</st> *<st c="41611">Figure 6</st>**<st c="41619">.14</st>*<st c="41622">.</st>
			![Figure 6.14 – Updating MikTeX using its console](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_014.jpg)

			<st c="41943">Figure 6.14 – Updating MikTeX using its console</st>
			<st c="41990">After the MikTeX installation and update, let’s create the Flask project by installing the</st> `<st c="42082">latex</st>` <st c="42087">module.</st>
			<st c="42095">Rendering LaTeX documents</st>
			<st c="42121">Asynchronous</st> <st c="42134">view functions can create, update, and render LaTeX documents through LaTeX-related modules and</st> `<st c="42231">matplotlib</st>` <st c="42241">for immediate textual and graphical plots or perform LaTeX to PDF transformation of existing LaTeX documents for rendition.</st> <st c="42366">The latter requires the installation of the</st> `<st c="42410">latex</st>` <st c="42415">module through the</st> `<st c="42435">pip</st>` <st c="42438">command:</st>

pip install latex


			<st c="42465">The</st> `<st c="42470">latex</st>` <st c="42475">module uses its built-in Jinja libraries to access</st> `<st c="42527">latex</st>` <st c="42532">files stored in the main project.</st> <st c="42567">So, the first step is to create a Jinja environment with all the details that will calibrate the Jinja engine regarding LaTeX file handling.</st> <st c="42708">The following snippet shows how to set up the Jinja environment using the</st> `<st c="42782">latex.jinja2</st>` <st c="42794">module:</st>

from jinja2 import FileSystemLoader, Environment

from latex.jinja2 import make_env environ:Environment = make_env(loader=FileSystemLoader('files'), enable_async=True, block_start_string = '\BLOCK{',

block_end_string = '}',

variable_start_string = 'VAR{',

variable_end_string = '}',

comment_start_string = '#{',

comment_end_string = '}',

line_statement_prefix = '%-',

line_comment_prefix = '%#',

trim_blocks = True,

autoescape = False,)

			<st c="43239">Since</st> `<st c="43246">ch06-project</st>` <st c="43258">uses</st> `<st c="43264">Blueprint</st>` <st c="43273">to organize the views and the corresponding components, only the rendition module (</st>`<st c="43357">/modules/rendition</st>`<st c="43376">) that builds the LaTeX web displays can access this environment configuration.</st> <st c="43457">This Jinja environment details, defined in</st> `<st c="43500">/modules/rendition/__init__.py</st>`<st c="43530">, declares that the</st> `<st c="43550">files</st>` <st c="43555">folder in the project directory will become the root folder for our LaTeX documents.</st> <st c="43641">Moreover, it tells Jinja the syntax preferences for some LaTeX commands, such as the</st> `<st c="43726">BLOCK</st>`<st c="43731">,</st> `<st c="43733">VAR</st>`<st c="43736">, conditional statement, and comment symbols.</st> <st c="43782">Instead of having a backslash pipe (</st>`<st c="43818">"\"</st>`<st c="43822">) in</st> `<st c="43828">\VAR{}</st>`<st c="43834">, the setup wants Jinja to recognize the</st> `<st c="43875">VAR{}</st>` <st c="43880">statement, an interpolation operator, without the backslash pipe.</st> <st c="43947">Violating the given syntax rules will flag an error in Flask.</st> <st c="44009">The</st> `<st c="44013">enable_async</st>` <st c="44025">property, on the other hand, allows the execution of</st> `<st c="44079">latex</st>` <st c="44084">commands in asynchronous view functions, such as the following</st> <st c="44147">view implementation that opens a document and updates it</st> <st c="44205">for display:</st>

from modules.rendition import rendition_bp

from flask import send_from_directory

from jinja2 import FileSystemLoader

from latex.jinja2 import make_env

@rendition_bp.route('/render/hpi/plot/eqns', methods = ['GET', 'POST']) async def convert_latex(): tpl = environ.get_template('/latex/hpi_plot.tex') outpath=os.path.join('./files/latex','hpi_plot.pdf')

outfile=open(outpath,'w') <st c="44597">outfile.write(await tpl.render_async(author='Sherwin</st> <st c="44649">约翰·特拉古拉', title="使用 LaTeX 渲染 HPI 图", date=datetime.now().strftime("%B %d, %Y"),</st> <st c="44695">renderTbl=True))</st><st c="44763">outfile.close()</st> os.system("pdflatex <st c="44800">--shell-escape</st> -output-directory=" + './files/latex' + " " + outpath) <st c="44959">get_template()</st> of the <st c="44981">环境</st>实例,<st c="45003">环境</st>,从根目录的<st c="45078">/latex</st>子目录中创建一个特定 LaTeX 文档的 Jinja2 模板。该模板的<st c="45134">render_async()</st>函数打开指定的 LaTeX 文档以进行更改,例如传递上下文值(例如,<st c="45244">作者</st>,<st c="45252">标题</st>,<st c="45259">日期</st>和<st c="45269">renderTbl</st>)以完成文档。

        <st c="45306">之后,</st> `<st c="45322">视图</st>` <st c="45326">函数将文档转换为 PDF 格式,这是此应用程序的必要方法。</st> `<st c="45433">os.path.join()</st>` <st c="45447">将指示文件保存的位置。</st> <st c="45486">现在,MikTeX 提供了三个编译器来编译并将 LaTeX 文档转换为 PDF,即 pdfLaTeX、XeLaTeX 和 LuaLaTeX,但我们的实现使用 pdfLaTeX,这是默认的。</st> `<st c="45675">os.system()</st>` <st c="45686">将运行编译器并将 PDF 保存到指定位置。</st> <st c="45754">为了渲染内容,Flask 有一个</st> `<st c="45789">send_from_directory()</st>` <st c="45810">方法可以显示目录中保存的 PDF 文件的内容。</st> *<st c="45885">图 6</st>**<st c="45893">.15</st>* <st c="45896">显示了通过运行</st> `<st c="45945">convert_latex()</st>` <st c="45960">视图函数得到的 PDF 文档的结果。</st>

        ![图 6.15 – 将 LaTeX 文档渲染为 PDF](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_015.jpg)

        <st c="46098">图 6.15 – 将 LaTeX 文档渲染为 PDF</st>

        <st c="46147">我们的 Flask 应用程序</st> <st c="46169">不仅渲染现有的 LaTeX 文档,而且在将其渲染到客户端之前先创建一个。</st>

        <st c="46271">创建 LaTeX 文档</st>

        <st c="46296">到目前为止,</st> `<st c="46309">latex</st>` <st c="46314">模块</st> <st c="46322">与 Jinja2 一起没有 LaTeX 创建功能,Flask 可以使用这些功能从各种数据源构建科学论文。</st> <st c="46440">然而,其他模块,如</st> `<st c="46472">pylatex</st>`<st c="46479">,可以提供辅助类和方法,在运行时序列化 LaTeX 内容。</st> <st c="46559">以下视图实现展示了如何使用</st> `<st c="46633">DataFrame</st>` <st c="46642">对象的数据,该数据来自上传的</st> `<st c="46682">XLSX</st>`文档来生成 LaTeX 文件:</st>
<st c="46696">from pylatex import Document, Section, Command, NoEscape, Subsection, Tabular, Center</st>
<st c="46782">from pylatex.utils import italic</st>
<st c="46815">from pylatex.basic import NewLine</st> @rendition_bp.route('/create/hpi/desc/latex', methods = ['GET', 'POST'])
async def create_latex_pdf():
    if request.method == 'GET':
         return render_template("hpi_latex_form.html"), 200
    else:
        … … … … … …
        … … … … … …
        try:
            df = read_excel(uploaded_file, sheet_name=2, skiprows=[1])
            hpi_data = df.loc[: , 'Australia':'US'].describe().to_dict()
            hpi_filename = os.path.join('./files/latex','hpi_analysis')
        在所有其他事情之前,环境设置必须安装 MikTeX 或 TeX Live 以支持 LaTeX 编译器。</st> <st c="47362">然后,通过</st> `<st c="47380">pylatex</st>` <st c="47387">模块通过</st> `<st c="47407">pip</st>` <st c="47410">命令安装:</st>
 pip install pylatex
        <st c="47439">要开始事务,给定的</st> `<st c="47476">create_latext_pdf()</st>` <st c="47495">检索上传的 XLSX 文档以提取用于报告生成的表格值:</st>
 geometry_options = {
                "landscape": True,
                "margin": "0.5in",
                "headheight": "20pt",
                "headsep": "10pt",
                "includeheadfoot": True
            }
            doc = Document(page_numbers=True, <st c="47748">geometry_options=geometry_options</st>, <st c="47783">document_options=['10pt','legalpaper']</st>)
            doc.preamble.append(Command('title', 'Mean HPI per Country'))
            doc.preamble.append(Command('author', 'Sherwin John C. Tragura'))
            doc.preamble.append(Command('date', NoEscape(r'\today')))
            doc.append(NoEscape(r'\maketitle'))
        <st c="48045">然后,它设置了一个</st> <st c="48064">字典</st>,`<st c="48077">geometry_options</st>`<st c="48093">,它包含 LaTeX 文档参数,例如文档方向(</st>`<st c="48177">landscape</st>`<st c="48187">),左右、顶部和底部边距(</st>`<st c="48233">margin</st>`<st c="48240">),从页眉底部到第一段文字最顶部的垂直高度(</st>`<st c="48343">headsep</st>`<st c="48351">),从页眉顶部到开始页眉部分的行的空间(</st>`<st c="48429">headheight</st>`<st c="48440">),以及切换参数以包含或排除文档的页眉和页脚(</st>`<st c="48539">includeheadfoot</st>`<st c="48555">)。</st> <st c="48559">这个字典对于实例化</st> `<st c="48616">pylatex</st>`<st c="48623">的</st> `<st c="48627">Document container</st>` <st c="48645">类至关重要,该类将代表 LaTeX 文档。</st>

        <st c="48693">最初,LaTeX 文档将是一个带有通过其</st> `<st c="48803">geometry_option</st>` <st c="48818">构造函数参数和包含其他选项(如字体大小和纸张大小)的</st> `<st c="48849">document_options</st>` <st c="48865">列表的空白实例。</st> <st c="48934">然后,为了开始自定义文档,</st> `<st c="48979">view</st>` <st c="48983">函数使用</st> `<st c="49002">Command</st>` <st c="49009">类创建用于文档标题、作者和日期的定制值,而不进行转义,因此使用了</st> `<st c="49134">NoEscape</st>` <st c="49142">类,并将它们附加到</st> `<st c="49198">Document</st>` <st c="49206">实例的 preamble 属性。</st> <st c="49217">这个过程类似于调用</st> `<st c="49252">\title</st>`<st c="49258">,`<st c="49260">\author</st>`<st c="49267">,和</st> `<st c="49273">\date</st>` <st c="49278">命令,并通过</st> `<st c="49327">\</st>``<st c="49328">VAR{}</st>` <st c="49333">命令插入自定义值。</st>

        `<st c="49342">接下来,视图必须添加`<st c="49374">\maketitle</st>` `<st c="49384">命令,而不需要转义反斜杠,以排版所有这些添加的文档细节。</st>` `<st c="49469">在`<st c="49488">\maketitle</st>` `<st c="49498">之后的行总是生成正文内容,在我们的例子中,是以下章节:</st>
 with doc.create(Section('The Data Analysis')):
              doc.append('Here are the statistical analysis derived from the uploaded excel data.')
        `<st c="49713">The</st>` `<st c="49718">pylatex</st>` `<st c="49725">模块类与一些 LaTeX 命令等价,例如</st>` `<st c="49788">Axis</st>`<st c="49792">,</st> `<st c="49794">Math</st>`<st c="49798">,</st> `<st c="49800">Matrix</st>`<st c="49806">,</st> `<st c="49808">Center</st>`<st c="49814">,</st> `<st c="49816">Alignat</st>`<st c="49823">,</st> `<st c="49825">Alignref</st>`<st c="49833">, 和</st> `<st c="49839">Plot</st>`<st c="49843">。`<st c="49849">Command</st>` `<st c="49856">类是一个模块类,用于运行自定义或通用命令,例如</st>` `<st c="49928">\title</st>`<st c="49934">,</st> `<st c="49936">\author</st>`<st c="49943">, 和</st> `<st c="49949">\date</st>`<st c="49954">。在这个`<st c="49964">create_latex_pdf()</st>` `<st c="49982">视图中,内容生成从运行带有章节标题的`<st c="50037">Section</st>` `<st c="50044">命令</st>` `<st c="50052">开始,标题为` *<st c="50075">数据分析。</st> <st c="50094">A</st>* `<st c="50095">章节是内容的一个有组织的部分,包含表格、文本、图表和数学公式的组合。</st>` `<st c="50219">之后,视图以文本形式添加一条声明。</st>` `<st c="50274">由于没有反斜杠需要转义,因此没有必要用`<st c="50358">NoEscape</st>` `<st c="50366">类`将文本包裹起来。</st>` `<st c="50374">然后,我们创建以下片段中指示的子章节:</st>
 with doc.create(Subsection('Statistical analysis generated by Pandas')):
                    with doc.create(Tabular('| c | c | c | c | c | c | c | c | c |')) as table:
                        table.add_hline()
                        table.add_row(("Country", "Count", "Mean", "Std Dev", "Min", "25%", "50%", "75%", "Max"))
                        table.add_empty_row()
                        for key, value in hpi_data.items():
                            table.add_hline()
                            table.add_row((key, value['count'], value['mean'], value['std'], value['min'], value['25%'], value['50%'], value['75%'], value['max']))
                        table.add_empty_row()
                        table.add_hline()
        except:
            raise FileSavingException()
        `<st c="50987">在文本之后,视图添加一个`<st c="51023">Subsection</st>` `<st c="51033">命令,这将细化最近创建的章节的内容。</st>` `<st c="51111">其部分组件是`<st c="51140">Tabular</st>` `<st c="51147">命令,它将构建一个由提取的表格值派生出的 HPI 值的电子表格。</st>` `<st c="51247">在 LaTeX 内容的组装之后,`<st c="51294">create_latex_pdf()</st>` `<st c="51312">视图现在将生成用于呈现的 PDF,如下面的片段所示:</st>
 doc.generate_pdf(hpi_filename, clean_tex=False, compiler="pdflatex")
        return send_from_directory('./files/latex', 'hpi_analysis.pdf')
        <st c="51526">文档</st> <st c="51531">实例有一个</st> <st c="51539">generate_pdf()</st> <st c="51555">方法,它编译并生成 LaTeX 文件,将 LaTeX 文件转换为 PDF 格式,并将这两个文件保存到特定目录。</st> <st c="51708">一旦 PDF 可用,视图可以通过 Flask 的</st> <st c="51787">send_from_directory()</st> <st c="51808">方法渲染 PDF 内容。</st> *<st c="51817">图 6.16</st>**<st c="51825">.16</st>* <st c="51828">显示了</st> <st c="51863">create_latex_pdf()</st> <st c="51881">视图函数生成的 PDF。</st>

        ![图 6.16 – 由 pylatex 模块生成的 PDF](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_016.jpg)

        <st c="52320">图 6.16 – 由 pylatex 模块生成的 PDF</st>

        <st c="52371">除了渲染 PDF 内容外,Flask 还可以利用流行的前端库来显示图表和</st> <st c="52482">图表。</st> <st c="52490">让我们集中讨论 Flask 如何与</st> <st c="52541">这些</st> **<st c="52547">JavaScript</st>** <st c="52557">(</st>**<st c="52559">JS</st>**<st c="52561">)-based libraries 在</st> <st c="52583">可视化数据集</st>中集成。</st>

        <st c="52604">使用前端库构建图形图表</st>

        <st c="52654">大多数开发者更喜欢使用前端库来渲染</st> <st c="52678">图形和图表</st>,而不是使用需要复杂 Python 编码来完善展示且缺乏 UI 相关功能(如响应性、适应性、用户交互)的`matplotlib`。</st> <st c="52905">本节将重点介绍 Chart.js、`Bokeh`和`Plotly`库,这些库都是流行的可视化外部工具,具有不同的优势和劣势。</st> <st c="53064">。</st>

        <st c="53082">让我们从 Chart.js 开始。</st>

        <st c="53109">使用 Chart.js 进行绘图</st>

        <st c="53132">在许多可视化应用中最常见</st> <st c="53149">且最受欢迎的图表库是 Chart.js。</st> <st c="53231">它是 100%的 JS,轻量级,易于使用,并且具有设计图表和图表的直观语法。</st> <st c="53344">以下是一个 Chart.js 实现,用于显示某些国家的平均 HPI 值:</st> <st c="53426">。</st>
 <!DOCTYPE html>
<html lang="en">
<head>
  … … … … … …
  … … … … … … <st c="53508"><script src='https://cdn.jsdelivr.net/npm/chart.js'></script></st> </head>
<body>
    <h1>{{ title }}</h1>
    <form action="{{request.path}}" method="POST" enctype="multipart/form-data">
      Upload XLSX file:
      <input type="file" name="data_file"/><br/>
      <input type="submit" value="Upload File"/>
  </form><br/> <st c="53800"><canvas id="linechart" width="300" height="100"></canvas></st> </body>
<script> <st c="53875">var linechart = document.getElementById("linechart");</st><st c="53928">Chart.defaults.font.family = "Courier";</st><st c="53968">Chart.defaults.font.size = 14;</st><st c="53999">Chart.defaults.color = "black";</st>
        <st c="54031">Chart.js 有三种来源:</st>

            +   **<st c="54071">Node.js</st>**<st c="54079">:通过运行 npm 安装</st> <st c="54112">chart.js 模块。</st>

            +   **<st c="54128">GitHub</st>**<st c="54135">:通过下载</st> [<st c="54157">https://github.com/chartjs/Chart.js/releases/download/v4.4.0/chart.js-4.4.0.tgz</st>](https://github.com/chartjs/Chart.js/releases/download/v4.4.0/chart.js-4.4.0.tgz) <st c="54236">文件或可用的最新</st> <st c="54256">版本。</st>

            +   **<st c="54274">内容分发网络</st>** **<st c="54299">(CDN)</st>**<st c="54305">:通过</st> <st c="54310">引用</st> [<st c="54323">https://cdn.jsdelivr.net/npm/chart.js</st>](https://cdn.jsdelivr.net/npm/chart.js)<st c="54360">。</st>

        <st c="54361">根据 HTML 脚本,我们的实现选择了</st> <st c="54421">CDN 源。</st>

        <st c="54432">在引用 Chart.js 之后,创建一个宽度高度适合您图表的 `<st c="54470"><canvas></st>` <st c="54478">标签。</st> <st c="54530">然后,创建一个带有 `<st c="54593"><canvas></st>` <st c="54601">的节点或 2D 上下文和某些</st> <st c="54610">配置选项的 `<st c="54545">Chart()</st>` <st c="54552">实例。</st> <st c="54634">此外,为全局默认属性设置新的和适当的值,例如字体名称、字体大小和</st> <st c="54742">字体颜色:</st>
 new Chart(linechart,{ <st c="54776">type: 'line',</st><st c="54789">options:</st> { <st c="54801">scales:</st> { <st c="54811">y</st>: {
              beginAtZero: true,
              title: {
                display: true,
                text: 'Mean HPI'
              }
            }, <st c="54881">x</st>: {
              offset: true,
              title: {
                display: true,
                text: 'Countries with HPI'
              }
            }
          }
      }, <st c="54960">data</st>: {
          borderWidth: ,
          labels : [
            {% for item in labels %}
              "{{ item }}",
            {% endfor %}
          ],
        <st c="55049">`<st c="55054">data</st>` <st c="55058">属性提供了 x 轴标签、数据点和连接线。</st> 它的 `<st c="55135">datasets</st>` <st c="55143">子属性</st> 包含了实际数据的图表外观和感觉细节。</st> `<st c="55236">label</st>` <st c="55241">和 `<st c="55246">data</st>` <st c="55250">列表都是其 `<st c="55290">view</st>` <st c="55290">函数</st> 提供的上下文数据:</st>
 datasets: [{
              fill : true,
              barPercentage: 0.5,
              barThickness: 20,
              maxBarThickness: 70,
              borderWidth : 1,
              minBarLength: 5,
              backgroundColor: "rgba(230,112,16,0.88)",
              borderColor : "rgba(38,22,6,0.88)",
              label: 'Mean HPI values',
              data : [
                {% for item in values %}
                  "{{ item }}",
                  {% endfor %}
              ]
            }]
        }
      });
</script>
</html>
        <st c="55617">现在,Chart.js 也可以构建多个折线图、各种条形图、饼图和甜甜圈,所有这些都使用与提供的折线图相同的设置。</st> <st c="55771">运行带有给定 Chart.js <st c="55820">脚本的 view 函数将渲染一个折线图,如</st> *<st c="55870">图 6</st>**<st c="55878">.17</st>**<st c="55881">所示。</st>

        ![图 6.17 – 每个国家 HPI 值的折线图](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_017.jpg)

        <st c="56131">图 6.17 – 每个国家 HPI 值的折线图</st>

        <st c="56184">Chart.js 支持响应式网页设计和交互式结果,例如提供的折线图,在鼠标悬停在每个线点上时提供一些信息。</st> <st c="56355">尽管它很受欢迎,但 Chart.js 仍然使用 HTML canvas,这不能有效地渲染大型和复杂的图表。</st> <st c="56474">此外,它还缺少 Bokeh 和 Plotly 中存在的其他交互式实用工具。</st>

        <st c="56545">现在,让我们使用一个对 Python 更友好的模块来创建</st> <st c="56564">图表,**<st c="56608">Plotly</st>**<st c="56614">。</st>

        <st c="56615">使用 Plotly 创建图表</st>

        <st c="56643">Plotly</st> 也是一个基于 JS 的 <st c="56669">库,可以渲染交互式 <st c="56706">图表和图形。</st> 它是各种需要交互式数据可视化和 3D 图形效果的统计和数学项目的流行库,可以无缝地绘制 <st c="56891">DataFrame 数据集。</st>

        <st c="56910">为了利用其类和方法进行绘图,请通过</st> `<st c="56979">plotly</st>` <st c="56985">模块使用</st> `<st c="57005">pip</st>` <st c="57008">命令安装:</st>
 pip install plotly
        <st c="57036">以下视图函数使用 Plotly 创建关于买家按装修状态偏好分类的价格和卧室偏好的分组条形图:</st> <st c="57199">偏好:</st>
 import json <st c="57230">import plotly</st>
<st c="57243">import plotly.express as px</st> @rendition_bp.route("/plotly/csv/bedprice", methods = ['GET', 'POST'])
async def create_plotly_stacked_bar():
    if request.method == 'GET':
        graphJSON = '{}'
    else:
        … … … … … …
        try:
            df_csv = read_csv(uploaded_file) <st c="57483">fig = px.bar(df_csv, x='Bedrooms', y='Price',</st> <st c="57528">color='FurnishingStatus', barmode='group')</st><st c="57571">graphJSON = json.dumps(fig,</st> <st c="57599">cls=plotly.utils.PlotlyJSONEncoder)</st> except:
            raise FileSavingException()
    return render_template('plotly.html', <st c="57813">plotly.express</st> module, which provides several plotting utilities that can set up build graphs with DataFrame as input, similar to <st c="57943">matplotlib</st>’s methods. In the given <st c="57979">create_plotly_stacked_bar()</st> view function, the goal is to create a grouped bar chart using the <st c="58074">bar()</st> method from the <st c="58096">plotly.express</st> module with the <st c="58127">DataFrame</st> object’s tabular values derived from the uploaded CSV file. The result is a <st c="58213">Figure</st> in dictionary form containing the details of the desired plot.
			<st c="58282">After creating the</st> `<st c="58302">Figure</st>`<st c="58308">, the view function will pass the resulting dictionary to the Jinja2 template</st> <st c="58385">for</st> <st c="58390">rendition and display using Plotly’s JS library.</st> <st c="58439">However, JS can only understand the dictionary details if they are in JSON string format.</st> <st c="58529">Thus, use the</st> `<st c="58543">json.dumps()</st>` <st c="58555">method to convert the dictionary</st> `<st c="58589">fig</st>` <st c="58592">to string.</st>
			<st c="58603">The following is the Jinja template that will render the graph using the Plotly</st> <st c="58684">JS library:</st>

<!doctype html>

<head>

    <title>Plotly 条形图</title>

</head>

<body>

    … … … … … …

    {%if graphJSON == '{}' %}

        <p>没有图表图像。</p>

    {% else %} <st c="58844"><div id='chart' class='chart'></div></st> {% endif %}

</body> <st c="58901"><script src='https://cdn.plot.ly/plotly-latest.js'></script></st><st c="58961"><script type='text/javascript'></st><st c="58993">var graphs = {{ graphJSON | safe }};</st><st c="59030">Plotly.plot('chart', graphs, {});</st><st c="59064"></script></st> </html>

			<st c="59082">The HTML script must reference the latest Plotly library from CDN.</st> <st c="59150">Then, a JS script must interpolate the JSON-formatted</st> `<st c="59204">Figure</st>` <st c="59210">from the view function with a safe filter to spare it from HTML escaping.</st> <st c="59285">Also, the JS must apply the</st> `<st c="59313">plot()</st>` <st c="59319">method of the</st> `<st c="59334">Plotly</st>` <st c="59340">class library</st> <st c="59354">to</st> <st c="59357">render the figure through the HTML’s</st> `<st c="59395"><div></st>` <st c="59400">component.</st> *<st c="59412">Figure 6</st>**<st c="59420">.18</st>* <st c="59423">shows the bar graph generated by the</st> `<st c="59461">create_plotly_stacked_bar()</st>` <st c="59488">view function and displayed by its</st> <st c="59524">Jinja template.</st>
			![Figure 6.18 – A bar graph created by Plotly](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_018.jpg)

			<st c="59767">Figure 6.18 – A bar graph created by Plotly</st>
			<st c="59810">Like Chart.js, the chart provides information regarding a data plot when hovered by the mouse.</st> <st c="59906">However, it seems that Chart.js loads faster than Plotly when the data size of the</st> `<st c="59989">DataFrame</st>` <st c="59998">object’s tabular values increases.</st> <st c="60034">Also, there is limited support for colors for the background, foreground, and</st> <st c="60112">bar shades, so it is hard to</st> <st c="60141">construct a more</st> <st c="60158">original theme.</st>
			<st c="60173">The next JS library supports many popular PyData tools and can generate plots directly from</st> `<st c="60266">pandas'</st>` `<st c="60273">DataFrame</st>`<st c="60283">,</st> **<st c="60285">Bokeh</st>**<st c="60290">.</st>
			<st c="60291">Visualizing data using Bokeh</st>
			<st c="60320">Bokeh and</st> <st c="60331">Plotly are similar in many ways.</st> <st c="60364">They</st> <st c="60369">have interactive and 3D graphing features, and both need module installation.</st> <st c="60447">However, Bokeh is more Pythonic than Plotly.</st> <st c="60492">Because of that, it can transact more with DataFrame objects, especially those with</st> <st c="60576">large datasets.</st>
			<st c="60591">To utilize the library, first install its module using the</st> `<st c="60651">pip</st>` <st c="60654">command:</st>

pip install bokeh


			<st c="60681">Once installed, the module provides a figure class from its</st> `<st c="60742">bokeh.plotting</st>` <st c="60756">module, which is responsible for setting up the plot configuration.</st> <st c="60825">The following view implementation uses Bokeh to create a line graph showing the UK’s HPI values through</st> <st c="60929">the years:</st>

从 bokeh.plotting 导入 figure

从 bokeh.embed 导入 components @rendition_bp.route('/bokeh/hpi/line', methods = ['GET', 'POST'])

def create_bokeh_line():

if request.method == 'GET':

    script = None

    div = None

else:

    … … … … … …

    try:

        df = read_excel(uploaded_file, sheet_name=1, skiprows=[1])

        x = df.index.values

        y = df['UK'] <st c="61268">plot = figure(max_width=600, max_height=800,title=None, toolbar_location="below", background_fill_color="#FFFFCC", x_axis_label='按季度 ID 的时期', y_axis_label='名义 HPI')</st><st c="61447">plot.line(x,y, line_width=4, color="#CC0000")</st><st c="61493">script, div = components(plot)</st> except:

        raise FileSavingException()

return render_template('bokeh.html', script=script, div=div, title="英国名义 HPI 折线图")

			<st c="61661">After creating the</st> `<st c="61681">Figure</st>` <st c="61687">instance with the plot details, such as</st> `<st c="61728">max_width</st>`<st c="61737">,</st> `<st c="61739">max_height</st>`<st c="61749">,</st> `<st c="61751">background_fill_color</st>`<st c="61772">,</st> `<st c="61774">x_axis_label</st>`<st c="61786">,</st> `<st c="61788">y_axis_label</st>`<st c="61800">, and other</st> <st c="61812">related</st> <st c="61820">configurations, the view function can now invoke any of its</st> *<st c="61880">glyph</st>* <st c="61885">or plotting methods, such as</st> `<st c="61915">vbar()</st>` <st c="61921">for plotting vertical bar graph,</st> `<st c="61955">hbar()</st>` <st c="61961">for horizontal bar graph,</st> `<st c="61988">scatter()</st>` <st c="61997">for scatter plots, and</st> `<st c="62021">wedge()</st>` <st c="62028">for pie charts.</st> <st c="62045">The given</st> `<st c="62055">create_bokeh_line()</st>` <st c="62074">view utilizes the</st> `<st c="62093">line()</st>` <st c="62099">method to build a line graph with x and y values derived from the</st> <st c="62166">tabular values.</st>
			<st c="62181">After assembling the</st> `<st c="62203">Figure</st>` <st c="62209">and its plot, call the</st> `<st c="62233">components()</st>` <st c="62245">function from</st> `<st c="62260">bokeh.embed</st>` <st c="62271">to wrap the plot instance and extract a tuple of two HTML embeddable components, namely the script that will contain the data of the graph and the</st> `<st c="62419">div</st>` <st c="62423">component that contains the dashboard embedded in a</st> `<st c="62475"><div></st>` <st c="62480">tag.</st> <st c="62486">The function must pass these two components to its Jinja template for rendition.</st> <st c="62567">The following is the Jinja template that will render the</st> `<st c="62624">div</st>` <st c="62627">component:</st>

<head>

    <meta charset="utf-8">

    <title>Bokeh HPI</title> <st c="62727"><script src="img/bokeh-3.2.2.js"></script></st> </head>

<body>

    … … … … … …

    {%if div == None and script == None %}

        <p>没有图表图像。</p>

    {% else %} <st c="62900">{{ div | safe }}</st><st c="62916">{{ script | safe }}</st> {% endif %}

</body>

			<st c="62964">Be sure to have the</st> <st c="62985">latest</st> <st c="62992">Bokeh JS library in your HTML script.</st> <st c="63030">Since both</st> `<st c="63041">div</st>` <st c="63044">and</st> `<st c="63049">script</st>` <st c="63055">are HTML-embeddable components, the template will directly interpolate them with the filter safe.</st> *<st c="63154">Figure 6</st>**<st c="63162">.19</st>* <st c="63165">shows the outcome of rendering the</st> `<st c="63201">create_bokeh_line()</st>` <st c="63220">view function using</st> <st c="63241">the datasets:</st>
			![Figure 6.19 – A line graph created by Bokeh](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_019.jpg)

			<st c="63439">Figure 6.19 – A line graph created by Bokeh</st>
			<st c="63482">Compared to that</st> <st c="63500">of</st> <st c="63502">Plotly and Chart.js, the dashboard of Bokeh is so interactive that you can drag the plot in any direction within the canvas.</st> <st c="63628">It offers menu options to save, reset, and wheel- or box-zoom the graph.</st> <st c="63701">The only problem with Bokeh is its lack of flexibility when going out of the box for more interactive features.</st> <st c="63813">But generally, Bokeh has enough utilities and themes to build powerful</st> <st c="63884">embeddable graphs.</st>
			<st c="63902">From the degree of interactivity of the graphs and charts, let us shift our discussions to building real-time visualization approaches</st> <st c="64038">with Flask.</st>
			<st c="64049">Building real-time data plots using WebSocket and SSE</st>
			<st c="64103">Flask’s WebSocket</st> <st c="64121">and SSE, discussed</st> <st c="64140">in</st> *<st c="64144">Chapter 5</st>*<st c="64153">, are effective mechanisms</st> <st c="64180">for implementing real-time</st> <st c="64206">graphical plots.</st> <st c="64224">Although other third-party modules can provide Flask with real-time capabilities, these two are still the safest, most flexible, and standard techniques because they are</st> <st c="64394">web components.</st>
			<st c="64409">Let us start with applying WebSocket for</st> <st c="64451">real-time charts.</st>
			<st c="64468">Utilizing the WebSocket</st>
			<st c="64492">An application</st> <st c="64508">can have a WebSocket server that receives data from a form and sends it for plotting to a frontend visualization library.</st> <st c="64630">The following</st> `<st c="64644">flask-sock</st>` <st c="64654">WebSocket server immediately sends all the data it receives from a form page to the Chart.js script for</st> <st c="64759">data plotting:</st>

@sock.route('/ch06/ws/server/hpi/plot') def ws_server_plot(ws):

async def process():

    while True: <st c="64871">hpi_data_json = ws.receive()</st> hpi_data_dict = loads(hpi_data_json) <st c="64937">json_data = dumps(</st><st c="64955">{'period': f"Y{hpi_data_dict['year']}</st> <st c="64993">Q{hpi_data_dict['quarter']}"</st>, <st c="65024">'hpi': float(hpi_data_dict['hpi'])})</st><st c="65060">ws.send(json_data)</st> run(process())

			<st c="65094">The Chart.js script will receive the JSON data as a WebSocket message, scrutinize it, and push it immediately as new labels and dataset values.</st> <st c="65239">The following snippet shows the frontend script that manages the WebSocket communication with the</st> `<st c="65337">flask-sock</st>` <st c="65347">server:</st>

const socket = new WebSocket('ws://' + location.host + '/ch06/ws/server/hpi/plot'); socket.addEventListener('message', msg => { const data = JSON.parse(msg.data); if (config.data.labels.length === 20) {

            config.data.labels.shift();

            config.data.datasets[0].data.shift();

        } <st c="65627">config.data.labels.push(data.period);</st><st c="65664">config.data.datasets[0].data.push(data.hpi);</st><st c="65709">lineChart.update();</st> });

			<st c="65733">The real-time line chart update occurs at every form submission of the new HPI and date values to the</st> <st c="65835">WebSocket server.</st>
			<st c="65853">Next, let’s see how we can use SSE with Redis as the</st> <st c="65907">broker storage.</st>
			<st c="65922">Using SSE</st>
			<st c="65932">If WebSocket does</st> <st c="65950">not fit the requirement, SSE can be a possible solution to real-time data plotting.</st> <st c="66035">But first, it requires the installation of the Redis database server and the</st> `<st c="66112">redis-py</st>` <st c="66120">module and the creation of the</st> `<st c="66152">redis-config.py</st>` <st c="66167">file for the</st> `<st c="66181">Blueprint</st>` <st c="66190">approach.</st> <st c="66201">The following code shows the configuration of the Redis client instance in</st> <st c="66276">our application:</st>

from redis import Redis

redis_conn = Redis(

db = 0,

host='127.0.0.1',

port=6379,

decode_responses=True

)


			<st c="66397">Place this</st> `<st c="66409">redis-config.py</st>` <st c="66424">file in the project directory</st> <st c="66455">with</st> `<st c="66460">main.py</st>`<st c="66467">.</st>
			<st c="66468">Now, the role of the Redis server is to create a channel where a view function can push the submitted form data containing the data values.</st> <st c="66609">The SSE implementation will subscribe to the message channel, listen to incoming messages, retrieve the recently published message, and yield the JSON data to the frontend plotting library.</st> <st c="66799">Our application still uses Chart.js for visualization, and here is a snippet that listens to the event stream for</st> <st c="66912">new data plots in</st> <st c="66931">JSON format:</st>

var source = new EventSource("/ch06/sse/hpi/data/stream"); source.onmessage = function (event) {

        const data = JSON.parse(event.data);

        if (config.data.labels.length === 20) {

            config.data.labels.shift();

            config.data.datasets[0].data.shift();

        } <st c="67186">config.data.labels.push(data.period);</st><st c="67223">config.data.datasets[0].data.push(data.hpi);</st> lineChart.update();

    };

			<st c="67291">Like the WebSocket approach, the given frontend script will listen to the stream, receive the JSON data, and validate it before pushing it to the current labels</st> <st c="67453">and datasets.</st>
			<st c="67466">Overall, WebSockets and SSE are not limited to web messaging because they can help establish real-time visualization components for many scientific applications to help solve problems that require</st> <st c="67664">impromptu analysis.</st>
			<st c="67683">Let us now focus on</st> <st c="67703">how Flask can implement computations that consume more server resources and effort and even create higher contention with</st> <st c="67826">other components.</st>
			<st c="67843">Using asynchronous background tasks for resource-intensive computations</st>
			<st c="67915">There are</st> <st c="67926">implementations</st> <st c="67941">of many approximation algorithms and P-complete problems that can create memory-related issues, thread problems, or even memory leaks.</st> <st c="68077">To avoid imminent problems when handling solutions for NP-hard problems with indefinite data sets, implement the solutions using asynchronous</st> <st c="68219">background tasks.</st>
			<st c="68236">But first, install the</st> `<st c="68260">celery</st>` <st c="68266">client using the</st> `<st c="68284">pip</st>` <st c="68287">command:</st>

pip install celery


			<st c="68315">Also, install the Redis database server for its broker.</st> <st c="68372">Place</st> `<st c="68378">celery_config.py</st>`<st c="68394">, which contains</st> `<st c="68411">celery_init_app()</st>`<st c="68428">, in the project directory and call the method in the</st> `<st c="68482">main.py</st>` <st c="68489">module.</st>
			<st c="68497">After the setup and installations, create a service package in the</st> `<st c="68565">Blueprint</st>` <st c="68574">module folder.</st> `<st c="68590">ch06-project</st>` <st c="68602">has the following Celery task in the</st> `<st c="68640">hpi_formula.py</st>` <st c="68654">service module found in the</st> `<st c="68683">internal</st>` <st c="68691">Blueprint module:</st>

@shared_task(ignore_result=False)

def compute_hpi_laspeyre(df_json): async def compute_hpi_task(df_json): try:

        df_dict = loads(df_json)

        df = DataFrame(df_dict)

        df["p1*q0"] = df["p1"] * df["q0"]

        df["p0*q0"] = df["p0"] * df["q0"]

        print(df)

        numer = df["p1*q0"].sum()

        denom = df["p0*q0"].sum()

        hpi = numer/denom

        return hpi

    except Exception as e:

        return 0

return <st c="69098">compute_hpi_laspeyre()</st> 运行一个异步任务,使用 Laspeyre 公式计算 HPI 值,输入包括特定房屋偏好的房价和特定年份购买该房屋的客户数量。当给定大量数据时,计算将需要更长的时间,因此当发生最坏情况时,使用异步 Celery 任务运行公式可能会提高其运行时的执行效率。

        <st c="69535">始终将重负载和资源密集型计算或进程在视图函数的线程之外运行,使用异步后台任务,这是一种良好的实践。</st> <st c="69704">它还采用了请求-响应事务与数值算法之间的松散耦合,这有助于避免这些进程的降级和饥饿。</st> <st c="69863">。</st>

        <st c="69879">将</st> <st c="69892">流行的</st> <st c="69900">数值和符号软件集成到 Flask 平台中,在处理现有科学项目时有时可以节省迁移时间。</st> <st c="70038">现在让我们探索 Flask 与</st> <st c="70103">Julia 语言集成的能力。</st>

        <st c="70118">将 Julia 包与 Flask 集成</st>

        **<st c="70158">Julia</st>** <st c="70164">是一种功能强大的</st> <st c="70183">编译型编程</st> <st c="70203">语言,提供了数学和符号库。</st> <st c="70264">它包含用于数值计算的简单语法,并为其应用程序提供了更好的运行时性能。</st>

        <st c="70385">尽管 Julia 有 Genie、Oxygen 和 Bukdu 等网络框架,可以实现基于 Julia 的网络应用程序,但 Flask 应用程序也可以运行并从</st> <st c="70580">Julia 函数中提取值。</st>

        首先,从 [<st c="70648">https://julialang.org/downloads/</st>](https://julialang.org/downloads/) 下载最新的 Julia 编译器并将其安装到您的系统上。将旧版本的 Julia 安装到更新的 Windows 操作系统中会导致系统崩溃,如 *<st c="70818">图 6</st>**<st c="70826">.20</st>*<st c="70829">* 所示。

        ![图 6.20 – 由于 Flask 运行过时的 Julia 而导致的系统崩溃](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_020.jpg)

        图 6.20 – 由于 Flask 运行过时的 Julia 而导致的系统崩溃

        现在,让我们看看创建和将 Julia 软件包集成到 `<st c="71294">Flask 应用</st>` 中的步骤。

        创建自定义 Julia 软件包

        安装完成后,通过控制台进入 Flask 应用的项目目录,并运行 `<st c="71472">julia</st>` `<st c="71477">命令</st>` 打开 Julia 壳。然后,按照 `<st c="71487">以下</st>` `<st c="71500">说明</st>` 进行操作:

            1.  使用 `<st c="71519">Pkg</st>` 在 `<st c="71542">Pkg</st>` 上 `<st c="71545">运行</st>` `<st c="71549">shell</st>` 命令。

            1.  通过运行以下命令在 `<st c="71559">Flask 应用目录</st>` 中创建一个 `<st c="71569">Julia</st>` `<st c="71574">软件包</st>`:

```py
 Pkg.generate("Ch06JuliaPkg")
```

                1.  通过运行以下命令安装 `<st c="71672">PythonCall</st>` `<st c="71685">插件</st>`:

```py
 Pkg.add("PythonCall")
```

                1.  此外,安装 Julia 软件包,如 `<st c="71796">DataFrame</st>`<st c="71805">`、`<st c="71807">Pandas</st>`<st c="71813">` 和 `<st c="71819">Statistics</st>`,以便在 `<st c="71878">Julia 环境</st>` 中转换和运行 Python 语法。

            1.  最后,运行 `<st c="71910">Pkg.resolve()</st>` `<st c="71923">和</st>` `<st c="71928">Pkg.instantiate()</st>` `<st c="71945">以完成</st>` `<st c="71958">设置</st>`。

        接下来,我们将安装 `<st c="71993">juliacall</st>` 客户端模块并将与 Julia 相关的配置细节添加到 **<st c="72072">TOML</st>** `<st c="72076">文件</st>` 中。

        配置 Flask 项目中的 Julia 可访问性

        在 Flask 应用内部创建一个 `<st c="72151">Julia 自定义软件包</st>` 后,打开应用的 `<st c="72209">config_dev.toml</st>` `<st c="72224">文件</st>` 并添加以下环境变量以将 Julia 集成到 `<st c="72302">Flask 平台</st>` 中:

            +   `<st c="72317">PYTHON_JULIAPKG_EXE</st>`:到 `<st c="72356">julia.exe</st>` `<st c="72365">文件</st>` 的路径,包括文件名(例如,`<st c="72396">e.g.</st>` `<st c="72403">C:/Alibata/Development/Language/Julia-1.9.2/bin/julia</st>` `<st c="72456">)。</st>`

            +   `<st c="72459">PYTHON_JULIAPKG_OFFLINE</st>` `<st c="72483">:设置为</st>` `<st c="72493">yes</st>` `<st c="72496">以停止在</st>` `<st c="72531">后台</st>` `<st c="72531">的任何 Julia 安装。</st>`

            +   `<st c="72546">PYTHON_JULIAPKG_PROJECT</st>` `<st c="72570">:在 Flask 应用程序内部新创建的自定义 Julia 包的路径(</st>` `<st c="72646">例如</st>` `<st c="72653">C:/Alibata/Training/Source/flask/mastering/ch06-web-final/Ch06JuliaPkg/</st>` `<st c="72724">)。</st>`

            +   `<st c="72727">JULIA_PYTHONCALL_EXE</st>` `<st c="72748">:虚拟环境 Python 编译器的路径,包括文件名(</st>` `<st c="72835">例如</st>` `<st c="72842">C:/Alibata/Training/Source/flask/mastering/ch06-web-env/Scripts/python</st>` `<st c="72912">)。</st>`

        `<st c="72915">之后,通过</st>` `<st c="72939">juliacall</st>` `<st c="72948">模块通过</st>` `<st c="72968">pip</st>` `<st c="72971">命令安装:</st>`
 pip install juliacall
        在 Flask 设置之后,现在让我们在 Julia 包内部创建 Julia 代码。

        在包中实现 Julia 函数

        在 Python `<st c="73128">配置</st>` `<st c="73145">之后,打开</st>` `<st c="73166">ch06-web-final\Ch06JuliaPkg\src\Ch06JuliaPkg.jl</st>` `<st c="73213">并使用导入的</st>` `<st c="73264">PythonCall</st>` `<st c="73274">包创建一些 Julia 函数,如下面的代码片段所示:</st>` `<st c="73296">:</st>`
 module Ch06JuliaPkg <st c="73335">using PythonCall</st>
<st c="73351">const re = PythonCall.pynew()</st> # import re <st c="73394">const np = PythonCall.pynew()</st> # import numpy
function __init__() <st c="73459">PythonCall.pycopy!(re, pyimport("re"))</st><st c="73497">PythonCall.pycopy!(re, pyimport("numpy"))</st> end <st c="73544">function sum_array(data_list)</st><st c="73573">total = 0</st><st c="73583">for n in eachindex(data_list)</st><st c="73613">total = total + data_list[n]</st><st c="73642">end</st><st c="73646">return total</st> end
export sum_array
end # module Ch06JuliaPkg
        Julia 包内部的所有语法都必须是有效的 Julia 语法。因此,给定的 `<st c="73787">sum_array()</st>` `<st c="73798">是 Julia 包。</st>` `<st c="73819">另一方面,导入 Python 模块需要通过 `<st c="73876">实例化</st>` `<st c="73893">PythonCall</st>` `<st c="73903">通过</st>` `<st c="73912">pynew()</st>` `<st c="73919">,并且实际的模块映射发生在其</st>` `<st c="73966">__init__()</st>` `<st c="73976">初始化方法</st>` `<st c="73999">通过</st>` `<st c="74007">pycopy()</st>` `<st c="74015">。</st>`

        创建 Julia 服务模块

        要访问自定义 Julia 包中的函数,例如 `<st c="74112">Ch06JuliaPkg</st>` `<st c="74124">,创建一个服务模块,该模块将激活 `<st c="74169">Ch06JuliaPkg</st>` `<st c="74181">并创建一个 Julia 模块,该模块将在特定的 `<st c="74273">Blueprint</st>` `<st c="74282">部分</st>` `<st c="74292">中执行 Flask 中的 Julia 命令。</st>` 《以下是从外部 `<st c="74313">\modules\external\services\julia_transactions.py</st>` `<st c="74361">服务模块中需要的</st>` `<st c="74395">Blueprint</st>` `<st c="74404">执行 juliacall</st>` `<st c="74421">执行:</st>`
 import juliacall
from juliacall import Pkg as jlPkg
jlPkg.activate(".\\Ch06JuliaPkg")
jl = juliacall.newmodule("modules.external.services")
jl.seval("using Pkg")
jl.seval("Pkg.instantiate()")
jl.seval("using Ch06JuliaPkg")
jl.seval("using DataFrames")
jl.seval("using PythonCall")
        <st c="74723">在每次启动</st> <st c="74741">Flask 服务器时,应用程序总是激活 Julia 包,因为应用程序总是加载所有蓝图的服务。</st> *<st c="74886">图 6</st>**<st c="74894">.21</st>* <st c="74897">显示了 Flask 应用程序服务器日志中的激活过程:</st>

        ![图 6.21 – 服务器启动期间 Julia 包激活日志](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_06_021.jpg)

        <st c="75365">图 6.21 – 服务器启动期间 Julia 包激活日志</st>

        <st c="75429">激活可能会降低服务器的启动时间,这对 Flask 来说是一个缺点。</st> <st c="75537">如果这种性能问题恶化,建议将所有实现迁移到流行的 Julia Web 框架,如 Oxygen、Genie 和 Bukduh,而不是进一步追求</st> `<st c="75724">Flask 集成</st>`。</st>

        <st c="75742">现在,为了使视图函数能够访问 Julia 函数,请向激活发生的</st> `<st c="75825">Blueprint</st>` <st c="75834">服务中添加服务方法。</st> <st c="75873">在我们的项目中,</st> `<st c="75893">modules\external\services\julia_transactions.py</st>` <st c="75940">服务模块实现了以下</st> `<st c="75981">total_array()</st>` <st c="75994">服务,以暴露</st> `<st c="76017">sum_array()</st>` <st c="76028">函数</st> <st c="76038">在</st> `<st c="76041">Ch06JuliaPkg</st>`<st c="76053">:</st>
 async def total_array(arrdata): <st c="76088">result = jl.seval(f"sum_array({arrdata})")</st> return result
        <st c="76144">Julia 模块或</st> `<st c="76165">jl</st>`<st c="76167">,使用其</st> `<st c="76179">seval()</st>` <st c="76186">方法,是访问和执行 Flask 服务中自定义或内置 Julia 函数的一个。</st> <st c="76290">鉴于所有应用程序都正确遵循了所有安装和设置,运行</st> `<st c="76381">jl.seval()</st>` <st c="76391">不应导致任何系统崩溃或</st> `<st c="76427">HTTP 状态 500</st>`<st c="76442">。再次强调,执行</st> `<st c="76493">jl.seval()</st>` <st c="76503">的 Python 服务函数必须放置在 Julia 包激活发生的服务模块中。</st>

        <st c="76585">总结</st>

        <st c="76593">Flask 3.0 是构建科学应用的最佳 Flask 版本,因为它具有异步特性和 asyncio 支持。</st> <st c="76734">异步 WebSocket、SSE、Celery 后台任务和服务,以及数学和计算模块,如</st> `<st c="76872">numpy</st>`<st c="76877">、</st> `<st c="76879">matplotlib</st>`<st c="76889">、</st> `<st c="76891">sympy</st>`<st c="76896">、</st> `<st c="76898">pandas</st>`<st c="76904">、</st> `<st c="76906">scipy</st>`<st c="76911">和</st> `<st c="76917">seaborn</st>`<st c="76924">,是构建强调可视化、计算和</st> <st c="77025">统计分析的应用程序的核心成分。</st>

        <st c="77046">正如本章所证明的,Flask 支持 LaTeX 文档的生成、更新和呈现,包括其 PDF 转换。</st> <st c="77172">这一特性对于大多数需要存档、报告和记录管理的科学计算至关重要。</st> <st c="77265">管理。</st>

        <st c="77284">本章中,Flask 对可视化的支持也是明确的,从实时数据绘图到 matplotlib 模块的本地绘图。</st> <st c="77416">matplotlib</st> <st c="77426">模块。</st> <st c="77435">Flask 可以无缝且直接地利用基于 JS 的库来绘制 DataFrame 对象的表格值。</st>

        <st c="77570">尽管目前还不稳定,但 Julia 与 Flask 的集成展示了互操作性属性在 Flask 中的工作方式。</st> <st c="77690">使用</st> `<st c="77696">PythonCall</st>` <st c="77706">和</st> `<st c="77711">JuliaCall</st>` <st c="77720">模块,只要设置和配置正确,现在就可以在 Flask 中运行现有的 Julia 函数。</st> <st c="77829">正确。</st>

        <st c="77841">总之,Flask,尤其是 Flask 的异步版本,是构建基于 Web 的科学应用的最佳选择。</st> <st c="77980">下一章将讨论 Flask 如何利用 NoSQL 数据库并解决一些大数据需求。</st> <st c="78068">数据。</st>



第八章:7

使用非关系型数据存储

大多数使用大量或大数据,并且用户交易持续增加的应用程序,不使用关系型数据库进行存储。 例如,科学信息管理系统、与销售相关的应用程序、股票和投资相关软件以及位置查找器等应用程序可能会利用来自各种类型的数据结构(如对象、列表、字典和字节)的数据。 这些数据可以是结构化的(例如,Excel 格式的医疗记录和 CSV 格式的位置数据),半结构化的(例如,销售库存的 XML 数据和电子邮件),以及非结构化的(例如,图像、视频、社交媒体帖子以及 Word 文档)。 关系型数据库没有支持管理此类数据,但 NoSQL 数据库可以。

NoSQL,代表 不仅限于 SQL,是一种无模式的存储形式,没有行和列的概念来存储信息记录。 像任何框架一样,Flask,当用于构建大数据应用程序时,可以支持访问这些非关系型数据库,以管理数据用于数据挖掘、建模、分析和 图形投影。

本章的主要目标是展示如何安装和配置不同的 NoSQL 数据库,以及 Flask 应用程序如何连接到这些数据库并执行 INSERT, UPDATE, DELETE, 和 QUERY 事务。

本章将涵盖以下主题——这将为您构建大数据应用程序 使用 Flask

  • 使用 Apache HBase 管理非关系型数据

  • 利用 Apache Cassandra 的列存储

  • 在 Redis 中存储搜索数据

  • 使用 MongoDB 处理 BSON 文档

  • 使用 Couchbase 管理基于键的 JSON 文档

  • 与 Neo4J 建立数据关系

技术要求

本章重点介绍一个 导师查找器 应用程序,该应用程序接受学生和导师的资料。 该应用程序的主要目标是提供一个平台,供寻找不同专业导师或教练的个人学生使用。 除了资料外,它还有一个 支付模块 ,学生可以根据支付方式支付导师的费用, 课程模块 用于课程详情,以及 搜索模块 用于查找合适的导师和学生资料。 该应用程序独特且实验性,因为它展示了所有 NoSQL 数据库作为其后端存储,作为本章的示例。 另一方面,该应用程序利用了 工厂模式 作为其主要项目结构设计。 所有文件均可在 https://github.com/PacktPublishing/Mastering-Flask-Web-Development/tree/main/ch07找到。

使用 Apache HBase 管理非关系型数据

最受欢迎的 NoSQL 数据库之一是 <st c="3208">byte[]</st> 类型。 这种 <st c="3226">byte[]</st> 数据由列族处理,列族由 列限定符 组成,每个存储在一个 单元格中。每个列限定符对应一个 数据字段 ,并带有 时间戳 ,用于跟踪每次更新中每个列字段的版本。

关于列式数据库,本章将专注于将 Apache HBase 集成到我们的 Flask 应用程序中。 最初,就像任何数据库一样,我们将首先设计 HBase 表,然后再将其 集成到 Flask 中。

设计 HBase 表

使用关系数据库的一个优势是,有许多设计工具可以帮助我们使用不同的规范化级别来规划和组织表模式。 只有少数数据建模 工具,例如 <st c="4168">支付</st> <st c="4181">预订</st> HBase 表使用 UML 类图方法。 图 7**.1 显示了 <st c="4285">支付</st> <st c="4298">预订</st> 表的 UML 设计:

图 7.1 – 付款和预订的 HBase 表设计

图 7.1 – 付款和预订的 HBase 表设计

<st c="4434">payments</st> <st c="4447">bookings</st> 上下文表示 HBase 数据库的两个表。 <st c="4515">payments</st> 表有两个列族,即 <st c="4562">PaymentDetails</st> <st c="4581">PaymentItems</st><st c="4599">bookings</st> 表有一个列族,即 <st c="4629">BookingDetails</st>

请注意 <st c="4663">PaymentDetails</st> 包含 <st c="4682">id</st>, <st c="4686">stud_id</st>, <st c="4695">tutor_id</st>, <st c="4705">ccode</st>, 和 <st c="4716">fee</st> 作为列限定符,而 <st c="4748">PaymentItems</st> 包含 <st c="4765">id</st>, <st c="4769">receipt_id</st>, 和 <st c="4785">amount</st>。此外, <st c="4806">BookingDetails</st> 包含 <st c="4825">id</st>, <st c="4829">tutor_id</st>, <st c="4839">stud_id</st>, 和 <st c="4852">date_booked</st> 列。 一个 JSON 格式的示例记录将看起来像这样:

 "payments": {
  "<st c="4940">details:id</st>": 1001, "<st c="4962">details:stud_id</st>": "STD-001",
  "<st c="4994">details:tutor_id</st>": "TUT-001", "<st c="5027">details:ccode</st>": "PY-100",
  "<st c="5056">details:fee</st>": 5000.00", "<st c="5083">items:id</st>": 1001,
  "<st c="5103">items:receipt_id</st>": "OR-901", "<st c="5135">items:amount</st>": 3000.00"
}
"bookings" : {
   "<st c="5179">details:id</st>": 101, "<st c="5200">details:tutor_id</st>": TUT-002",
   "<st c="5232">details:stud_id</st>": "STD-201",
   "<st c="5264">details:date_booked</st>": "2023-10-10"
}

当通过 Flask 应用程序访问时,列限定符的实际名称是列族和列名本身的 连接 给定记录的一个示例 <st c="5481">details:stud_id</st>

现在我们已经设计了 表结构,让我们看看我们如何安装和配置 Apache HBase 和 Apache Hadoop 平台。 我们将从 使用 Java 开始。

设置基本要求

Apache HBase 是一个基于 Java 的平台,并且所有 其组件 都依赖于 <st c="5933">JAVA_HOME</st> 系统环境变量在您的 Windows、Linux 或 macOS 环境中,并更新 <st c="6027">CLASSPATH</st> 以帮助 HBase 访问 Java 的 <st c="6085">/bin</st> 文件夹中的 JDK 命令,以便它可以执行服务器启动和 关闭操作。

由于 Apache HBase 是一个使用 <st c="6255">hadoop-3.3.6/hadoop-3.3.6.tar.gz</st> 文件(来自 https://hadoop.apache.org/releases.html并将其解压缩到与 HBase 安装文件夹相同的本地驱动器上。 创建一个 <st c="6424">HADOOP_HOME</st> 文件,并更新操作系统的 <st c="6456">CLASSPATH</st> 变量,以便在启动时使 Hadoop <st c="6518">/bin</st> 命令对 HBase 可用。 让我们简要了解一下 Apache Hadoop 框架,以便更好地理解。

配置 Apache Hadoop

Apache Hadoop 是一个基于 Java 的 框架,它管理着分布式集群设置中的可扩展大数据处理。 它因其 MapReduce 算法而流行,该算法在节点集群上并行执行数据处理,使框架的分布式操作快速。 此外,该 框架具有 HDFS 文件系统。 这是它包含输入和 输出数据集的地方。

The MapReduce 过程始于这些大数据集通过 Flask 应用程序存储在 HDFS 中。 它们通过内部 Hadoop 服务器传递,这些服务器运行 Map() 函数,将数据分解成键值对的元组。 然后,这些键值数据块组经过另一个称为 Reduce() 的过程,由其他 Hadoop 服务器执行。 这将这些数据块暴露给各种 reduce 函数,如求和、平均、连接、压缩、排序和洗牌,然后将其作为输出数据集 存储在 HDFS 中。

除了 MapReduce 分布式数据处理之外,HBase 需要使用 Hadoop 的 HDFS,因为它为 HBase 平台提供了高延迟的批量处理操作。 作为回报,HBase 允许读写事务访问存储在 HDFS 中的数据,并且可以提供一个 thrift 服务器,以便第三方应用程序可以访问 大数据集。

重要提示

一个 Thrift 服务器 是 HBase 中一个兼容 Hive 的接口,它支持多语言,允许使用 Python、Java、C#、C++、NodeJS、Go、PHP 和 JavaScript 开发的应用程序访问大数据。 另一方面,术语 Hive指的是运行在 Hadoop 之上并具有强大 SQL 工具的客户端应用程序,这些工具用于实现大型数据集的 CRUD 操作。

以下是使用单节点集群设置 Hadoop 平台的步骤:

  1. 前往 Apache Hadoop 3.3.6 的安装文件夹,并打开 <st c="8521">/etc/hadoop/core-site.xml</st>。然后,设置 <st c="8562">fs.defaultFS</st> 属性。 这表示集群中 NameNode (主节点)的默认位置 – 在我们的案例中,是默认的 文件系统。 它的值是一个 URL 地址,DataNode (从节点)将向其发送心跳。 NameNode 包含存储在 HDFS 中的数据的元数据,而 DataNode 包含大数据集。 以下是我们的 <st c="8895">core-site.xml</st> 文件:

     <configuration>
        <property> <st c="8942"><name>fs.defaultFS</name></st><st c="8967"><value>hdfs://localhost:9000</value></st> </property>
    </configuration>
    
  2. 在同一个安装文件夹内,创建一个名为 <st c="9101">data</st> 的自定义文件夹,其中包含两个子文件夹,分别命名为 <st c="9134">datanode</st> <st c="9147">namenode</st>,如图 7.2 所示.2**。这些文件夹最终将包含 DataNode NameNode的配置文件,分别:

图 7.2 – DataNode 和 NameNode 配置文件夹

图 7.2 – DataNode 和 NameNode 配置文件夹

  1. 接下来,打开 <st c="9453">/etc/hadoop/hdfs-site.xml</st> 并声明新创建的 <st c="9509">datanode</st> <st c="9522">namenode</st> 文件夹作为各自节点的最终配置位置。 此外,将 <st c="9610">dfs.replication</st> 属性设置为 <st c="9638">1</st> ,因为我们只有一个节点 集群用于我们的 Tutor Finder 项目。 以下是我们的 <st c="9723">hdf-site.xml</st> 文件:

     <configuration>
     <property> <st c="9769"><name>dfs.replication</name></st><st c="9797"><value>1</value></st> </property>
       <property> <st c="9838"><name>dfs.namenode.name.dir</name></st><st c="9872"><value></st><st c="9880">file:///C:/Alibata/Development/Database/hadoop-3.3.6/data/namenode</value></st> </property>
       <property> <st c="9979"><name>dfs.datanode.data.dir</name></st><st c="10013"><value></st><st c="10021">file:///C:/Alibata/Development/Database/hadoop-3.3.6/data/datanode</value></st> </property>
    </configuration>
    
  2. 由于我们的项目将使用安装在 Windows 上的 Hadoop,请从 <st c="10195">hadoop-3.3.6-src.tar.gz</st> 文件从 <st c="10229">https://hadoop.apache.org/releases.html</st> 下载并使用 Maven 编译 Hadoop 源文件以生成 Windows 的 Hadoop 二进制文件,例如 <st c="10366">winutils.exe</st>, <st c="10380">hadoop.dll</st>, 和 <st c="10396">hdfs.dll</st>。将这些文件放入 <st c="10432">/</st>``<st c="10433">bin</st> 文件夹。

  3. 通过在命令行运行以下命令来格式化新的活动 NameNode(s):

     hdfs namenode -format
    

    此命令将清理 NameNode(s) 如果它们有现有的 存储元数据。

现在,我们可以开始设置 一个与 Apache Hadoop 3.3.6 兼容的 Apache HBase 版本。

配置 Zookeeper 和 Apache HBase

Apache HBase 在运行其集群时依赖于 Apache Zookeeper ,因此下一步是安装和配置一个 Zookeeper 服务器。 Apache Zookeeper 是一个高性能服务,通过提供同步和集中式服务来管理分布式和基于云的应用程序,并维护这些应用程序的详细信息。 请注意,此项目使用与 HBase 捆绑的 Zookeeper,因此除非设置涉及 多个集群,否则您不应单独安装 Zookeeper。

现在,下载 Apache HBase 2.5.5,与 Apache Hadoop 3.3.6 最兼容的 HBase 发行版,到 Apache Hadoop 3.3.6。 将其解压到 Hadoop 所在的文件夹。 然后,通过执行以下步骤来配置 HBase:

  1. 首先,创建一个系统环境变量 <st c="11480">HBASE_HOME</st> 来注册 HBase 安装文件夹。

  2. 在安装文件夹内创建两个文件夹, <st c="11616">hbase</st> <st c="11626">zookeeper</st>。这些将分别作为 HBase 和内置 Zookeeper 服务器的根文件夹。

  3. 在安装文件夹中,打开 <st c="11769">/conf/hbase-site.xml</st>。在此,设置 <st c="11805">hbase.rootdir</st> 属性,使其指向 <st c="11853">hbase</st> 文件夹,并设置 <st c="11874">hbase.zookeeper.property.dataDir</st> 属性,使其指向 <st c="11941">zookeeper</st> 文件夹。 现在,注册 <st c="11977">hbase.zookeeper.quorum</st> 属性。 这将指示 Zookeeper 服务器的主机。 然后,设置 <st c="12072">hbase.cluster.distributed</st> 属性。 这将指定 HBase 服务器设置的类型。 以下是我们 <st c="12179">hbase-site.xml</st> 文件:

     <configuration>
      <property> <st c="12227"><name>hbase.cluster.distributed</name></st><st c="12265"><value>false</value></st> </property>
      <property> <st c="12310"><name>hbase.tmp.dir</name></st><st c="12336"><value>./tmp</value></st> </property>
      <property> <st c="12380"><name>hbase.rootdir</name></st><st c="12406"><value></st><st c="12414">file:///C:/Alibata/Development/Database/hbase-2.5.5/hbase</value></st> </property>
     <property> <st c="12504"><name>hbase.zookeeper.property.dataDir</name></st><st c="12549"><value></st><st c="12557">/C:/Alibata/Development/Database/hbase-2.5.5/zookeeper</value></st> </property>
     <property> <st c="12644"><name>hbase.zookeeper.quorum</name></st><st c="12679"><value>localhost</value></st> </property>
       … … … … … …
    </configuration>
    
  4. 接下来,打开 <st c="12756">/bin/hbase.cmd</st> 如果您使用的是 Windows,并搜索 <st c="12811">java_arguments</st> 属性。 删除 <st c="12843">%HEAP_SETTINGS%</st> 以便新的声明将是 如下所示:

     set java_arguments=%HBASE_OPTS% -classpath "%CLASSPATH%" %CLASS% %hbase-command-arguments%
    
  5. 打开 <st c="13001">/conf/hbase-env.cmd</st> 并添加 以下 <st c="13043">JAVA_HOME</st> <st c="13057">HBASE_*</st> 详细信息 文件:

     set JAVA_HOME=%JAVA_HOME%
    set HBASE_CLASSPATH=%HBASE_HOME%\lib\client-facing-thirdparty\*
    set HBASE_HEAPSIZE=8000
    set HBASE_OPTS="-Djava.net.preferIPv4Stack=true"
    set SERVER_GC_OPTS="-verbose:gc" <st c="13282">"-Xlog:gc*=info:stdout" "-XX:+UseG1GC"</st><st c="13320">"-XX:MaxGCPauseMillis=100" "-XX:-ResizePLAB"</st> %HBASE_GC_OPTS%
    set HBASE_USE_GC_LOGFILE=true
    set HBASE_JMX_BASE="-Dcom.sun.management.jmxremote.ssl=false" "-Dcom.sun.management.jmxremote.authenticate=false"
    set HBASE_MASTER_OPTS=%HBASE_JMX_BASE% "-Dcom.sun.management.jmxremote.port=10101"
    set HBASE_REGIONSERVER_OPTS=%HBASE_JMX_BASE% "-Dcom.sun.management.jmxremote.port=10102"
    set HBASE_THRIFT_OPTS=%HBASE_JMX_BASE% "-Dcom.sun.management.jmxremote.port=10103"
    set HBASE_ZOOKEEPER_OPTS=%HBASE_JMX_BASE% -Dcom.sun.management.jmxremote.port=10104"
    set HBASE_REGIONSERVERS=%HBASE_HOME%\conf\regionservers
    set HBASE_LOG_DIR=%HBASE_HOME%\logs
    set HBASE_IDENT_STRING=%USERNAME%
    set HBASE_MANAGES_ZK=true
    

    我们的 *导师寻找 项目使用 *Java JDK 11 来运行 HBase 数据库服务器。 因此,与 Java 1.8 一起工作的常规垃圾收集器现在已弃用且无效。 对于使用 Java JDK 11 的 HBase 平台,最合适的 GC 选项是 G1GC ,以实现更好的服务器性能。

  6. 最后,转到 <st c="14322">/bin</st> 文件夹 并运行 <st c="14346">start-hbase</st> 命令 以启动服务器。 图 7**.3 显示了启动时的 HBase 日志快照:

图 7.3 – 启动 HBase 服务器

图 7.3 – 启动 HBase 服务器

  1. 要停止服务器,运行 <st c="15355">stop-hbase</st>,然后 <st c="15372">hbase master stop --</st>``<st c="15392">shutDownCluster</st>

HBase 服务器日志,如 图 7**.4所示,显示了 Zookeeper 服务器检索所有 Hadoop 配置文件以处理所有 Hadoop 集群并提供必要的 操作服务:

图 7.4 – 使用 Hadoop 集群启动 Zookeeper

图 7.4 – 使用 Hadoop 集群启动 Zookeeper

现在我们已经完成了 这些服务器 配置,让我们运行 HBase 客户端,以便我们可以创建我们的 <st c="17049">支付</st> <st c="17062">预订</st>

设置 HBase 壳

Apache HBase 有一个内置的交互式 shell 客户端,通过 Java 创建,可以与 HDFS 进行大数据通信。 启动 shell 的命令是 <st c="17255">hbase shell</st>。在 Apache HBase 2.5.5 中,运行此命令将给出以下 错误信息

 This file has been superceded by packaging our ruby files into a jar and using jruby's bootstrapping to invoke them. If you need to source this file for some reason it is now named 'jar-bootstrap.rb' and is located in the root of the file hbase-shell.jar and in the source tree at 'hbase-shell/src/main/ruby'.

这个错误的背后原因是客户端壳缺少安装所需的 JAR 文件。 因此,为了修复这个错误,从 Maven 仓库下载 <st c="17804">jansi-1.18.jar</st> <st c="17823">jruby-complete-9.2.13.0.jar</st> ,并将它们放置在 <st c="17899">/lib</st> 目录中。 然后,进入 <st c="17931">/lib</st> 文件夹,并运行以下命令以打开 客户端壳

 java -cp hbase-shell-2.5.5.jar;client-facing-thirdparty/*;* org.jruby.JarBootstrapMain

图 7**.5 显示了打开 HBase 壳的给定命令:

图 7.5 – 调用 HBase 壳

图 7.5 – 调用 HBase 壳

日志中出现的警告是由于 Hadoop 的 <st c="19789">/common/lib</st> 和 HBase 的 <st c="19813">/lib/client-facing-thirdparty</st>中的 SL4J 日志库冲突引起的。 现在我们已经最终确定了表 设计并设置了 HBase 环境,我们将构建 HBase 表

创建 HBase 表

HBase 客户端应用程序 提供了不同的命令,用于执行 HBase 数据集的管理、表、数据操作、集群相关和通用操作。 它可以根据这些命令与 HBase 存储进行交互。 图 7**.6 显示了常见的通用命令,例如 <st c="20345">whoami</st>,该命令检查已记录在 shell 中的用户信息,以及 <st c="20424">version</st>,该命令指定正在运行的 HBase 的版本。 它还显示了 <st c="20501">status</st> 命令,该命令指定服务器状态和平均负载值 – 即所有服务器中每个区域服务器跨所有服务器的平均区域数:

图 7.6 – 运行通用的 HBase 命令

图 7.6 – 运行通用的 HBase 命令

大多数企业应用程序依赖于 DBA 进行表设计和创建。 对于 HBase 数据库用户,数据模型器允许在每次服务器启动时生成应用程序的数据层表。 但是,通常开发人员会在开发前使用 HBase shell 构建表。 在我们的应用程序中,例如, <st c="21690">payments</st> <st c="21703">bookings</st> 表是使用 HBase 的 <st c="21760">create</st> 命令预先生成的。 图 7**.7 显示了如何使用 <st c="21808">create</st> 命令:

图 7.7 – 使用创建和列表命令

图 7.7 – 使用创建和列表命令

要创建 HBase 表,请使用 <st c="22332">create</st> 命令并带有以下参数:

  • 单引号或双引号表名(例如, <st c="22431">'bookings'</st> <st c="22445">"payments"</st>)。

  • 引号中的列族名称(或包含列族属性的字典),包括 <st c="22563">NAME</st> 和其他属性,如 <st c="22597">VERSIONS</st>,它们的值都在引号中。

图 7**.7 显示了使用 <st c="22661">payments</st> 表创建 带有 <st c="22699">details</st> <st c="22711">items</st> 列族,每个列族最多只有五个版本。 VERSIONS 属性设置了可以施加在列族列上的最大更新数量。 因此,如果payments 表将VERSIONS 设置为 <st c="22930">5</st>,则其列族值允许的最大更新次数最多只有五次。 分配给每个单元格存储的列限定符的时间戳追踪这些更新。

现在,要查看所有表格,请使用 <st c="23167">list</st> 命令。 还有 <st c="23199">describe</st> 命令,您可以使用它来检查每个表格的元数据信息(例如, <st c="23297">describe "bookings"</st>)。 要删除一个表格,首先禁用该表格(例如, <st c="23375">disable "bookings"</st>),然后再删除它(例如,通过 <st c="23433">drop "bookings"</st>)。

在 HBase 存储中创建表格后 ,我们可以将我们的 HBase 数据库集成到我们的 Flask 应用程序中。

建立 HBase 连接

许多现代 Python 库可以将 HBase 集成到 Flask 中,这些库是专有的,例如 这个 CData Python 驱动程序(https://www.cdata.com/drivers/hbase/download/python/),它可以利用 SQLAlchemy 来管理 HBase 存储。 但是,在 PyPI 仓库中有一个可靠且流行的 Python 驱动程序,可以将任何 Python 应用程序集成到 Hbase 中:HappyBase库。

<st c="23976">happybase</st> 模块是一个标准的 Python 库,它使用 Python Thrift 库通过 Thrift 服务连接到任何 HBase 数据库,该服务已经是 Apache HBase 2.5.5 平台的一部分。

要使用 <st c="24190">happybase</st> 模块,请使用 <st c="24229">pip</st> 命令安装:

 pip install happybase

为了 Tutor Finder 建立与 HBase 的连接并创建多个线程以重用连接,<st c="24406">__init__.py</st> 中的应用程序工厂函数必须从 <st c="24430">ConnectionPool</st> 模块中导入 <st c="24454">happybase</st> ,并提供 Thrift 网关的 <st c="24490">主机</st> <st c="24499">端口</st> 值,以及池中的连接数。 以下脚本显示了初始化 <st c="24678">happybase</st> 设置的 <st c="24645">create_app()</st>应用程序工厂函数:

 from flask import Flask
import toml <st c="24731">import happybase</st> def create_app(config_file):
    app = Flask(__name__)
    app.config.from_file(config_file, toml.load) <st c="24844">global pool</st><st c="24855">pool = happybase.ConnectionPool(size=5,</st> <st c="24895">host='localhost', port=9090)</st> with app.app_context():
        import modules.api.hbase.payments
        import modules.api.hbase.bookings

HBase 平台的入口点是 <st c="25062">Connection</st> 类。 <st c="25084">Connection</st> 类通过 Thrift 服务创建一个到 HBase 数据库的开放套接字。 但是 <st c="25178">ConnectionPool</st> 比单个 <st c="25232">Connection</st> 实例提供更快的访问速度,尤其是在 Flask 应用程序处于异步模式时。 唯一的要求是应用程序使用一个 <st c="25367">with</st> 上下文管理器来为连接池生成一个 <st c="25423">Connection</st> 实例,分配一个线程给它,并在事务结束时处理线程,最终将连接的状态返回到 <st c="25540">池中</st>

让我们使用 <st c="25587">ConnectionPool</st> 来构建 `仓库层

构建仓库层

<st c="25667">ConnectionPool</st> 实例 <st c="25696">create_app()</st> 提供的 <st c="25722">Connection</st> 实例 实现了 CRUD 事务。 但它需要一个 <st c="25800">with</st> 上下文管理器来生成一个 <st c="25838">Connection</st> 实例 或从池中重用连接状态,以便线程可以使用 <st c="25949">happybase</st> 实用方法 运行 CRUD 事务。 以下脚本显示了使用 <st c="26038">ConnectionPool</st> 实例 来实现 <st c="26105">payments</st> 的 CRUD 事务的仓库类:

 from typing import Dict, List, Any <st c="26156">from happybase import Table</st> class PaymentRepository:
    def __init__(self, <st c="26228">pool</st>): <st c="26236">self.pool = pool</st> def upsert_details(self, rowkey, tutor_id, stud_id, ccode, fee) -> bool:
        record = <st c="26335">{'details:id' : str(rowkey),</st> <st c="26363">'details:tutor_id': tutor_id, 'details:stud_id':</st> <st c="26412">stud_id, 'details:course_code': ccode,</st> <st c="26451">'details:total_package': str(fee)}</st> try: <st c="26492">with self.pool.connection() as conn:</st><st c="26528">tbl:Table = conn.table("payments")</st><st c="26563">tbl.put(row=str(rowkey).encode('utf-8'),</st> <st c="26604">data=record)</st> return True
        except Exception as e:
            print(e)
        return False

<st c="26674">The</st> <st c="26679">PaymentRepository</st> <st c="26696">类需要一个ConnectionPool 实例(<st c="26739">pool</st>作为其构造函数参数以进行实例化。</st> <st c="26800">pool</st> <st c="26804">对象有一个connection() 方法,它返回一个 HBase 连接,该连接提供了<st c="26889">happybase</st> <st c="26898">实用方法,用于 CRUD 事务。</st> <st c="26938">借助线程的帮助,连接对象有一个table() 实用方法,它访问 HBase 表并返回一个<st c="27053">Table</st> <st c="27058">对象,该对象提供了一些方法来执行数据库事务,例如put()

<st c="27144">The</st> <st c="27149">put()</st> <st c="27154">方法执行了 *INSERT *和 *UPDATE *事务。它需要rowkey作为其主要参数,以便以字典格式插入记录。</st> 字典记录由一个 *<st c="27333">列限定符-值对</st> *组成,其中所有值都应该是字节字符串或任何转换为<st c="27435">bytes</st>的类型的值。 <st c="27472">此外,rowkey 应始终是字节字符串。 给定的<st c="27527">upsert_details()</st> <st c="27543">将支付记录插入到 HBase 数据库的payments 表`中。

除了<st c="27614">put()</st>之外,<st c="27637">Table</st>对象还有一个<st c="27656">delete()</st>方法,它使用其<st c="27704">rowkey</st>来删除记录。以下<st c="27726">delete_payment_details()</st>``<st c="27750">函数PaymentRepository<st c="27780">突出了从`<st c="27831">payments</st>表`中删除的支付详情:

 def delete_payment_items(self, rowkey) -> bool:
        try: <st c="27900">with self.pool.connection() as conn:</st><st c="27936">tbl:Table = conn.table("payments")</st><st c="27971">tbl.delete(rowkey.encode('utf-8'),</st> <st c="28006">columns=["items"])</st> return True
        except Exception as e:
            print(e)
        return False

<st c="28082">除了rowkey之外,delete() 方法 需要其<st c="28177">columns</st> <st c="28184">参数中的列族或列族的名称,这意味着删除整个记录。</st> <st c="28235">但有时,删除只需要删除列限定符(s)或列(s)而不是整个行,这样只有列限定符名称(s)出现在columns 参数中。`

<st c="28424">Table</st> 对象有一个<st c="28443">rows()</st> 方法,该方法返回一个<st c="28472">Tuple</st> 值或元组列表,每个元组包含<st c="28519">rowkey</st> 和以<st c="28544">bytes</st>形式存储的记录。 此方法有两个参数,即搜索中数据记录的<st c="28587">行键</st> <st c="28599">列族或列族</st> 这里,<st c="28666">select_records_ids()</st> 根据所选行键列表和一些指定的列族返回支付记录列表:

 def select_records_ids(self, rowkeys:List[str], cols:List[str] = None):
        try: <st c="28872">with self.pool.connection() as conn:</st> tbl:Table = conn.table("payments")
                if cols == None or len(cols) == 0: <st c="28979">rowkeys = tbl.rows(rowkeys)</st><st c="29006">rows = [rec[1] for rec in rowkeys]</st> else: <st c="29048">rowkeys = tbl.rows(rowkeys, cols)</st><st c="29081">rows = [rec[1] for rec in rowkeys]</st> records = list() <st c="29134">for r in rows:</st><st c="29148">records.append({key.decode():value.decode()</st> <st c="29192">for key, value in r.items()})</st> return records
        except Exception as e:
            print(e)
        return None

<st c="29286">rows()</st> 方法返回一个 <st c="29310">Tuple</st> 值或包含 行键 作为第一个元素和 字典格式的记录 作为第二个元素的元组。 因此,我们只需要使用列表推导来移动字典部分,如代码所示。 此外,解码每个字段字典将避免在 Flask 生成响应时的 JSON 错误。

对于其输入,<st c="29661">select_records_ids()</st> 函数可以接受包含搜索中记录行键的 JSON 请求,如下所示: 这里:

 { <st c="29783">"rowkeys": ["1", "2", "101"],</st> "cols": []
}

或者,它还可以接受行键和列族,例如以下 请求数据:

 {
    "rowkeys": ["1", "2", "101"], <st c="29970">"cols": ["details"]</st> }

它还可以接受需要在搜索输出中出现的特定列限定符,如下面的代码所示: 以下代码:

 {
    "rowkeys": ["1", "2", "101"], <st c="30143">"cols": ["details:stud_id", "details:tutor_id",</st> <st c="30190">"details:course_code"]</st> }

<st c="30253">happybase</st> 模块中检索数据的另一种方法是使用<st c="30285">scan()</st> 方法,该方法返回一个元组的生成器——类似于<st c="30365">rows()</st>返回的元组。 这里,<st c="30379">select_all_records()</st> 展示了如何使用<st c="30417">scan()</st> 检索所有 <st c="30444">支付记录:</st>

 def select_all_records(self):
        records = []
        try: <st c="30509">with self.pool.connection() as conn:</st> tbl:Table = conn.table("payments") <st c="30581">datalist = tbl.scan(columns=['details',</st> <st c="30620">'items'])</st><st c="30630">for key, data in datalist:</st><st c="30657">data_str = {k.decode(): v.decode() for</st> <st c="30696">k, v in data.items()}</st> records.append(data_str)
                return records
        except Exception as e:
            print(e)
        return records

该方法需要一个 <st c="30828">for</st> 循环来从生成器中提取所有这些记录并解码所有详细信息,包括列限定符作为键和每个键的值,然后将它们添加到列表中。 这种检索比使用许多列表和字典推导使用<st c="31118">rows()</st>`消耗的运行时间更少。

使用 scan() 而不是 rows() 的另一个优点是其高级功能,可以通过列上的谓词条件来过滤记录,类似于 SQL 语句中的 <st c="31276">WHERE</st> 子句。 以下查询事务检索所有具有特定 导师 ID 的支付记录,该 ID 由 客户指定:

 def select_records_tutor(self, tutor_id):
   records = []
   try: <st c="31481">with self.pool.connection() as conn:</st> tbl:Table = conn.table("payments") <st c="31553">datalist = tbl.scan(columns=["details", "items"],</st> <st c="31602">filter="SingleColumnValueFilter('details',</st> <st c="31645">'tutor_id', =,'binary:{}')".format(tutor_id))</st><st c="31691">for key, data in datalist:</st><st c="31718">data_str = {k.decode(): v.decode() for k, v in</st> <st c="31765">data.items()}</st><st c="31779">records.append(data_str)</st> return records
   except Exception as e:
       print(e)
   return records

scan() 方法有一个 filter 参数,它接受一个 过滤字符串 ,该字符串构成 过滤类 及其 构造函数参数,这将简化搜索。 filter 参数指示要实例化哪个过滤类以构建适当的搜索约束。 给定的 <st c="32151">select_records_tutor()</st> 函数使用 <st c="32188">SingleColumnValueFilter</st>,它根据提供给 列族列限定符条件运算符,以及 <st c="32332">BinaryComparator (二进制)</st>的值约束来过滤行。 除了 <st c="32370">SingleColumnValueFilter</st>之外,这里还有一些广泛使用的过滤类类型,可以为 <st c="32487">scan()</st> 方法创建搜索条件:

  • <st c="32501">RowFilter</st>:接受一个比较运算符和所需的比较器(例如,<st c="32587">ByteComparator</st><st c="32603">RegexStringComparator</st>,等等),用于将指示值与每一行的键进行比较。

  • <st c="32693">QualifierFilter</st>:接受一个条件运算符和所需的比较器(例如,<st c="32786">ByteComparator</st><st c="32802">RegexStringComparator</st>,等等),用于将每一行的列限定符名称与给定的值进行比较。

  • <st c="32913">ColumnRangeFilter</st>:接受最小范围列和最大范围列,然后检查指示值是否位于列值范围内。

  • <st c="33069">ValueFilter</st>:接受一个条件运算符和所需的比较器,用于将值与每一字段的值进行比较。

除了 <st c="33204">BinaryComparator</st>之外,其他提供转换和比较方法的比较器,用于过滤类,包括 <st c="33310">BinaryPrefixComparator</st> <st c="33334">RegexStringComparator</st> <st c="33357">和</st> SubStringComparator`

在下一节中,我们将应用PaymentsRepository 以便我们可以在payments 中存储和检索支付详情。

将仓库应用于 API 函数

以下 API 函数 使用 <st c="33587">upsert_details() <st c="33609">PaymentRepository</st> 执行一个 *INSERT 事务,在从客户端收到 JSON 请求数据后:

<st c="33710">from modules import pool</st>
<st c="33735">@current_app.post('/ch07/payment/details/add')</st> def add_payment_details():
    data = request.get_json() <st c="33836">repo = PaymentRepository(pool)</st><st c="33866">result = repo.upsert_details(data['id'],</st> <st c="33907">data['tutor_id'], data['stud_id'], data['ccode'],</st> <st c="33957">data['fee'])</st> if result == False:
        return jsonify(message="error encountered in payment details record insert"), 500
    return jsonify(message="inserted payment details record"), 201

仓库的 <st c="34153">select_all_records()</st> 提供了以下 <st c="34197">list_all_payments()</st> 函数,用于渲染 <st c="34235">所有来自 <st c="34261">payments</st> 表的 `记录:

<st c="34276">from modules import pool</st>
<st c="34301">@current_app.get('/ch07/payment/list/all')</st> def list_all_payments(): <st c="34370">repo = PaymentRepository(pool)</st><st c="34400">results = repo.select_all_records()</st> return jsonify(records=results), 201

在这里, <st c="34480">pool</st> <st c="34492">ConnectionPool</st> 实例,该实例是在 <st c="34540">create_app()</st> 工厂中从 <st c="34570">__init__.py</st> 文件中的 <st c="34594">modules</st> 包创建的。

现在,为了使 <st c="34620">happybase</st> 工作,启动 *thrift 服务器**。让我们在 HBase 平台上展示 Apache Thrift 框架。

运行 thrift 服务器

<st c="35027">hbase thrift start</st> 命令。 图 7**.8 显示了启动 thrift 服务器后的日志:

图 7.8 – 运行内置的 HBase thrift 服务器

图 7.8 – 运行内置的 HBase thrift 服务器

Apache Thrift 仅在 Apache HBase、Hadoop 和 Zookeeper 都运行时才会运行。

The <st c="36011">happybase</st> 模块是非 Flask 特定的,这意味着任何 Python 客户端都可以使用它来连接到 <st c="36110">HBase</st> 服务器。 考虑到 Python 库使用 Thrift 1 2 库来建立连接,thrift 服务器将始终在客户端和 HBase 之间建立桥梁。 happybase 模块使用 Thrift 1 库。

现在我们已经为 HBase 数据库创建了 Flask 仓库事务,让我们探索一种使用列和行进行 数据存储的 NoSQL 存储。

利用 Apache Cassandra 的列存储

Apache Cassandra 是一个列族 NoSQL 数据库 ,也可以存储大量数据。 HBase 可以通过自动分片在各个区域之间共享大数据,这使得 HBase 具有水平可扩展性。 同样,Cassandra 支持水平添加更多节点以提高服务器吞吐量,这是水平扩展的一个特性。 但是,在架构、表读写性能、数据 建模方法以及 查询 语言方面,这两种存储之间也存在一些差异。

让我们首先设计我们的 <st c="37075">课程</st> <st c="37083">学位水平</st> <st c="37097">学生</st>,以及 <st c="37110">学生表现</st> Cassandra 表。

设计 Cassandra 表

与 HBase 不同,Cassandra 按行存储其数据,将所有列字段分组,因此数据模型方法是一个列族。其数据库事务是原子性、隔离性和持久性的,但具有最终 或可调的一致性,因此它不提供类似关系数据库管理系统( 原子性、一致性、隔离性、持久性 ( ACID ))的模型,如 关系数据库管理系统 ( RDBMS )。 有时,Cassandra 的配置更倾向于 高可用性性能而不是原子性和 隔离性事务。

该项目使用了 draw.io 来使用 UML 类图设计 Cassandra 中的表。 图 7.9**.9 显示了项目 Cassandra 数据存储的数据模型: 数据存储:

图 7.9 – 使用 UML 设计的 Cassandra 表

图 7.9 – 使用 UML 设计的 Cassandra 表

每个 Cassandra 表都必须 有一个主键。 但是,与 RDBMS 不同,列族 存储中的主键至少有一个 分区键 <st c="38554>和零个或多个 聚类键。由于 Cassandra 存储运行在集群和节点分布的环境中,因此 分区键 将行数据均匀分布在集群存储中。 另一方面, 聚类键 对表中的数据进行排序和管理。 此外,查询事务的性能是表设计的最终依据;查询越快,设计就越好

让我们安装 Apache Cassandra,以便我们可以实现我们的 表设计。

安装和配置 Apache Cassandra

https://cassandra.apache.org/_/download.html下载 Apache Cassandra 的 ZIP 文件。3.x 及以下版本 通用可用性 (GA) 版本支持 Windows,但不支持 4.x 版本。 由于项目使用的是 4.1.3 版本,因此必须使用安装了 WSL2 的 Windows PowerShell 来配置和运行 服务器。

解压文件后,使用 <st c="39464">sudo</st> 命令启用 Ubuntu 防火墙:

 sudo ufw enable

然后,使用以下 <st c="39660">sudo</st> 命令允许非 WSL 客户端访问端口 7000 (集群通信端口), 9042 (客户端访问的默认端口),以及 7199 (JMX 端口):

 sudo ufw allow 7000
sudo ufw allow 9042
sudo ufw allow 7199

Apache Cassandra 4.1.3 需要 Java 11 作为其虚拟机,因此运行以下 <st c="39820">sudo</st> 命令在 Ubuntu 环境中安装 Java SDK 11:

 sudo apt install openjdk-11-jdk

之后,进入 Cassandra 安装文件夹的 <st c="39932">/conf</st> 目录并打开 <st c="39996">jvm11-server.options</st> 文件。 注释掉所有 CMS GC 选项的详细信息,并取消注释 G1GC,这是 Java 11 的默认 GC 选项。

最后,从 <st c="40160">/</st>``<st c="40161">conf</st> 目录中运行以下命令:

 cassandra -f

要关闭 Cassandra 服务器,请使用 <st c="40233">nodetool</st> <st c="40242">drain</st> 命令。

现在,让我们打开 Cassandra shell 客户端来创建 项目的表,并学习 Cassandra 查询语言 (CQL) 命令。

运行 CQL shell 客户端

Cassandra 4.1.3 有一个名为 <st c="40625">cqlsh</st> 的查询语言命令,位于 <st c="40646">/conf</st> 目录中。 图 7**.10 显示了打开 CQL shell (<st c="40719">cqlsh</st>):

图 7.10 – 运行 cqlsh 命令

图 7.10 – 运行 cqlsh 命令

CQL 有 <st c="41130">create</st> | alter<st c="41143">|</st>drop keyspace<st c="41159">,</st> create<st c="41167">|</st>alter<st c="41175">|</st>drop table<st c="41188">,</st> use<st c="41193">, 和</st> truncate<st c="41207">语句。</st> <st c="41220">对于 DML,它有</st>insert<st c="41242">,</st> delete<st c="41250">,</st> update<st c="41258">, 和</st> batch<st c="41269">命令。</st> <st c="41280">对于查询事务,它使用 SQL 中的</st>select<st c="41326">子句。</st> <st c="41347">然而,</st>where` 子句仅限于分区、聚簇和复合键。 一些 CQL 命令以 分号 结尾。

CQL 具有通用 命令,例如 <st c="41516">show version</st>, <st c="41530">expand</st>, 和 <st c="41542">describe</st>。要检查所有集群,请运行 <st c="41587">describe cluster</st> 命令。 要检查所有键空间,请运行 <st c="41649">describe keyspaces</st> 命令。 要列出键空间中的所有表,请运行 <st c="41723">describe tables</st> 命令。 图 7**.11 显示了查看 Cassandra 数据存储的一系列 CQL 命令:

图 7.11 – 运行 CQL 通用命令

图 7.11 – 运行 CQL 通用命令

在表上运行 <st c="42584">describe</st> 命令将返回 Cassandra 表的元数据描述,如图 图 7**.12所示:

图 7.12 – 在表上运行 describe 命令

图 7.12 – 在表上运行 describe 命令

一个 cluster 包含多个数据中心,一个 data center 可以有多个节点。 每个 node 必须有一个键空间来存储所有表、物化视图、用户定义的类型、函数和聚合。 因此,在使用 CQL shell 客户端构建项目表之前,你必须首先运行 <st c="43998">create keyspace</st> 命令。 以下代码创建 <st c="44087">packtspace</st>,它存储我们应用程序的表:

 CREATE KEYSPACE packtspace WITH replication = {'class': 'NetworkTopologyStrategy', 'datacenter1': '1'}  AND durable_writes = false;

所使用的复制策略称为 replication strategy ,称为 <st c="44320">NetworkTopologyStrategy</st>。它使 <st c="44354">packtspace</st> 在长期内对复制和数据存储扩展开放,并且也适用于 生产部署。

在创建 <st c="44491">packspace</st>后,您可以使用 CQL 命令在 <st c="44588">keyspace</st> 中手动创建 <st c="44530">course</st>, <st c="44538">student</st>, <st c="44547">degree_level</st>, 和 <st c="44565">student_perf</st> 表。 然而,DataStax 有一个 <st c="44644">cassandra-driver</st> 模块,该模块可以建立到 Cassandra 的数据库连接,并使用实体或模型类生成表。 让我们使用这个外部模块 来构建应用程序的 模型层。

建立数据库连接

cassandra-driver 模块是一个 Python 客户端驱动程序,它将 Apache Cassandra 集成到 Flask 应用程序中。 它包含在安装模块后才会可用的类和方法,使用 <st c="45095">pip</st> 命令安装模块:

 pip install cassandra-driver

为了我们的 导师搜索器 建立 Cassandra 数据库连接,从 <st c="45211">setup()</st> 导入 <st c="45228">cassandra.cqlengine.connection</st> 模块中的 <st c="45273">__init__.py</st> 文件中的 <st c="45297">modules</st> 包,并在 <st c="45343">create_app()</st> 工厂方法中调用 <st c="45324">setup()</st> ,传入其 <st c="45398">hosts</st>, <st c="45405">default_keyspace</st>, 和 <st c="45427">protocol_version</st> 参数。 以下代码片段展示了整个 过程:

<st c="45502">from cassandra.cqlengine.connection import setup</st> def create_app(config_file):
    app = Flask(__name__)
    app.config.from_file(config_file, toml.load) <st c="45707">hosts</st> parameter provides the initial set of IP addresses that will serve as the contact points for the clusters. The second parameter is <st c="45844">keyspace</st>, which was created beforehand with the CQL shell. The <st c="45907">protocol version</st> parameter refers to the native protocol that <st c="45969">cassandra-driver</st> uses to communicate with the server. It depicts the maximum number of requests a connection can handle during communication.
			<st c="46110">Next, we’ll create the</st> <st c="46134">model layer.</st>
			<st c="46146">Building the model layer</st>
			<st c="46171">Instead of using the CQL</st> <st c="46196">shell to create the tables, we’ll use the</st> `<st c="46239">cassandra-driver</st>` <st c="46255">module since it can create the tables programmatically using entity model classes that can translate into actual tables upon application server startup.</st> <st c="46409">These model classes are often referred to as</st> *<st c="46454">object mappers</st>* <st c="46468">since they also map</st> <st c="46488">to the metadata of the</st> <st c="46512">physical tables.</st>
			<st c="46528">Unlike HBase, Cassandra recognizes data structures and data types for its tables.</st> <st c="46611">Thus, the driver has a</st> `<st c="46634">Model</st>` <st c="46639">class that subclasses entities for Cassandra table generation.</st> <st c="46703">It also provides helper classes, such as</st> `<st c="46744">UUID</st>`<st c="46748">,</st> `<st c="46750">Integer</st>`<st c="46757">,</st> `<st c="46759">Float</st>`<st c="46764">, and</st> `<st c="46770">DateTime</st>`<st c="46778">, that can define column metadata in an entity class.</st> <st c="46832">The following code shows the entity models that are created through the</st> `<st c="46904">cassandra-driver</st>` <st c="46920">module:</st>

import uuid 从 cassandra.cqlengine.columns 导入 UUID, Text, Float, DateTime, Integer, Blob

从 cassandra.cqlengine.models 导入 Model

从 cassandra.cqlengine.management 模块中导入 sync_table class Course(Model):

id      = <st c="47151">UUID</st>(primary_key=True, default=uuid.uuid4)

code    = <st c="47202">Text</st>(primary_key=True, max_length=20, required=True, clustering_order="ASC")

title   = <st c="47287">Text</st>(required=True, max_length=100)

req_hrs = <st c="47334">Float</st>(required=True, default = 0)

total_cost = <st c="47382">Float</st>(required=True, default = 0.0)

course_offered  = <st c="47436">DateTime</st>()

level = <st c="47456">Integer</st>(required=True, default=-1)

description   = <st c="47506">Text</st>(required=False, max_length=200) <st c="47544">def get_json(self):</st> return {

        'id': str(self.id),

        'code': self.code,

        'title' : self.title,

        'req_hrs': self.req_hrs,

        'total_cost': self.total_cost,

        'course_offered': self.course_offered,

        'level': self.level,

        'description': self.description

}

			<st c="47783">In the given</st> `<st c="47797">Course</st>` <st c="47803">entity,</st> `<st c="47812">id</st>` <st c="47814">and</st> `<st c="47819">code</st>` <st c="47823">are columns</st> <st c="47835">that are declared as</st> *<st c="47857">primary keys</st>*<st c="47869">;</st> `<st c="47872">id</st>` <st c="47874">is the</st> *<st c="47882">partition key</st>*<st c="47895">, while</st> `<st c="47903">code</st>` <st c="47907">is the</st> *<st c="47915">clustering key</st>* <st c="47929">that will manage and sort the records per node in ascending order.</st> <st c="47997">The</st> `<st c="48001">title</st>`<st c="48006">,</st> `<st c="48008">req_hrs</st>`<st c="48015">,</st> `<st c="48017">total_cost</st>`<st c="48027">,</st> `<st c="48029">course_offered</st>`<st c="48043">,</st> `<st c="48045">level</st>`<st c="48050">, and</st> `<st c="48056">descriptions</st>` <st c="48068">columns are typical columns that contain their respective metadata.</st> <st c="48137">On the other hand, the</st> `<st c="48160">get_json()</st>` <st c="48170">custom method is an optional mechanism that will serialize the model when</st> `<st c="48245">jsonify()</st>` <st c="48254">needs to render them as a</st> <st c="48281">JSON response.</st>
			<st c="48295">The following model classes define the</st> `<st c="48335">degree_level</st>`<st c="48347">,</st> `<st c="48349">student</st>`<st c="48356">, and</st> `<st c="48362">student_perf</st>` <st c="48374">tables:</st>

class DegreeLevel(模型):

id = <st c="48416">UUID</st>(主键=True, 默认=uuid.uuid4)

code = <st c="48467">整数</st>(主键=True, 必填=True, 聚簇顺序="ASC")

description = <st c="48546">文本</st>(必填=True)

… … … … … …

… … … … … …

class Student(模型):

id = UUID(主键=True, 默认=uuid.uuid4)

std_id = Text(主键=True, 必填=True, 最大长度=12, 聚簇顺序="ASC")

firstname = Text(必填=True, 最大长度=60)

midname = Text(必填=True, 最大长度=60)

… … … … … …

… … … … … …

class StudentPerf(模型):

id = UUID(主键=True, 默认=uuid.uuid4)

std_id = Text(主键=True, 必填=True, 最大长度=12)

course_code = Text(必填=True, 最大长度=20)

… … … … … …

… … … … … …

sync_table(Course)

sync_table(DegreeLevel)

sync_table(Student)

sync_table(StudentPerf)


			<st c="49156">Here,</st> `<st c="49163">sync_table()</st>` <st c="49175">from</st> `<st c="49181">cassandra-driver</st>` <st c="49197">converts each model into a table and synchronizes any changes made in the model classes to the mapped table in</st> `<st c="49309">keyspace</st>`<st c="49317">. However, applying this method to the model class with too many changes may mess up the existing table’s metadata.</st> <st c="49433">So, it is more acceptable to drop all old tables using the CQL shell before running</st> `<st c="49517">sync_table()</st>` <st c="49529">with the updated</st> <st c="49547">model classes.</st>
			<st c="49561">After building the model layer, the subsequent</st> <st c="49608">procedure is to implement the repository transactions to access the data in Cassandra.</st> <st c="49696">So, let’s access the keyspace and tables in our Cassandra platform so that we can perform</st> <st c="49786">CRUD operations.</st>
			<st c="49802">Implementing the repository layer</st>
			<st c="49836">Entity models inherit</st> <st c="49858">some attributes from the</st> `<st c="49884">Model</st>` <st c="49889">class, such as</st> `<st c="49905">__table_name__</st>`<st c="49919">, which accepts and replaces the default table name of the mapping, and</st> `<st c="49991">__keyspace__</st>`<st c="50003">, which replaces the default</st> *<st c="50032">keyspace</st>* <st c="50040">of the</st> <st c="50048">mapped table.</st>
			<st c="50061">Moreover, entity models also inherit some other</st> <st c="50110">instance methods:</st>

				*   `<st c="50127">save()</st>`<st c="50134">: Persists the entity object in</st> <st c="50167">the database.</st>
				*   `<st c="50180">update(**kwargs)</st>`<st c="50197">: Updates the existing column fields based on the new column (</st>`<st c="50260">kwargs</st>`<st c="50267">) details.</st>
				*   `<st c="50278">delete()</st>`<st c="50287">: Removes the record from</st> <st c="50314">the database.</st>
				*   `<st c="50327">batch()</st>`<st c="50335">: Runs synchronized updates or inserts</st> <st c="50375">on replicas.</st>
				*   `<st c="50387">iff(**kwargs)</st>`<st c="50401">: Checks if the indicated</st> `<st c="50428">kwargs</st>` <st c="50434">matches the column values of the object before the update or</st> <st c="50496">delete happens.</st>
				*   `<st c="50511">if_exists()</st>`<st c="50523">/</st>`<st c="50525">if_not_exists()</st>`<st c="50540">: Verifies if the mapped record exists in</st> <st c="50583">the database.</st>

			<st c="50596">Also, the entity classes derive the</st> `<st c="50633">objects</st>` <st c="50640">class variable from their</st> `<st c="50667">Model</st>` <st c="50672">superclass, which can provide query methods such as</st> `<st c="50725">filter()</st>`<st c="50733">,</st> `<st c="50735">allow_filtering()</st>`<st c="50752">, and</st> `<st c="50758">get()</st>` <st c="50763">for record retrieval.</st> <st c="50786">They also inherit the</st> `<st c="50808">create()</st>` <st c="50816">class method, which can insert records into the database, an option other</st> <st c="50891">than</st> `<st c="50896">save()</st>`<st c="50902">.</st>
			<st c="50903">All these derived methods are the building blocks of our repository class.</st> <st c="50979">The following repository class shows how the</st> `<st c="51024">Course</st>` <st c="51030">model implements its</st> <st c="51052">CRUD transactions:</st>

从 modules.models.db.cassandra_models 导入 Course from datetime import datetime

from typing import Dict, Any

class CourseRepository:

def __init__(self):

    pass

def insert_course(self, details:Dict[str, Any]):

    try: <st c="51287">Course.create(**details)</st> return True

    except Exception as e:

        print(e)

    return False

			<st c="51368">Gere,</st> `<st c="51375">insert_course()</st>` <st c="51390">uses the</st> `<st c="51400">create()</st>` <st c="51408">method</st> <st c="51415">to persist a</st> `<st c="51429">course</st>` <st c="51435">record instead of applying</st> `<st c="51463">save()</st>`<st c="51469">. For the update transaction,</st> `<st c="51499">update_course()</st>` <st c="51514">filters a</st> `<st c="51525">course</st>` <st c="51531">record by course code</st> <st c="51554">for updating:</st>

def update_course(self, details:Dict[str, Any]):

    try: <st c="51622">rec = Course.objects.filter(</st><st c="51650">code=str(details['code']))</st><st c="51677">.allow_filtering().get()</st> del details['id']

        del details['code'] <st c="51740">rec.update(**details)</st> return True

    except Exception as e:

        print(e)

    return False

			<st c="51818">In Cassandra, when querying records with constraints, the</st> *<st c="51877">partition key</st>* <st c="51890">must always be included in the constraint.</st> <st c="51934">However,</st> `<st c="51943">update_course()</st>` <st c="51958">uses the</st> `<st c="51968">allow_filtering()</st>` <st c="51985">method to allow data retrieval without the</st> *<st c="52029">partition key</st>* <st c="52042">and bypass the</st> *<st c="52058">Invalid Query Error</st>* <st c="52077">or</st> *<st c="52081">error</st>* *<st c="52087">code 2200</st>*<st c="52096">.</st>
			<st c="52097">The following</st> `<st c="52112">delete_course_code()</st>` <st c="52132">transaction</st> <st c="52144">uses the</st> `<st c="52154">delete()</st>` <st c="52162">entity class method to remove the filtered record object.</st> <st c="52221">Again, the</st> `<st c="52232">allow_filtering()</st>` <st c="52249">method helps filter the record by code without messing up the</st> *<st c="52312">partition key</st>*<st c="52325">:</st>

def delete_course_code(self, code):

    try: <st c="52369">rec = Course.objects.filter(code=code)</st><st c="52407">.allow_filtering().get()</st><st c="52431">rec.delete()</st> return True

    except Exception as e:

        print(e)

    return False

			<st c="52501">Here,</st> `<st c="52508">search_by_code()</st>` <st c="52524">and</st> `<st c="52529">search_all_courses()</st>` <st c="52549">are the two query transactions of this</st> `<st c="52589">CourseRepository</st>`<st c="52605">. The former retrieves a single record based on a</st> `<st c="52655">course</st>` <st c="52661">code, while the latter filters all</st> `<st c="52697">course</st>` <st c="52703">records without any condition.</st> <st c="52735">The</st> `<st c="52739">get()</st>` <st c="52744">method of</st> `<st c="52755">objects</st>` <st c="52762">returns a non-JSONable</st> `<st c="52786">Course</st>` <st c="52792">object that</st> `<st c="52805">jsonify()</st>` <st c="52814">cannot process.</st> <st c="52831">But wrapping the object with</st> `<st c="52860">dict()</st>` <st c="52866">after converts it into a JSON serializable record.</st> <st c="52918">In</st> `<st c="52921">search_all_courses()</st>`<st c="52941">, the custom</st> `<st c="52954">get_json()</st>` <st c="52964">method helps generate a list of JSONable course records for easy</st> `<st c="53030">Response</st>` <st c="53038">generation:</st>

def search_by_code(self, code:str): result = Course.objects.filter(code=code).allow_filtering().get()records = dict(result) return records

def search_all_courses(self): <st c="53221">result = Course.objects.all()</st><st c="53250">records = [course.get_json() for course in result]</st> return records

			<st c="53316">Cassandra is known for its faster write than read operations.</st> <st c="53379">It writes data to the commit log and then caches it simultaneously, preserving data from unexpected occurrences, damage, or downtime.</st> <st c="53513">But there is one form of NoSQL data storage that’s popular</st> <st c="53571">for its faster reads operations:</st> **<st c="53605">Remote Dictionary</st>** **<st c="53623">Server</st>** <st c="53629">(</st>**<st c="53631">Redis</st>**<st c="53636">).</st>
			<st c="53639">Storing search data in Redis</st>
			<st c="53668">Redis is a fast, open</st> <st c="53690">source, in-memory,</st> *<st c="53710">key-value</st>* <st c="53719">form of NoSQL storage</st> <st c="53741">that’s popular in messaging and caching.</st> <st c="53783">In</st> *<st c="53786">Chapter 5</st>*<st c="53795">, we used it as the message broker of Celery, while in</st> *<st c="53850">Chapter 6</st>*<st c="53859">, we used it as a message queue for the SSE and WebSocket programming.</st> <st c="53930">However, this chapter will utilize Redis as a data cache to create a fast</st> <st c="54004">search mechanism.</st>
			<st c="54021">First, let’s install Redis on</st> <st c="54052">our system.</st>
			<st c="54063">Installing the Redis server</st>
			<st c="54091">For Windows, download</st> <st c="54113">the latest Redis</st> <st c="54130">TAR file from</st> [<st c="54145">https://redis.io/download/</st>](https://redis.io/download/)<st c="54171">, unzip it to an installation folder, and run</st> `<st c="54217">redis-server.exe</st>` <st c="54233">from</st> <st c="54239">the directory.</st>
			<st c="54253">For WSL, run the following series of</st> `<st c="54291">sudo</st>` <st c="54295">commands:</st>

sudo apt-add-repository ppa:redislabs/redis

sudo apt-get update

sudo apt-get upgrade

sudo apt-get install redis-server


			<st c="54424">Then, run</st> `<st c="54435">redis-cli -v</st>` <st c="54447">to check if the installation was successful.</st> <st c="54493">If so, run the</st> `<st c="54508">redis-server</st>` <st c="54520">command to start the Redis server.</st> *<st c="54556">Figure 7</st>**<st c="54564">.13</st>* <st c="54567">shows the server log after the Redis server starts up on the</st> <st c="54629">WSL-Ubuntu platform:</st>
			![Figure 7.13 – Running the redis-server command](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_07_013.jpg)

			<st c="56140">Figure 7.13 – Running the redis-server command</st>
			<st c="56186">To stop the server, run the</st> `<st c="56215">redis-cli</st>` <st c="56224">shutdown command or press</st> *<st c="56251">Ctrl</st>* <st c="56255">+</st> *<st c="56258">C</st>* <st c="56259">on</st> <st c="56263">your keyboard.</st>
			<st c="56277">Now, let’s explore the Redis</st> <st c="56306">server using its client shell and understand its CLI commands so that we can run its</st> <st c="56392">CRUD operations.</st>
			<st c="56408">Understanding the Redis database</st>
			<st c="56441">Key-value storage uses a</st> *<st c="56467">hashtable</st>* <st c="56476">data</st> <st c="56481">structure model wherein its unique key value serves as a pointer to a corresponding value of any type.</st> <st c="56585">For Redis, the key is always a string that points to values of type strings, JSON, lists, sets, hashes, sorted set, streams, bitfields, geospatial, and time series.</st> <st c="56750">Since Redis is an in-memory storage type, it stores all its simple to complex key-value pairs of data in the host’s RAM, which is volatile and cannot persist data permanently.</st> <st c="56926">However, in return, Redis can provide faster reads and access to its data than HBase</st> <st c="57011">and Cassandra.</st>
			<st c="57025">To learn more about this storage, Redis has a built-in shell client that interacts with the database through some commands.</st> *<st c="57150">Figure 7</st>**<st c="57158">.14</st>* <st c="57161">shows opening a client shell by running the</st> `<st c="57206">redis-cli</st>` <st c="57215">command and checking the number of databases the storage has using the</st> `<st c="57287">CONFIG GET</st>` `<st c="57298">databases</st>` <st c="57307">command:</st>
			![Figure 7.14 – Opening a Redis shell](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_07_014.jpg)

			<st c="57476">Figure 7.14 – Opening a Redis shell</st>
			<st c="57511">The typical number of databases</st> <st c="57543">in Redis storage is</st> *<st c="57564">16</st>*<st c="57566">. Redis databases are named from</st> *<st c="57599">0</st>* <st c="57601">to</st> *<st c="57604">15</st>*<st c="57606">, like indexes of an array.</st> <st c="57634">The default database name is</st> *<st c="57663">0</st>*<st c="57664">, but it has a</st> `<st c="57679">select</st>` <st c="57685">command that chooses the preferred database other than 0 (for example,</st> `<st c="57757">select 1</st>`<st c="57765">).</st>
			<st c="57768">Since Redis is a simple NoSQL database, it only has the following</st> <st c="57834">few commands, including CRUD, we need</st> <st c="57873">to consider:</st>

				*   `<st c="57885">set</st>`<st c="57889">: Adds a key-value pair to</st> <st c="57917">the database.</st>
				*   `<st c="57930">get</st>`<st c="57934">: Retrieves the value of</st> <st c="57960">a key.</st>
				*   `<st c="57966">hset</st>`<st c="57971">: Adds a hash with multiple</st> <st c="58000">key-value pairs.</st>
				*   `<st c="58016">hget</st>`<st c="58021">: Retrieves the value of a key in</st> <st c="58056">a hash.</st>
				*   `<st c="58063">hgetall</st>`<st c="58071">: Retrieves all the key-value pairs in</st> <st c="58111">a hash.</st>
				*   `<st c="58118">hkeys</st>`<st c="58124">: Retrieves all the keys in</st> <st c="58153">a hash.</st>
				*   `<st c="58160">hvals</st>`<st c="58166">: Retrieves all the values in</st> <st c="58197">a hash.</st>
				*   `<st c="58204">del</st>`<st c="58208">: Removes an existing key-value pair using the key or the</st> <st c="58267">whole hash.</st>
				*   `<st c="58278">hdel</st>`<st c="58283">: Removes single or multiple key-value pairs in</st> <st c="58332">a hash.</st>

			<st c="58339">Redis hashes are records or structured</st> <st c="58378">types that can hold collections of field-value pairs with values of varying types.</st> <st c="58462">In a way, it can represent a Python object persisted in the database.</st> *<st c="58532">Figure 7</st>**<st c="58540">.15</st>* <st c="58543">shows a list of Redis commands being run on the</st> <st c="58592">Redis shell:</st>
			![Figure 7.15 – Running Redis commands on the Redis shell](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_07_015.jpg)

			<st c="59167">Figure 7.15 – Running Redis commands on the Redis shell</st>
			<st c="59222">But how can our</st> *<st c="59239">Tutor Finder</st>* <st c="59251">application</st> <st c="59264">connect to a Redis database as a client?</st> <st c="59305">We’ll answer this question in the</st> <st c="59339">next section.</st>
			<st c="59352">Establishing a database connection</st>
			<st c="59387">In</st> *<st c="59391">Chapter 5</st>* <st c="59400">and</st> *<st c="59405">Chapter 6</st>*<st c="59414">, the</st> *<st c="59420">redis-py</st>* <st c="59428">module established</st> <st c="59447">a connection to Redis as a broker or message queue.</st> <st c="59500">This time, our application will connect to the Redis database for data storage</st> <st c="59579">and caching.</st>
			<st c="59591">So far, the Redis OM module is the most efficient and convenient Redis database connector that can provide database connectivity and methods for CRUD operations, similar to an ORM.</st> <st c="59773">However, before accessing its utilities, install it using the</st> `<st c="59835">pip</st>` <st c="59838">command:</st>

pip install redis-om


			`<st c="59868">redis-py</st>` <st c="59877">is the other library that’s included in the installation of the</st> `<st c="59942">redis-om</st>` <st c="59950">module.</st> <st c="59959">The</st> `<st c="59963">redis</st>` <st c="59968">module has a Redis callable object that builds the database connectivity.</st> <st c="60043">The callable has a</st> `<st c="60062">from_url()</st>` <st c="60072">method that accepts the database URL and some parameter values for the</st> `<st c="60144">encoding</st>` <st c="60152">and</st> `<st c="60157">decode_responses</st>` <st c="60173">parameters.</st> <st c="60186">The following code shows</st> `<st c="60211">create_app()</st>`<st c="60223">, which creates the Redis connection to</st> <st c="60263">database</st> `<st c="60272">0</st>`<st c="60273">:</st>

导入 redis def create_app(config_file):

app = Flask(__name__)

app.config.from_file(config_file, toml.load) <st c="60478">redis</st> 在 URI 中表示连接是 Redis 独立数据库 <st c="60563">0</st> 在 localhost 的 <st c="60586">6379</st> 端口。所有 <st c="60613">redis-om</st> 事务的响应都解码为字符串,因为 <st c="60672">decode_responses</st> 参数被分配了 <st c="60722">True</st> 的值。所有这些字符串结果都是 <st c="60760">UTF-8</st> 编码。

        <st c="60775">在此阶段,</st> `<st c="60795">redis-om</st>` <st c="60803">模块已准备好</st> <st c="60819">构建应用程序的</st> <st c="60847">模型层。</st>

        <st c="60859">实现模型层</st>

        `<st c="61031">redis-py</st>` <st c="61039">模块。</st> <st c="61048">每个模型类都包含由 Pydantic 验证器验证的类型哈希字段。</st> <st c="61135">一旦实例化,模型对象将持有键的值,在将它们插入</st> <st c="61230">数据库时,使用自动生成的</st> **<st c="61268">哈希值</st>** <st c="61278">或</st> **<st c="61282">pk</st>**<st c="61284">。</st>

        <st c="61285">The</st> `<st c="61290">redis-om</st>` <st c="61298">模块具有</st> `<st c="61314">HashModel</st>` <st c="61323">类,该类将实现应用程序的实体类。</st> <st c="61391">The</st> `<st c="61395">HashModel</st>` <st c="61404">类是 Redis 哈希的表示。</st> <st c="61448">它捕获键值对,并使用其实例方法来管理数据。</st> <st c="61530">它为每个模型对象自动生成主键或哈希键。</st> <st c="61608">以下是为</st> `<st c="61630">HashModel</st>` <st c="61639">类创建的</st> *<st c="61656">课程</st>*, *<st c="61664">学生</st>*, 和 *<st c="61671">导师</st> <st c="61682">数据:</st>
<st c="61688">from redis_om import  HashModel, Field,</st> <st c="61727">get_redis_connection</st>
<st c="61748">redis_conn = get_redis_connection(decode_responses=True)</st> class SearchCourse(<st c="61825">HashModel</st>):
    code: str  = Field(index=True)
    title: str
    description: str
    req_hrs: float
    total_cost: float
    level: int
class SearchStudent(<st c="61961">HashModel</st>):
    std_id: str
    firstname: str
    midname: str
    lastname: str
    … … … … … …
class SearchTutor(<st c="62059">HashModel</st>):
    firstname: str
    lastname: str
    midname: str
    … … … … … … <st c="62167">CourseSearch</st>, <st c="62181">SearchStudent</st>, and <st c="62200">SearchTutor</st> are model classes that have been created to cache incoming request data to the Redis database for fast search transactions. Each class has declared attributes that correspond to the keys of a record. After its instantiation, the model object will have a <st c="62466">pk</st> instance variable that contains the unique hash key of the data record.
			<st c="62540">Aside from relying on the Redis</st> <st c="62572">connection created by</st> `<st c="62595">Redis.from_url()</st>`<st c="62611">, a</st> `<st c="62615">HashModel</st>` <st c="62624">object can independently or directly connect to the Redis database by assigning a connection instance to its</st> `<st c="62734">Meta</st>` <st c="62738">object’s</st> `<st c="62748">database</st>` <st c="62756">variable.</st> <st c="62767">In either of these connectivity approaches, the model object can still emit the methods that will operate the</st> <st c="62877">repository layer.</st>
			<st c="62894">After establishing the Redis connection and creating the model classes, the next step is to build the</st> <st c="62997">repository layer.</st>
			<st c="63014">Building the repository layer</st>
			<st c="63044">Like in Cassandra’s repository</st> <st c="63075">layer, the model object of the</st> `<st c="63107">redis-om</st>` <st c="63115">module implements the repository class.</st> <st c="63156">The</st> `<st c="63160">HashModel</st>` <st c="63169">entity emits methods that will implement the CRUD transactions.</st> <st c="63234">The following</st> `<st c="63248">SearchCourseRepository</st>` <st c="63270">class manages course details in the</st> <st c="63307">Redis database:</st>

from modules.models.db.redis_models import SearchCourse from typing import Dict, Any

class SearchCourseRepository:

def __init__(self):

    pass

def insert_course(self, details:Dict[str, Any]):

    try: <st c="63517">课程 = SearchCourse(**details)</st><st c="63549">课程.save()</st> return True

    except Exception as e:

        print(e)

    return False

			<st c="63620">The given</st> `<st c="63631">insert_course()</st>` <st c="63646">method uses the</st> `<st c="63663">HashModel</st>` <st c="63672">entity’s</st> `<st c="63682">save()</st>` <st c="63688">instance method, which adds</st> <st c="63716">the course details as key-value pairs in database</st> `<st c="63767">0</st>` <st c="63768">of Redis.</st> <st c="63779">To update a record, retrieve the data object using its</st> `<st c="63834">pk</st>` <st c="63836">from the database and then invoke the</st> `<st c="63875">update()</st>` <st c="63883">method of the resulting model object with the new field values.</st> <st c="63948">The following</st> `<st c="63962">update_course()</st>` <st c="63977">method applies this</st> `<st c="63998">redis-om</st>` <st c="64006">approach:</st>

def update_course(self, details:Dict[str, Any]):

    try: <st c="64071">记录 = SearchCourse.get(details['pk'])</st><st c="64111">记录.update(**details)</st> return True

    except Exception as e:

        print(e)

    return False

			<st c="64193">When deleting a record,</st> `<st c="64218">HashModel</st>` <st c="64227">has a class method called</st> `<st c="64254">delete()</st>` <st c="64262">that removes a hashed object using its</st> `<st c="64302">pk</st>`<st c="64304">, similar to the following</st> `<st c="64331">delete_course()</st>` <st c="64346">method:</st>

def delete_course(self, pk):

    try: <st c="64389">SearchCourse.delete(pk)</st> return True

    except Exception as e:

        print(e)

    return False

			<st c="64469">When retrieving data</st> <st c="64490">from the database, the</st> `<st c="64514">get()</st>` <st c="64519">method is the only way to retrieve a single model object using an existing</st> `<st c="64595">pk</st>`<st c="64597">. Querying all the records requires a</st> `<st c="64635">for</st>` <st c="64638">loop to enumerate all</st> `<st c="64661">pk</st>` <st c="64663">values from the</st> `<st c="64680">HashModel</st>` <st c="64689">entity’s</st> `<st c="64699">all_pks()</st>` <st c="64708">generator, which retrieves all</st> `<st c="64740">pk</st>` <st c="64742">from the database.</st> <st c="64762">The loop will fetch all the model objects using the enumerated</st> `<st c="64825">pk</st>`<st c="64827">. The following</st> `<st c="64843">select_course()</st>` <st c="64858">class retrieves all course details from the</st> `<st c="64903">search_course</st>` <st c="64916">table using the</st> `<st c="64933">pk</st>` <st c="64935">value of</st> <st c="64945">each record:</st>

def select_course(self, pk):

    try: <st c="64992">记录 = SearchCourse.get(pk)</st><st c="65021">return 记录.dict()</st> except Exception as e:

        print(e)

    return None

def select_all_course(self):

    records = list() <st c="65133">for id in SearchCourse.all_pks():</st><st c="65166">records.append(SearchCourse.get(id).dict())</st> return records

			<st c="65225">All resulting objects from the query transactions are JSONable and don’t need a JSON serializer.</st> <st c="65323">Running the given</st> `<st c="65341">select_all_course()</st>` <st c="65360">class will return the following sample</st> <st c="65400">Redis records:</st>

{

"records": [

    {

        "code": "PY-201",

        "description": "高级 Python",

        "level": 3, <st c="65496">"pk": "01HDH2VPZBGJJ16JKE3KE7RGPQ",</st> "req_hrs": 50.0,

        "title": "高级 Python 编程",

        "total_cost": 15000.0

    },

    {

        "code": "PY-101",

        "description": "Python 编程入门",

        "level": 1, <st c="65692">"pk": "01HDH2SVYR7AYMRD28RE6HSHYB",</st> "req_hrs": 45.0,

        "title": "Python 基础",

        "total_cost": 5000.0

    },

			<st c="65794">Although Redis OM</st> <st c="65812">is perfectly compatible with FastAPI, it can also make any Flask application a client for Redis.</st> <st c="65910">Now, Redis OM cannot implement filtered queries.</st> <st c="65959">If Redis OM needs a filtered search with some constraints, it needs a</st> *<st c="66029">RediSearch</st>* <st c="66039">extension module that calibrates and provides more search constraints to query transactions.</st> <st c="66133">But</st> *<st c="66137">RediSearch</st>* <st c="66147">can only run with Redis OM if the application uses Redis Stack instead of the</st> <st c="66226">typical server.</st>
			<st c="66241">The next section will highlight a</st> *<st c="66276">document-oriented</st>* <st c="66293">NoSQL database that’s popular for enterprise application</st> <st c="66351">development:</st> *<st c="66364">MongoDB</st>*<st c="66371">.</st>
			<st c="66372">Handling BSON-based documents with MongoDB</st>
			**<st c="66415">MongoDB</st>** <st c="66423">is a NoSQL database that stores JSON-like</st> <st c="66465">documents of key-value pairs</st> <st c="66495">with a flexible and scalable schema, thus classified as a document-oriented database.</st> <st c="66581">It can store huge volumes of data with varying data structures, types,</st> <st c="66652">and formations.</st>
			<st c="66667">These JSON-like documents use</st> **<st c="66698">Binary Javascript Object Notation</st>** <st c="66731">(</st>**<st c="66733">BSON</st>**<st c="66737">), a binary-encoded representation</st> <st c="66772">of JSON documents suitable for network-based data transport because of its compact nature.</st> <st c="66864">It has non-JSON-native data type support for date and binary data and recognizes embedded documents or an array of documents because of its</st> <st c="67004">traversable structure.</st>
			<st c="67026">Next, we’ll install MongoDB and compare its process to HBase, Cassandra,</st> <st c="67100">and Redis.</st>
			<st c="67110">Installing and configuring the MongoDB server</st>
			<st c="67156">First, download the</st> <st c="67176">preferred MongoDB</st> <st c="67194">community server from</st> [<st c="67217">https://www.mongodb.com/try/download/community</st>](https://www.mongodb.com/try/download/community)<st c="67263">. Install it to</st> <st c="67278">your preferred drive and directory.</st> <st c="67315">Next, create a data directory where MongoDB can store its documents.</st> <st c="67384">If the data folder doesn’t exist, MongoDB will resort to</st> `<st c="67441">C:\data\db</st>` <st c="67451">as its default data folder.</st> <st c="67480">Afterward, run the server by running the</st> <st c="67521">following command:</st>

mongod.exe --dbpath="c:\data\db"


			<st c="67572">The default host of the server is localhost, and its port</st> <st c="67631">is</st> `<st c="67634">27017</st>`<st c="67639">.</st>
			<st c="67640">Download MongoDB Shell</st> <st c="67663">from</st> [<st c="67669">https://www.mongodb.com/try/download/shell</st>](https://www.mongodb.com/try/download/shell) <st c="67711">to open the client console for the server.</st> <st c="67755">Also, download</st> <st c="67769">MongoDB Compass from</st> [<st c="67791">https://www.mongodb.com/try/download/compass</st>](https://www.mongodb.com/try/download/compass)<st c="67835">, the GUI administration tool for the</st> <st c="67873">database server.</st>
			<st c="67889">So far, installing the MongoDB server and its tools takes less time than installing the other NoSQL databases.</st> <st c="68001">Next, we’ll integrate MongoDB into our</st> *<st c="68040">Tutor</st>* *<st c="68046">Finder</st>* <st c="68052">application.</st>
			<st c="68065">Establishing a database connection</st>
			<st c="68100">To create database</st> <st c="68119">connectivity, MongoDB uses the</st> `<st c="68151">pymongo</st>` <st c="68158">module as its native driver, which is made from BSON utilities.</st> <st c="68223">However, the driver requires more codes to implement the CRUD transactions because it offers low-level utilities.</st> <st c="68337">A high-level and object-oriented module, such as</st> `<st c="68386">mongoengine</st>`<st c="68397">, can provide a better database connection than</st> `<st c="68445">pymongo</st>`<st c="68452">. The</st> `<st c="68458">mongoengine</st>` <st c="68469">library is a popular</st> **<st c="68491">object document mapper</st>** <st c="68513">(</st>**<st c="68515">ODM</st>**<st c="68518">) that can build a client application</st> <st c="68556">with a model and</st> <st c="68574">repository layers.</st>
			<st c="68592">The</st> `<st c="68597">flask-mongoengine</st>` <st c="68614">library is written solely for Flask.</st> <st c="68652">However, since Flask 3.x, the</st> `<st c="68682">flask.json</st>` <st c="68692">module, on which the module is tightly dependent, was removed.</st> <st c="68756">This change affected the</st> `<st c="68781">MongoEngine</st>` <st c="68792">class of the</st> `<st c="68806">flask_mongoengine</st>` <st c="68823">library, which creates a MongoDB client.</st> <st c="68865">Until the library is updated so that it supports the latest version of Flask, the native</st> `<st c="68954">connect()</st>` <st c="68963">method from the native</st> `<st c="68987">mongoengine.connection</st>` <st c="69009">module will always be the solution to connect to the MongoDB database.</st> <st c="69081">The following snippet from the</st> *<st c="69112">Tutor Finder</st>* <st c="69124">application’s</st> `<st c="69139">create_app()</st>` <st c="69151">factory method uses</st> `<st c="69172">connect()</st>` <st c="69181">to establish communication with the</st> <st c="69218">MongoDB server:</st>

from mongoengine.connection 导入 连接 def 创建应用(配置文件):

app = Flask(__name__)

app.config.from_file(配置文件, toml.load <st c="69455">连接()</st> 方法需要 <st c="69481">数据库名</st>,<st c="69490">主机</st>,<st c="69496">端口</st>,以及服务器将用于识别 UUID 主键的 <st c="69518">UUID</st> 类型。这可以是 <st c="69590">未指定</st>,<st c="69603">标准</st>,<st c="69613">pythonLegacy</st>,<st c="69627">javaLegacy</st>,或 <st c="69642">csharpLegacy</st>。

        <st c="69655">另一方面,客户端</st> <st c="69685">可以调用</st> `<st c="69701">断开连接()</st>` <st c="69713">方法来关闭</st> <st c="69730">连接。</st>

        <st c="69745">现在,让我们使用来自</st> `<st c="69813">flask-mongoengine</st>` <st c="69830">模块的辅助类来构建模型层。</st>

        <st c="69838">构建模型层</st>

        <st c="69863">`<st c="69868">flask-mongoengine</st>` <st c="69885">模块有一个</st> `<st c="70151">文档</st>` <st c="70159">基类来构建我们应用的登录详细信息:</st>
 from mongoengine import <st c="70242">Document</st>, <st c="70252">SequenceField</st>, <st c="70267">BooleanField</st>, <st c="70281">EmbeddedDocumentField</st>, <st c="70304">BinaryField</st>, <st c="70317">IntField</st>, <st c="70327">StringField</st>, <st c="70340">DateField</st>, <st c="70351">EmailField</st>, <st c="70363">EmbeddedDocumentListField</st>, <st c="70390">EmbeddedDocument</st> class Savings(<st c="70421">EmbeddedDocument</st>):
    acct_name = StringField(db_field='acct_name', max_length=100, required=True)
    acct_number = StringField(db_field='acct_number', max_length=16, required=True)
        … … … … … …
class Checking(EmbeddedDocument):
    acct_name = StringField(db_field='acct_name', max_length=100, required=True)
    acct_number = StringField(db_field='acct_number', max_length=16, required=True)
    bank =  StringField(db_field='bank', max_length=100, required=True)
    … … … … … …
class PayPal(EmbeddedDocument):
    email = EmailField(db_field='email', max_length=20, required=True)
    address = StringField(db_field='address', max_length=200, required=True)
class Tutor(EmbeddedDocument):
    firstname = StringField(db_field='firstname', max_length=50, required=True)
    lastname = StringField(db_field='lastname', max_length=50, required=True)
    … … …. … … <st c="71245">savings = EmbeddedDocumentListField(Savings, required=False)</st><st c="71305">checkings = EmbeddedDocumentListField(Checking, required=False)</st><st c="71369">gcash = EmbeddedDocumentField(GCash, required=False)</st><st c="71422">paypal = EmbeddedDocumentField(PayPal, required=False)</st> class TutorLogin(Document):
    id = SequenceField(required=True, primary_key=True)
    username = StringField(db_field='email',max_length=25, required=True)
    password = StringField(db_field='password',maxent=25, required=True)
    encpass = BinaryField(db_field='encpass', required=True) <st c="71827">flask-mongengine</st> offers helper classes, such as <st c="71875">StringField</st>, <st c="71888">BinaryField</st>, <st c="71901">DateField</st>, <st c="71912">IntField</st>, and <st c="71926">EmailField</st>, that build the metadata of a document. These helper classes have parameters, such as <st c="72023">db_field</st> and <st c="72036">required</st>, that will add details to the key-value pairs. Moreover, some parameters appear only in one helper class, such as <st c="72159">min_length</st> and <st c="72174">max_length</st> in <st c="72188">StringField</st>, because they control the number of characters in the string. Likewise, <st c="72272">ByteField</st> has a <st c="72288">max_bytes</st> parameter that will not appear in other helper classes. Note that, the <st c="72369">Document</st> base class’ <st c="72390">BinaryField</st> translates to BSON’s binary data and <st c="72439">DateField</st> to BSON’s date type, not the common Python type.
			<st c="72497">Unlike Cassandra, Redis, and HBase, MongoDB</st> <st c="72541">allows relationships among structures.</st> <st c="72581">Although not normalized like in an RDBMS, MongoDB can link one document to its subdocuments using the</st> `<st c="72683">EmbeddedDocumentField</st>` <st c="72704">and</st> `<st c="72709">EmbeddedDocumentListField</st>` <st c="72734">helper classes.</st> <st c="72751">In the given model classes, the</st> `<st c="72783">TutorLogin</st>` <st c="72793">model will create a parent document collection called</st> `<st c="72848">tutor_login</st>` <st c="72859">that will reference a</st> `<st c="72882">tutor</st>` <st c="72887">sub-document because the sub-document’s</st> `<st c="72928">Tutor</st>` <st c="72933">model is an</st> `<st c="72946">EmbeddedDocumentField</st>` <st c="72967">helper class of the parent</st> `<st c="72995">TutorLogin</st>` <st c="73005">model.</st> <st c="73013">The idea is similar to a one-to-one relationship concept in a relational ERD but not totally the same.</st> <st c="73116">On the other hand, the relationship between</st> `<st c="73160">Tutor</st>` <st c="73165">and</st> `<st c="73170">Savings</st>` <st c="73177">is like a one-to-many relationship because</st> `<st c="73221">Savings</st>` <st c="73228">is the</st> `<st c="73236">EmbeddedDocumentListField</st>` <st c="73261">helper class of the</st> `<st c="73282">Tutor</st>` <st c="73287">model.</st> <st c="73295">In other words, the</st> `<st c="73315">tutor</st>` <st c="73320">document collections will reference a list of savings sub-documents.</st> <st c="73390">Here</st> `<st c="73395">EmbeddedDocumentField</st>` <st c="73416">does not have an</st> `<st c="73434">_id</st>` <st c="73437">field because it cannot construct an actual document</st> <st c="73490">collection, unlike in an independent</st> `<st c="73528">Document</st>` <st c="73536">base class.</st>
			<st c="73548">Next, we’ll create the repository layer from the</st> `<st c="73598">Tutor</st>` <st c="73603">document and</st> <st c="73617">its sub-documents.</st>
			<st c="73635">Implementing the repository</st>
			<st c="73663">The</st> `<st c="73668">Document</st>` <st c="73676">object emits utility methods</st> <st c="73705">that perform CRUD operations for the repository layer.</st> <st c="73761">Here is a</st> `<st c="73771">TutorLoginRepository</st>` <st c="73791">class that inserts, updates, deletes, and retrieves</st> `<st c="73844">tutor_login</st>` <st c="73855">documents:</st>

from typing 导入 Dict, Any

from 模块.模型.db.mongo_models 导入 导师登录

import json

class 导师登录存储库:

def 插入登录(self, 详细信息: Dict[str, Any]) -> bool:

    try: <st c="74051">登录 = 导师登录(**详细信息)</st><st c="74080">登录保存()</st> except Exception as e:

        print(e)

        返回 False

    返回 True

			<st c="74150">The</st> `<st c="74155">insert_login()</st>` <st c="74169">method uses the</st> `<st c="74186">save()</st>` <st c="74192">method of the</st> `<st c="74207">TutorLogin</st>` <st c="74217">model object for persistence.</st> <st c="74248">The</st> `<st c="74252">save()</st>` <st c="74258">method will persist all the</st> `<st c="74287">kwargs</st>` <st c="74293">data that’s passed to the constructor</st> <st c="74332">of</st> `<st c="74335">TutorLogin</st>`<st c="74345">.</st>
			<st c="74346">Like the Cassandra</st> <st c="74365">driver, the</st> `<st c="74378">Document</st>` <st c="74386">class has an</st> `<st c="74400">objects</st>` <st c="74407">class attribute that provides all query methods.</st> <st c="74457">Updating a document uses the</st> `<st c="74486">objects</st>` <st c="74493">attribute to filter the data using any document keys and then fetches the record using the attribute’s</st> `<st c="74597">get()</st>` <st c="74602">method.</st> <st c="74611">If the document exists, the</st> `<st c="74639">update()</st>` <st c="74647">method of the filtered record object will update the given</st> `<st c="74707">kwargs</st>` <st c="74713">of fields that require updating.</st> <st c="74747">The following code shows</st> `<st c="74772">update_login()</st>`<st c="74786">, which updates a</st> `<st c="74804">TutorLogin</st>` <st c="74814">document:</st>

def 更新登录(self, id: int, 详细信息: Dict[str, Any]) -> bool:

try: <st c="74894">登录 = 导师登录对象(id=id).获取()</st><st c="74933">登录更新(**详细信息)</st> except:

    返回 False

返回 True

			<st c="74990">Deleting a document in MongoDB also uses the</st> `<st c="75036">objects</st>` <st c="75043">attribute to filter and extract the document that needs to be removed.</st> <st c="75115">The</st> `<st c="75119">delete()</st>` <st c="75127">method of the retrieved model object will delete the filtered record from the database once the repository invokes it.</st> <st c="75247">Here,</st> `<st c="75253">delete_login()</st>` <st c="75267">removes a filtered document from</st> <st c="75301">the database:</st>

def 删除登录(self, id: int) -> bool:

    try: <st c="75360">登录 = 导师登录对象(id=id).获取()</st><st c="75399">登录删除()</st> except:

        返回 False

    返回 True

			<st c="75447">The</st> `<st c="75452">objects</st>` <st c="75459">attribute is responsible</st> <st c="75484">for implementing all the query transactions.</st> <st c="75530">Here,</st> `<st c="75536">get_login()</st>` <st c="75547">fetches a single object identified by its unique</st> `<st c="75597">_id</st>` <st c="75600">value using the</st> `<st c="75617">get()</st>` <st c="75622">method, while the</st> `<st c="75641">get_login_username()</st>` <st c="75661">transaction retrieves a single record filtered by the tutor’s username and password.</st> <st c="75747">On the other hand,</st> `<st c="75766">get_all_login()</st>` <st c="75781">retrieves all the</st> `<st c="75800">tutor_login</st>` <st c="75811">documents from</st> <st c="75827">the database:</st>

def 获取所有登录(self):登录 = 导师登录对象() return json.loads(登录.to_json()) def 获取登录(self, id: int):登录 = 导师登录对象(id=id).获取()返回 登录.to_json()def 获取登录用户名(self, 用户名: str, 密码: str):登录 = 导师登录对象(username=用户名, 密码=密码).获取() return 登录.to_json()


			<st c="76173">All these query transactions invoke the built-in</st> `<st c="76223">to_json()</st>` <st c="76232">method, which serializes and converts the BSON-based documents into JSON for the API’s response</st> <st c="76329">generation process.</st>
			<st c="76348">Embedded documents do not have dedicated collection storage because they are part of a parent document collection.</st> <st c="76464">Adding and removing embedded documents from the parent document requires using string queries and operators or typical object referencing in Python, such as setting to</st> `<st c="76632">None</st>` <st c="76636">when removing a sub-document.</st> <st c="76667">The following repository adds</st> <st c="76696">and removes a tutor’s profile details from the</st> <st c="76744">login credentials:</st>

from typing 导入 Dict, Any

from 模块.模型.db.mongo_models 导入 导师登录, 导师

class 导师档案存储库:

def 添加导师档案(self, 详细信息: Dict[str, Any]) -> bool:

    try: <st c="76949">登录 = 导师登录对象(id=详细信息['id'])</st> <st c="76993">.获取()</st> del 详细信息['id'] <st c="77018">档案 = 导师(**详细信息)</st><st c="77044">登录更新(导师=档案)</st> except Exception as e:

        print(e)

        返回 False

    返回 True

			<st c="77129">Here,</st> `<st c="77136">add_tutor_profile()</st>` <st c="77155">embeds the</st> `<st c="77167">TutorProfile</st>` <st c="77179">document via the tutor key of the</st> `<st c="77214">TutorLogin</st>` <st c="77224">main document.</st> <st c="77240">Another solution is to pass the</st> `<st c="77272">set_tutor=profile</st>` <st c="77289">query parameter to the</st> `<st c="77313">update()</st>` <st c="77321">operation.</st> <st c="77333">The following transaction removes the tutor</st> <st c="77376">profile from the</st> <st c="77394">main document:</st>

def 删除导师档案(self, id: int) -> bool:

    try: <st c="77462">登录 = 导师登录对象(id=id).获取()</st><st c="77501">登录更新(导师=None)</st> except Exception as e:

        print(e)

        返回 False

    返回 True

			<st c="77583">Then,</st> `<st c="77590">delete_tutor_profile()</st>` <st c="77612">unsets the profile document from the</st> `<st c="77650">TutorLogin</st>` <st c="77660">document by setting the tutor field to</st> `<st c="77700">None</st>`<st c="77704">. Another way to do this is to use the</st> `<st c="77743">unset__tutor=True</st>` <st c="77760">query parameter for the</st> `<st c="77785">update()</st>` <st c="77793">method.</st>
			<st c="77801">The most effective way to manage a list of embedded documents is to use query strings or query parameters to avoid lengthy implementations.</st> <st c="77942">The following</st> `<st c="77956">SavingsRepository</st>` <st c="77973">class adds and removes a bank account from a list of savings accounts of a tutor.</st> <st c="78056">Its</st> `<st c="78060">add_savings()</st>` <st c="78073">method adds a new saving account to the tutor’s list of saving accounts.</st> <st c="78147">It uses the</st> `<st c="78159">update()</st>` <st c="78167">method with the</st> `<st c="78184">push__tutor__savings=savings</st>` <st c="78212">query parameter, which pushes a new</st> `<st c="78249">Savings</st>` <st c="78256">instance to</st> <st c="78269">the list:</st>

from typing import Dict, Any

from modules.models.db.mongo_models import Savings, TutorLogin

class SavingsRepository: def add_savings(self, details:Dict[str, Any]): try: login = TutorLogin.objects(id=details['id']) .get()del details['id']savings = Savings(**details)login.update(push__tutor__savings=savings) except Exception as e:

        print(e)

        return False

    return True

			<st c="78645">On the other hand, the</st> `<st c="78669">delete_savings()</st>` <st c="78685">method deletes an account using the</st> `<st c="78722">pull__tutor__savings__acct_number= details['acct_number']</st>` <st c="78779">query parameter, which removes a savings account from</st> <st c="78834">the list:</st>

def delete_savings(self, details:Dict[str, Any]): try: login = TutorLogin.objects(id=details['id']) .get() login.update(pull__tutor__savings__acct_number= details['acct_number']) except Exception as e:

        print(e)

        return False

    return True

			<st c="79078">Although MongoDB is popular</st> <st c="79106">and has the most support, it slows down when the number of users increases.</st> <st c="79183">When the datasets become massive, adding more replications and configurations becomes difficult due to its master-slave architecture.</st> <st c="79317">Adding caches is also part of the plan to improve</st> <st c="79367">data retrieval.</st>
			<st c="79382">However, there is another document-oriented NoSQL database that’s designed for distributed architecture and high availability with internal caching for</st> <st c="79535">datasets:</st> **<st c="79545">Couchbase</st>**<st c="79554">.</st>
			<st c="79555">Managing key-based JSON documents with Couchbase</st>
			**<st c="79604">Couchbase</st>** <st c="79614">is a NoSQL database that’s designed</st> <st c="79650">for distributed architectures</st> <st c="79680">and offers high performance on concurrent, web-based, and cloud-based applications.</st> <st c="79765">It</st> <st c="79767">supports distributed</st> **<st c="79789">ACID</st>** <st c="79793">transactions and has a</st> <st c="79816">SQL-like language called</st> **<st c="79842">N1QL</st>**<st c="79846">. All documents stored in Couchbase databases</st> <st c="79892">are JSON-formatted.</st>
			<st c="79911">Now, let’s install and configure the Couchbase</st> <st c="79959">database server.</st>
			<st c="79975">Installing and configuring the database instance</st>
			<st c="80024">To begin, download</st> <st c="80043">Couchbase Community Edition from</st> [<st c="80077">https://www.couchbase.com/downloads/</st>](https://www.couchbase.com/downloads/)<st c="80113">. Once it’s been installed, Couchbase will need cluster</st> <st c="80168">configuration details</st> <st c="80190">to be added, including the user profile for accessing the server dashboard at</st> `<st c="80269">http://localhost:8091/ui/index.html</st>`<st c="80304">. Accepting the user agreement for the configuration is also part of the process.</st> <st c="80386">After configuring the cluster, the URL will show us the login form to access the default server instance.</st> *<st c="80492">Figure 7</st>**<st c="80500">.16</st>* <st c="80503">shows the login page of the Couchbase</st> <st c="80542">web portal:</st>
			![Figure 7.16 – Accessing the login page of Couchbase](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_07_016.jpg)

			<st c="80601">Figure 7.16 – Accessing the login page of Couchbase</st>
			<st c="80652">After logging in to the portal, the next step is to create a</st> *<st c="80714">bucket</st>*<st c="80720">. A</st> *<st c="80724">bucket</st>* <st c="80730">is a named container that saves all the data in Couchbase.</st> <st c="80790">It groups all the keys and values based on collections and scopes.</st> <st c="80857">Somehow, it is similar to the concept of a database schema in a relational DBMS.</st> *<st c="80938">Figure 7</st>**<st c="80946">.17</st>* <st c="80949">shows</st> **<st c="80956">packtbucket</st>**<st c="80967">, which has been created on the</st> **<st c="80999">Buckets</st>** <st c="81006">dashboard:</st>
			![Figure 7.17 – Creating a bucket in the cluster](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_07_017.jpg)

			<st c="81241">Figure 7.17 – Creating a bucket in the cluster</st>
			<st c="81287">Afterward, create a scope</st> <st c="81313">that will hold the tables or document</st> <st c="81352">collections of the database instance.</st> <st c="81390">A bucket scope is a named mechanism that manages and organizes these collections.</st> <st c="81472">In some aspects, it is similar to a tablespace in a relational DBMS.</st> <st c="81541">To create these scopes, click the</st> **<st c="81575">Scopes & Collections</st>** <st c="81595">hyperlink to the right of the bucket name on the</st> **<st c="81645">Add Bucket</st>** <st c="81655">page.</st> <st c="81662">The</st> **<st c="81666">Add Scope</st>** <st c="81675">page will appear, as shown in</st> *<st c="81706">Figure 7</st>**<st c="81714">.18</st>*<st c="81717">:</st>
			![Figure 7.18 – Creating a scope in a bucket](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_07_018.jpg)

			<st c="81968">Figure 7.18 – Creating a scope in a bucket</st>
			<st c="82010">On the</st> `<st c="82134">tfs</st>`<st c="82137">.</st>
			<st c="82138">Lastly, click the</st> `<st c="82342">tfs</st>` <st c="82345">collections:</st>
			![Figure 7.19 – List of collections in a tfs scope](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_07_019.jpg)

			<st c="82943">Figure 7.19 – List of collections in a tfs scope</st>
			<st c="82991">Now, let’s establish the bucket connection using the</st> `<st c="83045">couchbase</st>` <st c="83054">module.</st>
			<st c="83062">Setting up the server connection</st>
			<st c="83095">To create a client connection</st> <st c="83125">to Couchbase, install the</st> `<st c="83152">couchbase</st>` <st c="83161">module using the</st> `<st c="83179">pip</st>` <st c="83182">command:</st>

pip 安装 couchbase


			<st c="83213">In the</st> `<st c="83221">create_app()</st>` <st c="83233">factory function of the application, perform the following steps to access the Couchbase</st> <st c="83323">server instance:</st>

				1.  <st c="83339">Create a</st> `<st c="83349">PasswordAuthenticator</st>` <st c="83370">object with the correct user profile’s credential to access the</st> <st c="83435">specified bucket.</st>
				2.  <st c="83452">Instantiate the</st> `<st c="83469">Cluster</st>` <st c="83476">class with its required constructor arguments, namely the Couchbase URL and some options, such as the</st> `<st c="83579">PasswordAuthenticator</st>` <st c="83600">object, wrapped in the</st> `<st c="83624">ClusterOptions</st>` <st c="83638">instance.</st>
				3.  <st c="83648">Access the preferred bucket by calling the</st> `<st c="83692">Cluster</st>`<st c="83699">’s</st> `<st c="83703">bucket()</st>` <st c="83711">instance method.</st>

			<st c="83728">The following snippet shows</st> <st c="83756">how to implement these steps in our</st> *<st c="83793">Tutor Finder</st>* <st c="83805">application’s</st> `<st c="83820">create_app()</st>` <st c="83832">method:</st>

from couchbase.auth import PasswordAuthenticator

from couchbase.cluster import Cluster

from couchbase.options import ClusterOptions def create_app(config_file):

app = Flask(__name__)

app.config.from_file(config_file, toml.load) <st c="84069">auth = PasswordAuthenticator("sjctrags", "packt2255",)</st><st c="84123">cluster = Cluster('couchbase://localhost',</st> <st c="84166">ClusterOptions(auth))</st> cluster.wait_until_ready(timedelta(seconds=5))

全局 cb <st c="84285">Cluster</st> 对象有一个 <st c="84306">wait_until_ready()</st> 方法,它会 ping Couchbase 服务以检查连接状态,并在连接就绪后返回控制权给 <st c="84424">create_app()</st>。但是调用此方法会减慢 Flask 服务器的启动速度。我们的应用程序仅出于实验目的调用了该方法。

        `<st c="84608">在成功</st> `<st c="84628">设置</st>` `<st c="84654">Bucket</st>` `<st c="84660">对象后,我们必须确保</st>` `<st c="84698">存储层</st>` `<st c="84698">可以实施。</st>`

        <st c="84715">创建存储层</st>

        <st c="84745">存储层</st> <st c="84766">需要</st> `<st c="84777">Bucket</st>` <st c="84783">对象从</st> `<st c="84796">create_app()</st>` <st c="84808">中实现 CRUD 事务。</st> `<st c="84849">Bucket</st>` <st c="84855">对象有一个</st> `<st c="84869">scope()</st>` <st c="84876">方法,它将访问包含集合的容器空间。</st> `<st c="84952">它返回一个</st> `<st c="84965">Scope</st>` <st c="84970">对象,该对象会发出</st> `<st c="84989">collection()</st>`<st c="85001">,以检索首选文档集合。</st> `<st c="85055">在此处,</st>` `<st c="85061">DirectMessageRepository</st>` <st c="85084">管理学生向教练发送的所有直接消息以及</st> `<st c="85152">反之亦然:</st>`
 class DirectMessageRepository:
    def insert_dm(self, details:Dict[str, Any]):
        try: <st c="85245">cb_coll = cb.scope("tfs")</st> <st c="85270">.collection("direct_messages")</st> key = "chat_" + str(details['id']) + '-' + str(details["date_sent"]) <st c="85370">cb_coll.insert(key, details)</st> return True
        except Exception as e:
            print(e)
        return False
        `<st c="85455">The</st>` `<st c="85460">dm_insert()</st>` <st c="85471">方法使我们能够访问</st> `<st c="85502">tfs</st>` <st c="85505">范围及其</st> `<st c="85520">direct_messages</st>` <st c="85535">文档集合。</st> <st c="85558">其主要目标是使用集合的</st> `<st c="85719">insert()</st>` <st c="85727">方法通过给定的键将导师和训练师之间的聊天消息的详细信息插入文档集合。</st>

        `<st c="85735">另一方面,</st>` `<st c="85759">update_dm()</st>` <st c="85770">方法使用集合的</st> `<st c="85800">upsert()</st>` <st c="85808">方法通过键来更新 JSON 文档:</st>
 def update_dm(self, details:Dict[str, Any]):
        try: <st c="85905">cb_coll = cb.scope("tfs")</st> <st c="85930">.collection("direct_messages")</st> key = "chat_" + str(details['id']) + '-' + str(details["date_sent"]) <st c="86030">cb_coll.upsert(key, details)</st> return True
        except Exception as e:
            print(e)
        return False
        `<st c="86115">集合的</st>` `<st c="86133">remove()</st>` <st c="86141">方法从集合中删除一个文档。</st> <st c="86189">这可以在以下</st> `<st c="86223">delete_dm()</st>` <st c="86234">事务中看到,其中它使用</st> `<st c="86286">其</st>` `<st c="86290">键</st>`<st c="86293">删除一个聊天消息:</st>
 def delete_dm_key(self, details:Dict[str, Any]):
        try: <st c="86350">cb_coll = cb.scope("tfs")</st> <st c="86375">.collection("direct_messages")</st> key = "chat_" + str(details['id']) + '-' + str(details["date_sent"]) <st c="86475">cb_coll.remove(key)</st> return True
        except Exception as e:
            print(e)
        return False
        Couchbase 与 MongoDB 不同,使用一种类似 SQL 的机制,称为*<st c="86612">N1QL</st>*来检索文档。<st c="86616">以下</st> *<st c="86654">DELETE</st>* <st c="86660">事务使用 N1QL 查询事务而不是集合的</st> `<st c="86733">delete()</st>` <st c="86741">方法:</st>
 def delete_dm_sender(self, sender):
        try: <st c="86791">cb_scope = cb.scope("tfs")</st><st c="86817">stmt = f"delete from `direct_messages` where</st> <st c="86862">`sender_id` LIKE '{sender}'"</st><st c="86891">cb_scope.query(stmt)</st> return True
        except Exception as e:
            print(e)
        return False
        `<st c="86969">The</st>` `<st c="86974">Scope</st>` <st c="86979">实例,由</st> `<st c="87007">Bucket</st>` <st c="87013">对象的</st> `<st c="87023">scope()</st>` <st c="87030">方法派生而来,有一个</st> `<st c="87045">query()</st>` <st c="87052">方法,用于执行字符串形式的查询语句。</st> <st c="87108">查询语句应将集合和字段名称用引号括起来(</st>```py<st c="87190">``</st>```<st c="87193">),而其字符串约束值应使用单引号。</st> <st c="87260">因此,我们有了</st> ``<st c="87278">delete from `direct_messages` where `sender_id` LIKE '{sender},'</st>`` <st c="87342">查询语句在</st> `<st c="87362">delete_dm_sender()</st>`<st c="87380">中,其中</st> `<st c="87388">sender</st>` <st c="87394">是一个</st> <st c="87400">参数值。</st>

        `<st c="87416">在</st> *<st c="87456">DELETE</st>* <st c="87462">和</st> *<st c="87467">UPDATE</st>* <st c="87473">事务中使用 N1QL 查询的优势在于,键不是执行这些操作的唯一依据。</st> <st c="87524">*<st c="87562">DELETE</st>* <st c="87568">操作可以基于其他字段来删除文档,例如使用给定的</st> <st c="87674">sender ID</st> <st c="87678">删除聊天消息:</st>
 def delete_dm_sender(self, sender):
        try:
            cb_scope = cb.scope("tfs")
            stmt = f"delete from `direct_messages` where `sender_id` LIKE '{sender}'"
            cb_scope.query(stmt)
            return True
        except Exception as e:
            print(e)
        return False
        *<st c="87904">N1QL</st>* <st c="87909">在从键空间中检索 JSON 文档时,无论是否有约束,都很受欢迎。</st> <st c="87997">以下查询事务使用</st> *<st c="88038">SELECT</st>* <st c="88044">查询语句来检索</st> `<st c="88098">direct_messages</st>` <st c="88113">集合中的所有文档:</st>
 def select_all_dm(self):
        cb_scope = cb.scope("tfs")
        raw_data = cb_scope.query('select * from `direct_messages`', QueryOptions(read_only=True))
        records = [rec for rec in raw_data.rows()]
        return records
        <st c="88327">Couchbase 可以是 Flask 应用程序管理 JSON 数据转储的合适后端存储形式。</st> <st c="88438">Flask 和 Couchbase 可以构建快速、可扩展且高效的微服务或分布式应用程序,具有快速开发和较少的数据库管理。</st> <st c="88597">然而,与 HBase、Redis、Cassandra、MongoDB 和 Couchbase 相比,Flask 可以与图数据库,如 Neo4J,集成以进行</st> <st c="88686">图相关算法。</st>

        <st c="88753">与 Neo4J 建立数据关系</st>

        **<st c="88797">Neo4J</st>** <st c="88803">是一个专注于数据之间关系的 NoSQL 数据库。</st> <st c="88836">它不存储文档,而是存储节点、关系以及连接这些节点的属性。</st> <st c="88964">Neo4J 也因其基于由节点和节点之间有向线组成的图模型的概念而被称为流行的图数据库。</st>

        <st c="89107">在将我们的应用程序集成到 Neo4J 数据库之前,我们必须使用 Neo4J 桌面安装当前版本的 Neo4J 平台。</st> <st c="89232">。</st>

        <st c="89246">安装 Neo4J 桌面</st>

        <st c="89271">Neo4J 桌面提供了一个本地开发环境,并包括学习数据库所需的所有功能,从创建自定义本地数据库到启动</st> <st c="89441">Neo4J 浏览器。</st> <st c="89461">其安装程序可在</st> <st c="89488">[<st c="89491">https://neo4j.com/download/</st>](https://neo4j.com/download/)<st c="89518">找到。</st>

        <st c="89519">安装完成后,创建一个包含本地数据库和配置设置的 Neo4J 项目。</st> <st c="89635">除了项目名称外,此过程还会要求输入用户名和密码以供身份验证详情使用。</st> <st c="89752">完成这些操作后,删除其默认的 Movie 数据库,并创建必要的图数据库。</st> *<st c="89850">图 7</st>**<st c="89858">.20</st>* <st c="89861">显示</st> **<st c="89868">Packt Flask 项目</st>** <st c="89887">与一个</st> **<st c="89895">导师</st>** <st c="89900">数据库:</st>

        ![图 7.20 – Neo4J 桌面仪表板](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_07_020.jpg)

        <st c="90213">图 7.20 – Neo4J 桌面仪表板</st>

        <st c="90254">Flask 可以通过多种方式连接到图数据库,其中之一是通过</st> `<st c="90348">py2neo</st>` <st c="90354">库。</st> <st c="90364">我们将在下一节中对此进行更详细的探讨。</st>

        <st c="90417">建立数据库连接</st>

        <st c="90459">首先,使用</st> `<st c="90478">py2neo</st>` <st c="90484">通过</st> `<st c="90491">pip</st>` <st c="90498">命令安装:</st>
 pip install py2neo
        <st c="90526">接下来,在主项目文件夹中创建一个</st> `<st c="90542">neo4j_config.py</st>` <st c="90557">模块,包含以下脚本以确保</st> <st c="90628">数据库连接:</st>
<st c="90650">from py2neo import Graph</st> def db_auth(): <st c="90691">graph = Graph("bolt://127.0.0.1:7687", auth=("neo4j",</st> <st c="90744">"packt2255"))</st> return graph
        <st c="90771">现在,调用给定的</st> `<st c="90795">db_auth()</st>` <st c="90804">方法将启动与主机、端口和认证详情的 bolts 连接协议,通过</st> `<st c="90970">Graph</st>` <st c="90975">实例(负责仓库层实现的对象)为我们的</st> *<st c="90933">导师查找器</st>* <st c="90945">应用程序打开一个连接。</st>

        <st c="91045">实现仓库</st>

        `<st c="91333">Graph</st>` <st c="91338">实例具有几个实用方法来推导模块的构建块,即</st> `<st c="91428">SubGraph</st>`<st c="91436">,</st> `<st c="91438">Node</st>`<st c="91442">,</st> `<st c="91444">NodeMatcher</st>`<st c="91455">, 和</st> `<st c="91461">Relationship</st>`<st c="91473">。在这里,</st> `<st c="91481">StudentNodeRepository</st>` <st c="91502">展示了如何使用 py2neo 的 API 类和方法来管理</st> `<st c="91573">学生节点</st>`:</st>
<st c="91587">from main import graph</st>
<st c="91610">from py2neo import Node, NodeMatcher, Subgraph, Transaction</st>
<st c="91670">from py2neo.cypher import Cursor</st> from typing import Any, Dict
class StudentNodeRepository:
    def __init__(self):
        pass
    def insert_student_node(self, details:Dict[str, Any]):
        try: <st c="91847">tx:Transaction = graph.begin()</st><st c="91877">node_trainer = Node("Tutor", **details)</st><st c="91917">graph.create(node_trainer)</st><st c="91944">graph.commit(tx)</st> return True
        except Exception as e:
            print(e)
        return False
        <st c="92018">`<st c="92023">insert_student_node()</st>` <st c="92044">方法创建一个</st> `<st c="92062">Student</st>` <st c="92069">节点并将它的详细信息存储在图数据库中。</st> <st c="92121">节点是 Neo4J 中的基本数据单元;它可以是一个独立的节点,也可以通过</st> `<st c="92236">关系</st>` <st c="92251">与其他节点相连。</st>

        <st c="92251">使用</st> `<st c="92298">py2neo</st>` <st c="92304">库创建节点有两种方式:</st>

            +   <st c="92313">使用 Cypher 的</st> *<st c="92344">CREATE</st>* <st c="92350">事务通过 Graph 的</st> `<st c="92377">query()</st>` <st c="92384">或</st> `<st c="92388">run()</st>` <st c="92393">方法运行查询。</st>

            +   <st c="92402">使用</st> `<st c="92418">Node</st>` <st c="92422">对象通过</st> `<st c="92440">Graph</st>` <st c="92445">对象的</st> `<st c="92455">create()</st>` <st c="92463">方法持久化。</st>

        <st c="92471">创建节点需要事务管理,因此我们必须启动一个</st> `<st c="92540">事务</st>` <st c="92551">上下文来提交所有数据操作。</st> <st c="92608">在此处,</st> `<st c="92614">insert_student_node()</st>` <st c="92635">创建一个</st> `<st c="92646">事务</st>` <st c="92657">对象,为</st> `<st c="92733">图</st>` <st c="92738">对象的</st> `<st c="92748">commit()</st>` <st c="92756">方法</st> <st c="92764">创建一个逻辑上下文,以便提交:</st>
 def update_student_node(self, details:Dict[str, Any]):
        try: <st c="92835">tx = graph.begin()</st><st c="92853">matcher = NodeMatcher(graph)</st><st c="92882">student_node:Node  = matcher.match('Student',</st> <st c="92927">student_id=details['student_id']).first()</st> if not student_node == None:
                del details['student_id'] <st c="93025">student_node.update(**details)</st><st c="93055">graph.push(student_node)</st><st c="93080">graph.commit(tx)</st> return True
            else:
                return False
        except Exception as e:
            print(e)
        return False
        *<st c="93173">节点管理器</st>* <st c="93185">可以根据键值对中的条件定位特定的节点</st> <st c="93212">。</st> <st c="93252">在此处,</st> `<st c="93258">update_student_node()</st>` <st c="93279">使用</st> `<st c="93289">match()</st>` <st c="93296">方法从</st> `<st c="93309">节点管理器</st>` <st c="93320">中筛选出一个具有特定</st> `<st c="93337">节点</st>` <st c="93341">对象和</st> `<st c="93367">student_id</st>` <st c="93377">值</st>。</st> <st c="93385">在检索到图节点后,如果有的话,你必须调用</st> `<st c="93451">节点</st>` <st c="93455">对象的</st> `<st c="93465">update()</st>` <st c="93473">方法,并使用新数据的</st> `<st c="93490">kwargs</st>` <st c="93496">值。</st> <st c="93520">要将更新的</st> `<st c="93541">节点</st>` <st c="93545">对象与其提交版本合并,调用</st> `<st c="93592">图</st>` <st c="93597">对象的</st> `<st c="93607">push()</st>` <st c="93613">方法并执行</st> <st c="93633">提交。</st>

        <st c="93642">另一种搜索和检索</st> `<st c="93685">节点</st>` <st c="93689">匹配的方法是通过</st> `<st c="93711">图</st>` <st c="93716">对象的</st> `<st c="93726">query()</st>` <st c="93733">方法。</st> <st c="93742">它可以执行</st> *<st c="93757">CREATE</st>* <st c="93763">和其他 Cipher 操作命令,因为它具有自动提交功能。</st> <st c="93840">但在大多数情况下,它应用于节点检索事务。</st> <st c="93905">在此处,</st> `<st c="93911">delete_student_node()</st>` <st c="93932">使用带有</st> `<st c="93966">MATCH</st>` <st c="93971">命令的</st> `<st c="93942">query()</st>` <st c="93949">方法检索要删除的特定节点:</st>
 def delete_student_node(self, student_id:str):
        try: <st c="94074">tx = graph.begin()</st><st c="94092">student_cur:Cursor = graph.query(f"MATCH</st> <st c="94133">(st:Student) WHERE st.student_id =</st> <st c="94168">'{student_id}' Return st")</st><st c="94195">student_sg:Subgraph = student_cur.to_subgraph()</st><st c="94243">graph.delete(student_sg)</st><st c="94268">graph.commit(tx)</st> return True
        except Exception as e:
            print(e)
        return False
        `<st c="94342">The</st>` `<st c="94347">Graph</st>` <st c="94352">对象上的</st> `<st c="94362">query()</st>` <st c="94369">方法返回</st> `<st c="94385">Cursor</st>`<st c="94391">,它是节点流的导航器。</st> `<st c="94436">Graph</st>` <st c="94440">对象有一个</st> `<st c="94459">delete()</st>` <st c="94467">方法,可以删除通过</st> `<st c="94514">query()</st>`<st c="94521">检索到的任何节点,但节点应该以</st> *<st c="94550">SubGraph</st>* <st c="94558">形式存在。</st> `<st c="94565">要删除检索到的节点,需要通过调用</st> `<st c="94661">to_subgraph()</st>` <st c="94674">方法将</st> `<st c="94608">Cursor</st>` <st c="94614">对象转换为</st> *<st c="94629">SubGraph</st>* <st c="94637">。</st> `<st c="94683">然后,调用</st> `<st c="94694">commit()</st>` <st c="94702">来处理整个</st> <st c="94723">删除事务。</st>

        `<st c="94742">在</st> `<st c="94763">py2neo</st>` <st c="94769">中检索节点可以利用</st> `<st c="94781">NodeManager</st>` <st c="94800">或</st> `<st c="94808">Graph</st>` <st c="94813">对象的</st> `<st c="94823">query()</st>` <st c="94830">方法。</st> `<st c="94839">在此,</st> `<st c="94845">get_student_node()</st>` <st c="94863">使用</st> `<st c="94927">NodeMatcher</st>`<st c="94938">通过学生 ID 过滤检索特定的</st> `<st c="94885">Student</st>` <st c="94892">节点,而</st> `<st c="94946">select_student_nodes()</st>` <st c="94968">使用</st> `<st c="94974">query()</st>` <st c="94981">检索</st> `<st c="95004">Student</st>` <st c="95011">节点列表:</st>
 def get_student_node(self, student_id:str): <st c="95063">matcher = NodeMatcher(graph)</st><st c="95091">student_node:Node  = matcher.match('Student',</st> <st c="95136">student_id=student_id).first()</st><st c="95167">record = dict(student_node)</st> return record
    def select_student_nodes(self): <st c="95242">student_cur:Cursor = graph.query(f"MATCH (st:Student)</st> <st c="95295">Return st")</st> records = student_cur.data()
        return records
        `<st c="95351">The</st>` `<st c="95356">dict()</st>` <st c="95362">函数将一个</st> `<st c="95383">Node</st>` <st c="95387">对象转换为字典,从而通过给定的</st> `<st c="95481">get_student_node()</st>`<st c="95499">函数使用</st> `<st c="95452">dict()</st>` <st c="95458">函数将</st> `<st c="95430">Student</st>` <st c="95437">节点包装起来。另一方面,</st> `<st c="95520">Cursor</st>` <st c="95526">有一个</st> `<st c="95533">data()</st>` <st c="95539">函数,可以将</st> `<st c="95575">Node</st>` <st c="95579">对象的流转换为字典元素的列表。</st> <st c="95624">因此,</st> `<st c="95628">select_student_nodes()</st>` <st c="95650">返回的</st> `<st c="95673">Student</st>` <st c="95680">节点流是一个</st> `<st c="95686">Student</st>` <st c="95700">记录的列表。</st>

        `<st c="95716">Summary</st>`

        <st c="95724">有许多 NoSQL 数据库可以存储 Flask 3.x 构建的大数据应用的非关系型数据。</st> <st c="95842">Flask 可以</st> `<st c="95852">PUT</st>`<st c="95855">,</st> `<st c="95857">GET</st>`<st c="95860">, 和</st> `<st c="95866">SCAN</st>` <st c="95870">数据在 HBase 中使用 HDFS,访问 Cassandra 数据库,执行</st> `<st c="95936">HGET</st>` <st c="95940">一个</st> `<st c="95944">HSET</st>` <st c="95948">与 Redis,在 Couchbase 和 MongoDB 中执行 CRUD 操作,并使用 Neo4J 管理节点。</st> <st c="96040">尽管一些支持模块(例如在</st> `<st c="96103">flask-mongoengine</st>`<st c="96120">中)有所变化,因为 Flask 内部模块(例如,移除</st> `<st c="96212">flask.json</st>`<st c="96222">)发生了变化,但 Flask 仍然可以适应其他 Python 模块扩展和解决方案来连接和管理其数据,例如使用与 FastAPI 兼容的</st> <st c="96370">Redis OM。</st>

        <st c="96379">总的来说,本章展示了 Flask 几乎与所有高效、流行和广泛使用的 NoSQL 数据库的兼容性。</st> <st c="96510">它也是一个适合构建许多企业和科学发展的大数据应用的 Python 框架,因为它支持许多</st> <st c="96659">NoSQL 存储。</st>

        <st c="96674">下一章将介绍如何使用 Flask 通过工作流实现任务管理</st> <st c="96742">。</st>



第九章:8

使用 Flask 构建工作流

工作流是一系列或一组重复的任务、活动或小流程,需要从头到尾的完整执行以满足特定的业务流程。每个任务相当于日常交易,如发送电子邮件、运行脚本或终端命令、数据转换和序列化、数据库事务以及其他高度计算操作。 这些任务可以是简单的顺序、并行和 复杂类型。

几种工具和平台可以提供最佳实践、规则和技术规范,以构建针对行业、企业和科学问题的流程。然而,大多数这些解决方案的核心语言是 Java 而不是 Python。 现在,本章的主要目标是证明 Python,尤其是 Flask 框架,可以模拟使用 业务流程建模符号 (BPMN)和 非 BPMN 工作流,使用流行的现代平台,如 Zeebe/Camunda Airflow 2.0 Temporal。此外,本章还将展示如何使用 Celery 任务 Flask 应用程序构建自定义工作流。

本章将涵盖以下主题,讨论使用 Flask 框架实现工作流活动时的不同机制和程序:This chapter will cover the following topics that will discuss the different mechanisms and procedures in implementing workflow activities with the Flask framework:

  • 使用 Celery 任务构建工作流

  • 使用 SpiffWorkflow 创建 BPMN 和非 BPMN 工作流

  • 使用 Zeebe/Camunda 平台构建服务任务

  • 使用 Airflow 2.x 编排 API 端点

  • 使用 Temporal.io 实现工作流

技术要求

本章旨在实现使用工作流实现其业务流程的“医生预约管理软件” Doctor’s Appointment Management Software 。它具有以下五个不同的 Flask 项目,展示了构建 Flask 应用程序的不同工作流解决方案:

  • <st c="1823">ch08-celery-redis</st>,该应用专注于使用 Celery 任务设计动态工作流。

  • <st c="1906">ch08-spiff-web</st>,该应用实现了使用 SpiffWorkflow 库的预约系统。

  • <st c="2017">ch08-temporal</st>,它使用 Temporal 平台构建 分布式架构。

  • <st c="2100">ch08-zeebe</st>,它利用 Zeebe/Camunda 平台进行 BPMN 工作流。

  • <st c="2174">ch08-airflow</st>,它集成了 Airflow 2.x 工作流引擎来管理 API 服务。

尽管不同的工作流解决方案,但每个这些项目都针对用户登录交易 、预约流程 医生互动 计费流程 释放交易以及 释放交易的实际和最优流程性能。所有数据库事务都是关系型的,并使用 PostgreSQL 作为其数据库。 另一方面,所有这些项目都可在 以下链接 https://github.com/PacktPublishing/Mastering-Flask-Web-Development/tree/main/ch08找到。

使用 Celery 任务构建工作流

利用 Celery 作为任务队列管理器,并 Redis 作为其代理,这是我们 第五章 内容的一部分。 该章节明确讨论了所有设置和安装以构建 Flask-Celery-Redis 集成。 它还 阐述了 Celery 如何在 Flask 的请求-响应事务之外异步运行后台进程。 此外,本章还将展示 Celery 的另一个功能,可以解决业务 流程优化。

Celery 有一个机制来构建动态工作流,这种工作流类型在从工作流活动开始到结束的过程中,运行在某个模式定义和规则之外。 它的第一个要求是 将所有任务 封装在 签名中。

创建任务签名

在典型场景中,调用 Celery 任务需要直接调用其 <st c="3554">delay()</st> 方法以标准方式运行底层过程,或者 <st c="3619">apply_async()</st> 以异步运行。 但为了管理 Celery 任务以构建自定义动态工作流,单个任务必须首先调用 <st c="3754">signature()</st> <st c="3769">s()</st> 方法。 这允许将 Celery 任务调用传递给工作流操作,在任务成功执行后将其链接到另一个任务作为回调,并有助于管理其输入、参数和执行选项。 签名就像是一个准备传递给 Celery 工作流操作的任务的包装器。

以下 <st c="4130">add_login_task_wrapper()</st> 任务,例如,可以通过调用其 <st c="4229">signature()</st> <st c="4244">s()</st> 方法来包裹在一个签名中:

<st c="4255">@shared_task(ignore_result=False)</st> def add_login_task_wrapper(details): <st c="4327">async def add_login_task(details):</st> try:
            async with db_session() as sess:
              async with sess.begin():
                repo = LoginRepository(sess)
                details_dict = loads(details)
                print(details_dict)
                login = Login(**details_dict)
                result = await repo.insert_login(login)
                if result:
                    return str(True)
                else:
                    return str(False)
        except Exception as e:
            print(e)
            return str(False)
    return <st c="4774">signature()</st> method includes having a tuple argument, as in the following snippet:

add_login_task_wrapper.signature(s() equivalent with a typical parameter list containing the arguments, as in the following snippet:

 add_login_task_wrapper.s(<st c="5080">delay()</st>, <st c="5089">apply_async()</st>, or simply <st c="5114">()</st> right after the <st c="5133">signature()</st> call to run the task, if necessary. Now, let us explore Celery’s built-in signatures called *<st c="5237">primitives</st>*, used in building simple and complex workflows.
			<st c="5295">Utilizing Celery primitives</st>
			<st c="5323">Now, Celery provides the following core</st> <st c="5364">workflow operations called primitives, which are</st> <st c="5413">also signature objects themselves that take a list of task signatures to build dynamic</st> <st c="5500">workflow transactions:</st>

				*   `<st c="5522">chain()</st>` <st c="5530">– A Celery function that takes a series of signatures that are linked together to form a chain of callbacks executed from left</st> <st c="5658">to right.</st>
				*   `<st c="5667">group()</st>` <st c="5675">– A Celery operator that takes a list of signatures that will execute</st> <st c="5746">in parallel.</st>
				*   `<st c="5758">chord()</st>` <st c="5766">– A Celery operator that takes a list of signatures that will execute in parallel but with a callback that will consolidate</st> <st c="5891">their results.</st>

			<st c="5905">Let us first, in the next section, showcase Celery’s chained</st> <st c="5967">workflow execution.</st>
			<st c="5986">Implementing a sequential workflow</st>
			<st c="6021">Celery primitives are the components of building dynamic Celery workflows.</st> <st c="6097">The most commonly used primitive is the</st> *<st c="6137">chain</st>* <st c="6142">primitive, which can establish a pipeline of tasks with results passed from one task to another in a left-to-right manner.</st> <st c="6266">Since it is dynamic, it can follow any specific</st> <st c="6314">sequence based on the software specification, but it prefers smaller and straightforward tasks to avoid unwanted performance degradation.</st> *<st c="6452">Figure 8</st>**<st c="6460">.1</st>* <st c="6462">shows a workflow diagram that the</st> `<st c="6497">ch08-celery-redis</st>` <st c="6514">project implemented for an efficient user</st> <st c="6557">signup transaction:</st>
			![Figure 8.1 – Task signatures in a chain operation](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_08_001.jpg)

			<st c="6578">Figure 8.1 – Task signatures in a chain operation</st>
			<st c="6627">Similar to the</st> `<st c="6643">add_user_login_task_wrapper()</st>` <st c="6672">task,</st> `<st c="6679">add_user_profile_task_wrapper()</st>` <st c="6710">and</st> `<st c="6715">show_complete_profile_task_wrapper()</st>` <st c="6751">are asynchronous Celery tasks that can emit their respective signature to establish a dynamic workflow.</st> <st c="6856">The following endpoint function calls the signatures of these tasks in sequence using the</st> `<st c="6946">chain()</st>` <st c="6953">primitive:</st>

从模块.login.services.workflow_tasks 导入 add_user_login_task_wrapper, add_user_profile_task_wrapper, show_complete_login_task_wrapper

@login_bp.post('/login/user/add') async def add_user_workflow():

user_json = request.get_json()

user_str = dumps(user_json) <st c="7232">task = chain(add_user_login_task_wrapper.s(user_str),</st> <st c="7285">add_user_profile_task_wrapper.s(),</st> <st c="7320">show_complete_login_task_wrapper.s())()</st><st c="7360">result = task.get()</st> records = loads(result)

return jsonify(profile=records), 201

			<st c="7441">The presence of</st> `<st c="7458">()</st>` <st c="7460">at the end of the</st> `<st c="7479">chain()</st>` <st c="7486">primitive means the execution of the chained sequence since</st> `<st c="7547">chain()</st>` <st c="7554">is also a signature but a predefined one.</st> <st c="7597">Now, the purpose of the</st> `<st c="7621">add_user_workflow()</st>` <st c="7640">endpoint is to merge the</st> *<st c="7666">INSERT</st>* <st c="7672">transaction of the login credentials and the login profile details of the user instead of accessing two separate</st> <st c="7786">endpoints for the whole process.</st> <st c="7819">Also, it’s there to render the login credentials to the user after a successful workflow execution.</st> <st c="7919">So, all three tasks are in one execution frame with one JSON input of combined user profile and login details to the initial task,</st> `<st c="8050">add_user_login_task_wrapper()</st>`<st c="8079">. But what if tasks need arguments?</st> <st c="8115">Does the</st> `<st c="8124">signature()</st>` <st c="8135">method accept parameter(s) for its task?</st> <st c="8177">Let’s take a look in the</st> <st c="8202">next section.</st>
			<st c="8215">Passing inputs to signatures</st>
			<st c="8244">As mentioned earlier in this chapter, the</st> <st c="8286">required arguments for the Celery tasks can be passed to the</st> `<st c="8348">s()</st>` <st c="8351">or</st> `<st c="8355">signature()</st>` <st c="8366">function.</st> <st c="8377">In the given chained tasks, the</st> `<st c="8409">add_user_login_task_wrapper()</st>` <st c="8438">is the only task among the three that needs input from the API, as depicted in its</st> <st c="8522">code here:</st>

@shared_task(ignore_result=False) def add_user_login_task_wrapper(details): async def add_user_task(details):

    try:

        async with db_session() as sess:

        async with sess.begin():

            repo = LoginRepository(sess) <st c="8737">details_dict = loads(details)</st> … … … … … …

            login = Login(**user_dict)

            result = await repo.insert_login(login)

            if result:

                profile_details = dumps(details_dict)

                return profile_details

            else:

                return ""

    except Exception as e:

        print(e)

        return ""

return <st c="9014">details</st> parameter is the complete JSON details passed from the endpoint function to the <st c="9102">s()</st> method so that the task will retrieve only the *<st c="9153">login credentials</st>* for the *<st c="9179">INSERT</st>* login transaction. Now, the task will return the remaining details, the user profile information, as input to the next task in the sequence, <st c="9327">add_user_profile_task_wrapper()</st>. The following code shows the presence of a local parameter in the <st c="9426">add_user_profile_task_wrapper()</st> task that will receive the result of the previous task:
 @shared_task(ignore_result=False) <st c="9548">def add_user_profile_task_wrapper(details):</st> async def add_user_profile_task(<st c="9624">details</st>):
        try:
            async with db_session() as sess:
              async with sess.begin():
                … … … … … … <st c="9711">role = profile_dict['role']</st> result = False <st c="9754">if role == 0:</st><st c="9767">repo = AdminRepository(sess)</st> admin = Administrator(**profile_dict)
                    result = await repo.insert_admin(admin) <st c="9875">elif role == 1:</st><st c="9890">repo = DoctorRepository(sess)</st> doc = Doctor(**profile_dict)
                    result = await repo.insert_doctor(doc) <st c="9989">elif role == 2:</st><st c="10004">repo = PatientRepository(sess)</st> patient = Patient(**profile_dict)
                    result = await repo.insert_patient(patient)
                … … … … … …
                … … … … … …
    return <st c="10335">add_user_profile_task_wrapper()</st>, the <st c="10372">details</st> parameter pertains to the returned value of <st c="10424">add_user_login_task_wrapper()</st>. The first parameter will always receive the result of the preceding tasks. Now, the <st c="10539">add_user_profile_task_wrapper()</st> task will check the role to determine what table to insert the profile information in. Then, it will return the *<st c="10683">username</st>* as input to the final task, <st c="10720">show_complete_login_task_wrapper()</st>, which will render the user credentials.
			<st c="10795">The dynamic workflow must have strict exception handling from the inside of the tasks and from the outside of the Celery workflow execution to establish a continuous and blockage-free passing of results</st> <st c="10998">or input from the initial task to</st> <st c="11033">the end.</st>
			<st c="11041">On the other hand, running independent Celery tasks requires a different Celery primitive operation called</st> `<st c="11149">group()</st>`<st c="11156">. Let us now scrutinize some parallel tasks from</st> <st c="11205">our application.</st>
			<st c="11221">Running independent and parallel tasks</st>
			<st c="11260">The</st> `<st c="11265">group()</st>` <st c="11272">primitive can run tasks</st> <st c="11296">concurrently and even return consolidated results from functional tasks.</st> <st c="11370">Our sample grouped workflow, shown in</st> *<st c="11408">Figure 8</st>**<st c="11416">.2</st>*<st c="11418">, focuses only on void tasks that serialize a list of records to CSV files, so no consolidation of results</st> <st c="11525">is needed:</st>
			![Figure 8.2 – Task signatures in grouped workflow](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_08_002.jpg)

			<st c="11670">Figure 8.2 – Task signatures in grouped workflow</st>
			<st c="11718">The</st> `<st c="11723">group()</st>` <st c="11730">operation can accept varying Celery tasks with different arguments but prefers those that</st> *<st c="11821">read from and write to files</st>*<st c="11849">,</st> *<st c="11851">perform database transactions</st>*<st c="11880">,</st> *<st c="11882">extract resources from API endpoints</st>*<st c="11918">,</st> *<st c="11920">download files from external storages</st>*<st c="11957">, or</st> *<st c="11962">perform any I/O operations</st>*<st c="11988">. Our</st> `<st c="11994">create_reports()</st>` <st c="12010">endpoint function performs the grouped workflow presented in</st> *<st c="12072">Figure 8</st>**<st c="12080">.2</st>*<st c="12082">, which aims to back up the list of user administrators, patients, and doctors to their respective CSV files.</st> <st c="12192">The following is the code of the</st> <st c="12225">endpoint</st> <st c="12234">function:</st>

从 modules.admin.services.reports_tasks 导入 generate_csv_admin_task_wrapper, generate_csv_doctor_task_wrapper, generate_csv_patient_task_wrapper

@admin_bp.get('/admin/reports/create') async def create_reports():

admin_csv_filename = os.getcwd() + "/files/dams_admin.csv"

patient_csv_filename = os.getcwd() + "/files/dams_patient.csv"

doctor_csv_filename = os.getcwd() + "/files/dams_doc.csv" <st c="12641">workflow = group(</st><st c="12658">generate_csv_admin_task_wrapper.s(admin_csv_filename),</st> <st c="12713">generate_csv_doctor_task_wrapper.s(</st><st c="12749">doctor_csv_filename),</st> <st c="12771">generate_csv_patient_task_wrapper.s(</st><st c="12808">patient_csv_filename))()</st><st c="12833">workflow.get()</st> return jsonify(message="done backup"), 201

			<st c="12891">The</st> `<st c="12896">create_reports()</st>` <st c="12912">endpoint passes different filenames to the three tasks.</st> <st c="12969">The</st> `<st c="12973">generate_csv_admin_task_wrapper()</st>`<st c="13006">method will back up all administrator records to</st> `<st c="13056">dams_admin.csv</st>`<st c="13070">,</st> `<st c="13072">generate_csv_patient_task_wrapper()</st>` <st c="13107">will dump all patient records to</st> `<st c="13141">dams_patient.csv</st>`<st c="13157">, and</st> `<st c="13163">generate_csv_doctor_task_wrapper()</st>` <st c="13197">will save all doctor profiles to</st> `<st c="13231">dams_doctor.csv</st>`<st c="13246">. All three will concurrently execute after running the</st> `<st c="13302">group()</st>` <st c="13309">operation.</st>
			<st c="13320">But if the concern is to manage all the results of these concurrently running tasks, the</st> `<st c="13410">chord()</st>` <st c="13417">workflow operation, as shown in the next section, will be the best option for</st> <st c="13496">this scenario.</st>
			<st c="13510">Using callbacks to manage task results</st>
			<st c="13549">The</st> `<st c="13554">chord()</st>` <st c="13561">primitive works like the</st> `<st c="13587">group()</st>` <st c="13594">operation except for its callback task requirement, which will handle and</st> <st c="13668">manage all results of the independent tasks.</st> <st c="13714">The following API endpoint generates a report on a doctor’s appointments and</st> <st c="13791">laboratory requests:</st>

从 modules.admin.services.doctor_stats_tasks 导入 count_patients_doctor_task_wrapper, count_request_doctor_task_wrapper, create_doctor_stats_task_wrapper

@admin_bp.get('/admin/doc/stats') async def derive_doctor_stats():

docid = request.args.get("docid") <st c="14071">workflow =</st> <st c="14081">chord((count_patients_doctor_task_wrapper.s(docid), count_request_doctor_task_wrapper.s(docid)), create_doctor_stats_task_wrapper.s(docid))()</st><st c="14223">result = workflow.get()</st> return jsonify(message=result), 201

			<st c="14283">The</st> `<st c="14287">derive_doctor_stats()</st>` <st c="14309">method aims to execute the workflow shown in</st> *<st c="14355">Figure 8</st>**<st c="14363">.3</st>*<st c="14365">, which uses the</st> `<st c="14382">chord()</st>` <st c="14389">operation to run</st> `<st c="14407">count_patients_doctor_task_wrapper()</st>` <st c="14443">to determine the number of patients of a particular doctor and</st> `<st c="14507">count_request_doctor_task_wrapper()</st>` <st c="14542">to extract the total number of laboratory requests of the same doctor.</st> <st c="14614">The results of the tasks are stored in a list according to the order of their executions before passing it to the callback task,</st> `<st c="14743">create_doctor_stats_task_wrapper()</st>`<st c="14777">, for processing.</st> <st c="14795">Unlike in the</st> `<st c="14809">group()</st>` <st c="14816">primitive, the results are managed by a callback task before returning the final result to the</st> <st c="14912">API function:</st>
			![Figure 8.3 – Task signatures in chord() primitive](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_08_003.jpg)

			<st c="15032">Figure 8.3 – Task signatures in chord() primitive</st>
			<st c="15081">A sample output of the</st> `<st c="15105">create_doctor_stats_task_wrapper()</st>` <st c="15139">task will be like this: “</st>*<st c="15165">Doctor HSP-200 has 2 patients and 0</st>* *<st c="15202">lab requests.</st>*<st c="15215">”</st>
			<st c="15217">There are lots of ways to build complex dynamic workflows using combinations of</st> `<st c="15297">chain()</st>`<st c="15304">,</st> `<st c="15306">group()</st>`<st c="15313">, and</st> `<st c="15319">chord()</st>`<st c="15326">, which will implement the workflows that the Flask applications need to optimize some business processes.</st> <st c="15433">It is possible for a chained task to call the</st> `<st c="15479">group()</st>` <st c="15486">primitive from the inside to spawn and run a group of independent tasks.</st> <st c="15560">It is also feasible to use Celery’s</st> *<st c="15596">subtasks</st>* <st c="15605">to implement conditional task executions.</st> <st c="15647">There are also miscellaneous primitives such as</st> `<st c="15695">map()</st>`<st c="15700">,</st> `<st c="15702">starmap()</st>`<st c="15711">, and</st> `<st c="15717">chunks()</st>` <st c="15725">that can manage</st> <st c="15741">arguments of tasks in the workflow.</st> <st c="15778">A Celery workflow is flexible and open to any implementation using its primitives and signatures since it targets dynamic workflows.</st> <st c="15911">Celery workflows can read and execute workflows from XML files, such as BPMN workflows.</st> <st c="15999">However, there is a workflow solution that can work on both dynamic and BPMN</st> <st c="16076">workflows: SpiffWorkflow.</st>
			<st c="16101">Creating BPMN and non-BPMN workflows with SpiffWorkflow</st>
			**<st c="16157">SpiffWorkflow</st>** <st c="16171">is a flexible Python execution engine for workflow activities.</st> <st c="16235">Its latest installment focuses more on BPMN models, but it always</st> <st c="16300">has strong support classes to build and run non-BPMN</st> <st c="16353">workflows translated into Python and JSON.</st> <st c="16397">The library has a</st> *<st c="16415">BPMN interpreter</st>* <st c="16431">that can execute tasks indicated in</st> <st c="16467">BPMN diagrams created by BPMN modeling tools and</st> *<st c="16517">serializers</st>* <st c="16528">to run</st> <st c="16536">JSON-based workflows.</st>
			<st c="16557">To start SpiffWorkflow, we need to install some</st> <st c="16606">required dependencies.</st>
			<st c="16628">Setting up the development environment</st>
			<st c="16667">No broker or server is needed to run</st> <st c="16705">workflows with SpiffWorkflow.</st> <st c="16735">However, installing the main plugin using the</st> `<st c="16781">pip</st>` <st c="16784">command is</st> <st c="16796">a requirement:</st>

pip install spiffworkflow


			<st c="16836">Then, for serialization and parsing purposes, install the</st> `<st c="16895">lxml</st>` <st c="16899">dependency:</st>

pip install lxml


			<st c="16928">Since SpiffWorkflow uses the Celery client library for legacy support, install the</st> `<st c="17012">celery</st>` <st c="17018">module:</st>

pip install celery


			<st c="17045">Now, download and install a BPMN modeler tool that can provide BPMN diagrams supported by SpiffWorkflow.</st> <st c="17151">This chapter uses the</st> *<st c="17173">Camunda Modeler for Camunda 7 BPMN</st>* <st c="17207">version to generate BPMN diagrams, which we can download from</st> [<st c="17270">https://camunda.com/download/modeler/</st>](https://camunda.com/download/modeler/)<st c="17307">.</st> *<st c="17309">Figure 8</st>**<st c="17317">.4</st>* <st c="17319">provides a screenshot of the Camunda Modeler with a sample</st> <st c="17379">BPMN diagram:</st>
			![Figure 8.4 – Camunda Modeler with BPMN model for Camunda 7](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_08_004.jpg)

			<st c="17627">Figure 8.4 – Camunda Modeler with BPMN model for Camunda 7</st>
			<st c="17685">The version of SpiffWorkflow used by this chapter can only parse and execute the BPMN model for the Camunda 7 platform.</st> <st c="17806">Hopefully, its future releases can support Camunda 8 or higher versions of</st> <st c="17881">BPMN diagrams.</st>
			<st c="17895">Let us now create our workflow using the BPMN</st> <st c="17942">modeler tool.</st>
			<st c="17955">Creating a BPMN diagram</st>
			<st c="17979">BPMN is an open standard for business</st> <st c="18017">process diagrams.</st> <st c="18036">It is a graphical mechanism to visualize and simulate a systematic set of activities in one process flow that goals a successful result.</st> <st c="18173">A BPMN diagram has a set of graphical elements, called</st> *<st c="18228">flow objects</st>*<st c="18240">, composed of</st> *<st c="18254">activities</st>*<st c="18264">,</st> *<st c="18266">events</st>*<st c="18272">,</st> *<st c="18274">sequence flows</st>*<st c="18288">,</st> <st c="18290">and</st> *<st c="18294">gateways</st>*<st c="18302">.</st>
			<st c="18303">An activity represents work that needs execution inside a workflow process.</st> <st c="18380">A work can be simple and atomic, such as a</st> *<st c="18423">task</st>*<st c="18427">, or complex, such as a</st> *<st c="18451">sub-process</st>*<st c="18462">. When an activity is atomic and cannot break down further, despite the complexity of the process, then that is considered a task.</st> <st c="18593">A task in BPMN is denoted as a</st> *<st c="18624">rounded-corner rectangle shape</st>* <st c="18654">component.</st> <st c="18666">There are several types of tasks, but SpiffWorkflow only supports</st> <st c="18732">the following:</st>

				*   **<st c="18746">Manual task</st>** <st c="18758">– A non-automated task that a human can perform outside the context of</st> <st c="18830">the workflow.</st>
				*   **<st c="18843">Script task</st>** <st c="18855">– A task that runs a</st> <st c="18877">modeler-defined script.</st>
				*   **<st c="18900">User task</st>** <st c="18910">– A typical task that a human actor can carry out using some application-related operation, such as clicking</st> <st c="19020">a button.</st>

			<st c="19029">The tasks presented in the BPMN diagram of</st> *<st c="19073">Figure 8</st>**<st c="19081">.4</st>*<st c="19083">, namely</st> **<st c="19092">Doctor’s Specialization Form</st>**<st c="19120">,</st> **<st c="19122">List Specialized Doctors</st>**<st c="19146">,</st> **<st c="19148">Doctor’s Availability Form</st>**<st c="19174">, and</st> **<st c="19180">Patient Detail Form</st>**<st c="19199">, are</st> *<st c="19205">user tasks</st>*<st c="19215">. Usually, user tasks can represent actions such as web form handling, console-based transactions with user inputs, or transactions in applications involving editing and submitting form data.</st> <st c="19407">On the other hand, the</st> **<st c="19430">Evaluate Form Data</st>** <st c="19448">and</st> **<st c="19453">Finalize Schedule</st>** <st c="19470">tasks are considered</st> *<st c="19492">script tasks</st>*<st c="19504">.</st>
			<st c="19505">A</st> *<st c="19508">sequence flow</st>* <st c="19521">is a one-directional line connector between activities or tasks.</st> <st c="19587">The BPMN standard allows adding descriptions or labels to sequence flows to determine which paths to take from one activity</st> <st c="19711">to another.</st>
			<st c="19722">Now, the workflow will not work without</st> *<st c="19763">start</st>* <st c="19768">and</st> *<st c="19773">stop events</st>*<st c="19784">. An</st> *<st c="19789">event</st>* <st c="19794">is an occurrence along the workflow required to execute due to some triggers to produce some result.</st> <st c="19896">The start event, represented by a</st> *<st c="19930">small and open circle with a thin-lined boundary</st>*<st c="19978">, triggers the start of the workflow.</st> <st c="20016">The stop event, defined by a</st> *<st c="20045">small, open circle with a single thick-lined boundary</st>*<st c="20098">, ends the workflow activities.</st> <st c="20130">Other than these two, there are</st> *<st c="20162">cancel</st>*<st c="20168">,</st> *<st c="20170">signal</st>*<st c="20176">,</st> *<st c="20178">error</st>*<st c="20183">,</st> *<st c="20185">message</st>*<st c="20192">,</st> *<st c="20194">timer</st>*<st c="20199">, and</st> *<st c="20205">escalation</st>* <st c="20215">events supported by SpiffWorkflow, and all these are represented</st> <st c="20281">as circles.</st>
			<st c="20292">The</st> *<st c="20297">diamond-shaped component</st>* <st c="20321">in</st> *<st c="20325">Figure 8</st>**<st c="20333">.4</st>* <st c="20335">is a</st> *<st c="20341">gateway</st>* <st c="20348">component.</st> <st c="20360">It diverges or converges its incoming or outgoing process flows.</st> <st c="20425">It can control multiple incoming and multiple outgoing process flows.</st> <st c="20495">SpiffWorkflow supports the following types</st> <st c="20538">of gateways:</st>

				*   **<st c="20550">Exclusive gateway</st>** <st c="20568">– Caters to multiple incoming flows and will emit only one output flow based on</st> <st c="20649">some evaluation.</st>
				*   **<st c="20665">Parallel gateway</st>** <st c="20682">– Emits an independent</st> <st c="20706">process flow that will execute tasks without order but will wait for all the tasks</st> <st c="20789">to finish.</st>
				*   **<st c="20799">Event gateway</st>** <st c="20813">– Emits an outgoing flow based on some events from an</st> <st c="20868">outside source.</st>
				*   **<st c="20883">Inclusive gateway</st>** <st c="20901">– Caters to multiple incoming flows and can emit more than one output flow based on some</st> <st c="20991">complex evaluation.</st>

			<st c="21010">The gateway in</st> *<st c="21026">Figure 8</st>**<st c="21034">.4</st>* <st c="21036">is an example of an exclusive gateway because it will allow</st> **<st c="21097">the Finalize Schedule</st>** <st c="21118">task execution to proceed if, and only if, the form data is complete.</st> <st c="21189">Otherwise, it will redirect the sequence flow to the</st> **<st c="21242">Doctor’s Specialization Form</st>** <st c="21270">web form task again for</st> <st c="21295">data re-entry.</st>
			<st c="21309">Now, let us start the showcase on how SpiffWorkflow can interpret a BPMN diagram for</st> **<st c="21395">business process</st>** **<st c="21412">management</st>** <st c="21422">(</st>**<st c="21424">BPM</st>**<st c="21427">).</st>
			<st c="21430">Implementing the BPMN workflow</st>
			<st c="21461">SpiffWorkflow can translate mainly the</st> *<st c="21501">user</st>*<st c="21505">,</st> *<st c="21507">manual</st>*<st c="21513">, and</st> *<st c="21519">script</st>* <st c="21525">tasks of a BPMN diagram.</st> <st c="21551">So, it can best</st> <st c="21567">handle business process optimization involving sophisticated web flows in a</st> <st c="21643">web application.</st>
			<st c="21659">Since there is nothing to configure in the</st> `<st c="21703">create_app()</st>` <st c="21715">factory or</st> `<st c="21727">main.py</st>` <st c="21734">module for SpiffWorkflow, the next step after dependency module installations and the BPMN diagram design is the view function implementation for the BPMN diagram simulation.</st> <st c="21910">The view functions must initiate and execute SpiffWorkflow tasks to run the entire</st> <st c="21993">BPMN workflow.</st>
			<st c="22007">The first support class to call in the module script is</st> `<st c="22064">CamundaParser</st>`<st c="22077">, a support class found in the</st> `<st c="22108">SpiffWorkflow.camunda.parser.CamundaParser</st>` <st c="22150">module of SpiffWorkflow.</st> <st c="22176">The</st> `<st c="22180">CamundaParser</st>` <st c="22193">class will parse the BPMN tags of the BPMN file based on the Camunda 7 standards.</st> <st c="22276">The BPMN file is an XML document with tags corresponding to the</st> *<st c="22340">flow objects</st>* <st c="22352">of the workflow.</st> <st c="22370">Now, the</st> `<st c="22379">CamundaParser</st>` <st c="22392">class will need the name or ID of the BPMN definition to load the document and verify if the XML schema of the BPMN document is</st> <st c="22521">well formed and valid.</st> <st c="22544">The following is the first portion of the</st> `<st c="22586">/view/appointment.py</st>` <st c="22606">module of the</st> `<st c="22621">doctor</st>` <st c="22627">Blueprint module that instantiates the</st> `<st c="22667">CamundaParser</st>` <st c="22680">class that will load our</st> `<st c="22706">dams_appointment.bpmn</st>` <st c="22727">file, the workflow design depicted in the BPMN workflow diagram of</st> *<st c="22795">Figure 8</st>**<st c="22803">.4</st>*<st c="22805">:</st>

从 SpiffWorkflow.bpmn.workflow 导入 BpmnWorkflow

从 SpiffWorkflow.camunda.parser.CamundaParser 导入 CamundaParser

从 SpiffWorkflow.bpmn.specs.defaults 导入 ScriptTask

从 SpiffWorkflow.camunda.specs.user_task 导入 UserTask

从 SpiffWorkflow.task 导入 Task, TaskState

从 SpiffWorkflow.util.deep_merge 导入 DeepMerge parser = CamundaParser()

filepath = os.path.join("bpmn/dams_appointment.bpmn")

parser.add_bpmn_file(filepath)

add_bpmn_file() 函数的 API 将加载 BPMN 文件,而 get_spec() 函数将解析从进程定义 ID 调用开始的文档。现在,图 8**.5 展示了带有进程定义 ID 的 BPMN 文件快照:

        ![图 8.5 – 包含流程定义 ID 的 BPMN 文件快照](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_08_005.jpg)

        <st c="24570">图 8.5 – 包含流程定义 ID 的 BPMN 文件快照</st>

        <st c="24645">在激活 SpiffWorkflow 及其解析器后,下一步是通过视图函数构建网页流程。</st> <st c="24758">视图实现将是一系列页面重定向,这将收集 BPMN 工作流程的*<st c="24881">用户任务</st>* <st c="24891">所需的所有必要表单数据值。</st> <st c="24914">以下</st> `<st c="24928">choose_specialization()</st>` <st c="24951">视图将是第一个网页表单,因为它将模拟**<st c="25020">医生的专长</st>** **<st c="25044">表单</st>** <st c="25048">任务:</st>
<st c="25054">@doc_bp.route("/doctor/expertise",</st><st c="25089">methods = ["GET", "POST"])</st>
<st c="25116">async def choose_specialization():</st> if request.method == "GET":
      return render_template("doc_specialization_form.html")
    session['specialization'] = request.form['specialization'] <st c="25379">select_doctor()</st> to list all doctors with the specialization indicated by <st c="25452">choose_specialization()</st>. The following snippet presents the code for the <st c="25525">select_doctor()</st> view:

@doc_bp.route("/doctor/select", methods = ["GET", "POST"])

async def select_doctor():

if request.method == "GET":

    return render_template("doc_doctors_form.html")

session['docid'] = request.form['docid']

return redirect(url_for("doc_bp.reserve_schedule") )

			<st c="25802">After the</st> `<st c="25813">select_doctor()</st>` <st c="25828">view, the user will choose a date and time for the appointment through the</st> `<st c="25904">reserve_schedule()</st>` <st c="25922">view.</st> <st c="25929">The last view of the web flow is</st> `<st c="25962">provide_patient_details()</st>`<st c="25987">, which</st> <st c="25994">will ask for the patient details needed for the diagnosis and payment.</st> <st c="26066">The following code presents the implementation of the</st> `<st c="26120">reserve_schedule()</st>` <st c="26138">view:</st>

@doc_bp.route("/doctor/schedule",methods = ["GET", "POST"])

async def reserve_schedule(): if request.method == "GET": return render_template("doc_schedule_form.html"), 201 session['appt_date'] = request.form['appt_date']

session['appt_time'] = request.form['appt_time'] <st c="26505">provide_patient_details()</st>, 将触发工作流程执行,除了其提取预约安排所需的患者信息并与其他之前的视图中的其他详情合并的目标之外。以下是为`<st c="26761">provide_patient_details()</st>`视图的代码:
<st c="26792">from SpiffWorkflow.bpmn.workflow import BpmnWorkflow</st>
<st c="26845">from SpiffWorkflow.camunda.parser.CamundaParser import CamundaParser</st>
<st c="26914">from SpiffWorkflow.bpmn.specs.bpmn_task_spec import TaskSpec</st>
<st c="26975">from SpiffWorkflow.camunda.specs.user_task import UserTask</st>
<st c="27034">from SpiffWorkflow.task import Task, TaskState</st>
<st c="27081">@doc_bp.route("/doctor/patient", methods = ["GET", "POST"])</st>
<st c="27141">async def provide_patient_details():</st> if request.method == "GET": <st c="27207">return render_template("doc_patient_form.html"), 201</st><st c="27259">form_data = dict()</st> form_data['specialization'] = <st c="27309">session['specialization']</st> form_data['docid'] = <st c="27356">session['docid']</st> form_data['appt_date'] = <st c="27398">session['appt_date']</st> form_data['appt_time'] = <st c="27444">session['appt_time']</st> form_data['ticketid'] = <st c="27489">request.form['ticketid']</st> form_data['patientid'] = <st c="27539">request.form['patientid']</st> form_data['priority_level'] = <st c="27595">request.form['priority_level']</st><st c="27625">workflow = BpmnWorkflow(spec)</st><st c="27655">workflow.do_engine_steps()</st> ready_tasks: List[Task] = <st c="27709">workflow.get_tasks(TaskState.READY)</st> while len(ready_tasks) > 0:
        for task in ready_tasks:
            if isinstance(<st c="27812">task.task_spec</st>, UserTask):
                upload_login_form_data(task, form_data)
            workflow.run_task_from_id(task_id=<st c="27914">task.id</st>)
         else:
            task_details:TaskSpec = <st c="27955">task.task_spec</st> print("Complete Task ", <st c="27994">task_details.name</st>) <st c="28014">workflow.do_engine_steps()</st> ready_tasks = <st c="28055">workflow.get_tasks(TaskState.READY)</st><st c="28090">dashboard_page = workflow.data['finalize_sched']</st> if dashboard_page:
      return render_template("doc_dashboard.html"), 201
    else:
      return redirect(url_for("doc_bp.choose_specialization"))
        *<st c="28271">会话处理</st>* <st c="28288">提供了</st> `<st c="28302">provide_patient_details()</st>` <st c="28327">视图,具有从之前的网页视图收集所有预约详情的能力。</st> <st c="28413">如给定代码所示,所有会话数据,包括</st> <st c="28471">其表单中的患者详情,都被放置在其</st> `<st c="28525">form_data</st>` <st c="28534">字典中。</st> <st c="28547">利用会话是一个解决方案,因为将 SpiffWorkflow 库所需的流程循环与网页流程融合是不可行的。</st> <st c="28696">最后一个重定向的页面必须使用</st> `<st c="28757">BpmnWorkflow</st>` <st c="28769">类</st> <st c="28777">启动工作流程。</st> <st c="28777">但</st> `<st c="28816">CamundaParser</st>` <st c="28829">和</st> `<st c="28834">BpmnWorkflow</st>` <st c="28846">API 类</st> <st c="28860">之间有什么区别?</st> <st c="28891">我们将在下一节回答这个问题。</st>

        <st c="28904">区分工作流程规范和实例</st>

        <st c="28965">SpiffWorkflow 中包含两种组件类别:</st> *<st c="29023">规范</st>* <st c="29036">和</st> *<st c="29041">实例</st>* <st c="29049">对象。</st> `<st c="29059">CamundaParser</st>`<st c="29072">通过</st> <st c="29082">其</st> `<st c="29086">get_spec()</st>` <st c="29096">方法,返回一个</st> `<st c="29115">WorkflowSpec</st>` <st c="29127">实例对象,这是一个定义 BPMN 工作流的规范或模型对象。</st> <st c="29209">另一方面,</st> `<st c="29228">BpmnWorkflow</st>` <st c="29240">创建一个</st> `<st c="29251">Workflow</st>` <st c="29259">实例对象,该对象跟踪并返回实际的工作流活动。</st> <st c="29330">然而,</st> `<st c="29339">BpmnWorkflow</st>` <st c="29351">在实例化之前需要将工作流规范对象作为其构造函数参数</st> <st c="29424">。</st>

        <st c="29445">工作流实例将提供从开始事件到停止事件的全部序列流以及相应的任务状态。</st> <st c="29582">所有状态,例如</st> `<st c="29606">READY</st>`<st c="29611">,</st> `<st c="29613">CANCELLED</st>`<st c="29622">,</st> `<st c="29624">COMPLETED</st>`<st c="29633">,和</st> `<st c="29639">FUTURE</st>`<st c="29645">,都在</st> `<st c="29668">TaskState</st>` <st c="29677">API 中指示,该 API 与在</st> `<st c="29721">Task</st>` <st c="29725">实例对象中找到的钩子方法相关联。</st> <st c="29743">但是,SpiffWorkflow 如何确定 BPMN 任务呢?</st> <st c="29793">我们将在下一节中看到。</st>

        <st c="29830">区分任务规范和实例</st>

        <st c="29884">与工作流一样,每个 SpiffWorkflow 任务都有一个名为</st> `<st c="29965">TaskSpec</st>`<st c="29973">的规范对象,它提供有关任务定义的名称和任务类型等详细信息,例如</st> *<st c="30010">任务定义的名称</st>* <st c="30037">和</st> *<st c="30042">任务类型</st>*<st c="30051">,如</st> `<st c="30061">UserTask</st>` <st c="30069">或</st> `<st c="30073">ScriptTask</st>`<st c="30083">。另一方面,任务实例对象被命名为</st> `<st c="30138">Task</st>`<st c="30142">。工作流实例</st> <st c="30165">对象提供</st> `<st c="30182">get_tasks()</st>` <st c="30193">重载,根据特定状态或</st> `<st c="30253">TaskSpec</st>` <st c="30261">实例返回所有任务。</st> <st c="30272">此外,它还有</st> `<st c="30289">get_task_from_id()</st>` <st c="30307">,根据</st> *<st c="30353">任务 ID</st>*<st c="30360">提取</st> `<st c="30323">Task</st>` <st c="30327">实例对象,</st> `<st c="30362">get_task_spec_from_name()</st>` <st c="30387">根据其指示的 BPMN 名称检索</st> `<st c="30404">TaskSpec</st>` <st c="30412">名称,以及</st> `<st c="30456">get_tasks_from_spec_name()</st>` <st c="30482">根据</st> `<st c="30516">TaskSpec</st>` <st c="30524">定义名称检索所有任务。</st>

        <st c="30541">为了遍历和跟踪每个</st> `<st c="30570">UserTask</st>`<st c="30578">、</st> `<st c="30580">ManualTask</st>`<st c="30590">或</st> `<st c="30595">Gateway</st>` <st c="30602">任务及其后续的</st> `<st c="30627">ScriptTask</st>` <st c="30637">任务(们)</st>,基于从</st> `<st c="30686">StartEvent</st>`<st c="30696">开始的 BPMN 图,调用工作流实例的</st> `<st c="30709">do_engine_steps()</st>` <st c="30726">方法。</st> <st c="30753">必须调用</st> `<st c="30774">do_engine_steps()</st>` <st c="30791">方法来跟踪工作流中的每个活动,包括事件和</st> `<st c="30861">ScriptTask</st>` <st c="30871">任务,直到达到</st> `<st c="30895">EndEvent</st>`<st c="30903">。因此,</st> `<st c="30911">provide_patient_details()</st>` <st c="30936">在</st> `<st c="30961">POST</st>` <st c="30965">事务中有一个</st> `<st c="30943">while</st>` <st c="30948">循环来遍历工作流并执行每个</st> `<st c="31021">Task</st>` <st c="31025">对象,使用工作流实例的</st> `<st c="31042">run_task_from_id()</st>` <st c="31060">方法。</st>

        <st c="31093">但是运行任务,特别是</st> `<st c="31126">UserTask</st>` <st c="31134">和</st> `<st c="31139">ScriptTask</st>`<st c="31149">,不仅涉及工作流活动的完成,还包括一些</st> <st c="31248">任务数据。</st>

        <st c="31258">将表单数据传递给 UserTask</st>

        `<st c="31288">UserTask</st>`<st c="31297">的表单字段是 BPMN 工作流数据的几个来源之一。</st> <st c="31366">Camunda 模型器允许 BPMN</st> <st c="31402">设计者为每个</st> `<st c="31445">UserTask</st>` <st c="31453">任务创建表单变量。</st> *<st c="31460">图 8</st>**<st c="31468">.6</st>* <st c="31470">显示了三个表单字段,即</st> `<st c="31507">patientid</st>`<st c="31516">、</st> `<st c="31518">ticketid</st>`<st c="31526">和</st> `<st c="31532">priority_level</st>`<st c="31546">,的</st> **<st c="31555">患者详细信息表单</st>** <st c="31574">任务以及 Camunda 模型器中添加表单变量的部分:</st>

        ![图 8.6 – 向 UserTask 添加表单字段](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_08_006.jpg)

        <st c="31777">图 8.6 – 向 UserTask 添加表单字段</st>

        `<st c="31820">自定义生成的表单中存在表单字段需要通过视图函数将这些表单变量传递数据。</st>` `<st c="31949">没有值的表单字段将产生异常,这可能会停止工作流` `<st c="32021">执行,最终破坏 Flask 应用程序。</st>` `<st c="32075">以下代码片段中的`<st c="32079">while</st>` `<st c="32084">循环调用` `<st c="32127">provide_patient_details()</st>` `<st c="32152">视图调用一个` `<st c="32167">upload_login_form_data()</st>` `<st c="32191">自定义方法,该方法将` `<st c="32235">form_data</st>` `<st c="32244">字典中的值分配给每个` `<st c="32264">用户任务</st>` `<st c="32272">表单变量:</st>`
<st c="32287">from SpiffWorkflow.util.deep_merge import DeepMerge</st>
<st c="32339">def upload_login_form_data(task: UserTask, form_data):</st> form = task.task_spec.form <st c="32422">data = {}</st> if task.data is None:
        task.data = {}
    for field in form.fields:
        if field.id == "specialization":
            process_data = form_data["specialization"]
        elif field.id == "docid":
            process_data = form_data["docid"]
        elif field.id == "date_scheduled":
            process_data = form_data["appt_date"]
        … … … … … … <st c="32716">update_data(data, field.id,  process_data)</st><st c="32757">DeepMerge.merge(task.data, data)</st>
<st c="32790">@doc_bp.route("/doctor/patient", methods = ["GET", "POST"])</st>
<st c="32850">async def provide_patient_details():</st> … … … … … …
    while len(ready_tasks) > 0:
        for task in ready_tasks:
            if isinstance(task.task_spec, UserTask): <st c="32993">upload_login_form_data(task, form_data)</st> else:
                task_details:TaskSpec = task.task_spec
                print("Complete Task ", task_details.name)
            workflow.run_task_from_id(task_id=task.id)
        … … … … … …
        return redirect(url_for("doc_bp.choose_specialization"))
        `<st c="33232">The</st>` `<st c="33237">upload_login_form_data()</st>` `<st c="33261">方法通过其` *<st c="33308">ID</st>* `<st c="33310">确定每个表单字段,并从`<st c="33340">form_data</st>` `<st c="33355">字典中提取其适当的`<st c="33364">值。</st>` `<st c="33377">然后,自定义方法,如下面的代码片段所示,将值分配给表单字段,并使用`<st c="33530">DeepMerge</st>` `<st c="33539">实用类`<st c="33554">将字段值对作为` *<st c="33506">工作流数据</st>` `<st c="33519">上传到 SpiffWorkflow:</st>`
 def update_data(dct, name, value):
    path = name.split('.')
    current = dct
    for component in path[:-1]:
        if component not in current:
            current[component] = {}
        current = current[component]
    current[path[-1]] = value
        技术上讲,`<st c="33779">update_data()</st>` `<st c="33793">创建一个字典对象,其中字段名称作为键,其对应的`<st c="33894">form_data</st>` `<st c="33903">值。</st>`

        但是关于`<st c="33925">ScriptTask</st>` `<st c="33935">?它也能有表单变量吗?` `<st c="33970">让我们在下一节中探讨这个问题。</st>`

        添加 ScriptTask 的输入变量

        `<st c="34046">ScriptTask</st>` `<st c="34057">也可以有输入变量,但没有表单字段。</st>` `<st c="34109">这些输入变量也需要从视图函数中获取值,因为这些是其表达式的必要部分。</st>` `<st c="34225">有时,`<st c="34236">ScriptTask</st>` `<st c="34246">不需要从视图中获取输入,因为它可以提取现有的工作流数据来构建其条件表达式。</st>` `<st c="34362">但肯定的是,它必须发出后续`<st c="34428">网关</st>` `<st c="34435">` `<st c="34437">ScriptTask</st>` `<st c="34447">` 或 `<st c="34452">用户任务</st>` `<st c="34460">任务需要执行的输出变量。</st>` *<st c="34499">图 8</st>** `<st c="34507">.7</st>` `<st c="34509">显示了`<st c="34553">proceed</st>` `<st c="34560">输出变量以及它是如何从工作流数据中提取和使用配置文件信息的:</st>`

        ![图 8.7 – 在 ScriptTask 中利用变量](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_08_007.jpg)

        图 8.7 – 在 ScriptTask 中利用变量

        <st c="34955">在运行所有任务并将所有值上传到工作流程中不同变量的后,工作流程的结果必须是决定视图函数结果的变量;在我们的案例中,是</st> `<st c="35169">provide_patient_details()</st>` <st c="35194">视图。</st> <st c="35201">现在让我们检索这些结果以确定我们的视图将渲染的响应类型。</st>

        <st c="35292">管理工作流程的结果</st>

        <st c="35328">通过 SpiffWorkflow,我们工作流程的目标是确定路由函数将渲染的视图页面。</st> <st c="35436">与此相关的是执行所需的后端事务,例如将</st> <st c="35529">预约计划保存到数据库中,向医生发送新创建的预约通知,以及生成必要的日程安排文档。</st> <st c="35699">工作流程生成数据将决定视图的结果过程。</st> <st c="35781">在我们的预约工作流程中,当生成的</st> `<st c="35829">finalize_sched</st>` <st c="35843">变量是</st> `<st c="35856">True</st>`<st c="35860">时,视图将重定向用户到医生的仪表板页面。</st> <st c="35926">否则,用户将看到数据收集过程的第一页。</st>

        <st c="36000">现在让我们探索 SpiffWorkflow 实现</st> <st c="36065">非 BPMN 工作流程的能力。</st>

        <st c="36084">实现非 BPMN 工作流程</st>

        <st c="36117">SpiffWorkflow 可以使用 JSON 或 Python 配置实现工作流程。</st> <st c="36190">在我们的</st> `<st c="36197">ch08-spiff-web</st>` <st c="36211">项目中,我们有一个</st> <st c="36232">以下 Python 类,它实现了支付</st> <st c="36297">流程工作流程的原型:</st>
<st c="36314">from SpiffWorkflow.specs.WorkflowSpec import WorkflowSpec</st>
<st c="36372">from SpiffWorkflow.specs.ExclusiveChoice import</st> <st c="36420">ExclusiveChoice</st>
<st c="36436">from SpiffWorkflow.specs.Simple import Simple</st>
<st c="36482">from SpiffWorkflow.operators import Equal, Attrib</st>
<st c="36532">class PaymentWorkflowSpec(WorkflowSpec):</st> def __init__(self):
        super().__init__() <st c="36613">patient_pay = Simple(wf_spec=self, name='dams_patient_pay')</st> patient_pay.ready_event.connect(  callback=tx_patient_pay)
        self.start.connect(taskspec=patient_pay) <st c="36772">payment_verify = ExclusiveChoice(wf_spec=self, name='payment_check')</st> patient_pay.connect(taskspec=payment_verify)
        patient_release = Simple(wf_spec=self, name='dams_patient_release')
        cond = Equal(Attrib(name='amount'), Attrib(name='charge')) <st c="37013">payment_verify.connect_if(condition=cond,</st> <st c="37054">task_spec=patient_release)</st> patient_release.completed_event.connect( callback=tx_patient_release)
        patient_hold = Simple(wf_spec=self, name='dams_patient_onhold')
        payment_verify.connect(task_spec=patient_hold) <st c="37328">WorkflowSpec</st> is responsible for the non-BPMN workflow implementation in Python format. The constructor of the <st c="37439">WorkflowSpec</st> sub-class creates generic, simple, and atomic tasks using the <st c="37514">Simple</st> API of the <st c="37532">SpiffWorkflow.specs.Simple</st> module. The task can have more than one input and any number of output task variables. There is also an <st c="37663">ExclusiveChoice</st> sub-class that works like a gateway for the workflow.
			<st c="37732">Moreover, each task has a</st> `<st c="37759">connect()</st>` <st c="37768">method to</st> <st c="37778">establish sequence flows.</st> <st c="37805">It also has event variables, such as</st> `<st c="37842">ready_event</st>`<st c="37853">,</st> `<st c="37855">cancelled_event</st>`<st c="37870">,</st> `<st c="37872">completed_event</st>`<st c="37887">, and</st> `<st c="37893">reached_event</st>`<st c="37906">, that run their respective callback method, such as our</st> `<st c="37963">tx_patient_pay()</st>`<st c="37979">,</st> `<st c="37981">tx_patient_release()</st>`<st c="38001">, and</st> `<st c="38007">tx_patient_onhold()</st>` <st c="38026">methods.</st> <st c="38036">Calling these event objects marks a transition from one task’s current state</st> <st c="38113">to another.</st>
			<st c="38124">The</st> `<st c="38129">Attrib</st>` <st c="38135">helper class recognizes a task variable and retrieves its data for comparison performed by internal API classes, such as</st> `<st c="38257">Equal</st>`<st c="38262">,</st> `<st c="38264">NotEqual</st>`<st c="38272">, and</st> `<st c="38278">LessThan</st>`<st c="38286">, of the</st> `<st c="38295">SpiffWorkflow.operators</st>` <st c="38318">module.</st>
			<st c="38326">Let us now run our</st> `<st c="38346">PaymentWorkflowSpec</st>` <st c="38365">workflow using a</st> <st c="38383">view function.</st>
			<st c="38397">Running a non-BPMN workflow</st>
			<st c="38425">Since this is not a Camunda-based workflow, running</st> <st c="38477">the workflow does not need a parser.</st> <st c="38515">Immediately wrap and instantiate the custom</st> `<st c="38559">WorkflowSpec</st>` <st c="38571">sub-class inside the</st> `<st c="38593">Workflow</st>` <st c="38601">class and call</st> `<st c="38617">get_tasks()</st>` <st c="38628">inside the view function to prepare the non-BPMN workflow for the task traversal and executions.</st> <st c="38726">But the following</st> `<st c="38744">start_payment_form()</st>` <st c="38764">function opts for individual access of tasks using the workflow instance’s</st> `<st c="38840">get_tasks_from_spec_name()</st>` <st c="38866">function instead of using a</st> `<st c="38895">while</st>` <st c="38900">loop for</st> <st c="38910">task traversal:</st>

@payment_bp.route("/payment/start", methods = ["GET", "POST"])

async def start_payment_form():

if request.method == "GET":

    return render_template("payment_form.html"), 201

… … … … … … <st c="39220">任务</st> 列表将启动工作流程:
 start_tasks: list[Task] = workflow_instance.get_tasks_from_spec_name( name='Start')
    for task in start_tasks:
        if task.state == TaskState.READY:
            workflow_instance.run_task_from_id( task_id=task.id)
        <st c="39450">此</st> `<st c="39456">任务</st>` <st c="39460">列表将加载所有支付数据到工作流程中,并执行</st> `<st c="39525">tx_patient_pay()</st>` <st c="39541">回调方法</st> <st c="39557">以处理</st> <st c="39569">支付交易:</st>
 patient_pay_task: list[Task] = workflow_instance.get_tasks_from_spec_name( name='dams_patient_pay')
    for task in patient_pay_task:
        if task.state == TaskState.READY:
            task.set_data(ticketid=ticketid, patientid=patientid, charge=charge, amount=amount, discount=discount, status=status, date_released=date_released)
            workflow_instance.run_task_from_id(  task_id=task.id)
        <st c="39954">此部分工作流程将执行</st> `<st c="39998">ExclusiveChoice</st>` <st c="40013">事件,以比较患者支付的金额与患者的</st> <st c="40062">总费用:</st>
 payment_check_task: list[Task] = workflow_instance.get_tasks_from_spec_name( name='payment_check')
    for task in payment_check_task:
        if task.state == TaskState.READY:
            workflow_instance.run_task_from_id( task_id=task.id)
        <st c="40324">如果患者全额支付了费用,以下任务将执行</st> `<st c="40401">tx_patient_release()</st>` <st c="40421">回调方法以清除并发布给患者</st> `<st c="40482">的释放通知:</st>
 for_releasing = False
    patient_release_task: list[Task] = workflow_instance.get_tasks_from_spec_name( name='dams_patient_release')
    for task in patient_release_task:
        if task.state == TaskState.READY:
            for_releasing = True
            workflow_instance.run_task_from_id( task_id=task.id)
        <st c="40766">如果患者已部分支付费用,以下任务将执行</st> `<st c="40851">tx_patient_onhold()</st>` <st c="40870">回调方法:</st>
 patient_onhold_task: list[Task] = workflow_instance.get_tasks_from_spec_name( name='dams_patient_onhold')
    for task in patient_onhold_task:
        if task.state == TaskState.READY:
            workflow_instance.run_task_from_id( task_id=task.id)
    if for_releasing == True:
        return redirect(url_for('payment_bp.release_patient'), code=307)
    else:
       return redirect(url_for('payment_bp.hold_patient'), code=307)
        <st c="41272">工作流程的结果将决定视图将用户重定向到哪个页面,是</st> *<st c="41373">释放</st>* <st c="41382">还是</st> *<st c="41386">挂起</st>* <st c="41393">页面。</st>

        <st c="41399">现在,SpiffWorkflow 将减少构建工作流程的编码工作量,因为它已经定义了支持 BPMN 和非 BPMN 工作流程实现的 API 类。</st> <st c="41570">但如果需要通过 SpiffWorkflow 几乎无法处理的 API 端点触发</st> <st c="41604">工作流程呢?</st>

        <st c="41674">下一主题将重点介绍使用 Camunda 平台使用的 BPMN 工作流程引擎,通过 API 端点运行任务。</st>

        <st c="41803">使用 Zeebe/Camunda 平台构建服务任务</st>

        **<st c="41859">Camunda</st>** <st c="41867">是一个流行的轻量级工作流程和决策自动化引擎,内置强大的工具,如</st> *<st c="41975">Camunda Modeler</st>*<st c="41990">,</st> *<st c="41992">Cawemo</st>*<st c="41998">,以及</st> *<st c="42008">Zeebe</st>* <st c="42013">代理。</st> <st c="42022">但本章不是关于 Camunda</st> <st c="42059">,而是关于使用 Camunda 的</st> *<st c="42086">Zeebe 服务器</st>* <st c="42098">来部署、运行和执行由 Flask 框架构建的工作流程任务。</st> <st c="42155">目标是创建一个 Flask 客户端应用程序,该应用程序将使用 Zeebe 工作流程引擎部署和运行由 Camunda Modeler 设计的 BPMN 工作流程。</st> <st c="42172">目标是创建一个 Flask 客户端应用程序,该应用程序将使用 Zeebe 工作流程引擎部署和运行由 Camunda Modeler 设计的 BPMN 工作流程。</st>

        <st c="42325">让我们从整合 Flask 与 Zeebe 服务器所需的设置和配置开始。</st>

        <st c="42421">设置 Zeebe 服务器</st>

        <st c="42449">运行 Zeebe 服务器的最简单方法是使用 Docker 运行其</st> `<st c="42518">camunda/zeebe</st>` <st c="42531">镜像。</st> <st c="42539">因此,在下载和安装</st> *<st c="42566">Docker 订阅服务协议</st>* <st c="42603">之前,请先阅读更新后的内容</st> <st c="42638">Docker Desktop,可在</st> <st c="42664">以下链接</st> [<st c="42669">https://docs.docker.com/desktop/install/windows-install/</st>](https://docs.docker.com/desktop/install/windows-install/)<st c="42725">找到。</st>

        <st c="42726">安装完成后,启动 Docker 引擎,打开终端,并运行以下</st> <st c="42815">Docker 命令:</st>
 docker run --name zeebe --rm -p 26500-26502:26500-26502 -d --network=ch08-network camunda/zeebe:latest
        <st c="42933">一个</st> *<st c="42936">Docker 网络</st>*<st c="42950">,就像我们的</st> `<st c="42964">ch08-network</st>`<st c="42976">,需要暴露端口到开发平台。</st> <st c="43037">Zeebe 的端口</st> `<st c="43050">26500</st>` <st c="43055">是 Flask 客户端应用程序将通信到服务器网关 API 的地方。</st> <st c="43140">使用 Zeebe 后,使用</st> `<st c="43167">docker stop</st>` <st c="43178">命令与</st> *<st c="43192">Zeebe 的容器 ID</st>* <st c="43212">一起关闭</st> <st c="43226">代理。</st>

        <st c="43237">现在,下一步是为应用程序安装合适的 Python Zeebe 客户端。</st>

        <st c="43324">安装 pyzeebe 库</st>

        <st c="43355">许多有效且流行的 Zeebe 客户端库</st> <st c="43409">是基于 Java 的。</st> <st c="43425">然而,</st> `<st c="43434">pyzeebe</st>` <st c="43441">是少数几个简单、易于使用、轻量级且在建立与 Zeebe 服务器连接方面有效的 Python 外部模块之一。</st> <st c="43570">它是一个基于</st> *<st c="43599">gRPC</st>*<st c="43603">的 Zeebe 客户端库,通常设计用于管理涉及</st> <st c="43689">RESTful 服务</st>的工作流程。</st>

        <st c="43706">重要提示</st>

        <st c="43721">gRPC 是一个灵活且高性能的 RPC 框架,可以在任何环境中运行,并轻松连接到任何集群,支持访问认证、API 健康检查、负载均衡和开源跟踪。</st> <st c="43938">所有 Zeebe 客户端库都使用 gRPC 与</st> <st c="43994">服务器通信。</st>

        <st c="44005">现在让我们使用</st> `<st c="44029">pip</st>` <st c="44036">命令安装</st> `<st c="44029">pyzeebe</st>` <st c="44036">库:</st>
 pip install pyzeebe
        <st c="44087">安装和</st> <st c="44115">设置完成后,是时候使用</st> <st c="44177">Camunda Modeler</st>创建 BPMN 工作流程图了。</st>

        <st c="44193">为 pyzeebe 创建 BPMN 图</st>

        <st c="44229">The</st> `<st c="44234">pyzeebe</st>` <st c="44241">模块可以</st> <st c="44252">加载和解析</st> *<st c="44287">Camunda 版本 8.0</st>*<st c="44306">. 由于它是一个小型库,它只能读取和执行</st> `<st c="44366">ServiceTask</st>` <st c="44377">任务。</st> *<st c="44385">图 8.8</st>**<st c="44393">.8</st>* <st c="44395">显示了一个包含两个</st> `<st c="44426">ServiceTask</st>` <st c="44437">任务的 BPMN 图:</st> **<st c="44449">获取诊断</st>** <st c="44464">任务,该任务</st> <st c="44476">检索所有患者的诊断,以及</st> **<st c="44520">获取分析</st>** <st c="44532">任务,该任务将医生的决议或处方返回给</st> <st c="44598">诊断:</st>

        ![图 8.8 – 包含两个 ServiceTask 任务的 BPMN 图](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_08_008.jpg)

        <st c="44706">图 8.8 – 包含两个 ServiceTask 任务的 BPMN 图</st>

        `<st c="44760">下一步是使用</st>` `<st c="44828">pyzeebe</st>` `<st c="44835">客户端库加载和运行最终的 BPMN 文档。</st>` `<st c="44852">没有</st>` `<st c="44930">pyzeebe</st>` `<st c="44937">worker</st>` `<st c="44944">和</st>` `<st c="44949">client</st>` `<st c="44955">,无法运行 BPMN 图中的工作流活动。</st>` `<st c="44995">但 worker 的实现必须</st>` `<st c="44995">首先进行。</st>`

        `<st c="45006">创建 pyzeebe worker</st>`

        `<st c="45032">A</st>` `<st c="45035">pyzeebe</st>` `<st c="45042">worker 或一个</st>` `<st c="45055">ZeebeWorker</st>` `<st c="45066">worker 是一个</st>` `<st c="45078">典型的 Zeebe worker,它处理所有</st>` `<st c="45117">ServiceTask</st>` `<st c="45128">任务。</st>` `<st c="45136">它使用</st>` `<st c="45183">asyncio</st>` `<st c="45190">异步地在后台运行。</st>` `<st c="45192">pyzeebe</st>` `<st c="45199">作为一个异步库,更喜欢具有</st>` `<st c="45239">Flask[async]</st>` `<st c="45251">平台和</st>` `<st c="45266">asyncio</st>` `<st c="45273">工具的</st>` `<st c="45285">。但它需要</st>` `<st c="45301">grpc.aio.Channel</st>` `<st c="45317">作为构造函数</st>` `<st c="45335">参数</st>` `<st c="45345">在实例化之前。</st>`

        `<st c="45366">该库提供了三种创建所需通道的方法,即</st>` `<st c="45439">create_insecure_channel()</st>` `<st c="45464">,</st>` `<st c="45466">create_secure_channel()</st>` `<st c="45489">,和</st>` `<st c="45495">create_camunda_cloud_channel()</st>` `<st c="45525">。所有三种都实例化了通道,但</st>` `<st c="45568">create_insecure_channel()</st>` `<st c="45593">忽略了 TLS 协议,而</st>` `<st c="45627">create_camunda_cloud_channel()</st>` `<st c="45657">考虑了与 Camunda 云的连接。</st>` `<st c="45705">我们的</st>` `<st c="45709">ch08-zeebe</st>` `<st c="45719">应用程序使用不安全的通道来实例化</st>` `<st c="45773">ZeebeWorker</st>` `<st c="45784">worker,并最终管理我们 BPMN 文件中指示的</st>` `<st c="45818">ServiceTask</st>` `<st c="45829">任务。</st>` `<st c="45864">以下</st>` `<st c="45878">worker-tasks</st>` `<st c="45890">模块脚本显示了一个包含</st>` `<st c="45963">ZeebeWorker</st>` `<st c="45974">实例化和其任务</st>` `<st c="46003">或作业的独立 Python 应用程序:</st>`
<st c="46011">from pyzeebe import ZeebeWorker, create_insecure_channel</st> import asyncio
from modules.models.config import db_session, init_db
from modules.doctors.repository.diagnosis import DiagnosisRepository
print('starting the Zeebe worker...')
print('initialize database connectivity...')
init_db()
channel = create_insecure_channel() <st c="46479">ZeebeWorker</st> worker with its constructor parameters. The <st c="46535">initdb()</st> call is included in the module because our tasks will need CRUD transactions:

<st c="46621">@worker.task(task_type="select_diagnosis",</st> <st c="46664">**Zeebe.TASK_DEFAULT_PARAMS)</st>

<st c="46693">async def select_diagnosis(docid, patientid):</st> async with db_session() as sess:

    `async with sess.begin():`

        `try:`

        `repo = DiagnosisRepository(sess)`

        `records = await repo.select_diag_doc_patient(docid, patientid)`

        `diagnosis_rec = [rec.to_json() for rec in records]`

        `diagnosis_str = json.dumps(diagnosis_rec, default=json_date_serializer)`

        `return {"data": diagnosis_str}`

        `except Exception as e:`

        `print(e)`

        返回`{"data": json.dumps([])}`

			<st c="47117">The</st> `<st c="47122">select_diagnosis()</st>` <st c="47140">method is a</st> `<st c="47153">pyzeebe</st>` <st c="47160">worker decorated with the</st> `<st c="47187">@worker.task()</st>` <st c="47201">annotation.</st> <st c="47214">The</st> `<st c="47218">task_type</st>` <st c="47227">attribute of the</st> `<st c="47245">@worker.task()</st>` <st c="47259">annotation indicates its</st> `<st c="47285">ServiceTask</st>` <st c="47296">name in the</st> <st c="47308">BPMN model.</st> <st c="47321">The decorator can also include other attributes, such as</st> `<st c="47378">exception_handler</st>` <st c="47395">and</st> `<st c="47400">timeout_ms</st>`<st c="47410">. Now,</st> `<st c="47417">select_diagnosis()</st>` <st c="47435">looks for all patients’ diagnoses from the database with</st> `<st c="47493">docid</st>` <st c="47499">and</st> `<st c="47503">patientid</st>` <st c="47512">parameters as filters to the search.</st> <st c="47550">It returns a dictionary with a key named</st> `<st c="47591">data</st>` <st c="47595">handling</st> <st c="47605">the result:</st>

@worker.task(task_type="retrieve_analysis", **Zeebe.TASK_DEFAULT_PARAMS)

async def retrieve_analysis(records): try:

records_diagnosis = json.loads(records)

diagnosis_text = [dt['resolution'] for dt in records_diagnosis]

返回 {"result": diagnosis_text}

except Exception as e:

打印(e)

返回 {"result": []}


			<st c="47924">On the other hand, this</st> `<st c="47949">retrieve_analysis()</st>` <st c="47968">task takes</st> `<st c="47980">records</st>` <st c="47987">from</st> `<st c="47993">select_diagnosis()</st>` <st c="48011">in string form but is serialized back to the list form with</st> `<st c="48072">json.loads()</st>`<st c="48084">. This task will extract</st> <st c="48108">only all resolutions from the patients’ records</st> <st c="48156">and return them to the caller.</st> <st c="48188">The task returns a</st> <st c="48207">dictionary also.</st>
			<st c="48223">The</st> *<st c="48228">local parameter names</st>* <st c="48249">and the</st> *<st c="48258">dictionary keys</st>* <st c="48273">returned by the worker’s tasks must be</st> *<st c="48313">BPMN variable names</st>* <st c="48332">because the client will also fetch these local parameters to assign values and dictionary keys for the output extraction for the preceding</st> `<st c="48472">ServiceTask</st>` <st c="48483">task.</st>
			<st c="48489">Since our Flask client application uses its event loop, our worker must run on a separate event loop using</st> `<st c="48597">asyncio</st>` <st c="48604">to avoid exceptions.</st> <st c="48626">The following</st> `<st c="48640">worker_tasks.py</st>` <st c="48655">snippet shows how to run the worker on an</st> `<st c="48698">asyncio</st>` <st c="48705">environment:</st>

如果 name == "main": ZeebeWorker 实例有一个 work() 协程,它必须在后台异步运行,使用独立的事件,与 Flask 操作断开连接。始终使用 Python 命令运行模块,例如 python worker-tasks.py

        <st c="49055">现在让我们实现</st> `<st c="49081">pyzeebe</st>` <st c="49088">客户端。</st>

        <st c="49096">实现 pyzeebe 客户端</st>

        <st c="49128">Flask 应用需要</st> <st c="49159">实例化</st> `<st c="49176">ZeebeClient</st>` <st c="49187">类以连接到 Zeebe。</st> <st c="49215">与</st> `<st c="49227">ZeebeWorker</st>`<st c="49238">一样,它也需要在实例化之前将相同的</st> `<st c="49266">grpc.aio.Channel</st>` <st c="49282">参数作为构造函数参数。</st> <st c="49346">由于</st> `<st c="49352">ZeebeClient</st>` <st c="49363">的行为类似于</st> `<st c="49392">ZeebeWorker</st>`<st c="49403">,所有操作都必须在后台异步作为 Celery 任务运行。</st> <st c="49483">但是,与工作进程不同,</st> `<st c="49507">ZeebeClient</st>` <st c="49518">作为其 Celery 服务任务的一部分出现在每个 Blueprint</st> <st c="49546">模块中。</st> <st c="49590">以下是在 *<st c="49648">doctor</st>* <st c="49654">Blueprint 模块中实例化</st> `<st c="49690">ZeebeClient</st>` <st c="49701">并使用 Celery 任务</st> 的 `<st c="49611">diagnosis_tasks</st>` <st c="49626">模块脚本:</st>
 from celery import shared_task
import asyncio <st c="49771">from pyzeebe import ZeebeClient, create_insecure_channel</st> channel = create_insecure_channel(hostname="localhost", port=26500) <st c="49959">ZeebeClient</st> instance. The port to connect the Zeebe client is <st c="50021">26500</st>:

@shared_task(ignore_result=False)

def deploy_zeebe_wf(bpmn_file): async def zeebe_wf(bpmn_file):

    try: <st c="50130">await client.deploy_process(bpmn_file)</st> 返回 True

    except Exception as e:

        打印(e)

    返回 False <st c="50314">deploy_zeebe_wf()</st> 任务是在其他任何操作之前运行的第一个进程。调用此 API 端点的任务将加载、解析并将带有工作流的 BPMN 文件部署到 Zeebe 服务器,使用 <st c="50521">deploy_process()</st> 方法,这是 <st c="50548">ZeebeClient</st> 的异步方法。如果 BPMN 文件有模式问题、格式不正确或无效,任务将抛出异常:
<st c="50666">@shared_task(ignore_result=False)</st>
<st c="50700">def run_zeebe_task(docid, patientid):</st> async def zeebe_task(docid, patientid):
        try:
            process_instance_key, result = await <st c="50821">client.run_process_with_result(</st><st c="50852">bpmn_process_id</st>= "Process_Diagnostics", <st c="50894">variables</st>={"<st c="50907">docid</st>": docid, "<st c="50925">patientid</st>":patientid}, variables_to_fetch =["<st c="50972">result</st>"], timeout=10000)
            return result
        except Exception as e:
            print(e)
            return {} <st c="51147">ZeebeClient</st> has two asynchronous methods that can execute process definitions in the BPMN file, and these are <st c="51258">run_process()</st> and <st c="51276">run_process_with_result()</st>. Both methods pass values to the first task of the workflow, but only <st c="51372">run_process_with_result()</st> returns an output value. The given <st c="51433">run_zeebe_task()</st> method will execute the first <st c="51480">ServiceTask</st> task, the worker’s <st c="51511">select_diagnosis()</st> task, pass values to its <st c="51555">docid</st> and <st c="51565">patientid</st> parameters, and retrieve the dictionary output of the last <st c="51634">ServiceTask</st> task, <st c="51652">retrieve_analysis()</st>, indicated by the <st c="51690">result</st> key. A <st c="51704">ServiceTask</st> task’s parameters are considered BPMN variables that the BPMN file or the <st c="51790">ZeebeClient</st> operations can fetch at any time. Likewise, the key of the dictionary returned by <st c="51884">ServiceTask</st> becomes a BPMN variable, too. So, the <st c="51934">variables</st> parameter of the <st c="51961">run_process_with_result()</st> method fetches the local parameters of the first worker’s task, and its <st c="52059">variables_to_fetch</st> property retrieves the returned dictionary of any <st c="52128">ServiceTask</st> task indicated by the key name.
			<st c="52171">To enable the</st> `<st c="52186">ZeebeClient</st>` <st c="52197">operations, run Celery and the Redis broker.</st> <st c="52243">Let us now implement API endpoints that will simulate the</st> <st c="52301">diagnosis workflow.</st>
			<st c="52320">Building API endpoints</st>
			<st c="52343">The following API endpoint passes the</st> <st c="52381">filename of the BPMN file to the</st> `<st c="52415">pyzeebe</st>` <st c="52422">client by calling the</st> `<st c="52445">deploy_zeebe_wf()</st>` <st c="52462">Celery task:</st>

@doc_bp.get("/diagnosis/bpmn/deploy")

async def deploy_diagnosis_analysis_bpmn(): try:

    filepath = os.path.join(Zeebe.BPMN_DUMP_PATH, "<st c="52610">dams_diagnosis.bpmn</st>") <st c="52634">task = deploy_zeebe_wf.apply_async(args=[filepath])</st><st c="52685">result = task.get()</st> 返回 jsonify(data=result), 201

except Exception as e:

        打印(e)

return jsonify(data="error"), 500

			<st c="52804">Afterward, the following</st> `<st c="52830">extract_analysis_text()</st>` <st c="52853">endpoint can run the workflow by calling the</st> `<st c="52899">run_zeebe_task()</st>` <st c="52915">Celery task:</st>

<st c="52928">@doc_bp.post("/diagnosis/analysis/text")</st>

<st c="52969">async def extract_analysis_text():</st> try:

        data = request.get_json()

        docid = data['docid']

        patientid = int(data['patientid']) `<st c="53093">task = run_zeebe_task.apply_async(args=[docid,</st>` `<st c="53139">patientid])</st>` `<st c="53151">result = task.get()</st>` return jsonify(result), 201

    except Exception as e:

        print(e)

    return jsonify(data="error"), 500

			<st c="53265">The given endpoint will also pass the</st> `<st c="53304">docid</st>` <st c="53309">and</st> `<st c="53314">patientid</st>` <st c="53323">values to the</st> <st c="53338">client task.</st>
			<st c="53350">The</st> `<st c="53355">pyzeebe</st>` <st c="53362">library has many limitations, such as supporting</st> `<st c="53412">UserTask</st>` <st c="53420">and web flows and implementing workflows that</st> <st c="53466">call API endpoints for results.</st> <st c="53499">Although connecting our Flask application to the enterprise Camunda platform can address these problems with</st> `<st c="53608">pyzeebe</st>`<st c="53615">, it is a practical and clever approach to use the Airflow 2.x</st> <st c="53678">platform instead.</st>
			<st c="53695">Using Airflow 2.x in orchestrating API endpoints</st>
			**<st c="53744">Airflow 2.x</st>** <st c="53756">is an open source platform that provides workflow authorization, monitoring, scheduling, and maintenance with its easy-to-use UI dashboard.</st> <st c="53897">It can manage</st> **<st c="53911">extract, transform, load</st>** <st c="53935">(</st>**<st c="53937">ETL</st>**<st c="53940">) workflows</st> <st c="53953">and</st> <st c="53957">data analytics.</st>
			<st c="53972">Airflow uses Flask Blueprints internally and allows</st> <st c="54024">customization just by adding custom Blueprints in its Airflow directory.</st> <st c="54098">However, the main goal of this</st> <st c="54129">chapter is to use Airflow as an API orchestration tool to run sets of workflow activities that consume API services</st> <st c="54245">for resources.</st>
			<st c="54259">Let us begin with the installation of the Airflow</st> <st c="54310">2.x platform.</st>
			<st c="54323">Installing and configuring Airflow 2.x</st>
			<st c="54362">There is no direct Airflow 2.x installation for</st> <st c="54410">the Windows platform yet.</st> <st c="54437">But there is a Docker image that can run Airflow on Windows and operating systems with low</st> <st c="54528">memory resources.</st> <st c="54546">Our approach was to install Airflow directly on WSL2 (Ubuntu) through Windows PowerShell and also use Ubuntu to implement our Flask application for</st> <st c="54694">this topic.</st>
			<st c="54705">Now, follow the</st> <st c="54722">next procedures:</st>

				1.  <st c="54738">For Windows users, run the</st> `<st c="54766">wsl</st>` <st c="54769">command on PowerShell and log in to its home account using the</st> *<st c="54833">WSL credentials</st>*<st c="54848">.</st>
				2.  <st c="54849">Then, run the</st> `<st c="54864">cd ~</st>` <st c="54868">Linux command to ensure all installations happen in the</st> <st c="54925">home directory.</st>
				3.  <st c="54940">After installing Python 11.x and all its required Ubuntu libraries, create a virtual environment (for example,</st> `<st c="55052">ch08-airflow-env</st>`<st c="55068">) using the</st> `<st c="55081">python3 -m venv</st>` <st c="55096">command for the</st> `<st c="55113">airflow</st>` <st c="55120">module installation.</st>
				4.  <st c="55141">Activate the virtual environment by running the</st> `<st c="55190">source <</st>``<st c="55198">venv_folder>/bin/activate</st>` <st c="55224">command.</st>
				5.  <st c="55233">Next, find a directory in the system that can be the Airflow core directory where all Airflow configurations and customizations happen.</st> <st c="55370">In our case, it is the</st> `<st c="55393">/</st>``<st c="55394">mnt/c/Alibata/Development/Server/Airflow</st>` <st c="55434">folder.</st>
				6.  <st c="55442">Open the</st> `<st c="55452">bashrc</st>` <st c="55458">configuration file and add the</st> `<st c="55490">AIRFLOW_HOME</st>` <st c="55502">variable with the Airflow core directory path.</st> <st c="55550">The following is a sample of registering</st> <st c="55591">the variable:</st>

    ```

    `<st c="55684">airflow</st>` module using the `<st c="55709">pip</st>` command:

    ```py
     pip install apache-airflow
    ```

    ```py

    				7.  <st c="55748">Initialize its metadata database and generate configuration files in the</st> `<st c="55822">AIRFLOW_HOME</st>` <st c="55834">directory using the</st> `<st c="55855">airflow db</st>` `<st c="55866">migrate</st>` <st c="55873">command.</st>
				8.  <st c="55882">Create an administrator</st> <st c="55907">account for its UI dashboard using the</st> <st c="55946">following command:</st> `<st c="55965">airflow users create --username <user> --password <pass> --firstname <fname> --lastname <lname> --role Admin --email <xxxx@yyyy.com></st>`<st c="56097">. The role value should</st> <st c="56121">be</st> `<st c="56124">Admin</st>`<st c="56129">.</st>
				9.  <st c="56130">Verify if the user account is added to its database using the</st> `<st c="56193">airflow users</st>` `<st c="56207">list</st>` <st c="56211">command.</st>
				10.  <st c="56220">At this point, log in to the</st> *<st c="56250">root account</st>* <st c="56262">and activate the virtual environment using</st> `<st c="56306">root</st>`<st c="56310">. Run the scheduler using the</st> `<st c="56340">airflow</st>` `<st c="56348">scheduler</st>` <st c="56357">command.</st>
				11.  <st c="56366">With the root account, run the server using the</st> `<st c="56415">airflow webserver --port 8080</st>` <st c="56444">command.</st> <st c="56454">Port</st> `<st c="56459">8080</st>` <st c="56463">is its</st> <st c="56471">default port.</st>
				12.  <st c="56484">Lastly, access the Airflow portal at</st> `<st c="56522">http://localhost:8080</st>` <st c="56543">and use your</st> `<st c="56557">Admin</st>` <st c="56562">account to log in to</st> <st c="56584">the dashboard.</st>

			*<st c="56598">Figure 8</st>**<st c="56607">.9</st>* <st c="56609">shows the home dashboard of</st> <st c="56638">Airflow 2.x:</st>
			![Figure 8.9 – The home page of the Airflow 2.x UI](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_08_009.jpg)

			<st c="57451">Figure 8.9 – The home page of the Airflow 2.x UI</st>
			<st c="57499">An Airflow architecture is composed of the</st> <st c="57543">following components:</st>

				*   **<st c="57564">Web server</st>** <st c="57575">– Runs the UI management dashboard and executes and</st> <st c="57628">monitors tasks.</st>
				*   **<st c="57643">Scheduler</st>** <st c="57653">– Checks the status of tasks, updates tasks’ state details in the metadata database, and queues the next</st> <st c="57759">task</st> <st c="57764">for executions.</st>
				*   **<st c="57779">Metadata database</st>** <st c="57797">– Stores the states of a task,</st> **<st c="57829">cross-communications</st>** <st c="57849">(</st>**<st c="57851">XComs</st>**<st c="57856">) data, and</st> **<st c="57869">directed acyclic graph</st>** <st c="57891">(</st>**<st c="57893">DAG</st>**<st c="57896">) variables; processes perform read and write</st> <st c="57942">operations in</st> <st c="57957">this database.</st>
				*   **<st c="57971">Executor</st>** <st c="57980">– Executes tasks and updates the</st> <st c="58014">metadata database.</st>

			<st c="58032">Next, let us create</st> <st c="58053">workflow tasks.</st>
			<st c="58068">Creating tasks</st>
			<st c="58083">Airflow uses DAG files to implement tasks and their sequence flows.</st> <st c="58152">A DAG is a high-level design of the workflow and exclusive</st> <st c="58210">tasks based on their task definitions, schedules, relationships, and dependencies.</st> <st c="58294">Airflow provides the API classes that implement a DAG in Python code.</st> <st c="58364">But, before creating DAG files, open the</st> `<st c="58405">AIRFLOW_HOME</st>` <st c="58417">directory and create a</st> `<st c="58441">dags</st>` <st c="58445">sub-folder inside it.</st> *<st c="58468">Figure 8</st>**<st c="58476">.10</st>* <st c="58479">shows our Airflow core directory with the created</st> `<st c="58530">dags</st>` <st c="58534">folder:</st>
			![Figure 8.10 – Custom dags folder in AIRFLOW_HOME](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_08_010.jpg)

			<st c="58764">Figure 8.10 – Custom dags folder in AIRFLOW_HOME</st>
			<st c="58812">One of the files in our</st> `<st c="58837">$AIRFLOW_HOME/dag</st>` <st c="58854">directory is</st> `<st c="58868">report_login_count_dag.py</st>`<st c="58893">, which builds a sequence flow composed of two orchestrated API executions, each with</st> <st c="58979">service tasks.</st> *<st c="58994">Figure 8</st>**<st c="59002">.11</st>* <st c="59005">provides an overview of the</st> <st c="59034">workflow design:</st>
			![Figure 8.11 – An overview of an Airflow DAG](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_08_011.jpg)

			<st c="59172">Figure 8.11 – An overview of an Airflow DAG</st>
			`<st c="59215">DAG</st>` <st c="59219">is an API class from the</st> `<st c="59245">airflow</st>` <st c="59252">module that implements an entire workflow activity.</st> <st c="59305">It is composed of different</st> *<st c="59333">operators</st>* <st c="59342">that represent tasks.</st> <st c="59365">A DAG file can implement more than one DAG if needed.</st> <st c="59419">The following code is the</st> `<st c="59445">DAG</st>` <st c="59448">script in the</st> `<st c="59463">report_login_count_dag.py</st>` <st c="59488">file that implements the workflow depicted in</st> *<st c="59535">Figure 8</st>**<st c="59543">.11</st>*<st c="59546">:</st>

<st c="59548">from airflow import DAG</st>

<st c="59571">from airflow.operators.python import PythonOperator</st>

<st c="59623">from airflow.providers.http.operators.http import</st> <st c="59673">SimpleHttpOperator</st> from datetime import datetime <st c="59723">with DAG(dag_id="report_login_count",</st> description="Report the number of login accounts", <st c="59812">start_date=datetime(2023, 12, 27),</st> <st c="59846">schedule_interval="0 12 * * *",</st> ) as <st c="59918">dag_id</st> value. Aside from <st c="59943">description</st>, DAG has parameters, such as <st c="59984">start_date</st> and <st c="59999">schedule_interval</st>, that work like a Cron (time) scheduler for the workflow. The <st c="60079">schedule_interval</st> parameter can have the <st c="60120">@hourly</st>, <st c="60129">@daily</st>, <st c="60137">@weekly</st>, <st c="60146">@monthly</st>, or <st c="60159">@yearly</st> Cron preset options run periodically or a Cron-based expression, such as <st c="60240">*/15 * * * *</st>, that schedules the workflow to run every <st c="60295">15 minutes</st>. Setting the parameter to <st c="60332">None</st> will disable the periodic execution, requiring a trigger to run the tasks:

 task1 = <st c="60420">SimpleHttpOperator</st>( <st c="60441">task_id="list_all_login",</st><st c="60466">method="GET",</st><st c="60480">http_conn_id="packt_dag",</st><st c="60506">endpoint="/ch08/login/list/all",</st> headers={"Content-Type": "application/json"}, <st c="60586">response_check=lambda response:</st> <st c="60617">handle_response(response),</st><st c="60644">dag=dag</st> )
    task2 = <st c="60663">PythonOperator</st>( <st c="60680">task_id='count_login',</st><st c="60702">python_callable=count_login,</st><st c="60731">provide_context=True,</st><st c="60753">do_xcom_push=True,</st><st c="60772">dag=dag</st> )
        `<st c="60782">An</st>` `<st c="60785">Airflow operator</st>` `<st c="60801">implements a task.</st>` `<st c="60821">But, there are many types of operators to choose from depending on what kind</st>` `<st c="60898">of task the DAG requires.</st>` `<st c="60924">Some widely used operators in training and workplaces are</st>` `<st c="60982">the following:</st>`

            +   `<st c="60996">EmptyOperator</st>` `<st c="61010">– Initiates a</st>` `<st c="61025">built-in execution.</st>`

            +   `<st c="61044">PythonOperator</st>` `<st c="61059">– Calls a Python function that implements a</st>` `<st c="61104">business logic.</st>`

            +   `<st c="61119">BashOperator</st>` `<st c="61132">– Aims to run</st>` `<st c="61147">bash</st>` `<st c="61151">commands.</st>`

            +   `<st c="61161">EmailOperator</st>` `<st c="61175">– Sends an email through</st>` `<st c="61201">a protocol.</st>`

            +   `<st c="61212">SimpleHttpOperator</st>` `<st c="61231">– Sends an</st>` `<st c="61243">HTTP request.</st>`

        <st c="61256">其他操作可能需要安装所需的模块。</st> <st c="61316">例如,用于执行 PostgreSQL 命令的</st> `<st c="61333">PostgresOperator</st>` <st c="61349">操作符需要通过</st> `<st c="61422">apache-airflow[postgres]</st>` <st c="61446">模块通过</st> `<st c="61466">pip</st>` <st c="61469">命令安装。</st>

        <st c="61478">每个任务都必须有一个唯一的</st> `<st c="61508">task_id</st>` <st c="61515">值,以便 Airflow 识别。</st> <st c="61550">我们的</st> `<st c="61554">Task1</st>` <st c="61559">任务是一个</st> `<st c="61570">SimpleHTTPOperator</st>` <st c="61588">操作符,它向一个 HTTP</st> `<st c="61611">GET</st>` <st c="61614">请求发送到预期的 JSON 资源返回的 HTTP</st> `<st c="61634">GET</st>` <st c="61637">API 端点。</st> <st c="61687">它有一个名为</st> `<st c="61706">list_all_login</st>` <st c="61720">的 ID,并连接到名为</st> `<st c="61776">packt_dag</st>`<st c="61785">的 Airflow HTTP 连接对象。</st> <st c="61791">SimpleHTTPOperator</st> <st c="61809">所需的是一个</st> `<st c="61821">Connection</st>` <st c="61831">对象,该对象存储了操作将需要建立连接的外部服务器资源的 HTTP 详细信息。</st> <st c="61958">访问</st> `<st c="62075">Connection</st>` <st c="62085">对象。</st> *<st c="62094">图 8</st>**<st c="62102">.12</st>* <st c="62105">显示了接受连接的 HTTP 详细信息并创建</st> <st c="62177">对象的表单:</st>

        ![图 8.12 – 创建 HTTP 连接对象](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_08_012.jpg)

        <st c="62503">图 8.12 – 创建 HTTP 连接对象</st>

        <st c="62551">此外,一个</st> `<st c="62560">SimpleHTTPOperator</st>` <st c="62578">操作符提供了一个由其</st> `<st c="62632">response_check</st>` <st c="62646">参数指示的回调方法。</st> <st c="62658">回调方法访问响应和其他相关数据,可以对 API 响应进行评估和记录。</st> <st c="62738">以下是对</st> `<st c="62845">Task1</st>`<st c="62850">的回调方法的实现:</st>
<st c="62852">def handle_response(response, **context):</st> if response.status_code == 201:
        print("executed API successfully...")
        return True
    else:
        print("executed with errors...")
        return False
        <st c="63027">On the other hand,</st> `<st c="63047">Task2</st>` <st c="63052">is a</st> `<st c="63058">PythonOperator</st>` <st c="63072">operator that runs a Python function,</st> `<st c="63111">count_login()</st>`<st c="63124">, for retrieving the JSON data from the API executed in</st> `<st c="63180">Task1</st>` <st c="63185">and counting the number of records from the JSON resource.</st> <st c="63245">Setting its</st> `<st c="63257">provide_context</st>` <st c="63272">parameter to</st> `<st c="63286">True</st>` <st c="63290">allows its</st> `<st c="63302">python_callable</st>` <st c="63317">method to access the</st> `<st c="63339">taskInstance</st>` <st c="63351">object that pulls the API resource from</st> `<st c="63392">Task1</st>`<st c="63397">. The</st> `<st c="63403">count_login()</st>` <st c="63416">function can also set an</st> `<st c="63442">xcom</st>` <st c="63446">variable, a form of workflow data, because the value of</st> `<st c="63503">Task2</st>`<st c="63508">’s</st> `<st c="63512">do_xcom_push</st>` <st c="63524">parameter is</st> `<st c="63538">True</st>`<st c="63542">. The following snippet is the implementation</st> <st c="63588">of</st> `<st c="63591">count_login()</st>`<st c="63604">:</st>
 def count_login(<st c="63623">ti, **context</st>): <st c="63641">data = ti.xcom_pull(task_ids=['list_all_login'])</st> if not len(data):
        raise ValueError('Data is empty') <st c="63742">records_dict = json.loads(data[0])</st> count = len(records_dict["records"]) <st c="63814">ti.xcom_push(key="records", value=count)</st> return count
    task3 = <st c="63876">SimpleHttpOperator</st>( <st c="63897">task_id='report_count',</st><st c="63920">method="GET",</st><st c="63934">http_conn_id="packt_dag",</st><st c="63960">endpoint="/ch08/login/report/count",</st> data={"login_count": "{{ <st c="64023">task_instance.xcom_pull( task_ids=['list_all_login','count_login'], key='records')[0]</st> }}"},
        headers={"Content-Type": "application/json"},
        dag=dag
    )
    … … … … … … <st c="64215">Task3</st> is also a <st c="64232">SimpleHTTPOperator</st> operator, but its goal is to call an HTTP <st c="64293">GET</st> API and pass a request parameter, <st c="64331">login_count</st>, with a value derived from XCom data. Operators can access Airflow built-in objects, such as <st c="64436">dag_run</st> and <st c="64448">task_instance</st>, using the <st c="64473">{{ }}</st> Jinja2 delimiter. In <st c="64500">Task3</st>, <st c="64507">task_instance</st>, using its <st c="64532">xcom_pull()</st> function, retrieves from the list of tasks the XCom variable records. The result of <st c="64628">xcom_pull()</st> is always a list with the value of the XCom variable at its *<st c="64700">0 index</st>*.
			<st c="64708">The last portion of the DAG file is where to place the sequence flow of the DAG’s task.</st> <st c="64797">There are two ways to establish dependency from one task to another.</st> `<st c="64866">>></st>`<st c="64868">, or the</st> *<st c="64877">upstream dependency</st>*<st c="64896">, connects a flow from left to right, which means the execution of the task from the right depends on the success of the left task.</st> <st c="65028">The other one,</st> `<st c="65043"><<</st>` <st c="65045">or the</st> *<st c="65053">downstream dependency</st>*<st c="65074">, follows the</st> <st c="65087">reverse flow.</st> <st c="65102">If two or more tasks depend on the same task, brackets enclose those dependent tasks, such as the</st> `<st c="65200">task1 >> [task2, task3]</st>` <st c="65223">flow, where</st> `<st c="65236">task2</st>` <st c="65241">and</st> `<st c="65246">task3</st>` <st c="65251">are dependent tasks of</st> `<st c="65275">task1</st>`<st c="65280">. In the given DAG file, it is just a sequential flow from</st> `<st c="65339">task1</st>` <st c="65344">to</st> `<st c="65348">task4</st>`<st c="65353">.</st>
			<st c="65354">What executes our tasks are called</st> *<st c="65390">executors</st>*<st c="65399">. The</st> <st c="65405">default executor is</st> `<st c="65425">SequentialExecutor</st>`<st c="65443">, which runs the task flows one task at a time.</st> `<st c="65491">LocalExecutor</st>` <st c="65504">runs the workflow sequentially, but the tasks may run in parallel mode.</st> <st c="65577">There is</st> `<st c="65586">CeleryExecutor</st>`<st c="65600">, which runs workflows composed of Celery tasks, and</st> `<st c="65653">KubernetesExecutor</st>`<st c="65671">, which runs tasks on</st> <st c="65693">a cluster.</st>
			<st c="65703">To deploy and re-deploy the DAG files,</st> *<st c="65743">restart</st>* <st c="65750">the scheduler and the web server.</st> <st c="65785">Let us now implement an API endpoint function that will run the DAG deployed in the</st> <st c="65869">Airflow server.</st>
			<st c="65884">Utilizing Airflow built-in REST endpoints</st>
			<st c="65926">To trigger the DAG is to run the workflow.</st> <st c="65970">Running</st> <st c="65978">a DAG requires using the Airflow UI’s DAG page, applying Airflow APIs for console-based triggers, or consuming Airflow’s built-in REST API with the Flask application or Postman.</st> <st c="66156">This chapter implemented the</st> `<st c="66185">ch08-airflow</st>` <st c="66197">project to provide the</st> `<st c="66221">report_login_count</st>` <st c="66239">DAG with API endpoints for</st> `<st c="66267">Task1</st>` <st c="66272">and</st> `<st c="66277">Task3</st>` <st c="66282">executions and also to trigger the workflow using some Airflow REST endpoints.</st> <st c="66362">The following is a custom endpoint function that triggers the</st> `<st c="66424">report_login_count</st>` <st c="66442">DAG with a</st> `<st c="66454">dag_run_id</st>` <st c="66464">value of a</st> <st c="66476">UUID type:</st>

@login_bp.get("/login/dag/report/login/count") async def trigger_report_login_count(): token = "cGFja3RhZG1pbjpwYWNrdGFkbWlu" dag_id = "report_login_count"

deployment_url = "localhost:8080"

response = <st c="66688">requests.post(</st><st c="66702">url=f"http://{deployment_url}</st> <st c="66732">/api/v1/dags/{dag_id}/dagRuns",</st> headers={ <st c="66775">"Authorization": f"Basic {token}",</st> "Content-Type": "application/json",

        "Accept": "*/*",

        "Connection": "keep-alive",

        "Accept-Encoding": "gzip, deflate, br"

    }, <st c="66933">data = '{"dag_run_id": "d08a62c6-ed71-49fc-81a4-47991221aea5"}'</st> )

result = response.content.decode(encoding="utf-8")

return jsonify(message=result), 201

			<st c="67085">Airflow requires</st> *<st c="67103">Basic authentication</st>* <st c="67123">before consuming its REST endpoints.</st> <st c="67161">Any REST access must include an</st> `<st c="67193">Authorization</st>` <st c="67206">header with</st> <st c="67219">the generated token of a valid username and password.</st> <st c="67273">Also, install a REST client module, such as</st> `<st c="67317">requests</st>`<st c="67325">, to consume the API libraries.</st> <st c="67357">Running</st> `<st c="67365">/api/v1/dags/report_login_count/dagRuns</st>` <st c="67404">with an HTTP</st> `<st c="67418">POST</st>` <st c="67422">request will give us a JSON response</st> <st c="67460">like this:</st>

{

"conf": {}, <st c="67485">"dag_id": "report_login_count",</st><st c="67516">"dag_run_id": "01c04a4b-a3d9-4dc5-b0c3-e4e59e2db554",</st> "data_interval_end": "2023-12-27T12:00:00+00:00",

"data_interval_start": "2023-12-26T12:00:00+00:00",

"end_date": null,

"execution_date": "2023-12-27T13:55:44.910773+00:00",

"external_trigger": true,

"last_scheduling_decision": null,

"logical_date": "2023-12-27T13:55:44.910773+00:00",

"note": null, <st c="67871">"run_type": "manual",</st> "start_date": null,

"state": "queued"

}


			<st c="67932">Then, running the following</st> <st c="67961">Airflow REST endpoint using the same</st> `<st c="67998">dag_run_id</st>` <st c="68008">value will provide us with the result of</st> <st c="68050">the workflow:</st>

@login_bp.get("/login/dag/xcom/values")

async def extract_xcom_count():

try:

    token = "cGFja3RhZG1pbjpwYWNrdGFkbWlu"

    dag_id = "report_login_count"

    task_id = "return_report" <st c="68236">dag_run_id = "d08a62c6-ed71-49fc-81a4-47991221aea5"</st> deployment_url = "localhost:8080"

    response = <st c="68333">requests.get(</st><st c="68346">url=f"http://{deployment_url}</st> <st c="68376">/api/v1/dags/{dag_id}/dagRuns</st> <st c="68406">/{dag_run_id}/taskInstances/{task_id}</st> <st c="68444">/xcomEntries/{'report_msg'}",</st> headers={ <st c="68485">"Authorization": f"Basic {token}",</st> … … … … … …

        }

    )

    result = response.json()

    message = result['value']

    返回 jsonify(message=message)

except Exception as e:

    打印(e)

返回 jsonify(message="")

			<st c="68676">The given HTTP</st> `<st c="68692">GET</st>` <st c="68695">request API</st> <st c="68707">will provide us a JSON result</st> <st c="68738">like so:</st>

{

"message": "截至 2023-12-28 00:38:17.592287,有 20 个用户。"}

			<st c="68816">Airflow is a big platform that can offer us many solutions, especially in building pipelines of tasks for data transformation, batch processing, and data analytics.</st> <st c="68981">Its strength is also in implementing API orchestration for microservices.</st> <st c="69055">But for complex, long-running, and distributed workflow transactions, it is Temporal.io that can provide durable, reliable, and</st> <st c="69183">scalable solutions.</st>
			<st c="69202">Implementing workflows using Temporal.io</st>
			<st c="69243">The</st> **<st c="69248">Temporal.io</st>** <st c="69259">server manages loosely coupled workflows and activities, those not limited by the architecture of the Temporal.io platform.</st> <st c="69384">Thus, all workflow components are coded from the ground up</st> <st c="69443">without hooks and callable methods appearing in the implementation.</st> <st c="69511">The server expects the execution of activities rather than tasks.</st> <st c="69577">In BPMN, an activity is more complex than a task.</st> <st c="69627">The server is responsible for building a fault-tolerant workflow because it can recover failed activity execution by restarting its execution from</st> <st c="69774">the start.</st>
			<st c="69784">So, let us begin this topic with the Temporal.io</st> <st c="69834">server setup.</st>
			<st c="69847">Setting up the environment</st>
			<st c="69874">The Temporal.io server has an installer for</st> *<st c="69919">macOS</st>*<st c="69924">,</st> *<st c="69926">Windows</st>*<st c="69933">, and</st> *<st c="69939">Linux</st>* <st c="69944">platforms.</st> <st c="69956">For Windows users, download the ZIP file from the</st> [<st c="70006">https://temporal.download/cli/archive/latest?platform=windows&arch=amd64</st>](https://temporal.download/cli/archive/latest?platform=windows&arch=amd64) <st c="70078">link.</st> <st c="70085">Then, unzip the file to the local</st> <st c="70119">machine.</st> <st c="70128">Start the server using the</st> `<st c="70155">temporal server</st>` `<st c="70171">start-dev</st>` <st c="70180">command.</st>
			<st c="70189">Now, to integrate our Flask application with the server, install the</st> `<st c="70259">temporalio</st>` <st c="70269">module to the virtual environment using the</st> `<st c="70314">pip</st>` <st c="70317">command.</st> <st c="70327">Establish a server connection in the</st> `<st c="70364">main.py</st>` <st c="70371">module of the application using the</st> `<st c="70408">Client</st>` <st c="70414">class of the</st> `<st c="70428">temporalio</st>` <st c="70438">module.</st> <st c="70447">The following</st> `<st c="70461">main.py</st>` <st c="70468">script shows how to instantiate a</st> `<st c="70503">Client</st>` <st c="70509">instance:</st>

从 temporalio.client 导入 Client

从 modules 导入 create_app

导入 asyncio

app, celery_app= create_app("../config_dev.toml") async def connect_temporal(app):client = await Client.connect("localhost:7233")app.temporal_client = client 如果 name == "main": asyncio.run(connect_temporal(app)) app.run(debug=True)


			<st c="70844">The</st> `<st c="70849">connect_temporal()</st>` <st c="70867">method instantiates the Client API class and creates a</st> `<st c="70923">temporal_client</st>` <st c="70938">environment variable in the Flask platform for the API endpoints to run the workflow.</st> <st c="71025">Since</st> `<st c="71031">main.py</st>` <st c="71038">is the entry point module, an event loop will execute the method during the Flask</st> <st c="71121">server startup.</st>
			<st c="71136">After setting up the Temporal.io server and its connection to the Flask application, let us discuss the distinct approach to</st> <st c="71261">workflow implementation.</st>
			<st c="71286">Implementing activities and a workflow</st>
			<st c="71325">Temporal uses the code-first approach of implementing a workflow and its activities.</st> <st c="71411">Activities in a Temporal platform must be</st> *<st c="71453">idempotent</st>*<st c="71463">, meaning its parameters and results are non-changing through the</st> <st c="71529">course or history of its executions.</st> <st c="71566">The following is an example of a complex but</st> <st c="71611">idempotent activity:</st>

从 temporalio 导入 activity

@activity.defn 异步 def reserve_schedule(appointmentwf: AppointmentWf) -> str:

try:

    异步使用 db_session() 作为 sess:

        异步使用 sess.begin(): <st c="71807">repo = AppointmentRepository(sess)</st> … … … … … … … <st c="71855">result = await repo.insert_appt(appt)</st> 如果 result == False:

                … … … … … … <st c="71925">return "failure"</st> … … … … … … <st c="71953">return "success"</st> except Exception as e:

    打印(e)

    … … … … … … <st c="72084">@activity.defn</st> 注解,使用工作流程数据类作为局部参数,并返回一个不变化的值。返回的值可以是固定字符串、数字或任何长度不变的字符串。避免返回具有可变属性值的集合或模型对象。我们的 <st c="72379">reserve_schedule()</st> 活动接受一个包含预约详情的 <st c="72418">AppointmentWf</st> 对象,并将信息记录保存到数据库中。它只返回 <st c="72548">"successful"</st> 或 <st c="72564">"failure"</st>。

        <st c="72574">活动是 Temporal 允许访问外部服务,如数据库、电子邮件或 API 的地方,而不是在工作流程实现中。</st> <st c="72690">以下代码是一个</st> *<st c="72750">Temporal 工作流程</st>* <st c="72768">,它运行 <st c="72782">reserve_schedule()</st> <st c="72800">活动:</st>
<st c="72810">@workflow.defn(sandboxed=False)</st> class ReserveAppointmentWorkflow():
    def __init__(self) -> None: <st c="73046">@workflow.defn</st> decorator implements a workflow. An example is our <st c="73112">ReserveAppointmentWorkflow</st> class.
			<st c="73145">It maintains the same execution state starting from the beginning, making it a deterministic workflow.</st> <st c="73249">It also manages all its states through replays to determine some exceptions and provide recovery after a</st> <st c="73354">non-deterministic state.</st>
			<st c="73378">Moreover, Temporal workflows are designed to run continuously without time limits but with proper scheduling to handle long-running and complex activities.</st> <st c="73535">However, using threads for concurrency is not allowed in Temporal workflows.</st> <st c="73612">It must have an instance method decorated by</st> `<st c="73657">@workflow.run</st>` <st c="73670">to create a continuous loop for its activities.</st> <st c="73719">The following</st> `<st c="73733">run()</st>` <st c="73738">method accepts a request model with appointment details from the user and loops until the cancellation of the appointment, where</st> `<st c="73868">appointmentwf.status</st>` <st c="73888">becomes</st> `<st c="73897">False</st>`<st c="73902">:</st>

@workflow.run 异步 def run(self, data: ReqAppointment) -> None:

    持续时间 = 12

    self.appointmentwf.ticketid = data.ticketid

    self.appointmentwf.patientid = data.patientid

    … … … … … … `<st c="74085">while self.appointmentwf.status:</st>` self.appointmentwf.remarks = "Doctor reservation being processed...." `<st c="74188">try:</st>` `<st c="74192">await workflow.execute_activity(</st>` `<st c="74225">reserve_schedule,</st>` `<st c="74243">self.appointmentwf,</st>` `<st c="74263">start_to_close_timeout=timedelta(</st>` `<st c="74297">seconds=10),</st>` `<st c="74313">await asyncio.sleep(duration)</st>` `<st c="74342">except asyncio.CancelledError as err:</st>` self.appointmentwf.status = False

            self.appointmentwf.remarks = "Appointment with doctor done." `<st c="74739">ReserveAppointmentWorkflow</st>` 实例,当取消时,将抛出 `<st c="74804">CancelledError</st>` 异常,这将触发设置 `<st c="74878">appointmentwf.status</st>` 为 `<st c="74902">False</st>` 并执行 `<st c="74925">start_to_close()</st>` 活动的异常处理子句。

        `<st c="74951">除了循环和构造函数之外,工作流程实现可以发出</st>` `<st c="74979">constructor, a workflow implementation can emit</st>` `<st c="75028">resultset</st>` `<st c="75037">instances or information about the workflow.</st>` `<st c="75083">为了执行此操作,实现一个实例方法并用</st>` `<st c="75152">@workflow.query</st>` `<st c="75167">.以下方法返回一个</st>` `<st c="75201">appointment record:</st>`
 <st c="75220">@workflow.query</st> def details(self) -> AppointmentWf:
        return self.appointmentwf
        `<st c="75298">与 Zeebe/Camunda 不同,在那里服务器执行并管理工作流程,Temporal.io 服务器不运行任何工作流程实例,而是工作线程。</st>` `<st c="75450">我们将在下一节中了解更多关于工作线程的信息。</st>`

        `<st c="75503">构建工作线程</st>`

        `<st c="75521">A</st>` `<st c="75579">Worker</st>` `<st c="75585">class from the</st>` `<st c="75601">temporalio.worker</st>` `<st c="75618">module requires the</st>` `<st c="75639">client</st>` `<st c="75645">connection,</st>` `<st c="75658">task_queue</st>` `<st c="75668">,</st>` `<st c="75670">workflows</st>` `<st c="75679">, and</st>` `<st c="75685">activities</st>` `<st c="75695">as constructor</st>` `<st c="75711">parameters before its instantiation.</st>` `<st c="75748">我们的工作线程应该位于</st>` `<st c="75781">Flask 的上下文之外,因此我们在参数中添加了</st>` `<st c="75815">workflow_runner</st>` `<st c="75830">参数。</st>` `<st c="75860">以下代码是我们对</st>` `<st c="75908">Temporal 工作线程的实现:</st>`
<st c="75924">from temporalio.client import Client</st>
<st c="75961">from temporalio.worker import Worker,</st> <st c="75999">UnsandboxedWorkflowRunner</st> import asyncio
from modules.admin.activities.workflow import reserve_schedule, close_schedule
from modules.models.workflow import appt_queue_id
from modules.workflows.transactions import ReserveAppointmentWorkflow
async def main(): <st c="76258">client = await Client.connect("localhost:7233")</st> worker = <st c="76315">Worker(</st><st c="76322">client,</st><st c="76330">task_queue=appt_queue_id,</st><st c="76356">workflows=[ReserveAppointmentWorkflow],</st><st c="76396">activities=[reserve_schedule, close_schedule],</st><st c="76443">workflow_runner=UnsandboxedWorkflowRunner,</st> ) <st c="76489">await worker.run()</st> if __name__ == "__main__":
    print("Temporal worker started…") <st c="76679">Worker</st> instance needs to know what workflows to queue and activities to run before the client application triggers their executions. Now, passing the <st c="76829">UnsandboxedWorkflowRunner</st> object to the <st c="76869">workflow_runner</st> parameter indicates that our worker will be running as an independent Python application outside the context of our Flask platform or any sandbox environment, thus the setting of the <st c="77068">sandboxed</st> parameter in the <st c="77095">@workflow.defn</st> decorator of every workflow class to <st c="77147">False</st>. To run the worker, call and await the <st c="77192">run()</st> method of the <st c="77212">Worker</st> instance.
			<st c="77228">Lastly, after implementing the</st> <st c="77259">workflows, activities, and the worker, it is time to trigger the workflow</st> <st c="77334">for execution.</st>
			<st c="77348">Running activities</st>
			<st c="77367">The</st> `<st c="77372">ch08-temporal</st>` <st c="77385">project is a</st> <st c="77398">RESTful application in Flask, so to run a workflow, an API endpoint must import and use</st> `<st c="77487">app.temporal_client</st>` <st c="77506">to connect to the server and to invoke the</st> `<st c="77550">start_workflow()</st>` <st c="77566">method that will trigger the</st> <st c="77596">workflow execution.</st>
			<st c="77615">The</st> `<st c="77620">start_workflow()</st>` <st c="77636">method requires the workflow’s “</st>*<st c="77669">run</st>*<st c="77673">” method, the single model object parameter, the unique workflow ID, and</st> `<st c="77747">task_queue</st>`<st c="77757">. The following API endpoint triggers the execution of our</st> `<st c="77816">ReserveAppointmentWorkflow</st>` <st c="77842">class:</st>

<st c="77849">@admin_bp.route("/appointment/doctor", methods=["POST"])</st> async def request_appointment(): <st c="77940">client = get_client()</st> appt_json = request.get_json()

appointment = ReqAppointment(**appt_json) `<st c="78035">await client.start_workflow(</st>` `<st c="78063">ReserveAppointmentWorkflow.run,</st>` `<st c="78095">appointment,</st>` `<st c="78108">id=appointment.ticketid,</st>` `<st c="78133">task_queue=appt_queue_id,</st>` )

message = jsonify({"message": "Appointment for doctor requested...."})

response = make_response(message, 201)

return response

			<st c="78287">After a successful workflow trigger, another API can query the details or results of the workflow by extracting the workflow’s</st> `<st c="78415">WorkflowHandler</st>` <st c="78430">class from the client using its</st> *<st c="78463">workflow ID</st>*<st c="78474">. The following endpoint function shows how to retrieve the result of the</st> <st c="78548">completed workflow:</st>

@admin_bp.route("/appointment/details", methods=["GET"]) async def get_appointment_details():

client = get_client()

ticketid = request.args.get("ticketid")

print(ticketid)

handle = <st c="78749">client.get_workflow_handle_for(ReserveAppointmentWorkflow.run, ticketid)</st><st c="78822">results = await handle.query(</st> <st c="78852">ReserveAppointmentWorkflow.details)</st> message = jsonify({

        "ticketid": results.ticketid,

        "patientid": results.patientid,

        "docid": results.docid,

        "date_scheduled": results.date_scheduled,

        "time_scheduled": results.time_scheduled,

    }

)

response = make_response(message, 200)

return response

			<st c="79137">To prove that Temporal workflows</st> <st c="79170">can respond to cancellation events, the following API invokes the</st> `<st c="79237">cancel()</st>` <st c="79245">method from the</st> `<st c="79262">WorkflowHandler</st>` <st c="79277">class for its workflow to throw a</st> `<st c="79312">CancelledError</st>` <st c="79326">exception, leading to the execution of the</st> `<st c="79370">close_schedule()</st>` <st c="79386">activity:</st>

@admin_bp.route("/appointment/close", methods=["DELETE"]) async def end_subscription():

client = get_client()

ticketid = request.args.get("ticketid")

response = make_response(message, 202)

return response

			<st c="79728">Although there is still a lot to discuss about the architecture and the behavior of these big-time workflow</st> <st c="79836">solutions, the main goal is to highlight the feasibility of integrating different workflow engines into the asynchronous Flask platform and take into consideration workarounds for integrations to work with</st> <st c="80043">Flask applications.</st>
			<st c="80062">Summary</st>
			<st c="80070">This chapter proved that</st> `<st c="80096">Flask[async]</st>` <st c="80108">can work with different workflow engines, starting with Celery tasks.</st> `<st c="80179">Flask[async]</st>`<st c="80191">, combined with the workflows created by Celery’s signatures and primitives, works well in building chained, grouped, and</st> <st c="80313">chorded processes.</st>
			<st c="80331">Then,</st> `<st c="80338">Flask[async]</st>` <st c="80350">was proven to work with SpiffWorkflow for some BPMN serialization that focuses on</st> `<st c="80433">UserTask</st>` <st c="80441">and</st> `<st c="80446">ScriptTask</st>` <st c="80456">tasks.</st> <st c="80464">Also, this chapter even considered solving BPMN enterprise problems using the Zeebe/Camunda platform that showcases</st> `<st c="80580">ServiceTask</st>` <st c="80591">tasks.</st>
			<st c="80598">Moreover,</st> `<st c="80609">Flask[async]</st>` <st c="80621">created an environment with Airflow 2.x to implement pipelines of tasks building an API orchestration.</st> <st c="80725">In the last part, the chapter established the integration between</st> `<st c="80791">Flask[async]</st>` <st c="80803">and Temporal.io and demonstrated the implementation of deterministic and</st> <st c="80877">distributed workflows.</st>
			<st c="80899">This chapter provided a clear picture of the extensibility, usability, and scalability of the Flask framework in building scientific and big data applications and even BPMN-related and ETL-involved</st> <st c="81098">business processes.</st>
			<st c="81117">The next chapter will discuss the different authentication and authorization mechanisms to secure</st> <st c="81216">Flask applications.</st>











第十章:9

保护 Flask 应用程序

像任何 Web 应用程序一样,Flask 应用程序都有需要保护免受外部攻击的漏洞,这些攻击利用了这些软件缺陷。 这些网络攻击主要是由于访问控制问题不完善, 跨站脚本 (XSS), 跨站请求伪造 (CSRF), 服务器端请求伪造 (SSRF), SQL 注入,以及 拒绝服务 (DoS),以及过时的模块 和库。

实施安全措施必须是任何 Flask 应用程序的最高优先级,尤其是当它在构建模型、仓库层和工作流相关事务时更加依赖外部模块时。 使用第三方库可能会给 Flask 应用程序带来风险,因为一些库代码可能包含编码错误或漏洞。 这对于来自过时第三方模块和库的代码来说尤其如此,这些模块和库的来源不可靠。

使用外部模块,如使用 Authlib 模块而不是从头开始构建,来构建 Flask 组件和功能更容易。 为了减少,如果不是避免,网络攻击的机会,应该制定一个仅使用可靠和更新模块的安全计划。 这将保护应用程序免受 外部攻击者。

本章的主要目标是提供可能的解决方案,以帮助 Flask 应用程序避免使用 Flask 内置组件进行的一些知名网络攻击,以及一些最新且可靠的 第三方库。

以下是我们将在帮助保护我们的 Flask 应用程序的上下文中涵盖的主题:

  • 添加对 网络漏洞的保护

  • 保护 响应数据

  • 管理 用户凭据

  • 实现网络 表单认证

  • 防止 CSRF 攻击

  • 实现用户身份验证 和授权

  • 控制视图或 API 访问

技术要求

本章介绍的是 <st c="2705">Flask[async]</st> 功能,包括异步 <st c="2751">Flask-SQLAlchemy</st> 事务。 它们可在 https://github.com/PacktPublishing/Mastering-Flask-Web-Development/tree/main/ch09找到。

添加对网络漏洞的保护

SQL 注入、SSRF 和 XSS 攻击是最常见的网络漏洞,它们破坏了许多网络应用程序。 它们还影响任何使用基于 HTTP 的事务的应用程序,例如 <st c="3119">POST</st>, <st c="3125">PUT</st>, <st c="3130">PATCH</st>, 和 <st c="3141">DELETE</st> SQL 注入发生在攻击者渗透管理受信任应用程序内容的后端数据存储时。 嵌入 恶意 SQL 代码可以篡改数据,生成不想要的页面或破坏数据库。 XSS 攻击通常会将恶意脚本插入到应用程序的页面中,以从系统中窃取 cookie、会话数据和敏感凭证。 另一方面,CSRF 发生在认证环境中。 它发生在有效用户执行 HTTP 事务时,浏览器中的恶意脚本篡改了有效的凭据,用虚假和无效的凭据来引导事务到 不受信任的系统。

将表单验证应用于请求数据

避免这些攻击的一个解决方案是设计一个表单验证,它不会占用几行代码或给视图或 API 函数增加更多性能开销 开销。 The <st c="4066">FlaskForm</st> 类通过将属性映射到适当的字段类来子类化表单模型。 每个字段类(例如, <st c="4192">StringField</st>, <st c="4205">BooleanField</st>, <st c="4219">DateField</st>, 或 <st c="4233">TimeField</st>)都有属性和内置验证器(例如, <st c="4291">Length()</st>, <st c="4301">Email()</st>, 或 <st c="4313">DataRequired()</st>),并支持自定义验证器 当验证过程需要复杂条件时。 有了这些验证器,它可以在一定程度上保护应用程序免受利用。 关于使用 Flask-WTF 的进一步讨论包括在 第四章

如果应用程序不是基于 Web 或不需要表单模型类, <st c="5199">pip</st> 命令:

 pip install flask-gladiator

安装后,该模块无需进一步设置。 在构建验证规则时可以立即使用,例如以下验证规则将仔细审查 管理员资料:

<st c="5458">import gladiator as glv</st>
<st c="5482">from gladiator.core import ValidationResult</st> def validate_form(form_data):
    field_validations = (
        ('adminid', <st c="5591">glv.required</st>, <st c="5605">glv.length_max(12)</st>),
        ('username', glv.required, <st c="5654">glv.type_(str)</st>),
        ('firstname', glv.required, glv.length_max(50), <st c="5720">glv.regex_('[a-zA-Z][a-zA-Z ]+')</st>),
        ('midname', glv.required, glv.length_max(50), glv.regex_('[a-zA-Z][a-zA-Z ]+')),
        ('lastname', glv.required, glv.length_max(50), glv.regex_('[a-zA-Z][a-zA-Z ]+')),
        ('email', glv.required, glv.length_max(25), <st c="5963">glv.format_email</st>),
        ('mobile', glv.required, glv.length_max(15)),
        ('position', glv.required,  glv.length_max(100)),
        ('status', glv.required, <st c="6103">glv.in_(['true', 'false'])</st>),
        ('gender', glv.required, glv.in_(['male', 'female'])),
    ) <st c="6190">result:ValidationResult = glv.validate(field_validations, form_data)</st> return <st c="6317">gladiator</st> module is the <st c="6341">validate()</st> method, which has two required parameters: <st c="6395">form_data</st> and <st c="6409">validators</st>. The validators are placed in a tuple of tuples, as shown in the preceding code, wherein each tuple contains the request parameter name followed by all its validators. Our <st c="6592">ch09-web-passphrase</st> project uses the following validators:

				*   `<st c="6650">required()</st>`<st c="6661">: Requires the parameter to have</st> <st c="6695">a value.</st>
				*   `<st c="6703">length_max()</st>`<st c="6716">: Checks whether the given string length is lower than or equal to a</st> <st c="6786">maximum value.</st>
				*   `<st c="6800">type_()</st>`<st c="6808">: Checks the type of the request data (e.g., a form parameter is always</st> <st c="6881">a string).</st>
				*   `<st c="6891">regex_ ()</st>`<st c="6901">: Matches the string to a</st> <st c="6928">regular expression.</st>
				*   `<st c="6947">format_email()</st>`<st c="6962">: Checks whether the request data follows the</st> <st c="7009">email regex.</st>
				*   `<st c="7021">in_()</st>`<st c="7027">: Checks whether the value is within the list</st> <st c="7074">of options.</st>

			<st c="7085">The list shows only a few of the many validator functions that the</st> `<st c="7153">gladiator</st>` <st c="7162">module can provide to establish the validation</st> <st c="7209">rules.</st> <st c="7217">Now, the</st> `<st c="7226">validate()</st>` <st c="7236">method returns a</st> `<st c="7254">ValidationResult</st>` <st c="7270">object, which has a boolean</st> `<st c="7299">success</st>` <st c="7306">variable that yields</st> `<st c="7328">True</st>` <st c="7332">if all the validators have no</st> <st c="7362">hits.</st> <st c="7369">Otherwise, it yields</st> `<st c="7390">False</st>`<st c="7395">. The following code shows how the</st> `<st c="7430">ch09-web-passphrase</st>`<st c="7449">’s</st> `<st c="7453">add_admin_profile()</st>` <st c="7472">method utilizes the given</st> `<st c="7499">validate_form()</st>` <st c="7514">view function:</st>

@current_app.route('/admin/profile/add', methods=['GET', 'POST'])

async def add_admin_profile():

if not session.get("user"):

    return redirect('/login/auth')

… … … … … …

if request.method == 'GET':

    return render_template('admin/add_admin_profile.html', admin=admin_rec), 200

else:

    result = validate_form(request.form)

    if result == False:

        flash(f'验证问题。', 'error')

        return render_template('admin/add_admin_profile.html', admin=admin_rec), 200

    … … … … … …

    return render_template('admin/add_admin_profile.html', admin=admin_rec), 200

			<st c="8070">Now, filtering malicious text can be effective if we combine the validation and sanitation of this form data.</st> <st c="8181">Sanitizing inputs</st> <st c="8199">means encoding special</st> <st c="8221">characters that might trigger the execution of malicious scripts from</st> <st c="8292">the browser.</st>
			<st c="8304">Sanitizing form inputs</st>
			<st c="8327">Aside from validation, view or API functions must also sanitize incoming request data by converting special characters and suspicious</st> <st c="8462">symbols to purely text so that XML- and HTML-based templates can render them without side effects.</st> <st c="8561">This process is</st> <st c="8576">known as</st> `<st c="8600">markupsafe</st>` <st c="8610">module has an</st> `<st c="8625">escape()</st>` <st c="8633">method that can normalize request data with query strings that intend to control the JavaScript codes, modify the UI experience, or tamper browser cookies when Jinja2 templates render them.</st> <st c="8824">The following snippet is a portion of the</st> `<st c="8866">add_admin_profile()</st>` <st c="8885">view function that sanitizes the form data after</st> `<st c="8935">gladiator</st>` <st c="8944">validation:</st>

@current_app.route('/admin/profile/add', methods=['GET', 'POST'])

async def add_admin_profile():

… … … … … …

result = validate_form(request.form)

if result == False:

    flash(f'验证问题。', 'error')

    return render_template('admin/add_admin_profile.html', admin=admin_rec), 200

username = request.form['username']

… … … … … …

admin_details = {

    "adminid": escape(request.form['adminid'].strip()),

    "username": escape(request.form['username'].strip()),

    "firstname": escape(request.form['firstname'].strip()),

    … … … … … …

    "gender": escape(request.form['gender'].strip())

}

admin = Administrator(**admin_details)

result = await repo.insert_admin(admin)

if result == False:

    flash(f'添加 … 资料时出错。', 'error')

else:

    flash(f'成功添加用户 … )

return render_template('admin/add_admin_profile.html', admin=admin_rec), 200

			<st c="9792">Removing leading and trailing whitespaces or defined suspicious characters using Python’s</st> `<st c="9883">strip()</st>` <st c="9890">method with the escaping process may lower the risk of injection and XSS attacks.</st> <st c="9973">However, be sure that the validation rules and sanitation techniques combined will neither ruin the performance of the view or API function nor change the actual request data.</st> <st c="10149">Also, tight</st> <st c="10161">validation rules can affect the overall runtime performance, so choose the appropriate number and types of validators for</st> <st c="10283">every form.</st>
			<st c="10294">To avoid SQL injection, use</st> <st c="10323">an ORM such as</st> **<st c="10338">SQLAlchemy</st>**<st c="10348">,</st> **<st c="10350">Pony</st>**<st c="10354">, or</st> **<st c="10359">Peewee</st>** <st c="10365">that can provide a</st> <st c="10384">more abstract form of SQL transactions and even escape utilities to sanitize column</st> <st c="10469">values before persistence.</st> <st c="10496">Avoid using native and dynamic queries where the field values are concatenated to the query string because they are prone to manipulation</st> <st c="10634">and exploitation.</st>
			<st c="10651">Sanitation can also be applied to response data to</st> <st c="10703">avoid another type of attack called the</st> **<st c="10743">Server-Side Template Injection</st>** <st c="10773">(</st>**<st c="10775">SSTI</st>**<st c="10779">).</st> <st c="10783">Let us now discuss how to protect the application from SSTIs by managing the</st> <st c="10859">response data.</st>
			<st c="10874">Securing response data</st>
			<st c="10897">Jinja2 has a built-in escaping mechanism to avoid SSTIs.</st> <st c="10955">SSTIs allow attackers to inject malicious template scripts or fragments that can run in the background.</st> <st c="11059">These then ruin the response or perform unwanted</st> <st c="11107">executions that can ruin server-side operations.</st> <st c="11157">Thus, applying the</st> `<st c="11176">safe</st>` <st c="11180">filter in Jinja templates to perform dynamic content augmentation is not a good practice.</st> <st c="11271">The</st> `<st c="11275">safe</st>` <st c="11279">filter turns off the Jinja2’s escaping mechanism and allows for running these malicious attacks.</st> <st c="11377">In connection with this, avoid</st> <st c="11408">using</st> `<st c="11448"><a></st>` <st c="11451">tag in templates (e.g.,</st> `<st c="11476"><a href="{{ var_link }}">Click Me</a></st>`<st c="11513">).</st> <st c="11517">Instead, utilize the</st> `<st c="11538">url_for()</st>` <st c="11547">utility method to call dynamic view functions because it validates and checks whether the Jinja variable in the expression is a valid view name.</st> *<st c="11693">Chapter 1</st>* <st c="11702">discusses how to apply</st> `<st c="11726">url_for()</st>` <st c="11735">for hyperlinks.</st>
			<st c="11751">On the other hand, there are also issues in Flask that need handling to prevent injection attacks on the Jinja templates, such as managing how the view functions will render the context data and add security</st> <st c="11960">response headers.</st>
			<st c="11977">Rendering Jinja2 variables</st>
			<st c="12004">There is no ultimate solution to avoid injection but to</st> <st c="12060">apply escaping to context data before rendering them to Jinja2 templates.</st> <st c="12135">Moreover, avoid using</st> `<st c="12157">render_template_string()</st>` <st c="12181">even if this is part of the Flask framework.</st> <st c="12227">Rendering HTML page-generated content may accidentally run malicious data from inputs overlooked by</st> <st c="12326">filtering and escaping.</st> <st c="12351">It is always good practice to place all HTML content in a file with an</st>`<st c="12421">.html</st>` <st c="12426">extension, or XML content in a</st> `<st c="12458">.xml</st>` <st c="12462">file, to enable Jinja2’s default escaping feature.</st> <st c="12514">Then, render them using the</st> `<st c="12542">render_template()</st>` <st c="12559">method with or without the escaped and validated context data.</st> <st c="12623">All our projects use</st> `<st c="12644">render_template()</st>` <st c="12661">in rendering</st> <st c="12675">Jinja2 templates.</st>
			<st c="12692">Security response headers must also be part of the response object when rendering every view template.</st> <st c="12796">Let us explore these security response headers and learn where to</st> <st c="12862">build them.</st>
			<st c="12873">Adding security response headers</st>
			<st c="12906">HTTP security response headers are directives used by many web applications to mitigate vulnerability attacks, such as XXS and public exposure of user details.</st> <st c="13067">They are headers added in the response object</st> <st c="13113">during the rendition</st> <st c="13133">of the Jinja2 templates or JSON results.</st> <st c="13175">Some of these headers include</st> <st c="13205">the following:</st>

				*   `<st c="13371">UTF-8</st>` <st c="13376">charset to</st> <st c="13388">avoid XSS.</st>
				*   `<st c="13471">content-type</st>`<st c="13483">. It also blocks the browser’s</st> `<st c="13514">media-type</st>` <st c="13524">sniffing, so its value should</st> <st c="13555">be</st> `<st c="13558">nosniff</st>`<st c="13565">.</st>
				*   `<st c="13653"><frame></st>`<st c="13660">,</st> `<st c="13662"><iframe></st>`<st c="13670">,</st> `<st c="13672"><embed></st>`<st c="13679">, or</st> `<st c="13684"><objects></st>`<st c="13693">. Possible values include</st> `<st c="13719">DENY</st>` <st c="13723">and</st> `<st c="13728">SAMEORIGIN</st>`<st c="13738">. The</st> `<st c="13744">DENY</st>` <st c="13748">option disallows rending pages on a frame, while</st> `<st c="13798">SAMEORIGIN</st>` <st c="13808">allows rendering a page on a frame with the same URL site as</st> <st c="13870">the page.</st>
				*   **<st c="13879">Strict-Transport-Security</st>**<st c="13905">: This indicates that the browser can only access the page through the</st> <st c="13977">HTTPS protocol.</st>

			<st c="13992">In our</st> `<st c="14000">ch09-web-passphrase</st>` <st c="14019">project, the global</st> `<st c="14040">@after_request</st>` <st c="14054">function creates a list of security response headers for every view function call.</st> <st c="14138">The following code snippet in the</st> `<st c="14172">main.py</st>` <st c="14179">module shows this</st> <st c="14198">function implementation:</st>

@app.after_request def create_sec_resp_headers(response):

response.headers['Content-Type'] = 'text/html; charset=UTF-8'

response.headers['X-Content-Type-Options'] = 'nosniff'

response.headers['X-Frame-Options'] = 'SAMEORIGIN'

response.headers['Strict-Transport-Security'] = 'Strict-Transport-Security: max-age=63072000; includeSubDomains; preload'

return response

			<st c="14586">Here,</st> `<st c="14593">Content-Type</st>`<st c="14605">,</st> `<st c="14607">X-Content-Type-Options</st>`<st c="14629">,</st> `<st c="14631">X-Frame-Options</st>`<st c="14646">, and</st> `<st c="14652">Strict-Transport-Security</st>` <st c="14677">are the most essential response headers for web applications.</st> <st c="14740">By the way,</st> `<st c="14752">SAMEORIGIN</st>` <st c="14762">is the ideal value for</st> `<st c="14786">X-Frame-Options</st>` <st c="14801">because it prevents view pages from</st> <st c="14838">displaying outside the site domain of the</st> <st c="14880">project, mitigating</st> `<st c="15007">/</st>``<st c="15008">admin/profile/add</st>` <st c="15025">view.</st>
			![Figure 9.1 – The response headers when running the view function](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_09_001.jpg)

			<st c="15768">Figure 9.1 – The response headers when running the view function</st>
			<st c="15832">On the other hand, another</st> <st c="15859">way to manage security response headers is</st> <st c="15903">through the Flask module</st> `<st c="15990">flask-talisman</st>` <st c="16004">module using the following</st> `<st c="16032">pip</st>` <st c="16035">command:</st>

pip install flask-talisman


			<st c="16071">Afterward, instantiate the</st> `<st c="16099">Talisman</st>` <st c="16107">class in the</st> `<st c="16121">create_app()</st>` <st c="16133">method and integrate the module into the Flask application by adding and configuring the web application’s security response headers using Talisman libraries, as shown in the</st> <st c="16309">following snippet:</st>

from flask_talisman import Talisman def create_app(config_file):

app = Flask(__name__,template_folder= '../modules/pages', static_folder= '../modules/resources')

app.config.from_file(config_file, toml.load)

… … … … … … <st c="16547">talisman = Talisman(app)</st> csp = {

    'default-src': [

        '\'self\'',

        'https://code.jquery.com',

        'https://cdnjs.com',

        'https://cdn.jsdelivr.net',

    ]

}

hsts = {

    'max-age': 31536000,

    'includeSubDomains': True

} <st c="16747">talisman.force_https = True</st> talisman.force_file_save = True <st c="16807">talisman.x_xss_protection = True</st><st c="16839">talisman.session_cookie_secure = True</st> talisman.frame_options_allow_from = 'https://www.google.com' <st c="17226">default-src</st>, <st c="17239">image-src</st>, <st c="17250">style-src</st>, <st c="17261">media-src</st>, <st c="17272">object-src</st> ). 在我们的配置中,JS 文件必须仅来自<st c="17337">https://code.jquery.com</st>、<st c="17362">https://cdnjs.com</st>、<st c="17381">https://cdn.jsdelivr.net</st>和本地主机,而 CSS 和图像必须如<st c="17503">default-src</st>中所示,从本地主机获取,作为每个视图页面的后备资源。通过指定具有特定 JS 资源的<st c="17570">script-src</st>、具有 CSS 资源的<st c="17607">style-src</st>和具有目标图像的<st c="17641">image-src</st>,将绕过<st c="17692">default-src</st>设置。

        除了 CSP 之外,Talisman 还可以添加<st c="17750">X-XSS-Protection</st>、<st c="17768">Referrer-Policy</st>、<st c="17783">和<st c="17789">Set-Cookie</st>,以及之前由<st c="17867">@after_request</st> <st c="17881">函数</st>包含在响应中的头部。在结合这两种方法时需要谨慎,因为头部设置的<st c="17892">重叠</st>可能会发生。

        <st c="17992">在响应中添加</st> `<st c="18004">Strict-Transport-Security</st>` <st c="18029">头部并设置 Talisman 属性的</st> `<st c="18069">force_https</st>` <st c="18080">为</st> `<st c="18107">True</st>` <st c="18111">需要以 HTTPS 模式运行应用程序。</st> <st c="18160">让我们探索为</st> <st c="18224">Flask 应用程序</st>启用 HTTPS 的最新和最简单的方法。

        <st c="18242">使用 HTTPS 运行请求/响应事务</st>

        <st c="18291">HTTPS 是一种 TLS 加密的 HTTP 协议。</st> <st c="18332">它建立了数据发送者和接收者之间的安全通信,保护了在交换过程中流动的 cookies、URLs 和敏感信息</st> <st c="18425">。它还保护了数据的完整性和用户的真实性,因为它需要用户的私钥来允许访问。</st> <st c="18503">因此,要启用 HTTPS 协议,WSGI 服务器必须使用由 SSL 密钥生成器生成的公钥和私钥证书运行。</st> <st c="18630">按照惯例,证书必须保存在项目目录内或主机服务器上的安全位置。</st> <st c="18773">本章</st> <st c="18897">使用</st> **<st c="18911">OpenSSL</st>** <st c="18918">工具生成</st> <st c="18935">证书。</st>

        <st c="18951">使用以下</st> `<st c="18971">pyopenssl</st>` <st c="18980">命令安装最新版本:</st> `<st c="19001">pip</st>` <st c="19004">命令:</st>
 pip install pyopenssl
        <st c="19035">现在,要运行应用程序,请将</st> <st c="19076">私钥和公钥</st> <st c="19104">包含在</st> `<st c="19109">run()</st>` <st c="19109">中</st> 通过其 <st c="19122">ssl_context</st> <st c="19133">参数。</st> <st c="19145">以下</st> `<st c="19159">main.py</st>` <st c="19166">代码片段展示了如何使用 HTTPS</st> <st c="19220">在开发服务器上</st> <st c="19225">运行应用程序:</st>
 app, celery_app, auth = create_app('../config_dev.toml')
… … … … … …
if __name__ == '__main__': <st c="19398">python main.py</st> command with the <st c="19430">ssl_context</st> parameter will show a log on the terminal console, as shown in *<st c="19505">Figure 9</st>**<st c="19513">.2</st>*:
			![Figure 9.2 – The server log when running on HTTPS](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_09_002.jpg)

			<st c="19742">Figure 9.2 – The server log when running on HTTPS</st>
			<st c="19791">When opening the</st> `<st c="19809">https://127.0.0.1:5000/</st>` <st c="19832">link on a browser, a warning page will pop up on the screen, such as the one depicted in</st> *<st c="19922">Figure 9</st>**<st c="19930">.3</st>*<st c="19932">, indicating that we are entering a secured page from a</st> <st c="19988">non-secured browser.</st>
			![Figure 9.3 – A warning page on opening secured links](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_09_003.jpg)

			<st c="20319">Figure 9.3 – A warning page on opening secured links</st>
			<st c="20371">Another way to run Flask applications</st> <st c="20409">on an HTTP protocol is to include the key files in the command line, such as</st> `<st c="20487">python main.py --cert=cert.pem --key=key.pem</st>`<st c="20531">. In the production environment, we run Flask applications according to the procedure followed by the</st> <st c="20633">secured</st> <st c="20641">production server.</st>
			<st c="20659">Encryption does not apply only when establishing an HTTP connection but also when securing sensitive user information such as usernames and passwords.</st> <st c="20811">In the next section, we will discuss the</st> <st c="20851">different ways of</st> **<st c="20870">hashing</st>** <st c="20877">and encrypting</st> <st c="20893">user credentials.</st>
			<st c="20910">Managing user credentials</st>
			<st c="20936">The most common procedure for protecting any application from attacks is to control access to the user’s sensitive details, such as their username and password.</st> <st c="21098">Direct use of saved raw user credentials for login</st> <st c="21149">validation will not protect the application from attacks unless the application derives passphrases from the passwords, saves them into the database, and applies them for user</st> <st c="21325">validation instead.</st>
			<st c="21344">This topic will</st> <st c="21360">cover password</st> <st c="21375">hashing using</st> `<st c="21478">sqlalchemy_utils</st>` <st c="21494">module for the seamless and automatic encryption of</st> <st c="21547">sensitive data.</st>
			<st c="21562">Encrypting user passwords</st>
			<st c="21588">Generating a passphrase from the username and password of the user is the typical and easiest way to protect the</st> <st c="21702">application from attackers who want to crack down or hack a user account.</st> <st c="21776">In Flask, there are two ways to generate a passphrase from</st> <st c="21835">user credentials:</st>

				*   **<st c="21852">The hashing process</st>**<st c="21872">: A one-way</st> <st c="21885">approach that involves generating a fixed-length passphrase of the</st> <st c="21952">original text.</st>
				*   **<st c="21966">The encryption process</st>**<st c="21989">: A two-way</st> <st c="22002">approach that involves generating a variable-length text using random symbols that can be traced back to its</st> <st c="22111">original text.</st>

			<st c="22125">The</st> `<st c="22130">ch09-api-bcrypt</st>` <st c="22145">and</st> `<st c="22150">ch09-auth-basic</st>` <st c="22165">projects use hashing to manage the passwords of a user.</st> <st c="22222">The</st> `<st c="22226">ch09-auth-basic</st>` <st c="22241">project utilizes Hashlib as its primary hashing library for passphrase generation.</st> <st c="22325">Flask has the</st> `<st c="22339">werkzeug.security</st>` <st c="22356">module that provides</st> `<st c="22378">generate_password_hash()</st>`<st c="22402">, a function that uses Hashlib’s</st> `<st c="22435">scrypt</st>` <st c="22441">algorithm to generate a passphrase from a text.</st> <st c="22490">The project’s</st> `<st c="22504">add_signup()</st>` <st c="22516">API endpoint function that utilizes the</st> `<st c="22557">werkzeug.security</st>` <st c="22574">module in generating the passphrase from the user’s password is</st> <st c="22639">as follows:</st>

从 werkzeug.security 模块导入 generate_password_hash @current_app.post('/login/signup')

async def add_signup():

login_json = request.get_json() <st c="22795">密码 = login_json["password"]</st><st c="22828">passphrase = generate_password_hash(password)</st> async with db_session() as sess:

    async with sess.begin():

    repo = LoginRepository(sess)

    login = Login(username=login_json["username"], <st c="23009">密码=passphrase</st>, role=login_json["role"])

    result = await repo.insert_login(login)

    if result == False:

        return jsonify(message="插入错误"), 201

    return jsonify(record=login_json), 200

			<st c="23200">The</st> `<st c="23205">generate_password_hash()</st>` <st c="23229">method has</st> <st c="23241">three parameters:</st>

				*   <st c="23258">The actual password is</st> <st c="23281">the</st> <st c="23286">first parameter.</st>
				*   <st c="23302">The hashing method is the second parameter with a default value</st> <st c="23367">of</st> `<st c="23370">scrypt</st>`<st c="23376">.</st>
				*   <st c="23377">The</st> **<st c="23382">salt</st>** <st c="23386">length is the</st> <st c="23401">third parameter.</st>

			<st c="23417">The salt length will determine the number of alphanumerics that the method will use to generate a salt.</st> <st c="23522">A salt is the additional alphanumerics with a fixed length that are added to the end of the password to make the passphrase more unbreachable or uncrackable.</st> <st c="23680">The process of adding salt to hashing</st> <st c="23717">is</st> <st c="23721">called</st> **<st c="23728">salting</st>**<st c="23735">.</st>
			<st c="23736">On the other hand, the</st> `<st c="23760">werkzeug.security</st>` <st c="23777">module also supports</st> `<st c="23799">pbkdf2</st>` <st c="23805">as an option for the hashing method parameter.</st> <st c="23853">However, it is less secure than the Scrypt algorithm.</st> <st c="23907">Scrypt is a simple and effective hashing algorithm that requires salt to hash a password.</st> <st c="23997">The</st> `<st c="24001">generate_password_hash()</st>` <st c="24025">method defaults the salt length to</st> `<st c="24061">16</st>`<st c="24063">, which can be replaced anytime by passing any preferred length.</st> <st c="24128">Moreover, Scrypt is memory intensive, since it needs storage to temporarily hold all the initial salted random alphanumerics</st> <st c="24252">until it returns the</st> <st c="24274">final passphrase.</st>
			<st c="24291">Since there is no way to re-assemble the passphrase to extract the original text, the</st> `<st c="24378">werkzeug.security</st>` <st c="24395">module has a</st> `<st c="24409">check_password_hash()</st>` <st c="24430">method that returns</st> `<st c="24451">True</st>` <st c="24455">if the given text value matches the hashed value.</st> <st c="24506">The following snippet validates the password of an authenticated user if it matches an account in the database with the same username but a</st> <st c="24646">hashed password:</st>

从 werkzeug.security 模块导入 check_password_hash @auth.verify_password

def verify_password(username, password):

task = get_user_task_wrapper.apply_async( args=[username])

login:Login = task.get()

if login == None:

    abort(403) <st c="24889">if check_password_hash(login.password, password)</st>: <st c="24940">return login.username</st> else:

    abort(403)

			<st c="24978">The</st> `<st c="24983">check_password_hash()</st>` <st c="25004">method requires two parameters, namely the passphrase as the first and the original password as the second.</st> <st c="25113">If the</st> `<st c="25120">werkzeug.security</st>` <st c="25137">module is not the option for your requirement due to its slowness, the</st> `<st c="25317">hashlib</st>` <st c="25324">module using the following</st> `<st c="25352">pip</st>` <st c="25355">command before</st> <st c="25371">applying it:</st>

pip install hashlib


			<st c="25403">On the other hand,</st> `<st c="25423">ch09-api-bcrypt</st>` <st c="25438">uses the Bcrypt algorithm to generate a passphrase for a password.</st> <st c="25506">Since</st> `<st c="25597">pip</st>` <st c="25600">command:</st>

pip install bcrypt


			<st c="25628">Afterward, instantiate the</st> `<st c="25656">Bcrypt</st>` <st c="25662">class container in the</st> `<st c="25686">create_app()</st>` <st c="25698">factory method and integrate the</st> <st c="25731">module into the Flask application through the</st> `<st c="25778">app</st>` <st c="25781">instance.</st> <st c="25792">The following snippet shows the setup of the Bcrypt module in the</st> <st c="25858">Flask application:</st>

从 flask_bcrypt 模块导入 Bcrypt

bcrypt = Bcrypt() def create_app(config_file):

app = Flask(__name__, template_folder='../modules/pages', static_folder='../modules/resources')

app.config.from_file(config_file, toml.load)

app.config.from_prefixed_env()

… … … … … … <st c="26140">bcrypt.init_app(app)</st> … … … … … …

			<st c="26171">The</st> `<st c="26176">bcrypt</st>` <st c="26182">object will provide every module component with the utility methods to hash credential details such as passwords.</st> <st c="26297">The following</st> `<st c="26311">ch09-api-bcrypt</st>`<st c="26326">’s version of the</st> `<st c="26345">add_signup()</st>` <st c="26357">endpoint</st> <st c="26366">hashes the password of an account using the imported</st> `<st c="26420">bcrypt</st>` <st c="26426">object before saving the user’s credentials into</st> <st c="26476">the database:</st>

从 modules 模块导入 bcrypt @current_app.post('/login/signup')

async def add_signup():

login_json = request.get_json()

password = login_json["password"] <st c="26642">passphrase = bcrypt.generate_password_hash(password)</st><st c="26694">.decode('utf-8')</st> async with db_session() as sess:

    async with sess.begin():

    repo = LoginRepository(sess)

    … … … … … …

    result = await repo.insert_login(login)

    if result == False:

        return jsonify(message="插入错误"), 201

    return jsonify(record=login_json), 200

			<st c="26955">Like Hashlib algorithms (e.g.,</st> `<st c="26987">scrypt</st>` <st c="26993">or</st> `<st c="26997">pbkdf2</st>`<st c="27003">), Bcrypt is not capable of extracting the original password from the passphrase.</st> <st c="27086">However, it also has a</st> `<st c="27109">check_password_hash()</st>` <st c="27130">method, which validates whether a password has the correct passphrase.</st> <st c="27202">However, compared to</st> <st c="27223">Hashlib, Bcrypt is more secure and modern because it uses the</st> **<st c="27285">Blowfish Cipher</st>** <st c="27300">algorithm.</st> <st c="27312">Its only drawback is its slow</st> <st c="27342">hashing process, which may affect the application’s</st> <st c="27394">overall performance.</st>
			<st c="27414">Aside from hashing, the encryption algorithms can also help secure the internal data of any Flask application, especially passwords.</st> <st c="27548">A well-known module that can provide reliable encryption methods is the</st> `<st c="27620">cryptography</st>` <st c="27632">module.</st> <st c="27641">So, let us first install the module using the following</st> `<st c="27697">pip</st>` <st c="27700">command before using its cryptographic recipes</st> <st c="27748">and utilities:</st>

pip install cryptography


			<st c="27787">The</st> `<st c="27792">cryptography</st>` <st c="27804">module offers both symmetric and asymmetric cryptography.</st> <st c="27863">The former uses one key to initiate the encryption and decryption algorithms, while the latter uses two keys: the public and private keys.</st> <st c="28002">Since our application only needs one key to encrypt user credentials, it will use symmetric cryptography through the</st> `<st c="28119">Fernet</st>` <st c="28125">class, the utility class that implements symmetric cryptography for the module.</st> <st c="28206">Now, after the installation, call</st> `<st c="28240">Fernet</st>` <st c="28246">in the</st> `<st c="28254">create_app()</st>` <st c="28266">method to generate a key through its</st> `<st c="28304">generate_key()</st>` <st c="28318">class</st> <st c="28325">method.</st> <st c="28333">The following snippet in the factory method shows how the application created and kept the key for the</st> <st c="28436">entire runtime:</st>

从 cryptography.fernet 模块导入 Fernet def create_app(config_file):

app = Flask(__name__, template_folder='../modules/pages', static_folder='../modules/resources')

app.config.from_file(config_file, toml.load)

… … … … … … <st c="28764">Fernet</st> 令牌或密钥是一个 URL 安全的 base64 编码的字母数字,它将启动加密和解密算法。应用程序应在启动时将密钥存储在安全且不可访问的位置,例如在安全目录内的文件中保存。缺少密钥将导致 <st c="29069">cryptography.fernet.InvalidToken</st> 和 <st c="29106">cryptography.exceptions.InvalidSignature</st> 错误。

        <st c="29154">生成密钥后,使用密钥作为构造函数参数实例化</st> `<st c="29204">Fernet</st>` <st c="29210">类以发出</st> `<st c="29270">encrypt()</st>` <st c="29279">方法。</st> <st c="29288">以下</st> `<st c="29302">ch09-auth-digest</st>`<st c="29318">版本的</st> `<st c="29333">add_signup()</st>` <st c="29345">使用</st> `<st c="29379">Fernet</st>`<st c="29385">加密用户密码</st>:<st c="29373">:</st>
<st c="29387">from cryptography.fernet import Fernet</st> @current_app.post('/login/signup')
async def add_signup():
     … … … … … …
     password = login_json["password"] <st c="29531">with open("enc_key.txt", mode="r") as file:</st><st c="29574">enc_key = bytes(file.read(), "utf-8")</st><st c="29612">fernet = Fernet(enc_key)</st><st c="29637">passphrase = fernet.encrypt(bytes(password, 'utf-8'))</st> async with db_session() as sess:
         async with sess.begin():
           … … … … … …
           result = await repo.insert_login(login)
           … … … … …
           return jsonify(record=login_json), 200
        <st c="29850">为了实例化</st> `<st c="29866">Fernet</st>`<st c="29872">,</st> `<st c="29874">add_signup()</st>` <st c="29886">必须从文件中提取令牌,将其转换为字节,并将其作为构造函数参数传递给</st> `<st c="29993">Fernet</st>` <st c="29999">类。</st> <st c="30007">`<st c="30011">Fernet</st>` <st c="30017">实例提供了一个</st> `<st c="30039">encrypt()</st>` <st c="30048">方法,该方法使用</st> `<st c="30174">decrypt()</st>` <st c="30183">方法从加密消息中提取原始</st> `<st c="30194">密码。</st>` <st c="30252">以下是</st> `<st c="30269">ch09-auth-digest</st>`<st c="30285">的密码验证方案,该方案从数据库中检索带有编码密码的用户凭据,并解密编码消息以提取</st> `<st c="30444">实际密码:</st>`
 @auth.get_password
def get_passwd(username):
    task = get_user_task_wrapper.apply_async( args=[username])
    login:Login = task.get() <st c="30590">with open("enc_key.txt", mode="r") as file:</st><st c="30633">enc_key = bytes(file.read(), "utf-8")</st><st c="30671">fernet = Fernet(enc_key)</st><st c="30696">password = fernet.decrypt(login.password)</st><st c="30738">.decode('utf-8')</st> if login == None:
        return None
    else:
        return password
        <st c="30806">再次强调,</st> `<st c="30814">get_passwd()</st>` <st c="30826">需要从文件中获取令牌以实例化</st> `<st c="30872">Fernet</st>`<st c="30878">。使用</st> `<st c="30890">Fernet</st>` <st c="30896">实例,</st> `<st c="30907">get_passwd()</st>` <st c="30919">可以发出</st> `<st c="30933">decrypt()</st>` <st c="30942">方法来重新组装加密消息并从</st> `<st c="31025">UTF-8</st>` <st c="31030">格式中提取实际密码。</st> <st c="31039">与哈希相比,加密涉及使用令牌将明文重新组装成不可读且无法破解的密文,并将该密文还原为其原始的可读形式。</st> <st c="31150">因此,它是一个双向过程,与</st> <st c="31262">哈希不同。</st>

        <st c="31273">如果目标是持久化编码数据到数据库中,而不添加可能减慢软件性能的不必要的加密错误,解决方案是</st> <st c="31441">使用</st> `<st c="31445">sqlalchemy_utils</st>`<st c="31461">。</st>

        <st c="31462">使用 sqlalchemy_utils 对加密列进行操作</st>

        <st c="31507">`sqlalchemy_utils`</st> <st c="31512">模块为 SQLAlchemy 模型类提供了额外的实用方法和列类型,其中包括</st> `<st c="31632">StringEncryptedType</st>`<st c="31651">。由于该模块使用了 cryptography 模块的加密方案,在使用</st> `<st c="31766">sqlalchemy_utils</st>` <st c="31782">之前,请务必使用以下</st> `<st c="31803">pip</st>` <st c="31806">命令安装该模块:</st>
 pip install cryptography sqlalchemy_utils
        <st c="31857">之后,通过将</st> `<st c="31907">StringEncryptedType</st>` <st c="31926">应用于需要</st> `<st c="31954">Fernet</st>`<st c="31960">加密的表列,例如</st> `<st c="31988">用户名</st>` <st c="31996">和</st> `<st c="32001">密码</st>` <st c="32009">列。</st> <st c="32019">以下是在</st> `<st c="32040">Login</st>` <st c="32045">模型类中包含</st> `<st c="32065">ch09-web-passphrase</st>` <st c="32084">项目,并具有</st> `<st c="32098">用户名</st>` <st c="32106">和</st> `<st c="32111">密码</st>` <st c="32119">列</st> <st c="32128">的</st> `<st c="32131">StringEncryptedType</st>`<st c="32150">的</st>示例:
<st c="32152">from sqlalchemy_utils import StringEncryptedType</st>
<st c="32200">enc_key = "packt_pazzword"</st> class Login(Base):
   __tablename__ = 'login'
   id = Column(Integer, Sequence('login_id_seq', increment=1), primary_key = True) <st c="32351">username = Column(StringEncryptedType(String(20), enc_key), nullable=False, unique=True)</st><st c="32439">password = Column(StringEncryptedType(String(50), enc_key), nullable=False)</st> role = Column(Integer, nullable=False)
   … … … … … …
        `<st c="32566">StringEncryptedType</st>` <st c="32586">会在</st> `<st c="32637">INSERT</st>` <st c="32643">事务期间自动加密列数据,并在</st> `<st c="32704">SELECT</st>` <st c="32710">语句中解密编码的字段值。</st> <st c="32723">要将实用类应用于列,请将其映射到包含实际 SQLAlchemy 列</st> `<st c="32830">类型</st>` <st c="32862">和自定义生成的</st> `<st c="32862">Fernet</st>` <st c="32868">令牌的列字段。</st> <st c="32876">它看起来像是一个列字段包装器,它将过滤和加密插入的字段值,并在检索时解密。</st> <st c="33003">对于这些字段值,不需要从</st> `<st c="33039">视图</st>` <st c="33043">函数或存储库层进行其他额外的编码来执行加密</st> `<st c="33109">和解密过程。</st>`

        <st c="33157">当使用</st> `<st c="33169">Flask-Migrate</st>`<st c="33182">时,在运行</st> `<st c="33243">db init</st>` <st c="33249">命令后,并在运行</st> `<st c="33254">script.py.mako</st>` <st c="33268">文件中的</st> `<st c="33286">migrations</st>` <st c="33296">文件夹内的</st> `<st c="33243">env.py</st>` <st c="33249">和</st> `<st c="33254">script.py.mako</st>` <st c="33268">文件之前,将</st> `<st c="33192">import sqlalchemy_utils</st>` <st c="33215">语句添加到生成的</st> `<st c="33243">env.py</st>` <st c="33249">和</st> `<st c="33254">script.py.mako</st>` <st c="33268">文件中。</st> <st c="33399">以下是在导入</st> `<st c="33484">sqlalchemy_utils</st>` <st c="33500">模块后修改的</st> `<st c="33430">ch09-web-passphrase</st>` <st c="33449">迁移文件:</st>
 (env.py)
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context <st c="33629">import sqlalchemy_utils</st> … … … … … …
(script.py.mako)
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa <st c="33838">import sqlalchemy_utils</st> ${imports if imports else ""}
… … … … … …
        <st c="33903">在提供的迁移文件中提供的突出显示的行是添加用于 SQLAlchemy 模型类的额外导入的正确位置。</st> <st c="34045">这包括不仅</st> `<st c="34072">sqlalchemy_util</st>` <st c="34087">类,还包括可能帮助建立所需</st> `<st c="34160">模型层</st>` 的其他库。</st>

        <st c="34172">在自定义用户认证时,应用程序利用默认的 Flask 会话来存储用户信息,例如用户名。</st> <st c="34307">此会话将信息保存到浏览器。</st> <st c="34354">为了保护应用程序免受破坏性访问控制攻击,您可以使用可靠的认证和授权机制,或者如果自定义基于会话的认证符合您的需求,可以通过</st> **<st c="34525">Flask-Session</st>** <st c="34538">模块应用服务器端会话处理。</st> <st c="34591">如果</st> <st c="34538">自定义会话基于认证</st> <st c="34591">满足您的需求。</st>

        <st c="34609">利用服务器端会话</st>

        <st c="34644">在</st> *<st c="34648">第四章</st>*<st c="34657">中,将</st> `<st c="35144">username</st>`<st c="35152">公开。</st>

        <st c="35168">在通过</st> `<st c="35190">Session</st>` <st c="35197">模块的</st> `<st c="35230">app</st>` <st c="35233">实例设置</st> `<st c="35261">session</st>` <st c="35268">字典对象后,Flask 的内置</st> `<st c="35261">session</st>` <st c="35268">会话对象可以轻松地在服务器端存储会话数据。</st> <st c="35338">以下</st> `<st c="35352">login_user()</st>` <st c="35364">视图函数在用户</st> <st c="35390">username</st> <st c="35398">凭证</st><st c="35415">确认后,将凭证</st> <st c="35456">credential</st> <st c="35467">存储到服务器端会话中:</st>
 @current_app.route('/login/auth', methods=['GET', 'POST'])
async def login_user():
    if request.method == 'GET':
        return render_template('login/authenticate.html'), 200
    username = request.form['username'].strip()
    password = request.form['password'].strip()
    async with db_session() as sess:
        async with sess.begin():
            repo = LoginRepository(sess)
            records = await repo.select_login_username_passwd(username, password)
            login_rec = [rec.to_json() for rec in records]
            if len(login_rec) >= 1:
                session["user"] = username
                return redirect('/menu')
            else:
                … … … … … …
                return render_template('login/authenticate.html'), 200
        <st c="36087">在登出时需要清除所有会话数据。</st> <st c="36143">删除所有会话数据将会</st> `<st c="36294">logout()</st>` <st c="36302">视图函数的</st> `<st c="36324">ch09-web-paraphrase</st>` <st c="36343">项目:</st>
 @current_app.route('/logout', methods=['GET'])
async def logout(): <st c="36420">session["user"] = None</st> return redirect('/login/auth')
        <st c="36473">除了将</st> <st c="36496">会话属性设置为</st> `<st c="36518">None</st>`<st c="36522">之外,</st> `<st c="36528">pop()</st>` <st c="36533">方法也可以帮助删除会话数据。</st> <st c="36602">删除所有会话数据等同于使当前会话失效。</st>

        <st c="36676">现在,如果自定义网页登录实现不符合您的需求,</st> **<st c="36757">Flask-Login</st>** <st c="36768">模块可以提供用户登录和登出的内置实用工具。</st> <st c="36832">现在让我们讨论如何使用 Flask-Login 进行</st> <st c="36880">Flask 应用程序。</st>

        <st c="36898">实现网页表单认证</st>

        `<st c="37184">flask-login</st>`<st c="37195">,首先使用以下</st> `<st c="37234">pip</st>` <st c="37237">命令安装它:</st>
 pip install flask-login
        <st c="37270">同时,安装并设置 Flask-Session 模块,以便 Flask-Login 将其用户会话存储在</st> <st c="37366">文件系统中。</st>

        <st c="37381">然后,要将 Flask-login 集成到 Flask 应用中,需要在</st> `<st c="37457">LoginManager</st>` <st c="37469">类中</st> `<st c="37483">create_app()</st>` <st c="37495">方法中实例化它,并通过</st> `<st c="37529">app</st>` <st c="37532">实例</st> `<st c="37543">设置它。</st> <st c="37581">session_protection</st>`<st c="37599">属性需要安装 Flask-Bcrypt,而</st> `<st c="37654">login_view</st>`<st c="37664">属性则指定了</st> `<st c="37687">登录视图</st>` <st c="37697">函数。</st> <st c="37708">以下代码片段</st> <st c="37730">展示了为我们的</st> `<st c="37769">ch09-web-login</st>` <st c="37783">项目</st> `<st c="37730">设置 Flask-Login 的方法:</st>
<st c="37792">from flask_login import LoginManager</st> def create_app(config_file):
    app = Flask(__name__,template_folder= '../modules/pages', static_folder=   '../modules/resources')
    app.config.from_file(config_file, toml.load)
    app.config.from_prefixed_env()
    … … … … … … <st c="38224">Login</st> model class to the model layer through your desired ORM and sub-class it with the <st c="38312">UserMixin</st> helper class of the <st c="38342">flask-login</st> module. The following is the <st c="38383">Login</st> model of our project that will persist the user’s <st c="38439">id</st>, <st c="38443">username</st>, <st c="38453">passphrase</st>, and <st c="38469">role</st>:

from flask_login import UserMixin from sqlalchemy_utils import StringEncryptedType

enc_key = "packt_pazzword" class Login(UserMixin, Base): tablename = 'login'

id = Column(Integer, Sequence('login_id_seq', increment=1), primary_key = True)

username = Column(StringEncryptedType(String(20), enc_key), nullable=False, unique=True)

password = Column(StringEncryptedType(String(50), enc_key), nullable=False)

role = Column(Integer, nullable=False)

… … … … … …


			<st c="38934">Instead of utilizing the Flask-Bcrypt module, our application uses the built-in</st> `<st c="39015">StringEncryptedType</st>` <st c="39034">hashing mechanism from the</st> `<st c="39062">sqlalchemy_utils</st>` <st c="39078">module.</st> <st c="39087">Now, the use of the</st> `<st c="39107">UserMixin</st>` <st c="39116">superclass allows the use of some properties, such as</st> `<st c="39171">is_authenticated</st>`<st c="39187">,</st> `<st c="39189">is_active</st>`<st c="39198">, and</st> `<st c="39204">is_anonymous</st>`<st c="39216">, as well as some utility methods, such</st> <st c="39255">as</st> `<st c="39259">get_id ()</st>`<st c="39268">, provided by the</st> `<st c="39286">current_user</st>` <st c="39298">object from the</st> `<st c="39315">flask_login</st>` <st c="39326">module.</st>
			<st c="39334">Flask-Login stores the</st> `<st c="39358">id</st>` <st c="39360">of a user in the session after a successful login authentication.</st> <st c="39427">With</st> `<st c="39432">Flask-Session</st>`<st c="39445">, it will store the</st> `<st c="39465">id</st>` <st c="39467">somewhere that has been secured.</st> <st c="39501">The</st> `<st c="39505">id</st>` <st c="39507">is vital to the</st> `<st c="39702">id</st>` <st c="39704">from the session, retrieves the object using its</st> `<st c="39754">id</st>` <st c="39756">parameter and a repository class, and returns the</st> `<st c="39807">Login</st>` <st c="39812">object to the application.</st> <st c="39840">Here is</st> `<st c="39848">ch09-web-login</st>`<st c="39862">’s implementation for the</st> <st c="39889">user loader:</st>

@login_auth.user_loader def load_user(id):

task = get_user_task_wrapper.apply_async(args=[id])

result = task.get() <st c="40077">main.py</st> 模块。现在,使用 <st c="40108">get_user_task_wrapper()</st> Celery 任务,<st c="40149">load_user()</st> 使用 <st c="40170">select_login()</st> 事务的 <st c="40204">LoginRepository</st> 来根据给定的 <st c="40234">id</st> 参数检索一个 <st c="40266">Login</st> 记录。应用程序会自动为每个请求访问视图而调用 <st c="40316">load_user()</st>。对回调函数的连续调用检查用户的合法性。返回的 <st c="40458">Login</st> 对象作为令牌,允许用户访问应用程序。要声明用户加载回调函数,创建一个带有本地 <st c="40615">id</st> 参数的函数,并用 <st c="40653">userloader()</st> 装饰器装饰 <st c="40683">LoginManager</st> 实例。

        <st c="40705">登录视图</st> `<st c="40710">功能</st>` <st c="40720">缓存</st> `<st c="40741">登录</st>` <st c="40746">对象,保存</st> `<st c="40765">登录</st>`<st c="40770">的</st> `<st c="40774">id</st>` <st c="40776">到会话中,并将其映射到</st> `<st c="40812">当前用户</st>` <st c="40824">的</st> `<st c="40845">flask_login</st>` <st c="40856">模块的代理对象。</st> <st c="40865">以下代码片段</st> <st c="40887">显示了</st> `<st c="40897">登录视图</st>` <st c="40907">功能,该功能在我们的</st> `<st c="40930">设置</st>` <st c="40930">中指定:</st>
<st c="40940">from flask_login import login_user</st> @current_app.route('/login/auth', methods=['GET', 'POST']) <st c="41035">async def login_valid_user():</st> if request.method == 'GET':
     return render_template('login/authenticate.html'), 200
  username = request.form['username'].strip()
  password = request.form['password'].strip()
  async with db_session() as sess:
      async with sess.begin():
         repo = LoginRepository(sess)
         records = await repo.select_login_username_passwd( username, password)
         if(len(records) >= 1): <st c="41417">login_user(records[0])</st> return render_template('login/signup.html'), 200
         else:
            … … … … … …
           return render_template( 'login/authenticate.html'), 200
        <st c="41562">如果根据数据库验证,</st> `<st c="41570">POST</st>`<st c="41574">-提交的用户凭据是正确的,那么</st> `<st c="41653">登录视图</st>` <st c="41663">功能,即我们的</st> `<st c="41685">login_valid_user()</st>`<st c="41703">,应该调用</st> `<st c="41723">login_user()</st>` <st c="41735">模块的</st> `<st c="41750">flask_login</st>` <st c="41761">方法。</st> <st c="41770">视图必须将包含用户登录凭据的查询</st> `<st c="41801">登录</st>` <st c="41806">对象传递给</st> `<st c="41861">login_user()</st>` <st c="41873">函数。</st> <st c="41884">除了</st> `<st c="41899">登录</st>` <st c="41904">对象之外,该方法还有其他选项,例如</st> <st c="41951">以下内容:</st>

            +   `<st c="41965">记住</st>`<st c="41974">: 一个布尔</st> <st c="41987">参数,用于启用</st> `<st c="42014">记住我</st>` <st c="42025">功能,该功能允许用户会话在浏览器意外退出后仍然保持活跃。</st>

            +   `<st c="42116">fresh</st>`<st c="42122">: 一个布尔参数,用于将用户设置为</st> `<st c="42165">未新鲜</st>` <st c="42174">如果他们的会话在浏览器关闭后立即有效。</st>

            +   `<st c="42238">强制</st>`<st c="42244">: 一个布尔参数,用于强制用户登录。</st>

            +   `<st c="42302">duration</st>`<st c="42311">: 在</st> `<st c="42344">remember_me</st>` <st c="42356">cookie 过期之前的时间。</st>

        <st c="42371">在成功认证之后,用户现在可以访问受限视图或 API,这些视图或 API 对未认证用户不可用:那些带有</st> `<st c="42524">@login_required</st>` <st c="42539">装饰器的视图。</st> <st c="42551">以下是我们</st> `<st c="42599">ch09-web-login</st>` <st c="42613">的示例视图函数,它需要认证</st> <st c="42639">用户访问:</st>
 from flask_login import login_required
@current_app.route('/doctor/profile/add', methods=['GET', 'POST'])
@login_required
async def add_doctor_profile():
    if request.method == 'GET':
        async with db_session() as sess:
            async with sess.begin():
                repo = LoginRepository(sess)
                records = await repo.select_all_doctor()
                doc_rec = [rec.to_json() for rec in records]
                return render_template('doctor/add_doctor_profile.html', docs=doc_rec), 200
    else:
        username = request.form['username']
        … … … … … …
        return render_template('doctor/add_doctor_profile.html', doctors=doc_rec), 200
        <st c="43215">除了装饰器之外,</st> `<st c="43246">当前用户</st>`<st c="43258">的</st> `<st c="43262">is_authenticated</st>` <st c="43278">属性还可以限制视图和</st> `<st c="43320">Jinja 模板</st>` <st c="43360">中某些代码片段的执行。</st>

        <st c="43376">最后,为了完成 Flask-Login 的集成,实现一个</st> <st c="43468">logout_user()</st>` <st c="43481">的 flask_login</st> <st c="43497">模块的实用工具。</st> <st c="43517">以下代码是我们项目的注销视图实现:</st>
<st c="43586">from flask_login import logout_user</st> @current_app.route("/logout") <st c="43653">async def logout():</st><st c="43672">logout_user()</st> return <st c="43694">redirect(url_for('login_valid_user'))</st>
        <st c="43731">确保注销视图将用户重定向到登录视图页面,而不是仅仅渲染登录页面,以</st> <st c="43846">避免</st> **<st c="43853">HTTP 状态码 405</st>** <st c="43873">(</st>*<st c="43875">方法不允许</st>*<st c="43893">)</st> <st c="43896">在重新登录期间。</st>

        <st c="43912">拥有一个安全的</st> <st c="43934">Web 表单认证能否防止 CSRF 攻击的发生?</st> <st c="43996">让我们专注于保护我们的应用程序免受那些想要将事务转移到其他</st> <st c="44096">可疑网站上的攻击者。</st>

        <st c="44113">防止 CSRF 攻击</st>

        <st c="44137">CSRF 攻击是一种攻击方式,通过欺骗已认证用户将敏感数据转移到隐藏和恶意网站。</st> <st c="44255">这种攻击发生在</st> <st c="44274">用户执行</st> `<st c="44294">POST</st>`<st c="44298">,</st> `<st c="44300">DELETE</st>`<st c="44306">,</st> `<st c="44308">PUT</st>`<st c="44311">, 或</st> `<st c="44316">PATCH</st>` <st c="44321">事务时,此时表单数据被检索并提交到应用程序。</st> <st c="44402">在 Flask 中,最常用的解决方案是使用</st> `<st c="44447">Flask-WTF</st>` <st c="44456">,因为它有一个内置的</st> `<st c="44483">CSRFProtect</st>` <st c="44494">类,可以全局保护应用程序的每个表单事务。</st> <st c="44567">一旦启用,</st> `<st c="44581">CSRFProtect</st>` <st c="44592">允许为每个表单事务生成唯一的令牌。</st> <st c="44660">那些不会生成令牌的表单提交将导致</st> `<st c="44725">CSRFProtect</st>` <st c="44736">触发错误消息,检测到</st> <st c="44778">CSRF 攻击。</st>

        *<st c="44790">第四章</st>* <st c="44800">强调了在 Flask 应用程序中设置 Flask-</st>`<st c="44835">WTF</st>` <st c="44839">模块。</st> <st c="44871">安装后,导入</st> `<st c="44902">CSRFProtect</st>` <st c="44913">并在</st> `<st c="44936">create_app()</st>`<st c="44948">中实例化它,如下面的</st> <st c="44976">代码片段</st>所示:
<st c="44989">from flask_wtf.csrf import CSRFProtect</st> def create_app(config_file):
    app = Flask(__name__,template_folder= '../modules/pages', static_folder= '../modules/resources')
    … … … … … … <st c="45223">SECRET_KEY</st> or <st c="45237">WTF_CSRF_SECRET_KEY</st> to be defined in the configuration file. Now, after integrating it into the application through the <st c="45357">app</st> instance, all <st c="45375"><form></st> in Jinja templates must have the <st c="45415">{{ csrf_token() }}</st> component or a <st c="45449"><input type="hidden" name="csrf_token" value = "{{ csrf_token() }}" /></st> component to impose CSRF protection.
			<st c="45556">If there is no plan to use the entire</st> `<st c="45595">Flask-WTF</st>`<st c="45604">, another</st> <st c="45614">option is to apply</st> **<st c="45633">Flask-Seasurf</st>** <st c="45646">instead.</st>
			<st c="45655">After showcasing the web-based authentication strategies, it is now time to discuss the different authentication types</st> <st c="45774">for</st> <st c="45779">API-based applications.</st>
			<st c="45802">Implementing user authentication and authorization</st>
			<st c="45853">There is a strong foundation of extension modules that can secure API services from unwanted access, such as the</st> `<st c="46426">Flask-HTTPAuth</st>` <st c="46440">module in</st> <st c="46451">our application.</st>
			<st c="46467">Utilizing the Flask-HTTPAuth module</st>
			<st c="46503">After you have installed the</st> `<st c="46533">Flask-HTTPAuth</st>` <st c="46547">module and its extensions, it can provide its</st> `<st c="46594">HTTPBasicAuth</st>` <st c="46607">class to</st> <st c="46617">build Basic authentication, the</st> `<st c="46649">HTTPDigestAuth</st>` <st c="46663">class to implement Digest authentication, and the</st> `<st c="46714">HTTPTokenAuth</st>` <st c="46727">class for the Bearer token</st> <st c="46755">authentication scheme.</st>
			<st c="46777">Basic authentication</st>
			<st c="46798">Basic authentication requires</st> <st c="46829">an unencrypted base64 format of the user’s</st> `<st c="46872">username</st>` <st c="46880">and</st> `<st c="46885">password</st>` <st c="46893">credentials through the</st> `<st c="46918">Authorization</st>` <st c="46931">request</st> <st c="46939">header.</st> <st c="46948">To implement this authentication type in Flask, instantiate the module’s</st> `<st c="47021">HTTPBasicAuth</st>` <st c="47034">in</st> `<st c="47038">create_app()</st>` <st c="47050">and register the instance to the Flask</st> `<st c="47090">app</st>` <st c="47093">instance, as shown in the</st> <st c="47120">following snippet:</st>

from flask_httpauth import HTTPBasicAuth def create_app(config_file):

app = Flask(__name__,template_folder= '../modules/pages', static_folder= '../modules/resources')

app.config.from_file(config_file, toml.load)

… … … … … … <st c="47408">HTTPBasicAuth</st> 实现需要一个回调函数,该函数将从客户端检索用户名和密码,查询数据库以检查用户记录,并在存在有效用户的情况下将有效用户名返回给应用程序,如下面的代码所示:
 app, celery_app, <st c="47681">auth</st> = <st c="47688">create_app('../config_dev.toml')</st> … … … … … … <st c="47732">@auth.verify_password</st> def verify_password(<st c="47774">username, password</st>): <st c="47797">task = get_user_task_wrapper.apply_async( args=[username])</st><st c="47855">login:Login = task.get()</st> if login == None: <st c="47899">abort(403)</st><st c="47909">if check_password_hash(login.password,password) == True:</st><st c="47966">return login.username</st> else: <st c="48012">HTTPBasicAuth</st>’s callback function, the given <st c="48058">check_password()</st> must have the <st c="48089">username</st> and <st c="48102">password</st> parameters and should be annotated with <st c="48151">HTTPBasicAuth</st>’s <st c="48168">verify_password()</st> decorator. Our callback uses the Celery task to search and retrieve the <st c="48258">Login</st> object containing the <st c="48286">username</st> and <st c="48299">password</st> details and raises <st c="48481">HTTPBasicAuth</st>’s <st c="48498">login_required()</st> decorator.
			<st c="48525">The</st> `<st c="48530">Flask-HTTPAuth</st>` <st c="48544">module has built-in authorization</st> <st c="48579">support.</st> <st c="48588">If the basic authentication needs a</st> <st c="48623">role-based authorization, the application only needs to have a separate callback function decorated by</st> `<st c="48727">get_user_roles()</st>` <st c="48743">from the</st> `<st c="48753">HTTPBasicAuth</st>` <st c="48766">class.</st> <st c="48774">The following is</st> `<st c="48791">ch09-auth-basic</st>`<st c="48806">’s callback function for retrieving user roles from</st> <st c="48859">its users:</st>

from werkzeug.datastructures.auth import Authorization app, celery_app, auth = create_app('../config_dev.toml') … … … … … … @auth.get_user_roles def get_scope(user:Authorization):

task = get_user_task_wrapper.apply_async( args=[user.username])

login:Login = task.get() <st c="49173">get_scope()</st> 自动从请求中检索 <st c="49213">werkzeug.datastructures.auth.Authorization</st> 对象。该 <st c="49285">Authorization</st> 对象包含 <st c="49319">username</st>,这是 <st c="49341">get_user_task_wrapper()</st> Celery 任务将基于其从数据库中搜索用户的 <st c="49406">Login</st> 记录对象的基础。回调函数的返回值可以是字符串格式的单个角色,也可以是分配给用户的角色列表。来自 <st c="49623">ch09-auth-digest</st> 项目的 <st c="49590">del_doctor_profile_id()</st> 不允许任何经过身份验证的用户,除了那些 <st c="49713">role</st> 等于 <st c="49739">1</st> 的医生:
 @current_app.delete('/doctor/profile/delete/<int:id>') <st c="49802">@auth.login_required(role="1")</st> async def del_doctor_profile_id(id:int):
    async with db_session() as sess:
      async with sess.begin():
          repo = DoctorRepository(sess)
          … … … … … …
          return jsonify(record="deleted record"), 200
        <st c="50018">在这里,</st> `<st c="50025">del_doctor_profile_id()</st>` <st c="50048">是一个 API 函数,用于在数据库中删除医生的个人信息。</st> <st c="50129">只有医生本人(</st>`<st c="50181">role=1</st>`<st c="50188">)才能执行此交易。</st>

        <st c="50207">摘要认证</st>

        <st c="50229">另一方面,该模块的</st> `<st c="50262">HTTPDigestAuth</st>` <st c="50276">为基于 API 的应用程序构建摘要认证方案,该方案</st> <st c="50352">加密了凭证以及一些附加头信息,例如</st> `<st c="50441">realm</st>`<st c="50446">,</st> `<st c="50448">nonce</st>`<st c="50453">,</st> `<st c="50455">opaque</st>`<st c="50461">,以及</st> `<st c="50467">nonce count</st>`<st c="50478">。因此,它比</st> <st c="50509">基本认证方案</st> <st c="50542">更安全。</st> <st c="50542">以下代码片段展示了如何在</st> `<st c="50613">create_app()</st>` <st c="50625">工厂中设置摘要认证:</st>
<st c="50634">from flask_httpauth import HTTPDigestAuth</st> def create_app(config_file):
    app = Flask(__name__,template_folder= '../modules/pages', static_folder= '../modules/resources')
    app.config.from_file(config_file, toml.load)
    … … … … … … <st c="50900">HTTPDigestAuth</st>’s constructors have five parameters, two of which have default values, namely <st c="50994">qop</st> and <st c="51002">algorithm</st>. The <st c="51061">auth</st> value, which means that the application is at the basic protection level of the digest scheme. So far, the highest protection level is <st c="51201">auth-int</st>, which is, at the time of writing this book, not yet functional in the <st c="51281">Flask-HTTPAuth</st> module. The other parameter, <st c="51325">algorithm</st>, has the <st c="51344">md5</st> default value for the encryption, but the requirement can change it to <st c="51419">md5-sess</st> or any supported encryption method. Now, the three other optional parameters are the following:

				*   `<st c="51523">realm</st>`<st c="51529">: This contains the</st> `<st c="51550">username</st>` <st c="51558">of the user and the name of the</st> <st c="51591">application’s host.</st>
				*   `<st c="51610">scheme</st>`<st c="51617">: This is a replacement to the default value of the</st> `<st c="51670">Digest scheme</st>` <st c="51683">header in the</st> `<st c="51698">WWW-Authenticate</st>` <st c="51714">response.</st>
				*   `<st c="51724">use_hw1_pw</st>`<st c="51735">: If this is set to</st> `<st c="51756">True</st>`<st c="51760">, the</st> `<st c="51766">get_password()</st>` <st c="51780">callback function must return a</st> <st c="51813">hashed password.</st>

			<st c="51829">In digest authentication, the user must submit their username, password, nonce, opaque, and nonce count to the application for verification.</st> *<st c="51971">Figure 9</st>**<st c="51979">.4</st>* <st c="51981">shows a postman client submitting the header information to the</st> `<st c="52046">ch09-auth-digest</st>` <st c="52062">app:</st>
			![Figure 9.4 – The Digest authentication scheme’s additional headers](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_09_004.jpg)

			<st c="52864">Figure 9.4 – The Digest authentication scheme’s additional headers</st>
			<st c="52930">A nonce is a unique base64 or hexadecimal string that the server generates for every</st> **<st c="53016">HTTP status code 401</st>** <st c="53036">response.</st> <st c="53047">The content of the compressed string, usually the estimated timestamp when the client</st> <st c="53133">received the response, must be unique to</st> <st c="53174">every access.</st>
			<st c="53187">Also specified by the server is</st> **<st c="53220">opaque</st>**<st c="53226">, a base64 or hexadecimal string value that the client needs to return to the</st> <st c="53304">server for validation if it is the same value as</st> <st c="53353">generated before.</st>
			<st c="53370">The nonce count value or</st> `<st c="53463">0000001</st>`<st c="53470">, that checks the integrity of the user credentials and protects data from playback attacks.</st> <st c="53563">The server increments its copy of the nc-value when it receives the same nonce value from a new request.</st> <st c="53668">Every authentication request must bear a new nonce value.</st> <st c="53726">Otherwise, it is</st> <st c="53743">a replay.</st>
			`<st c="53752">Flask-HTTPAuth</st>`<st c="53767">’s digest authentication scheme will only work if our API application provides the following</st> <st c="53861">callback implementations:</st>

server_nonce = "H9OVSzjcB57StMQFPInmX22uZ0Kwu_4JptsWrj0oPpU"

server_opaque = "XJIXDX615CMGXXL0COHQQ0IJRG33OFTNGNFYT72VJ8XF5U3RYZ"

@auth.generate_nonce def gen_nonce():

return server_nonce <st c="54075">@auth.generate_opaque</st> def gen_opaque():

return server_opaque

			<st c="54135">The application must generate the nonce and opaque values using the likes of the</st> `<st c="54217">gen_nonce()</st>` <st c="54228">and</st> `<st c="54233">gen_opaque()</st>` <st c="54245">callbacks.</st> <st c="54257">These are just trivial implementations of the methods in our</st> `<st c="54318">ch09-auth-digest</st>` <st c="54334">application and need better solutions that use a UUID generator</st> <st c="54398">or a</st> `<st c="54404">secrets</st>` <st c="54411">module to generate the values.</st> <st c="54443">The nonce generator callback must have a</st> `<st c="54484">generate_nonce()</st>` <st c="54500">decorator, while the opaque generator must be decorated by the</st> `<st c="54564">generate_opaque()</st>` <st c="54581">annotation.</st>
			<st c="54593">Aside from these generators, the authentication scheme also needs the following validators of nonce and opaque values that the server needs from the</st> <st c="54743">client request:</st>

@auth.verify_nonce def verify_once(nonce):

if nonce == server_nonce:

    return True

else:

    return False <st c="54859">@auth.verify_opaque</st> def verify_opaque(opaque):

if opaque == server_opaque:

    return True

else:

    return False

			<st c="54964">The validators check whether the values from the request are correct based on the server’s corresponding values.</st> <st c="55078">Now, the last</st> <st c="55092">requirement for the digest authentication to work is the</st> `<st c="55149">get_password()</st>` <st c="55163">callback that</st> <st c="55178">retrieves the password from the client for database validation of the user’s existence, as shown in the</st> <st c="55282">following snippet:</st>

@auth.get_password def get_passwd(username):

print(username)

task = get_user_task_wrapper.apply_async(args=[username])

login:Login = task.get()

… … … … … …

if login == None: <st c="55475">return None</st> else: <st c="55548">get_password()</st> 方法每次访问 API 资源时都会调用,并提供有效用户的密码作为令牌。

        <st c="55652">Bearer 令牌认证</st>

        <st c="55680">除了基本和摘要之外,</st> `<st c="55714">Flask-HTTPAuth</st>` <st c="55728">模块还通过使用</st> `<st c="55806">HTTPTokenAuth</st>` <st c="55819">类</st> <st c="55827">支持</st> `<st c="55841">create_app()</st>` <st c="55853">片段</st> <st c="55861">的</st> `<st c="55869">ch09-auth-token</st>` <st c="55884">项目</st> <st c="55897">设置</st> `<st c="55912">Bearer</st>` <st c="55912">令牌认证:</st>
<st c="55933">from flask_httpauth import HTTPTokenAuth</st> def create_app(config_file):
    app = Flask(__name__,template_folder= '../modules/pages', static_folder=   '../modules/resources')
    app.config.from_file(config_file, toml.load)
    … … … … … … <st c="56236">token</st> column field of the <st c="56262">string</st> type in the <st c="56281">Login</st> model to persist the token associated with the user. The application generates the token after the user signs up for login credentials. Our application uses the <st c="56448">PyJWT</st> module for token generation, as depicted in the following endpoint function:

from jwt import encode

@current_app.post('/login/signup') async def add_signup():

login_json = request.get_json()

password = login_json["password"]

passphrase = generate_password_hash(password) <st c="56725">token = encode({'username': login_json["username"],</st> <st c="56776">'exp': int(time()) + 3600},</st> <st c="56804">current_app.config['SECRET_KEY'],</st> <st c="56838">algorithm='HS256')</st> async with db_session() as sess:

    async with sess.begin():

        repo = LoginRepository(sess)

        login = Login(username=login_json["username"], password=passphrase, <st c="57013">token=token</st>,   role=login_json["role"])

        result = await repo.insert_login(login)

        … … … … … …

        return jsonify(record=login_json), 200

			<st c="57141">The token’s</st> `<st c="57175">username</st>` <st c="57183">and the token’s supposed expiration time in seconds.</st> <st c="57237">The encoding indicated in the given</st> `<st c="57273">add_signup()</st>` <st c="57285">API method is the</st> `<st c="57371">SECRET_KEY</st>`<st c="57381">. The token is always part of the</st> <st c="57414">request’s</st> `<st c="57425">Authorization</st>` <st c="57438">header with the</st> `<st c="57455">Bearer</st>` <st c="57461">value.</st> <st c="57469">Now, a callback function retrieves the bearer token from the request and checks whether it is the saved token of the user.</st> <st c="57592">The</st> <st c="57596">following is</st> `<st c="57609">ch09-auth-token</st>`<st c="57624">’s callback</st> <st c="57637">function implementation:</st>

from jwt import decode

@auth.verify_token def verify_token(token):

try: <st c="57734">data = decode(token, app.config['SECRET_KEY'],</st><st c="57780">algorithms=['HS256'])</st> except:

    return False

if 'username' in data:

    return data['username']

			<st c="57870">The Bearer token’s callback function must have the</st> `<st c="57922">verify_token()</st>` <st c="57936">method decorator.</st> <st c="57955">It has the</st> `<st c="57966">token</st>` <st c="57971">parameter and it returns either a boolean value or the username.</st> <st c="58037">It must use the same</st> `<st c="58155">PyJWT</st>` <st c="58160">module encodes and decodes</st> <st c="58188">the token.</st>
			<st c="58198">Like basic, the digest and bearer token</st> <st c="58239">authentication schemes use the</st> `<st c="58270">login_required()</st>` <st c="58286">decorator to impose restrictions on API endpoints.</st> <st c="58338">Also, both can implement role-based authorization</st> <st c="58388">with the</st> `<st c="58397">get_user_roles()</st>` <st c="58413">callback.</st>
			<st c="58423">The next Flask module,</st> `<st c="58447">Authlib</st>`<st c="58454">, has core classes and methods for implementing OAuth2, OpenID Connect, and JWT Token-based authentication schemes.</st> <st c="58570">Let us now</st> <st c="58581">showcase it.</st>
			<st c="58593">Utilizing the Authlib module</st>
			`<st c="59046">authlib</st>` <st c="59053">module, install its module using the following</st> `<st c="59101">pip</st>` <st c="59104">command:</st>

pip install authlib


			<st c="59133">If the application to secure is not running on an HTTPS protocol, set the</st> `<st c="59208">AUTHLIB_INSECURE_TRANSPORT</st>` <st c="59234">environment variable to</st> `<st c="59259">1</st>` <st c="59260">or</st> `<st c="59264">True</st>` <st c="59268">for Authlib to work because it is for a</st> <st c="59309">secured environment.</st>
			<st c="59329">Unlike the HTTP Basic, Digest, and Bearer Token authentication schemes, the OAuth2.0 scheme uses an authorization server that provides several endpoints for authorization procedures, as well as issuing tokens, refreshing tokens, and revoking tokens.</st> <st c="59580">The authorization server is always part of an application that protects its resources from malicious access and attacks.</st> <st c="59701">Our</st> `<st c="59705">ch09-oauth2-password</st>` <st c="59725">project implements the Vaccine and Reports applications with the OAuth2 Resource Owner Password authorization scheme using Authlib.</st> <st c="59858">The following</st> `<st c="59872">create_app()</st>` <st c="59884">factory method shows how to set up</st> <st c="59920">this scheme:</st>

from authlib.integrations.flask_oauth2 import AuthorizationServer

from authlib.integrations.flask_oauth2 import ResourceProtector from modules.security.oauth2_config import PasswordGrant, query_client, save_token

require_oauth = ResourceProtector()

oauth_server = AuthorizationServer() def create_app(config_file):

app = Flask(__name__,template_folder= '../modules/pages', static_folder=   '../modules/resources')

… … … … … …

oauth_server.init_app(app, query_client=<st c="60397">query_client</st>, save_token=<st c="60423">save_token</st>)

oauth_server.register_grant(<st c="60485">AuthorizationServer</st> 类管理应用程序的认证请求和响应。它提供了适合应用程序强制执行的认证授予的不同类型的端点。现在,实例化该类是构建客户端或其他应用程序的 OAuth2 授权服务器的第一步。它需要 <st c="60830">query_client()</st> 和 <st c="60849">save_token()</st> 来进行令牌生成和授权机制的授权类型。

        <st c="60937">Authlib</st> <st c="60946">提供</st> `<st c="60959">ResourceOwnerPasswordCredentialsGrant</st>` <st c="60996">类以实现</st> `<st c="61133">authenticate_user()</st>` <st c="61152">在执行 <st c="61197">query_client()</st> 和 <st c="61211">以及 <st c="61216">save_token()</st> <st c="61228">方法</st> 之前进行验证。</st> <st c="61238">以下代码片段显示了</st> `<st c="61270">ResourceOwnerPasswordCredentialsGrant</st>` <st c="61307">子类,它是我们</st> `<st c="61324">ch09-oauth2-password</st>` <st c="61344">项目的:</st>
<st c="61353">from authlib.oauth2.rfc6749.grants import</st> <st c="61395">ResourceOwnerPasswordCredentialsGrant</st> class PasswordGrant(<st c="61454">ResourceOwnerPasswordCredentialsGrant</st>): <st c="61496">TOKEN_ENDPOINT_AUTH_METHODS</st> = [
      'client_secret_basic', 'client_secret_post' ]
    def authenticate_user(self, <st c="61602">username</st>, <st c="61612">password</st>):
         task = get_user_task_wrapper.apply_async(args=[username])
         login:Login = task.get()
         if login is not None and check_password_hash( login.password, password) == True: <st c="61805">PasswordGrant</st> custom class is in the <st c="61842">/modules/security/oauth2_config.py</st> module with the <st c="61893">query_client()</st> and <st c="61912">save_token()</st> authorization server methods. The first component of <st c="61978">PasswordGrant</st> to configure is its <st c="62012">TOKEN_ENDPOINT_AUTH_METHODS</st>, which, from its default <st c="62065">public</st> value, needs to be set to <st c="62098">client_secret_basic</st>, <st c="62119">client_secret_post</st>, or both. The <st c="62152">client_secret_basic</st> is a client authentication that passes client secrets through a basic authentication scheme, while <st c="62271">client_secret_post</st> utilizes form parameters to pass client secrets to the authorization server. On the other hand, the overridden <st c="62401">authenticate_user()</st> retrieves the <st c="62435">username</st> and <st c="62448">password</st> from the token generator endpoint through basic authentication or form submission. It also retrieves the <st c="62562">Login</st> record object from the database through a <st c="62610">get_user_task_wrapper()</st> Celery task and validates the <st c="62664">Login</st>’s hashed password with the retrieved password from the client. The method returns the <st c="62757">Login</st> object that will signal the execution of the <st c="62808">query_client()</st> method. The following snippet shows our <st c="62863">query_client()</st> implementation:

def query_client(client_id):

task = get_client_task_wrapper.apply_async(args=[<st c="62975">client_id</st>])

client:Client = task.get()

return client

			<st c="63029">The</st> `<st c="63034">query_client()</st>` <st c="63048">is a necessary method of the</st> `<st c="63078">AuthorizationServer</st>` <st c="63097">instance.</st> <st c="63108">Its goal is to find the client who requested the authentication and return the</st> `<st c="63187">Client</st>` <st c="63193">object.</st> <st c="63202">It retrieves</st> <st c="63215">the</st> `<st c="63219">client_id</st>` <st c="63228">from the</st> `<st c="63238">AuthorizationServer</st>` <st c="63258">endpoint and uses it to search for the</st> `<st c="63297">Client</st>` <st c="63303">object from the database.</st> <st c="63330">The following snippet shows how to build the</st> `<st c="63375">Client</st>` <st c="63381">blueprint with</st> `<st c="63397">Authlib</st>`<st c="63404">’s</st> `<st c="63408">OAuth2ClientMixin</st>`<st c="63425">:</st>

from authlib.integrations.sqla_oauth2 import OAuth2ClientMixin class Client(Base, OAuth2ClientMixin):

__tablename__ = 'oauth2_client'

id = Column(Integer, Sequence('oauth2_client_id_seq', increment=1), primary_key = True)

user_id = Column(String(20), ForeignKey('login.username'), unique=True)

login = relationship('Login', back_populates="client")

… … … … … …

			<st c="63788">Authlib’s</st> `<st c="63799">OAuth2ClientMixin</st>` <st c="63816">will pad all the necessary column fields to the model class, including those that are optional.</st> <st c="63913">The required pre-tokenization fields, such as</st> `<st c="63959">id</st>`<st c="63961">,</st> `<st c="63963">user_id</st>` <st c="63970">or</st> `<st c="63974">username</st>`<st c="63982">,</st> `<st c="63984">client_id</st>`<st c="63993">,</st> `<st c="63995">client_id_issued_at</st>`<st c="64014">, and</st> `<st c="64020">client_secret</st>`<st c="64033">, must be submitted to the database during client signup before the authentication starts.</st> <st c="64124">Now, if the client is valid, the</st> `<st c="64157">save_token()</st>` <st c="64169">will execute to retrieve the</st> `<st c="64199">access_token</st>` <st c="64211">from the authorization server and save it to the database.</st> <st c="64271">The following snippet is our</st> <st c="64299">implementation</st> <st c="64315">for</st> `<st c="64319">save_token()</st>`<st c="64331">:</st>

from authlib.integrations.flask_oauth2.requests import FlaskOAuth2Request def save_token(token_data, request:FlaskOAuth2Request):

if request.user:

    user_id = request.user.user_id

else:

    user_id = request.client.user_id

token_dict = dict()

token_dict['client_id'] = request.client.client_id

token_dict['user_id'] = user_id

token_dict['issued_at'] = request.client.client_id_issued_at

token_dict['access_token_revoked_at'] = 0

token_dict['refresh_token_revoked_at'] = 0

token_dict['scope'] = request.client.client_metadata["scope"]

token_dict.update(token_data)

token_str = dumps(token_dict) <st c="64998">token_data</st> 包含了 <st c="65022">access_token</st>,并且请求包含了从 <st c="65091">query_client()</st> 获取的 <st c="65060">Client</st> 数据。该方法将这些详细信息合并到一个 <st c="65152">token_dict</st> 中,使用 <st c="65198">token_dict</st> 作为参数实例化 <st c="65181">Token</st> 类,并将对象记录存储在数据库中。以下是 <st c="65307">Token</st> 模型的蓝图:
<st c="65319">from authlib.integrations.sqla_oauth2 import OAuth2TokenMixin</st> class Token(Base, <st c="65400">OAuth2TokenMixin</st>):
    __tablename__ = 'oauth2_token'
    id = Column(Integer, Sequence('oauth2_token_id_seq', increment=1), primary_key=True)
    user_id = Column(String(40), ForeignKey('login.username'), nullable=False)
    login = relationship('Login', back_populates="token")
    … … … … … …
        <st c="65676">The</st> `<st c="65681">OAuth2TokenMixin</st>` <st c="65697">pads the</st> `<st c="65707">Token</st>` <st c="65712">class with the attributes related to</st> `<st c="65750">access_token</st>`<st c="65762">, such as</st> `<st c="65772">id,</st>` `<st c="65775">user_id</st>`<st c="65783">,</st> `<st c="65785">client_id</st>`<st c="65794">,</st> `<st c="65796">token_type</st>`<st c="65806">,</st> `<st c="65808">refresh_token</st>`<st c="65821">, and</st> `<st c="65827">scope</st>`<st c="65832">. By the way,</st> `<st c="65846">scope</st>` <st c="65851">is a mandatory field in Authlib that restricts access to the API resources based on some access level</st> <st c="65954">or role.</st>

        <st c="65962">To trigger the authorization</st> <st c="65992">server, the client must access the</st> `<st c="66027">/oauth/token</st>` <st c="66039">endpoint through basic authentication or form-based transactions.</st> <st c="66106">The following code shows the endpoint implementation of</st> <st c="66162">our application:</st>
 from flask import current_app, request <st c="66218">from modules import oauth_server</st> @current_app.route('/oauth/token', <st c="66286">methods=['POST']</st>)
async def issue_token(): <st c="66443">POST</st> transaction mode. The <st c="66470">Authorization</st> <st c="66483">Server</st> object from the <st c="66507">create_app()</st> provides the <st c="66533">create_token_response()</st> with details that the method needs to return for the user to capture the <st c="66630">access_token()</st>. Given the <st c="66656">client_id</st> of <st c="66669">Xd3LH9mveF524LOscPq4MzLY</st> and <st c="66698">client_secret</st> <st c="66711">t8w56Y9OBRsxdVV9vrNwdtMzQ8gY4hkKLKf4b6F6RQZlT2zI</st> with the <st c="66770">sjctrags</st> username, the following <st c="66803">curl</st> command shows how to run the <st c="66837">/</st><st c="66838">oauth/token</st> endpoint:

curl -u Xd3LH9mveF524LOscPq4MzLY:t8w56Y9OBRsxdVV9vrNwdtMzQ 8gY4hkKLKf4b6F6RQZlT2zI -XPOST http://localhost:5000/oauth/token -F grant_type=password -F username=sjctrags -F password=sjctrags -F scope=user_admin -F token_endpoint_auth_method=client_secret_basic


			<st c="67118">A sample result of executing</st> <st c="67148">the preceding command will contain the following details aside from</st> <st c="67216">the</st> `<st c="67220">access_token</st>`<st c="67232">:</st>

{"access_token": "fVFyaS06ECKIKFVtIfVj3ykgjhQjtc80JwCKyTMlZ2", "expires_in": 864000, "scope": "user_admin", "token_type": "Bearer"}


			<st c="67366">As indicated in the result, the</st> `<st c="67399">token_type</st>` <st c="67409">is</st> `<st c="67413">Bearer</st>`<st c="67419">, so we can use the</st> `<st c="67439">access_token</st>` <st c="67451">to access or run an API endpoint through a bearer Token authentication, like in the following</st> `<st c="67546">curl</st>` <st c="67550">command:</st>

curl -H "Authorization: Bearer fVFyaS06ECKIKFVtIfVj3y kgjhQjtc80JwCKyTMlZ2" http://localhost:5000/doctor/profile/add


			<st c="67676">A secured API endpoint must have the</st> `<st c="67714">require_oauth("user_admin")</st>` <st c="67741">method decorator, wherein</st> `<st c="67768">require_oath</st>` <st c="67780">is the</st> `<st c="67788">ResourceProtector</st>` <st c="67805">instance from the</st> `<st c="67824">create_app()</st>`<st c="67836">. A sample secured endpoint is the following</st> `<st c="67881">add_doctor_profile()</st>` <st c="67901">API function:</st>

from modules import require_oauth @current_app.route('/doctor/profile/add', methods = ['GET', 'POST']) @require_oauth("user_admin") async def add_doctor_profile():

… … … … … …

async with db_session() as sess:

    async with sess.begin():

    repo = DoctorRepository(sess)

    doc = Doctor(**doctor_json)

    result = await repo.insert_doctor(doc)

    … … … … … …

    return jsonify(record=doctor_json), 200

			<st c="68298">Aside from the Resource Owner Password grant, Authlib has an</st> `<st c="68360">AuthorizationCodeGrant</st>` <st c="68382">class to</st> <st c="68391">implement an</st> `<st c="68449">JWTBearerGrant</st>` <st c="68463">for implementing the</st> `<st c="68559">ch09-oauth-code</st>` <st c="68574">project will</st> <st c="68588">showcase the full implementation</st> <st c="68620">of the OAuth2 authorization code flow, while</st> `<st c="68666">ch09-oauth2-jwt</st>` <st c="68681">will implement the JWT authorization scheme (</st>`<st c="68749">pyjwt</st>` <st c="68754">module.</st>
			<st c="68762">If Flask supports popular and ultimate authentication and authorization modules, like Authlib, it also supports unpopular but reliable extension modules that can secure web-based and API-based Flask applications.</st> <st c="68976">One of</st> <st c="68983">these modules is</st> *<st c="69000">Flask-Limiter</st>*<st c="69013">, which can prevent</st> `<st c="69103">ch09-web-passphrase</st>` <st c="69122">project.</st>
			<st c="69131">Controlling the view or API access</st>
			<st c="69166">DoS attacks happen when a user maliciously accesses a web page or API multiple times to disrupt the traffic and make the</st> <st c="69287">resources inaccessible to others.</st> `<st c="69322">Flask-Limiter</st>` <st c="69335">can provide an immediate solution by managing the number of access of a user to an API endpoint.</st> <st c="69433">First, install the</st> `<st c="69452">Flask-Limiter</st>` <st c="69465">module using the following</st> `<st c="69493">pip</st>` <st c="69496">command:</st>

pip install flask-limiter


			<st c="69531">Also, install the module dependency for caching its configuration details to the</st> <st c="69613">Redis server:</st>

pip install flask-limiter[redis]


			<st c="69659">Now, we can set up the module’s</st> `<st c="69692">Limiter</st>` <st c="69699">class in the</st> `<st c="69713">create_app()</st>` <st c="69725">factory method, like in the</st> <st c="69754">following snippet:</st>

from flask_limiter import Limiter from flask_limiter.util import get_remote_address

def create_app(config_file):

app = Flask(__name__,template_folder= '../modules/pages', static_folder= '../modules/resources')

… … … … … …

global limiter

limiter = <st c="70020">Limiter</st>(

app=app, key_func=get_remote_address,

default_limits=["30 per day", "5 per hour"],

storage_uri="memory://", )

			<st c="70139">Instantiating the</st> `<st c="70158">Limiter</st>` <st c="70165">class requires at least the</st> `<st c="70194">app</st>` <st c="70197">instance, the host of the application through the</st> `<st c="70248">get_remote_address()</st>`<st c="70268">, the</st> `<st c="70274">default_limits</st>` <st c="70288">(e.g.,</st> `<st c="70296">10 per hour</st>`<st c="70307">,</st> `<st c="70309">10 per 2 hours</st>`<st c="70323">, or</st> `<st c="70328">10/hour</st>`<st c="70335">), and the storage URI for the Redis server.</st> <st c="70381">The</st> `<st c="70385">Limiter</st>` <st c="70392">instance will</st> <st c="70407">provide each protected API with the</st> `<st c="70443">limit()</st>` <st c="70450">decorator that specifies the number of accesses not lower than the set default limit.</st> <st c="70537">The following API is restricted not to be accessed by a user more than a</st> *<st c="70610">maximum count of 5 times</st>* *<st c="70635">per minute</st>*<st c="70645">:</st>

from modules import limiter @current_app.route('/login/auth', methods=['GET', 'POST']) @limiter.limit("5 per minute") async def login_user():

if request.method == 'GET':

    return render_template('login/authenticate.html'), 200

username = request.form['username'].strip()

password = request.form['password'].strip()

async with db_session() as sess:

    async with sess.begin():

        repo = LoginRepository(sess)

        … … … … … …

            return render_template('login/authenticate.html'), 200

			<st c="71115">Running</st> `<st c="71124">login_user()</st>` <st c="71136">more than the</st> <st c="71151">limit will give us the message shown in</st> *<st c="71191">Figure 9</st>**<st c="71199">.5</st>*<st c="71201">.</st>
			![Figure 9.5 – Accessing /login/auth more than the limit](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_09_005.jpg)

			<st c="71287">Figure 9.5 – Accessing /login/auth more than the limit</st>
			<st c="71341">Violating the number of access rules set by Talisman will lead users to its built-in error handling mechanism: the application rendering an error page with its</st> <st c="71502">error message.</st>
			<st c="71516">Summary</st>
			<st c="71524">In this chapter, we learned that compared to FastAPI and Tornado, there is quite a list of extension modules that provide solutions to secure a Flask application against various attacks.</st> <st c="71712">For instance, Flask-Seasurf and Flask-WTF can help minimize CSRF attacks.</st> <st c="71786">When pursuing web authentication, Flask-Login can provide a reliable authentication mechanism with added password hashing and encryption mechanisms, as we learned in</st> <st c="71952">this chapter.</st>
			<st c="71965">On the other hand, Flask-HTTPAuth can provide API-based applications with HTTP basic, digest, and bearer token authentication schemes.</st> <st c="72101">We learned that OAuth2 Authorization server grants and OAuth2 JWT Token-based types can also protect Flask applications from other</st> <st c="72232">applications’ access.</st>
			<st c="72253">The Flask-Talisman ensures security rules on response headers to filter the outgoing response of every API endpoint.</st> <st c="72371">Meanwhile, the Flask-Session module saves Flask sessions in the filesystem to avoid browser-based attacks.</st> <st c="72478">Escaping, stripping of whitespaces, and form validation of incoming inputs using modules like Gladiator and Flask-WTF helps prevent injection attacks by eliminating suspicious text or alphanumerics in</st> <st c="72679">the inputs.</st>
			<st c="72690">This chapter proved that several updated and version-compatible modules can help protect our applications from malicious and unwanted attacks.</st> <st c="72834">These modules can save time and effort compared to ground-up solutions in securing</st> <st c="72917">our applications.</st>
			<st c="72934">The next chapter will be about testing Flask components before running and deploying them to</st> <st c="73028">production servers.</st>









第三部分:测试、部署和构建企业级应用程序

在本节的最后部分,您将学习一些测试、部署和运行我们的 Flask 3 应用程序的选择和解决方案。 此外,您还将了解如何使用互操作性功能将 Flask 应用程序集成到 GraphQL、React 表单、Flutter 移动应用程序以及其他使用 FastAPI、Django、Tornado 和 Flask 构建的应用程序中。

本部分包括以下章节:

  • 第十章, 为 Flask 创建测试用例

  • 第十一章, 部署 Flask 应用程序

  • 第十二章, 将 Flask 集成到其他工具和框架中

第十一章:10

为 Flask 创建测试用例

在构建 Flask 组件之后,创建测试用例以确保它们的正确性并修复它们的错误是至关重要的。在测试类型中, 单元测试 专注于测试组件在独立于其他模块或任务时的有效性和性能。 另一方面, 集成测试 确保 Flask 组件的功能性和可靠性在所有依赖项一起 运行时的正确性。

为了实现这些测试用例,Python 有一个内置的模块叫做unittest,它可以提供一个TestCase 超类以及setUp() tearDown() 方法,这些方法可以构建测试用例和测试套件的变体。还有一个名为 pytest的第三方模块,它简单易用,非模板化,并且可以提供用于设置测试环境的可重用固定值。在本章中,我们将重点介绍如何使用 pytest 为从第 *1 到第 *9 的项目中选定的功能实现测试用例。

本章的主要目标是提供 Flask 项目所需的测试环境,在其中我们可以运行、研究、审查、分析和改进 Flask 组件,而无需部署应用程序。 本章的另一个目标是培养测试(至少是单元测试)是任何企业级 应用程序开发的必要部分的思维方式。

以下是本章涵盖的主题:

  • 为 Web 视图、存储库类和 本地服务创建测试用例

  • 应用工厂中的组件和蓝图创建测试用例

  • 异步组件创建测试用例

  • 为受保护 API 和 Web 组件创建测试用例

  • MongoDB 事务创建测试用例

  • WebSocket创建测试用例

技术要求

所有测试用例都将来自从 第一章 第九章 创建的不同应用程序。 所有这些应用程序都在这个 GitHub 仓库中: https://github.com/PacktPublishing/Mastering-Flask-Web-Development

为 Web 视图、存储库类和本地服务创建测试用例

pytest <st c="2046">模块</st> 支持单元 <st c="2074">和集成或功能测试。</st> <st c="2113">它需要简单的语法来构建测试用例,这使得它非常容易使用,并且它有一个</st> 平台<st c="2205">可以自动运行所有测试文件。</st> <st c="2258">此外,</st>pytest<st c="2274">是一个免费和开源模块,因此使用以下</st>pip` 命令安装:

 pip install pytest

然而, <st c="2384">pytest</st> 只能与由 Blueprints 和 应用程序工厂 管理的目录结构 的 Flask 项目一起工作。 我们的 在线个人咨询服务 第一章 不遵循 Flask 的目录结构标准。 所有视图模块都通过 <st c="2641">app</st> 实例通过 <st c="2662">__main__</st>导入,这在测试期间成为 <st c="2690">pytest</st> 模块而不是 <st c="2716">main.py</st> 模块。 因此,测试我们的 <st c="2765">ch01</st> 项目给我们以下运行时 <st c="2809">错误信息</st>:

 ImportError cannot import name 'app' from '__main__'

错误意味着在 <st c="2905">app</st> 对象中 <st c="2933">pytest</st> 模块 中没有可导入的内容。 因此,一个可测试的新版本 在线个人咨询服务 位于 <st c="3031">ch01-testing</st> 项目中,该项目将所有视图函数放在 Python 函数中,这些函数是 <st c="3122">main.py</st> 模块将访问以传递 <st c="3161">app</st> 实例。 以下 <st c="3189">main.py</st> 代码片段显示了这些函数调用替换了视图的 <st c="3253">import 语句</st>:

 app = Flask(__name__, template_folder='pages')
… … … … … …
create_index_routes(app)
create_signup_routes(app)
create_examination_routes(app)
create_reports_routes(app)
create_admin_routes(app)
create_login_routes(app)
create_profile_routes(app)
create_certificates_routes(app)

每个函数中封装的视图函数将 使用 <st c="3611">app</st> 实例来实现 <st c="3641">GET</st> <st c="3649">POST</st> 路由。 此外,为了 提供一个由 Flask 提供的测试环境,在配置文件中将 <st c="3725">Testing</st> 环境设置为 <st c="3748">true</st>

现在,在结构准确且无循环导入的 Flask 项目主文件夹中,紧邻 <st c="3916">main.py</st>的位置创建一个 <st c="3794">tests</st> 文件夹。在这个文件夹中,使用以 <st c="4015">test_</st> 关键字为前缀的模块文件实现测试用例。 如果测试用例的数量增加,可以通过功能(例如,视图、存储库、服务、API 等)或测试类型(例如,单元、集成)进一步组织这些文件到子文件夹中。 图 10**.1 显示了包含 <st c="4289">ch01-testing</st> 项目的最终目录结构,其中包含 <st c="4319">tests</st> 文件夹。

图 10.1 – 测试文件夹

图 10.1 – 测试文件夹

现在,以模块(<st c="4629">python -m pytest</st>)的形式运行 <st c="4601">pytest</st> 命令来执行所有测试方法,并使用以下命令运行每个 测试文件:

 python -m pytest tests/xxxx/test_xxxxxx.py

或者,使用以下命令运行单个测试函数:

 python -m pytest tests/xxxx/test_xxxxxx.py::test_xxxxxxx

现在,让我们通过为 <st c="4954">ch01-testing</st>的模型类、存储库事务、本地服务和 视图函数创建测试用例来探索 <st c="4920">pytest</st>

测试模型类

展示单元测试的其中一个测试文件是 <st c="5126">test_models.py</st>,它包含以下实现:

 import pytest
from model.candidates import AdminUser <st c="5240">@pytest.fixture(scope='module', autouse=True)</st> def admin_details(<st c="5304">scope="module"</st>):
    data = {"id": 101, "position": "Supervisor","age": 45, "emp_date": "1980-02-16", "emp_status": "regular", "username": "pedro", "password": "pedro", "utype": 0, "firstname": "Pedro", "lastname" :"Cruz"}
    yield data
    data = None
def <st c="5552">test_admin_user_model</st>(<st c="5575">admin_details</st>):
    admin = AdminUser(**admin_details) <st c="5726">unittest</st>, test cases in <st c="5750">pytest</st> are in the form of *<st c="5776">test functions</st>*. The test function’s name is unique, descriptive of its purpose, and must start with the <st c="5880">_test</st> keyword like its test file. Its code structure follows the <st c="6171">test_models.py</st>, the *<st c="6191">Given</st>* part is the creating of <st c="6221">admin_details</st> fixture, the *<st c="6248">When</st>* is the instantiation of the <st c="6281">AdminUser</st> class, and the *<st c="6306">Then</st>* depicts the series of asserts that validates if the extracted <st c="6373">firstname</st>, <st c="6384">lastname</st>, and <st c="6398">age</st> response details are precisely the same as the inputs. Unlike the <st c="6468">unittest</st>, <st c="6478">pytest</st> only uses the <st c="6499">assert</st> statement and the needed conditional expression to perform assertion.
			<st c="6575">The input to the</st> `<st c="6593">test_admin_user_model()</st>` <st c="6616">test case is an injectable and reusable admin record created through</st> `<st c="6686">pytest</st>`<st c="6692">’s</st> `<st c="6696">fixture()</st>`<st c="6705">. The</st> `<st c="6711">pytest</st>` <st c="6717">module has a decorator function called</st> `<st c="6757">fixture()</st>` <st c="6766">that defines functions as injectable resources.</st> <st c="6815">Like in</st> `<st c="6823">unittest</st>`<st c="6831">,</st> `<st c="6833">pytest</st>`<st c="6839">’s fixture performs</st> `<st c="6860">setUp()</st>` <st c="6867">before the call to</st> `<st c="6887">yield</st>` <st c="6892">and</st> `<st c="6897">tearDown()</st>` <st c="6907">after the yielding of the resource.</st> <st c="6944">In the given</st> `<st c="6957">test_models.py</st>`<st c="6971">, the fixture sets up the</st> <st c="6997">admin details in JSON format, and garbage collects the JSON object data after the</st> `<st c="7079">yield</st>` <st c="7084">statement.</st> <st c="7096">But how do test methods utilize</st> <st c="7128">these fixtures?</st>
			<st c="7143">A fixture function has</st> <st c="7167">four scopes:</st>

				*   `<st c="7179">function</st>`<st c="7188">: This fixture runs</st> <st c="7208">only once exclusively on some selected test methods in a</st> <st c="7266">test file.</st>
				*   `<st c="7276">class</st>`<st c="7282">: This fixture runs only once on a test class containing test methods that require</st> <st c="7366">the resource.</st>
				*   `<st c="7379">module</st>`<st c="7386">: This fixture runs only once on a test file containing the test methods that require</st> <st c="7473">the resource.</st>
				*   `<st c="7486">package</st>`<st c="7494">: This fixture runs only once on a package level containing the test methods that require</st> <st c="7585">the resource.</st>
				*   `<st c="7598">session</st>`<st c="7606">: This fixture runs only once to be distributed across all test methods that require the resource in</st> <st c="7708">a session.</st>

			<st c="7718">To utilize the fixture during its scoped execution, inject the resource function to test the method’s parameter list.</st> <st c="7837">Our</st> `<st c="7841">admin_details()</st>` <st c="7856">fixture executes at the module level and is injected into</st> `<st c="7915">test_admin_user_model()</st>` <st c="7938">through the parameter list.</st> <st c="7967">On the other hand,</st> `<st c="7986">fixture()</st>`<st c="7995">’s</st> `<st c="7999">autouse</st>` <st c="8006">forces all test methods to request the resource</st> <st c="8055">during testing.</st>
			<st c="8070">To run our test file, execute the</st> `<st c="8105">python -m pytest tests/repository/test_models.py</st>` <st c="8153">command.</st> <st c="8163">If the testing is successful, the console output will be similar to</st> *<st c="8231">Figure 10</st>**<st c="8240">.2</st>*<st c="8242">:</st>
			![Figure 10.2 – The pytest result when a test succeeded](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_10_002.jpg)

			<st c="8801">Figure 10.2 – The pytest result when a test succeeded</st>
			<st c="8854">The</st> `<st c="8859">pytest</st>` <st c="8865">result includes the</st> `<st c="8886">pytest</st>` <st c="8892">plugin installed and its configuration details, a testing directory, and a horizontal</st> <st c="8979">green marker indicating the number of successful tests executed.</st> <st c="9044">On the other hand, the console output will be similar to</st> *<st c="9101">Figure 10</st>**<st c="9110">.2</st>* <st c="9112">if a test</st> <st c="9123">case fails:</st>
			![Figure 10.3 – The pytest result when a test failed](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_10_003.jpg)

			<st c="9628">Figure 10.3 – The pytest result when a test failed</st>
			<st c="9678">The console will show the assertion statement that fails and a short description of</st> `<st c="9763">AssertionError</st>`<st c="9777">. Now, test cases must only catch</st> `<st c="9811">AssertionError</st>` <st c="9825">due to failed assertions and nothing else because it is understood that codes under testing have already handled all</st> `<st c="9943">RuntimeError</st>` <st c="9955">internally using</st> `<st c="9973">try-except</st>` <st c="9983">before testing.</st>
			<st c="9999">A few components in</st> `<st c="10020">ch01-testing</st>` <st c="10032">need unit testing.</st> <st c="10052">Almost all components are connected to build functionality crucial to the application, such as database connection and</st> <st c="10171">repository transactions.</st>
			<st c="10195">Testing the repository classes</st>
			<st c="10226">At this point, we will start highlighting functional or integration test cases for our application.</st> <st c="10327">Our</st> `<st c="10331">ch01-testing</st>` <st c="10343">project uses</st> `<st c="10357">psycopgy2</st>`<st c="10366">’s cursor methods to implement the database transactions.</st> <st c="10425">To</st> <st c="10427">impose a clean approach, a custom decorator</st> `<st c="10472">connect_db()</st>` <st c="10484">decorates all repository transactions to provide the connection object for the</st> `<st c="10564">execute()</st>` <st c="10573">and</st> `<st c="10578">fetchall()</st>` <st c="10588">cursor methods.</st> <st c="10605">But first, it is always a standard practice to check whether all database connection details, such as</st> `<st c="10707">DB_USER</st>`<st c="10714">,</st> `<st c="10716">DB_PASSWORD</st>`<st c="10727">,</st> `<st c="10729">DB_PORT</st>`<st c="10736">,</st> `<st c="10738">DB_HOST</st>`<st c="10745">, and</st> `<st c="10751">DB_NAME</st>`<st c="10758">, are all registered as environment variables in the configuration file.</st> <st c="10831">The following test case implementation showcases how to test custom decorators that provide database connection to</st> <st c="10946">repository transactions:</st>

from config.db import connect_db def test_connection(): @connect_db def create_connection(conn): assert conn is not None create_connection()


			<st c="11111">The local</st> `<st c="11122">create_connection()</st>` <st c="11141">method will capture the</st> `<st c="11166">conn</st>` <st c="11170">object from the</st> `<st c="11187">db_connect()</st>` <st c="11199">decorator.</st> <st c="11211">Its purpose as a dummy transaction is to assert whether the</st> `<st c="11271">conn</st>` <st c="11275">object created by</st> `<st c="11294">psycopgy2</st>` <st c="11303">with the database details is valid and ready for CRUD operations.</st> <st c="11370">This approach will also apply to other test cases implemented to check the validity and correctness of custom decorator functions, database-oriented or not.</st> <st c="11527">Now, run the</st> `<st c="11540">python -m pytest tests/repository/test_db_connect.py</st>` <st c="11592">command to check whether the database</st> <st c="11631">configurations work.</st>
			<st c="11651">Let us now concentrate on testing repository transactions with database connection and test data generated</st> <st c="11759">by</st> `<st c="11762">pytest</st>`<st c="11768">.</st>
			<st c="11769">Passing test data to test functions</st>
			<st c="11805">If testing the database connection is successful, the next test cases must check and refine the repository classes and their CRUD transactions.</st> <st c="11950">The following test function of</st> `<st c="11981">test_repo_admin.py</st>` <st c="11999">showcases</st> <st c="12010">how to test an</st> `<st c="12025">INSERT</st>` <st c="12031">admin detail transaction using</st> `<st c="12063">cursor()</st>` <st c="12071">from</st> `<st c="12077">psycopg2</st>`<st c="12085">:</st>

import pytest

from repository.admin import insert_admin @pytest.mark.parametrize(("id", "fname", "lname", "age", "position", "date_employed", "status"), (("8999", "Juan", "Luna", 76, "Manager", "2010-10-10", "active"),

("9999", "Maria", "Clara", 45, "Developer", "2015-08-15", "inactive")

))

def test_insert_admin(id, fname, lname, age, position, date_employed, status):

result = insert_admin(<st c="12483">id, fname, lname, age, position, date_employed, status</st>)

assert result is True

			`<st c="12585">pytest.mark</st>` <st c="12596">attribute provides additional metadata to test functions by adding built-in markers, such</st> <st c="12686">as the</st> `<st c="12694">userfixtures()</st>`<st c="12708">,</st> `<st c="12710">skip()</st>`<st c="12716">,</st> `<st c="12718">xfail()</st>`<st c="12725">,</st> `<st c="12727">filterwarnings()</st>`<st c="12743">, and</st> `<st c="12749">parametrize()</st>` <st c="12762">decorators.</st> <st c="12775">With</st> `<st c="12807">parametrize()</st>` <st c="12820">marker generates and provides a set of test data</st> <st c="12870">to test functions using the local parameter list.</st> <st c="12920">The test functions will utilize these multiple inputs to produce varying</st> <st c="12993">assert results.</st>
			`<st c="13008">test_insert_admin()</st>` <st c="13028">has local parameters corresponding to the parameter names indicated in the</st> `<st c="13104">parametrize()</st>` <st c="13117">marker.</st> <st c="13126">The marker will pass all these inputs to their respective local parameters in the test function to make the testing happen.</st> <st c="13250">It will also seem to iterate all the tuples of inputs in the decorator until the test function consumes all the inputs.</st> <st c="13370">Running</st> `<st c="13378">test_insert_admin()</st>` <st c="13397">gave me</st> *<st c="13406">Figure 10</st>**<st c="13415">.4</st>*<st c="13417">, proof that</st> `<st c="13430">@pytest.mark.parametrize()</st>` <st c="13456">iterates all its</st> <st c="13474">test inputs.</st>
			![Figure 10.4 – Result of parameterized testing](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_10_004.jpg)

			<st c="13786">Figure 10.4 – Result of parameterized testing</st>
			<st c="13831">But how about if there</st> <st c="13854">is a need to control the behaviors of some external components connected to the functionality</st> <st c="13948">under testing?</st> <st c="13964">Let us now</st> <st c="13975">discuss</st> **<st c="13983">mocking</st>**<st c="13990">.</st>
			<st c="13991">Mocking other functionality during testing</st>
			<st c="14034">Now, there are times in integration or functionality testing when applying control to other dependencies or systems</st> <st c="14151">connected to a feature is necessary to test and analyze that specific feature.</st> <st c="14230">Controlling other connected parts requires the process of</st> `<st c="14505">pytest</st>`<st c="14511">, install</st> `<st c="14521">pytest-mock</st>` <st c="14532">first using the following</st> `<st c="14559">pip</st>` <st c="14562">command:</st>

pip install pytest-mock


			<st c="14595">The</st> `<st c="14600">pytest-mock</st>` <st c="14611">plugin derives its mocking capability from the</st> `<st c="14659">unittest.mock</st>` <st c="14672">but provides a cleaner and simpler approach.</st> <st c="14718">Because of that, using some helper classes and methods, such as the</st> `<st c="14786">patch()</st>` <st c="14793">decorator, from</st> `<st c="14810">unittest.mock</st>` <st c="14823">will work with the</st> `<st c="14843">pytest-mock</st>` <st c="14854">module.</st> <st c="14863">Another option is to install and use the</st> `<st c="14904">mock</st>` <st c="14908">extension module, which is an acceptable replacement</st> <st c="14962">for</st> `<st c="14966">unittest.mock</st>`<st c="14979">.</st>
			<st c="14980">The following</st> `<st c="14995">test_mock_insert_admin()</st>` <st c="15019">mocks the</st> `<st c="15030">psycopg2</st>` <st c="15038">connection to focus the testing solely on the correctness and performance of the</st> `<st c="15120">INSERT</st>` <st c="15126">admin profile</st> <st c="15141">details process:</st>

import pytest from unittest.mock import patch from repository.admin import insert_admin

@pytest.mark.parametrize(("id", "fname", "lname", "age", "position", "date_employed", "status"),

(("8999", "Juan", "Luna", 76, "Manager", "2010-10-10", "active"),

("9999", "Maria", "Clara", 45, "Developer", "2015-08-15", "inactive")

)) @patch("psycopg2.connect") def test_mock_insert_admin(mock_connect, id, fname, lname, age, position, date_employed, status): mocked_conn = mock_connect.return_valuemock_cur = mocked_conn.cursor.return_value result = insert_admin(id, fname, lname, age, position, date_employed, status) mock_cur.execute.assert_called_once()mocked_conn.commit.assert_called_once() assert result is True


			<st c="15868">Instead of using the database connection,</st> `<st c="15911">test_mock_insert_admin()</st>` <st c="15935">mocks</st> `<st c="15942">psycopgy2.connect()</st>`<st c="15961">and replaces it with a</st> `<st c="15985">mock_connect</st>` <st c="15997">mock object through the</st> `<st c="16022">patch()</st>` <st c="16029">decorator of</st> `<st c="16043">unittest.mock</st>`<st c="16056">. The</st> `<st c="16062">patch()</st>` <st c="16069">decorator or context manager makes mocking easier by decorating the test functions in a test class or module.</st> <st c="16180">The first decorator passes the mock object to the first parameter of the test function, followed by other mock</st> <st c="16291">objects, if there are any, in the same order as their corresponding</st> `<st c="16359">@patch()</st>` <st c="16367">decorator in the layer of decorators.</st> <st c="16406">The</st> `<st c="16410">pytest</st>` <st c="16416">module will restore to their original state all mocked objects</st> <st c="16480">after testing.</st>
			<st c="16494">A mocked object emits a</st> `<st c="16519">return_value</st>` <st c="16531">attribute to set its value when invoked or to call the mocked object’s properties or methods.</st> <st c="16626">In the given</st> `<st c="16639">test_mock_insert_admin()</st>`<st c="16663">, the</st> `<st c="16669">mocked_conn</st>` <st c="16680">and</st> `<st c="16685">mock_curr</st>` <st c="16694">objects were derived from calling</st> `<st c="16729">return_value</st>` <st c="16741">of the mocked database connection (</st>`<st c="16777">mock_connect</st>`<st c="16790">) and the mocked</st> `<st c="16808">cursor()</st>` <st c="16816">method.</st>
			<st c="16824">Moreover, mocked objects also emit assert methods such as</st> `<st c="16883">assert_called()</st>`<st c="16898">,</st> `<st c="16900">assert_not_called()</st>`<st c="16919">,</st> `<st c="16921">assert_called_once()</st>`<st c="16941">,</st> `<st c="16943">assert_called_once_with()</st>`<st c="16968">, and</st> `<st c="16974">assert_called_with()</st>` <st c="16994">to verify the invocation of these mocked objects during testing.</st> <st c="17060">The</st> `<st c="17064">assert_called_once_with()</st>` <st c="17089">and</st> `<st c="17094">assert_called_with()</st>` <st c="17114">methods verify the call of the mocked objects based on specific constraints or arguments.</st> <st c="17205">Our example verifies the execution of the mocked</st> `<st c="17254">cursor()</st>` <st c="17262">and</st> `<st c="17267">commit()</st>` <st c="17275">methods in the</st> `<st c="17291">INSERT</st>` <st c="17297">transaction</st> <st c="17310">under testing.</st>
			<st c="17324">Another use of</st> `<st c="17340">return_value</st>` <st c="17352">is to</st> <st c="17359">mock the result of the function under test to focus on testing the performance or algorithm of the transaction.</st> <st c="17471">The following test case implementation shows mocking the return value of the</st> `<st c="17548">select_all_user()</st>` <st c="17565">transaction:</st>

@patch("psycopg2.connect") def test_mock_select_users(mock_connect):

expected_rec = [(222, "sjctrags", "sjctrags", "2023-02-26"), ( 567, "owen", "owen", "2023-10-22")] <st c="17749">mocked_conn = mock_connect.return_value</st><st c="17788">mock_cur = mocked_conn.cursor.return_value</st><st c="17831">mock_cur.fetchall.return_value = expected_rec</st> result = select_all_user()

assert result is expect_rec

			<st c="17932">The purpose of setting</st> `<st c="17956">expected_rec</st>` <st c="17968">to</st> `<st c="17972">return_value</st>` <st c="17984">of the mocked</st> `<st c="17999">fetchall()</st>` <st c="18009">method of</st> `<st c="18020">mock_cur</st>` <st c="18028">is to establish an assertion that will complete the GWT process of the test case.</st> <st c="18111">The goal is to run and scrutinize the performance and correctness of the algorithms in</st> `<st c="18198">select_all_user()</st>` <st c="18215">with the mocked</st> `<st c="18232">cursor()</st>` <st c="18240">and</st> `<st c="18245">fetchall()</st>` <st c="18255">methods.</st>
			<st c="18264">Aside from repository methods, native services also need thorough testing to examine their impact on</st> <st c="18366">the application.</st>
			<st c="18382">Testing the native services</st>
			<st c="18410">Native services or transactions in the service layer build the business processes and logic of the Flask application.</st> <st c="18529">The following test</st> <st c="18547">case implementation performs testing on</st> `<st c="18588">record_patient_exam()</st>`<st c="18609">, which stores the patient’s counseling exams in the database and computes the average score given</st> <st c="18708">the data:</st>

import pytest

from services.patient_monitoring import record_patient_exam

@pytest.fixture

def exam_details():

params = dict()

params['pid'] = 1111

params['qid'] = 568

params['score'] = 87

params['total'] = 100

yield params

def test_record_patient_exam(exam_details):

result = record_patient_exam(exam_details)

assert result is True

			<st c="19049">The function-scoped fixture generated the test data for the test function.</st> <st c="19125">The result of testing</st> `<st c="19147">record_patient_exam()</st>` <st c="19168">will depend on the</st> `<st c="19188">insert_patient_score()</st>` <st c="19210">repository transaction with the actual</st> <st c="19250">database connection.</st>
			<st c="19270">The next things to test are the view functions.</st> <st c="19319">What are the aspects of a view function that require testing?</st> <st c="19381">Is it feasible to test views without</st> <st c="19418">using browsers?</st>
			<st c="19433">Testing the view functions</st>
			<st c="19460">The Flask</st> `<st c="19471">app</st>` <st c="19474">instance has a</st> `<st c="19490">test_client()</st>` <st c="19503">utility</st> <st c="19512">to handle</st> `<st c="19522">GET</st>` <st c="19525">and</st> `<st c="19530">POST</st>` <st c="19534">routes.</st> <st c="19543">This method generates an object of the</st> `<st c="19582">Client</st>` <st c="19588">type, a built-in class to Werkzeug.</st> <st c="19625">A test file should have a fixture to set up the</st> `<st c="19673">test_client()</st>` <st c="19686">context and yield the</st> `<st c="19709">Client</st>` <st c="19715">instance to each test function for views.</st> <st c="19758">The following test case implementation focuses on testing</st> `<st c="19816">GET</st>` <st c="19819">routes with a fixture that yields the</st> `<st c="19858">Client</st>`  <st c="19864">instance:</st>

import pytest

from main import app as flask_app @pytest.fixture(autouse=True) def client(): with flask_app.test_client() as client:yield client def test_default_page(client): test_default_page() runs the root page using the Client instance and checks whether the rendered Jinja template contains the "OPCS" substring. res.data is always in bytes, so decoding it will give us the string equivalent:

 def test_home_page(client): <st c="20364">res = client.get("/home")</st> assert "Welcome" in res.data.decode()
    assert res.request.path == "/home"
        <st c="20462">On the other hand,</st> `<st c="20482">test_home_page()</st>` <st c="20498">runs the</st> `<st c="20508">/home GET</st>` <st c="20517">route and verifies whether there is a</st> `<st c="20556">"Welcome"</st>` <st c="20565">word on its template page.</st> <st c="20593">Also, it checks whether the path of the rendered page is still the</st> `<st c="20660">/home</st>` <st c="20665">URL path:</st>
 def test_exam_page(client): <st c="20704">res = client.get("/exam/assign")</st> assert res.status_code == 200
        <st c="20766">验证</st> `<st c="20817">client.get()</st>`<st c="20829">响应的状态码也是理想的选择。</st> <st c="20843">给定的</st> `<st c="20853">test_exam_page()</st>` <st c="20869">检查</st> <st c="20877">运行</st> `<st c="20897">/exam/assign</st>` <st c="20909">URL 是否会返回 HTTP 状态</st> `<st c="20944">Code 200。</st>`

        <st c="20953">另一方面,</st> `<st c="20977">Client</st>` <st c="20983">实例有一个</st> `<st c="20999">post()</st>` <st c="21005">方法来测试和运行</st> `<st c="21029">POST</st>` <st c="21033">路由。</st> <st c="21042">以下实现展示了如何模拟</st> `<st c="21093">表单处理事务:</st>
 import pytest
from main import app as flask_app
@pytest.fixture(autouse=True)
def client():
   with flask_app.test_client() as client:
       yield client
@pytest.fixture(autouse=True)
def form_data():
    params = dict()
    params["username"] = "jean"
    params["password"] = "jean"
    … … … … … …
    yield params
    params = None
def test_signup_post(client, form_data): <st c="21465">response = client.post("/signup/submit",</st> <st c="21505">data=form_data)</st> assert response.status_code == 200
        <st c="21556">由于表单参数理想情况下是哈希表格式,因此固定值必须以字典集合的形式提供这些表单参数及其对应的值,就像在我们的</st> `<st c="21732">form_data()</st>` <st c="21743">固定值。</st> <st c="21753">然后,我们将生成的表单数据传递给</st> `<st c="21819">client.post()</st>` <st c="21832">方法的 data 参数。</st> <st c="21841">之后,我们执行必要的断言以验证视图过程及其响应的正确性。</st>

        <st c="21953">除了检查</st> <st c="21977">渲染的 URL 路径、内容和状态码之外,还可以使用</st> `<st c="22095">pytest</st>`<st c="22101">来测试视图中的重定向。以下实现展示了如何测试一个</st> `<st c="22164">POST</st>` <st c="22168">事务是否将用户重定向到另一个</st> <st c="22210">视图页面:</st>
 def test_assign_exam_redirect(client, form_data):
    res = client.post('/exam/assign', data=form_data, <st c="22321">follow_redirects=True</st>) <st c="22447">test_assign_exam_redirect()</st> is to test the <st c="22490">/exam/assign</st> <st c="22502">POST</st> transaction and see whether it can successfully persist the score details (<st c="22583">form_data</st>) from the counseling exam and compute the rating based on the total number of exam items. The <st c="22689">client.post()</st> method has a <st c="22716">follow_redirects</st> parameter that can enforce redirection during testing when set to <st c="22799">True</st>. In our case, <st c="22818">client.post()</st> will run the <st c="22845">/exam/assign</st> <st c="22857">POST</st> transaction with redirection. If the view performs redirection during testing, its resulting <st c="22956">status</st> must be <st c="22971">"200 OK"</st> or its <st c="22987">status_code</st> is <st c="23002">200</st> and not <st c="23014">"302 FOUND"</st> or <st c="23029">302</st> because <st c="23041">follow_redirects</st> ensures that redirection or the HTTP Status Code 302 will happen. So, the assertion will be there is redirection (<st c="23172">200</st>) or none.
			<st c="23187">Another option to verify redirection is to set</st> `<st c="23235">follow_redirects</st>` <st c="23251">to</st> `<st c="23255">False</st>` <st c="23260">and then assert whether the</st> `<st c="23289">status_code</st>` <st c="23300">is</st> `<st c="23304">302</st>`<st c="23307">. The following test method shows this kind of</st> <st c="23354">testing approach:</st>

def 测试分配考试重定向 _302(client, form_data):

res = client.post('/exam/assign', data=form_data) <st c="23622">302</st> 因为在</st> `<st c="23680">client.post()</st>`中没有设置</st> `<st c="23646">follow_redirects</st>`参数。此外,<st c="23701">res.location</st>是提取 URL 路径的适当属性,因为<st c="23775">res.request.path</st>将给出</st> `<st c="23822">POST</st>`事务的 URL 路径。

        <st c="23847">除了断言</st> `<st c="23869">status_code</st>`<st c="23880">之外,验证重定向的正确性还包括检查正确的重定向路径和内容类型。</st> <st c="23995">如果存在,模拟也可以是检查</st> `<st c="24078">POST</st>` <st c="24082">事务及其重定向的额外策略。</st> **<st c="24137">猴子补丁</st>** <st c="24152">可以通过测试来帮助细化视图过程。</st>

        <st c="24204">现在让我们学习如何在测试</st> `<st c="24259">视图函数</st>`中应用猴子补丁。</st>

        <st c="24274">应用猴子补丁</st>

        `<st c="24325">pytest</st>` <st c="24331">功能涉及拦截视图事务中的函数并将其替换为自定义实现的模拟函数,该模拟函数返回我们期望的结果。</st> <st c="24490">模拟函数必须</st> <st c="24513">具有与原始函数相同的参数列表和返回类型。</st> <st c="24579">否则,猴子补丁将不会工作。</st> <st c="24621">以下是一个使用</st> <st c="24676">猴子补丁</st> <st c="24679">的重定向测试用例:</st>
 @connect_db
def insert_question_details(conn, id:int, cid:str, pid:int, exam_date:date, duration:int):
        return True <st c="24808">@pytest.fixture</st>
<st c="24823">def insert_question_patched(monkeypatch):</st><st c="24865">monkeypatch.setattr</st>( <st c="24888">"views.examination.insert_question_details", insert_question_details)</st> def test_assign_mock_exam(<st c="24984">insert_question_patched</st>, client, form_data):
    res = client.post('/exam/assign', data=form_data, follow_redirects=True)
    assert res.status == '200 OK'
    assert res.request.path == url_for('redirect_success_exam')
        `<st c="25192">monkeypatch</st>` <st c="25204">是注入到</st> <st c="25232">固定函数中的对象。</st> <st c="25254">它可以发出各种方法来伪造包和模块中其他对象的属性和函数。</st> <st c="25366">在给定的示例中,目标是测试使用模拟的</st> `<st c="25417">/exam/assign</st>` <st c="25429">表单事务的</st> `<st c="25461">insert_question_details()</st>`<st c="25486">。而不是使用</st> `<st c="25509">patch()</st>` <st c="25516">装饰器,固定函数中的</st> `<st c="25532">monkeypatch</st>` <st c="25543">对象使用其</st> `<st c="25646">setattr()</st>` <st c="25655">方法替换原始函数,使用一个虚拟的</st> `<st c="25610">insert_question_details()</st>` <st c="25635">。虚拟方法需要返回一个</st> `<st c="25699">True</st>` <st c="25703">值,因为测试需要检查视图函数在</st> `<st c="25791">INSERT</st>` <st c="25797">事务成功时的行为。</st> <st c="25825">现在,为了启用猴子补丁,你必须像典型的固定函数那样将包含</st> `<st c="25900">monkeypatch</st>` <st c="25911">对象</st> <st c="25972">的固定函数注入到测试函数中,它提供资源。</st>

        <st c="25989">猴子补丁不会替换被模拟函数的实际代码。</st> <st c="26063">在给定的</st> `<st c="26076">setattr()</st>`<st c="26085">中,</st> `<st c="26091">views.examination.insert_question_details</st>` <st c="26132">表达式表示的是</st> `<st c="26183">/exam/assign</st>` <st c="26195">路由中的存储库方法,而不是其实际存储库类中的方法。</st> <st c="26242">因此,这只是在替换视图函数中方法调用的状态,而不是修改方法的</st> <st c="26350">实际实现。</st>

        <st c="26372">测试存储库、服务和视图层需要与或无模拟以及</st> `<st c="26479">parametrize()</st>` <st c="26492">标记进行集成测试,以找到算法中的所有错误和不一致性。</st> <st c="26561">无论如何,在利用应用程序工厂和蓝图的组织化应用程序中设置测试类和文件更容易,因为这些项目不需要目录重构,例如对</st> `<st c="26786">ch01</st>` <st c="26790">应用程序施加的那种重构。</st>

        让我们讨论一下在测试 Flask 组件时使用 `<st c="26844">create_app()</st>` `<st c="26856">和蓝图</st>` 的好处。

        为应用工厂和蓝图中的组件创建测试用例

        应用工厂函数和蓝图通过管理上下文加载,并允许 Flask `<st c="27110">app</st>` `<st c="27113">实例</st>` 在整个应用中可访问,从而帮助解决循环导入问题,无需调用 `<st c="27183">__main__</st>` `<st c="27191">顶层模块</st>`。由于每个组件和层都处于其适当的位置,因此设置测试环境变得更加容易。

        我们在 *<st c="27334">第二章</st>* 和 *<st c="27349">第三章</st>* 的应用中拥有需要测试的基本 Flask 组件,例如由 SQLAlchemy 构建的存储库事务、异常和标准 API 函数。所有这些组件都是由 `<st c="27540">create_app()</st>` `<st c="27552">工厂</st>` `<st c="27561">和蓝图</st>` 构建的。

        让我们从为 SQLAlchemy 存储库事务制定测试用例开始。

        测试 ORM 事务

        在 *<st c="27705">第二章</st>* 中,*<st c="27682">在线发货</st>* `<st c="27697">应用</st>` 使用标准的 SQLAlchemy ORM 实现 CRUD 事务。集成测试可以帮助测试我们应用的存储库层。让我们检查以下测试用例实现,它运行了 `<st c="27951">ProductRepository</st>` 的 `<st c="27927">insert()</st>` `<st c="27935">事务</st>`。
 import pytest
from mock import patch
from main import app as flask_app <st c="28042">from modules.product.repository.product import</st> <st c="28088">ProductRepository</st>
<st c="28106">from modules.model.db import Products</st> @pytest.fixture(autouse=True)
def form_data():
    params = dict()
    params["name"] = "eraser"
    params["code"] = "SCH-8977"
    params["price"] = "125.00"
    yield params
    params = None <st c="28316">@patch("modules.model.config.db_session")</st> def test_mock_add_products(<st c="28385">mocked_sess</st>, form_data): <st c="28411">db_sess = mocked_sess.return_value</st><st c="28445">with flask_app.app_context() as context:</st> repo = ProductRepository(db_sess)
        prod = Products(price=form_data["price"], code=form_data["code"], name=form_data["name"]) <st c="28611">res = repo.insert(prod)</st><st c="28634">db_sess.add.assert_called_once()</st><st c="28667">db_sess.commit.assert_called_once()</st> assert res is True
        `<st c="28722">test_mock_add_products()</st>` `<st c="28747">关注于检查向数据库添加新产品线时` `<st c="28785">INSERT</st>` `<st c="28791">事务</st>` 的流程。</st> `<st c="28850">它模拟了 SQLAlchemy 的 `<st c="28863">db_session</st>` `<st c="28873">from SQLAlchemy’s `<st c="28892">scoped_session</st>` `<st c="28906">,因为测试是在代码行上进行的,而不是与` `<st c="28966">db_session</st>` `<st c="28976">’s `<st c="28980">add()</st>` `<st c="28985">方法</st>` 进行。</st> `<st c="28994">assert_called_once()</st>` `<st c="29014">of mocked `<st c="29025">add()</st>` `<st c="29030">和` `<st c="29035">commit()</st>` `<st c="29043">将在测试期间验证这些方法的执行。</st>

        <st c="29103">现在,</st> `<st c="29113">ch02-blueprint</st>` <st c="29127">项目</st> <st c="29136">使用</st> `<st c="29145">before_request()</st>` <st c="29161">和</st> `<st c="29166">after_request()</st>` <st c="29181">事件来追踪每个视图的请求以及访问视图的用户。</st> <st c="29267">这两个应用级事件成为应用程序自定义认证机制的核心实现。</st> <st c="29387">项目中的所有视图页面都恰好是受保护的。</st> <st c="29439">因此,运行和测试</st> `<st c="29467">/ch02/products/add</st>` <st c="29485">视图,例如,不作为有效用户登录,将导致重定向到登录页面,如下面的</st> `<st c="29617">测试用例</st>` <st c="29633">所示:</st>
 def test_add_product_no_login(form_data, client):
    res = client.post("/ch02/products/add", data=form_data) <st c="29865">add_product()</st> view directly will redirect us to <st c="29913">login_db_auth()</st> from the <st c="29938">login_bp</st> Blueprint, thus the HTTP Status Code 302\. To prove that login authentication is required for the user to access the <st c="30063">add_product()</st> view, create a test case that will include the <st c="30124">/ch02/login/auth</st> access, like in the following test case:

def test_add_product_with_login(form_data, login_data, client): res_login = client.post("/ch02/login/auth", data=login_data)with client.session_transaction() as session:assert 'admin' == session["username"] assert res_login.location.split('?')[0] == url_for('home_bp.menu')

res = client.post("/ch02/products/add", data=form_data)

assert res.status_code == 200

			<st c="30543">Testing and running</st> `<st c="30564">/ch02/login/auth</st>` <st c="30580">must be the initial goal before running</st> `<st c="30621">/ch02/products/add</st>`<st c="30639">. The</st> `<st c="30645">login_data()</st>` <st c="30657">fixture must provide a valid user detail for authentication.</st> <st c="30719">Since Flask’s built-in</st> `<st c="30742">session</st>` <st c="30749">is responsible for storing</st> `<st c="30777">username</st>`<st c="30785">, you can open a session in the test using</st> `<st c="30828">session_transaction()</st>` <st c="30849">of the test</st> `<st c="30862">Client</st>` <st c="30868">and the</st> `<st c="30877">with</st>` <st c="30881">context manager.</st> <st c="30899">Within the context of the simulated session, check and confirm whether</st> `<st c="30970">/ch02/login/auth</st>` <st c="30986">saved</st> `<st c="30993">username</st>` <st c="31001">in the</st> `<st c="31009">session</st>` <st c="31016">object.</st> <st c="31025">Also, assert whether the aftermath of a successful authentication will redirect the user to the</st> `<st c="31121">home_bp.menu</st>` <st c="31133">page.</st> <st c="31140">If all these verifications are</st> `<st c="31171">True</st>`<st c="31175">, run and test now the</st> `<st c="31198">add_product()</st>` <st c="31211">view and</st> <st c="31221">perform the proper</st> <st c="31240">verifications afterward.</st>
			<st c="31264">Next, we will test Flask API functions</st> <st c="31304">with</st> `<st c="31309">pytest</st>`<st c="31315">.</st>
			<st c="31316">Testing API functions</st>
			*<st c="31338">Chapter 3</st>* <st c="31348">introduced and used Flask API endpoint functions in building our</st> *<st c="31414">Online Pizza Ordering System</st>*<st c="31442">. Testing these API functions is not the same as consuming them.</st> <st c="31507">A test</st> `<st c="31514">Client</st>` <st c="31520">provides the</st> <st c="31533">utility methods, such as</st> `<st c="31559">get()</st>`<st c="31564">,</st> `<st c="31566">post()</st>`<st c="31572">,</st> `<st c="31574">put()</st>`<st c="31579">,</st> `<st c="31581">delete()</st>`<st c="31589">, and</st> `<st c="31595">patch()</st>`<st c="31602">, to run the API and consume its resources, while extension modules such as</st> `<st c="31678">requests</st>` <st c="31686">build client applications to access and consume</st> <st c="31735">the APIs.</st>
			<st c="31744">Like in testing view pages, it is still the test</st> `<st c="31794">Client</st>` <st c="31800">class that can run, test, and mock our Flask API functions.</st> <st c="31861">The following test cases show how to examine, scrutinize, and analyze the performance and responses of the APIs in the</st> `<st c="31980">ch03</st>` <st c="31984">project</st> <st c="31993">using</st> `<st c="31999">pytest</st>`<st c="32005">:</st>

import pytest

from mock import patch, MagicMock

from main import app as flask_app

import json

@pytest.fixture

def client():

with flask_app.test_client() as client:

yield client

def test_index(client): res = client.get('/index')data = json.loads(res.get_data(as_text=True)) assert data["message"] == "This is an Online Pizza Ordering System."


			<st c="32350">The given</st> `<st c="32361">test_index()</st>` <st c="32373">is part of the</st> `<st c="32389">test_http_get_api.py</st>` <st c="32409">test file and has the task of scrutinizing</st> <st c="32452">if calling</st> `<st c="32464">/index</st>` <st c="32470">will have a response of</st> `<st c="32495">{"message": "This is an Online Pizza Ordering System."}</st>`<st c="32550">. Like in the views, the response will always give data in</st> `<st c="32609">bytes</st>`<st c="32614">. However, by using</st> `<st c="32634">get_data(as_text=True)</st>` <st c="32656">with the</st> `<st c="32666">json.loads()</st>` <st c="32678">utility, the</st> `<st c="32692">data</st>` <st c="32696">response will become a</st> <st c="32720">JSON object.</st>
			<st c="32732">Now, the following is a test case that performs adding new order details to the</st> <st c="32813">existing database:</st>

@pytest.fixture

def order_data(): order_details = {"date_ordered": "2020-12-10", "empid": "EMP-101" , "cid": "CUST-101", "oid": "ORD-910"} yield order_details order_details = None

def test_add_order(client, order_data): res = client.post("/order/add", json=order_data) assert res.status_code == 201

assert res.content_type == 'application/json'

			<st c="33177">Like in views, the</st> `<st c="33197">client.post()</st>` <st c="33210">method consumes the POST API transactions, but with input details passed to its</st> `<st c="33291">json</st>` <st c="33295">parameter and not</st> `<st c="33314">data</st>`<st c="33318">. The given</st> `<st c="33330">test_add_order()</st>` <st c="33346">performs and asserts the</st> `<st c="33372">/order/add</st>` <st c="33382">API to verify whether SQLAlchemy’s</st> `<st c="33418">add()</st>` <st c="33423">function works successfully given the configured</st> `<st c="33473">db_session</st>`<st c="33483">. The test expects</st> `<st c="33502">content_type</st>` <st c="33514">of</st> `<st c="33518">application/json</st>` <st c="33534">in</st> <st c="33538">its response.</st>
			<st c="33551">Aside from</st> `<st c="33563">get()</st>` <st c="33568">and</st> `<st c="33573">post()</st>`<st c="33579">, the test</st> `<st c="33590">Client</st>` <st c="33596">has a</st> `<st c="33603">delete()</st>` <st c="33611">method to run</st> `<st c="33626">HTTP DELETE</st>` <st c="33637">API transactions.</st> <st c="33656">The following test function runs</st> `<st c="33689">/order/delete</st>` <st c="33702">with a path variable</st> <st c="33724">of</st> `<st c="33727">ORD-910</st>`<st c="33734">:</st>

def test_delete_order(client): res = client.delete("/order/delete/ORD-910") assert res.status_code == 201


			<st c="33842">The given test class studies</st> <st c="33871">the deletion of an order with</st> `<st c="33902">ORD-910</st>` <st c="33909">as the order ID will not throw runtime errors even if the order does</st> <st c="33979">not exist.</st>
			<st c="33989">Now, the test</st> `<st c="34004">Client</st>` <st c="34010">also has a</st> `<st c="34022">patch()</st>` <st c="34029">method to run</st> `<st c="34044">PATCH API</st>` <st c="34053">transactions and a</st> `<st c="34073">put()</st>` <st c="34078">method for</st> `<st c="34090">PUT API</st>`<st c="34097">.</st>
			<st c="34098">Determining what exception a repository, service, API, or view under test will throw is a testing mechanism</st> <st c="34207">called</st> `<st c="34406">pytest</st>` <st c="34412">implement</st> <st c="34423">exception testing?</st>
			<st c="34441">Implementing exception testing</st>
			<st c="34472">There are many variations of implementing exception testing in</st> `<st c="34536">pytest</st>`<st c="34542">, but the most common is to use the</st> `<st c="34578">raises()</st>` <st c="34586">function and the</st> `<st c="34604">xfail()</st>` <st c="34611">marker</st> <st c="34619">of</st> `<st c="34622">pytest</st>`<st c="34628">.</st>
			<st c="34629">The</st> `<st c="34634">raises()</st>` <st c="34642">utility applies to</st> <st c="34661">testing features that explicitly call</st> `<st c="34700">abort()</st>` <st c="34707">or</st> `<st c="34711">raise()</st>` <st c="34718">methods to throw specific built-in or custom</st> `<st c="34764">HTTPException</st>` <st c="34777">classes.</st> <st c="34787">Its goal is to verify whether the functionality under test is throwing the exact exception class.</st> <st c="34885">For instance, the following test case checks whether the</st> `<st c="34942">/ch03/employee/add</st>` <st c="34960">API raises</st> `<st c="34972">DuplicateRecordException</st>` <st c="34996">during duplicate</st> <st c="35014">record insert:</st>

import pytest

from main import app as flask_app from app.exceptions.db import DuplicateRecordException @pytest.fixture

def client():

with flask_app.test_client() as client:

yield client

@pytest.fixture

def employee_data():

order_details = {"empid": "EMP-101", "fname": "Sherwin John" , "mname": "Calleja", "lname": "Tragura", "age": 45 , "role": "clerk", "date_employed": "2011-08-11", "status": "active", "salary": 60000.99}

yield order_details

order_details = None

def test_add_employee(client, employee_data): with pytest.raises(DuplicateRecordException) as ex: res = client.post('/employee/add', json=employee_data)

    assert res.status_code == 200

assert str(ex.value) == "insert employee record encountered a problem"

			<st c="35749">The</st> `<st c="35754">add_employee()</st>` <st c="35768">endpoint function raises</st> `<st c="35794">DuplicateRecordException</st>` <st c="35818">if the return value of</st> `<st c="35842">EmployeeRepository</st>`<st c="35860">’s</st> `<st c="35864">insert()</st>` <st c="35872">is</st> `<st c="35876">False</st>`<st c="35881">.</st> `<st c="35883">test_add_employee()</st>` <st c="35902">checks</st> <st c="35910">whether the endpoint raises the exception given an</st> `<st c="35961">order_details()</st>` <st c="35976">fixture that yields an existing employee record.</st> <st c="36026">If</st> `<st c="36029">status_code</st>` <st c="36040">is not</st> `<st c="36048">200</st>`<st c="36051">, then there is a glitch in</st> <st c="36079">the code.</st>
			<st c="36088">On the other hand, the</st> `<st c="36112">xfail()</st>` <st c="36119">marker applies to testing components with overlooked and unhandled risky lines of code that have a considerable chance of messing up the application anytime</st> <st c="36277">at runtime.</st> <st c="36289">But</st> `<st c="36293">xFail()</st>` <st c="36300">can also apply to test classes that verify known custom exceptions, like in the</st> <st c="36381">following snippet:</st>

@pytest.mark.xfail(strict=True, raises=NoRecordException, reason="No existing record.") def test_update_employee(client, employee_data): res = client.patch(f'/employee/update/ {employee_data["empid"]}', json=employee_data) assert res.status_code == 201


			`<st c="36652">test_update_employee()</st>` <st c="36675">runs</st> `<st c="36681">PATCH API</st>`<st c="36690">, the</st> `<st c="36696">/ch03/employee/update</st>`<st c="36717">, and verifies whether the</st> `<st c="36744">employee_data()</st>` <st c="36759">fixture provides new details for an existing employee record.</st> <st c="36822">If the employee, determined by</st> `<st c="36853">empid</st>`<st c="36858">, is not in the database record throwing</st> `<st c="36899">NoRecordException</st>`<st c="36916">,</st> `<st c="36918">pytest</st>` <st c="36924">will trigger the</st> `<st c="36942">xFail()</st>` <st c="36949">marker and render the marker’s</st> *<st c="36981">“No existing</st>* *<st c="36994">record.”</st>* <st c="37002">reason.</st>
			<st c="37010">Because of the organized directory structures that applications in</st> *<st c="37078">Chapter 2</st>* <st c="37087">and</st> *<st c="37092">Chapter 3</st>* <st c="37101">follow, testing becomes clear-cut, isolated, reproducible, and categorized based on functionality.</st> <st c="37201">Using the application factories and Blueprints will not only give benefits to the development side but also to the</st> <st c="37316">testing environment.</st>
			<st c="37336">Let us try using</st> `<st c="37354">pytest</st>` <st c="37360">to test asynchronous transactions in Flask.</st> <st c="37405">Do we need to install additional modules to work out the kind</st> <st c="37467">of testing?</st>
			<st c="37478">Creating test cases for asynchronous components</st>
			<st c="37526">Flask 3.x supports</st> <st c="37546">asynchronous transactions with the</st> `<st c="37581">asyncio</st>` <st c="37588">platform.</st> *<st c="37599">Chapter 5</st>* <st c="37608">introduced creating asynchronous API</st> <st c="37646">endpoint functions, web views, background tasks and services, and repository transactions using the</st> `<st c="37746">async</st>`<st c="37751">/</st>`<st c="37753">await</st>` <st c="37758">features.</st> <st c="37769">The test</st> `<st c="37778">Client</st>` <st c="37784">class of Flask 3.x is part of the</st> `<st c="37819">Flask[async]</st>` <st c="37831">core libraries, so there will be no problem running the</st> `<st c="37888">async</st>` <st c="37893">components</st> <st c="37904">with</st> `<st c="37910">pytest</st>`<st c="37916">.</st>
			<st c="37917">The following test cases on the asynchronous repository layer, Celery tasks, and API endpoints will provide proof on</st> `<st c="38035">pytest</st>` <st c="38041">supporting Flask 3.x</st> <st c="38063">asynchronous platform.</st>
			<st c="38085">Testing asynchronous views and API endpoint function</st>
			<st c="38138">The test</st> `<st c="38148">Client</st>` <st c="38154">can run and test this</st> `<st c="38177">async</st>` <st c="38182">route function similar to running standard Flask routes using its</st> `<st c="38249">get()</st>`<st c="38254">,</st> `<st c="38256">post()</st>`<st c="38262">,</st> `<st c="38264">delete()</st>`<st c="38272">,</st> `<st c="38274">patch()</st>`<st c="38281">, and</st> `<st c="38287">put()</st>` <st c="38292">methods.</st> <st c="38302">In other</st> <st c="38310">words, the same testing</st> <st c="38334">rules apply to testing asynchronous view functions, as shown in the following</st> <st c="38413">test case:</st>

导入 pytest

from main import app as flask_app

@pytest.fixture(scope="module", autouse=True)

def client():

with flask_app.test_client() as app:

    yield app

def test_add_vote_ws_client(client): ch05-web项目有一个异步 WebSocket 客户端,即add_vote_count_client()视图函数。给定的test_add_vote_ws_client()运行并测试了使用client.get()HTTP GET请求事务。因此,当使用测试Client类运行标准视图函数,以及异步 API 端点函数时,情况相同,如下面的测试用例实现所示:

 @pytest.fixture(autouse=True, scope="module")
def login_details():
    data = {"username": "sjctrags", "password":"sjctrags"}
    yield data
    data = None <st c="39256">@pytest.xfail(reason="An exception is encountered")</st> def test_add_login(client, login_details): <st c="39351">res = client.post("/ch05/login/add",</st> <st c="39387">json=login_details)</st> assert res.status_code == 201
        <st c="39437">The</st> `<st c="39442">add_login()</st>` <st c="39453">API with the</st> `<st c="39467">/ch05/login/add</st>` <st c="39482">URL pattern is an</st> `<st c="39501">async</st>` <st c="39506">API endpoint function that adds new login details to the database.</st> `<st c="39574">test_add_login()</st>` <st c="39590">performs exception testing on the</st> <st c="39625">API to check whether adding existing records will throw an error.</st> <st c="39691">So, the</st> <st c="39698">process and formulation of the test cases are the same as testing their standard counterparts.</st> <st c="39794">But what if the transactions under testing are asynchronous such that test functions need to await to execute them?</st> <st c="39910">How can</st> `<st c="39918">pytest</st>` <st c="39924">directly call an async method?</st> <st c="39956">Let us take a look at testing asynchronous</st> <st c="39999">SQLAlchemy transactions.</st>

        <st c="40023">Testing the asynchronous repository layer</st>

        在`ch05-web`和`ch05-api`项目中使用的 ORM 是异步 SQLAlchemy。<st c="40145">在异步 ORM 中,所有 CRUD 操作都以</st> `<st c="40151">async</st>` <st c="40156">协程的形式运行,需要使用</st> `<st c="40189">await</st>` <st c="40214">关键字来执行。</st> <st c="40245">同样,测试函数需要</st> `<st c="40278">await</st>` <st c="40283">这些待测试的异步组件</st> <st c="40314">以协程的形式执行。</st> <st c="40356">然而,</st> `<st c="40365">pytest</st>` <st c="40371">需要名为</st> `<st c="40408">pytest-asyncio</st>` <st c="40422">的扩展模块来添加对实现异步测试函数的支持。</st> <st c="40484">因此,在实现测试用例之前,请使用以下</st> `<st c="40542">pip</st>` <st c="40545">命令安装</st> `<st c="40500">pytest-asyncio</st>` <st c="40514">模块:</st>
 pip install pytest-asyncio
        <st c="40615">实现方式与之前的相同,只是</st> `<st c="40683">pytest_plugins</st>` <st c="40697">组件,它导入必要的</st> `<st c="40737">pytest</st>` <st c="40743">扩展,例如</st> `<st c="40763">pytest-asyncio</st>`<st c="40777">。`<st c="40783">pytest_plugins</st>` <st c="40797">组件导入已安装的</st> `<st c="40830">pytest</st>` <st c="40836">扩展,并为测试环境添加了`<st c="40881">pytest</st>` <st c="40898">本身无法执行的功能。</st> <st c="40927">使用`<st c="40932">pytest-asyncio</st>`<st c="40946">,现在可以实现对由协程运行的交易的测试,如下面的代码片段所示:</st> <st c="41022">现在可行:</st>
 import pytest
from app.model.config import db_session
from app.model.db import Login
from app.repository.login import LoginRepository <st c="41170">pytest_plugins = ('pytest_asyncio',)</st> @pytest.fixture
def login_details():
    login_details = {"username": "user-1908", "password": "pass9087" }
    login_model = Login(**login_details)
    return login_model <st c="41367">@pytest.mark.asyncio</st>
<st c="41387">async def test_add_login(login_details):</st> async with db_session() as sess:
        async with sess.begin():
            repo = LoginRepository(sess) <st c="41516">res = await repo.insert_login(login_details)</st> assert res is True
        <st c="41579">调用异步方法进行测试始终需要一个测试函数是</st> `<st c="41661">async</st>` <st c="41666">的,因为它需要等待被测试的函数。</st> <st c="41718">给定的</st> `<st c="41728">test_add_login()</st>` <st c="41744">是一个</st> `<st c="41751">async</st>` <st c="41756">方法,因为它需要调用和等待异步</st> `<st c="41815">insert_login()</st>` <st c="41829">事务。</st> <st c="41843">然而,对于</st> `<st c="41856">pytest</st>` <st c="41862">运行一个</st> `<st c="41873">async</st>` <st c="41878">测试函数,它将需要测试函数被</st> <st c="41935">装饰为</st> `<st c="41948">@pytest.mark.asyncio()</st>` <st c="41970">由`<st c="41987">pytest-asyncio</st>` <st c="42001">库提供的。</st> <st c="42011">但是,当 Celery 后台任务将</st> `<st c="42071">进行测试时</st>`,情况会怎样呢?

        <st c="42087">测试 Celery 任务</st>

        `<st c="42108">Pytest</st>` <st c="42115">需要</st> `<st c="42126">pytest-celery</st>` <st c="42139">扩展模块来在测试中运行 Celery 任务。</st> <st c="42192">因此,测试文件需要</st> <st c="42221">包含</st> `<st c="42229">pytest_celery</st>` <st c="42242">在其</st> `<st c="42250">pytest_plugins</st>`<st c="42264">. 以下测试函数</st> <st c="42293">运行</st> `<st c="42303">add_vote_task_wrapper()</st>` <st c="42326">任务,将候选人的投票添加到</st> <st c="42362">数据库:</st>
 import pytest <st c="42390">from app.services.vote_tasks import add_vote_task_wrapper</st> import json <st c="42460">from main import app as flask_app</st>
<st c="42493">pytest_plugins = ('pytest_celery',)</st> @pytest.fixture(scope='session') <st c="42563">def celery_config():</st> yield {
        'broker_url': 'redis://localhost:6379/1',
        'result_backend': 'redis://localhost:6379/1'
    }
@pytest.fixture
def vote():
    login_details = {"voter_id": "BCH-111-789", "election_id": 1, "cand_id": "PHL-102" , "vote_time": "09:11:19" }
    login_str = json.dumps(login_details)
    return login_str
def test_add_votes(vote):
    with flask_app.app_context() as context: <st c="43116">python_celery</st> library in <st c="43141">pytest_plugins</st>. Then, create a test function, such as <st c="43195">test_add_votes()</st>, run the Celery task the usual way with the app’s context, and perform the needed verifications. By the way, running the Celery task with the application’s context means that testing will utilize the configured Redis broker. However, if testing decides not to use the configured Redis configurations (e.g., <st c="43519">broker_url</st>, <st c="43531">result_backend</st>) of <st c="43551">app</st>, <st c="43556">pytest_celery</st> can allow <st c="43580">pytest</st> to inject dummy Redis configurations into its test functions through the fixture, like the given <st c="43684">celery_config()</st>, or override the built-in configuration through <st c="43748">@pytest.mark.celery(result_backend='xxxxx')</st>. Running the task without the default Redis details will lead to a <st c="43859">kombu.connection:connection.py:669 no hostname was</st> <st c="43910">supplied</st> error.
			<st c="43925">Can</st> `<st c="43930">pytest</st>` <st c="43936">create a test case for asynchronous file upload for some</st> <st c="43994">web-based applications?</st>
			<st c="44017">Testing asynchronous file upload</st>
			*<st c="44050">Chapter 6</st>* <st c="44060">showcases an</st> *<st c="44074">Online Housing Pricing Prediction and Analysis</st>* <st c="44120">application, which highlights creating</st> <st c="44159">views that capture data from uploaded</st> *<st c="44198">XLSX</st>* <st c="44202">files for data analysis and graphical plotting.</st> <st c="44251">The project also has</st> `<st c="44272">pytest</st>` <st c="44278">test files that analyze the file uploading</st> <st c="44322">process of some form views and verify the rendition types of their</st> <st c="44389">Flask responses.</st>
			`<st c="44405">Pytest</st>` <st c="44412">supports running and testing web views that involve uploading files of any mime type and converting them to</st> `<st c="44521">FileStorage</st>` <st c="44532">objects for content processing.</st> <st c="44565">Uploading a</st> *<st c="44577">multipart</st>* <st c="44586">file requires the</st> `<st c="44605">client.post()</st>` <st c="44618">function to have the</st> `<st c="44640">content_type</st>` <st c="44652">parameter set to</st> `<st c="44670">multipart/form-data</st>`<st c="44689">, the</st> `<st c="44695">buffered</st>` <st c="44703">parameter to</st> `<st c="44717">True</st>`<st c="44721">, and its</st> `<st c="44731">data</st>` <st c="44735">parameter to a</st> *<st c="44751">dictionary</st>* <st c="44761">consisting of the form parameter name of the file-type form component as the key, and the file object opened as a binary file for reading as its value.</st> <st c="44914">The following test case verifies whether</st> `<st c="44955">/ch06/upload/xlsx/analysis</st>` <st c="44981">can upload an XLSX file, extract some columns, and render them on</st> <st c="45048">HTML tables:</st>

import os

def 测试上传文件(client):

测试文件 = os.getcwd() + "/tests/files/2011Q2.xlsx" <st c="45154">数据 = {</st>

'数据文件': (open(test_file, 'rb'), test_file)

} 响应 = client.post("/ch06/upload/xlsx/analysis", buffered=True, content_type='multipart/form-data', data=data)

assert 响应.status_code == 200

assert 响应.mimetype == "text/html"

			`<st c="45403">test_upload_file()</st>` <st c="45422">fetches some XLSX sample files within the project and opens these as binary files for reading.</st> <st c="45518">The object extracted from the</st> `<st c="45548">open()</st>` <st c="45554">file becomes the value of the</st> `<st c="45585">data_file</st>` <st c="45594">form parameter of the Jinja template.</st> `<st c="45633">client.post()</st>` <st c="45646">will run</st> `<st c="45656">/ch06/upload/xlsx/analysis</st>` <st c="45682">and use the file object as input.</st> <st c="45717">If the</st> `<st c="45724">pytest</st>` <st c="45730">execution has no uploading-related exceptions, the response should emit</st> `<st c="45803">status_code</st>` <st c="45814">of</st> `<st c="45818">200</st>` <st c="45821">with the</st> `<st c="45831">content-type</st>` <st c="45843">header</st> <st c="45851">of</st> `<st c="45854">text/html</st>`<st c="45863">.</st>
			<st c="45864">After testing unsecured</st> <st c="45889">components, let us now</st> <st c="45911">deal with test cases that run views or APIs that require authentication</st> <st c="45984">and authorization.</st>
			<st c="46002">Creating test cases for secured API and web components</st>
			<st c="46057">All applications in</st> *<st c="46078">Chapter 9</st>* <st c="46087">implement the authentication methods essential to small-, middle-, or large-scale Flask applications.</st> `<st c="46190">pytest</st>` <st c="46196">can test secured components, both standard and asynchronous ones.</st> <st c="46263">This</st> <st c="46268">chapter will cover testing Cross-Site Request Forgery- or CSRF-protected views running on an HTTPS with</st> `<st c="46372">flask-session</st>` <st c="46385">managing the user session, HTTP basic authenticated views, and web views secured by the</st> `<st c="46474">flask-login</st>` <st c="46485">extension module.</st>
			<st c="46503">Testing secured API functions</st>
			*<st c="46533">Chapter 9</st>* <st c="46543">showcases a</st> *<st c="46556">Vaccine Reporting and Management</st>* <st c="46588">system with web-based and API-based versions.</st> <st c="46635">The</st> `<st c="46639">ch09-web-passphrase</st>` <st c="46658">project is a web version of the prototype with views</st> <st c="46712">protected by a custom authentication mechanism using the</st> `<st c="46769">flask-session</st>` <st c="46782">module, web forms that are CSRF-protected, and all components running on an</st> <st c="46859">HTTPS protocol.</st>
			<st c="46874">The</st> `<st c="46879">/ch09/login/auth</st>` <st c="46895">route is the entry point to the application, where users must log in using their</st> `<st c="46977">username</st>` <st c="46985">and</st> `<st c="46990">password</st>` <st c="46998">credentials.</st> <st c="47012">To test the secured view routes, the</st> `<st c="47049">/ch09/login/auth</st>` <st c="47065">route must have the first execution in the test function to allow access to other views.</st> <st c="47155">The following test case runs the</st> `<st c="47188">/ch09/patient/profile/add</st>` <st c="47213">view without</st> <st c="47227">user authentication:</st>

import pytest

from flask import url_for

from main import app as flask_app

@pytest.fixture

def client(): flask_app.config["WTF_CSRF_ENABLED"] = False with flask_app.test_client() as app:

    yield app

@pytest.fixture(scope="module")

def 用户凭证():

参数 = dict()

参数["用户名"] = "sjctrags"

参数["密码"] = "sjctrags"

return 参数

def 测试患者档案添加无效访问(client): res = client.get("/ch09/patient/profile/add", base_url='https://localhost') assert res.status_code == 302 pytest 要访问和运行 /ch09/patient/profile/add,这是一个表单视图,您必须首先使用其 flask_wtf.csrf 模块的 WTF_CSRF_ENABLED 内置环境变量禁用 CSRF 保护,然后在 client() 修复件中提取测试 Client 实例之前。使用正确的用户凭证运行给定的测试函数将显示成功结果,因为访问未经身份验证,导致重定向到 /ch09/login/auth 视图。到目前为止,应用程序使用自定义身份验证,但由 flask-session 扩展模块管理的数据库加密用户名。

        <st c="48408">此测试证明,访问我们</st> *<st c="48459">在线疫苗注册</st>* <st c="48486">应用程序的任何视图都需要从其</st> `<st c="48537">/ch09/login/auth</st>` <st c="48553">视图页面进行用户身份验证。</st> <st c="48565">任何未经身份验证的访问尝试都会将用户重定向到登录页面。</st> <st c="48647">以下片段使用用户身份验证构建了访问我们应用程序视图的正确访问流程:</st>
 def test_patient_profile_add_valid_access(client, user_credentials): <st c="48821">res_login = client.post('/ch09/login/auth',</st> <st c="48864">data=user_credentials,</st> <st c="48887">base_url='https://localhost')</st> assert res_login.status_code == 302
        assert res_login.location.split('?')[0] == url_for('view_signup') <st c="49020">res = client.get("/patient/profile/add",</st> <st c="49060">base_url='https://localhost:5000')</st> assert res.status_code == 200 <st c="49208">test_patient_profile_add_valid_access()</st> has the same test flow as in the <st c="49282">ch02-blueprint</st> project. The only difference is the presence of the <st c="49349">base_url</st> parameter in <st c="49371">client.post()</st> since the view runs on an HTTPS platform. The goal of the test is to run <st c="49458">/ch09/profile/add</st> successfully after logging into <st c="49508">/ch09/login/auth</st> with the correct login details. Also, this test function verifies whether the <st c="49603">flask-session</st> module is working on saving the user data in the <st c="49666">session</st> object.
			<st c="49681">How about testing APIs secured by HTTP-based authentication mechanisms that use the</st> `<st c="49766">Authorization</st>` <st c="49779">header?</st> <st c="49788">How does</st> `<st c="49797">pytest</st>` <st c="49803">run these types of secured API</st> <st c="49835">endpoint functions?</st>
			<st c="49854">Testing HTTP Basic authentication</st>
			<st c="49888">The</st> `<st c="49893">ch09-api-auth-basic</st>` <st c="49912">project, an API-based version of the</st> *<st c="49950">Online Vaccine Registration</st>* <st c="49977">application, uses the HTTP basic authentication scheme to secure all API endpoint access.</st> <st c="50068">An</st> `<st c="50071">Authorization</st>` <st c="50084">header with the</st> `<st c="50101">base64</st>`<st c="50107">-encoded</st> `<st c="50117">username:password</st>` <st c="50134">credential</st> <st c="50146">must be part of the request headers to access an API.</st> <st c="50200">Moreover, the access is also</st> <st c="50228">restricted by the</st> `<st c="50247">flask-cors</st>` <st c="50257">extension module.</st> <st c="50276">The following test case accesses</st> `<st c="50309">/ch09/vaccine/add</st>` <st c="50326">without authentication:</st>

import pytest

from main import app as flask_app import base64 @pytest.fixture

def client():

with flask_app.test_client() as app:

    yield app

@pytest.fixture

def 疫苗():

vacc = {"vacid": "VAC-899", "vacname": "Narvas", "vacdesc": "For Hypertension", "qty": 5000, "price": 1200.5, "status": True}

return vacc

def test_add_vaccine_unauth(client, vaccine):

res = client.post("/ch09/vaccine/add", json=vaccine, <st c="50758">headers={'Access-Control-Allow-Origin': "http://localhost:5000"}</st>)

assert res.status_code == 201

			<st c="50854">The test</st> `<st c="50864">Client</st>` <st c="50870">methods have</st> `<st c="50884">header</st>` <st c="50890">parameters that can contain a dictionary of request headers, such as</st> `<st c="50960">Access-Control-Allow-Headers</st>`<st c="50988">,</st> `<st c="50990">Access-Control-Allow-Methods</st>`<st c="51018">,</st> `<st c="51020">Access-Control-Allow-Credentials</st>`<st c="51052">, and</st> `<st c="51058">Access-Control-Allow-Origin</st>` <st c="51085">for managing the</st> <st c="51102">application’s</st> `<st c="51275">Authorization</st>` <st c="51288">header is present with the</st> `<st c="51316">Basic</st>` <st c="51321">credential.</st> <st c="51334">The</st> <st c="51337">following snippet is the correct test case for successful access to the API endpoint secured by the</st> <st c="51438">basic scheme:</st>

@pytest.fixture def auth_header():credentials = base64.b64encode(b'sjctrags:sjctrags') .decode('utf-8') return credentials

def test_add_vaccine_auth(client, vaccine, auth_header):

res = client.post("/vaccine/add", json=vaccine, <st c="51682">headers={'Authorization': 'Basic ' + auth_header, 'Access-Control-Allow-Origin': "http://localhost:5000"}</st>)

assert res.status_code == 201

			<st c="51819">The preceding test case will show a successful result given the correct</st> `<st c="51892">base64</st>`<st c="51898">-encoded credentials.</st> <st c="51921">The inclusion of the</st> `<st c="51942">Authorization</st>` <st c="51955">header with the</st> `<st c="51972">Basic</st>` <st c="51977">and</st> `<st c="51982">base64</st>`<st c="51988">-encoded credentials from the</st> `<st c="52019">auth_header()</st>` <st c="52032">fixture in the</st> `<st c="52048">header</st>` <st c="52054">parameter of</st> `<st c="52068">client.post()</st>` <st c="52081">will fix the HTTP Status Code</st> <st c="52112">403 error.</st>
			<st c="52122">The</st> `<st c="52127">Authorization</st>` <st c="52140">header must be in the</st> `<st c="52163">header</st>` <st c="52169">parameter of any</st> `<st c="52187">Client</st>` <st c="52193">method when testing and running the API endpoint secured by HTTP basic, digest, and bearer-token authentication schemes.</st> <st c="52315">In the</st> `<st c="52322">Authorization Digest</st>` <st c="52342">header, the</st> *<st c="52355">nonce</st>*<st c="52360">,</st> *<st c="52362">opaque</st>*<st c="52368">, and</st> *<st c="52374">nonce count</st>* <st c="52385">must be</st> <st c="52393">part of the header details.</st> <st c="52422">On the other hand, the token-based scheme needs a secure</st> `<st c="52514">Authorization Bearer</st>` <st c="52534">header or with the</st> `<st c="52554">token_auth</st>` <st c="52564">parameter of the test</st> `<st c="52587">Client</st>` <st c="52593">methods.</st>
			<st c="52602">But how does</st> `<st c="52616">pytest</st>` <st c="52622">scrutinize</st> <st c="52633">the view routes secured by the</st> `<st c="52665">flask-login</st>` <st c="52676">extension?</st> <st c="52688">Is there an</st> <st c="52699">added behavior that</st> `<st c="52720">pytest</st>` <st c="52726">should adopt when testing views secured</st> <st c="52767">by</st> `<st c="52770">flask-login</st>`<st c="52781">?</st>
			<st c="52782">Testing web logins</st>
			<st c="52800">The</st> `<st c="52805">ch09-web-login</st>` <st c="52819">application is</st> <st c="52835">another version of the</st> *<st c="52858">Online Vaccine Registration</st>* <st c="52885">application that uses the</st> `<st c="52912">flask-login</st>` <st c="52923">module as its source for security.</st> <st c="52959">It uses the</st> `<st c="52971">flask-session</st>` <st c="52984">module to store the user session in the file system instead of the browser.</st> <st c="53061">Like in the</st> `<st c="53073">ch09-web-passphrase</st>` <st c="53092">and</st> `<st c="53097">ch02-blueprint</st>` <st c="53111">projects, users</st> <st c="53128">must log into the application before accessing any views.</st> <st c="53186">Otherwise, the application will redirect them to the login page.</st> <st c="53251">The following test case is similar to the previous test files where the test accesses</st> `<st c="53337">/ch09/login/auth</st>` <st c="53353">first before accessing any views</st> <st c="53387">or APIs:</st>

import pytest

from flask_login import current_user

from main import app as flask_app

def test_add_admin_profile(client, admin_details, user_credentials): res_login = client.post('/ch09/login/auth', data=user_credentials)assert res_login.status_code == 200with client.session_transaction() as session:assert current_user.username == "sjctrags"res = client.post("/ch09/admin/profile/add", data=admin_details) assert res.status_code == 200


			<st c="53836">The given</st> `<st c="53847">test_admin_admin_profile()</st>` <st c="53873">will run the</st> `<st c="53887">/ch09/admin/profile/add</st>` <st c="53910">route with a successful result given the valid</st> `<st c="53958">user_credentials()</st>` <st c="53976">fixture.</st> <st c="53986">One advantage of using the</st> `<st c="54013">flask-login</st>` <st c="54024">module compared to custom session-handling is the</st> `<st c="54075">current_user</st>` <st c="54087">object it has that can give proof if the user login transaction created a session, if a user depicted in the</st> `<st c="54197">user_credentials()</st>` <st c="54215">fixture is the one</st> <st c="54234">stored in its session, or if the authentication was done using the</st> *<st c="54302">remember me</st>* <st c="54313">feature.</st> <st c="54323">The given test function verifies whether</st> `<st c="54364">username</st>` <st c="54372">indicated in the</st> `<st c="54390">user_credentials()</st>` <st c="54408">fixture is the one saved in the</st> `<st c="54441">flask-login</st>` <st c="54452">session.</st>
			<st c="54461">Another feature of</st> `<st c="54481">flask-login</st>` <st c="54492">that is</st> <st c="54501">beneficial to</st> `<st c="54515">pytest</st>` <st c="54521">is its capability to turn off all the authentication mechanisms during testing.</st> <st c="54602">The following test class runs the same</st> `<st c="54641">/ch09/admin/profile/add</st>` <st c="54664">route successfully without</st> <st c="54692">logging in:</st>

@pytest.fixture

def client(): flask_app.config["LOGIN_DISABLED"] = True with flask_app.test_client() as app:

    yield app

def test_add_admin_profile(client, admin_details):

res = client.post("/admin/profile/add", data=admin_details)

assert res.status_code == 200

			<st c="54963">Disabling authentication in</st> `<st c="54992">flask-login</st>` <st c="55003">requires setting its built-in</st> `<st c="55034">LOGIN_DISABLED</st>` <st c="55048">environment variable to</st> `<st c="55073">True</st>` <st c="55077">at the configuration level.</st> <st c="55106">The setup should be part of the</st> `<st c="55138">client()</st>` <st c="55146">fixture before extracting the test</st> `<st c="55182">Client</st>` <st c="55188">object from the Flask’s</st> `<st c="55213">app</st>` <st c="55216">instance.</st>
			<st c="55226">Pytest and its add-ons can test all authentication schemes and authorization rules applied to Flask 3.x apps.</st> <st c="55337">Using the</st> <st c="55346">same GWT unit testing strategy and behavioral testing mechanisms, such as mocking and monkey patching,</st> `<st c="55450">pytest</st>` <st c="55456">is a complete and adequate testing library to run and verify secured APIs and</st> <st c="55535">view routes.</st>
			<st c="55547">How can</st> `<st c="55556">pytest</st>` <st c="55562">mock a</st> <st c="55569">MongoDB connection when running and testing routes?</st> <st c="55622">Let’s</st> <st c="55628">learn how.</st>
			<st c="55638">Creating test cases for MongoDB transactions</st>
			<st c="55683">The formulation of the test files, classes, and functions is the same when testing components with the Flask application</st> <st c="55805">running on MongoDB.</st> <st c="55825">The</st> <st c="55828">only difference is how</st> `<st c="55852">pytest</st>` <st c="55858">will mock the MongoDB connection to</st> <st c="55895">pursue testing.</st>
			<st c="55910">This chapter showcases the</st> `<st c="55938">mongomock</st>` <st c="55947">module and its</st> `<st c="55963">MongoClient</st>` <st c="55974">mock object that can replace a configured MongoDB connection.</st> <st c="56037">So, install the</st> `<st c="56053">mongomock</st>` <st c="56062">module using the following</st> `<st c="56090">pip</st>` <st c="56093">command before creating the</st> <st c="56122">test file:</st>

pip install mongomock


			*<st c="56154">Chapter 7</st>* <st c="56164">has a</st> *<st c="56171">Tutor Finder</st>* <st c="56183">application with components running on NoSQL databases such as MongoDB.</st> <st c="56256">The application uses the</st> `<st c="56281">connect()</st>` <st c="56290">method of the</st> `<st c="56305">mongoengine</st>` <st c="56316">module to establish a MongoDB connection for a few of the APIs.</st> <st c="56381">Instead of using the configured connection in the Flask’s</st> `<st c="56439">app</st>` <st c="56442">context, a</st> `<st c="56454">MongoClient</st>` <st c="56466">object from</st> `<st c="56478">mongomock</st>` <st c="56487">can replace the</st> `<st c="56504">mongoengine</st>`<st c="56515">’s</st> `<st c="56519">connect()</st>` <st c="56528">method with a fake one.</st> <st c="56553">The following snippet of the</st> `<st c="56582">test_tutor_login.py</st>` <st c="56601">file mocks the MongoDB connection to run</st> `<st c="56643">insert_login()</st>` <st c="56657">of</st> `<st c="56661">LoginRepository</st>`<st c="56676">:</st>

import pytest 导入 mongomock

从 mongoengine 导入 connect, get_connection, disconnect from main 导入 app 作为 flask_app

from modules.repository.mongo.tutor_login import TutorLoginRepository

from bcrypt import hashpw, gensalt

@pytest.fixture

def login_details():

login = dict()

login["username"] = "sjctrags"

login["password"] = "sjctrags"

login["encpass"] = hashpw(str(login['username']) .encode(), gensalt())

return login

@pytest.fixture

def client():

disconnect()

with flask_app.test_client() as client:

yield client <st c="57203">@pytest.fixture</st>

def connect_db():connect(host='localhost', port=27017, db='tfs_test', uuidRepresentation='standard', mongo_client_class=mongomock.MongoClient) conn = get_connection() return conn


			<st c="57400">The given</st> `<st c="57411">connect_db()</st>` <st c="57423">recreates the</st> `<st c="57438">MongoClient</st>` <st c="57449">object using the same</st> `<st c="57472">mongoengine</st>`<st c="57483">’s</st> `<st c="57487">connect()</st>` <st c="57496">method but now with the fake</st> `<st c="57526">MongoClient</st>`<st c="57537">. However, the parameter values of</st> `<st c="57572">connect()</st>`<st c="57581">, like the values of</st> `<st c="57602">db</st>`<st c="57604">,</st> `<st c="57606">host</st>`<st c="57610">, and</st> `<st c="57616">port</st>`<st c="57620">, must be part of the testing environment setup.</st> <st c="57669">Also, the</st> `<st c="57679">uuidRepresentation</st>` <st c="57697">parameter must</st> <st c="57713">be present in</st> <st c="57727">the mocking.</st>
			<st c="57739">After the mocked</st> `<st c="57757">connect()</st>` <st c="57766">setup, it</st> <st c="57776">needs to call the</st> `<st c="57795">mongoengine</st>`<st c="57806">’s</st> `<st c="57810">get_connection()</st>` <st c="57826">and yield it to the test function.</st> <st c="57862">So, the connection created from a mocked</st> `<st c="57903">MongoClient</st>` <st c="57914">is fake but with the existing database</st> <st c="57954">configuration details.</st>
			<st c="57976">Now, before injecting the</st> `<st c="58003">connect_db()</st>` <st c="58015">fixture to the test functions, call the</st> `<st c="58056">disconnect()</st>` <st c="58068">method to kill an existing connection in the Flask</st> `<st c="58120">app</st>` <st c="58123">context and avoid multiple connections running in the background, which will cause an error.</st> <st c="58217">The following test function has the injected mocked MongoDB connection for testing the</st> `<st c="58304">insert_login()</st>` <st c="58318">MongoDB</st> <st c="58327">repository transaction:</st>

def test_add_login(client, connect_db, login_details):

repo = TutorLoginRepository()

res = repo.insert_login(login_details)

assert res is True

			<st c="58493">Aside from</st> `<st c="58505">mongomock</st>`<st c="58514">, the</st> `<st c="58520">pytest-mongo</st>` <st c="58532">and</st> `<st c="58537">pytest-mongodb</st>` <st c="58551">modules allow mocking</st> `<st c="58574">mongoengine</st>` <st c="58585">models and collections by using the actual MongoDB</st> <st c="58637">database configuration.</st>
			<st c="58660">Can</st> `<st c="58665">pytest</st>` <st c="58671">run and test WebSocket endpoints created by</st> `<st c="58716">flask-sock</st>`<st c="58726">? Let us implement a test case that will</st> <st c="58767">analyze WebSockets.</st>
			<st c="58786">Creating test cases for WebSockets</st>
			<st c="58821">WebSockets are components of our</st> `<st c="58855">ch05-web</st>` <st c="58863">and</st> `<st c="58868">ch05-api</st>` <st c="58876">projects.</st> <st c="58887">The applications use</st> `<st c="58908">flask-sock</st>` <st c="58918">to implement the</st> <st c="58935">WebSocket endpoints.</st> <st c="58957">So far,</st> `<st c="58965">pytest</st>` <st c="58971">can only provide the testing</st> <st c="59000">environment for WebSockets.</st> <st c="59029">However, it needs the</st> `<st c="59051">websockets</st>` <st c="59061">module to run, test, and assert the response of our WebSocket endpoints.</st> <st c="59135">So, install this module using the following</st> `<st c="59179">pip</st>` <st c="59182">command:</st>

pip install websockets


			<st c="59214">There are three components that the</st> `<st c="59251">websockets</st>` <st c="59261">module can provide</st> <st c="59281">to</st> `<st c="59284">pytest</st>`<st c="59290">:</st>

				*   <st c="59292">The simulated route that will receive the message from</st> <st c="59347">the client</st>
				*   <st c="59357">The</st> <st c="59362">mock server</st>
				*   <st c="59373">The test function that will serve as</st> <st c="59411">the client</st>

			<st c="59421">All these components must</st> <st c="59447">be</st> `<st c="59451">async</st>` <st c="59456">because running WebSockets requires the</st> `<st c="59497">asyncio</st>` <st c="59504">platform.</st> <st c="59515">So, also</st> <st c="59523">install the</st> `<st c="59536">pytest-asyncio</st>` <st c="59550">module to give asynchronous support to these</st> <st c="59596">three components:</st>

Pip install pytest-asyncio


			<st c="59640">Then, start implementing the simulated or mocked view similar to the following implementation to receive and process the messages sent by</st> <st c="59779">a WebSocket:</st>

导入 websockets import pytest

import json import pytest_asyncio

pytest_plugins = ('pytest_asyncio',)

异步 def simulated_add_votecount_view(websocket): 异步 for message in websocket: print("received: ",message)

    # 在此处放置 VoteCount 仓库事务 <st c="60087">simulated_add_votecount_view()</st>将作为模拟 WebSocket 端点函数,该函数接收并将计票结果保存到数据库中。

        <st c="60234">接下来,使用</st> `<st c="60272">websockets.serve()</st>` <st c="60290">方法创建一个模拟服务器来运行模拟路由</st> <st c="60324">在</st> `<st c="60361">主机</st>`<st c="60365">,</st> `<st c="60367">端口</st>`<st c="60371">,以及模拟视图名称,例如</st> `<st c="60410">simulated_add_votecount_view</st>`<st c="60438">,进行操作。</st> <st c="60452">以下是我们 WebSocket 服务器,它</st> <st c="60497">将在</st> `<st c="60513">ws://localhost:5001</st>` <st c="60532">地址上运行:</st>
<st c="60541">@pytest_asyncio.fixture</st>
<st c="60565">async</st> def create_ws_server(): <st c="60596">async with websockets.serve(</st> <st c="60624">simulated_add_votecount_view,  "localhost", 5001</st>) as server:
        yield server
        <st c="60698">由于</st> `<st c="60705">create_ws_server()</st>` <st c="60723">必须是</st> `<st c="60732">异步</st>`<st c="60737">的,用</st> `<st c="60758">@pytest.fixture</st>` <st c="60773">装饰它将导致错误。</st> <st c="60795">因此,使用</st> `<st c="60803">@pytest_asyncio.fixture</st>` <st c="60826">来声明</st> `<st c="60864">pytest</st>`<st c="60874">的异步固定值。</st>

        <st c="60875">最后,我们使用上下文管理器开始测试函数的实现,该上下文管理器打开</st> `<st c="60967">websockets</st>` <st c="60977">客户端对象以执行 WebSocket 端点,并在之后关闭它。</st> <st c="61050">以下实现显示了一个针对</st> `<st c="61109">add_vote_count_server()</st>` <st c="61132">WebSocket 的测试函数,该函数使用</st> `<st c="61152">ws://localhost:5001/ch05/vote/save/ws</st>` <st c="61189">URL 地址:</st>
<st c="61202">@pytest.mark.asyncio</st>
<st c="61223">async</st> def test_votecount_ws(<st c="61252">create_ws_server</st>, vote_tally_details): <st c="61292">async with websockets.connect(</st> <st c="61322">"ws://localhost:5001/ch05/vote/save/ws"</st>) as websocket: <st c="61582">websockets.connect()</st> method with the URI of the WebSocket as its parameter argument. The client object can send a string or numeric message to the simulated route and receive a string or numeric response from that server. This send-and-receive process will only happen once per execution of the test function. Since the <st c="61902">with</st>-<st c="61908">context</st> manager, <st c="61925">send()</st>, and <st c="61937">recv()</st> are all awaited, the test function must be <st c="61987">async</st>. Now, use the <st c="62007">assert</st> statement to verify whether our client receives the proper message from the server.
			<st c="62097">Another way to test the WebSocket endpoint is to use the actual development environment, for instance, running our</st> `<st c="62213">ch05-web</st>` <st c="62221">project with the PostgreSQL database, Redis, and the</st> `<st c="62355">test_websocket_actual()</st>` <st c="62378">method runs the same WebSocket server</st> <st c="62417">without monkey patching or a</st> <st c="62446">mocked server:</st>

import pytest

import websockets

import json

pytest_plugins = ('pytest_asyncio',)

@pytest.fixture(scope="module", autouse=True)

def vote_tally_details():

tally = {"election_id":"1", "precinct": "111-C", "final_tally": "6000", "approved_date": "2024-10-10"}

yield tally

tally = None

@pytest.mark.asyncio

async def test_websocket_actual(vote_tally_details): 异步 with websockets.connect("ws://localhost:5001/ch05/ vote/save/ws") as websocket:

        await websocket.send(json.dumps( vote_tally_details))

        response = await websocket.recv()

        断言响应等于"data not added"

			<st c="63026">The test method adds a new vote tally to the database.</st> <st c="63082">If the voting precinct number of the record is not yet in the table, then the WebSocket will return the</st> `<st c="63186">"data added"</st>` <st c="63199">message to the client.</st> <st c="63222">Otherwise, it will return the</st> `<st c="63252">"data not added"</st>` <st c="63268">message.</st> <st c="63278">This approach also tests the correct configuration details of the Redis and PostgreSQL servers used by the WebSocket endpoint.</st> <st c="63405">Others may mock the Redis connectivity and PostgreSQL database connection</st> <st c="63478">to focus on the WebSocket implementation and refine its</st> <st c="63535">client response.</st>
			<st c="63551">Testing Flask components</st> <st c="63576">should focus on different perspectives to refine the application’s performance and quality.</st> <st c="63669">Unit testing components using monkey patching or mocking is an effective way of refining, streamlining, and scrutinizing the inputs and results.</st> <st c="63814">However, most often, the integration testing with the servers, internal modules, and external dependencies included can help identify and resolve major technical issues, such as compatibility and versioning problems, bandwidth and connection overhead, and</st> <st c="64070">performance issues.</st>
			<st c="64089">Summary</st>
			<st c="64097">There are many strategies and approaches in testing Flask applications, but this chapter focuses on the components found in our applications from</st> *<st c="64244">Chapters 1</st>* <st c="64254">to</st> *<st c="64258">9</st>*<st c="64259">. Also, the goal is to build test cases using the straightforward syntax of the</st> `<st c="64339">pytest</st>` <st c="64345">module.</st>
			<st c="64353">This chapter started with testing the standard Flask components with the web views, API functions, repository transactions, and native services.</st> <st c="64499">Aside from simply running the components and verifying their response details using the</st> `<st c="64587">assert</st>` <st c="64593">statement, mocking becomes an essential ingredient in many test cases of this chapter.</st> <st c="64681">The</st> `<st c="64685">patch()</st>` <st c="64692">decorator from the</st> `<st c="64712">unittest</st>` <st c="64720">module mocks the</st> `<st c="64738">psycopg2</st>` <st c="64746">connections, repository transactions in views and services, and the SQLAlchemy utility methods.</st> <st c="64843">This chapter also discussed monkey patching, which replaces a function with a mock one, and exception testing, which determines raised exceptions and</st> <st c="64993">undetected bugs.</st>
			<st c="65009">This chapter also established proof that it is easier to test asynchronous</st> `<st c="65085">Flask[async]</st>` <st c="65097">components, such as asynchronous SQLAlchemy transactions, services, views, and API endpoints, using</st> `<st c="65198">pytest</st>` <st c="65204">and its</st> `<st c="65213">pytest-asyncio</st>` <st c="65227">module.</st> <st c="65236">On the other hand, another module called</st> `<st c="65277">pytest-celery</st>` <st c="65290">helps</st> `<st c="65297">pytest</st>` <st c="65303">examine and verify the</st> <st c="65327">Celery tasks.</st>
			<st c="65340">However, the most challenging part is how this chapter uses</st> `<st c="65401">pytest</st>` <st c="65407">to examine components from secured applications, run repository transactions that connect to MongoDB, and analyze and</st> <st c="65526">build WebSockets.</st>
			<st c="65543">It is always recommended to apply testing on Flask components during development to study the process flows, runtime performance, and the feasibility of</st> <st c="65697">the implementations.</st>
			<st c="65717">The next chapter will discuss the different deployment strategies of our</st> <st c="65791">Flask applications.</st>






第十二章:11

部署 Flask 应用程序

当 Flask 应用程序的开发完成时,你总是可以决定将其部署到 Werkzeug 的 HTTP 服务器之外的地方。 最终的应用程序需要一个快速且可靠的、具有最小或无潜在安全风险的、可配置且易于管理的生产服务器。 除了利用内置的 Werkzeug 服务器外,产品需要一个非开发服务器,该服务器不是用于开发、调试或测试,而是用于运行软件产品。 Flask 部署需要一个稳定且独立的 Python 服务器或 托管平台。

本章将重点介绍将 Flask 应用程序部署到适合产品范围、环境和 目标的生产服务器的不同方法、选项和程序。

本章将涵盖以下主题:

  • 在 Gunicorn 和 uWSGI 上运行应用程序

  • 在 Uvicorn 上运行应用程序

  • 将应用程序部署到 Apache HTTP 服务器

  • 将应用程序部署到 Docker

  • 将应用程序部署到 Kubernetes

  • 使用 NGINX 创建一个 API 网关

技术要求

我们的应用程序将使用 PostgreSQL 来管理其数据。 项目还将应用 <st c="1191">蓝图</st> 方法来管理 Flask 组件。 项目原型将专注于小型杂货店的简单电子商务、库存和库存交易,并将被称为 在线杂货店 应用程序。 所有这些应用程序都可以在 以下位置找到 https://github.com/PacktPublishing/Mastering-Flask-Web-Development/tree/main/ch11

准备部署

在本章中,我们将创建一个 在线杂货店 应用程序,该应用程序可以部署到不同的平台。 该应用程序是一个基于 API 的类型,具有管理、登录、库存、库存、订单和购买模块,专为小型购物或 杂货店的商业交易设计。

Peewee ORM 构建应用程序的模型和仓库层。 要利用标准 <st c="1959">Peewee</st> 模块,请使用以下 <st c="1993">pip</st> 命令安装它和 <st c="2029">psycopg2</st> 驱动程序:

 pip install psycopg2 peewee

Peewee ORM 提供了标准的 INSERT, UPDATE, DELETE, 和 SELECT 事务,因此包括 <st c="2175">psycopg2</st> 驱动程序作为依赖库。 让我们开始构建 Peewee ORM 的模型层。

标准 Peewee ORM 的类和方法

我们的 在线杂货店 应用程序 部署到一个 Gunicorn 服务器 上,并使用标准的 Peewee 辅助类和方法来建立模型层和仓库类。 以下是针对 PostgreSQL 数据库连接的典型 Peewee 配置: 数据库连接:

<st c="2578">(app/models/config.py)</st> from peewee import PostgresqlDatabase <st c="2640">database = PostgresqlDatabase(</st> 'ogs', user='postgres', password='admin2255', <st c="2775">PostgresqlDatabase</st>, <st c="2795">MySQLDatabase</st>, and <st c="2814">SqliteDatabase</st> driver classes that will create a connection object for the application. Our option is <st c="2916">PostgresqlDatabase</st>, as shown in the preceding code, since our application uses the <st c="3065">autocommit</st> constructor parameter to <st c="3101">False</st> to enable transaction management for CRUD operations.
			<st c="3160">The</st> `<st c="3165">database</st>` <st c="3173">connection object will map Peewee’s model classes to their actual table schemas.</st> <st c="3255">The</st> <st c="3259">following are some model classes of</st> <st c="3295">our applications:</st>

(app/models/db.py)

from app.models.config import database from peewee import Model, CharField, IntegerField, BigIntegerField, ForeignKeyField, DateField class Product(Model): id = BigIntegerField(primary_key=True, null=False, sequence="product_id_seq")

code = CharField(max_length="20", unique="True", null=False)

name = CharField(max_length="100", null=False) <st c="3676">btype = ForeignKeyField(model=Brand, null=False,</st> <st c="3724">to_field="code", backref="brand")</st><st c="3758">ctype = ForeignKeyField(model=Category, null=False,</st> <st c="3810">to_field="code", backref="category")</st> … … … … … … <st c="4014">Product</st>模型类表示杂货店销售产品的记录详情,而下面的<st c="4124">Stock</st>模型创建关于产品的库存信息:
<st c="4178">class Stock(Model):</st> id = BigIntegerField(<st c="4220">primary_key=True</st>, null=False, <st c="4251">sequence="stock_id_seq"</st>) <st c="4277">sid = ForeignKeyField(model=Supplier, null=False,</st> <st c="4326">to_field="sid", backref="supplier")</st><st c="4362">invcode = ForeignKeyField(model=InvoiceRequest,</st> <st c="4410">null=False, to_field="code", backref="invoice")</st> qty = IntegerField(null=False)
    payment_date = DateField(null=True)
    received_date = DateField(null=False)
    recieved_by = CharField(max_length="100") <st c="4606">class Meta:</st><st c="4617">db_table = "stock"</st><st c="4636">database = database</st> … … … … … …
        <st c="4667">所有模型</st> <st c="4678">类必须</st> <st c="4691">继承 Peewee 的</st> `<st c="4709">Model</st>` <st c="4714">类以成为数据库表的逻辑表示。</st> <st c="4783">Peewee 模型类,如给定的</st> `<st c="4824">Product</st>` <st c="4831">和</st> `<st c="4836">Stock</st>`<st c="4841">,有</st> `<st c="4852">Meta</st>` <st c="4856">类,它包含</st> `<st c="4880">database</st>` <st c="4888">和</st> `<st c="4893">db_table</st>` <st c="4901">属性,负责将它们映射到我们数据库的物理表。</st> <st c="4982">Peewee 的列辅助类构建模型类的列属性。</st> <st c="5063">现在,</st> `<st c="5072">main.py</st>` <st c="5079">模块必须启用 Flask 的</st> `<st c="5103">before_request()</st>` <st c="5119">全局事件来处理数据库连接。</st> <st c="5177">以下片段显示了</st> `<st c="5231">before_request()</st>` <st c="5247">全局事件的实现:</st>
 from app import create_app
from app.models.config import database
app = create_app('../config_dev.toml') <st c="5367">@app.before_request</st> def db_connect(): <st c="5405">database.connect()</st>
<st c="5423">@app.teardown_request</st> def db_close(exc): <st c="5517">teardown_request()</st> closes the connection during server shutdown.
			<st c="5581">Like</st> <st c="5587">in SQLAlchemy, the Peewee ORM needs the model classes to create the transaction layer to</st> <st c="5676">perform the CRUD operations.</st> <st c="5705">The following is a</st> `<st c="5724">ProductRepository</st>` <st c="5741">class that manages and executes SQL statements using the standard</st> <st c="5808">Peewee transactions:</st>

从 app.models.db 导入 Product

从 app.models.db 导入 database

从 typing 导入 Dict, Any

class ProductRepository:

def insert_product(self, details:Dict[str, Any]) -> bool:

    try: <st c="6015">使用 database.atomic() 作为 tx:</st><st c="6044">Product.create(**details)</st><st c="6070">tx.commit()</st> 返回 True

    except Exception as e:

        打印(e)

    返回 False

			<st c="6139">The Peewee repository class derives its transaction management from the</st> `<st c="6212">database</st>` <st c="6220">connection object.</st> <st c="6240">Its emitted</st> `<st c="6252">atomic()</st>` <st c="6260">method provides a transaction object that performs</st> `<st c="6312">commit()</st>` <st c="6320">and</st> `<st c="6325">rollback()</st>` <st c="6335">during SQL execution.</st> <st c="6358">The given</st> `<st c="6368">insert_product()</st>` <st c="6384">function performs an</st> `<st c="6406">INSERT</st>` <st c="6412">operation of a</st> `<st c="6428">Product</st>` <st c="6435">record by calling the model’s</st> `<st c="6466">create()</st>` <st c="6474">class method with the</st> `<st c="6497">kwargs</st>` <st c="6503">variable of details and returns</st> `<st c="6536">True</st>` <st c="6540">if the operation is successful.</st> <st c="6573">Otherwise, it</st> <st c="6587">returns</st> `<st c="6595">False</st>`<st c="6600">.</st>
			<st c="6601">On the</st> <st c="6609">other hand, an</st> `<st c="6624">UPDATE</st>` <st c="6630">operation in standard Peewee requires a transaction layer to retrieve the</st> <st c="6705">record object that needs an update, access its concerned field(s), and replace them with new values.</st> <st c="6806">The following</st> `<st c="6820">update_product()</st>` <st c="6836">function shows the implementation of a</st> `<st c="6876">Product</st>` <st c="6883">update:</st>

def update_product(self, details:Dict[str,Any]) -> bool:

try: <st c="6954">使用 database.atomic() 作为 tx:</st><st c="6983">prod = Product.get(</st> <st c="7003">Product.code==details["code"])</st> prod.rate = details["名称"]

            prod.code = details["类型"]

            prod.rate = details["类型"]

            prod.code = details["单位类型"]

            prod.rate = details["售价"]

            prod.code = details["采购价格"]

            prod.rate = details["折扣"] <st c="7258">prod.save()</st><st c="7269">tx.commit()</st> 返回 True

except Exception as e:

    打印(e)

返回 False

			<st c="7338">The</st> `<st c="7343">get()</st>` <st c="7348">method of the</st> <st c="7363">model class retrieves a single instance matching the given query constraint.</st> <st c="7440">The goal is to update only one record, so be sure that the constraint parameters in the record object retrieval only involve the</st> `<st c="7569">unique</st>` <st c="7575">or</st> `<st c="7579">primary key</st>` <st c="7590">column fields.</st>
			<st c="7605">Now, the</st> `<st c="7615">save()</st>` <st c="7621">method of the</st> <st c="7636">record object will eventually merge the new record object with the old one linked to the database.</st> <st c="7735">This</st> `<st c="7740">commit()</st>` <st c="7748">will finally persist and flush the updated record to</st> <st c="7802">the table.</st>
			<st c="7812">When</st> <st c="7818">it comes to deletion, the initial step is similar to updating a record, which involves retrieving the record</st> <st c="7927">object for deletion.</st> <st c="7948">The following</st> `<st c="7962">delete_product_code()</st>` <st c="7983">repository method depicts this</st> <st c="8015">initial process:</st>

def delete_product_code(self, code:str) -> bool:

    try: <st c="8086">使用 database.atomic() 作为 tx:</st><st c="8115">prod = Product.get(Product.code==code)</st><st c="8154">prod.delete_instance()</st><st c="8177">tx.commit()</st> 返回 True

    except Exception as e:

        打印(e)

    返回 False

			<st c="8246">The record object has a</st> `<st c="8271">delete_instance()</st>` <st c="8288">function that removes the record from the schema.</st> <st c="8339">In the case of</st> `<st c="8354">delete_product_code()</st>`<st c="8375">, it deletes a</st> `<st c="8390">Product</st>` <st c="8397">record through the record object retrieved by its</st> <st c="8448">product code.</st>
			<st c="8461">When</st> <st c="8467">retrieving records, the</st> <st c="8491">Peewee ORM has a</st> `<st c="8508">select()</st>` <st c="8516">method that builds variations of query implementations.</st> <st c="8573">The following</st> `<st c="8587">select_product_code()</st>` <st c="8608">and</st> `<st c="8613">select_product_id()</st>` <st c="8632">functions show how to retrieve single records based on unique or primary</st> <st c="8706">key constraints:</st>

def select_product_code(self, code:str): prod = Product.select(Product.codecode) 返回 prod.to_json() def select_product_id(self, id:int): prod = Product.select(Product.idid) 返回 prod.to_json()


			<st c="8924">On the other hand, the following</st> `<st c="8958">select_all_product()</st>` <st c="8978">function retrieves all records in the</st> `<st c="9017">product</st>` <st c="9024">table:</st>

def select_all_product(self): prods = Product.select()records = [log.to_json() for log in prods] 返回 records


			<st c="9144">All model classes retrieved by the</st> `<st c="9180">select()</st>` <st c="9188">method are non-serializable or non-JSONable.</st> <st c="9234">So, in the implementation, be sure to include the conversion of all model objects into JSON records using any accepted method.</st> <st c="9361">In the given sample, all our model classes have a</st> `<st c="9411">to_json()</st>` <st c="9420">method that returns a JSON object containing all the</st> `<st c="9474">Product</st>` <st c="9481">fields and values.</st> <st c="9501">The query transactions include a list comprehension in its procedure to generate a list of JSONable records of</st> `<st c="9612">Product</st>` <st c="9619">details using the</st> `<st c="9638">to_json()</st>` <st c="9647">method.</st>
			<st c="9655">Classes and methods for the Async Peewee ORM</st>
			<st c="9700">Some parts of</st> <st c="9715">our deployed</st> *<st c="9728">Online Grocery</st>* <st c="9742">application runs on the</st> `<st c="9975">peewee-async</st>` <st c="9987">module using the</st> <st c="10005">following</st> `<st c="10015">pip</st>` <st c="10018">command:</st>

pip install aiopg peewee-async


			<st c="10058">Also, include the</st> `<st c="10077">aiopg</st>` <st c="10082">module, which provides PostgreSQL asynchronous database access through the</st> *<st c="10158">DB</st>* *<st c="10161">API</st>* <st c="10164">specification.</st>
			<st c="10179">Async Peewee has</st> `<st c="10197">PooledPostgresqlDatabase</st>`<st c="10221">,</st> `<st c="10223">AsyncPostgresqlConnection</st>`<st c="10248">, and</st> `<st c="10254">AsyncMySQLConnection</st>` <st c="10274">driver classes that create database connection objects in</st> `<st c="10333">async</st>` <st c="10338">mode.</st> <st c="10345">Our configuration uses the</st> `<st c="10372">PooledPostgresqlDatabase</st>` <st c="10396">driver class to include the creation of a</st> <st c="10439">connection pool:</st>

从 peewee_async 导入 PooledPostgresqlDatabase

database = PooledPostgresqlDatabase( 'ogs', user='postgres', password='admin2255',

    host='localhost', port='5432', <st c="10620">最大连接数 = 3</st>,

    `connect_timeout = 3, <st c="10731">3</st>` `<st c="10738">autocommit</st>` `<st c="10756">设置为</st>` `<st c="10756">False</st>.`

        `<st c="10762">异步 Peewee ORM 处理数据库连接的方式不同:它不使用</st>` `<st c="10847">before_request()</st>` `<st c="10863">和</st>` `<st c="10868">teardown_request()</st>` `<st c="10886">事件,而是使用与</st>` `<st c="10933">create_app()</st>` `<st c="10945">工厂方法</st>` `<st c="10962">的配置。</st>` `<st c="10962">以下代码片段展示了如何使用</st>` `<st c="11050">peewee-async</st>` `<st c="11062">模块</st>` `<st c="11062">建立 PostgreSQL 数据库连接:</st>`
<st c="11070">from app.models.config import database</st>
<st c="11109">from peewee_async import Manager</st> def create_app(config_file):
    app = Flask(__name__)
    app.config.from_file(config_file, toml.load)
    global conn_mgr <st c="11255">conn_mgr = Manager(database)</st><st c="11283">database.set_allow_sync(False)</st> … … … … … …
        在这里,`<st c="11325">` `<st c="11332">经理</st>` `<st c="11339">建立了一个</st>` `<st c="11355">asyncio</st>` `<st c="11362">数据库连接模式,不使用</st>` `<st c="11405">before_request()</st>` `<st c="11421">来连接到</st>` `<st c="11440">teardown_request()</st>` `<st c="11458">来断开与数据库的</st>` `<st c="11482">连接。</st>` `<st c="11492">然而,它可以在查询执行期间显式地发出</st>` `<st c="11517">connect()</st>` `<st c="11526">和</st>` `<st c="11531">close()</st>` `<st c="11538">方法来管理数据库连接。</st>` `<st c="11581">` `<st c="11616">实例化</st>` `<st c="11634">Manager</st>` `<st c="11641">类需要数据库连接对象和一个可选的</st>` `<st c="11704">asyncio</st>` `<st c="11711">事件循环。</st>` `<st c="11724">通过</st>` `<st c="11736">Manager</st>` `<st c="11743">对象,你可以调用它的</st>` `<st c="11771">set_allow_sync()</st>` `<st c="11787">方法并将其设置为</st>` `<st c="11809">False</st>` `<st c="11814">以限制非异步实用程序</st>` `<st c="11858">Peewee 方法的使用。</st>`

        `<st c="11873">` `<st c="11878">conn_mgr</st>` `<st c="11886">和</st>` `<st c="11891">database</st>` `<st c="11899">对象对于构建仓库层同样至关重要,如下面的</st>` `<st c="11994">DiscountRepository</st>` `<st c="12012">实现</st>` `<st c="12012">所示:</st>`
 from app.models.db import Discount
from app.models.db import database
from app import conn_mgr
from typing import Dict, Any
class DiscountRepository:
    async def insert_discount(self, details:Dict[str, Any]) -> bool:
        try: <st c="12249">async with database.atomic_async() as tx:</st><st c="12290">await conn_mgr.create(Discount, **details)</st><st c="12333">await tx.commit()</st> return True
        except Exception as e:
            print(e)
        return False
        `<st c="12408">尽管模型层的实现与标准 Peewee 相似,但由于 ORM 使用的</st>` `<st c="12543">asyncio</st>` `<st c="12550">平台执行 CRUD 事务,其存储库</st> `<st c="12506">层并不相同。</st> `<st c="12528">例如,以下</st>` `<st c="12638">insert_discount()</st>` `<st c="12655">函数从`<st c="12695">conn_mgr</st>` `<st c="12703">实例发出`<st c="12671">atomic_async()</st>` `<st c="12685">,以生成异步事务层,该层将提交由`<st c="12836">conn_mgr</st>` `<st c="12844">的`<st c="12817">create()</st>` `<st c="12825">方法执行的插入`<st c="12784">Discount</st>` `<st c="12792">记录,而不是由`<st c="12853">Discount</st>` `<st c="12861">执行。</st> `<st c="12878">async</st>` `<st c="12883">/</st>` `<st c="12885">await</st>` `<st c="12890">关键字在实现中存在。</st>`

        在 `<st c="12934">UPDATE</st>` <st c="12948">操作中,`<st c="12964">get()</st>` <st c="12969">方法从`<st c="12980">conn_mgr</st>` <st c="12988">中检索需要更新的记录对象,并且其`<st c="13046">update()</st>` <st c="13054">方法将新更新的字段刷新到表中。</st> `<st c="13109">再次强调,异步</st>` `<st c="13126">Manager</st>` <st c="13133">方法操作的是事务,而不是模型类。</st> `<st c="13188">以下</st>` `<st c="13202">update_discount()</st>` <st c="13219">函数展示了 Peewee 更新表记录的异步方法:</st>
 async def update_discount(self, details:Dict[str,Any]) -> bool:
       try: <st c="13359">async with database.atomic_async():</st><st c="13394">discount = await conn_mgr.get(Discount,</st> <st c="13434">code=details["code"])</st> discount.rate = details["rate"] <st c="13489">await conn_mgr.update(discount,</st> <st c="13520">only=("rate", ))</st> return True
       except Exception as e:
           print(e)
       return False
        <st c="13594">本地</st> <st c="13605">参数包括</st> `<st c="13623">update()</st>` <st c="13631">方法中的</st> `<st c="13642">conn_mgr</st>` <st c="13650">记录对象以及需要更新的字段</st> <st c="13701">的</st> `<st c="13709">唯一</st>` <st c="13713">参数,这些字段需要在表中进行更新。</st>

        `<st c="13795">另一方面,DELETE</st>` `<st c="13819">操作使用与`<st c="13825">update_discount()</st>` `<st c="13884">中相同的异步</st>` `<st c="13856">get()</st>` `<st c="13861">方法从`<st c="13872">conn_mgr</st>` `<st c="13880">中检索要删除的记录对象。</st> `<st c="13946">以下</st>` `<st c="13972">delete_discount_code()</st>` `<st c="13994">函数显示了`<st c="14015">conn_mgr</st>` `<st c="14034">的异步`<st c="14023">delete()</st>` `<st c="14042">方法如何使用记录对象从表中删除记录:</st>
 async def delete_discount_code(self, code:str) -> bool:
        try: <st c="14163">async with database.atomic_async():</st><st c="14198">discount = await conn_mgr.get(Discount,</st> <st c="14238">code=code)</st><st c="14249">await conn_mgr.delete(discount)</st> return True
        except Exception as e:
            print(e)
        return False
        <st c="14338">在实现异步查询事务时,Async Peewee ORM 使用</st> `<st c="14413">Manager</st>` <st c="14420">类的异步</st> `<st c="14435">get()</st>` <st c="14440">方法来检索单个记录,并使用</st> `<st c="14484">execute()</st>` <st c="14493">方法来</st> <st c="14504">包装并运行用于检索单个或所有记录的异步</st> `<st c="14521">select()</st>` <st c="14529">语句。</st> <st c="14599">以下代码片段显示了针对</st> `<st c="14656">DiscountRepository</st>`<st c="14674">的查询实现</st>:
 async def select_discount_code(self, code:str): <st c="14725">discount = await conn_mgr.get(Discount, code=code)</st> return discount.to_json()
    async def select_discount_id(self, id:int): <st c="14846">discount = await conn_mgr.get(Discount, id=id)</st> return discount.to_json()
    async def select_all_discount(self): <st c="14956">discounts = await conn_mgr.execute(</st> <st c="14991">Discount.select())</st> records = [log.to_json() for log in discounts]
        return records
        <st c="15072">因此,在</st> `<st c="15110">Manager</st>` <st c="15117">类实例中的所有这些捆绑方法都提供了在异步</st> <st c="15217">事务层中实现 CRUD 事务的操作。</st>

        <st c="15235">Peewee 是一个简单且灵活的 ORM,适用于小型到中型 Flask 应用程序。</st> <st c="15318">尽管 SQLAlchemy 提供了更强大的实用工具,但它不适合像我们的</st> *<st c="15420">在线杂货店</st>* <st c="15434">应用程序这样的小型应用程序,该应用程序的范围</st> <st c="15469">和复杂性较低。</st>

        <st c="15484">接下来,我们将部署利用标准异步 Peewee ORM 的</st> <st c="15590">存储层</st> 的应用程序。</st>

        <st c="15608">在 Gunicorn 和 uWSGI 上运行应用程序</st>

        <st c="15654">Flask 应用程序之所以从运行</st> `<st c="15715">flask run</st>` <st c="15724">命令或通过在</st> `<st c="15760">main.py</st>` <st c="15767">中调用</st> `<st c="15747">app.run()</st>` <st c="15756">开始,主要是由于</st> `<st c="15835">werkzeug</st>` <st c="15843">模块内置的 WSGI 服务器。</st> <st c="15856">然而,这个服务器存在一些限制,例如它无法在不减慢速度的情况下响应用户的更多请求,以及它无法最大化生产服务器的资源。</st> <st c="16072">此外,内置服务器还包含几个漏洞,这些漏洞可能带来安全风险。</st> <st c="16158">对于标准 Flask 应用程序,最好使用另一个 WSGI 服务器</st> <st c="16229">用于生产,例如</st> **<st c="16253">Gunicorn</st>** <st c="16261">或</st> **<st c="16265">uWSGI</st>**<st c="16270">。</st>

        <st c="16271">让我们首先将我们的应用程序部署到</st> *<st c="16320">Gunicorn</st>* <st c="16328">服务器。</st>

        <st c="16336">使用 Gunicorn 服务器</st>

        `<st c="16723">ch11-guni</st>` <st c="16732">应用程序。</st> <st c="16746">但首先,我们必须使用以下</st> `<st c="16854">pip</st>` <st c="16857">命令在应用程序的虚拟环境中安装</st> `<st c="16777">gunicorn</st>` <st c="16785">模块:</st>
 pip install gunicorn
        <st c="16887">然后,使用模块名称和</st> `<st c="16948">app</st>` <st c="16951">实例</st> <st c="16961">在</st> `<st c="16964">{module}</st>` **<st c="16972">:{flask_app}</st>** <st c="16985">格式,绑定主机地址和端口来运行</st> `<st c="16902">gunicorn</st>` <st c="16910">命令。</st> <st c="17034">以下是在 Gunicorn 服务器上运行标准 Flask 应用程序的完整命令,使用单个工作进程:</st>
 gunicorn --bind 127.0.0.1:8000 main:app
        *<st c="17192">图 11</st>**<st c="17202">.1</st>* <st c="17204">显示了使用默认</st> <st c="17288">单个工作进程成功运行给定命令后的服务器日志:</st>

        ![图 11.1 – 启动 Gunicorn 服务器后的服务器日志](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_001.jpg)

        <st c="17670">图 11.1 – 启动 Gunicorn 服务器后的服务器日志</st>

        <st c="17729">一个</st> *<st c="17732">Gunicorn</st> <st c="17740">工作进程是一个 Python 进程,它一次管理一个 HTTP 请求-响应事务。</st> <st c="17830">默认的 Gunicorn 服务器在后台运行一个工作进程。</st> <st c="17906">从逻辑上讲,产生更多的工作进程来管理请求和响应,将提高应用程序的性能。</st> <st c="18031">然而,对于 Gunicorn 来说,工作进程的数量取决于服务器机器上的 CPU 处理器数量,并使用</st> `<st c="18162">(2*CPU)+1</st>` <st c="18171">公式计算。</st> <st c="18181">这些子进程将同时管理 HTTP 请求,利用硬件可以提供的最大资源级别。</st> <st c="18317">Gunicorn 的一个优点是它能够有效地利用资源来管理</st> <st c="18421">运行时性能:</st>

        ![图 11.2 – Windows 系统 CPU 利用率仪表板](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_002.jpg)

        <st c="18863">图 11.2 – Windows 系统 CPU 利用率仪表板</st>

        *<st c="18926">图 11</st>**<st c="18936">.2</st>* <st c="18938">显示我们的生产服务器机器有</st> `<st c="18984">4</st>` <st c="18985">个 CPU 核心,这意味着我们的 Gunicorn 服务器可以使用的可接受工作进程数量是</st> `<st c="19087">9</st>`<st c="19088">。因此,以下命令运行了一个具有</st> `<st c="19146">9</st>` <st c="19147">个工作进程的 Gunicorn 服务器:</st>
 gunicorn --bind 127.0.0.1:8000 main:app --workers 9
        <st c="19208">在命令语句中添加</st> `<st c="19220">--workers</st>` <st c="19229">设置,允许我们将适当的工人数包含在 HTTP</st> <st c="19325">请求处理中。</st>

        <st c="19344">向 Gunicorn 服务器添加工作进程,但不会提高应用程序的总 CPU 性能,这是一种资源浪费。</st> <st c="19481">一种补救方法是向工作进程添加更多线程,而不是添加</st> <st c="19541">无益的工作进程。</st>

        `<st c="19559">工作进程或进程消耗更多的内存空间。</st>` `<st c="19608">此外,与线程不同,没有两个工作进程可以共享内存空间。</st>` `<st c="19682">一个</st>` `<st c="19684">线程</st>` `<st c="19690">消耗的内存空间更少,因为它比工作进程更轻量级。</st>` `<st c="19762">为了获得最佳的服务器性能,每个工作进程必须至少启动</st>` `<st c="19837">2</st>` `<st c="19838">个线程,这些线程将并发处理 HTTP 请求和响应。</st>` `<st c="19907">因此,运行以下 Gunicorn 命令可以启动一个具有</st>` `<st c="19974">1</st>` `<st c="19975">个工作进程和</st>` `<st c="19988">2</st>` `<st c="19989">个线程的服务器:</st>`
 gunicorn --bind 127.0.0.1:8000 main:app --workers 1 --threads 2
        `<st c="20062">The</st>` `<st c="20067">--threads</st>` `<st c="20076">设置允许我们为每个工作进程至少添加</st>` `<st c="20111">2</st>` `<st c="20112">个线程</st>` `<st c="20121">。</st>`

        `<st c="20132">尽管在工作进程中设置线程意味着并发,但这些线程仍然在其工作进程的范围内运行,它们是同步的。</st>` `<st c="20232">因此,工作进程的阻塞限制阻碍了线程发挥其真正的并发性能。</st>` `<st c="20275">然而,与纯工作进程设置相比,线程可以管理处理 I/O 事务的开销,因为应用于 I/O 阻塞的并发不会消耗</st>` `<st c="20575">更多空间。</st>`

        `<st c="20586">图 11**<st c="20611">.3</st>** 中所示的服务器日志显示了从</st>` `<st c="20651">同步</st>` `<st c="20655">工作进程</st>` `<st c="20666">到</st>` `<st c="20673">gthread</st>` `<st c="20673">的变化,因为当在</st>` `<st c="20740">Gunicorn 平台</st>` `<st c="20740">上使用时,所有生成的 Python 线程都变成了 gthread:</st>`

        ![图 11.3 – 运行 Gunicorn 后服务器的日志](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_003.jpg)

        `<st c="21045">图 11.3 – 运行 Gunicorn 后服务器的日志</st>`

        `<st c="21105">现在,当需要 I/O 事务的特征数量增加时,Gunicorn 以及工作进程和服务器都不会帮助加快 HTTP 请求和响应的处理速度。</st>` `<st c="21292">另一个解决方案是通过</st>` `<st c="21319">伪线程</st>` `<st c="21333">或</st>` `<st c="21337">绿色线程</st>` `<st c="21350">,通过</st>` `<st c="21364">eventlet</st>` `<st c="21372">和</st>` `<st c="21377">gevent</st>` `<st c="21383">库,将伪线程或绿色线程作为工作进程类添加到 Gunicorn 服务器中。</st>` `<st c="21437">这两个库都使用异步工具和</st>` `<st c="21483">greenlet</st>` `<st c="21491">线程来接口和执行标准的 Flask 组件,特别是 I/O 事务,以提高效率。</st>` `<st c="21606">它们使用</st>` `<st c="21619">monkey-patching</st>` `<st c="21634">机制来</st>` `<st c="21648">替换标准或阻塞组件,以它们的异步对应物。</st>`

        <st c="21729">要将我们的应用程序部署到使用</st> `<st c="21777">eventlet</st>` <st c="21785">库的 Gunicorn,首先使用以下</st> `<st c="21849">pip</st>` <st c="21852">命令安装</st> `<st c="21807">greenlet</st>` <st c="21815">模块,然后是</st> `<st c="21874">eventlet</st>`<st c="21882">:</st>
 pip install greenlet eventlet
        <st c="21914">对于</st> `<st c="21919">psycopg2</st>` <st c="21927">或数据库相关的 monkey-patching,使用以下</st> `<st c="22014">pip</st>` <st c="22017">命令安装</st> `<st c="21977">psycogreen</st>` <st c="21987">模块:</st>
 pip install psycogreen
        <st c="22049">然后,通过在</st> `<st c="22221">main.py</st>` <st c="22228">文件的最上方调用</st> `<st c="22162">psycogreen.eventlet</st>` <st c="22181">模块的</st> `<st c="22130">patch_psycopg()</st>` <st c="22145">函数,对 Peewee 和</st> `<st c="22093">psycopg2</st>` <st c="22101">事务进行 monkey-patching,并在调用</st> `<st c="22253">create_app()</st>` <st c="22265">方法之前执行。</st> <st c="22274">以下代码片段显示了包含</st> `<st c="22343">psycogreen</st>` <st c="22353">设置的</st> `<st c="22321">main.py</st>` <st c="22328">文件的部分:</st>
<st c="22360">import psycogreen.eventlet</st>
<st c="22387">psycogreen.eventlet.patch_psycopg()</st> from app import create_app
from app.models.config import database
app = create_app('../config_dev.toml')
… … … … … …
        <st c="22540">The</st> `<st c="22545">psycogreen</st>` <st c="22555">module provides a blocking interface or wrapper for</st> `<st c="22608">psycopg2</st>` <st c="22617">transactions to interact with coroutines or asynchronous components of the</st> `<st c="22692">eventlet</st>` <st c="22700">worker without altering the standard</st> <st c="22738">Peewee codes.</st>

        <st c="22751">要将我们的</st> *<st c="22766">在线杂货</st>* <st c="22780">应用程序(</st>`<st c="22794">ch11-guni-eventlet</st>`<st c="22813">)部署到使用</st> `<st c="22849">1</st>` `<st c="22850">eventlet</st>` <st c="22858">工作进程和</st> `<st c="22871">2</st>` <st c="22872">线程的 Gunicorn 服务器上,请运行以下命令:</st>
 gunicorn --bind 127.0.0.1:8000 main:app --workers 1 --worker-class  eventlet --threads 2
        *<st c="22996">图 11</st>**<st c="23006">.4</st>* <st c="23008">显示了运行 Gunicorn 服务器后的服务器日志:</st>

        ![图 11.4 – 使用 eventlet 工作进程启动 Gunicorn 服务器后的服务器日志](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_004.jpg)

        <st c="23522">图 11.4 – 使用 eventlet 工作进程启动 Gunicorn 服务器后的服务器日志</st>

        <st c="23607">日志描述了</st> <st c="23624">服务器使用的工作进程是一个</st> `<st c="23674">eventlet</st>` <st c="23682">工作进程类型。</st>

        <st c="23695">The</st> `<st c="23700">eventlet</st>` <st c="23708">library provides</st> <st c="23726">concurrent utilities that run standard or non-async Flask components asynchronously using task switching, a shift from sync to async tasks internally without explicitly</st> <st c="23895">programming it.</st>

        <st c="23910">除了</st> `<st c="23922">eventlet</st>`<st c="23930">外,</st> `<st c="23932">gevent</st>` <st c="23938">还可以管理来自应用程序 I/O 密集型任务的并发请求。</st> <st c="24017">像</st> `<st c="24022">eventlet</st>`<st c="24030">一样,</st> `<st c="24032">gevent</st>` <st c="24038">是一个基于协程的库,但它更多地依赖于其</st> `<st c="24100">greenlet</st>` <st c="24108">对象及其事件循环。</st> <st c="24140">《st c="24144">gevent</st>` <st c="24150">库的</st> `<st c="24161">greenlet</st>` <st c="24169">是一个轻量级且强大的线程,以合作调度方式执行。</st> <st c="24258">要在 Gunicorn 服务器中运行一个</st> `<st c="24271">gevent</st>` <st c="24277">工作进程,请使用以下</st> `<st c="24380">pip</st>` <st c="24383">命令安装</st> `<st c="24321">greenlet</st>`<st c="24329">,</st> `<st c="24331">eventlet</st>`<st c="24339">,和</st> `<st c="24345">gevent</st>` <st c="24351">模块:</st>
 pip install greenlet eventlet gevent
        <st c="24429">此外,安装</st> `<st c="24444">psycogreen</st>` <st c="24454">以使用其</st> `<st c="24534">gevent</st>` `<st c="24540">patch_psycopg()</st>`<st c="24556">对应用程序的数据库相关事务进行猴子补丁。以下代码片段显示了</st> `<st c="24603">main.py</st>` <st c="24610">文件的一部分,这是</st> `<st c="24623">ch11-guni-gevent</st>` <st c="24639">项目的版本,是我们</st> *<st c="24666">在线杂货店</st>* <st c="24680">应用程序的一个版本,需要在 Gunicorn 上使用</st> `<st c="24728">gevent</st>` <st c="24734">工作进程运行:</st>
<st c="24743">import gevent.monkey</st>
<st c="24764">gevent.monkey.patch_all()</st>
<st c="24790">import psycogreen.gevent</st>
<st c="24815">psycogreen.gevent.patch_psycopg()</st> import gevent
from app import create_app
… … … … … …
app = create_app('../config_dev.toml')
… … … … … …
        <st c="24953">在</st> `<st c="24957">gevent</st>`<st c="24963">中,主模块必须在其</st> `<st c="24995">patch_all()</st>` <st c="25006">方法中调用</st> `<st c="25023">gevent.monkey</st>` <st c="25036">模块,在任何其他内容之前,以显式地将所有事件在运行时异步地运行,就像协程一样。</st> <st c="25155">之后,它需要调用</st> `<st c="25187">psycogreen</st>` <st c="25197">模块的</st> `<st c="25207">patch_psycopg()</st>`<st c="25222">,但这次是在</st> `<st c="25248">gevent</st>` <st c="25254">子模块下。</st>

        <st c="25266">要使用</st> <st c="25280">Gunicorn 服务器</st> <st c="25284">并使用</st> `<st c="25306">2</st>` `<st c="25307">gevent</st>` <st c="25313">工作进程以及</st> `<st c="25327">2</st>` <st c="25328">线程利用率来启动,请运行以下命令:</st>
 gunicorn --bind 127.0.0.1:8000 main:app --workers 2 --worker-class gevent --threads 2
        *<st c="25466">图 11</st>**<st c="25476">.5</st>* <st c="25478">显示了启动</st> <st c="25522">Gunicorn 服务器</st>后的服务器日志:

        ![图 11.5 – 使用 gevent 工作进程启动 Gunicorn 服务器后的服务器日志](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_005.jpg)

        <st c="26061">图 11.5 – 使用 gevent 工作进程启动 Gunicorn 服务器后的服务器日志</st>

        <st c="26145">Gunicorn 使用的进程现在是一个</st> `<st c="26187">gevent</st>` <st c="26193">进程,如前述服务器日志所示。</st>

        <st c="26242">现在,让我们使用 uWSGI 作为我们的生产</st> <st c="26282">应用服务器。</st>

        <st c="26301">使用 uWSGI</st>

        `<st c="26534">pyuwsgi</st>` <st c="26541">模块使用以下</st> `<st c="26569">pip</st>` <st c="26572">命令:</st>
 pip install pyuwsgi
        <st c="26601">uWSGI 有几个必需和可选的设置选项。</st> <st c="26659">其中一个是</st> `<st c="26670">-w</st>` <st c="26672">设置,它需要服务器运行所需的 WSGI 模块。</st> <st c="26743">`<st c="26747">-p</st>` <st c="26749">设置表示可以管理 HTTP 请求的工作进程或进程数。</st> <st c="26834">`<st c="26838">--http</st>` <st c="26844">设置表示服务器将监听的地址和端口。</st> <st c="26919">`<st c="26923">--enable-threads</st>` <st c="26939">设置允许服务器利用 Python 线程进行</st> <st c="26996">后台进程。</st>

        <st c="27017">要将我们的</st> *<st c="27032">在线杂货</st>* <st c="27046">应用程序(</st>`<st c="27060">ch11-uwsgi</st>`<st c="27071">)部署到具有</st> `<st c="27097">4</st>` <st c="27098">个工作进程和后台 Python 线程的 uWSGI 服务器上,请运行以下命令:</st>
 uwsgi --http 127.0.0.1:8000 --master -p 4 -w main:app --enable-threads
        <st c="27235">在这里,</st> `<st c="27242">--master</st>` <st c="27250">是一个可选设置,允许主进程及其工作进程优雅地关闭和</st> <st c="27338">重启。</st>

        与 Gunicorn 不同,uWSGI 生成一个长的服务器日志,提到了它包含的几个可管理的配置细节,以提高应用程序的性能。</st> <st c="27457">*<st c="27522">图 11</st>**<st c="27531">.6</st>* <st c="27533">显示了 uWSGI 启动后的服务器日志:</st>

        ![图 11.6 – 使用 4 个工作进程启动 uWSGI 服务器后的服务器日志](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_006.jpg)

        <st c="29113">图 11.6 – 启动 uWSGI 服务器并使用 4 个工作进程后的服务器日志</st>

        <st c="29184">使用</st> `<st c="29225">--master</st>` <st c="29233">设置关闭 uWSGI 服务器,允许我们向主进程及其工作进程发送</st> `<st c="29299">SIGTERM</st>` <st c="29306">信号,以执行优雅的关闭、重启或重新加载,这比突然终止进程要好。</st> *<st c="29409">图 11</st>**<st c="29418">.7</st>* <st c="29420">显示了在命令中设置</st> `<st c="29455">--master</st>` <st c="29463">设置的优势:</st>

        ![图 11.7 – 使用 --master 设置关闭 uWSGI 服务器后的服务器日志](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_007.jpg)

        <st c="29792">图 11.7 – 使用 --master 设置关闭 uWSGI 服务器后的服务器日志</st>

        <st c="29879">与易于配置的 Gunicorn 相比,管理 uWSGI 是</st> <st c="29898">复杂的。</st> <st c="29950">到目前为止,Gunicorn 仍然是部署标准</st> <st c="30030">Flask 应用程序时推荐的服务器。</st>

        <st c="30049">现在,让我们部署</st> *<st c="30068">Flask[async]</st>* <st c="30080">到名为</st> *<st c="30106">Uvicorn</st>*<st c="30113">的 ASGI 服务器</st>。

        <st c="30114">将应用程序部署到 Uvicorn</st>

        `<st c="30419">uvicorn.workers.UvicornWorker</st>` <st c="30448">作为其</st> <st c="30456">HTTP 服务器。</st>

        <st c="30468">尽管 Gunicorn 是基于 WSGI 的服务器,但它可以通过其</st> `<st c="30595">--worker-class</st>` <st c="30609">设置支持在标准</st> 和异步模式下运行 Flask 应用程序。</st> <st c="30619">对于 Flask[async] 应用程序,Gunicorn 可以使用</st> `<st c="30675">aiohttp</st>` <st c="30682">或</st> `<st c="30686">uvicorn</st>` <st c="30693">工作</st> <st c="30701">类类型。</st>

        <st c="30713">我们的异步</st> *<st c="30724">在线杂货</st>* <st c="30738">应用程序(</st>`<st c="30752">ch11-async</st>`<st c="30763">)使用 Gunicorn 和一个</st> `<st c="30787">uvicorn</st>` <st c="30794">工作作为其部署平台。</st> <st c="30830">在应用工作类型之前,首先通过运行以下</st> `<st c="30921">pip</st>` <st c="30924">命令安装</st> `<st c="30875">uvicorn</st>` <st c="30882">模块:</st>
 pip install uvicorn
        <st c="30953">然后,从</st> `<st c="30967">WsgiToAsgi</st>` <st c="30977">导入</st> `<st c="30987">uvicorn</st>` <st c="30994">模块的</st> `<st c="31004">asgiref.wsgi</st>` <st c="31016">模块,以包装 Flask 应用程序实例。</st> <st c="31056">以下代码片段显示了如何将 WSGI 应用程序转换为 ASGI 类型:</st>
<st c="31138">from asgiref.wsgi import WsgiToAsgi</st> from app import create_app
app = create_app('../config_dev.toml') <st c="31297">asgi_app</st> instead of the original Flask <st c="31336">app</st>. To start Gunicorn using two Uvicorn workers with two threads each, run the following command:

gunicorn main:asgi_app --bind 0.0.0.0:8000 --workers 2 --worker-class uvicorn.workers.UvicornWorker --threads 2


			<st c="31546">Here,</st> `<st c="31553">UvicornWorker</st>`<st c="31566">, a Gunicorn-compatible worker class from the</st> `<st c="31612">uvicorn</st>` <st c="31619">library, provides an interface to an ASGI-based application so that Gunicorn can communicate with all the HTTP requests from the coroutines of the applications and eventually handle</st> <st c="31802">those requests.</st>
			*<st c="31817">Figure 11</st>**<st c="31827">.8</st>* <st c="31829">shows the server log after running the</st> <st c="31869">Gunicorn server:</st>
			![Figure 11.8 – Server log after starting the Gunicorn server using UvicornWorker](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_008.jpg)

			<st c="33005">Figure 11.8 – Server log after starting the Gunicorn server using UvicornWorker</st>
			<st c="33084">The server log depicts the use of</st> `<st c="33119">uvicorn.workers.UvicornWorker</st>` <st c="33148">as the Gunicorn worker, and it also shows the “</st>*<st c="33196">ASGI ‘lifespan’ protocol appears unsupported.</st>*<st c="33242">” log message, which means Flask does not yet support ASGI with the lifespan</st> <st c="33320">protocol used to manage server startup</st> <st c="33359">and shutdown.</st>
			<st c="33372">The Apache HTTP Server, a popular production server for most PHP applications, can also host and run standard Flask applications.</st> <st c="33503">So, let’s explore the process of migrating our applications to the</st> *<st c="33570">Apache</st>* *<st c="33577">HTTP Server</st>*<st c="33588">.</st>
			<st c="33589">Deploying the application on the Apache HTTP Server</st>
			**<st c="33641">Apache HTTP Server</st>** <st c="33660">is an</st> <st c="33667">open source server under the Apache</st> <st c="33703">projects that can</st> <st c="33721">run on Windows and UNIX-based platforms to provide an efficient, simple, and flexible HTTP server for</st> <st c="33823">various applications.</st>
			<st c="33844">Before anything else, download the latest server from</st> [<st c="33899">https://httpd.apache.org/download.cgi</st>](https://httpd.apache.org/download.cgi) <st c="33936">and unzip the file to the production server’s installation directory.</st> <st c="34007">Then, download the latest</st> *<st c="34033">Microsoft Visual C++ Redistributable</st>* <st c="34069">from</st> [<st c="34075">https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist</st>](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist)<st c="34147">, install it, and run the server through the</st> `<st c="34192">httpd.exe</st>` <st c="34201">file of its</st> `<st c="34214">/</st>``<st c="34215">bin</st>` <st c="34218">folder.</st>
			<st c="34226">After the installation, follow these steps to deploy our application to the Apache</st> <st c="34310">HTTP Server:</st>

				1.  <st c="34322">Build your Flask application, as we did with our</st> *<st c="34372">Online Grocery</st>* <st c="34386">application, run it using the built-in WSGI server, and refine the components using</st> `<st c="34471">pytest</st>` <st c="34477">testing.</st>
				2.  <st c="34486">Next, install the</st> `<st c="34505">mod_wsgi</st>` <st c="34513">module, which enables the Apache HTTP Server’s support to run WSGI applications.</st> <st c="34595">Install the module using the following</st> `<st c="34634">pip</st>` <st c="34637">command:</st>

    ```

    pip install mod_wsgi

    ```py

    				3.  <st c="34667">If the installation encounters an error similar to what’s shown in the error log in</st> *<st c="34752">Figure 11</st>**<st c="34761">.9</st>*<st c="34763">, run the</st> `<st c="34773">set</st>` <st c="34776">command to assign the</st> `<st c="34850">MOD_WSGI_APACHE_ROOTDIR</st>` <st c="34873">environment variable:</st>

    ```

    set "MOD_WSGI_APACHE_ROOTDIR= C:/.../Server/Apache24"

    ```py

    				4.  <st c="34949">Apply</st> *<st c="34956">forward slashes</st>* <st c="34971">(</st>`<st c="34973">/</st>`<st c="34974">) to create the directory path.</st> <st c="35006">Afterward, re-install the</st> `<st c="35032">mod_wsgi</st>` <st c="35040">module:</st>

			![Figure 11.9 – No MOD_WSGI_APACHE_ROOTDIR error](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_009.jpg)

			<st c="35501">Figure 11.9 – No MOD_WSGI_APACHE_ROOTDIR error</st>

				1.  <st c="35547">Again, if the</st> <st c="35562">re-installation</st> <st c="35578">of</st> `<st c="35581">mod_wsgi</st>` <st c="35589">gives another</st> <st c="35604">error stating the required</st> `<st c="35685">VisualStudioSetup.exe</st>` <st c="35706">from</st> [<st c="35711">https://visualstudio.microsoft.com/downloads</st>](https://visualstudio.microsoft.com/downloads)<st c="35756">.</st>2.  <st c="35757">Run the</st> `<st c="35766">VisualStudioSetup.exe</st>` <st c="35787">file; a menu dashboard will appear, as shown in</st> *<st c="35836">Figure 11</st>**<st c="35845">.10</st>*<st c="35848">.</st>3.  <st c="35849">Click the</st> **<st c="35860">Desktop Development with C++</st>** <st c="35888">menu option to show the installation details on the right-hand side of</st> <st c="35960">the dashboard:</st>

			![Figure 11.10 – Microsoft Visual Studio Library dashboard](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_010.jpg)

			<st c="37920">Figure 11.10 – Microsoft Visual Studio Library dashboard</st>
			<st c="37976">This</st> <st c="37982">installation</st> <st c="37995">is different from the previous Microsoft Visual C++ Redistributable</st> <st c="38063">installation procedure.</st>

				1.  <st c="38086">Now, select</st> `<st c="38322">mod_wsgi</st>` <st c="38330">installation.</st>
				2.  <st c="38344">After choosing the necessary components, click the</st> **<st c="38396">Install</st>** <st c="38403">button at the bottom right of</st> <st c="38434">the dashboard.</st>
				3.  <st c="38448">After installing</st> `<st c="38497">pip install mod_wsgi</st>` <st c="38517">once more.</st> <st c="38529">This time, the</st> `<st c="38544">mod_wsgi</st>` <st c="38552">installation must</st> <st c="38571">proceed successfully.</st>
				4.  <st c="38592">The</st> `<st c="38597">mod_wsgi</st>` <st c="38605">module needs a configuration file inside the project that the Apache HTTP Server needs to load during startup.</st> <st c="38717">This file should be in a separate folder, say</st> `<st c="38763">wsgi</st>`<st c="38767">, and must be in the main project folder.</st> <st c="38809">In our</st> `<st c="38816">ch11-apache</st>` <st c="38827">project, the configuration file is</st> `<st c="38863">conf.wsgi</st>` <st c="38872">and has been placed in the</st> `<st c="38900">wsgi</st>` <st c="38904">folder.</st> <st c="38913">Be sure to add the</st> `<st c="38932">__init__.py</st>` <st c="38943">file to this folder too.</st> <st c="38969">The following is the content</st> <st c="38998">of</st> `<st c="39001">conf.wsgi</st>`<st c="39010">:</st>

    ```

    import sys

    sys.path.insert(0, 'C:/Alibata/Training/ Source/flask/mastering/ch11-apache') <st c="39142">conf.wsgi</st> 配置文件为 Apache HTTP 服务器提供了一个通道,以便通过 <st c="39287">mod_wsgi</st> 模块访问 Flask <st c="39233">app</st> 实例进行部署和执行。

    ```py

    				5.  <st c="39303">Run the</st> `<st c="39312">mod_wsgi-express module-config</st>` <st c="39342">command to generate the</st> `<st c="39367">LoadModule</st>` <st c="39377">configuration statements that the Apache HTTP Server needs to integrate</st> <st c="39450">with the project</st> <st c="39467">directory.</st> <st c="39478">The following are the</st> `<st c="39500">LoadModule</st>` <st c="39510">snippets that have been generated for our</st> *<st c="39553">Online</st>* *<st c="39560">Grocery</st>* <st c="39567">application:</st>

    ```

    LoadFile "C:/Alibata/Development/Language/ Python/Python311/python311.dll"

    LoadModule wsgi_module "C:/Alibata/Training/Source/ flask/mastering/ch11-apache-env/Lib/site-packages/mod_wsgi/server/mod_wsgi.cp311-win_amd64.pyd"

    WSGIPythonHome "C:/Alibata/Training/Source/ flask/mastering/ch11-apache-env"

    ```py

    				6.  <st c="39880">Place these</st> `<st c="39893">LoadModule</st>` <st c="39903">configuration statements in the Apache HTTP Server’s</st> `<st c="39957">/conf/http.conf</st>` <st c="39972">file, specifically anywhere in the</st> `<st c="40008">LoadModule</st>` <st c="40018">area under the</st> **<st c="40034">Dynamic Shared Object (DSO)</st>** **<st c="40062">Support</st>** <st c="40069">segment.</st>
				7.  <st c="40078">At the end of the</st> `<st c="40097">/conf/http.conf</st>` <st c="40112">file, import the custom</st> `<st c="40137">VirtualHost</st>` <st c="40149">configuration file of the project.</st> <st c="40184">The following is a sample import statement for our</st> *<st c="40235">Online</st>* *<st c="40242">Grocery</st>* <st c="40249">application:</st>

    ```

    包含在 *<st c="40310">VirtualHost</st> 配置文件中,该文件在 *<st c="40355">步骤 10</st>* 中引用。以下是我们 <st c="40417">ch11_apache.conf</st> 文件中的示例配置设置:

    ```py
     <VirtualHost *:<st c="40455">8080</st>> <st c="40463">ServerName localhost</st> WSGIScriptAlias / C<st c="40503">:/Alibata/Training/Source/ flask/mastering/ch11-apache/wsgi/conf.wsgi</st> <Directory C:/Alibata/Training/Source/ flask/mastering/ch11-apache> <st c="40642">Require all granted</st> </Directory>
    </VirtualHost>
    ```

    <st c="40689">The</st> `<st c="40694">VirtualHost</st>` <st c="40705">配置定义了服务器将监听的主机地址和端口,以便运行我们的应用程序。</st> <st c="40770">它的</st> `<st c="40825">WSGIScriptAlias</st>` <st c="40840">指令给出了应用程序的</st> `<st c="40874">mod_wsgi</st>` <st c="40882">配置文件的引用。</st> <st c="40922">此外,配置允许服务器访问</st> `<st c="40996">ch11-apache</st>` <st c="41007">项目中的所有文件。</st>

    ```py

    				8.  <st c="41016">Now, open a terminal and run or restart the server through</st> `<st c="41076">httpd.exe</st>`<st c="41085">. Access all the APIs using</st> `<st c="41113">pytest</st>` <st c="41119">or</st> <st c="41123">API clients.</st>

			<st c="41135">Choosing the Apache HTTP Server as the production server is a common approach in many deployment plans for Flask projects involving the standalone server platform.</st> <st c="41300">Although the deployment process is tricky and lengthy, the server’s fast and stable performance, once configured and managed well, makes it a better choice for setting up a significantly effective production environment for</st> <st c="41524">Flask applications.</st>
			<st c="41543">There is another way of deploying Flask applications that involves fewer tweaks and configurations</st> <st c="41643">but provides an enterprise-grade production setup:</st> **<st c="41694">the containerized deployment approach</st>**<st c="41731">. Let’s discuss how to deploy the application to</st> *<st c="41780">Docker</st>* <st c="41787">containers.</st>
			<st c="41798">Deploying the application on Docker</st>
			**<st c="41834">Docker</st>** <st c="41841">is a</st> <st c="41847">powerful tool for deploying and running applications using software</st> <st c="41915">units instead of hardware setups.</st> <st c="41949">Each</st> <st c="41954">independent, lightweight, standalone, and executable</st> <st c="42007">unit, called a</st> **<st c="42022">container</st>**<st c="42031">, must contain all the files of the applications that it needs to run.</st> <st c="42102">Docker is the core container engine that manages all the containers</st> <st c="42170">and packages applications in their appropriate containers.</st> <st c="42229">To download Docker, download the</st> **<st c="42262">Docker Desktop</st>** <st c="42276">installer that’s appropriate for your system from</st> [<st c="42327">https://docs.docker.com/engine/install/</st>](https://docs.docker.com/engine/install/)<st c="42366">. Be sure to enable the Window’s</st> **<st c="42399">Hyper-V service</st>** <st c="42414">before</st> <st c="42422">installing Docker.</st> <st c="42441">Use your Docker credentials to log in to the application.</st> *<st c="42499">Figure 11</st>**<st c="42508">.11</st>* <st c="42511">shows a sample account dashboard of the Docker</st> <st c="42559">Desktop application:</st>
			![Figure 11.11 – A Desktop Docker profile](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_011.jpg)

			<st c="43320">Figure 11.11 – A Desktop Docker profile</st>
			<st c="43359">Docker requires some rules when deploying applications to its containers.</st> <st c="43434">The first requirement is to create a Dockerfile inside the project’s</st> *<st c="43503">main</st>* <st c="43507">directory, on the same level as the</st> `<st c="43544">main.py</st>` <st c="43551">and</st> `<st c="43556">.toml</st>` <st c="43561">configuration files.</st> <st c="43583">The following is the content of the</st> `<st c="43619">ch11-asgi</st>` <st c="43628">file’s Dockerfile:</st>

FROM python:3.11 WORKDIR /usr/src/ch11-asgi

运行 pip install --upgrade pip

COPY ./requirements.txt /usr/src/ch11-asgi/requirements.txt

RUN pip install -r requirements.txt

COPY . /usr/src/ch11-asgi

EXPOSE 8000

CMD ["gunicorn", "main:asgi_app", "--bind", "0.0.0.0:8000", "--worker-class", "uvicorn.workers.UvicornWorker", "--threads", "2"]


			<st c="43984">A</st> **<st c="43987">Dockerfile</st>** <st c="43997">contains a</st> <st c="44009">series of instructions made by Docker commands that the engine will use to assemble an image.</st> <st c="44103">A</st> **<st c="44105">Docker image</st>** <st c="44117">is a software</st> <st c="44132">template containing the needed project files, folders, Python modules, server details, and commands to start the Flask server.</st> <st c="44259">Docker will run the image to generate a running image instance called</st> <st c="44329">a container.</st>
			<st c="44341">The first</st> <st c="44352">line of our Dockerfile is the</st> `<st c="44382">FROM</st>` <st c="44386">instruction, which</st> <st c="44406">creates a stage or a copy of the base image from the Docker repository.</st> <st c="44478">Here are the guidelines to follow when choosing the</st> <st c="44530">base image:</st>

				*   <st c="44541">Ensure it is complete with libraries, tools, filesystem structure, and network structures so that the container will</st> <st c="44659">be stable.</st>
				*   <st c="44669">Ensure it can be updated in terms of operating system plugins</st> <st c="44732">and libraries.</st>
				*   <st c="44746">Ensure it’s equipped with up-to-date and stable Python compilers and</st> <st c="44816">core libraries.</st>
				*   <st c="44831">Ensure it’s loaded with extensions and additional plugins for additional</st> <st c="44905">complex integrations.</st>
				*   <st c="44926">Ensure it has a smaller</st> <st c="44951">file size.</st>

			<st c="44961">Choosing the right base image is crucial for the application to avoid problems during</st> <st c="45048">production phases.</st>
			<st c="45066">The next instruction is the</st> `<st c="45095">WORKDIR</st>` <st c="45102">command, which creates and sets the new application’s working directory.</st> <st c="45176">The first</st> `<st c="45186">RUN</st>` <st c="45189">command updates the container’s</st> `<st c="45222">pip</st>` <st c="45225">command, which will install all the libraries from the</st> `<st c="45281">requirements.txt</st>` <st c="45297">file copied by the</st> `<st c="45317">COPY</st>` <st c="45321">command from our local project folder.</st> <st c="45361">After installing the modules in the container, the next instruction is to</st> `<st c="45435">COPY</st>` <st c="45439">all the project files from the local folder to</st> <st c="45487">the container.</st>
			<st c="45501">The</st> `<st c="45506">EXPOSE</st>` <st c="45512">command defines the port the application will listen on.</st> <st c="45570">The</st> `<st c="45574">CMD</st>` <st c="45578">command, on the other hand, tells Docker how to start the Gunicorn server with</st> `<st c="45657">UvicornWorker</st>` <st c="45670">when the</st> <st c="45680">container starts.</st>
			<st c="45697">After composing the Dockerfile, open a terminal to run the</st> `<st c="45757">docker login</st>` <st c="45769">CLI command</st> <st c="45782">and input your credentials.</st> <st c="45810">The</st> `<st c="45814">docker login</st>` <st c="45826">command enables access to your Docker repository using other Docker’s</st> <st c="45897">CLI commands, such as</st> `<st c="45919">docker run</st>` <st c="45929">to execute the instructions from the Dockerfile.</st> <st c="45979">By the way, aside from our Flask[async] application, there is a need to pull an image to generate a container for the PostgreSQL database of our application.</st> <st c="46137">Conventionally, to connect these containers, such as our PostgreSQL and Redis containers, to the Python container with the Flask application, Docker networking, through running the</st> `<st c="46318">docker network</st>` <st c="46332">command, creates the network connections that will link these containers to establish the needed connectivity.</st> <st c="46444">But this becomes complex if there are more containers to attach.</st> <st c="46509">As a replacement to save time and effort,</st> *<st c="46551">Docker Compose</st>* <st c="46565">can establish all these step-by-step networking procedures by only running the</st> `<st c="46645">docker-compose</st>` <st c="46659">command.</st> <st c="46669">There is no need to install Docker Compose since it is part of the bundle that’s installed by the Docker Desktop installer.</st> <st c="46793">Docker Compose uses Docker Engine, so installing the engine also includes Compose.</st> <st c="46876">To start Docker Compose, just run</st> `<st c="46910">docker login</st>` <st c="46922">and enter a valid</st> <st c="46941">Docker account.</st>
			<st c="46956">Using Docker Compose</st>
			`<st c="47177">docker-compose.yaml</st>`<st c="47196">. The following is the configuration file that’s used by our</st> `<st c="47257">ch11-asgi-deployment</st>` <st c="47277">project:</st>

version: '3.0'

services: api: build: ./ch11-asgi volumes:

- ./ch11-asgi/:/usr/src/ch11-asgi/

ports:

- 8000:8000 <st c="47401">depends_on</st>:

- postgres <st c="47425">postgres</st>: <st c="47436">image: «bitnami/postgresql:latest»</st> ports:

- 5432:5432

env_file:

- db.env # 配置 postgres <st c="47530">volumes</st>:

- <st c="47542">database-data:/var/lib/postgresql/data/</st>

volumes: version 指令表示配置将在 Compose 指令中使用 Compose 语法版本。我们的 Compose 配置文件使用版本 3.0,这是撰写本书时的最新版本。较低版本意味着已弃用的关键字和命令。

        <st c="47878">现在,</st> `<st c="47888">services</st>` <st c="47896">指令定义了 Compose 将创建和运行的容器。</st> <st c="47968">我们的包括</st> *<st c="47985">在线杂货</st>* <st c="47999">应用程序(</st>`<st c="48013">api</st>`<st c="48017">)和 PostgreSQL 数据库平台(</st>`<st c="48058">postgres</st>`<st c="48067">)。</st> <st c="48071">在这里,</st> `<st c="48077">api</st>` <st c="48080">是我们应用程序服务的名称。</st> <st c="48129">它包含以下</st> <st c="48155">必需的子指令:</st>

            +   `<st c="48179">build</st>`<st c="48185">: 指向包含 Dockerfile 的本地项目文件夹的位置。</st>

            +   `<st c="48265">ports</st>`<st c="48271">: 将容器的端口映射到主机的端口,可以是 TCP</st> <st c="48333">或 UDP。</st>

            +   `<st c="48340">volumes</st>`<st c="48348">: 将本地项目文件附加到容器指定的目录,如果项目文件有更改,则可以节省镜像重建。</st> <st c="48496">项目文件。</st>

            +   `<st c="48510">depends_on</st>`<st c="48521">: 指出被视为容器依赖项之一的服务名称。</st>

        <st c="48600">另一项服务是</st> `<st c="48620">postgres</st>`<st c="48628">,它为</st> `<st c="48675">api</st>` <st c="48679">服务提供数据库平台,因此存在</st> <st c="48697">两个服务之间的依赖关系。</st> <st c="48734">而不是使用</st> `<st c="48755">build</st>` <st c="48760">指令,它的</st> `<st c="48776">image</st>` <st c="48781">指令将拉取最新的</st> `<st c="48813">bitnami/postgresql</st>` <st c="48831">镜像来创建一个具有空数据库模式的 PostgreSQL 平台的容器。</st> <st c="48919">它的</st> `<st c="48923">ports</st>` <st c="48928">指令表明容器将使用端口</st> `<st c="48982">5432</st>` <st c="48986">来监听数据库连接。</st> <st c="49024">数据库凭据位于由</st> `<st c="49060">db.env</st>` <st c="49066">文件指示的</st> `<st c="49089">env_file</st>` <st c="49097">指令。</st> <st c="49109">以下代码片段显示了</st> `<st c="49156">db.env</st>` <st c="49162">文件的内容:</st>
 POSTGRES_USER=postgres
POSTGRES_PASSWORD=admin2255
POSTGRES_DB=ogs
        <st c="49235">对于</st> `<st c="49240">volumes</st>` <st c="49247">指令的</st> `<st c="49266">postgres</st>` <st c="49274">服务至关重要,因为配置中缺少它意味着容器重启后的数据清理。</st>

        <st c="49406">在最终确定</st> `<st c="49428">docker-compose.yaml</st>` <st c="49447">文件后,运行</st> `<st c="49462">docker-compose --build</st>` <st c="49484">命令来构建或重建服务,然后在</st> `<st c="49553">docker-compose up</st>` <st c="49570">命令之后再次运行,以创建和运行容器。</st> *<st c="49613">图 11</st>**<st c="49622">.12</st>* <st c="49625">显示了运行</st> `<st c="49667">docker-compose up --</st>``<st c="49687">build</st>` <st c="49693">命令后的命令日志:</st>

        ![图 11.12 – 运行 docker-compose up --build 命令时的日志](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_012.jpg)

        <st c="50912">图 11.12 – 运行 docker-compose up --build 命令时的日志</st>

        <st c="50982">另一方面,Docker</st> <st c="50994">桌面仪表板将在成功运行生成的容器后,在</st> *<st c="51082">图 11</st>**<st c="51091">.13</st>* <st c="51094">中显示以下容器结构:</st>

        ![图 11.13 – Docker Desktop 显示 ch11-asgi 和 PostgreSQL 容器](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_013.jpg)

        <st c="51293">图 11.13 – Docker Desktop 显示 ch11-asgi 和 PostgreSQL 容器</st>

        <st c="51370">在这里,</st> `<st c="51377">ch11-asgi-deployment</st>` <st c="51397">在给定的容器结构中是包含</st> `<st c="51483">db.env</st>` <st c="51489">和</st> `<st c="51494">docker-compose.yaml</st>` <st c="51513">文件的部署文件夹的名称,以及执行</st> `<st c="51576">docker-compose</st>` <st c="51590">命令的终端目录。</st> <st c="51610">在 Compose 容器结构内部是服务生成的两个容器。</st> <st c="51709">点击</st> `<st c="51722">api-1</st>` <st c="51727">容器将为我们提供如</st> *<st c="51797">图 11.14</st>**<st c="51806">.14</st>*<st c="51809">所示的 Gunicorn 服务器日志:</st>

        ![图 11.14 – api-1 容器中 ch11-asgi 应用程序的 Gunicorn 服务器日志](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_014.jpg)

        <st c="52764">图 11.14 – api-1 容器中 ch11-asgi 应用程序的 Gunicorn 服务器日志</st>

        <st c="52844">另一方面,点击</st> `<st c="52877">postgres-1</st>` <st c="52887">容器将显示如</st> *<st c="52926">图 11</st>**<st c="52935">.15</st>*<st c="52938">所示的日志:</st>

        ![图 11.15 – postgres-1 容器中的 PostgreSQL 服务器日志](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_015.jpg)

        <st c="54308">图 11.15 – postgres-1 容器中的 PostgreSQL 服务器日志</st>

        <st c="54376">现在,postgres-1 容器中的数据库模式是空的。</st> <st c="54409">为了将本地 PostgreSQL 服务器中的表和数据填充到数据库中,运行</st> `<st c="54528">pg_dump</st>` <st c="54535">来创建一个</st> `<st c="54548">.sql</st>` <st c="54552">转储文件。</st> <st c="54564">然后,在</st> `<st c="54603">.sql</st>` <st c="54607">备份文件所在的目录位置,运行以下</st> `<st c="54639">docker copy</st>` <st c="54650">命令来复制备份文件,例如</st> `<st c="54688">ogs.sql</st>`<st c="54695">,到容器的</st> `<st c="54704">entrypoint</st>` <st c="54714">目录:</st>
 docker cp ogs.sql ch11-asgi-deployment-postgres-1:/docker-entrypoint-initdb.d/ogs.sql
        <st c="54828">然后,使用有效的凭据,例如</st> `<st c="54898">postgres</st>` <st c="54906">及其密码,通过</st> `<st c="54969">docker</st>` `<st c="54976">exec</st>` <st c="54980">命令来转储或执行</st> `<st c="54949">.sql</st>` <st c="54953">文件:</st>
 docker exec -it ch11-asgi-deployment-postgres-1 psql -U postgres -d ogs -f docker-entrypoint-initdb.d/ogs.sql
        <st c="55099">最后,使用数据库</st> `<st c="55123">ch11-asgi-deployment-postgres-1</st>` <st c="55154">的凭据通过</st> `<st c="55172">docker exec</st>` <st c="55183">命令登录到服务器:</st>
 docker exec -it ch11-asgi-deployment-postgres-1 psql -U postgres
        <st c="55293">此外,别忘了</st> <st c="55313">将 `<st c="55350">PooledPostgresqlDatabase</st>` 驱动类的 `<st c="55328">host</st>` 参数替换为容器的名称,而不是 `<st c="55425">localhost</st>` 和其 `<st c="55443">端口</st>` <st c="55447">到 `<st c="55451">5432</st>`<st c="55455">。以下代码片段显示了在 `<st c="55556">app/models/config</st>` 模块中可以找到的驱动类配置更改:</st>
 from peewee_async import PooledPostgresqlDatabase
database = PooledPostgresqlDatabase(
        'ogs',
        user='postgres',
        password='admin2255', <st c="55715">host='ch11-asgi-deployment-postgres-1',</st><st c="55754">port='5432',</st> max_connections = 3,
        connect_timeout = 3
    )
        现在,在生产过程中,如果一个或一些容器失败时,问题就会出现。<st c="55890">默认情况下,它支持在应用程序出现运行时错误或某些内存相关问题时自动重启容器。</st> <st c="56026">此外,Compose 无法在分布式设置中执行容器编排。</st>

        将应用程序部署到不同的主机而不是单个服务器上的另一种强大方法是使用 *<st c="56220">Kubernetes</st>**<st c="56230">。在下一节中,我们将使用 Kubernetes 将我们的 `<st c="56288">ch11-asgi</st>` 应用程序与 Gunicorn 一起作为服务器进行部署。

        部署应用程序到 Kubernetes

        与 Compose 类似,**<st c="56393">Kubernetes</st>** 或 **<st c="56407">K8</st>** 可以管理多个容器,无论它们是否有相互依赖关系。<st c="56482">Kubernetes 可以利用卷存储进行数据持久化,并具有 CLI 命令来管理容器的生命周期。</st> <st c="56606">唯一的区别是 Kubernetes 可以在分布式设置中运行容器,并使用 Pods 来管理其容器。</st>

        在众多安装 Kubernetes 的方法中,本章利用 Docker Desktop 的 **<st c="56835">设置</st>** 中的 **<st c="56796">Kubernetes</st>** 功能,如图 *<st c="56857">图 11.16</st>* 所示。

        ![图 11.16 – 桌面 Docker 中的 Kubernetes](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_016.jpg)

        图 11.16 – 桌面 Docker 中的 Kubernetes

        在 **<st c="57467">设置</st>** 区域勾选 **<st c="57431">启用 Kubernetes</st>** 复选框,并在仪表板右下角点击 **<st c="57495">应用 & 重启</st>** 按钮。<st c="57563">根据 Docker Engine 上运行的容器数量,Kubernetes 出现运行或 *<st c="57620">绿色</st> * <st c="57625">在仪表板左下角,需要一段时间。</st>

        <st c="57732">当 Kubernetes 引擎失败时,在重启</st> <st c="58006">Docker Desktop</st> 之前点击</st> `<st c="57942">C:\Users\alibatasys\AppData\Local\Temp</st>` <st c="57980">文件夹。</st>

        Kubernetes 使用 YAML 文件来定义和创建 Kubernetes 对象,例如</st> **<st c="58098">Deployment</st>**<st c="58108">,</st> **<st c="58110">Pods</st>**<st c="58114">,</st> **<st c="58116">Services</st>**<st c="58124">, 和</st> **<st c="58130">PersistentVolume</st>**<st c="58146">, 所有这些都是建立</st> <st c="58187">一些容器规则、管理主机资源以及构建</st> <st c="58246">容器化</st> <st c="58260">应用程序所必需的。</st> <st c="58274">YAML 格式的对象定义</st> <st c="58310">始终包含以下</st> <st c="58343">清单字段:</st>

            +   `<st c="58359">apiVersion</st>`<st c="58370">: 该字段指示创建 Kubernetes 对象时应使用的适当且稳定的 Kubernetes API。</st> <st c="58474">此字段必须始终出现在文件的第一位。</st> <st c="58523">Kubernetes 有多个 API,例如</st> `<st c="58560">batch/v1</st>`<st c="58568">,</st> `<st c="58570">apps/v1</st>`<st c="58577">,</st> `<st c="58579">v1</st>`<st c="58581">, 和</st> `<st c="58587">rbac.authorization.k8s.io/v1</st>`<st c="58615">, 但最常见的</st> `<st c="58640">v1</st>` <st c="58642">用于</st> `<st c="58647">PersistentVolume</st>`<st c="58663">,</st> `<st c="58665">PersistentVolumeClaims</st>`<st c="58687">,</st> `<st c="58689">Service</st>`<st c="58696">,</st> `<st c="58698">Secret</st>`<st c="58704">, 和</st> `<st c="58710">Pod</st>` <st c="58713">对象创建,以及</st> `<st c="58734">apps/v1</st>` <st c="58741">用于</st> `<st c="58746">Deployment</st>` <st c="58756">和</st> `<st c="58761">ReplicaSets</st>` <st c="58772">对象。</st> <st c="58782">到目前为止,</st> `<st c="58790">v1</st>` <st c="58792">是 Kubernetes API 的第一个稳定版本。</st>

            +   `<st c="58839">kind</st>`<st c="58844">: 该字段标识文件需要创建的 Kubernetes 对象。</st> <st c="58921">在此处,</st> `<st c="58927">kind</st>` <st c="58931">可以是</st> `<st c="58939">Secret</st>`<st c="58945">,</st> `<st c="58947">Service</st>`<st c="58954">,</st> `<st c="58956">Deployment</st>`<st c="58966">,</st> `<st c="58968">Role</st>`<st c="58972">,</st> <st c="58974">或</st> `<st c="58977">Pod</st>`<st c="58980">。</st>

            +   `<st c="58981">metadata</st>`<st c="58990">: 该字段指定文件中定义的 Kubernetes 对象的属性。</st> <st c="59075">属性可能包括</st> *<st c="59106">name</st>*<st c="59110">,</st> *<st c="59112">labels</st>*<st c="59118">,</st> <st c="59120">和</st> *<st c="59124">namespace</st>*<st c="59133">。</st>

            +   `<st c="59134">spec</st>` `<st c="59139">`: 此字段以键值格式提供对象的规范。</st> `<st c="59215">具有不同</st>` `<st c="59253">apiVersion</st>` `<st c="59263">的同一种对象类型可以有不同的</st>` `<st c="59283">规范细节。</st>

        在本章中,Kubernetes 部署涉及从 Docker 仓库中心拉取我们的`<st c="59370">ch11-asgi</st>`文件和最新的`<st c="59415">bitnami/postgresql</st>`镜像。<st c="59433">但在创建部署文件之前,我们的第一个清单关注的是包含`<st c="59556">Secret</st>`对象定义,其目的是存储和确保数据库 PostgreSQL 凭据的安全。</st> `<st c="59650">以下是我们</st>` `<st c="59671">kub-secrets.yaml</st>` `<st c="59687">文件,其中包含我们的</st>` `<st c="59713">Secret</st>` `<st c="59719">对象定义:</st>
<st c="59738">apiVersion: v1</st>
<st c="59753">kind: Secret</st>
<st c="59766">metadata:</st><st c="59776">name: postgres-credentials</st> data:
  # replace this with your base4-encoded username
  user: cG9zdGdyZXM=
  # replace this with your base4-encoded password
  password: YWRtaW4yMjU1
        `<st c="59947">一个</st>` `<st c="59950">Secret</st>` `<st c="59956">对象包含受保护的数据,如密码、用户令牌或访问密钥。</st> `<st c="60035">而不是将这些机密数据硬编码到应用程序中,将它们存储在 Pod 中是安全的,这样其他 Pod 就可以在集群中访问它们。</st>

        `<st c="60193">我们的第二个</st>` `<st c="60205">YAML 文件</st>` `<st c="60216">kub-postgresql-pv.yaml</st>` `<st c="60238">定义了将为我们 PostgreSQL 创建持久存储资源的对象,即</st>` `<st c="60329">PersistentVolume</st>` `<st c="60345">对象。</st> `<st c="60354">由于我们的 Kubernetes 运行在单个节点服务器上,默认存储类是</st>` `<st c="60434">hostpath</st>` `<st c="60442">。此存储将永久保存 PostgreSQL 的数据,即使在我们的容器化应用程序被删除后也是如此。</st> `<st c="60564">以下</st>` `<st c="60578">kub-postgresql-pv.yaml</st>` `<st c="60600">文件定义了将管理我们应用程序数据存储的</st>` `<st c="60618">PersistentVolume</st>` `<st c="60634">对象:</st>
<st c="60690">apiVersion: v1</st>
<st c="60705">kind: PersistentVolume</st>
<st c="60728">metadata:</st><st c="60738">name: postgres-pv-volume</st> labels:
      type: local <st c="60784">spec:</st><st c="60789">storageClassName: manual</st> capacity:
      storage: 5Gi
   accessModes:
       - ReadWriteOnce
   hostPath:
      path: "/mnt/data"
        `<st c="60894">在 Kubernetes 中,利用</st>` `<st c="60937">PersistentVolume</st>` `<st c="60953">对象需要一个</st>` `<st c="60972">PersistentVolumeClaims</st>` `<st c="60994">对象。</st> `<st c="61003">此对象请求集群存储的一部分,Kubernetes</st>` `<st c="61057">Pods</st>` `<st c="61073">将使用这部分存储进行应用程序的读写。</st> `<st c="61125">以下</st>` `<st c="61139">kub-postgresql-pvc.yaml</st>` `<st c="61162">文件为部署的存储创建了一个</st>` `<st c="61179">PersistentVolumeClaims</st>` `<st c="61201">对象:</st>
<st c="61238">kind: PersistentVolumeClaim</st>
<st c="61266">apiVersion: v1</st>
<st c="61281">metadata:</st><st c="61291">name: postgresql-db-claim</st>
<st c="61317">spec:</st> accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
        <st c="61386">持久卷声明</st> `<st c="61391">和</st> `<st c="61413">持久卷</st>` `<st c="61434">对象协同工作,动态地为</st> `<st c="61507">bitnami/postgresql</st>` `<st c="61525">容器</st>` `<st c="61537">分配新的卷存储。</st> `<st c="61541">手动</st>` `<st c="61547">存储类</st>` `<st c="61560">类型表示,对于存储请求,存在从</st> `<st c="61605">持久卷声明</st>` `<st c="61627">到</st> `<st c="61631">持久卷</st>` `<st c="61647">的绑定。</st>

        <st c="61679">在为</st> `<st c="61727">Secret</st>` `<st c="61733">、</st> `<st c="61735">PersistentVolume</st>` `<st c="61751">和</st> `<st c="61757">PersistentVolumeClaims</st>` `<st c="61779">对象创建配置文件后,下一步关键步骤是创建部署配置文件,这些文件将连接</st> `<st c="61881">ch11-asgi</st>` `<st c="61890">和</st> `<st c="61895">bitnami/postgresql</st>` `<st c="61913">Docker 镜像,使用来自</st> `<st c="61973">Secret</st>` `<st c="61979">对象的数据库配置详细信息,利用持久卷声明进行 PostgreSQL 数据持久性,并使用 Kubernetes 服务和 Pods 一起部署和运行它们。</st>` `<st c="62119">在这里,</st>` `<st c="62125">Deployment</st>` `<st c="62135">管理一组 Pod 以运行应用程序工作负载。</st>` `<st c="62190">Pod 作为 Kubernetes 的基本构建块,代表 Kubernetes 集群中的单个运行进程。</st>` `<st c="62307">以下</st>` `<st c="62321">kub-postgresql-deployment.yaml</st>` `<st c="62351">文件告诉 Kubernetes 管理一个实例,该实例将保留</st> `<st c="62415">PostgreSQL 容器</st>` `<st c="62415">:`
<st c="62436">apiVersion: apps/v1</st>
<st c="62456">kind: Deployment</st>
<st c="62541">v1</st> or <st c="62547">apps/v1</st> is the proper choice for the <st c="62584">apiVersion</st> metadata. The <st c="62609">kub-postgresql-deployment.yaml</st> file is a <st c="62650">Deployment</st> type of Kubernetes document, as indicated in the <st c="62710">kind</st> metadata, which will generate a container named <st c="62763">ch11-postgresql</st>:

规格: 副本数: 1

选择器:

匹配标签:

应用: ch11-postgresql

模板:

元数据:

标签:

    应用: ch11-postgresql

规格说明:

终止宽限期秒数: 180 <st c="62932">容器:</st> - 名称: ch11-postgresql

    镜像: bitnami/postgresql:latest

    镜像拉取策略: IfNotPresent

    端口:

        - 名称: tcp-5432

        容器端口: 5432

			<st c="63074">From the</st> <st c="63084">overall state indicated</st> <st c="63108">in the</st> `<st c="63115">spec</st>` <st c="63119">metadata, the deployment will create</st> *<st c="63157">1 replica</st>* <st c="63166">in a Kubernetes pod, with</st> `<st c="63193">ch11-postgresql</st>` <st c="63208">as its label, to run the PostgreSQL server.</st> <st c="63253">Moreover, the deployment will pull the</st> `<st c="63292">bitnami/postgresql:latest</st>` <st c="63317">image to create the PostgreSQL container, bearing the</st> `<st c="63372">ch11-postgresql</st>` <st c="63387">label also.</st> <st c="63400">The configuration also includes a</st> `<st c="63434">terminationGracePeriodSeconds</st>` <st c="63463">value of</st> `<st c="63473">180</st>` <st c="63476">to shut down the database</st> <st c="63503">server safely:</st>

环境:

    - 名称: POSTGRES_USER

        值来源:

        密钥引用: <st c="63570">名称: postgres-credentials</st> 密钥: user

    - 名称: POSTGRES_PASSWORD

        值来源:

        密钥引用: <st c="63658">名称: postgres-credentials</st> 密钥: password

    - 名称: POSTGRES_DB

        值: ogs

    - 名称: PGDATA

        值: /var/lib/postgresql/data/pgdata

			<st c="63783">The</st> `<st c="63788">env</st>` <st c="63791">or environment variables portion provides the database credentials,</st> `<st c="63860">POSTGRES_USER</st>` <st c="63873">and</st> `<st c="63878">POSTGRES_DB</st>`<st c="63889">, to the database, which are base64-encoded values</st> <st c="63940">from the previously created</st> `<st c="63968">Secret</st>` <st c="63974">object,</st> `<st c="63983">postgres-credentials</st>`<st c="64003">. Note that this deployment will also</st> <st c="64041">auto-generate the database with the</st> <st c="64077">name</st> `<st c="64082">ogs</st>`<st c="64085">:</st>

卷挂载:

        - 名称: data-storage-volume

        挂载路径: /var/lib/postgresql/data

    资源:

        请求:

        CPU: "50m"

        内存: "256Mi"

        限制:

        CPU: "500m"

        内存: "256Mi"

卷:

    - 名称: 数据存储卷

    持久卷声明:

        请求名称: postgresql-db-claim

			<st c="64340">The deployment will also allow us to save all data files in the</st> `<st c="64405">/var/lib/postgresql/data</st>` <st c="64429">file of the generated container in the</st> `<st c="64469">ch11-postgresql</st>` <st c="64484">pod, as indicated in the</st> `<st c="64510">volumeMounts</st>` <st c="64522">metadata.</st> <st c="64533">Specifying the</st> `<st c="64548">volumeMounts</st>` <st c="64560">metadata avoids data loss when the database shuts down and makes the database and tables accessible across the network.</st> <st c="64681">The pod will access the volume storage created by the</st> `<st c="64735">postgres-pv-volume</st>` <st c="64753">and</st> `<st c="64758">postgresql-db-claim</st>` <st c="64777">objects.</st>
			<st c="64786">Aside from the</st> `<st c="64802">Deployment</st>` <st c="64812">object, this document defines a</st> `<st c="64845">Service</st>` <st c="64852">type that will expose our PostgreSQL container to other Pods within the cluster at port</st> `<st c="64941">5432</st>` <st c="64945">through</st> <st c="64954">a</st> *<st c="64956">ClusterIP</st>*<st c="64965">:</st>

--- API 版本: v1

类型: Service

元数据:名称: ch11-postgresql-service 标签: 名称: ch11-postgresql

规格:端口:- 端口: 5432 选择器:

应用: ch11-postgresql

			<st c="65127">The</st> `<st c="65132">---</st>` <st c="65135">symbol is a valid separator syntax separating the</st> `<st c="65186">Deployment</st>` <st c="65196">and</st> `<st c="65201">Service</st>` <st c="65208">definitions.</st>
			<st c="65221">Our last</st> <st c="65231">deployment file,</st> `<st c="65248">kub-app-deployment.yaml</st>`<st c="65271">, pulls the</st> `<st c="65283">ch11-asgi</st>` <st c="65292">Docker image and assigns the generated</st> <st c="65332">container to</st> <st c="65345">the Pods:</st>

API 版本: apps/v1

类型: Deployment

元数据:名称: ch11-app 标签:

名称: ch11-app

			<st c="65439">The</st> `<st c="65444">apiVersion</st>` <st c="65454">field of our deployment configuration file is</st> `<st c="65501">v1</st>`<st c="65503">, an appropriate Kubernetes version for deployment.</st> <st c="65555">In this case, our container will be labeled</st> `<st c="65599">ch11-app</st>`<st c="65607">, as indicated in the</st> `<st c="65629">metadata/name</st>` <st c="65642">configuration:</st>

规范: 副本: 1

选择器:

匹配标签:

app: ch11-app

			<st c="65712">The</st> `<st c="65717">spec</st>` <st c="65721">field</st> <st c="65728">describes the overall</st> <st c="65750">state of the deployment, starting with the number of</st> `<st c="65803">replicas</st>` <st c="65811">the deployment will create, how many</st> `<st c="65849">containers</st>` <st c="65859">the Pods will run, the environment variables – namely</st> `<st c="65914">username</st>`<st c="65922">,</st> `<st c="65924">password</st>`<st c="65932">, and</st> `<st c="65938">SERVICE_POSTGRES_SERVICE_HOST</st>` <st c="65967">– that</st> `<st c="65975">ch11-app</st>` <st c="65983">will use to connect to the PostgreSQL container, and the</st> `<st c="66041">containerPort</st>` <st c="66054">variable the container will</st> <st c="66083">listen to:</st>

模板:

元数据:

标签:

    app: ch11-app

规范:

containers:

- <st c="66156">名称: ch11-app</st><st c="66170">镜像: sjctrags/ch11-app:latest</st> 环境变量:

        - 名称: SERVICE_POSTGRES_SERVICE_HOST

        值: ch11-postgresql-service. default.svc.cluster.local

        - 名称: POSTGRES_DB_USER

        值来源:

            密钥键引用: <st c="66354">名称: postgres-credentials</st> 键: user

        - 名称: POSTGRES_DB_PSW

        值来源:

            密钥键引用: <st c="66440">名称: postgres-credentials</st> 键: password

    端口:

    - 容器端口: 8000

			<st c="66509">Also</st> <st c="66515">included in the YAML file</st> <st c="66541">is the</st> `<st c="66548">Service</st>` <st c="66555">type that will make the application to</st> <st c="66595">the users:</st>

--- API 版本: v1

类型: Service

元数据:名称: ch11-app-service

规范:类型: LoadBalancer 选择器:

app: ch11-app <st c="66721">端口:</st> - <st c="66730">协议: TCP</st><st c="66743">端口: 8000</st> 目标端口: 8000

			<st c="66771">The definition links the</st> `<st c="66797">postgres-credentials</st>` <st c="66817">object to the pod’s environment variables that refer to the</st> <st c="66878">database credentials.</st> <st c="66900">It also defines a</st> *<st c="66918">LoadBalancer</st>* `<st c="66931">Service</st>` <st c="66938">to expose our containerized Flask[async] to the HTTP client at</st> <st c="67002">port</st> `<st c="67007">8000</st>`<st c="67011">.</st>
			<st c="67012">To apply</st> <st c="67022">these configuration files, Kubernetes has a</st> `<st c="67066">kubectl</st>` <st c="67073">client command to communicate with Kubernetes</st> <st c="67120">and run its APIs defined in the manifest files.</st> <st c="67168">Here is the order of applying the given</st> <st c="67208">YAML files:</st>

				1.  `<st c="67219">kubectl apply -</st>``<st c="67235">f kub-secrets.yaml</st>`<st c="67254">.</st>
				2.  `<st c="67255">kubectl apply -</st>``<st c="67271">f kub-postgresql-pv.yaml</st>`<st c="67296">.</st>
				3.  `<st c="67297">kubectl apply -</st>``<st c="67313">f kub-postgresql-pvc.yaml</st>`<st c="67339">.</st>
				4.  `<st c="67340">kubectl apply -</st>``<st c="67356">f kub-postgresql-deployment.yaml</st>`<st c="67389">.</st>
				5.  `<st c="67390">kubectl apply -</st>``<st c="67406">f kub-app-deployment</st>`<st c="67427">.</st>

			<st c="67428">To learn about the status and instances that run the applications, run</st> `<st c="67500">kubectl get pods</st>`<st c="67516">. To view the Services that have been created, run</st> `<st c="67567">kubectl get services</st>`<st c="67587">.</st> *<st c="67589">Figure 11</st>**<st c="67598">.17</st>* <st c="67601">shows the list of Services after applying all our</st> <st c="67652">deployment files:</st>
			![Figure 11.17 – Listing all Kubernetes Services with their details](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_017.jpg)

			<st c="67994">Figure 11.17 – Listing all Kubernetes Services with their details</st>
			<st c="68059">To learn all the details about the Services and Pods that have been deployed and the status of each pod, run</st> `<st c="68169">kubectl get all</st>`<st c="68184">. The result will be similar to what’s shown in</st> *<st c="68232">Figure 11</st>**<st c="68241">.18</st>*<st c="68244">:</st>
			![Figure 11.18 – Listing all the Kubernetes cluster details](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_018.jpg)

			<st c="68901">Figure 11.18 – Listing all the Kubernetes cluster details</st>
			<st c="68958">All the</st> <st c="68967">Pods and the containerized</st> <st c="68994">applications can be viewed on Docker Desktop, as shown in</st> *<st c="69052">Figure 11</st>**<st c="69061">.19</st>*<st c="69064">:</st>
			![Figure 11.19 – Docker Desktop view of all Pods and applications](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_11_019.jpg)

			<st c="69586">Figure 11.19 – Docker Desktop view of all Pods and applications</st>
			<st c="69649">Before accessing the</st> `<st c="69671">ch11-asgi</st>` <st c="69680">container, populate the empty PostgreSQL database with the</st> `<st c="69740">.sql</st>` <st c="69744">dump file from the local database.</st> <st c="69780">Use the</st> `<st c="69788">Pod</st>` <st c="69791">name (for example,</st> `<st c="69811">ch11-postgresql-b7fc578f4-6g4nc</st>`<st c="69842">) of the deployed PostgreSQL container and copy the</st> `<st c="69895">.sql</st>` <st c="69899">file to the</st> `<st c="69912">/temp</st>` <st c="69917">directory of the container (for example,</st> `<st c="69959">ch11-postgresql-b7fc578f4-6g4nc:/temp/ogs.sql</st>`<st c="70004">) using the</st> `<st c="70017">kubectl cp</st>` <st c="70027">command and the pod.</st> <st c="70049">Be sure to run the command in the location of the</st> `<st c="70099">.</st>``<st c="70100">sql</st>` <st c="70104">file:</st>

kubectl cp ogs.sql ch11-postgresql-b7fc578f4-6g4nc:/tmp/ogs.sql


			<st c="70174">Run the</st> `<st c="70183">.sql</st>` <st c="70187">file in the</st> `<st c="70200">/temp</st>` <st c="70205">folder of the container using the</st> `<st c="70240">kubectl exec</st>` <st c="70252">command and</st> <st c="70265">the pod:</st>

kubectl exec -it ch11-postgresql-b7fc578f4-6g4nc -- psql -U postgres -d ogs -f /tmp/ogs.sql


			<st c="70365">Also, replace the</st> `<st c="70384">user</st>`<st c="70388">,</st> `<st c="70390">password</st>`<st c="70398">,</st> `<st c="70400">port</st>`<st c="70404">, and</st> `<st c="70410">host</st>` <st c="70414">parameters of</st> <st c="70429">Peewee’s</st> `<st c="70438">Pooled</st>` **<st c="70444">PostgresqlDatabase</st>** <st c="70463">with the environment variables declared in the</st> `<st c="70511">kub-app-deployment.yaml</st>` <st c="70534">file.</st> <st c="70541">The following snippet shows the changes in the driver</st> <st c="70595">class configuration found</st> <st c="70621">in the</st> `<st c="70628">app/models/config</st>` <st c="70645">module:</st>

from peewee_async import PooledPostgresqlDatabase

导入 os

数据库 = PooledPostgresqlDatabase(

    'ogs', <st c="70758">用户=os.environ.get('POSTGRES_DB_USER'),</st><st c="70798">密码=os.environ.get('POSTGRES_DB_PSW'),</st><st c="70842">主机=os.environ.get(</st> <st c="70863">'SERVICE_POSTGRES_SERVICE_HOST'),</st><st c="70897">端口='5432',</st> 最大连接数 = 3,

    连接超时 = 3

)

			<st c="70953">After migrating the tables and the data, the client application can now access the API endpoints of our</st> *<st c="71058">Online Grocery</st>* <st c="71072">application (</st>`<st c="71086">ch11-asgi</st>`<st c="71096">).</st>
			<st c="71099">A Kubernetes pod undergoes</st> `<st c="71434">CrashLoopBackOff</st>` <st c="71450">and stay in</st> **<st c="71463">Awaiting</st>** <st c="71471">mode.</st> <st c="71478">To avoid Pods crashing, always carefully review the definitions files before applying them and monitor the logs of running Pods from time</st> <st c="71616">to time.</st>
			<st c="71624">Sometimes, a Docker or Kubernetes deployment requires adding a reverse proxy server to manage all the incoming requests of the deployed applications.</st> <st c="71775">In the next section, we’ll add the</st> *<st c="71810">NGINX</st>* <st c="71815">gateway server to our containerized</st> `<st c="71852">ch11-asgi</st>` <st c="71861">application.</st>
			<st c="71874">Creating an API gateway using NGINX</st>
			<st c="71910">Our deployment needs</st> `<st c="72284">ch11-asgi</st>` <st c="72293">app and PostgreSQL database platform.</st> <st c="72332">It will serve as the facade of the Gunicorn server running</st> <st c="72391">our application.</st>
			<st c="72407">Here,</st> `<st c="72414">ch11-asgi-dep-nginx</st>` <st c="72433">is a Docker Compose folder consisting of the</st> `<st c="72479">ch11-asgi</st>` <st c="72488">project directory, which contains a Dockerfile, the</st> `<st c="72541">docker-compose.yaml</st>` <st c="72560">file, and the</st> `<st c="72575">nginx</st>` <st c="72580">folder containing a Dockerfile and our NGINX configuration settings.</st> <st c="72650">The following is the</st> `<st c="72671">nginx.conf</st>` <st c="72681">file that’s used by Compose to set up our</st> <st c="72724">NGINX server:</st>

server { 监听 80; 服务器名称 localhost; 位置 / { 代理传递 http://ch11-asgi-dep-nginx-api-1:8000/;

    代理设置头 <st c="72864">X-Forwarded-For</st> $proxy_add_x_forwarded_for;

    代理设置头 <st c="72925">X-Forwarded-Proto</st> $scheme;

    代理设置头 <st c="72969">X-Forwarded-Host</st> $host;

    代理设置头 <st c="73010">X-Forwarded-Prefix</st> /;

}

}


			<st c="73035">The NGINX configuration depends on its installation setup, the applications that have been deployed to the servers, and the server architecture.</st> <st c="73181">Ours is for a reverse proxy NGINX</st> <st c="73215">server of our application deployed on a single server.</st> <st c="73270">NGINX</st> <st c="73276">will allow access to our application through</st> `<st c="73321">localhost</st>` <st c="73330">and port</st> `<st c="73340">80</st>` <st c="73342">instead of</st> `<st c="73354">http://ch11-asgi-dep-nginx-api-1:8000</st>`<st c="73391">, as indicated in</st> `<st c="73409">proxy_pass</st>`<st c="73419">. Since we don’t have a new domain name,</st> `<st c="73460">localhost</st>` <st c="73469">will be the proxy’s hostname.</st> <st c="73500">The de facto request headers, such as</st> `<st c="73538">X-Forwarded-Host</st>`<st c="73554">,</st> `<st c="73556">X-Forwarded-Proto</st>`<st c="73573">,</st> `<st c="73575">X-Forwarded-Host</st>`<st c="73591">, and</st> `<st c="73597">X-Forwarded-Prefix</st>`<st c="73615">, will collectively help the load balancing mechanism during NGINX’s interference on</st> <st c="73700">a request.</st>
			<st c="73710">When the</st> `<st c="73720">docker-compose</st>` <st c="73734">command runs the YAML file, NGINX’s Dockerfile will pull the latest</st> `<st c="73803">nginx</st>` <st c="73808">image and copy the given</st> `<st c="73834">nginx.conf</st>` <st c="73844">settings to the</st> `<st c="73861">/etc/nginx/conf.d/</st>` <st c="73879">directory of its container.</st> <st c="73908">Then, it will instruct the container to run the NGINX server using the</st> `<st c="73979">nginx -g daemon</st>` `<st c="73995">off</st>` <st c="73998">command.</st>
			<st c="74007">Adding NGINX makes the deployed application manageable, scalable, and maintainable.</st> <st c="74092">It can also centralize user request traffic in a microservice architecture, ensuring that the access reaches the expected API endpoints, containers,</st> <st c="74241">or sub-modules.</st>
			<st c="74256">Summary</st>
			<st c="74264">There are several solutions and approaches to migrating a Flask application from the development to the production stage.</st> <st c="74387">The most common server that’s used to run Flask’s WSGI applications in production is Gunicorn.</st> <st c="74482">uWSGI, on the other hand, can run WSGI applications in more complex and refined settings.</st> <st c="74572">Flask[async] applications can run on Uvicorn workers with a</st> <st c="74632">Gunicorn server.</st>
			<st c="74648">For external server-based deployment, the Apache HTTP Server with Python provides a stable and reliable container for running Flask applications with the support of Python’s</st> `<st c="74823">mod_wsgi</st>` <st c="74831">module.</st>
			<st c="74839">Flask applications can also run on containers through Docker and Docker Compose to avoid the nitty gritty configuration and installations in the Apache HTTP Server.</st> <st c="75005">In Dockerization, what matters is the Dockerfile for a single deployment or the</st> `<st c="75085">docker-compose.yaml</st>` <st c="75104">file for multiple deployments and the combinations of Docker instructions that will contain these configuration files.</st> <st c="75224">For a more distributed, flexible, and complex orchestration, Kubernetes’s Pods and Services can aid a better deployment scheme for</st> <st c="75355">multiple deployments.</st>
			<st c="75376">To manage incoming requests across the servers, the Gunicorn servers running in containers can work with NGINX for reverse proxy, load balancing, and additional HTTP security protocols.</st> <st c="75563">A good NGINX setting can provide a better facade for the entire</st> <st c="75627">production setup.</st>
			<st c="75644">Generally, the deployment procedures that were created, applied, and utilized in this chapter are translatable, workable, and reversible to other more modern and advanced approaches, such as deploying Flask applications to Google Cloud and AWS cloud services.</st> <st c="75905">Apart from deployment, Flask has the edge to compete with other frameworks when dealing with innovation and building</st> <st c="76022">enterprise-grade solutions.</st>
			<st c="76049">In the next chapter, we will showcase the use of the Flask platform in providing middleware solutions to many</st> <st c="76160">popular integrations.</st>




第十三章:12

将 Flask 与其他工具和框架集成

Flask 的灵活性、无缝性和可插拔性为构建各种应用程序提供了便利,从简单的基于表单的咨询系统到基于 Docker 的应用程序。 *第 1 * 章到 * 第 10 * 章展示了其简约但强大的框架,以及提供支持、快速解决方案和清洁编码的几个扩展模块和库,适用于 Web 和 API 应用程序。

尽管 Flask 不像 Django 那样适合处理大型企业解决方案,但它可以作为许多企业级系统的中间件或组件,甚至可以成为构建微服务的良好解决方案。 Flask 的卓越灵活性使其成为许多软件基础设施和许多 业务流程的理想选择。

为了清楚地说明 Flask 在流行 Python 框架列表中的位置,本章的目标是强调为移动和前端应用提供后端 Flask 实现的可行性,为微服务架构提供解决方案,以及通过 GraphQL 实现创建和运行查询以及 CRUD 事务的实施方案 通过 GraphQL。

本章涵盖了以下主题: 本章内容:

  • 实现涉及 FastAPI、Django 和 Tornado 的微服务应用

  • 实现 Flask 仪表盘

  • 使用 Swagger 应用 OpenAPI 3.x 规范 与 Swagger

  • 为 Flutter 移动应用程序 提供 REST 服务

  • 使用 React 应用程序消费 REST 端点 React 应用程序

  • 构建一个 GraphQL 应用程序

技术要求

本章使用一个 在线图书馆管理系统 来阐述构建一个微服务应用,该应用集成了 FastAPI、Django 和 Flask 子应用程序,以 Tornado 作为前端应用程序和服务器。 子应用程序使用不同的 URL 前缀进行挂载。 以下是挂载的每个应用程序提供的服务:

  • Django 子模块 – 管理学生 图书借阅者

  • Flask 子模块 – 管理教师 图书浏览器

  • FastAPI 子模块 – 管理借阅者的反馈和投诉 借阅者

  • Flask 主应用程序 – 核心事务 核心事务

  • Tornado 应用程序 – 前端应用程序

图 12**.1 展示了这些已安装应用程序的交易流程。

图 12.1 – 结合 Django、Flask、FastAPI 和 Tornado 的应用程序

图 12.1 – 结合 Django、Flask、FastAPI 和 Tornado 的应用程序

子模块 使用SQLAlchemy 作为 ORM,而 Flask 主应用程序使用的是 标准 Peewee ORM。 本章的所有项目都已上传至 https://github.com/PacktPublishing/Mastering-Flask-Web-Development/tree/main/ch12

实现涉及 FastAPI、Django 和 Tornado 的微服务应用程序

Flask 3.x 提供了一个来自 Werkzeug <st c="2755">DispatcherMiddleware</st> 类,它将隔离的和有效的基于 WSGI 的应用程序组合成一个完整且更大的系统。 这些组合应用程序可以是所有 Flask 或不同的基于 WSGI 的应用程序,如 Django,每个都有独特的 URL 前缀。 图 12**.2 展示了我们 组合项目 的目录结构:

图 12.2 – Django、FastAPI、Flask 和 Tornado 在一个项目结构中

图 12.2 – Django、FastAPI、Flask 和 Tornado 在一个项目结构中

主 Flask 应用程序的所有视图、存储库、服务、模型和配置文件都在 <st c="3496">modules</st> 文件夹中。 另一方面,FastAPI 应用程序的所有应用程序文件都在 <st c="3587">modules_fastapi</st> 文件夹中,Django 应用程序的所有组件都在 <st c="3655">modules_django</st> 文件夹中,所有 Tornado API 处理程序都在 <st c="3710">modules_tornado</st>,所有 GraphQL 组件都在 <st c="3765">modules_sub_flask</st> 目录中。

当涉及到它们各自的模块脚本时,FastAPI 的app 实例位于 <st c="3875">main_fastapi.py</st>,Flask 子模块的app 实例位于 <st c="3934">main_sub_flask.py</st>,而 Flask 主模块的app 实例以及 Tornado 服务器位于 <st c="4033">main.py</st>

现在,让我们讨论如何使用一个 Tornado 服务器 来运行所有这些子应用程序。

添加 Flask 子应用程序

<st c="4168">DispatcherMiddleware</st> 需要 一个 Flask <st c="4207">app</st> 实例作为其第一个参数,以及一个包含子应用程序挂载点的字典,其中 是映射到相应 WSGI <st c="4371">app</st> 实例的 URL 模式。 将 Flask 子应用程序挂载到主 Flask 应用程序是直接的。 以下代码片段展示了如何将 <st c="4510">flask_sub_app</st> 实例的 Flask 子应用程序挂载到核心 Flask <st c="4580">app</st> 实例:

<st c="4593">(main_sub_flask.py)</st> from modules_sub_flask import create_app_sub
from flask_cors import CORS
… … … … … …
flask_sub_app = create_app_sub("../config_dev_sub.toml")
CORS(flask_sub_app) <st c="4776">(main.py)</st>
<st c="4785">from werkzeug.middleware.dispatcher import DispatcherMiddleware</st>
<st c="4849">from main_sub_flask import flask_sub_app</st> … … … … … …
from modules import create_app
app = create_app('../config_dev.toml')
… … … … … …
final_app = <st c="4996">DispatcherMiddleware</st>(<st c="5018">app</st>, {
    '/fastapi': ASGIMiddleware(fast_app),
    '/django': django_app, <st c="5086">'/flask': flask_sub_app</st> })

Flask 子应用程序 必须有一个为其 <st c="5212">flask_sub_app</st> 实例化 专用的模块脚本(例如, <st c="5184">main_flask_sub.py</st>)。 <st c="5241">main.py</st> 必须从专用模块导入 <st c="5265">flask_sub_app</st> 实例,而不是在 <st c="5339">main.py</st> 中创建它,以便于追踪、易于调试和代码整洁。 将 Flask 应用程序组合成更大的单元不需要额外的配置,这与向主上下文添加 FastAPI 应用程序不同。 我们如何将 FastAPI 应用程序注册到 <st c="5589">DispatcherMiddleware</st>中呢?

添加 FastAPI 子应用程序

并非所有基于 ASGI 的应用程序都与 Flask 的上下文兼容,并且可以成为 <st c="5736">DispatcherMiddleware</st> 挂载点的一部分。 对于 FastAPI,解决方案是在运行时使用来自 <st c="5873">a2wsgi</st> 模块的 <st c="5849">ASGIMiddleware</st> <st c="5811">app</st> 实例转换为 WSGI。 要使用 ASGI 到 WSGI 转换器,首先使用以下 <st c="5968">pip</st> 命令安装 <st c="5941">a2wsgi</st>

 pip install a2wsgi

<st c="5999">ASGIMiddleware</st> 不依赖于许多外部模块,因此从其内置机制转换是直接的。 它不会因为转换而消耗更多内存。 但是,如果 FastAPI 应用程序有多个后台任务要执行,实用类有一个构造函数参数, <st c="6296">wait_time</st>,用于设置每个后台任务在请求完成之前允许运行的时间长度。 此外,它的构造函数有一个 <st c="6445">loop</st> 参数,允许设置另一个事件循环,以防核心平台需要不同类型的事件循环。 现在,以下 <st c="6579">main.py</st> 代码片段显示了如何将我们的 FastAPI <st c="6624">app</st> 实例添加到 挂载的应用程序:

 (main_fastapi.py)
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from modules_fastapi.api import faculty
fast_app = FastAPI()
fast_app.include_router(faculty.router, prefix='/ch12')
fast_app.add_middleware(
    CORSMiddleware, allow_origins=['*'],
    allow_credentials=True, allow_methods=['*'], allow_headers=['*'])
(main.py)
from main_fastapi import fast_app
from a2wsgi import ASGIMiddleware
… … … … … …
final_app = DispatcherMiddleware(app, { <st c="7138">'/fastapi': ASGIMiddleware(fast_app),</st> '/django': django_app,
    '/flask': flask_sub_app
})

a2wsgi 模块 与 FastAPI 应用程序配合得很好。 并非所有基于 ASGI 的应用程序都可以像 FastAPI 应用程序一样与 <st c="7357">a2wsgi</st> 无缝转换。

现在让我们将我们的 Django 子模块添加到我们的 挂载的应用程序。

添加 Django 子应用程序

Django 是一个纯 WSGI 框架,但可以通过额外的配置在 ASGI 服务器上运行。 与 FastAPI 不同,将 Django 应用程序添加到挂载需要几个步骤,包括在 main.py 模块和 Django 管理员的 <st c="7740">settings.py</st>中的以下程序:

  1. 由于 <st c="7759">module_django</st> 不是主项目文件夹,导入 <st c="7816">os</st> 模块并将 <st c="7859">DJANGO_SETTINGS_MODULE</st> 环境变量 设置为 <st c="7906">modules_django.modules_django.settings</st>

     os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'modules_django.modules_django.settings')
    

    此设置定义了 Django 管理文件夹中 <st c="8074">settings.py</st> 的位置。 未能调整此设置将导致以下 运行时错误:

    <st c="8316">settings.py</st> with the Django directory name requires adjusting some package names in the Django project. Among the modifications is the change of <st c="8461">ROOT_URLCONF</st> in <st c="8477">settings.py</st> from <st c="8494">'modules_django.urls'</st> to <st c="8519">'modules_django.modules_django.urls'</st>.
    
  2. 将 Django 应用程序注册到 <st c="8597">INSTALLED_APPS</st> 设置中必须包含 Django 项目名称:

     INSTALLED_APPS = [
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
        'rest_framework',
        'corsheaders', <st c="8873">'modules_django.olms'</st> ]
    
  3. 此外,在定义 Django 应用程序对象时,包括 Django 项目文件夹 <st c="8981">settings.py</st>中:

     WSGI_APPLICATION = '<st c="9088">modules_django.olms</st>), import all the custom components with the project folder included. The following snippet shows the implementation of REST services that manage student borrowers using Django RESTful services and Django ORM:
    
    

    (views.py) from rest_framework.response import Response

    from rest_framework.decorators import api_view

    <st c="9426">modules_django</st>.olms.serializer 导入 BorrowedHistSerializer, StudentBorrowerSerializer

    <st c="9519">modules_django</st>.olms.models 导入 StudentBorrower, BorrowedHist

    @api_view(['GET'])

    def getData(request):

    app = StudentBorrower.objects.all()
    
    serializer = StudentBorrowerSerializer(app, many=True)
    
    return Response(serializer.data)
    

    @api_view(['POST'])

    def postData(request):

    serializer = StudentBorrowerSerializer(data=request.data)
    
    if serializer.is_valid():
    
        serializer.save()
    
        return Response(serializer.data)
    
    else:
    
        return Response({"message:error"})
    
    
    
  4. 由于我们正在将 Django 应用程序作为 WSGI 子应用程序进行挂载,请使用 <st c="10088">os</st> 模块将 <st c="10043">DJANGO_ALLOW_ASYNC_UNSAFE</st> 设置为<st c="10072">false</st>

     os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
    

    设置此设置为 <st c="10179">true</st> 将导致此 运行时异常:

    django.core.exceptions.SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.
    
  5. 最后,从 <st c="10389">django.core.wsgi</st> 模块导入 <st c="10359">get_wsgi_application</st> 并将其注册到 <st c="10432">DispatcherMiddleware</st>。对于 Django 网络应用程序,从 <st c="10518">django.contrib.staticfiles.handlers</st> 模块导入 <st c="10490">StaticFilesHandler</st> 并将 <st c="10574">get_wsgi_application()</st> 返回的对象包装起来以访问静态网络文件(例如 CSS、JS、图像):

    <st c="10671">from django.core.wsgi import get_wsgi_application</st>
    <st c="10721">from django.contrib.staticfiles.handlers import StaticFilesHandler</st> … … … … … … <st c="10800">django_app = StaticFilesHandler(</st> <st c="10832">get_wsgi_application())</st> … … … … … …
    final_app = DispatcherMiddleware(app, {
        '/fastapi': ASGIMiddleware(fast_app), <st c="10946">'/django': django_app,</st> '/flask': flask_sub_app
    })
    

现在,在挂载 FastAPI、Django 和 Flask 子应用程序之后,是时候将主 Flask 应用程序挂载到 Tornado 服务器上了。

将所有内容与 Tornado 结合起来

尽管在生产服务器上使用 Gunicorn 运行 Flask 应用程序是理想的,但有时对于更注重事件驱动事务、WebSocket 和 服务器端事件 (SSE) 的 Flask 项目,使用非阻塞的 Tornado 服务器是一个完美的选择。对于本章,我们的设计是将实现核心 在线图书馆管理系统 事务的主 Flask 应用程序挂载到 Tornado 应用程序上。顺便说一下,Tornado 是一个 Python 框架和异步网络库,其优势在于事件驱动、非阻塞和需要长时间连接到用户的长时间轮询事务。它包含一个内置的非阻塞 HTTP 服务器,与 FastAPI 不同。

要在 Tornado 的 HTTP 服务器上运行一个 兼容的 WSGI 应用程序,它 有一个 <st c="11940">WSGIContainer</st> 实用类,该类封装了一个核心 Flask <st c="11992">app</st> 实例并在非阻塞或异步 服务器模式下运行应用程序。

重要提示

在撰写本书时, <st c="12130">Flask[async]</st> 在 Tornado 服务器上挂载异步 HTTP 请求会抛出 <st c="12207">SynchronousOnlyOperation</st> 错误。 因此,本章仅关注标准的 Flask 请求 事务。

以下 <st c="12329">main.py</st> 代码片段显示了我们的核心 <st c="12378">Flask app</st> 与 Tornado 服务器的集成:

<st c="12413">from tornado.wsgi import WSGIContainer</st>
<st c="12452">from tornado.web import FallbackHandler, Application</st> from tornado.platform.asyncio import AsyncIOMainLoop <st c="12559">from modules_tornado.handlers.home import MainHandler</st> import asyncio
… … … … … …
from modules import create_app
app = create_app('../config_dev.toml')
… … … … … …
final_app = DispatcherMiddleware(app, {
    '/fastapi': ASGIMiddleware(fast_app),
    '/django': django_app,
    '/flask': flask_sub_app
}) <st c="12850">main_flask = WSGIContainer(final_app)</st>
<st c="12887">application = Application([</st> (r"/ch12/tornado", MainHandler), <st c="12949">(r".*", FallbackHandler, dict(fallback=main_flask))</st>, <st c="13002">])</st> if __name__ == "__main__":
    loop = asyncio.get_event_loop() <st c="13064">application.listen(5000)</st> loop().run_forever()

Tornado 不是一个 WSGI 应用程序,也不是线程安全的。 它使用一个 线程来管理一次一个进程。 它是一个为事件驱动应用程序设计的框架,内置的服务器用于运行非阻塞 I/O 套接字。 但是,现在它可以直接使用 <st c="13375">asyncio</st> 来异步运行我们的挂载应用程序,以替代 <st c="13448">IOLoop</st> 用于事件。 在给定的代码片段中,我们的 Tornado 和主 Flask 应用程序使用通过 <st c="13540">asyncio</st> 平台获取的主事件循环 <st c="13596">get_event_loop()</st>运行。 顺便说一下,我们的完整系统有一个 Tornado 处理程序, <st c="13669">MainHandler</st>,位于 <st c="13685">modules_tornado</st> 中,用于以 JSON 格式显示欢迎信息。

此时,由于挂载了不同的 WSGI 和 ASGI 应用程序,我们的应用程序已经变得复杂,因此它需要一个名为 仪表化 的微服务机制来 监控每个挂载应用程序的低级行为并捕获低级指标,例如数据库相关的日志、内存使用情况以及特定库的日志和问题。

实现 Flask 仪表化

仪表化是一种 机制,用于生成、收集和导出有关应用程序、微服务或分布式设置的运行时诊断数据。 通常,这些可观察数据包括跟踪、日志和指标,可以提供对系统的理解。 在实现仪表化的许多方法中, <st c="14669">pip</st> 命令:

 pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-flask opentelemetry-instrumentation-requests

<st c="14804">以下</st> <st c="14819">片段添加到 <st c="14840">create_app()</st> <st c="14852">工厂函数中,位于 <st c="14868">__init__.py</st> <st c="14879">的主 Flask 应用程序中,提供了基于控制台的监控:</st>

 from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.trace.export import ConsoleSpanExporter
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.instrumentation.flask import FlaskInstrumentor
def create_app(config_file):
    provider = TracerProvider(resource= Resource.create({SERVICE_NAME: "<st c="15437">packt-flask-service</st>"}))
    processor = BatchSpanProcessor(ConsoleSpanExporter())
    provider.add_span_processor(processor)
    trace.set_tracer_provider(provider)
    global tracer
    tracer = trace.get_tracer("<st c="15633">packt-flask-tracer</st>")
    app = OpenAPI(__name__, info=info)
    … … … … … … <st c="15703">FlaskInstrumentor(app).instrument(</st><st c="15737">enable_commenter=True, commenter_options={})</st> … … … … … …

<st c="15793">OpenTelemetry</st> <st c="15807">要求 Flask 应用程序设置由 <st c="15879">TracerProvider</st> <st c="15893"> Tracer <st c="15907">Span</st> <st c="15911">(s)</st> <st c="15917">组成的追踪 API。</st> TracerProvider 是 API 的入口点和创建追踪器的注册表。 <st c="16027">追踪器负责创建跨度。</st> 另一方面,跨度是一个 API,可以监控应用程序的任何部分。

在实例化<st c="16152">TracerProvider</st>之后,创建追踪器的一部分设置是应用<st c="16238">Batch</st> SpanProcessor,它将批处理跨度在导出到另一个系统、工具或后端之前进行预处理。 它在其构造函数参数中需要一个特定的导出器类,例如<st c="16433">ConsoleSpanExporter</st>,它将跨度发送到控制台。 为了完成<st c="16507">TracerProvider</st>的设置,使用其<st c="16586">add_span_processor()</st> 方法将处理器添加到<st c="16554">TracerProvider</st> 对象中。

<st c="16614">最后,从 <st c="16661">opentelemetry</st> <st c="16674">模块导入跟踪 API 对象,并调用其 <st c="16697">set_tracer_provider()</st> <st c="16718">类方法来设置创建的 <st c="16751">TracerProvider</st> <st c="16765">实例。</st> 要提取 tracer 对象,调用其 get_tracer() 方法并指定其名称,例如 packt-flask-tracer `。

<st c="16886">现在,在任何地方导入</st> <st c="16903">tracer</st> <st c="16909">对象。</st> 以下模块脚本导入 tracer 对象以监控 add_login() 端点:`

<st c="17040">from modules import tracer</st> @current_app.post("/login/add)
def add_login(): <st c="17116">with tracer.start_as_current_span('users_span'):</st> login_json = request.get_json()
        repo = LoginRepository()
        result = repo.insert_login(login_json)
        if result == False:
            return jsonify(message="error"), 500
        else:
            return jsonify(record=login_json)

调用 <st c="17371">start_as_current_span()</st> 方法创建一个 <st c="17407">tracer</st> 对象,创建一个跟踪内的单个操作,即跨度。 对于更大的系统,跨度可以嵌套以形成一个跟踪树,以便进行详细的监控。 嵌套跟踪包含一个根跨度,通常描述了 上层操作,以及一个或多个子跨度来描述其下层操作。 图 12**.3 显示了运行 <st c="17760">add_login()</st> 端点后的控制台日志:

图 12.3 – 导出至控制台的 tracer 日志

图 12.3 – 导出至控制台的 tracer 日志

此外,Jaeger,一个分布式跟踪 平台,可以将跟踪中的所有日志以图形视图进行可视化。 OpenTelemetry 有一个导出类,可以在预处理后将跨度导出到 Jaeger 平台。 但是首先,通过其 Docker 镜像或二进制文件安装 Jaeger。 在本章中,我们通过其二进制文件中的 <st c="18672">jaeger-all-in-one.exe</st> 命令启动 Jaeger 服务器。

然后,使用以下 <st c="18799">pip</st> 命令安装 Jaeger 支持的 OpenTelemetry 模块:

 pip install opentelemetry-exporter-jaeger

安装完成后,将以下片段添加到 <st c="18947">create_app()</st> 工厂中的先前 OpenTelemetry 设置:

<st c="18968">from opentelemetry.exporter.jaeger.thrift import JaegerExporter</st> … … … … … …
    trace.set_tracer_provider(provider) <st c="19080">jaeger_exporter = JaegerExporter(agent_host_name= "localhost", agent_port=6831,)</st><st c="19160">trace.get_tracer_provider().add_span_processor( BatchSpanProcessor(jaeger_exporter))</st> global tracer
    tracer = trace.get_tracer("packt-flask-tracer")
    … … … … … …

<st c="19319">JaegerExporter</st> 将跟踪发送到通过 HTTP 协议运行的 Thrift 服务器。 导出类的构造函数参数都是关于 Thrift 服务器服务器细节的。 在我们的案例中, <st c="19523">agent_host_name</st> <st c="19542">localhost</st> ,而 <st c="19556">agent_port</st> <st c="19570">6831</st>

重启 Tornado 服务器,运行监控的 API,并使用浏览器打开 <st c="19661">http://localhost:16686/</st> 以检查跟踪。 图 12**.4 显示了搜索 搜索 四个跟踪后的 Jaeger 仪表板的 搜索 页面:

图 12.4 – Jaeger 对跨度跟踪的搜索结果

图 12.4 – Jaeger 对跨度跟踪的搜索结果

Jaeger 的左侧部分是packt-flask-service,当时搜索时提供了四个搜索结果。 在仪表板的右侧部分是搜索结果列表,列出了监控所执行事务的跨度产生的跟踪。 点击每一行都会以图形格式显示跟踪详情。 另一方面,页眉部分的图形总结了在指定时间段内的所有跟踪。

`现在让我们探索如何将 OpenAPI 3.x 文档添加到我们 Flask 应用程序的 API 端点。

`使用 Swagger 应用 OpenAPI 3.x 规范

<st c="21046">除了</st> 仪器化之外,一些解决方案提供了格式良好的 API 文档,例如 FastAPI。 这些解决方案之一是使用<st c="21180">flask_openapi3</st>,它将 OpenAPI 3.x 规范应用于实现 Flask 组件,并使用 Swagger、ReDoc、 和 RapiDoc 来记录 API 端点。

<st c="21330">flask_openapi3</st> 不是 Flask 的库或依赖模块,而是一个基于当前 Flask 3.x 的独立框架,使用<st c="21458">pydantic</st> 模块来支持 OpenAPI 文档。 它还支持<st c="21525">Flask[async]</st> 组件。

<st c="21549">在</st> <st c="21567">flask_openapi3</st> 使用<st c="21592">pip</st> 命令安装后,将<st c="21617">Flask</st> 类替换为<st c="21634">OpenAPI</st> <st c="21646">Blueprint</st> 替换为<st c="21661">APIBlueprint</st>。这些仍然是原始的 Flask 类,但增加了添加 API 文档的功能。 以下是我们 <st c="21789">create_app()</st> 工厂方法 ,它使用<st c="21857">OpenAPI</st> 创建应用程序对象,并添加了 文档组件:

<st c="21938">from flask_openapi3 import Info</st>
<st c="21970">from flask_openapi3 import OpenAPI</st> … … … … … … <st c="22017">info = Info(title="Flask Interoperability (A</st> <st c="22061">Microservice)", version="1.0.0")</st> … … … … … …
def create_app(config_file):
    … … … … … … <st c="22147">app = OpenAPI(__name__, info=info)</st> app.config.from_file(config_file, toml.load)
    cors = CORS(app)
    app.config['CORS_HEADERS'] = 'Content-Type'
    … … … … … …

<st c="22299">The</st> <st c="22304">Info</st> 实用程序类提供了 OpenAPI 文档的项目标题。 它的实例是 OpenAPI 构造函数参数值的一部分。

<st c="22448">要记录 API,请添加端点的摘要、API 事务的完整描述、标签、请求字段描述和响应细节。</st> <st c="22603">以下代码片段显示了list_login()` 端点的简单文档:

<st c="22683">from flask_openapi3 import Tag</st>
<st c="22714">list_login_tag = Tag(name="list_login", description="List all user credentials.")</st> … … … … … …
@current_app.get("/login/list/all", <st c="22844">summary="List all login records.", tags=[list_login_tag]</st>)
def list_login(): <st c="22921">"""</st><st c="22924">API for retrieving all the records from the olms database.</st><st c="22983">"""</st> with tracer.start_as_current_span('users_span'):
        repo = LoginRepository()
        result = repo.select_all_login()
        print(result)
        return jsonify(records=result)

OpenAPI 的 HTTP 方法装饰器允许额外的 参数,例如描述 API 端点的摘要和标签。 Tags 类是一个 <st c="23265">flask_openapi3</st> 实用工具,它能够为端点创建标签。 端点可以与一系列标签对象相关联。 另一方面,放置在 API 函数第一行的文档注释成为 API 实现的完整和详细描述。 API 实现。

现在,使用 <st c="23607">flask_openapi3</st> 框架的应用程序有一个额外的端点, <st c="23660">/openapi/swagger</st>,当在网页浏览器上运行时,将渲染 Swagger 文档,如图 图 12**.5所示:

图 12.5 – Flask 中的 OpenAPI/Swagger 文档

图 12.5 – Flask 中的 OpenAPI/Swagger 文档

只要 <st c="24455">flask-openapi3</st> 保持最新并与 Flask 版本同步,它就是一个有用且可行的解决方案,用于使用 OpenAPI/Swagger 实现文档的 Flask 应用程序。 该框架可以提供可接受和标准的 API 文档,而无需在 <st c="24772">main.py</st>中进行额外的 YAML 或 JSON 配置。

在下一节中,让我们讨论我们的 Flask 应用程序如何以 Python 平台之外的方式提供服务,从 移动开发 开始。

为 Flutter 移动应用程序提供 REST 服务

Flask 应用程序可以成为许多流行移动平台(如 Flutter)的潜在后端 API 服务提供商。 Flutter 是由 Google 创建的开源移动工具包,旨在为移动平台提供商业上可接受的本地编译应用程序。 它可以作为我们 微服务应用程序的前端框架。

要启动 Flutter,下载最新的 Flutter SDK 版本 – 在我的案例中,对于 Windows,从 以下 https://docs.flutter.dev/release/archive?tab=windows 下载站点。 将文件解压到您的开发目录,并在 Windows 全局类路径中注册 <st c="25550">%FLUTTER_HOME%\bin</st>

之后,从https://developer.android.com/studio下载最新的 Android Studio。然后,打开新安装的 Android Studio 并转到 工具 | SDK 管理器 以安装以下组件:

  • Android SDK 平台,最新的 API 版本

  • Android SDK 命令行工具

  • Android SDK 构建工具

  • Android SDK 平台工具

  • Android 模拟器

在成功更新 Android SDK 和安装 SDK 组件后,打开终端并执行以下步骤进行 Flutter 诊断:

  1. 在终端上运行 <st c="26108">flutter doctor</st> 命令。

  2. 在开发之前,所有设置和工具都必须满足。 在这个安装阶段不应该有任何问题。

  3. 使用带有 Flutter 扩展的 VS Code 编辑器创建一个 Flutter 项目;它应该具有 图 12**.6中展示的项目结构:

图 12.6 – 我们的 ch12-flutter-flask 项目

图 12.6 – 我们的 ch12-flutter-flask 项目

<st c="26681">/lib/olms/provider</st>中, <st c="26701">providers.dart</st> 实现了消耗 HTTP <st c="26766">GET</st> <st c="26774">POST</st> 的我们的 <st c="26785">APIs</st> <st c="26789">用户管理的事务。</st> <st c="26812">以下是我们的</st> dart` 服务提供者的代码:

 import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:library_app/olms/models/login.dart';
class LoginProvider with ChangeNotifier{
  List<Login> _items = [];
  List<Login> get items {
    return [..._items];
  }
  Future<void> addLogin(String username, String password, String role ) async { <st c="27210">String url = 'http://<actual IP address>:5000/login/add';</st> try{
      if(username.isEmpty || password.isEmpty || role.isEmpty){
         return;
      }
      Map<String, dynamic> request = {"username": username, "password": password, "role": int.parse(role)};
      final headers = {'Content-Type': 'application/json'}; <st c="27497">final response = await http.post(Uri.parse(url), headers: headers, body: json.encode(request));</st> Map<String, dynamic> responsePayload = json.decode(response.body);
      final login = Login(
          username: responsePayload["username"],
          password: responsePayload["password"],
          role: responsePayload["role"]
      );
      print(login);
      notifyListeners();
    }catch(e){
      print(e);
    }
  }

给定的 <st c="27860">addLogin()</st> 消耗了 <st c="27879">的</st> add_login()<st c="27895">API</st> <st c="27900">从我们的 Flask</st>微服务应用:

 Future<void> get getLogin async { <st c="27967">String url = 'http://<actual IP address>:5000/login/list/all';</st> var response;
    try{ <st c="28049">response = await http.get(Uri.parse(url));</st><st c="28091">Map body = json.decode(response.body);</st> List<Map> loginRecs = body["records"].cast<Map>();
      print(loginRecs);
      _items = loginRecs.map((e) => Login(
          id: e["id"],
          username: e["username"],
          password: e["password"],
          role: e["role"],
      )
      ).toList();
    }catch(e){
      print(e);
    }
    notifyListeners();
  }
}

<st c="28381">登录</st> 模型类 中提到的代码是一个 Flutter 类,该类将 <st c="28449">addLogin()</st> <st c="28464">getLogin()</st> 提供者事务映射到 Flask API 端点的 JSON 记录。 以下是从 <st c="28583">Login</st> 模型类派生出的 Flutter 的 <st c="28618">ch12-microservices-interop</st>的 SQLAlchemy `模型层:

<st c="28671">(/lib/olms/models/login.dart</st> class Login{
  int? id;
  String username;
  String password;
  int role;
  Login({ required this.username, required this.password, required this.role, this.id=0});
}

现在,给定的 <st c="28873">getLogin()</st> 从我们的 <st c="28898">登录</st> 记录中检索 JSON 格式的记录,这些记录来自我们的 <st c="28936">list_login()</st> 端点函数。 请注意,Flutter 需要主服务器的实际 IP 地址来访问 API 端点资源

以下 <st c="29097">/lib/olms/tasks/task.dart</st> 文件实现了表单小部件及其对应的事件,这些事件调用这两个服务方法。 实现的部分展示了 <st c="29275">登录</st> 表单:

<st c="29286">(/lib/olms/tasks/task.dart)</st> … … … … … … <st c="29326">class _TasksWidgetState extends State<LoginViewWidget> {</st> … … … … … …
  @override
  Widget build(BuildContext context) {
    return Padding(
    … … … … … …
        children: [
          Row(
            children: [
              Expanded(
                child: TextFormField(
                  controller: userNameController,
                  decoration: const InputDecoration(
                    labelText: 'Username',
                    border: OutlineInputBorder(),
                  ),
              … … … … … …
              Expanded(
                child: TextFormField(
                  … … … … … …
                    labelText: 'Password',
                    border: OutlineInputBorder(),
                  ),
                ),
              ),
              Expanded(
                child: TextFormField(
                    … … … … … …
                    labelText: 'Role',
                    border: OutlineInputBorder(),
                  ),
              … … … … … …
              const SizedBox(width: 10,),
              ElevatedButton(
                  … … … … … …
                  child: const Text("Add"),
                  onPressed: () {
                    Provider.of<LoginProvider>(context, listen: false).addLogin(userNameController.text, passwordController.text, roleController.text);
                    … … … … … …
                  }
              )
            ],
          ),

一般来说, <st c="30143">LoginViewWidget</st> 组件 返回一个由两个子小部件组成的<st c="30179">Padding</st> 小部件。 前面的代码渲染了一个包含三个<st c="30277">TextFormField</st> 小部件的水平表单,这些小部件将接受用户的登录详情,以及一个<st c="30351">ElevatedButton</st> 小部件,该小部件将触发<st c="30388">add_login()</st> 提供者事务以调用我们的 Flask <st c="30441">/login/add</st> 端点。 以下代码显示了<st c="30504">LoginViewWidget</st> 的下一部分,该部分渲染了从我们的数据库中获取的<st c="30545">登录</st> 记录列表:

 … … … … … …
FutureBuilder(future: Provider.of<LoginProvider>(context, listen: false).getLogin,
       builder: (ctx, snapshot) =>
         snapshot.connectionState == ConnectionState.waiting
          ? const Center(child: CircularProgressIndicator())
          : Consumer<LoginProvider>(
              … … … … … …
              builder: (ctx, loginProvider, child) =>
                … … … … … …
                    Container(
                    … … … … … … …
                      child: SingleChildScrollView(
                        scrollDirection: Axis.horizontal,
                            child: DataTable(
                              columns: <DataColumn>[
                                 DataColumn(
                                  label: Text(
                                    'Username',
                                    style:
                                        … … … … … …
                                ),
                                DataColumn(
                                  label: Text(
                                    'Password',
                                    style:
                                 … … … … … …
                                ),
                                DataColumn(
                                  label: Text(
                                    'Role',
                                … … … … … …
                                ),],
                         rows: <DataRow>[
                           DataRow(cells: <DataCell>[
                             DataCell(Text( loginProvider. items[i].username)),
                             DataCell(Text(loginProvider. items[i].password)),
                             DataCell(Text(loginProvider. items[i].role.toString())),
                                  ],
                             … … … … … …

前面的代码 渲染了一个 <st c="31438">FutureWidget</st> 组件,该组件由<st c="31473">ListView</st> 中的<st c="31485">DataColumn</st> <st c="31500">DataRow</st> 小部件组成,用于展示从我们的 Flask <st c="31561">/login/list/all</st> 端点获取的 <st c="31532">登录</st> 记录。 它有一个由其<st c="31631">SingleChildScrollView</st> 小部件渲染的垂直滚动条。 图 12**.7 显示了运行我们的 <st c="31732">ch12-flask-flutter</st> 应用程序后得到的 <st c="31693">LoginViewWidget</st> 表单,使用的是在<st c="31804">/</st>``<st c="31805">library_app</st> 目录内执行的<st c="31773">flutter run</st> 命令。

图 12.7 – 用于登录事务的 Flutter 表单

图 12.7 – 用于登录事务的 Flutter 表单

除了移动开发之外,Flask API 还可以为流行的前端框架,如 React 提供数据服务。 让我们在下一节中看看如何实现。

使用 React 应用程序消费 REST 端点

React 是一个流行的前端服务器端 JavaScript 库,用于构建网站的可扩展用户界面,主要用于 单页应用程序 (SPAs)。 它是一个流行的库,用于渲染页面,该页面在 不重新加载 页面的情况下改变其状态。

使用 <st c="32447">create-react-app</st> 命令创建 React 项目后,我们的 <st c="32501">ch12-react-flask</st> 应用程序实现了以下功能组件来构建表单页面和列出所有在线图书馆管理系统教员借阅者的表格:

 export const FacultyBorrowers =(props)=>{
    const [id] = React.useState(0);
    const [firstname, setFirstname] = React.useState('');
    const [lastname, setLastname] = React.useState('');
    const [empid, setEmpid] = React.useState('');
    const [records, setRecords] = React.useState([]);

给定的 <st c="32980">useState()</st> 钩子方法定义了表单组件在提交详细信息到 <st c="33123">add_faculty_borrower()</st> API 端点之前将使用的状态变量:

 React.useEffect(() => {
        const url_get = 'http://localhost:5000/fastapi/ ch12/faculty/borrower/list/all';
        fetch(url_get)
        .then((response) =>  response.json() )
        .then((json) =>  { setRecords(json)})
        .catch((error) => console.log(error));
      }, []);
    const addRecord = () =>{
         const url_post = 'http://localhost:5000/fastapi/ ch12/faculty/borrower/add';
         const options = {
            method: 'POST',
            headers:{
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(
                {
                'id': id,
                'firstname': firstname,
                'lastname': lastname,
                'empid': empid
                }
            )
        }
         fetch(url_post, options)
            .then((response) => { response.json() })
            .then((json) => { console.log(json)})
            .catch((error) => console.log(error));
            const url_get = 'http://localhost:5000/fastapi/ ch12/faculty/borrower/list/all';
            fetch(url_get)
            .then((response) =>  response.json() )
            .then((json) =>  { setRecords(json)})
            .catch((error) => console.log(error));
    }

在将 JSON 数据提交到 <st c="34175">add_faculty_borrower()</st> API 端点之前, <st c="34077">addRecord()</st> 事件 方法会从状态变量中形成 JSON 数据。 同样,它通过同一 <st c="34295">list_all_faculty_borrowers()</st> 端点从微服务中检索所有教员借阅者。

 return <div>
      <form id='idForm1' <st c="34402">onSubmit={ addRecord }</st>>
        Employee ID: <input type='text' <st c="34459">onChange={ (e) => {setEmpid(e.target.value)}}</st> /><br/>
        First Name: <input type='text' <st c="34544">onChange={ (e) => {setFirstname(e.target.value) }}</st> /><br/>
        Last Name: <input type='text' <st c="34633">onChange={ (e) => {setLastname(e.target.value)}}</st>/><br/>
        <input type='submit' value='ADD Faculty Borrower'/>
            </form>
            <br/>
            <h2>List of Faculty Borrowers</h2>
            <table >
              <thead>
                  <tr><th>Id</th>
                      <th>Employee ID</th>
                      <th>First Name</th>
                      <th>Last Name</th>
              </tr></thead>
              <tbody> <st c="34906">{records.map((u) => (</st> <tr>
                    <td>{u.id}</td>
                    <td>{u.empid}</td>
                    <td>{u.firstname}</td>
                    <td>{u.lastname}</td>
                  </tr>
                ))}
            </tbody></table>
        </div>}

在数据库中添加一个新的教员借阅记录后, <st c="35052">records.map()</st> 函数构建了记录表。 <st c="35163">addRecord()</st>,借助 <st c="35197">useEffect()</st> 钩子方法,从 <st c="35256">list_all_faculty_borrowers()</st> API 中捕获所有记录,并将 JSON 格式的数据列表存储到状态 变量 <st c="35355">records</st>中。

除了为其他平台构建服务提供商之外,Flask 还可以构建用于轻松 CRUD 操作的 GraphQL 应用。 让我们在下一讨论中了解它。

构建 GraphQL 应用

GraphQL 是一种 机制,它提供了一个平台无关的 CRUD 事务,无需指定实际的数据库连接细节和数据库方言。 此机制以模型为中心或以数据为中心,并关注用户通过后端 API 实现想要获取的数据。

我们设计的 Flask 子应用程序 是一个 GraphQL 应用程序,具有以下 HTTP GET 端点,用于创建 GraphQL UI 探索器:

<st c="36039">from ariadne.explorer import ExplorerGraphiQL</st> … … … … … … <st c="36097">flask_sub_app = create_app_sub("../config_dev_sub.toml")</st> CORS(flask_sub_app) <st c="36174">explorer_html = ExplorerGraphiQL().html(None)</st>
<st c="36219">@flask_sub_app.route("/graphql", methods=["GET"])</st> def graphql_explorer():
    return explorer_html, 200

我们的解决方案 使用了 <st c="36342">Ariadne</st> 模块,因为它更新且可以与 Flask 3.x 组件集成。 图 12**.8 显示了访问给定的 <st c="36496">/</st>``<st c="36497">flask/graphql</st> 端点后的实际 GraphQL 探索器。

图 12.8 – Ariadne GraphQL 探索器

图 12.8 – Ariadne GraphQL 探索器

接下来,构建关键的 GraphQL 模式配置,即 <st c="36742">schema.graphql</st> 文件,该文件创建所有从 ORM 模型派生的 GraphQL 模型类、请求数据和响应对象,这些对象是假设的 API 端点。 以下是我们 Flask 子应用程序使用的 <st c="36937">schema.graphql</st> 文件的快照:

<st c="36991">schema {</st><st c="37000">query: Query</st><st c="37013">mutation: Mutation</st>
<st c="37032">}</st>
<st c="37034"># These are the GraphQL model classes</st>
<st c="37071">type Complainant {</st>
 <st c="37090">id: ID!</st>
 <st c="37098">firstname: String!</st>
 <st c="37117">lastname: String!</st>
 <st c="37135">middlename: String!</st>
 <st c="37155">email: String!</st>
 <st c="37170">date_registered: String!</st>
<st c="37195">}</st>
<st c="37197">type Complaint {</st>
 <st c="37213">id: ID!</st>
 <st c="37221">ticketId: String!</st>
 <st c="37239">catid: Int!</st>
 <st c="37251">complainantId: Int!</st>
 <st c="37271">ctype: Int!</st>
<st c="37320">schema.graphql</st> file is the <st c="37492">Mutation</st> and <st c="37505">Query</st>. Then, what follows are the definitions of GraphQL *<st c="37562">object types</st>*, the building blocks of GraphQL that represent the records that the REST services will fetch from or persist in the data repository. In the given definition file, the GraphQL transactions will focus on utilizing <st c="37787">Complainant</st>, <st c="37800">Complaint</st>, and their related model classes to manage the feedback sub-module of the *<st c="37884">Online Library</st>* *<st c="37899">Management System</st>*.
			<st c="37917">Each model class consists of</st> `<st c="37947">Int</st>`<st c="37950">,</st> `<st c="37952">Float</st>`<st c="37957">,</st> `<st c="37959">Boolean</st>`<st c="37966">,</st> `<st c="37968">ID</st>`<st c="37970">, or any custom scalar object type.</st> <st c="38006">GraphQL also allows model classes to have</st> `<st c="38048">enum</st>` <st c="38052">and list (</st>`<st c="38063">[]</st>`<st c="38066">) field types.</st> <st c="38082">The scalar or multi-valued fields can be nullable or non-nullable (</st>`<st c="38149">!</st>`<st c="38150">).</st> <st c="38153">So far, the given model classes all consist of non-nullable scalar fields.</st> <st c="38228">By the way, the octothorpe or hashtag (</st>`<st c="38267">#</st>`<st c="38269">) sign is the comment symbol of</st> <st c="38301">the SDL.</st>
			<st c="38309">After</st> <st c="38315">building the model classes, the next step is to define the</st> `<st c="38375">Query</st>` <st c="38380">and</st> `<st c="38385">Mutation</st>` <st c="38393">operations with their parameters and</st> <st c="38431">return types.</st>

The GraphQL operations

type Query {

listAllComplainants: <st c="38504">ComplainantListResult</st>!

listAllComplaints: <st c="38546">ComplaintListResult</st>!

listAllCategories: <st c="38586">CategoryListResult</st>!

listAllComplaintTypes: <st c="38629">ComplaintTypeListResult</st>!

}

type Mutation {

createCategory(<st c="38687">name: String</st>!): CategoryInsertResult! createComplaintType(<st c="38746">name: String</st>!): ComplaintTypeInsertResult! createComplainant(<st c="38808">input: ComplainantInput</st>!): ComplainantInsertResult! createComplaint(<st c="38877">input: ComplaintInput</st>!): ComplaintInsertResult! }

			<st c="38927">Our Flask sub-application focuses on the persistence and retrieval of feedback about the Online Library’s processes.</st> <st c="39044">Its</st> `<st c="39048">Query</st>` <st c="39053">operations involve retrieving the complaints (</st>`<st c="39100">listAllComplaints</st>`<st c="39118">), complainants (</st>`<st c="39136">listAllComplainants</st>`<st c="39156">), and the category (</st>`<st c="39178">listAllCategories</st>`<st c="39196">) and complaint type (</st>`<st c="39219">listAllComplaintTypes</st>`<st c="39241">) lookups.</st> <st c="39253">On the other hand, the</st> `<st c="39276">Mutation</st>` <st c="39284">operations involve adding complaints (</st>`<st c="39323">createComplaint</st>`<st c="39339">), complainants (</st>`<st c="39357">createComplainant</st>`<st c="39375">), complaint categories (</st>`<st c="39401">createCategory</st>`<st c="39416">), and complaint types (</st>`<st c="39441">createComplaintType</st>`<st c="39461">) to the database.</st> `<st c="39481">createCategory</st>` <st c="39495">and</st> `<st c="39500">createComplaintType</st>` <st c="39519">have their respective</st> `<st c="39542">String</st>` <st c="39548">parameter name, but the other mutators use input types to organize and manage their lengthy parameter list.</st> <st c="39657">Here are the</st> <st c="39669">implementations of the</st> `<st c="39693">ComplaintInput</st>` <st c="39707">and</st> `<st c="39712">ComplainantInput</st>` <st c="39728">types:</st>

These are the input types input ComplainantInput {

firstname: String! lastname: String! middlename: String! email: String! date_registered: String! } <st c="39888">input</st> ComplaintInput {

ticketId: String! complainantId: Int! catid: Int! ctype: Int! }


			<st c="39974">Aside from input types,</st> `<st c="39998">Query</st>` <st c="40003">and</st> `<st c="40008">Mutation</st>` <st c="40016">operators need result types to manage the response of GraphQL’s REST service executions.</st> <st c="40106">Here are some of the result types used by our</st> `<st c="40152">Query</st>` <st c="40157">and</st> `<st c="40162">Mutation</st>` <st c="40170">operations:</st>

These are the result types

type ComplainantInsertResult {

success: Boolean! errors: [String]

model: Complainant! }

type ComplaintInsertResult {

success: Boolean! errors: [String]

model: Complaint! }

… … … … … …

type ComplainantListResult {

success: Boolean! errors: [String]

complainants: [Complainant]! }

type ComplaintListResult {

success: Boolean! errors: [String]

complaints: [Complaint]! }

			<st c="40579">Now, all these</st> <st c="40593">object types, input types, and result types build GraphQL resolvers that implement these</st> `<st c="40683">Query</st>` <st c="40688">and</st> `<st c="40693">Mutation</st>` <st c="40701">operations.</st> <st c="40714">A</st> *<st c="40716">GraphQL resolver</st>* <st c="40732">connects the application’s repository and data layer to the GraphQL architecture.</st> <st c="40815">Although GraphQL can provide auto-generated resolver implementations, it is still practical to implement a custom resolver for each operation to capture the needed requirements, especially if the operations involve complex constraints and scenarios.</st> <st c="41065">The following snippet from</st> `<st c="41092">modules_sub_flask/resolvers/complainant_repo.py</st>` <st c="41139">implements the resolvers of our</st> <st c="41172">defined</st> `<st c="41180">Query</st>` <st c="41185">and</st> `<st c="41190">Mutation</st>` <st c="41198">operations:</st>

from ariadne import QueryType, MutationType

from typing import List, Any, Dict

从 modules_sub_flask.models.db 导入 Complainant

从 sqlalchemy.orm 导入 Session query = QueryType()

mutation = MutationType() class ComplainantResolver:

def __init__(self, sess:Session):

    self.sess = sess <st c="41501">@mutation.field('complainant')</st>

def insert_complainant(self, obj, info, input) -> bool:

try:

complainant = Complainant(**input)

self.sess.add(complainant)

self.sess.commit()

payload = {

"success": True,

"model": complainant

}

except Exception as e:

print(e)

payload = {

"success": False,

"errors": [f"Complainant … not found"]

}

return payload … … … … … …


			<st c="41854">The</st> `<st c="41859">insert_complainant()</st>` <st c="41879">transaction</st> <st c="41891">accepts the input from the GraphQL dashboard and saves the data to the database, while the following</st> `<st c="41993">select_all_complainant()</st>` <st c="42017">retrieves all the records from the database and renders them as a list of complainant records to the</st> <st c="42119">GraphQL dashboard:</st>

def select_all_complainant(self, obj, info) -> List[Any]:

    complainants = self.sess.query(Complainant).all()

    try:

        records = [todo.to_json() for todo in complainants]

        print(records) <st c="42318">payload = {</st><st c="42329">"success": True,</st><st c="42346">"complainants": records</st><st c="42370">}</st> except Exception as e:

        print(e) <st c="42405">payload = {</st><st c="42416">"success": False,</st><st c="42434">"errors": [str("Empty records")]</st><st c="42467">}</st> … … … … … …

			<st c="42480">The</st> `<st c="42485">ariadne</st>` <st c="42492">module has</st> `<st c="42504">QueryType</st>` <st c="42513">and</st> `<st c="42518">MutationType</st>` <st c="42530">that map GraphQL components such as</st> *<st c="42567">input types</st>*<st c="42578">. The</st> `<st c="42584">MutationType</st>` <st c="42596">object, for instance, maps the</st> `<st c="42628">ComplainantInput</st>` <st c="42644">type to the</st> `<st c="42657">input</st>` <st c="42662">parameter of the</st> `<st c="42680">insert_complainant()</st>` <st c="42700">method.</st>
			<st c="42708">Our GraphQL provider looks</st> <st c="42735">like a repository class, but it can also be a service type as long as it meets the requirements of the</st> `<st c="42839">Query</st>` <st c="42844">and</st> `<st c="42849">Mutation</st>` <st c="42857">functions defined in the</st> `<st c="42883">schema.graphql</st>` <st c="42897">definition file.</st>
			<st c="42914">Now, the mapping of each resolver function to its respective HTTP request function in</st> `<st c="43001">schema.graphql</st>` <st c="43015">always happens in</st> `<st c="43034">main.py</st>`<st c="43041">. The following snippet in</st> `<st c="43068">main_sub_flask.py</st>` <st c="43085">performs mapping of these two</st> <st c="43116">GraphQL components:</st>

… … … … … …

从 ariadne 导入 load_schema_from_path, make_executable_schema, \

graphql_sync, snake_case_fallback_resolvers, ObjectType

从 ariadne.explorer 导入 ExplorerGraphiQL 从 modules_sub_flask.resolvers.complainant_repo 导入 ComplainantResolver

从 modules_sub_flask.resolvers.complaint_repo 导入 ComplaintResolver 从 modules_sub_flask.models.config 导入 db_session

… … … … … … complainant_repo = ComplainantResolver(db_session)

category_repo = CategoryResolver(db_session) … … … … … … query = ObjectType("Query") query.set_field("listAllComplainants", complainant_repo.select_all_complainant)

query.set_field("listAllComplaints", complaint_repo.select_all_complaint)

… … … … … … mutation = ObjectType("Mutation") mutation.set_field("createComplainant", complainant_repo.insert_complainant)

mutation.set_field("createComplaint", complaint_repo.insert_complaint)

… … … … … … type_defs = load_schema_from_path("./schema.graphql") schema = make_executable_schema(

type_defs, <st c="44128">query</st>, <st c="44135">mutation</st>,

)

… … … … … …


			`<st c="44158">main_sub_flask.py</st>` <st c="44176">loads all the</st> <st c="44191">components from</st> `<st c="44207">schema.graphql</st>` <st c="44221">and maps all its operations to the repository and model layers of the mounted application.</st> <st c="44313">It is recommended to place the schema definition file in the main project directory for easy access to the file.</st> *<st c="44426">Figure 12</st>**<st c="44435">.9</st>* <st c="44437">shows the sequence of operations needed to run the</st> `<st c="44489">createComplainant</st>` <st c="44506">mutator.</st>
			![Figure 12.9 – Syntax for running a GraphQL mutator](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_12_009.jpg)

			<st c="44782">Figure 12.9 – Syntax for running a GraphQL mutator</st>
			<st c="44832">And</st> *<st c="44837">Figure 12</st>**<st c="44846">.10</st>* <st c="44849">shows how</st> <st c="44860">to run the</st> `<st c="44871">listAllComplainants</st>` <st c="44890">query operation.</st>
			![Figure 12.10 – Syntax for running a GraphQL query operator](https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/ms-flask-web-api-dev/img/B19383_12_010.jpg)

			<st c="45310">Figure 12.10 – Syntax for running a GraphQL query operator</st>
			<st c="45368">There are other libraries Flask can integrate to implement the GraphQL architecture, but they need to be up to date to support</st> <st c="45496">Flask 3.x.</st>
			<st c="45506">Summary</st>
			<st c="45514">Flexibility, adaptability, extensibility, and maintainability are the best adjectives that fully describe Flask as a</st> <st c="45632">Python framework.</st>
			<st c="45649">Previous chapters have proven Flask to be a simple, minimalist, and Pythonic framework that can build API and web applications with fewer configurations and setups.</st> <st c="45815">Its vast support helps us build applications that manage workflows and perform scientific calculations and visualization using plots, graphs, and charts.</st> <st c="45969">Although a WSGI application at the core, it can implement asynchronous API and view functions with</st> `<st c="46068">async</st>` <st c="46073">services and</st> <st c="46087">repository transactions.</st>
			<st c="46111">Flask has</st> *<st c="46122">Flask-SQLAlchemy</st>*<st c="46138">,</st> *<st c="46140">Flask-WTF</st>*<st c="46149">,</st> *<st c="46151">Flask-Session</st>*<st c="46164">,</st> *<st c="46166">Flask-CORS</st>*<st c="46176">, and</st> *<st c="46182">Flask-Login</st>* <st c="46193">that can lessen the cost and time of development.</st> <st c="46244">Other than that, stable and up-to-date extensions are available to help a Flask application secure its internals, run on an HTTPS platform, and protect its form handling from</st> **<st c="46419">Cross-Site Request Forgery</st>** <st c="46445">(</st>**<st c="46447">CSRF</st>**<st c="46451">) problems.</st> <st c="46464">On the other hand, Flask can use SQLAlchemy, Pony, or Peewee to manage data persistency and protect applications from SQL injection.</st> <st c="46597">Also, the framework can  can manage NoSQL data using MongoDB, Neo4j, Redis, and</st> <st c="46676">CouchBase databases.</st>
			<st c="46696">Flask can also build WebSocket and SSE using standard and</st> `<st c="46755">asyncio</st>` <st c="46762">platforms.</st>
			<st c="46773">This last chapter has added, to Flask’s long list of capabilities and strengths, the ability to connect to various Python frameworks and to provide interfaces and services to applications outside the</st> <st c="46974">Python environment.</st>
			<st c="46993">Aside from managing project modules using Blueprints, Flask can use Werkzeug’s</st> **<st c="47072">DispatcherMiddleware</st>** <st c="47093">to dispatch requests to other mounted WSGI applications such as Django and Flask sub-applications and compatible ASGI applications, such as FastAPI.</st> <st c="47243">This mechanism shows Flask’s interoperability feature, which can lead to building microservices.</st> <st c="47340">On the other hand, Flask can help provide services to Flutter apps, React web UIs, and GraphQL Explorer to run platform-agnostic</st> <st c="47469">query transactions.</st>
			<st c="47488">Hopefully, this book showcased Flask’s strengths as a web and API framework cover to cover and also helped discover some of its downsides along the way.</st> <st c="47642">Flask 3.x is a lightweight Python framework that can offer many things in building enterprise-grade small-, middle-, and hopefully</st> <st c="47773">large-scale applications.</st>
			<st c="47798">This book has led us on a long journey of learning, understanding, and hands-on experience about Flask 3’s core and new asynchronous features.</st> <st c="47942">I hope this reference book has provided the ideas and solutions that may help create the necessary features, deliverables, or systems for your business requirements, software designs, or daily goals and targets.</st> <st c="48154">Thank you very much for choosing this book as your companion for knowledge.</st> <st c="48230">And do not forget to share your Flask experiences with others because mastering something starts with sharing what</st> <st c="48345">you learned.</st>

posted @ 2025-09-23 21:56  绝不原创的飞龙  阅读(55)  评论(0)    收藏  举报