29. Ćwiczenie praktyczne: wersja 11
29.1. Wprowadzenie
W poprzednich wersjach aplikacji klient-serwer służącej do obliczania podatku warstwa [métier], która realizuje reguły biznesowe tego obliczenia, znajdowała się po stronie serwera. Obecnie proponuje się przeniesienie jej na stronę klienta. Jaka jest tego zaleta? Część zadań wykonywanych dotychczas przez serwer zostanie przeniesiona na stronę klienta. Jeśli weźmiemy pod uwagę sytuację, w której serwer jest obsługiwany przez N klientów, to N obliczeń podatkowych zostanie wykonanych przez samych klientów. W poprzednich wersjach serwer wykonywał te N obliczeń. Ponieważ serwer nie będzie już wykonywał obliczeń biznesowych, będzie szybciej odpowiadał na zapytania klientów i będzie mógł obsłużyć jednocześnie większą liczbę użytkowników.
Architektura klient-serwer wygląda następująco:

- warstwa [métier] [10] została zduplikowana jako [12] po stronie klienta;
- do klienta dodano nowy skrypt [main2] [11];
Klient internetowy będzie miał dwie możliwości obliczenia podatku dla listy podatników znalezionej w pliku [3]:
- wykorzystanie metody z poprzedniej wersji. Wykorzystuje ona warstwę serwera [métier] [10]. Skrypt [main] będzie korzystał z tej metody;
- po prostu poprosić serwer o dane z urzędu skarbowego [2-4], a następnie skorzystać z warstwy [métier] i [12] dostępnej lokalnie u klienta;
Porównamy wydajność obu metod.
29.2. Serwer WWW
Struktura serwera internetowego będzie wyglądać następująco:

