27. Практичне завдання: версія 9
Повернемося до версії 7 практичного завдання: замість того, щоб клієнт і веб-сервер обмінювалися рядками типу jSON, вони будуть обмінюватися рядками типу XML.
Архітектура залишається незмінною:

27.1. Веб-сервер

Папка [http-servers/04] створюється шляхом копіювання папки [http-servers/02], за винятком підпапки [utilities]. Потім змінюються такі елементи:

Файл [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,
# Logger, 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 = 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] отримує як параметри:
- словник [résultat], який потрібно перетворити на XML;
- код статусу [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. Веб-клієнт
27.2.1. Код

Файл [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: відповідь 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. Далі йде відповідь <відповідь>…</відповідь>. Отже, рядок XML, який нам потрібно перетворити, починається після перших 39 символів рядка XML. Вона починається з символу № 39, причому перший символ має номер 0. Цей рядок отримується за допомогою виразу [résultat["réponse"]].
Якщо запустити клієнт (дотримуючись процедури з попередніх прикладів), ми отримаємо у файлі [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/?marié=oui&enfants=2&salaire=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] на стороні клієнта

Тестовий клас [TestHttpClientDao] є таким самим, як у |версії 7|, і дає ті самі результати.