Skip to content

28. Ćwiczenie praktyczne: wersja 10

28.1. Wprowadzenie

W przykładach klientów serwera obliczającego podatek wątki wysyłały N żądań sekwencyjnie, jeśli miały przetwarzać N podatników. Chodzi tu o wysłanie jednego żądania obejmującego N podatników. Dla każdego z nich należy wysłać informacje [marié, enfants, salaire]. Można je wysłać jako parametry:

  • jako parametry żądania URL. W ten sposób otrzymamy długie żądanie URL o niewielkim znaczeniu;
  • w treści (body) żądania HTTP. Wiemy, że treść ta jest ukryta przed użytkownikiem korzystającym z przeglądarki;

W obu przypadkach można użyć żądania [GET] lub [POST]. Wykorzystamy żądanie POST z parametrami zawartymi w treści żądania HTTP.

Architektura klient-serwer nie uległa zmianie:

Image

28.2. Serwer WWW

Image

Folder [http-servers/05] jest początkowo tworzony poprzez skopiowanie folderu [http-servers/02]. Wracamy do wymiany danych jSON między klientem a serwerem. Widzieliśmy już, że przejście z jSON do XML jest bardzo proste.

28.2.1. Konfiguracja

Konfiguracja [config, config_database, config_layers] pozostaje analogiczna do konfiguracji poprzednich wersji. Nie będziemy do niej wracać.

28.2.2. Główny skrypt [main]

Skrypt [main] jest identyczny ze skryptem z folderu [http-servers/02], który skopiowaliśmy. Różni się tylko jedną rzeczą:


# Strona główna URL
@app.route('/', methods=['POST'])
@auth.login_required
def index():

  • wiersz 2: teraz URL / uzyskuje się poprzez POST;

28.2.3. Kontroler [index_controller]

Kontroler [index_controller] zmienia się w następujący sposób:


# import zależności

import json

from flask_api import status
from werkzeug.local import LocalProxy


