精通-Flask-Web-API-开发-全-
精通 Flask Web API 开发(全)
原文:
zh.annas-archive.org/md5/82ba6adc70e4941205c46ebff3a4ad3d译者:飞龙
第一章:前言
本书面向对象
本书涵盖内容
<st c="2549">@before_request</st> <st c="2569">@after_request</st>
<st c="3510">asyncio</st>
<st c="3905">numpy</st><st c="3918">,《st c="3920">matplotlib</st><st c="3939">,《st c="3941">scipy</st><st c="3952">sympy</st>
为了充分利用本书
下载示例代码文件
使用的约定
<st c="8530">文本中的代码</st><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]
联系我们
分享您的想法
免费下载此书的 PDF 副本
扫描二维码或访问以下链接: 以下链接:

-
提交您的购买 证明。 -
这就完了! 我们将直接将您的免费 PDF 和其他福利发送到您的 电子邮件。
第一部分:学习 Flask 3.x 框架
-
第一章 , 深入探索 Flask 框架 -
第二章 , 添加高级核心功能 -
第三章 , 创建 REST Web 服务 -
第四章 , 利用 Flask 扩展
第二章:1
深入探讨 Flask 框架
-
设置 项目基线 -
创建路由 和导航 -
管理请求和 响应数据 -
实现 视图模板 -
创建 Web 表单 -
使用 PostgreSQL 构建数据层 。 -
管理项目结构
技术要求
github.com/PacktPublishing/Mastering-Flask-Web-Development/tree/main/ch01
设置项目基线
安装最新版本的 Python
安装 Visual Studio (VS) Code 编辑器
<st c="2364">django-admin</st>

创建虚拟环境
-
它可以避免损坏的模块版本,以及与其他现有类似的全局 仓库库的冲突。 -
它可以帮助为 项目构建依赖树。 -
它可以帮助简化将应用程序与库部署到物理和 基于云的服务器。
<st c="4700">virtualenv</st>
pip install virtualenv
<st c="4893">python virtualenv -m ch01-01</st> <st c="5029">ch01-env</st>

<st c="5522">Python: Select Interpreter</st><st c="5730">Python.exe</st> <st c="5753">/Scripts</st> <st c="5853">/</st>``<st c="5854">Scripts</st>

