31. Klienty internetowe dla usług jSON i XML w wersji 12
Napiszemy trzy konsolowe aplikacje klienckie dla usług jSON i XML serwera internetowego, który właśnie stworzyliśmy. Wykorzystamy architekturę klient-serwer z wersji 11:

Napiszemy trzy skrypty konsolowe:
- skrypty [main] i [main3] będą korzystać z warstwy serwera [métier];
- skrypt [main2] będzie korzystał z warstwy [métier] po stronie klienta;
31.1. Struktura drzewa skryptów klientów
Folder [http-clients/07] jest początkowo tworzony poprzez skopiowanie folderu [http-clients/06]. Następnie jest on modyfikowany.

- do [1]: dane wykorzystywane lub tworzone przez klienta;
- do pliku [2] – konfiguracja i skrypty konsoli klienta;
- do [3] – warstwa klienta [dao];
- w [4] – folder testów warstwy [dao] klienta;
31.2. Warstwa [dao] klientów


31.2.1. Interfejs
Warstwa [dao] będzie implementować następujący interfejs [InterfaceImpôtsDaoWithHttpSession]:
from abc import abstractmethod
from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from TaxPayer import TaxPayer
class InterfaceImpôtsDaoWithHttpSession(AbstractImpôtsDao):
# obliczanie podatku na jednostkę
@abstractmethod
def calculate_tax(self, taxpayer: TaxPayer):
pass
# obliczanie podatku dla partii
@abstractmethod
def calculate_tax_in_bulk_mode(self, taxpayers: list):
pass
# inicjowanie sesji
@abstractmethod
def init_session(self, type_session: str):
pass
# zakończenie sesji
@abstractmethod
def end_session(self):
pass
# uwierzytelnianie
@abstractmethod
def authenticate_user(self, user: str, password: str):
pass
# lista symulacji
@abstractmethod
def get_simulations(self) -> list:
pass
# usunięcie symulacji
@abstractmethod
def delete_simulation(self, id: int) -> list:
pass
# pobranie danych umożliwiających obliczenie podatku
@abstractmethod
def get_admindata(self) -> AdminData:
pass
Każda metoda interfejsu odpowiada metodzie serwisu serwera obliczającego podatek o nazwie URL.
- wiersz 7: interfejs rozszerza klasę [AbstractDao], która zarządza dostępem do systemu plików;
Odpowiedniki metod i usług URL są określone w pliku konfiguracyjnym [config]:
# serwer obliczania podatku
"server": {
"urlServer": "http://127.0.0.1:5000",
"user": {
"login": "admin",
"password": "admin"
},
"url_services": {
"calculate-tax": "/calculer-impot",
"get-admindata": "/get-admindata",
"calculate-tax-in-bulk-mode": "/calculer-impots",
"init-session": "/init-session",
"end-session": "/fin-session",
"authenticate-user": "/authentifier-utilisateur",
"get-simulations": "/lister-simulations",
"delete-simulation": "/supprimer-simulation"
}
31.2.2. Implementacja
Interfejs [InterfaceImpôtsDaoWithHttpSession] jest zaimplementowany przez następującą klasę [ImpôtsDaoWithHttpSession]:
# importy
import json
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ôtsDaoWithHttpSession import InterfaceImpôtsDaoWithHttpSession
from TaxPayer import TaxPayer
class ImpôtsDaoWithHttpSession(InterfaceImpôtsDaoWithHttpSession):
# producent
def __init__(self, config: dict):
# inicjalizacja elementu nadrzędnego
AbstractImpôtsDao.__init__(self, config)
# zapisywanie elementów konfiguracji
# ogólna konfiguracja
self.__config = config
# serwer
self.__config_server = config["server"]
# usługi
self.__config_services = config["server"]['url_services']
# tryb debugowania
self.__debug = config["debug"]
# rejestrator
self.__logger = None
# pliki cookie
self.__cookies = None
# typ sesji (json, xml)
self.__session_type = None
# etap żądania / odpowiedzi
def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
# [method]: metoda HTTP, GET lub POST
# [url_service]: URL usługi
# [data]: parametry POST w formacie x-www-form-urlencoded
# [json]: parametry POST w formacie JSON
# [cookies]: pliki cookie, które należy dołączyć do żądania
# należy posiadać sesję XML lub JSON, w przeciwnym razie nie będzie można przetworzyć odpowiedzi
if self.__session_type not in ['json', 'xml']:
raise ImpôtsError(73, "il n'y a pas de session valide en cours")
# logowanie
if method == "GET":
# GET
response = requests.get(url_service, cookies=self.__cookies)
else:
# POST
response = requests.post(url_service, data=data_value, json=json_value, cookies=self.__cookies)
# tryb debugowania?
if self.__debug:
# logger
if not self.__logger:
self.__logger = self.__config['logger']
# trwa logowanie
self.__logger.write(f"{response.text}\n")
# wynik
if self.__session_type == "json":
résultat = json.loads(response.text)
else: # xml
résultat = xmltodict.parse(response.text[39:])['root']
# pobieramy pliki cookie z odpowiedzi, jeśli takie istnieją
if response.cookies:
self.__cookies = response.cookies
# kod statusu
status_code = response.status_code
# jeśli kod statusu jest inny niż 200 OK
if status_code != status.HTTP_200_OK:
raise ImpôtsError(35, résultat['réponse'])
# zwracamy wynik
return résultat['réponse']
def init_session(self, session_type: str):
# odnotowuje się typ sesji
self.__session_type = session_type
# adres URL usługi
config_server = self.__config_server
url_service = f"{config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
# wykonanie zapytania
self.get_response("GET", url_service)
…
- wiersze 16–34: konstruktor klasy;
- wiersz 19: inicjowana jest klasa nadrzędna;
- wiersze 21–28: zapisywane są niektóre dane konfiguracyjne;
- wiersze 29–34: tworzone są trzy właściwości wykorzystywane w metodach klasy;
- wiersze 36–82: metoda [get_response] wyodrębnia elementy wspólne dla wszystkich metod warstwy [dao]: wysyłanie żądania HTTP i pobieranie odpowiedzi HTTP z serwera;
- wiersze 38–42: definicja 5 parametrów metody [get_response];
- wiersz 42: należy zauważyć, że ponieważ serwer utrzymuje sesję, klient musi odczytać i odesłać pliki cookie;
- wiersze 44–46: sprawdzamy, czy rzeczywiście trwa ważna sesja;
- wiersz 51: przypadek metody GET. Odsyłamy otrzymane pliki cookie;
- wiersz 54: przypadek POST. Model ten może mieć dwa rodzaje parametrów:
- typ [x-www-form-urlencoded]. Dotyczy to parametrów URL, [/calculer-impot] oraz [/authentifier-utilisateur]. W takim przypadku stosuje się parametr [data_value] otrzymany przez metodę;
- typ [json]. Dotyczy to parametrów URL i [/calculer-impots]. W takim przypadku wykorzystuje się parametr [json_value] otrzymany przez metodę;
Również w tym przypadku zwracany jest plik cookie sesji.
- wiersze 56–62: jeśli działamy w trybie [debug], odpowiedź serwera jest rejestrowana w dzienniku. Ten wpis w dzienniku jest ważny, ponieważ pozwala dokładnie ustalić, co zwrócił serwer;
- wiersze 64–68: w zależności od tego, czy jesteśmy w trybie json czy xml, tekstowa odpowiedź serwera jest przekształcana w słownik. Weźmy na przykład URL [/init-session]:
Odpowiedź jSON wygląda następująco:
2020-08-03 11:45:21.218116, MainThread : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
Odpowiedź XML wygląda następująco:
2020-08-03 11:45:54.671871, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
Kod w wierszach 64–68 gwarantuje, że w obu przypadkach w [résultat] otrzymujemy słownik z kluczami [action, état, réponse];
- wiersze 70–72: jeśli odpowiedź zawiera pliki cookie, są one pobierane. Należy je przesłać ponownie przy następnym żądaniu;
- wiersze 74–79: jeśli status odpowiedzi HTTP różni się od 200, zgłaszany jest wyjątek z komunikatem o błędzie zawartym w wyniku [’réponse’]. Może to być pojedynczy błąd lub lista błędów;
- wiersze 81–82: zwracamy odpowiedź serwera do kodu wywołującego;
[init_session]
- wiersz 84: metoda [init_session] służy do określenia typu sesji (json lub xml), którą klient chce zainicjować z serwerem;
- wiersz 86: w klasie odnotowuje się żądany typ sesji. Wszystkie metody potrzebują bowiem tej informacji, aby poprawnie zdekodować odpowiedź serwera;
- wiersze 88–90: na podstawie konfiguracji aplikacji określa się usługę URL, do której należy skierować zapytanie;
- wiersz 93: wysyłane jest zapytanie do usługi URL. Nie pobiera się wyniku metody [get_response]:
- jeśli wygeneruje wyjątek, operacja zakończyła się niepowodzeniem. Wyjątek nie jest tutaj obsługiwany i zostanie przekazany bezpośrednio do kodu wywołującego, który następnie zatrzyma klienta, wyświetlając komunikat o błędzie;
- jeśli nie zgłasza wyjątku, oznacza to, że inicjalizacja sesji zakończyła się powodzeniem;
[authenticate_user]
def authenticate_user(self, user: str, password: str):
# adres URL usługi
config_server = self.__config_server
url_service = f"{config_server['urlServer']}{self.__config_services['authenticate-user']}"
post_params = {
"user": user,
"password": password
}
# wykonanie żądania
self.get_response("POST", url_service, post_params)
- metoda [authenticate_user] służy do uwierzytelnienia się na serwerze. W tym celu w linii 1 otrzymuje dane logowania [user, password];
- wiersze 2–4: określa się usługę URL, do której należy wysłać zapytanie;
- wiersze 5–8: ustalane są parametry usługi POST, ponieważ usługa URL oczekuje na usługę [/authentifier-utilisateur] z parametrami POST;
- wiersz 11: zapytanie jest wykonywane. Również w tym przypadku nie otrzymujemy odpowiedzi z serwera. To wyjątek zgłoszony przez [get_response] wskazuje, czy operacja zakończyła się powodzeniem, czy nie;
[calculate_tax]
def calculate_tax(self, taxpayer: TaxPayer):
# adres URL usługi
config_server = self.__config_server
url_service = f"{config_server['urlServer']}{self.__config_services['calculate-tax']}"
# parametry POST
post_params = {
"marié": taxpayer.marié,
"enfants": taxpayer.enfants,
"salaire": taxpayer.salaire
}
# wykonanie zapytania
response = self.get_response("POST", url_service, post_params)
# aktualizacja TaxPayer o odpowiedź
taxpayer.fromdict(response)
- metoda [calculate_tax] pozwala obliczyć podatek podatnika [taxpayer] przekazanego jako parametr. Parametr ten jest modyfikowany przez metodę (wiersz 15) i stanowi zatem wynik działania metody;
- wiersze 2–4: definiuje się usługę URL, do której ma zostać wysłane zapytanie;
- wiersze 6–10: parametry żądania POST, które ma zostać wysłane. W rzeczywistości usługa URL usługi [/calculer-impot] oczekuje na POST z parametrami [marié, enfants, salaire];
- wiersze 12–13: żądanie zostało wykonane, a odpowiedź serwera pobrana. Usługa URL powiązana z usługą [/calculer-impot] zwraca słownik zawierający klucze [impôt, décôte, surcôte, réduction, taux] dotyczące podatku;
- wiersz 15: uzyskany słownik [response] jest wykorzystywany do aktualizacji danych podatnika [taxpayer];
[calculate_tax_in_bulk_mode]
# obliczenie podatku w trybie zbiorczym
def calculate_tax_in_bulk_mode(self, taxpayers: list):
# zezwala się na zgłaszanie wyjątków
# przekształcamy podatników w listę słowników
# zachowuje się tylko właściwości [marié, enfants, salaire]
list_dict_taxpayers = list(
map(lambda taxpayer:
taxpayer.asdict(included_keys=[
'_TaxPayer__marié',
'_TaxPayer__enfants',
'_TaxPayer__salaire']),
taxpayers))
# adres URL usługi
config_server = self.__config_server
url_service = f"{config_server['urlServer']}{self.__config_services['calculate-tax-in-bulk-mode']}"
# wykonanie zapytania
list_dict_taxpayers2 = self.get_response("POST", url_service, data_value=None, json_value=list_dict_taxpayers)
# gdy występuje tylko jeden podatnik i trwa sesja XML, [list_dict_taxpayers2] nie stanowi listy
# w takim przypadku tworzy się z niej listę
if not isinstance(list_dict_taxpayers2, list):
list_dict_taxpayers2 = [list_dict_taxpayers2]
# aktualizujemy początkową listę podatników o otrzymane wyniki
for i in range(len(taxpayers)):
# aktualizacja podatników [i]
taxpayers[i].fromdict(list_dict_taxpayers2[i])
# tutaj parametr [taxpayers] został zaktualizowany o wyniki z serwera
- wiersz 2: metoda otrzymuje listę podatników typu TaxPayer;
- wiersze 7–13: ta lista elementów typu [TaxPayer] jest przekształcana w listę słowników typu [marié, enfants, salaire];
- wiersze 15–17: ustala się URL usługi;
- wiersze 19–20: wykonywane jest zapytanie POST, którego treść jSON składa się z listy słowników utworzonej w wierszu 7. Pobierana jest odpowiedź z serwera;
- wiersze 23–24: testy wykazały problem w przypadku sesji typu XML:
- jeśli początkowa lista podatników zawiera N elementów (N>1), otrzymujemy w wyniku listę N słowników typu [OrderedDict];
- jeśli początkowa lista zawiera tylko jeden element, otrzymujemy nie listę, ale jeden element typu [OrderedDict];
- wiersze 23–24: jeśli mamy do czynienia z tym ostatnim przypadkiem (1 element), przekształcamy wynik w listę zawierającą 1 element;
- wiersze 25–28: ta lista otrzymanych słowników zawiera podatek każdego podatnika z listy początkowej. Następnie aktualizujemy każdy z nich o otrzymane wyniki;
[get_simulations]
def get_simulations(self) -> list:
# adres URL usługi
config_server = self.__config_server
url_service = f"{config_server['urlServer']}{self.__config_services['get-simulations']}"
# wykonanie zapytania
return self.get_response("GET", url_service)
- wiersz 1: metoda żąda listy symulacji przeprowadzonych w bieżącej sesji;
- wiersz 2: metoda zwraca odpowiedź serwera;
[delete_simulation]
def delete_simulation(self, id: int) -> list:
# adres URL usługi
config_server = self.__config_server
url_service = f"{config_server['urlServer']}{self.__config_services['delete-simulation']}/{id}"
# wykonanie zapytania
return self.get_response("GET", url_service)
- wiersz 1: metoda usuwa symulację o podanym identyfikatorze;
- wiersz 7: zwraca odpowiedź serwera, czyli listę symulacji pozostałych po żądanym usunięciu;
[get-admindata]
def get_admindata(self) -> AdminData:
# pozwalamy na przekazywanie wyjątków
# adres URL usługi
config_server = self.__config_server
url_service = f"{config_server['urlServer']}{self.__config_services['get-admindata']}"
# wykonanie zapytania
résultat = self.get_response("GET", url_service)
# wynik to słownik wartości typu str, jeśli sesja jest w formacie XML
if self.__session_type == 'xml':
# nowy słownik
résultat2 = {}
# wszystko przekształcamy na wartości liczbowe
for key, value in résultat.items():
# niektóre elementy słownika są listami
if isinstance(value, list):
values = []
for value2 in value:
values.append(float(value2))
résultat2[key] = values
else:
# inne to pojedyncze elementy
résultat2[key] = float(value)
else:
résultat2 = résultat
# wynik typu AdminData
return AdminData().fromdict(résultat2)
- wiersz 1: metoda żąda od serwera stałych podatkowych umożliwiających obliczenie podatku;
- wiersz 29: zwraca typ [AdminData];
- wiersz 9: pobierana jest odpowiedź serwera w postaci słownika. Testy wykazały, że pojawia się problem, gdy sesja ma numer XML: zamiast wartości liczbowych wartości w słowniku są ciągami znaków. Zgłosiliśmy ten problem podczas analizy modułu [xmltodict] i stwierdziliśmy, że jest to normalne zachowanie. [xmltodict] nie zawiera informacji o typach w strumieniu XML, który jest mu przekazywany. W związku z tym w tym konkretnym przypadku należy przekonwertować na wartości liczbowe wszystkie wartości z otrzymanego słownika. Zawiera on trzy listy [limites, coeffr, coeffn] oraz ciąg właściwości liczbowych;
- wiersze 13–25: utworzenie słownika [résultat2] o wartościach liczbowych na podstawie słownika [résultat] o wartościach typu ciąg znaków;
- wiersz 29: słownik [resultat2] służy do zainicjowania typu [AdminData];
31.2.3. Fabryka warstwy [dao]
Nasze klienckie aplikacje będą wielowątkowe. Ponieważ warstwa [dao] jest zaimplementowana przez klasę posiadającą stan do odczytu/zapisu (= właściwości do odczytu/zapisu), każdy wątek musi posiadać własną warstwę [dao] lub też należy zsynchronizować dostęp do danych współdzielonych między wątkami. W tym przypadku wybieramy pierwsze rozwiązanie. Korzystamy z klasy [ImpôtsDaoWithHttpSessionFactory], która może tworzyć instancje warstwy [dao]:
from ImpôtsDaoWithHttpSession import ImpôtsDaoWithHttpSession
class ImpôtsDaoWithHttpSessionFactory:
def __init__(self, config: dict):
# zapisujemy parametr
self.__config = config
def new_instance(self):
# zwracamy instancję warstwy [dao]
return ImpôtsDaoWithHttpSession(self.__config)
31.3. Konfiguracja klientów

Klienci są konfigurowani za pomocą plików [config] i [config_layers]. Plik [config] ma następującą treść:
def configure(config: dict) -> dict:
import os
# krok 1 ------
# katalog tego pliku
script_dir = os.path.dirname(os.path.abspath(__file__))
# ścieżka główna
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# 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",
# ImpôtsDaoWithHttpSession, ImpôtsDaoWithHttpSessionFactory, InterfaceImpôtsDaoWithHttpSession
f"{script_dir}/../services",
# skrypty konfiguracyjne
script_dir,
# Rejestrator
f"{root_dir}/impots/http-servers/02/utilities",
]
# ustawiamy ścieżkę systemową
from myutils import set_syspath
set_syspath(absolute_dependencies)
# krok 2 ------
# konfiguracja aplikacji za pomocą stałych
config.update({
# plik podatników
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
# plik wyników
"resultsFilename": f"{script_dir}/../data/output/résultats.json",
# plik błędów
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
# plik logów
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# serwer obliczający podatek
"server": {
"urlServer": "http://127.0.0.1:5000",
"user": {
"login": "admin",
"password": "admin"
},
"url_services": {
"calculate-tax": "/calculer-impot",
"get-admindata": "/get-admindata",
"calculate-tax-in-bulk-mode": "/calculer-impots",
"init-session": "/init-session",
"end-session": "/fin-session",
"authenticate-user": "/authentifier-utilisateur",
"get-simulations": "/lister-simulations",
"delete-simulation": "/supprimer-simulation"
}
},
# tryb debugowania
"debug": True
}
)
# etap 3 ------
# instancjonowanie warstw
import config_layers
config['layers'] = config_layers.configure(config)
# przekazujemy konfigurację
return config
Plik [config_layers] ma następującą treść:
def configure(config: dict) -> dict:
# instancjonowanie warstw aplikacji
# warstwa [métier]
from ImpôtsMétier import ImpôtsMétier
métier = ImpôtsMétier()
# fabryka warstwy DAO
from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
dao_factory = ImpôtsDaoWithHttpSessionFactory(config)
# zwracamy konfigurację warstw
return {
"dao_factory": dao_factory,
"métier": métier
}
- Klienci nie będą mieli bezpośredniego dostępu do warstwy [dao]. Aby uzyskać taki dostęp, będą musieli skorzystać z fabryki warstwy [dao];
31.4. Klient [main]