def execute(request: LocalProxy, config: dict) -> tuple:
    # zależności
    from ImpôtsError import ImpôtsError
    from TaxPayer import TaxPayer

    # pobieramy treść wpisu – oczekujemy listy słowników
    msg_erreur = None
    list_dict_taxpayers = None
    # treść jSON z POST
    request_text = request.data
    try:
        # którą przekształcamy w listę słowników
        list_dict_taxpayers = json.loads(request_text)
    except BaseException as erreur:
        # odnotowuje się błąd
        msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
    # czy lista jest niepusta?
    if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
        # odnotowujemy błąd
        msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
    # czy mamy listę słowników?
    if not msg_erreur:
        erreur = False
        i = 0
        while not erreur and i < len(list_dict_taxpayers):
            erreur = not isinstance(list_dict_taxpayers[i], dict)
            i += 1
        # błąd?
        if erreur:
            msg_erreur = "le corps du POST doit être une liste de dictionnaires"
    # błąd?
    if msg_erreur:
        # wysyłamy odpowiedź o błędzie do klienta
        résultats = {"réponse": {"erreurs": [msg_erreur]}}
        return résultats, status.HTTP_400_BAD_REQUEST

    # sprawdzamy po kolei TaxPayers
    # na początku brak błędów
    list_erreurs = []
    for dict_taxpayer in list_dict_taxpayers:
        # tworzy się TaxPayer na podstawie dict_taxpayer
        msg_erreur = None
        try:
            # kolejna operacja wyeliminuje przypadki, w których parametry nie są
            # właściwości klasy TaxPayer, a także przypadki, w których ich wartości
            # są nieprawidłowe
            TaxPayer().fromdict(dict_taxpayer)
        except BaseException as erreur:
            msg_erreur = f"{erreur}"
        # niektóre klucze muszą znajdować się w słowniku
        if not msg_erreur:
            # klucze [marié, enfants, salaire] muszą znajdować się w słowniku
            keys = dict_taxpayer.keys()
            if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
                msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
        # czy są jakieś błędy?
        if msg_erreur:
            # błąd występuje w samym TaxPayer
            dict_taxpayer['erreur'] = msg_erreur
            # dodajemy TaxPayer do listy błędów
            list_erreurs.append(dict_taxpayer)

    # przetworzono wszystkich podatników – czy występują błędy?
    if list_erreurs:
        # wysyłamy klientowi komunikat o błędzie
        résultats = {"réponse": {"erreurs": list_erreurs}}
        return résultats, status.HTTP_400_BAD_REQUEST

    # brak błędów, można kontynuować pracę
    # pobieranie danych z urzędu skarbowego
    admindata = config["admindata"]
    métier = config["layers"]["métier"]
    try:
        # przetwarzamy pliki TaxPayer pojedynczo
        list_taxpayers = []
        for dict_taxpayer in list_dict_taxpayers:
            # obliczanie podatku
            taxpayer = TaxPayer().fromdict(
                {'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
                 'wynagrodzenie': dict_taxpayer['salaire']})
            métier.calculate_tax(taxpayer, admindata)
            # zapisujemy wynik jako słownik
            list_taxpayers.append(taxpayer.asdict())
        # wysyłamy odpowiedź do klienta
        return {"réponse": {"results": list_taxpayers}}, status.HTTP_200_OK
    except ImpôtsError as erreur:
        # wysyłamy komunikat o błędzie do klienta
        return {"réponse": {"erreurs": f"[{erreur}]"}}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • wiersz 9: kontroler otrzymuje:
      • żądanie [request] od klienta;
      • konfigurację [config] z serwera;
  • wiersze 14–18: pobieramy treść żądania POST. Parametry zawarte w treści żądania HTTP mogą być zakodowane na różne sposoby. Jeden z nich już poznaliśmy: [x-www-form-urlencoded]. W tym przypadku użyjemy innego kodowania: jSON;
  • wiersz 18: [request.data] pozwala odzyskać treść (body) żądania HTTP. Pobieramy tutaj tekst i wiemy, że tekst ten to jSON, który reprezentuje listę słowników [marié, enfants, salaire];
  • wiersze 19–24: pobieramy tę listę słowników;
  • wiersze 22–24: jeśli pobranie obiektu jSON nie powiodło się, odnotowujemy błąd;
  • wiersze 26–28: jeśli okaże się, że pobrany obiekt nie jest listą lub jest listą pustą, odnotowuje się błąd;
  • wiersze 29–38: jeśli lista została pomyślnie pobrana, sprawdzamy, czy rzeczywiście jest to lista słowników;
  • wiersze 40–43: jeśli wystąpił błąd, zatrzymujemy się w tym miejscu i wysyłamy klientowi komunikat o błędzie;
  • wiersze 45–69: teraz sprawdzamy każdy ze słowników:
    • muszą one zawierać klucze [marié, enfants, salaire];
    • muszą umożliwiać utworzenie prawidłowego obiektu [TaxPayer];
  • wiersze 65–69: jeśli w słowniku wykryto błąd, jest on umieszczany w tym samym słowniku, powiązany z kluczem „błąd”;
  • wiersze 72–75: słowniki zawierające błędy zostały zebrane na liście [list_erreurs]. Jeśli lista ta nie jest pusta, wówczas jest wysyłana w odpowiedzi o błędzie do klienta;
  • wiersz 77: w tym momencie wiadomo, że można utworzyć listę obiektów typu [TaxPayer] na podstawie treści żądania wysłanego przez klienta;
  • wiersze 84–91: przetwarzamy listę otrzymanych słowników;
  • wiersz 86: na podstawie słownika tworzymy obiekt typu [TaxPayer];
  • wiersz 89: oblicza się podatek dla tego obiektu typu [TaxPayer];
  • wiersz 91: wiadomo, że obiekt [taxpayer] został zmodyfikowany w wyniku obliczenia podatku. Przekształca się go w słownik i dodaje do listy wyników;
  • wiersz 93: wysyłamy tę listę wyników do klienta;

28.2.4. Testy serwera

Przetestujemy serwer za pomocą klienta Postman:

  • uruchamiamy serwer WWW o nazwie SGBD oraz serwer pocztowy o nazwie [hMailServer];
  • uruchamiamy klienta Postman wraz z jego konsolą (Ctrl-Alt-C);

Image

  • w [1]: wysyłamy żądanie [POST];
  • w [2]: URL z serwera;
  • w [3]: treść żądania HTTP;
  • w [5]: określa się, że treść ta powinna zostać wysłana w postaci ciągu znaków jSON;
  • w [4]: przechodzi się w tryb [raw], aby móc skopiować i wkleić ciąg znaków jSON;
  • w [6]: wkleja się ciąg znaków jSON pobrany z jednego z plików [résultats.json] z różnych wersji. Następnie dla każdego podatnika zachowuje się tylko właściwości [marié, salaire, enfants];

