27. Ćwiczenie praktyczne: wersja 9
Wracamy do wersji 7 ćwiczenia praktycznego i zamiast wymiany ciągów znaków typu jSON między klientem a serwerem WWW, tym razem będą one wymieniać ciągi typu XML.
Architektura pozostaje taka sama:

27.1. Serwer WWW

Folder [http-servers/04] uzyskuje się poprzez skopiowanie folderu [http-servers/02] z wyjątkiem podfolderu [utilities]. Następnie zmienia się następujące elementy:

Plik [config] zostaje zmodyfikowany w następujący sposób:
# zależności bezwzględne
absolute_dependencies = [
# foldery projektu
# 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",
# Stałe, przedziały
f"{root_dir}/impots/v05/entities",
# index_controller
f"{root_dir}/impots/http-servers/01/controllers",
# skrypty [config_database, config_layers]
script_dir,
# Rejestrator, SendAdminMail
f"{root_dir}/impots/http-servers/02/utilities",
]
- wiersz 21: należy wskazać folder z narzędziami, które pozostały w pliku [http-servers/02];
Główny skrypt [main] zmienia się w następujący sposób:
# Strona główna URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
logger = None
try:
# logger
logger = Logger(config["logsFilename"])
# zapisujemy to w konfiguracji powiązanej z wątkiem
thread_config = {"logger": logger}
thread_name = threading.current_thread().name
config[thread_name] = {"config": thread_config}
# rejestruje się żądanie
logger.write(f"[index] requête : {request}\n")
…
# zlecamy wykonanie żądania kontrolerowi
résultat, status_code = index_controller.execute(request, config)
# czy wystąpił błąd krytyczny?
…
# rejestruje się odpowiedź
logger.write(f"[index] {résultat}\n")
# wysyłamy odpowiedź
return xml_response(résultat, status_code)
except BaseException as erreur:
# rejestruje się błąd, jeśli to możliwe
if logger:
logger.write(f"[index] {erreur}")
# przygotowuje się odpowiedź dla klienta
résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
# wysyłamy odpowiedź
return xml_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
finally:
# zamykamy plik dziennika, jeśli został otwarty
if logger:
logger.close()
- Jedyna zmiana dotyczy wiersza 23: teraz wysyłana jest odpowiedź XML;
Funkcja [xml_response] jest zdefiniowana w module [myutils]:
import xmltodict
…
def xml_response(résultat: dict, status_code: int) -> tuple:
# wynik: słownik do przekształcenia w ciąg znaków XML
xmlString = xmltodict.unparse(résultat)
# zwracamy odpowiedź HTTP
response = make_response(xmlString)
response.headers['Content-Type'] = 'application/xml; charset=utf-8'
return response, status_code
- wiersz 3: funkcja [xml_response] otrzymuje jako parametry:
- słownik [résultat], który ma zostać przekształcony w XML;
- kod statusu [status_code], który ma zostać zwrócony do klienta internetowego;
- wiersz 5: wykorzystuje się bibliotekę [xmltodict] do wygenerowania ciągu znaków XML;
- wiersz 8: używa się nagłówka [Content-Type], aby poinformować klienta, że wysyłamy mu XML;
Funkcja [xml_response] musi zostać zaimportowana do skryptu [__init__.py]:
from .myutils import set_syspath, json_response, decode_flask_session, xml_response
Następnie moduł [myutils] należy włączyć do modułów o zasięgu maszynowym. Odbywa się to w terminalu PyCharm za pomocą polecenia [pip install .] (w folderze packages).
27.2. Klient internetowy
27.2.1. Kod

Folder [http-clients/04] uzyskuje się poprzez skopiowanie klienta [http-clients/02]. Następnie modyfikujemy klasę [ImpôtsDaoWithHttpClient] w następujący sposób:
# importy
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):
# konstruktor
def __init__(self, config: dict):
…
# metoda nieużywana
def get_admindata(self) -> AdminData:
pass
# obliczenie podatku
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
….
# kod statusu odpowiedzi HTTP
status_code = response.status_code
# odpowiedź XML jest umieszczana w słowniku
résultat = xmltodict.parse(response.text[39:])
# błąd, jeśli kod statusu różni się od 200 OK
….
- wiersz 30: odpowiedź serwera WWW HTTP [response] jest teraz ciągiem znaków XML. Logi pokazują jego charakter:
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>
Ciąg znaków [<?xml version="1.0" encoding="utf-8"?>] składa się z 38 znaków. Ponadto, jeśli przyjrzymy się plikowi logów za pomocą edytora szesnastkowego, zauważymy, że za tym ciągiem znajduje się znak końca linii \n. Następnie pojawia się odpowiedź <odpowiedź>…</odpowiedź>. Ciąg XML, który musimy przekonwertować, zaczyna się zatem po pierwszych 39 znakach ciągu XML. Zaczyna się od znaku nr 39, przy czym pierwszy znak ma numer 0. Ciąg ten otrzymujemy za pomocą wyrażenia [résultat["réponse"]].
Jeśli uruchomimy klienta (postępując zgodnie z procedurą z poprzednich przykładów), otrzymamy w pliku [résultats.json] te same wyniki, co w poprzednich wersjach. Logi wyglądają następująco:
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]
Po stronie serwera logi wyglądają następująco:
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/?marié=oui&enfants=2&salaire=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}}}
- moduł logujący nadal zapisuje słownik odpowiedzi, a nie ciąg znaków XML, który jest wysyłany do klienta. Nie jest to błąd i jest to zamierzone zachowanie;
27.2.2. Test warstwy klienckiej [dao]

Klasa testowa [TestHttpClientDao] jest taka sama jak w przypadku |version 7| i daje te same wyniki.