Skip to content

27. 应用练习:第 9 版

我们回到应用练习的第 7 版,与客户端和 Web 服务器交换 jSON 字符串不同,这里它们将交换 XML。

架构保持不变:

Image

27.1. Web 服务器

Image

文件夹 [http-servers/04] 是通过复制文件夹 [http-servers/02] 获得的,但去除了子文件夹 [utilities]。然后修改以下内容:

Image

文件 [config] 按以下方式进行修改:


    # 绝对依赖
    absolute_dependencies = [
        # 项目文件夹
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        # AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        # ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        # AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        # 常量、区间
        f"{root_dir}/impots/v05/entities",
        # index_controller
        f"{root_dir}/impots/http-servers/01/controllers",
        # 脚本 [config_database, config_layers]
        script_dir,
        # 日志记录器,SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
    ]
  • 第 21 行:指定保留在 [http-servers/02] 中的实用程序文件夹;

主脚本 [main] 修改如下:


# 首页 URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    logger = None
    try:
        # 日志记录器
        logger = Logger(config["logsFilename"])
        # 将其存储在与线程关联的配置中
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        # 记录请求
        logger.write(f"[index] requête : {request}\n")
        
        # 由控制器执行该请求
        résultat, status_code = index_controller.execute(request, config)
        # 是否发生了致命错误?
        
        # 记录响应
        logger.write(f"[index] {résultat}\n")
        # 发送响应
        return xml_response(résultat, status_code)
    except BaseException as erreur:
        # 如果可能,记录错误
        if logger:
            logger.write(f"[index] {erreur}")
        # 准备回复给客户
        résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
        # 发送响应
        return xml_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # 若日志文件已打开,则关闭
        if logger:
            logger.close()
  • 唯一的修改是第23行:现在发送响应XML;

函数 [xml_response] 在模块 [myutils] 中定义:


import xmltodict

def xml_response(résultat: dict, status_code: int) -> tuple:
    # 结果:待转换为字符串的字典XML
    xmlString = xmltodict.unparse(résultat)
    # 返回响应 HTTP
    response = make_response(xmlString)
    response.headers['Content-Type'] = 'application/xml; charset=utf-8'
    return response, status_code
  • 第3行:函数[xml_response]接收的参数为:
    • 待转换为 XML 的字典 [résultat]
    • 要返回给 Web 客户端的状态代码 [status_code]
  • 第5行:使用库[xmltodict]生成字符串XML;
  • 第 8 行:使用标头 [Content-Type] 告知客户端正在向其发送 XML;

函数 [xml_response] 必须导入到脚本 [__init__.py] 中:


from .myutils import set_syspath, json_response, decode_flask_session, xml_response

随后,需将模块 [myutils] 集成到机器范围的模块中。此操作需在终端 PyCharm 中通过命令 [pip install .] 完成(位于 packages 文件夹内)。

27.2. Web客户端

27.2.1. 代码

Image

文件 [http-clients/04] 是通过复制客户端 [http-clients/02] 生成的。随后,我们按以下方式修改类 [ImpôtsDaoWithHttpClient]


# 导入

import requests
import xmltodict
from flask_api import status

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsMétier import InterfaceImpôtsMétier
from TaxPayer import TaxPayer


class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):

    # 构造函数
    def __init__(self, config: dict):
        

    # 未使用的方法
    def get_admindata(self) -> AdminData:
        pass

    # 税额计算
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        ….
        # 响应状态代码HTTP
        status_code = response.status_code
        # 将响应 XML 放入字典
        résultat = xmltodict.parse(response.text[39:])
        # 状态码不为 200 时报错 OK
        ….
  • 第 30 行:Web 服务器的响应 HTTP [response] 现已成为字符串 XML。日志显示了该字符串的性质:

2020-07-27 15:53:47.886283, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<réponse><result><marié>non</marié><enfants>2</enfants><salaire>100000</salaire><impôt>19884</impôt><surcôte>4480</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction></result></réponse>

字符串 [<?xml version="1.0" encoding="utf-8"?>] 共有 38 个字符。此外,如果使用十六进制编辑器查看日志文件,可以看到该字符串后面有一个换行符 \n。随后是响应 <réponse>…</réponse>。 因此,我们需要转换的字符串 XML 始于字符串 XML 的前 39 个字符之后。 它从第39个字符开始,其中第1个字符编号为0。该字符串由表达式 [response.text[39:]] 生成。

如果我们运行客户端(按照前面的示例步骤操作),在文件 [résultats.json] 中得到的结果与前几个版本相同。日志如下:


2020-07-27 16:21:14.015941, Thread-1 : début du thread [Thread-1] avec 2 contribuable(s)
2020-07-27 16:21:14.016940, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-27 16:21:14.016940, Thread-2 : début du thread [Thread-2] avec 3 contribuable(s)
2020-07-27 16:21:14.018939, Thread-2 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-27 16:21:14.019979, Thread-3 : début du thread [Thread-3] avec 3 contribuable(s)
2020-07-27 16:21:14.019979, Thread-3 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000}
2020-07-27 16:21:14.021938, Thread-4 : début du thread [Thread-4] avec 2 contribuable(s)
2020-07-27 16:21:14.021938, Thread-4 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000}
2020-07-27 16:21:14.021938, Thread-5 : début du thread [Thread-5] avec 1 contribuable(s)
2020-07-27 16:21:14.022939, Thread-5 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-27 16:21:14.031942, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<réponse><result><marié>oui</marié><enfants>2</enfants><salaire>55555</salaire><impôt>2814</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>0</décôte><réduction>0</réduction></result></réponse>
2020-07-27 16:21:14.031942, Thread-1 : fin du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-27 16:21:14.031942, Thread-1 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-27 16:21:14.034941, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<réponse><result><marié>oui</marié><enfants>2</enfants><salaire>30000</salaire><impôt>0</impôt><surcôte>0</surcôte><taux>0.0</taux><décôte>0</décôte><réduction>0</réduction></result></réponse>

2020-07-27 16:21:17.055931, Thread-3 : fin du thread [Thread-3]
2020-07-27 16:21:17.059930, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<réponse><result><marié>non</marié><enfants>3</enfants><salaire>100000</salaire><impôt>16782</impôt><surcôte>7176</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction></result></réponse>
2020-07-27 16:21:17.060971, Thread-2 : fin du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-27 16:21:17.060971, Thread-2 : fin du thread [Thread-2]

服务器端的日志如下:


2020-07-27 16:32:04.983020, Thread-46 : [index] requête : <Request 'http://127.0.0.1:5000/?已婚=是&子女=2&工资=50000' [GET]>
2020-07-27 16:32:04.983020, Thread-46 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-27 16:32:04.984021, Thread-47 : [index] requête : <Request 'http://127.0.0.1:5000/?已婚=是&子女=2&工资=55555' [GET]>
2020-07-27 16:32:04.984021, Thread-47 : [index] mis en pause du thread pendant 1 seconde(s)

2020-07-27 16:32:07.001271, Thread-56 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-27 16:32:07.003078, Thread-54 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
2020-07-27 16:32:07.006078, Thread-55 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-27 16:32:08.002824, Thread-56 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
  • 日志记录器继续写入响应字典,而非发送给客户端的字符串 XML。这并非错误,而是设计使然;

27.2.2. 客户端 [dao] 层的测试

Image

测试类 [TestHttpClientDao]|第 7 版| 中的相同,并产生相同的结果。