Image

  • w pliku [7] sprawdzamy nagłówki HTTP, które klient Postman wyśle do serwera;
  • w [8] widać, że klient wyśle nagłówek [Content-Type], informujący serwer, że żądanie zawiera treść zakodowaną w formacie jSON. Wynika to z wcześniejszego wyboru [5];

Image

  • w [9-12]: w żądaniu umieszczamy identyfikatory oczekiwane przez serwer;

Wysyłamy to żądanie. Odpowiedź serwera jest następująca:

Image

  • w [3] otrzymano z jSON;
  • w [4] – podatek podatników;

Przyjrzyjmy się w konsoli Postman (Ctrl-Alt-C) dialogowi klient–serwer, który miał miejsce:

Klient Postman wysłał następujący tekst:

POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 03c4aa28-5a5d-4bb5-ac51-7ad51968c71d
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 824

[
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 55555
  },
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 50000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 50000
  },
  {
    "marié": "non",
    "enfants": 2,
    "salaire": 100000
  },
  {
    "marié": "non",
    "enfants": 3,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 5,
    "salaire": 100000
  },
  {
    "marié": "non",
    "enfants": 0,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 30000
  },
  {
    "marié": "non",
    "enfants": 0,
    "salaire": 200000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 200000
  }
]
  • wiersz 1: POST do serwera;
  • wiersz 2: nagłówek uwierzytelniający HTTP;
  • wiersz 3: klient informuje serwer, że wysyła mu ciąg znaków jSON o długości 824 bajtów (wiersz 11);
  • wiersze 13–69: treść żądania jSON;

Serwer odpowiedział następującym tekstem:


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: Tue, 28 Jul 2020 07:16:34 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}, {"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}, {"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}, {"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}, {"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}]}}
  • wiersz 1: żądanie zakończyło się powodzeniem;
  • wiersz 2: treść odpowiedzi serwera to ciąg znaków jSON. Ma on długość 1461 bajtów (wiersz 3);
  • wiersz 7: odpowiedź serwera jSON;

Przetestujmy teraz przypadki błędów.

Przypadek 1: wysyłamy dowolną treść


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 47652706-9744-46a0-a682-de010e5406c0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 3

abc

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 125
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:43:27 GMT

{"réponse": {"erreurs": ["le corps du POST n'est pas une chaîne jSON valide : Expecting value: line 1 column 1 (char 0)"]}}
  • wiersz 13: wysłano ciąg [abc], który nie jest prawidłowym ciągiem jSON (wiersz 3);
  • wiersz 15: serwer odpowiada kodem błędu 400;
  • wiersz 21: odpowiedź serwera jSON;

Przypadek 2: wyślijmy prawidłowy ciąg znaków jSON, który nie jest listą


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 03b64735-9239-47b3-b92d-be7c9ebc7559
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 17

{"att1":"value1"}

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 97
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:50:11 GMT

{"réponse": {"erreurs": ["le corps du POST n'est pas une liste ou alors cette liste est vide"]}}

Przypadek 3: wyślijmy ciąg jSON, który jest listą, której nie wszystkie elementy są słownikami


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: a1528a5f-777c-413f-b3be-7d4e9955b12a
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 7

[0,1,2]

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 85
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:52:10 GMT

{"réponse": {"erreurs": ["le corps du POST doit être une liste de dictionnaires"]}}

Przypadek 4: wyślijmy listę słowników zawierającą słownik, który nie ma prawidłowych kluczy


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: ba964d81-c9d9-46ff-a521-b4c4e5639484
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 19

[{"att1":"value1"}]

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 112
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:54:33 GMT

{"réponse": {"erreurs": [{"att1": "value1", "erreur": "MyException[2, la clé [att1] n'est pas autorisée]"}]}}

Przypadek 5: wyślijmy listę słowników zawierającą słownik z brakującymi kluczami:


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 98aec51d-f37d-4c14-81cd-c7ffcbbcdc65
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 18

[{"marié":"oui"}]

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 125
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:56:40 GMT

{"réponse": {"erreurs": [{"marié": "oui", "erreur": "le dictionnaire doit inclure les clés [marié, enfants, salaire]"}]}}