- Folder [http-servers/06] jest początkowo tworzony poprzez skopiowanie folderu [http-servers/05]. Zachowamy bowiem osiągnięcia poprzedniej wersji 10. Dodamy do niej po prostu nową funkcjonalność. Wyraża się ona obecnością nowego kontrolera o nazwie [get_admindata_controller] [1]. Drugi kontroler [calculate_tax_controller] to nic innego jak stary kontroler [index_controller], który został przemianowany;
29.3. Konfiguracja
Serwer będzie oferował dwie usługi URL:
- [/calculate-tax] do obliczania podatku dla listy podatników przekazanej w treści POST. Odpowiada on zatem URL i [/] z poprzedniej wersji 10;
- [/get-admindata] generuje ciąg jSON zawierający dane z urzędu skarbowego;
Konfiguracja [config] przypisuje każdy z tych identyfikatorów URL do kontrolera, który go przetwarza:
# słownik kontrolerów aplikacji internetowej
import calculate_tax_controller, get_admindata_controller
controllers = {"calculate-tax": calculate_tax_controller, "get-admindata": get_admindata_controller}
config['controllers'] = controllers
29.4. Główny skrypt [main]
Główny skrypt [main] przebudowuje skrypt [main] z poprzedniej wersji:
# oczekuje się parametru mysql lub pgres
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
sgbd = sys.argv[1].lower()
erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# konfiguracja aplikacji
import config
config = config.configure({'sgbd': sgbd})
# zależności
…
# pobieranie danych z urzędu skarbowego
erreur = False
try:
# admindata będzie danymi o zasięgu aplikacji, dostępnymi wyłącznie do odczytu
config["admindata"] = config["layers"]["dao"].get_admindata()
# log pomyślnego wykonania
logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
# odnotowano błąd
…
…
# w przypadku wystąpienia błędu proces zostaje zatrzymany
if erreur:
sys.exit(2)
# aplikacja Flask może się uruchomić
app = Flask(__name__)
# główny kontroler
def main_controller() -> tuple:
# pobieramy żądaną akcję
dummy, action=request.path.split('/')
logger = None
try:
# logger
logger = Logger(config["logsFilename"])
# zapisywanie 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")
# przerywamy wątek, jeśli zostało to zlecone
sleep_time = config["sleep_time"]
if sleep_time != 0:
# przerwa jest losowa, aby niektóre wątki zostały przerwane, a inne nie
aléa = randint(0, 1)
if aléa == 1:
# log przed przerwą
logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
# przerwa
time.sleep(sleep_time)
# żądanie jest wykonywane przez kontroler żądań
controller = config['controllers'][action]
résultat, status_code = controller.execute(request, config)
# czy wystąpił błąd krytyczny?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# wysyłamy wiadomość e-mail do administratora aplikacji
config_mail = config["adminMail"]
config_mail["logger"] = logger
SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
# replika jest rejestrowana
logger.write(f"[index] {résultat}\n")
# wysyłamy odpowiedź
print(résultat)
return json_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 json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
finally:
# zamykamy plik dziennika, jeśli został otwarty
if logger:
logger.close()
# obliczanie podatku
@app.route('/calculate-tax', methods=['POST'])
@auth.login_required
def calculate_tax():
# przekazanie kontroli do głównego kontrolera
return main_controller()
# pobieranie danych z urzędu skarbowego
@app.route('/get-admindata', methods=['GET'])
@auth.login_required
def get_admindata():
# przekazanie kontroli do głównego kontrolera
return main_controller()
# tylko ręcznie
if __name__ == '__main__':
# uruchamiamy serwer
app.config.update(ENV="development", DEBUG=True)
app.run(threaded=True)
- wiersze 88–93: funkcja [calculate_tax] przetwarza skrypty URL i [/calculate-tax];
- wiersze 95–100: funkcja [get_admindata] przetwarza funkcje URL i [/get-admindata];
- te dwie funkcje same w sobie nic nie robią. Natychmiast przekazują kontrolę głównemu kontrolerowi [main_controller] z wierszy 37–86;
- wiersze 37–86: główny kontroler [main_controller] to nic innego jak funkcja [index] z poprzedniej wersji, z jednym wyjątkiem: podczas gdy funkcja [index] przetwarzała tylko jedną instancję URL, tutaj [main_controller] przetwarza dwie instancje URL. Musi więc zlecić ich przetworzenie jednemu z dwóch kontrolerów [calculate_tax_controller, get_admin,data_controller];
- wiersze 39–40: pobieramy żądaną akcję [calculate_tax] lub [get_admindata]. Informacja ta znajduje się w ścieżce URL [request.path]. W zależności od sytuacji [request.path] odpowiada [/get-admindata] lub [/calculate_tax]. Podział wiersza 40 da dwa elementy:
- pusty ciąg znaków dla części poprzedzającej znak /;
- nazwę żądanej akcji dla części następującej po znaku /;
- wiersze 62–63: po pobraniu akcji z URL wiadomo, którego kontrolera należy użyć do przetworzenia URL. Informacje te znajdują się w konfiguracji [config];
29.5. Kontrolery
Kontroler [calculate_tax_controller] to nic innego jak kontroler [index_controller] z poprzedniej wersji.
Kontroler [get_admindata_controller] ma następujące parametry:
from flask_api import status
from werkzeug.local import LocalProxy
def execute(request: LocalProxy, config: dict) -> tuple:
# zwracamy odpowiedź
return {"réponse": {"result": config["admindata"].asdict()}}, status.HTTP_200_OK
- URL [/get-admindata] musi zwrócić ciąg jSON z danych administracji podatkowej;
- wiersz 6: dane te zostały pobrane przez główny skrypt [main] i umieszczone w słowniku [config] w postaci obiektu [AdminData]. Zwracamy słownik tego obiektu;
29.6. Testy w Postmanie
Uruchamiamy serwer WWW, SGBD oraz serwer pocztowy [hMailServer]. Następnie za pomocą klienta Postman obliczamy podatek dla kilku podatników:

W konsoli Postmana dialog między klientem a serwerem wygląda następująco:
POST /calculate-tax HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 5e71461a-fec8-4315-85e8-41721de939e5
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 824
[
{
"marié": "oui",
"enfants": 2,
"salaire": 55555
},
…
{
"marié": "oui",
"enfants": 3,
"salaire": 200000
}
]
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 1461
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 29 Jul 2020 07:02:07 GMT
{"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347…]}}
Teraz wyślijmy żądanie do serwera URL i [/get-admindata] przy użyciu GET:

Dialog klient–serwer w konsoli Postman wygląda następująco:
GET /get-admindata HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 4af342c4-7ecb-4ab2-9e12-d653f81da424
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 596
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 29 Jul 2020 07:07:24 GMT
{"réponse": {"result": {"limites": [9964.0, 27519.0, 73779.0, 156244.0, 93749.0], "coeffr": [0.0, 0.14, 0.3, 0.41, 0.45], "coeffn": [0.0, 1394.96, 5798.0, 13913.7, 20163.4], "plafond_decote_couple": 1970.0, "valeur_reduc_demi_part": 3797.0, "plafond_revenus_celibataire_pour_reduction": 21037.0, "plafond_qf_demi_part": 1551.0, "abattement_dixpourcent_max": 12502.0, "plafond_impot_celibataire_pour_decote": 1595.0, "plafond_decote_celibataire": 1196.0, "plafond_revenus_couple_pour_reduction": 42074.0, "id": 1, "abattement_dixpourcent_min": 437.0, "plafond_impot_couple_pour_decote": 2627.0}}}
29.7. Klient internetowy


Folder [http-clients/06] powstaje początkowo poprzez skopiowanie folderu [http-clients/05]. Praca nad modyfikacją polega głównie na:
- zmianę konfiguracji [config_layers] tak, aby zawierała teraz warstwę [métier]. Wcześniej zawierała ona tylko warstwę [dao];
- dodanie nowej metody do warstwy [dao];
- napisać skrypt [main2], który będzie opierał się na warstwie [métier] klienta w celu obliczenia podatku podatników;
29.7.1. Konfiguracja warstw klienta
Konfiguracja warstw odbywa się w dwóch miejscach:
- w konfiguracji [config], która musi uwzględniać w zależnościach klienta folder zawierający implementację warstwy [métier]. Folder ten był już uwzględniony w zależnościach:
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ôtsDaoWithHttpClient
f"{script_dir}/../services",
# skrypty konfiguracyjne
script_dir,
# Rejestrator
f"{root_dir}/impots/http-servers/02/utilities",
]
Następnie należy zmodyfikować plik [config_layers]:
def configure(config: dict) -> dict:
# instancjonowanie warstw aplikacji
# warstwa [métier]
from ImpôtsMétier import ImpôtsMétier
métier = ImpôtsMétier()
# warstwa DAO
from ImpôtsDaoWithHttpClient import ImpôtsDaoWithHttpClient
dao = ImpôtsDaoWithHttpClient(config)
# konfiguracja warstw
return {
"dao": dao,
"métier": métier
}
- wiersze 4–6: instancja warstwy [métier];
- wiersze 13–16: warstwa [métier] została dodana do słownika warstw;
29.7.2. Implementacja warstwy [dao]