Klient [main] umożliwia testowanie URL i [/init-session, /authentifier-utilisateur, /calculer-impots, /fin-session]:
# oczekuje się parametru w formacie JSON lub XML
import sys
syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
session_type = sys.argv[1].lower()
erreur = session_type != "json" and session_type != "xml"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# konfiguracja aplikacji
import config
config = config.configure({"session_type": session_type})
# zależności
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger
# uruchomienie warstwy [dao] w wątku
# taxpayers to lista podatników
def thread_function(config: dict, taxpayers: list):
# pobieramy fabrykę warstwy [dao]
dao_factory = config['layers']['dao_factory']
# tworzymy instancję warstwy [dao]
dao = dao_factory.new_instance()
# typ sesji
session_type = config['session_type']
# liczba podatników
nb_taxpayers = len(taxpayers)
# log
logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
# inicjowanie sesji
dao.init_session(session_type)
# autoryzacja
dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
# obliczanie podatku podatników
dao.calculate_tax_in_bulk_mode(taxpayers)
# koniec sesji
dao.end_session()
# log
logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")
# lista wątków klienta
threads = []
logger = None
# kod
try:
# logger
logger = Logger(config["logsFilename"])
# zapisujemy to w konfiguracji
config["logger"] = logger
# log początkowy
logger.write("début du calcul de l'impôt des contribuables\n")
# pobieramy fabrykę warstwy [dao]
dao_factory = config["layers"]["dao_factory"]
# tworzymy instancję warstwy [dao]
dao = dao_factory.new_instance()
# odczyt danych podatników
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# podatników?
if not taxpayers:
raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
# obliczanie podatku podatników przy użyciu wielu wątków
i = 0
l_taxpayers = len(taxpayers)
while i < len(taxpayers):
# każdy wątek przetworzy od 1 do 4 podatników
nb_taxpayers = min(l_taxpayers - i, random.randint(1, 4))
# lista podatników przetworzonych przez wątek
thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
# zwiększamy wartość i dla następnego wątku
i += nb_taxpayers
# tworzy się wątek
thread = threading.Thread(target=thread_function, args=(config, thread_taxpayers))
# dodaje się go do listy wątków skryptu głównego
threads.append(thread)
# uruchamia się wątek – operacja ta jest asynchroniczna – nie czeka się na wynik wątku
thread.start()
# główny wątek czeka na zakończenie wszystkich wątków, które uruchomił
for thread in threads:
thread.join()
# w tym momencie wszystkie wątki zakończyły swoją pracę – każdy z nich zmodyfikował jeden lub więcej obiektów [taxpayer]
# wyniki zapisuje się w pliku jSON
dao.write_taxpayers_results(taxpayers)
# koniec
except BaseException as erreur:
# wyświetlenie błędu
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# zamknięcie rejestratora
if logger:
# log zakończenia
logger.write("fin du calcul de l'impôt des contribuables\n")
# zamknięcie rejestratora
logger.close()
# zakończono
print("Travail terminé...")
# zakończenie wątków, które mogłyby nadal istnieć, gdyby proces zatrzymał się z powodu błędu
sys.exit()
- wiersze 4–11: klient oczekuje na parametr określający typ sesji (json lub xml), który ma być używany z serwerem;
- wiersze 13–15: klient jest skonfigurowany;
- wiersze 48–104: ten kod jest znany. Był już wielokrotnie wykorzystywany. Rozdziela on podatników, dla których ma zostać obliczony podatek, na kilka wątków;
- wiersz 26: metoda [thread_function] jest metodą wykonywaną przez każdy wątek w celu obliczenia podatku dla przypisanych mu podatników;
- wiersze 27–30: każdy wątek ma własną warstwę [dao];
- obliczanie podatku odbywa się w czterech etapach:
- wiersze 37–38: inicjalizacja sesji (json lub xml) z serwerem;
- wiersze 39–40: uwierzytelnianie na serwerze;
- wiersze 41–42: obliczenie podatku;
- wiersze 43–44: zamknięcie sesji z serwerem;
Po uruchomieniu tego kodu w trybie [json] otrzymujemy następujące logi:
2020-08-03 14:28:34.320751, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:28:34.328749, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.328749, Thread-2 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.333592, Thread-3 : début du calcul de l'impôt des 3 contribuables
2020-08-03 14:28:34.368651, Thread-2 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.375699, Thread-1 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.377432, Thread-3 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.385653, Thread-2 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.392656, Thread-1 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.396377, Thread-3 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.406528, Thread-2 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}]}
2020-08-03 14:28:34.413837, Thread-1 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}]}
2020-08-03 14:28:34.416695, Thread-3 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 3}]}
2020-08-03 14:28:34.425747, Thread-2 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.425747, Thread-2 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.428956, Thread-1 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.428956, Thread-1 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.428956, Thread-3 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.428956, Thread-3 : fin du calcul de l'impôt des 3 contribuables
2020-08-03 14:28:34.428956, MainThread : fin du calcul de l'impôt des contribuables
Powyżej przedstawiono przebieg wątku [Thread-2].
Jeśli uruchomimy [main] w trybie XML, logi będą wyglądały następująco:
2020-08-03 14:32:48.495316, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:32:48.496452, Thread-1 : début du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.498992, Thread-2 : début du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.498992, Thread-3 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:32:48.498992, Thread-4 : début du calcul de l'impôt des 3 contribuables
2020-08-03 14:32:48.538637, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.540783, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.547811, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.547811, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.555184, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.564793, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.564793, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.568333, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.568333, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><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><id>1</id></réponse><réponse><marié>oui</marié><enfants>2</enfants><salaire>50000</salaire><impôt>1384</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>384</décôte><réduction>347</réduction><id>2</id></réponse></root>
2020-08-03 14:32:48.579205, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>3</enfants><salaire>50000</salaire><impôt>0</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>720</décôte><réduction>0</réduction><id>1</id></réponse><réponse><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><id>2</id></réponse></root>
2020-08-03 14:32:48.579205, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><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><id>1</id></réponse><réponse><marié>oui</marié><enfants>3</enfants><salaire>100000</salaire><impôt>9200</impôt><surcôte>2180</surcôte><taux>0.3</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse><réponse><marié>oui</marié><enfants>5</enfants><salaire>100000</salaire><impôt>4230</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>0</décôte><réduction>0</réduction><id>3</id></réponse><réponse><marié>non</marié><enfants>0</enfants><salaire>100000</salaire><impôt>22986</impôt><surcôte>0</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>4</id></réponse></root>
2020-08-03 14:32:48.588051, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><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><id>1</id></réponse><réponse><marié>non</marié><enfants>0</enfants><salaire>200000</salaire><impôt>64210</impôt><surcôte>7498</surcôte><taux>0.45</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse><réponse><marié>oui</marié><enfants>3</enfants><salaire>200000</salaire><impôt>42842</impôt><surcôte>17283</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>3</id></réponse></root>
2020-08-03 14:32:48.594058, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-1 : fin du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.595198, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-2 : fin du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.595198, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-3 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:32:48.603351, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.603351, Thread-4 : fin du calcul de l'impôt des 3 contribuables
2020-08-03 14:32:48.603351, MainThread : fin du calcul de l'impôt des contribuables
Powyżej przedstawiono przebieg wątku [Thread-2].
31.5. Klient [main2]