Przypadek 6: wyślijmy listę słowników zawierającą słownik z prawidłowymi kluczami, ale z niektórymi błędnymi wartościami:


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 3083e601-dee4-4e15-9ea4-fc0328d0fcf0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 46

[{"marié":"x", "enfants":"x", "salaire":"x"}]

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 167
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:59:32 GMT

{"réponse": {"erreurs": [{"marié": "x", "enfants": "x", "salaire": "x", "erreur": "MyException[31, l'attribut marié [x] doit avoir l'une des valeurs oui / non]"}]}}

28.3. Klient internetowy

Image

Folder [http-clients/05] (wersja 10) powstaje początkowo poprzez skopiowanie folderu [http-clients/02] (wersja 7). Następnie jest on modyfikowany.

28.3.1. Warstwa [dao]

Warstwa [dao] jest zaimplementowana przez następującą klasę [ImpôtsDaoWithHttpClient]:


# importy

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ô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

    # obliczanie podatku
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        

    # obliczanie podatku w trybie zbiorczym
    def calculate_tax_in_bulk_mode(self, taxpayers: list) -> list:
        # pozwala na przekazywanie wyjątków

        # przekształcanie 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))

        # połączenie z serwerem
        config_server = self.__config_server
        if config_server['authBasic']:
            response = requests.post(config_server['urlServer'], json=list_dict_taxpayers,
                                     auth=(config_server["user"]["login"],
                                           config_server["user"]["password"]))
        else:
            response = requests.post(config_server['urlServer'], json=list_dict_taxpayers)
        # tryb debugowania?
        if self.__debug:
            # moduł logowania
            if not self.__logger:
                self.__logger = self.__config['logger']
            # trwa logowanie
            self.__logger.write(f"{response.text}\n")
        # kod statusu odpowiedzi HTTP
        status_code = response.status_code
        # odpowiedź jSON jest umieszczana w słowniku
        résultat = response.json()
        # błąd, jeśli kod statusu różni się od 200 OK
        if status_code != status.HTTP_200_OK:
            # wiemy, że błędy zostały powiązane z kluczem [erreurs] odpowiedzi
            raise ImpôtsError(93, résultat['réponse']['erreurs'])
        # wiemy, że wynik został powiązany z kluczem [results] z odpowiedzi
        list_dict_taxpayers2 = résultat['réponse']['results']
        # 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
  • wiersze 1–26: kod pozostaje taki sam jak w wersji 7 i w innych wersjach;
  • wiersze 27–70: wprowadzono nową metodę [calculate_tax_in_bulk_mode], której zadaniem jest obliczenie podatku dla listy podatników;
  • wiersz 28: [taxpayers] to właśnie ta lista podatników;
  • wiersze 31–39: za pomocą funkcji [map] dokonuje się przejścia z listy obiektów typu [TaxPayer] na listę słowników;
  • wiersze 34–38: zastosowana funkcja lambda przekształca obiekt typu [TaxPayer] w słownik typu [dict], zawierający wyłącznie klucze typu [marié, enfants, salaire]. W tym celu wykorzystuje się parametr o nazwie [included_keys] metody [BaseEntity.asdict]. Przypominamy, że aby poznać dokładne nazwy właściwości, które należy umieścić w parametrach [excluded_keys, included_keys], należy skorzystać z predefiniowanego słownika [taxpayer.__dict__];
  • wiersze 41–48: połączenie z serwerem, a następnie uzyskanie odpowiedzi HTTP;
  • wiersze 44, 48:
    • używa się metody statycznej [requests.post] w celu wysłania POST do serwera;
    • używa się parametru o nazwie [json], aby wskazać, że treść POST jest ciągiem znaków jSON. Będzie to miało dwie konsekwencje:
      • obiekt przypisany do parametru o nazwie [json], w tym przypadku lista słowników, zostanie przekształcony w ciąg znaków jSON;
      • nagłówek
Content-Type: application/json

zostanie uwzględniony w nagłówkach HTTP pliku POST;

  • wiersz 59: odpowiedź serwera jSON jest deserializowana do słownika [résultat];
  • wiersze 61–63: obsługiwany jest ewentualny błąd przesłany przez serwer;
  • wiersz 65: wyniki obliczeń podatku znajdują się na liście słowników;
  • wiersze 67–69: wyniki te są wykorzystywane do aktualizacji początkowej listy podatników [taxpayers], pierwotnie otrzymanej przez metodę w wierszu 28;
  • wiersz 70: w tym miejscu początkowa lista podatników została zaktualizowana o wyniki obliczeń podatku;