Warstwa [dao] będzie posiadać następujący interfejs [InterfaceImpôtsDaoWithHttpClient]:
from abc import abstractmethod
from AbstractImpôtsDao import AbstractImpôtsDao
class InterfaceImpôtsDaoWithHttpClient(AbstractImpôtsDao):
# obliczanie podatku
@abstractmethod
def calculate_tax_in_bulk_mode(self, taxpayers: list):
pass
- wiersz 5: interfejs [InterfaceImpôtsDaoWithHttpClient] dziedziczy po klasie abstrakcyjnej [AbstractImpôtsDao], która zarządza dostępem do systemu plików klienta. Przypomnijmy, że posiada ona metodę abstrakcyjną [get_admindata];
- wiersze 7–10: metoda [calculate_tax_in_bulk_mode], którą zdefiniowaliśmy w poprzedniej wersji, umożliwia obliczenie podatku dla listy podatników;
Interfejs ten jest zaimplementowany przez następującą klasę [ImpôtsDaoWithHttpClient]:
# importy
import json
import requests
from flask_api import status
from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDaoWithHttpClient import InterfaceImpôtsDaoWithHttpClient
class ImpôtsDaoWithHttpClient(InterfaceImpôtsDaoWithHttpClient):
# 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"]
# tryb debugowania
self.__debug = config["debug"]
# rejestrator
self.__logger = None
def get_admindata(self) -> AdminData:
# zezwalamy na zgłaszanie wyjątków
# adres URL usługi
config_server = self.__config_server
config_services = config_server['url_services']
url_service = f"{config_server['urlServer']}{config_services['get-admindata']}"
# połączenie
if config_server['authBasic']:
response = requests.get(url_service,
auth=(
config_server["user"]["login"],
config_server["user"]["password"]))
else:
response = requests.get(url_service)
# tryb debugowania?
if self.__debug:
# moduł logujący
if not self.__logger:
self.__logger = self.__config['logger']
# rejestruje się
self.__logger.write(f"{response.text}\n")
# kod statusu
status_code = response.status_code
# wynik w postaci słownika
résultat = json.loads(response.text)
# jeśli kod statusu jest inny niż 200 OK
if status_code != status.HTTP_200_OK:
raise ImpôtsError(58, résultat['réponse']['erreurs'])
# zwracamy wynik (słownik)
return AdminData().fromdict(résultat["réponse"]["result"])
# obliczanie podatku w trybie zbiorczym
def calculate_tax_in_bulk_mode(self, taxpayers: list):
# pozwala na zgłaszanie wyjątków
…
- wiersz 13: klasa [ImpôtsDaoWithHttpClient] implementuje interfejs [InterfaceImpôtsDaoWithHttpClient]. Wywodzi się zatem z klasy [AbstractImpôtsDao];
- wiersze 65–66: metoda [calculate_tax_in_bulk_mode] omówiona w poprzedniej wersji;
- wiersze 29–62: metoda [get_admindata], którą klasa nadrzędna [AbstractImpôtsDao] zadeklarowała jako abstrakcyjną. Jest ona zatem zaimplementowana w klasie podrzędnej;
- wiersze 33–35: określa się identyfikator URL usługi internetowej, do której metoda [get-admindata] ma wysłać zapytanie. Te identyfikatory usługi URL są zdefiniowane w konfiguracji klienta [config]:
# serwer obliczający podatek
"server": {
"urlServer": "http://127.0.0.1:5000",
"authBasic": True,
"user": {
"login": "admin",
"password": "admin"
},
"url_services": {
"calculate-tax": "/calculate-tax",
"get-admindata": "/get-admindata"
}
},
- (ciąg dalszy)
- wiersze 9–12: dwa URL serwera internetowego;
- wiersze 37–44: usługa URL jest wywoływana synchronicznie;
- wiersze 46–42: jeśli wymaga tego konfiguracja, odpowiedź serwera jest rejestrowana;
- wiersz 57: wiadomo, że serwer wysłał ciąg znaków jSON z słownika;
- wiersze 58–60: jeśli status odpowiedzi HTTP nie wynosi 200, generowany jest wyjątek;
- wiersze 61–62: zwracany jest obiekt [AdminData] zawierający dane urzędu skarbowego przesłane przez serwer;
29.8. Skrypty [main, main2]
Skrypt [main] pochodzi z poprzedniej wersji. Wykorzystuje on metodę [calculate_tax_in_bulk_mode] z warstwy [dao], a zatem korzysta z warstwy [métier] serwera;
Skrypt [main2] działa tak samo jak skrypt [main], ale korzysta z warstwy [métier] po stronie klienta:
# konfiguracja aplikacji
import config
config = config.configure({})
# zależności
from ImpôtsError import ImpôtsError
from Logger import Logger
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 warstwę [dao]
dao = config["layers"]["dao"]
# 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']}")
# pobieramy dane z urzędu skarbowego
admindata = dao.get_admindata()
# 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)
# z wyjątkiem BaseException jako błąd:
# # wyświetlenie błędu
# print(f"Wystąpił następujący błąd: {błąd}")
finally:
# zamykamy logger
if logger:
# log końcowy
logger.write("fin du calcul de l'impôt des contribuables\n")
# zamknięcie modułu logującego
logger.close()
# zakończono
print("Travail terminé...")
- wiersze 26–27: pobierane są dane z urzędu skarbowego z serwera;
- wiersze 28–31: następnie lokalnie obliczany jest podatek podatników;
29.9. Testy klienta
W każdym ze skryptów [main, main2] rejestruje się początek i koniec działania skryptu. Dzięki temu można obliczyć czas wykonania skryptu. Spróbujmy sformułować kilka prognoz:
- skrypt [main] z poprzedniej wersji:
- tworzy N wątków, które działają jednocześnie;
- każdy wątek przetwarza partię podatników, dla których oblicza podatek za pomocą pojedynczego zapytania do serwera;
- ponieważ N wątków działa jednocześnie, żądanie N+1 jest wysyłane, zanim żądanie N otrzyma odpowiedź. W ten sposób N żądań kosztuje więcej niż jedno żądanie, ale prawdopodobnie nieznacznie więcej. Ponadto na serwerze wykonywanych jest 11 (liczba podatników) obliczeń biznesowych;
- skrypt [main2] w tej wersji:
- wysyła jedno zapytanie do serwera;
- wykonuje 11 obliczeń biznesowych lokalnie na kliencie;
Obliczenia biznesowe będą trwały tyle samo, niezależnie od tego, czy są wykonywane na serwerze, czy na kliencie. Różnica będzie więc wynikała z zapytań. Można zatem oczekiwać, że czas wykonania skryptu [main] będzie nieco dłuższy niż skryptu [main2].
Uruchamiamy serwer w wersji 11, SGBD oraz serwer pocztowy [hMailServer]. Po stronie serwera ustawiamy parametr [sleep_time] na zero, aby oba testy zostały przeprowadzone w takich samych warunkach.
Uruchomienie 1 [main]
Uruchomienie [main] generuje następujące logi:
2020-07-29 14:35:50.016079, MainThread : début du calcul de l'impôt des contribuables
2020-07-29 14:35:50.016079, Thread-1 : début du calcul de l'impôt des 1 contribuables
2020-07-29 14:35:50.016079, Thread-2 : début du calcul de l'impôt des 4 contribuables
2020-07-29 14:35:50.016079, Thread-3 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.016079, Thread-4 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.024426, Thread-5 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.050473, Thread-1 : {"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.050473, Thread-1 : fin du calcul de l'impôt des 1 contribuables
2020-07-29 14:35:50.050473, Thread-3 : {"réponse": {"results": [{"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-3 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, Thread-5 : {"réponse": {"results": [{"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-5 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, Thread-2 : {"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-2 : fin du calcul de l'impôt des 4 contribuables
2020-07-29 14:35:50.051214, Thread-4 : {"réponse": {"results": [{"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-4 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, MainThread : fin du calcul de l'impôt des contribuables
Czas trwania wykonania wyniósł [051214-016079] nanosekund (wiersz 17 – wiersz 1), czyli 35 milisekund i 135 nanosekund.
Widać, że między pierwszym żądaniem wysłanym do serwera a ostatnią odpowiedzią otrzymaną przez klienta upłynął ten sam czas [051214-016079] (wiersz 15 – wiersz 1), czyli 35 milisekund i 135 nanosekund.
Wykonanie 2 [main2]
Wykonanie [main2] generuje następujące logi:
2020-07-29 14:41:03.303520, MainThread : début du calcul de l'impôt des contribuables
2020-07-29 14:41:03.345084, MainThread : {"réponse": {"result": {"limites": [9964.0, 27519.0, 73779.0, 156244.0, 13500.0], "coeffr": [0.0, 0.14, 0.3, 0.41, 0.45], "coeffn": [0.0, 1394.96, 5798.0, 13913.7, 20163.4], "plafond_decote_couple": 1970.0, "valeur_reduc_demi_part": 3797.0, "plafond_revenus_celibataire_pour_reduction": 21037.0, "plafond_qf_demi_part": 1551.0, "abattement_dixpourcent_max": 12502.0, "plafond_impot_celibataire_pour_decote": 1595.0, "plafond_decote_celibataire": 1196.0, "plafond_revenus_couple_pour_reduction": 42074.0, "id": 1, "abattement_dixpourcent_min": 437.0, "plafond_impot_couple_pour_decote": 2627.0}}}
2020-07-29 14:41:03.349975, MainThread : fin du calcul de l'impôt des contribuables
Czas wykonania wyniósł [349975-303520] nanosekund (wiersz 3 – wiersz 1), czyli 46 milisekund i 455 nanosekund. Zupełnie nieoczekiwanie [main] działa szybciej niż [main2].
Widać, że pojedyncze zapytanie o numerze [main2] trwało [345084-303520] (wiersz 2 – wiersz 1), czyli 41 milisekund i 564 nanosekund. Następnie obliczenie podatku trwało [349975-345084] (wiersz 3 – wiersz 2), czyli 4 milisekundy i 91 nanosekund. To właśnie zapytanie HTTP decyduje o czasie wykonania. Co zaskakujące, widać tutaj, że pojedyncze żądanie o numerze [main2] trwało dłużej niż cztery równoczesne żądania o numerach [main] i [35 millisecondes].
Po stronie serwera logi wyglądają następująco:
2020-07-29 14:35:27.047721, MainThread : [serveur] démarrage du serveur
2020-07-29 14:35:27.140927, MainThread : [serveur] connexion à la base de données réussie
2020-07-29 14:35:28.790716, MainThread : [serveur] démarrage du serveur
2020-07-29 14:35:28.847518, MainThread : [serveur] connexion à la base de données réussie
2020-07-29 14:35:50.039178, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.039178, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.043220, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.044307, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.045796, Thread-2 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 3, 'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'taux': 0.3, 'décôte': 0, 'réduction': 0}, {'marié': 'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.045796, Thread-3 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 2, 'salaire': 55555, 'impôt': 2814, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.046825, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.046825, Thread-6 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'taux': 0.14, 'décôte': 384, 'réduction': 347}, {'marié': 'oui', 'enfants': 3, 'salaire': 50000, 'impôt': 0, 'surcôte': 0, 'taux': 0.14, 'décôte': 720, 'réduction': 0}, {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41, 'décôte': 0, 'réduction': 0}, {'marié': 'non', 'enfants': 3, 'salaire': 100000, 'impôt': 16782, 'surcôte': 7176, 'taux': 0.41, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.046825, Thread-4 : [index] {'réponse': {'results': [{'marié': 'non', 'enfants': 0, 'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'taux': 0.45, 'décôte': 0, 'réduction': 0}, {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.046825, Thread-5 : [index] {'réponse': {'results': [{'marié': 'non', 'enfants': 0, 'salaire': 100000, 'impôt': 22986, 'surcôte': 0, 'taux': 0.41, 'décôte': 0, 'réduction': 0}, {'marié': 'oui', 'enfants': 2, 'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'taux': 0.0, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:41:03.341582, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/get-admindata' [GET]>
2020-07-29 14:41:03.341582, Thread-7 : [index] {'réponse': {'result': {'limites': [9964.0, 27519.0, 73779.0, 156244.0, 13500.0], 'coeffr': [0.0, 0.14, 0.3, 0.41, 0.45], 'coeffn': [0.0, 1394.96, 5798.0, 13913.7, 20163.4], 'plafond_decote_couple': 1970.0, 'valeur_reduc_demi_part': 3797.0, 'plafond_revenus_celibataire_pour_reduction': 21037.0, 'plafond_qf_demi_part': 1551.0, 'abattement_dixpourcent_max': 12502.0, 'plafond_impot_celibataire_pour_decote': 1595.0, 'plafond_decote_celibataire': 1196.0, 'plafond_revenus_couple_pour_reduction': 42074.0, 'id': 1, 'abattement_dixpourcent_min': 437.0, 'plafond_impot_couple_pour_decote': 2627.0}}}
- wiersz 5: pierwsze żądanie klienta [main];
- wiersz 14: ostatnia odpowiedź dla klienta [main]. Między nimi upłynęło 6 milisekund i 647 nanosekund;
- wiersze 15–16: jedyne żądanie klienta [main2]. Odpowiedź jest natychmiastowa;