<st c="6928">/Scripts/activate.bat</st> <st c="6964">/bin/activate</st> <st c="7132">(</st>``<st c="7133">ch01-env) C:\</st>
安装 Flask 3.x 库
<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>
<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>
<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>
<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>
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>
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(
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:
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>
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
从
从 repository.user 导入 validate_user
@app.route('/login/params')
def login_with_params():
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>

<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>

<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>
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
添加高级核心功能
-
构建庞大且 可扩展的项目 -
应用对象关系映射(ORM) -
配置 日志机制 -
创建 用户会话 -
应用 闪存消息 -
利用一些高级 Jinja2 功能 -
实现 错误处理解决方案 -
添加 静态资源
技术要求
-
利用应用工厂设计模式的 <st c="1684">ch02-factory</st>项目。 -
使用 <st c="1763">ch02-blueprint</st>项目的 Flask 蓝图。 -
使用应用工厂和 <st c="1821">ch02-blueprint-factory</st>项目同时使用蓝图结构。
github.com/PacktPublishing/Mastering-Flask-Web-Development/tree/main/ch02
构建大型且可扩展的项目
使用应用程序工厂
<st c="3449">main.py</st>
app之前使用参数(如template_folder <st c="3912">static_folder</st>
<st c="4531">__init__.py</st> <st c="4644">__init__.py</st> <st c="4668">app</st> <st c="4777">ch02-factory</st>

__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">,但任何人都可以用适合他们项目的适当名称替换它。 <st c="7170">app</st> <st c="7173">实例,调用了<st c="7190">db_init()</st> <st c="7199">的<st c="7206">SQLAlchemy 的实例,以使用对象定义和配置 ORM,并设置了日志</st> <st c="7308">机制。</st> <st c="7320">由于它在中,所以文件必须导入工厂方法,最终通过调用的
为了使应用程序工厂方法灵活和可配置,向其中添加本地参数。 <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>
-
显式地使用 <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><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>
将数据存储到应用程序上下文中
<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')
<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> <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>

<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
<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
导入 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>
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)
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>

<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()
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://
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>
username =
password =
user_type =
admins =
customer =
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>
id = Column(Integer, ForeignKey('login.id'), primary_key = True)
firstname = Column(String(45))
lastname = Column(String(45))
middlename = Column(String(45))
… … … … … …
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
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,
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)
app = Flask(name, template_folder='../app/pages', static_folder='../app/resources')
app.config.from_file("
<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>

<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>
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>

<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>
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>
<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:

<st c="59837">Figure 2.6 – The location of /resources in the main application</st>

<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 网络服务
-
设置 RESTful 应用程序 -
实现 API 端点 -
管理请求 和响应 -
利用响应编码器 和解码器 -
过滤 API 请求 和响应 -
处理异常 -
消费 API 端点
技术要求
<st c="1508">3</st> <st c="1724">ch03-client</st>
设置 RESTful 应用程序
<st c="2273">flask</st> <st c="2306">pip</st>
<st c="2527">蓝图</st><st c="2772">app</st> <st c="2827">create_app()</st>

<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>
实现 API 端点
<st c="4160">request</st> <st c="4201">Response</st> <st c="4237">@route()</st>
<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>

<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>
response = make_response(
return response
response = make_response(
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>
response =
return response
response =
return response
response =
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>
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>
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>
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>
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>
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():
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():
… … … … … …
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():
… … … … … …
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>
def __init__(self, *args, **kwargs):
self.options = kwargs
super().__init__(*args, **kwargs) <st c="18857">def default(self, o):</st>
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>

<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
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.
'orders_client_bp': [before_check_api_server],
'pizza_client_bp': [before_log_pizza_bp]
}
app.
'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):
def bad_request(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 添加邮件功能
技术要求
<st c="2114">ch04-web</st> <st c="2178">ch04-api</st> <st c="2295">Blueprints</st>
应用数据库迁移
使用 Flask-Migrate 应用数据库迁移
<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> <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(
tablename = 'category'
id = Column(Integer,
name = Column(String(45), nullable = False)
class ComplaintType(Base):
tablename = 'complaint_type'
id = Column(Integer,
name = Column(String(45), nullable = False)
class ComplaintDetails(
tablename = 'complaint_details'
id = Column(Integer,
compid = Column(Integer,
statement = Column(String(100), nullable = False)
status = Column(String(50))
resolution = Column(String(100))
date_resolved = Column(Date)
<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>
import toml
app.config.from_file('config-dev.toml', toml.load)
<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>

<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>

<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>

<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>

<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>` 后的日志消息:

<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>

<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>
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>
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(
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(
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_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>

<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_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>
app.config.from_file('config-dev.toml', toml.load)

<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 构建工作流程 -
第九章 , 保护 Flask 应用程序
第六章:5
构建异步事务
<st c="589">asyncio</st> <st c="659">SQLAlchemy 2.x</st>
<st c="1035">Flask 框架</st>
-
创建异步 Flask 组件 -
构建异步 SQLAlchemy 存储层 -
使用 asyncio实现异步事务 -
利用异步 信号通知 -
使用 Celery 和 Redis 构建后台任务 -
使用异步事务构建 WebSocket -
实现异步 服务器端发送 事件 ( SSE ) -
使用 RxPy 应用响应式编程 -
选择 Quart 而非 Flask 2.x
技术要求
本章将突出介绍一个具有一些异步任务和后台进程的《在线投票系统》原型,这些进程用于管理来自不同地区的候选人申请和与选举相关的提交的高带宽,以及满足从各个政党同时检索投票计数的需要。该系统由三个独立的项目组成,分别是 <st c="1900">ch05-api</st><st c="1939">ch05-web</st><st c="2018">ch05-quart</st><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> **《在线》** **《投票》**
@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> <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> 来管理它们各自的请求和响应对象。 创建协程,Flask 2.x 可以使用 的
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_request 和 after_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">中创建的 ,使用内置的
另一方面,<st c="5321">flask[async]</st> <st c="5333">模块可以允许创建异步的错误处理器。</st>
创建异步错误处理器
Flask 2.x 可以使用 <st c="5472">@errorhandler</st> <st c="5485">装饰器</st> 装饰协程,以管理引发的异常和 HTTP 状态码。
<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)
构建异步 SQLAlchemy 存储层
<st c="6472">asyncio</st> <st c="6497">greenlet</st> <st c="6555">await</st> <st c="6606">ch05-web</st> <st c="6619">ch05-api</st> <st c="6791">/models/config.py</st> <st c="6831">asyncpg</st>
设置数据库连接
<st c="6983">asyncpg</st> <st c="7021">asyncio</st><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> <st c="7591">create_async_engine()</st> <st c="7674">create_engine()</st> <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> <st c="7995">NullPool</st> <st c="8087">pool_pre_ping</st><st c="8386">future</st> <st c="8418">True</st>
<st c="8566">sessionmaker</st> <st c="8602">AsyncEngine</st> <st c="8733">AsyncSession</st> <st c="8760">async_scoped_session</st> <st c="8836">scopefunc</st> <st c="8982">AsyncSession</st>
<st c="9119">declarative_base()</st>
<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
<st c="9960">echo</st> <st c="9988">create_async_engine</st> <st c="10032">AsyncEngine</st><st c="10076">init_db()</st> <st c="10232">Base</st> <st c="10361">flask[async]</st> <st c="10378">flask-sqlalchemy</st>
<st c="10435">async_scoped_session()</st>
Building the asynchronous repository layer
<st c="10596">AsyncSession</st> <st c="10678">VoterRepository</st> <st c="10764">Voter</st>
<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>
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>
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>
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>
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>
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>
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>
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
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
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>
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>
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>
<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>
<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>
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>
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>

<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>
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>

<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>
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>

<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
<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>
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>
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>* 展示了为投票者提供投诉的表单视图:

<st c="56748">图 5.4 – 发布到 Redis 的数据投诉表单视图</st>
*<st c="56813">图 5</st>**<st c="56822">.5</st>* 提供了包含从 Redis 代理推送的消息的 SSE 客户端页面的快照。

<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
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 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.<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>
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>
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
开发计算和科学应用
-
上传 逗号分隔值 ( CSV ) 和 Microsoft Excel 工作表 ( XLSX ) 文档 用于计算 -
实现符号计算 与可视化 -
使用 <st c="1691">pandas</st>模块进行数据和 图形分析 -
创建和渲染 LaTeX 文档 -
使用 前端库 构建图形图表 -
使用 WebSocket 和 服务器端事件 (SSE) ( SSE **) -
使用异步后台任务进行 资源密集型计算 -
将 Julia 包与 Flask 集成
技术要求
<st c="2871">蓝图</st>
上传 CSV 和 XLSX 文档进行计算
<st c="3726"><form></st> <st c="3738">enctype</st> <st c="3749">multipart/form-data</st><st c="3726"><form></st> <st c="3806">request.files</st> <st c="3834">FileStorage</st> <st c="3857">FileStorage</st> <st c="3963">以下是一个 HTML 脚本,它使用</st>
<!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>
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>时引发。
此外,部分关注点是,在利用任何文件事务之前,对多部分对象的文件名进行清理。 可能会使应用程序暴露于多个漏洞,因为 <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>,并引发必要的异常类以记录运行时可能出现的所有底层问题。
使用<st c="8305">pandas 模块进行数据和分析</st>
<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
<st c="8935">openpyxl</st> <st c="8965">pandas</st>
pip install openpyxl
<st c="9116">DataFrame</st>
利用 DataFrame
<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])

<st c="11441">DataFrame</st> <st c="11515">show_analysis()</st>

df_tbl = df_xlsx.loc[: , 'Australia':'US'].describe().to_html()

-
<st c="15228">to_html()</st>:这生成一个带有数据集的 HTML 表格格式。 -
<st c="15295">to_latex()</st>:这创建了一个 LaTeX 格式的结果,数据已准备好进行 PDF 转换。 -
<st c="15390">to_markdown()</st>:这生成一个带有数据值的 Markdown 模板。
<st c="15510">to_html()</st> <st c="15581">to_html()</st><st c="15639">safe</st> <st c="15739">to_html()</st> <st c="15839">DataFrame</st> <st c="15890">to_html()</st>
使用 matplotlib 绘制图表和图表
<st c="15997">DataFrame</st>
<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>: 这将应用指定的颜色到画布边框和坐标轴之间的边界区域 绘图边框。
<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:

<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,
<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>

<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>

<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>`

`<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> 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.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.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>

<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
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>

<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>

<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
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>

<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>

<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>
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>

<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>

<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>
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'])
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>

<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>

<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>

<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>

<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>
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>

<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>
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>
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>
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):
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 而导致的系统崩溃
现在,让我们看看创建和将 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>

<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
使用非关系型数据存储
-
使用 Apache HBase 管理非关系型数据 -
利用 Apache Cassandra 的列存储 -
在 Redis 中存储搜索数据 -
使用 MongoDB 处理 BSON 文档 -
使用 Couchbase 管理基于键的 JSON 文档 -
与 Neo4J 建立数据关系
技术要求
使用 Apache HBase 管理非关系型数据
<st c="3208">byte[]</st> <st c="3226">byte[]</st>
设计 HBase 表
<st c="4168">支付</st> <st c="4181">预订</st> <st c="4285">支付</st> <st c="4298">预订</st>

<st c="4434">payments</st> <st c="4447">bookings</st> <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>
"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"
}
<st c="5481">details:stud_id</st>
设置基本要求
<st c="5933">JAVA_HOME</st> <st c="6027">CLASSPATH</st> <st c="6085">/bin</st>
<st c="6255">hadoop-3.3.6/hadoop-3.3.6.tar.gz</st> <st c="6424">HADOOP_HOME</st> <st c="6456">CLASSPATH</st> <st c="6518">/bin</st>
配置 Apache Hadoop
-
前往 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> -
在同一个安装文件夹内,创建一个名为 <st c="9101">data</st>的自定义文件夹,其中包含两个子文件夹,分别命名为 <st c="9134">datanode</st>和 <st c="9147">namenode</st>,如图 7.2 所示 .2 **。这些文件夹最终将包含 DataNode 和 NameNode 的配置文件,分别:

-
接下来,打开 <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> -
由于我们的项目将使用安装在 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>文件夹。 -
通过在命令行运行以下命令来格式化新的活动 NameNode (s): hdfs namenode -format此命令将清理 NameNode (s) 如果它们有现有的 存储元数据。
配置 Zookeeper 和 Apache HBase
-
首先,创建一个
系统环境变量 <st c="11480">HBASE_HOME</st>来注册 HBase 安装文件夹。 -
在安装文件夹内创建两个文件夹, <st c="11616">hbase</st>和 <st c="11626">zookeeper</st>。这些将分别作为 HBase 和内置 Zookeeper 服务器的根文件夹。 -
在安装文件夹中,打开 <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> -
接下来,打开 <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% -
打开 <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 ,以实现更好的服务器性能。 -
最后,转到 <st c="14322">/bin</st>文件夹 并运行 <st c="14346">start-hbase</st>命令 以启动服务器。 图 7 **.3 显示了启动时的 HBase 日志快照:

要停止服务器,运行 <st c="15355">stop-hbase</st>,然后 <st c="15372">hbase master stop --</st>``<st c="15392">shutDownCluster</st>。

<st c="17049">支付</st> <st c="17062">预订</st>
设置 HBase 壳
<st c="17255">hbase shell</st>
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'.
<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

<st c="19789">/common/lib</st> <st c="19813">/lib/client-facing-thirdparty</st>
创建 HBase 表
<st c="20345">whoami</st><st c="20424">version</st><st c="20501">status</st>

<st c="21690">payments</st> <st c="21703">bookings</st> <st c="21760">create</st> <st c="21808">create</st>

<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>,它们的值都在引号中。
<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 连接
HappyBase库。
<st c="23976">happybase</st>
<st c="24190">happybase</st> <st c="24229">pip</st>
pip install happybase
<st c="24406">__init__.py</st> <st c="24430">ConnectionPool</st> <st c="24454">happybase</st> <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
<st c="25062">Connection</st> <st c="25084">Connection</st> <st c="25178">ConnectionPool</st> <st c="25232">Connection</st> <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> <st c="25800">with</st> <st c="25838">Connection</st> <st c="25949">happybase</st> <st c="26038">ConnectionPool</st> <st c="26105">payments</st>
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">类需要一个 <st c="26739">pool</st>)作为其构造函数参数以进行实例化。</st> <st c="26800">pool</st> <st c="26804">对象有一个 <st c="26889">happybase</st> <st c="26898">实用方法,用于 CRUD 事务。</st> <st c="26938">借助线程的帮助,连接对象有一个 <st c="27053">Table</st> <st c="27058">对象,该对象提供了一些方法来执行数据库事务,例如
<st c="27144">The</st> <st c="27149">put()</st> <st c="27154">方法执行了 * *和 * *事务。它需要作为其主要参数,以便以字典格式插入记录。</st> *<st c="27333">列限定符-值对</st> *组成,其中所有值都应该是字节字符串或任何转换为<st c="27435">bytes</st>的类型的值。<st c="27472">此外, <st c="27527">upsert_details()</st> <st c="27543">将支付记录插入到 HBase 数据库的
除了<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">函数<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">除了之外, <st c="28177">columns</st> <st c="28184">参数中的列族或列族的名称,这意味着删除整个记录。</st> <st c="28235">但有时,删除只需要删除列限定符(s)或列(s)而不是整个行,这样只有列限定符名称(s)出现在
<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>
<st c="29661">select_records_ids()</st>
{ <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()<st c="31276">WHERE</st>
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()filterfilter<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>
在下一节中,我们将应用PaymentsRepositorypayments
将仓库应用于 API 函数
<st c="33587">upsert_details() <st c="33609">PaymentRepository</st>
<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 服务器
<st c="35027">hbase thrift start</st>

<st c="36011">happybase</st> <st c="36110">HBase</st> happybase
利用 Apache Cassandra 的列存储
<st c="37075">课程</st><st c="37083">学位水平</st><st c="37097">学生</st><st c="37110">学生表现</st>
设计 Cassandra 表

安装和配置 Apache Cassandra
<st c="39464">sudo</st>
sudo ufw enable
<st c="39660">sudo</st>
sudo ufw allow 7000
sudo ufw allow 9042
sudo ufw allow 7199
<st c="39820">sudo</st>
sudo apt install openjdk-11-jdk
<st c="39932">/conf</st> <st c="39996">jvm11-server.options</st>
<st c="40160">/</st>``<st c="40161">conf</st>
cassandra -f
<st c="40233">nodetool</st> <st c="40242">drain</st>
运行 CQL shell 客户端
<st c="40625">cqlsh</st> <st c="40646">/conf</st> <st c="40719">cqlsh</st>

<st c="41130">create</st> <st c="41143">|</st><st c="41159">,</st> <st c="41167">|</st><st c="41175">|</st><st c="41188">,</st> <st c="41193">, 和</st> <st c="41207">语句。</st> <st c="41220">对于 DML,它有</st><st c="41242">,</st> <st c="41250">,</st> <st c="41258">, 和</st> <st c="41269">命令。</st> <st c="41280">对于查询事务,它使用 SQL 中的</st><st c="41326">子句。</st> <st c="41347">然而,</st>
<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>

<st c="42584">describe</st>

<st c="43998">create keyspace</st> <st c="44087">packtspace</st>
CREATE KEYSPACE packtspace WITH replication = {'class': 'NetworkTopologyStrategy', 'datacenter1': '1'} AND durable_writes = false;
<st c="44320">NetworkTopologyStrategy</st><st c="44354">packtspace</st>
<st c="44491">packspace</st><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> <st c="44644">cassandra-driver</st>
建立数据库连接
<st c="45095">pip</st>
pip install cassandra-driver
<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
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>
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):
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>

<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>

<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>

<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>
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>
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>
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>
<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:
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>
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>

<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>

<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>

<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>

<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>
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>

<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 构建工作流
工作流是一系列或一组重复的任务、活动或小流程,需要从头到尾的完整执行以满足特定的业务流程。
几种工具和平台可以提供最佳实践、规则和技术规范,以构建针对行业、企业和科学问题的流程。
本章将涵盖以下主题,讨论使用 Flask 框架实现工作流活动时的不同机制和程序:
-
使用 Celery 任务构建工作流
-
使用 SpiffWorkflow 创建 BPMN 和非 BPMN 工作流
-
使用 Zeebe/Camunda 平台构建服务任务
-
使用 Airflow 2.x 编排 API 端点
-
使用 Temporal.io 实现工作流
技术要求
本章旨在实现使用工作流实现其业务流程的
-
<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 服务。
使用 Celery 任务构建工作流
创建任务签名
<st c="3554">delay()</st> <st c="3619">apply_async()</st> <st c="3754">signature()</st> <st c="3769">s()</st>
<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(
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>

<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>
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)
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>

<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>
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>
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>

<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>

<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

<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>
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>

<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 中利用变量
<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>

<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>
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":
<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>:
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>
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>

<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>

<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>

<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>

<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>
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")
<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>
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>
持续时间 = 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>
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>
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 应用程序
-
添加对 网络漏洞 的保护 -
保护 响应数据 -
管理 用户凭据 -
实现网络 表单认证 -
防止 CSRF 攻击 -
实现用户身份验证 和授权 -
控制视图或 API 访问
技术要求
<st c="2705">Flask[async]</st> <st c="2751">Flask-SQLAlchemy</st>
添加对网络漏洞的保护
<st c="3119">POST</st><st c="3125">PUT</st><st c="3130">PATCH</st><st c="3141">DELETE</st>
将表单验证应用于请求数据
<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>
<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>
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>

<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>
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>*:

<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>

<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>
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>
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>
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>
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>
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>:
enc_key = "packt_pazzword"
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>
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>
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>
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>

<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>
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>
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>
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:
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>
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>
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(
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>
__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>
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>
… … … … … …
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>
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>
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>

<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 创建测试用例 -
第十一章 , 部署 Flask 应用程序 -
第十二章 , 将 Flask 集成到其他工具和框架中
第十一章:10
为 Flask 创建测试用例
在构建 Flask 组件之后,创建测试用例以确保它们的正确性并修复它们的错误是至关重要的。
为了实现这些测试用例,Python 有一个内置的模块叫做
-
为 Web 视图、存储库类和 本地服务 创建测试用例 -
为
应用工厂 中的组件和蓝图 创建测试用例 -
为
异步组件 创建测试用例 -
为受保护 API 和 Web 组件 创建测试用例 -
为 MongoDB 事务 创建测试用例 -
为 WebSocket 创建测试用例
技术要求
为 Web 视图、存储库类和本地服务创建测试用例
pytest<st c="2046">模块</st> <st c="2074">和集成或功能测试。</st> <st c="2113">它需要简单的语法来构建测试用例,这使得它非常容易使用,并且它有一个</st> <st c="2205">可以自动运行所有测试文件。</st> <st c="2258">此外,</st><st c="2274">是一个免费和开源模块,因此使用以下</st>
pip install pytest
<st c="2384">pytest</st> <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> <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> <st c="3725">Testing</st> <st c="3748">true</st>
<st c="3916">main.py</st><st c="3794">tests</st> <st c="4015">test_</st> <st c="4289">ch01-testing</st> <st c="4319">tests</st>

<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>

<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>

<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>
<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
("9999", "Maria", "Clara", 45, "Developer", "2015-08-15", "inactive")
))
def test_insert_admin(
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>

<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
@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")
))
<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>
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
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 = 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):
<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>
def test_add_order(client,
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):
<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
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):
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>
`<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):
@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>
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():
yield app
@pytest.fixture(scope="module")
def 用户凭证():
参数 = dict()
参数["用户名"] = "sjctrags"
参数["密码"] = "sjctrags"
return 参数
def 测试患者档案添加无效访问(client):
<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
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 test_add_vaccine_auth(client, vaccine,
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):
<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():
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
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>
<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,
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>
import json
# 在此处放置 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):
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 应用程序
-
在 Gunicorn 和 uWSGI 上运行应用程序 -
在 Uvicorn 上运行应用程序 中 -
将应用程序部署到 Apache HTTP 服务器 -
将应用程序部署到 Docker 中 -
将应用程序部署到 Kubernetes 中 -
使用 NGINX 创建一个 API 网关 中
技术要求
<st c="1191">蓝图</st>
准备部署
<st c="1959">Peewee</st> <st c="1993">pip</st> <st c="2029">psycopg2</st>
pip install psycopg2 peewee
<st c="2175">psycopg2</st>
标准 Peewee ORM 的类和方法
<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>
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):
<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):
<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>
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>

<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>

<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>`

`<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>

<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>后的服务器日志:

<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>

<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>

<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>

<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>

<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>

<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>

<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>
运行 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>
- ./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>
<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>

<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>

<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>

<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>

<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
在 **<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>:
选择器:
匹配标签:
应用: 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>
---
应用: 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>
名称: 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>
选择器:
匹配标签:
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>
---
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>

<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>

<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>

<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 {
代理设置头 <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 与其他工具和框架集成
-
实现涉及 FastAPI、Django 和 Tornado 的微服务应用 -
实现 Flask 仪表盘 -
使用 Swagger 应用 OpenAPI 3.x 规范 与 Swagger -
为 Flutter 移动应用程序 提供 REST 服务 -
使用 React 应用程序消费 REST 端点 React 应用程序 -
构建一个 GraphQL 应用程序
技术要求
-
Django 子模块 – 管理学生 图书借阅者 -
Flask 子模块 – 管理教师 图书浏览器 -
FastAPI 子模块 – 管理借阅者的反馈和投诉 借阅者 -
Flask 主应用程序 – 核心事务 核心事务 -
Tornado 应用程序 – 前端应用程序

实现涉及 FastAPI、Django 和 Tornado 的微服务应用程序
<st c="2755">DispatcherMiddleware</st>

<st c="3496">modules</st> <st c="3587">modules_fastapi</st> <st c="3655">modules_django</st> <st c="3710">modules_tornado</st><st c="3765">modules_sub_flask</st>
<st c="3875">main_fastapi.py</st><st c="3934">main_sub_flask.py</st><st c="4033">main.py</st>
添加 Flask 子应用程序
<st c="4168">DispatcherMiddleware</st> <st c="4207">app</st> <st c="4371">app</st> <st c="4510">flask_sub_app</st> <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> })
<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> <st c="5589">DispatcherMiddleware</st>
添加 FastAPI 子应用程序
<st c="5736">DispatcherMiddleware</st> <st c="5873">a2wsgi</st> <st c="5849">ASGIMiddleware</st> <st c="5811">app</st> <st c="5968">pip</st> <st c="5941">a2wsgi</st>
pip install a2wsgi
<st c="5999">ASGIMiddleware</st> <st c="6296">wait_time</st><st c="6445">loop</st> <st c="6579">main.py</st> <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<st c="7357">a2wsgi</st>
添加 Django 子应用程序
<st c="7740">settings.py</st>
-
由于 <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>. -
将 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> ] -
此外,在定义 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 Responsefrom 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"}) -
由于我们正在将 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. -
最后,从
<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 和
<st c="11940">WSGIContainer</st> <st c="11992">app</st>
<st c="12130">Flask[async]</st> <st c="12207">SynchronousOnlyOperation</st>
<st c="12329">main.py</st> <st c="12378">Flask app</st>
<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()
<st c="13375">asyncio</st> <st c="13448">IOLoop</st> <st c="13540">asyncio</st> <st c="13596">get_event_loop()</st><st c="13669">MainHandler</st><st c="13685">modules_tornado</st>
实现 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">、 和 <st c="15907">Span</st> <st c="15911">(s)</st> <st c="15917">组成的追踪 API。</st> <st c="16027">追踪器负责创建跨度。</st>
在实例化<st c="16152">TracerProvider</st>之后,创建追踪器的一部分设置是应用<st c="16238">Batch</st> <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>
<st c="16886">现在,在任何地方导入</st> <st c="16903">tracer</st> <st c="16909">对象。</st>
<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> <st c="17760">add_login()</st>

<st c="18672">jaeger-all-in-one.exe</st>
<st c="18799">pip</st>
pip install opentelemetry-exporter-jaeger
<st c="18947">create_app()</st>
<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> <st c="19523">agent_host_name</st> <st c="19542">localhost</st> <st c="19556">agent_port</st> <st c="19570">6831</st>
<st c="19661">http://localhost:16686/</st>

Jaeger 的左侧部分是packt-flask-service,当时搜索时提供了四个搜索结果。 在仪表板的右侧部分是搜索结果列表,列出了监控所执行事务的跨度产生的跟踪。 点击每一行都会以图形格式显示跟踪详情。 另一方面,页眉部分的图形总结了在指定时间段内的所有跟踪。
`
`使用 Swagger 应用 OpenAPI 3.x 规范
<st c="21046">除了</st> <st c="21180">flask_openapi3</st>
<st c="21330">flask_openapi3</st> <st c="21458">pydantic</st> <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><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>
<st c="22448">要记录 API,请添加端点的摘要、API 事务的完整描述、标签、请求字段描述和响应细节。</st> <st c="22603">以下代码片段显示了
<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)
Tags <st c="23265">flask_openapi3</st>
<st c="23607">flask_openapi3</st> <st c="23660">/openapi/swagger</st>

<st c="24455">flask-openapi3</st> <st c="24772">main.py</st>
为 Flutter 移动应用程序提供 REST 服务
<st c="25550">%FLUTTER_HOME%\bin</st>
-
Android SDK 平台,最新的 API 版本 -
Android SDK 命令行工具 -
Android SDK 构建工具 -
Android SDK 平台工具 -
Android 模拟器
-
在终端上运行 <st c="26108">flutter doctor</st>命令。 -
在开发之前,所有设置和工具都必须满足。 在这个安装阶段不应该有任何问题。 -
使用带有 Flutter 扩展的 VS Code 编辑器创建一个 Flutter 项目;它应该具有 图 12 **.6 中展示的项目结构:

<st c="26681">/lib/olms/provider</st><st c="26701">providers.dart</st> <st c="26766">GET</st> <st c="26774">POST</st> <st c="26785">APIs</st> <st c="26789">用户管理的事务。</st> <st c="26812">以下是我们的</st>
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> <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> <st c="28449">addLogin()</st> <st c="28464">getLogin()</st> <st c="28583">Login</st> <st c="28618">ch12-microservices-interop</st>
<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> <st c="28936">list_login()</st>
<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> <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> <st c="31561">/login/list/all</st> <st c="31532">登录</st> <st c="31631">SingleChildScrollView</st> <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>

使用 React 应用程序消费 REST 端点
<st c="32447">create-react-app</st> <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>
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));
}
<st c="34175">add_faculty_borrower()</st> <st c="34077">addRecord()</st> <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> <st c="35355">records</st>
构建 GraphQL 应用
<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> <st c="36496">/</st>``<st c="36497">flask/graphql</st>

<st c="36742">schema.graphql</st> <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
def __init__(self, sess:Session):
self.sess = sess <st c="41501">@mutation.field('complainant')</st>
<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
… … … … … …
query.set_field("listAllComplaints", complaint_repo.select_all_complaint)
… … … … … …
mutation.set_field("createComplaint", complaint_repo.insert_complaint)
… … … … … …
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>

<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>

<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>


浙公网安备 33010602011771号