28.3.2. Główny skrypt [main]

Główny skrypt [main] zmienia się w następujący sposób: modyfikowana jest wyłącznie funkcja [thread_function] wykonywana przez wątki utworzone przez klienta. Pozostała część kodu pozostaje bez zmian.


# uruchomienie warstwy [dao] w wątku
# „taxpayers” to lista podatników
def thread_function(dao: ImpôtsDaoWithHttpClient, logger: Logger, taxpayers: list):
    # dziennik rozpoczęcia wątku
    thread_name = threading.current_thread().name
    nb_taxpayers = len(taxpayers)
    # log
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    # obliczamy podatek podatników
    dao.calculate_tax_in_bulk_mode(taxpayers)
    # log
    logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")
  • wiersze 9–10: podczas gdy wcześniej stosowano pętlę, która kolejno przekazywała każdego podatnika do metody [dao.calculate_tax], tutaj wykonuje się tylko jedno wywołanie metody [dao.calculate_tax_in_bulk_mode], do której przekazywani są wszyscy podatnicy;

28.3.3. Wykonanie klienta

Porównamy czasy wykonania następujących wersji:

  • 7, w której dla każdego podatnika generowane jest osobne zapytanie HTTP;
  • 10 (ta wersja), w której podatnicy są grupowani w jednym zapytaniu HTTP;

Najpierw wersja 6. Aby porównać obie wersje, ustawiamy właściwość serwera [sleep_time] na zero, aby nie było wymuszonego oczekiwania wątków. Logi klienta są następujące:


2020-07-28 14:20:45.811347, Thread-1 : début du thread [Thread-1] avec 4 contribuable(s)
2020-07-28 14:20:45.811347, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}

2020-07-28 14:20:45.913065, Thread-3 : fin du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-28 14:20:45.913065, Thread-3 : fin du thread [Thread-3]

Czas wykonania operacji przez klienta w celu obliczenia podatku dla 11 podatników wynosi zatem [913065-811347= 101718], czyli około 102 milisekund.

Zróbmy to samo z wersją 10 (sleep_time z serwerem ustawionym na zero). Logi klienta wyglądają wówczas następująco:


2020-07-28 14:25:31.871428, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-07-28 14:25:31.873594, Thread-2 : début du calcul de l'impôt des 3 contribuables
2020-07-28 14:25:31.877429, Thread-3 : début du calcul de l'impôt des 3 contribuables
2020-07-28 14:25:31.882855, Thread-4 : début du calcul de l'impôt des 1 contribuables
2020-07-28 14:25:31.930723, Thread-2 : {"réponse": {"results": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}, {"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-28 14:25:31.935958, Thread-4 : fin du calcul de l'impôt des 1 contribuables
2020-07-28 14:25:31.935958, Thread-1 : fin du calcul de l'impôt des 4 contribuables

Czas wykonania operacji przez klienta w celu obliczenia podatku dla 11 podatników wynosi zatem [935958-871428= 64530 ns] (wiersz 8 – wiersz 1), czyli około 65 milisekund. Ta nowa wersja 10 zapewnia zatem poprawę wydajności o około 57% w porównaniu z wersją 7.

28.3.4. Testy warstwy klienta [dao]

Image

Test klienta [TestHttpClientDao] w wersji 10 jest bardzo zbliżony do testu w wersji 7:


import unittest

from Logger import Logger


class TestHttpClientDao(unittest.TestCase):

    def test_1(self) -> None:
        from TaxPayer import TaxPayer

        # {'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_in_bulk_mode([taxpayer])
        # weryfikacja
        self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    

if __name__ == '__main__':
    # konfigurujemy aplikację
    import config
    config = config.configure({})

    # rejestruje
    logger = Logger(config["logsFilename"])
    # zapisywanie w konfiguracji
    config["logger"] = logger
    # pobieramy warstwę [dao]
    dao = config["layers"]["dao"]

    # wykonuje się metody testowe
    print("tests en cours...")
    unittest.main()
  • wiersz 14: zamiast wywoływać metodę [dao.calculate_tax], wywołuje się metodę [dao.calculate_tax_in_bulk_mode], do której przekazuje się listę (obecność nawiasów kwadratowych) podatnika;

Wszystkie testy zakończyły się powodzeniem.