Skip to content

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:

Image

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.

Image

  • 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

Image

Image

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

Image

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]

Image

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(36f"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(14))
        # 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]

Image

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(36f"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]:

Image


# 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(36f"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]

Image

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