Klient [main2] umożliwia testowanie URL i [/init-session, /authentifier-utilisateur, /get-admindata, /fin-session]:
# oczekuje się parametru w formacie JSON lub XML
import sys
syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
session_type = sys.argv[1].lower()
erreur = session_type != "json" and session_type != "xml"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# konfiguracja aplikacji
import config
config = config.configure({"session_type": session_type})
# zależności
from ImpôtsError import ImpôtsError
from Logger import Logger
logger = None
# kod
try:
# logger
logger = Logger(config["logsFilename"])
# zapisuje się to w konfiguracji
config["logger"] = logger
# log początkowy
logger.write("début du calcul de l'impôt des contribuables\n")
# pobieramy fabrykę z warstwy [dao]
dao_factory = config['layers']['dao_factory']
# tworzymy instancję warstwy [dao]
dao = dao_factory.new_instance()
# pobieramy podatników
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# podatników?
if not taxpayers:
raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
# typ sesji
session_type = config['session_type']
# inicjowanie sesji
dao.init_session(session_type)
# autoryzacja
dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
# pobieranie danych z urzędu skarbowego
admindata = dao.get_admindata()
# zakończenie sesji
dao.end_session()
# obliczanie podatku podatników przez warstwę [métier]
métier = config['layers']['métier']
for taxpayer in taxpayers:
métier.calculate_tax(taxpayer, admindata)
# zapisywanie wyników do pliku jSON
dao.write_taxpayers_results(taxpayers)
except BaseException as erreur:
# wyświetlenie błędu
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# zamykamy program rejestrujący
if logger:
# log zakończenia
logger.write("fin du calcul de l'impôt des contribuables\n")
# zamknięcie rejestratora
logger.close()
# zakończono
print("Travail terminé...")
- wiersze 1–11: pobieramy parametr [json, xml], który określa typ sesji, jaką należy nawiązać z serwerem;
- wiersze 13–15: konfiguruje się klienta;
- wiersze 30–33: tworzy się warstwę [dao];
- wiersze 34–35: za jej pomocą pobierana jest lista podatników, dla których należy obliczyć podatek;
- następnie następują cztery etapy komunikacji z serwerem;
- wiersze 41–42: rozpoczyna się sesja z serwerem;
- wiersze 43–44: następuje uwierzytelnienie na serwerze;
- wiersze 45–46: wysyłamy do serwera żądanie o stałe podatkowe umożliwiające obliczenie podatku;
- wiersze 47–48: zamyka się sesję z serwerem;
- wiersze 49–52: dzięki tym współczynnikom można obliczyć podatek podatników przy użyciu warstwy [métier] dostępnej lokalnie u klienta;
- wiersze 53–54: zapisuje się uzyskane wyniki;
W przypadku sesji XML wyniki są następujące:
2020-08-03 14:44:43.194294, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:44:43.231633, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:44:43.240872, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:44:43.250061, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>get-admindata</action><état>1000</état><réponse><limites>9964.0</limites><limites>27519.0</limites><limites>73779.0</limites><limites>156244.0</limites><limites>93749.0</limites><coeffr>0.0</coeffr><coeffr>0.14</coeffr><coeffr>0.3</coeffr><coeffr>0.41</coeffr><coeffr>0.45</coeffr><coeffn>0.0</coeffn><coeffn>1394.96</coeffn><coeffn>5798.0</coeffn><coeffn>13913.7</coeffn><coeffn>20163.4</coeffn><abattement_dixpourcent_min>437.0</abattement_dixpourcent_min><plafond_impot_couple_pour_decote>2627.0</plafond_impot_couple_pour_decote><plafond_decote_couple>1970.0</plafond_decote_couple><valeur_reduc_demi_part>3797.0</valeur_reduc_demi_part><plafond_revenus_celibataire_pour_reduction>21037.0</plafond_revenus_celibataire_pour_reduction><id>1</id><abattement_dixpourcent_max>12502.0</abattement_dixpourcent_max><plafond_impot_celibataire_pour_decote>1595.0</plafond_impot_celibataire_pour_decote><plafond_decote_celibataire>1196.0</plafond_decote_celibataire><plafond_revenus_couple_pour_reduction>42074.0</plafond_revenus_couple_pour_reduction><plafond_qf_demi_part>1551.0</plafond_qf_demi_part></réponse></root>
2020-08-03 14:44:43.269850, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:44:43.269850, MainThread : fin du calcul de l'impôt des contribuables
31.6. Klient [main3]
Klient [main3] umożliwia testowanie URL i [/init-session, /calculer-impots, /get-simulations, /delete-simulation, /fin-session]:

# oczekuje się na parametr w formacie JSON lub XML
import sys
syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
session_type = sys.argv[1].lower()
erreur = session_type != "json" and session_type != "xml"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# konfiguracja aplikacji
import config
config = config.configure({"session_type": session_type})
# zależności
from ImpôtsError import ImpôtsError
import sys
from Logger import Logger
logger = None
# kod
try:
# logger
logger = Logger(config["logsFilename"])
# zapisuje się to w konfiguracji
config["logger"] = logger
# log początkowy
logger.write("début du calcul de l'impôt des contribuables\n")
# pobieramy fabrykę z warstwy [dao]
dao_factory = config["layers"]["dao_factory"]
# tworzymy instancję warstwy [dao]
dao = dao_factory.new_instance()
# odczyt danych podatników
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# podatników?
if not taxpayers:
raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
# obliczanie podatku podatników
# liczba podatników
nb_taxpayers = len(taxpayers)
# log
logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
# inicjowanie sesji
dao.init_session(session_type)
# uwierzytelnianie
dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
# obliczamy podatek podatników
dao.calculate_tax_in_bulk_mode(taxpayers)
# pobieranie listy symulacji
simulations = dao.get_simulations()
# usuwamy co drugą
for i in range(len(simulations)):
if i % 2 == 0:
# usuwamy symulację
dao.delete_simulation(simulations[i]['id'])
# koniec sesji
dao.end_session()
# sprawdź logi, aby zobaczyć różne wyniki (tryb debugowania = True)
except BaseException as erreur:
# wyświetlenie błędu
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# zamykamy rejestrator
if logger:
# log końcowy
logger.write("fin du calcul de l'impôt des contribuables\n")
# zamknięcie logera
logger.close()
# zakończono
print("Travail terminé...")
- wiersze 1–11: pobieramy typ sesji z parametrów skryptu;
- wiersze 13–15: konfiguruje się aplikację;
- wiersze 25–50: pojawia się kod, który został już kiedyś omówiony;
- wiersze 51–52: pobieramy listę symulacji wykonanych w bieżącej sesji;
- wiersze 53–57: usuwa się co drugą symulację;
- wiersze 58–59: kończy się sesję;
Podczas sesji jSON logi wyglądają następująco:
2020-08-03 15:01:52.702297, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 15:01:52.702297, MainThread : début du calcul de l'impôt des 11 contribuables
2020-08-03 15:01:52.734806, MainThread : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 15:01:52.747961, MainThread : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 15:01:52.765721, MainThread : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 5}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 6}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 7}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 8}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 9}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 10}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 11}]}
2020-08-03 15:01:52.785505, MainThread : {"action": "lister-simulations", "état": 500, "réponse": [{"décôte": 0, "enfants": 2, "id": 1, "impôt": 2814, "marié": "oui", "réduction": 0, "salaire": 55555, "surcôte": 0, "taux": 0.14}, {"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 720, "enfants": 3, "id": 3, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.801475, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 720, "enfants": 3, "id": 3, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.810129, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.818803, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.834604, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.843803, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.851855, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}]}
2020-08-03 15:01:52.863165, MainThread : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 15:01:52.863165, MainThread : fin du calcul de l'impôt des contribuables
- wiersz 6: mamy 11 symulacji;
- wiersz 12: po usunięciu niektórych pozostało ich tylko 5;
31.7. Klasa testowa [Test2HttpClientDaoWithSession]

Klasa [Test2HttpClientDaoWithSession] testuje warstwę [dao] klientów w następujący sposób:
import unittest
from ImpôtsError import ImpôtsError
from Logger import Logger
from TaxPayer import TaxPayer
class Test2HttpClientDaoWithSession(unittest.TestCase):
def test_init_session_json(self) -> None:
print('test_init_session_json')
erreur = False
try:
dao.init_session('json')
except ImpôtsError as ex:
print(ex)
erreur = True
# nie powinno być żadnych błędów
self.assertFalse(erreur)
def test_init_session_xml(self) -> None:
print('test_init_session_xml')
erreur = False
try:
dao.init_session('xml')
except ImpôtsError as ex:
print(ex)
erreur = True
# nie powinno być żadnych błędów
self.assertFalse(erreur)
def test_init_session_xxx(self) -> None:
print('test_init_session_xxx')
erreur = False
try:
dao.init_session('xxx')
except ImpôtsError as ex:
print(ex)
erreur = True
# musi być błąd
self.assertTrue(erreur)
def test_authenticate_user_success(self) -> None:
print('test_authenticate_user_success')
# rozpocznij sesję
dao.init_session('json')
# test
erreur = False
try:
dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
except ImpôtsError as ex:
print(ex)
erreur = True
# nie powinno być błędu
self.assertFalse(erreur)
def test_authenticate_user_failed(self) -> None:
print('test_authenticate_user_failed')
# inicjowanie sesji
dao.init_session('json')
# test
erreur = False
try:
dao.authenticate_user('x', 'y')
except ImpôtsError as ex:
print(ex)
erreur = True
# powinien wystąpić błąd
self.assertTrue(erreur)
def test_get_simulations(self) -> None:
print('test_get_simulations')
# inicjowanie sesji
dao.init_session('json')
# uwierzytelnianie
dao.authenticate_user('admin', 'admin')
# obliczenie podatku
# {'stan cywilny': 'żonaty', 'dzieci': 2, 'wynagrodzenie': 55555,
# 'podatek': 2814, 'dopłata': 0, 'ulga': 0, 'obniżka': 0, 'stawka': 0,14}
taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
dao.calculate_tax(taxpayer)
# get_simulations
simulations = dao.get_simulations()
# weryfikacje
# musi być 1 symulacja
self.assertEqual(1, len(simulations))
simulation = simulations[0]
# weryfikacja obliczonego podatku
self.assertAlmostEqual(simulation['impôt'], 2815, delta=1)
self.assertEqual(simulation['décôte'], 0)
self.assertEqual(simulation['réduction'], 0)
self.assertAlmostEqual(simulation['taux'], 0.14, delta=0.01)
self.assertEqual(simulation['surcôte'], 0)
def test_delete_simulation(self) -> None:
print('test_delete_simulation')
# inicjowanie sesji
dao.init_session('json')
# uwierzytelnianie
dao.authenticate_user('admin', 'admin')
# obliczenie podatku
taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
dao.calculate_tax(taxpayer)
# get_simulations
simulations = dao.get_simulations()
# delete_simulation
dao.delete_simulation(simulations[0]['id'])
# get_simulations
simulations = dao.get_simulations()
# weryfikacja – nie powinno już być żadnych symulacji
self.assertEqual(0, len(simulations))
# usuwamy symulację, która nie istnieje
erreur = False
try:
dao.delete_simulation(100)
except ImpôtsError as ex:
print(ex)
erreur = True
# musi wystąpić błąd
self.assertTrue(erreur)
if __name__ == '__main__':
# konfiguracja aplikacji
import config
config = config.configure({})
# logger
logger = Logger(config["logsFilename"])
# zapisuje się to w konfiguracji
config["logger"] = logger
# warstwa [dao]
dao_factory = config['layers']['dao_factory']
dao = dao_factory.new_instance()
# uruchamia się metody testowe
print("tests en cours...")
unittest.main()
- warstwa [dao] wysyła żądanie do serwera, odbiera jego odpowiedź i formatuje ją, aby przekazać ją kodowi wywołującemu. Gdy serwer wysyła odpowiedź z kodem statusu innym niż 200, warstwa [dao] generuje wyjątek. W związku z tym część testów polega na sprawdzeniu, czy wystąpił wyjątek, czy nie;
- wiersze 9–18: inicjuje się sesję jSON. Nie powinno wystąpić żadnego błędu;
- wiersze 20–29: inicjujemy sesję XML. Nie powinno wystąpić żadnego błędu;
- wiersze 31–40: inicjujemy sesję z nieprawidłowym typem. Powinien wystąpić błąd;
- wiersze 42–54: następuje uwierzytelnienie przy użyciu prawidłowych danych logowania. Nie powinno wystąpić żadnego błędu;
- wiersze 56–68: uwierzytelnianie przy użyciu nieprawidłowych danych logowania. Powinien wystąpić błąd;
- wiersze 70–92: wykonuje się obliczenie podatku, a następnie żąda się listy symulacji. Powinna pojawić się co najmniej jedna. Ponadto sprawdza się, czy ta symulacja rzeczywiście zawiera żądany podatek;
- wiersze 94–119: tworzymy symulację, którą następnie usuwamy. Następnie próbujemy usunąć symulację, której już nie ma. Powinien pojawić się błąd;
- wiersze 121–137: test uruchamia się jako klasyczny skrypt konsolowy;
- wiersze 122–124: konfigurujemy aplikację;
- wiersze 126–129: konfiguruje się moduł logowania. Umożliwi to śledzenie logów;
- wiersze 131–133: tworzymy instancję warstwy [dao], która ma zostać przetestowana;
- wiersze 135–137: uruchamia się testy;
Wyniki wyświetlane w konsoli są następujące:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-clients/07/tests/Test2HttpClientDaoWithSession.py
tests en cours...
test_authenticate_user_failed
..MyException[35, ["Echec de l'authentification"]]
test_authenticate_user_success
test_delete_simulation
MyException[35, ["la simulation n° [100] n'existe pas"]]
test_get_simulations
test_init_session_json
test_init_session_xml
test_init_session_xxx
MyException[73, il n'y a pas de session valide en cours]
----------------------------------------------------------------------
Ran 7 tests in 0.171s
OK
Process finished with exit code 0