Skip to content

31. Webclients voor de diensten jSON en XML van versie 12

We gaan drie console-clienttoepassingen schrijven voor de diensten jSON en XML van de webserver die we zojuist hebben geschreven. We gebruiken hiervoor de client/server-architectuur van versie 11:

Image

We gaan drie consolescripts schrijven:

  • de scripts [main] en [main3] maken gebruik van de serverlaag [métier];
  • het script [main2] maakt gebruik van de clientlaag [métier];

31.1. De scriptboomstructuur van de clients

De map [http-clients/07] wordt in eerste instantie verkregen door de map [http-clients/06] te kopiëren. Vervolgens wordt deze aangepast.

Image

  • in [1]: de gegevens die door de klant worden gebruikt of aangemaakt;
  • in [2], de configuratie en de consolescripts van de klant;
  • in [3], de laag [dao] van de klant;
  • in [4], de testmap van de laag [dao] van de klant;

31.2. De [dao]-laag van de klanten

Image

Image

31.2.1. Interface

De laag [dao] zal de volgende interface [InterfaceImpôtsDaoWithHttpSession] implementeren:


from abc import abstractmethod

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from TaxPayer import TaxPayer

class InterfaceImpôtsDaoWithHttpSession(AbstractImpôtsDao):

    # de belasting per eenheid berekenen
    @abstractmethod
    def calculate_tax(self, taxpayer: TaxPayer):
        pass

    # berekening van de belasting per batch
    @abstractmethod
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        pass

    # initialisatie van een sessie
    @abstractmethod
    def init_session(self, type_session: str):
        pass

    # einde van de sessie
    @abstractmethod
    def end_session(self):
        pass

    # authenticatie
    @abstractmethod
    def authenticate_user(self, user: str, password: str):
        pass

    # lijst met simulaties
    @abstractmethod
    def get_simulations(self) -> list:
        pass

    # een simulatie verwijderen
    @abstractmethod
    def delete_simulation(self, id: int) -> list:
        pass

    # gegevens ophalen voor de belastingberekening
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass

Elke methode van de interface komt overeen met een servicemethode van de belastingberekeningsserver URL.

  • regel 7: de interface breidt de klasse [AbstractDao] uit, die de toegang tot het bestandssysteem beheert;

De koppeling tussen de methoden en de URL-services wordt vastgelegd in het configuratiebestand [config]:


        # de server voor de belastingberekening
        "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. Implementatie

De interface [InterfaceImpôtsDaoWithHttpSession] wordt geïmplementeerd door de volgende klasse [ImpôtsDaoWithHttpSession]:


# importen
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):

    # fabrikant
    def __init__(self, config: dict):
        # initialisatie bovenliggend element
        AbstractImpôtsDao.__init__(self, config)
        # opslag van configuratie-elementen
        # algemene configuratie
        self.__config = config
        # server
        self.__config_server = config["server"]
        # services
        self.__config_services = config["server"]['url_services']
        # debugmodus
        self.__debug = config["debug"]
        # logger
        self.__logger = None
        # cookies
        self.__cookies = None
        # sessietype (json, xml)
        self.__session_type = None

        # verzoek-/antwoordfase
     def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
        # [method]: methode HTTP GET of POST
        # [url_service]: URL-service
        # [data]: instellingen van POST in x-www-form-urlencoded
        # [json]: parameters van POST in JSON
        # [cookies]: cookies die in het verzoek moeten worden opgenomen

        # er moet een sessie zijn van het type XML of JSON, anders kan het antwoord niet worden verwerkt
        if self.__session_type not in ['json''xml']:
            raise ImpôtsError(73"il n'y a pas de session valide en cours")

        # aanmelding
        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)

        # debugmodus?
        if self.__debug:
            # logboek
            if not self.__logger:
                self.__logger = self.__config['logger']
            # wordt gelogd
            self.__logger.write(f"{response.text}\n")

        # resultaat
        if self.__session_type == "json":
            résultat = json.loads(response.text)
        else:  # xml
            résultat = xmltodict.parse(response.text[39:])['root']

        # we halen de cookies uit het antwoord op, indien aanwezig
        if response.cookies:
            self.__cookies = response.cookies

        # statuscode
        status_code = response.status_code

        # als de statuscode anders is dan 200 OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(35, résultat['réponse'])

        # het resultaat wordt teruggestuurd
        return résultat['réponse']

    def init_session(self, session_type: str):
        # het type van de sessie noteren
        self.__session_type = session_type

        # service-URL
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        # verzoek uitvoeren
        self.get_response("GET", url_service)

  • regels 16-34: de constructor van de klasse;
  • regel 19: de bovenliggende klasse wordt geïnitialiseerd;
  • regels 21-28: bepaalde configuratiegegevens worden opgeslagen;
  • regels 29-34: er worden drie eigenschappen aangemaakt die in de methoden van de klasse worden gebruikt;
  • regels 36-82: de methode [get_response] factoriseert wat alle methoden van de laag [dao] gemeen hebben: het verzenden van een verzoek HTTP en het ophalen van het antwoord HTTP van de server;
  • regels 38-42: definitie van de 5 parameters van de methode [get_response];
  • regel 42: merk op dat, omdat de server een sessie onderhoudt, de client cookies moet lezen en terugsturen;
  • regels 44-46: er wordt gecontroleerd of er daadwerkelijk een geldige sessie actief is;
  • regel 51: geval van GET. De ontvangen cookies worden teruggestuurd;
  • regel 54: het geval van POST. Deze kan twee soorten parameters hebben:
    • het type [x-www-form-urlencoded]. Dit is het geval bij URL, [/calculer-impot] en [/authentifier-utilisateur]. In dat geval wordt de parameter [data_value] gebruikt die door de methode is ontvangen;
    • het type [json]. Dit is het geval bij de URL en [/calculer-impots]. In dat geval wordt de parameter [json_value] gebruikt die door de methode is ontvangen;

Ook hier wordt de sessiecookie teruggestuurd.

  • regels 56-62: als men zich in de modus [debug] bevindt, wordt het antwoord van de server gelogd. Dit logboek is belangrijk omdat het precies laat zien wat de server heeft teruggestuurd;
  • regels 64-68: afhankelijk van of men zich in de json- of xml-modus bevindt, wordt het tekstantwoord van de server omgezet in een woordenboek. Laten we het voorbeeld nemen van URL [/init-session]:

Het antwoord jSON is als volgt:


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"]}

Het antwoord XML is als volgt:


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>

De code in de regels 64-68 zorgt ervoor dat we in beide gevallen in [résultat] een woordenboek krijgen met de sleutels [action, état, réponse];

  • regels 70-72: als het antwoord cookies bevat, worden deze opgehaald. Deze moeten bij het volgende verzoek worden teruggestuurd;
  • regels 74-79: als de status HTTP van het antwoord niet 200 is, wordt er een uitzondering gegenereerd met de foutmelding die is opgenomen in resultaat[’réponse’]. Dit kan een fout zijn of een lijst met fouten;
  • regels 81-82: het antwoord van de server wordt teruggegeven aan de aanroepende code;

[init_session]

  • regel 84: de methode [init_session] wordt gebruikt om het type JSON- of XML-sessie vast te leggen dat de client met de server wil starten;
  • regel 86: het gewenste sessietype wordt binnen de klasse vastgelegd. Alle methoden hebben deze informatie namelijk nodig om het antwoord van de server correct te decoderen;
  • regels 88-90: aan de hand van de configuratie van de applicatie wordt bepaald welke servicemethode URL moet worden aangeroepen;
  • regel 93: de service URL wordt opgevraagd. Het resultaat van de methode [get_response] wordt niet opgehaald:
    • als deze een uitzondering genereert, is de bewerking mislukt. De uitzondering wordt hier niet afgehandeld en wordt direct doorgegeven aan de aanroepende code, die vervolgens de client afsluit met een foutmelding;
    • als er geen uitzondering wordt gegenereerd, dan is de initialisatie van de sessie geslaagd;

[authenticate_user]


    def authenticate_user(self, user: str, password: str):
        # service-URL
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['authenticate-user']}"
        post_params = {
            "user": user,
            "password": password
        }

        # verzoek uitgevoerd
        self.get_response("POST", url_service, post_params)
  • de methode [authenticate_user] wordt gebruikt om zich bij de server te authenticeren. Hiervoor ontvangt deze de inloggegevens [user, password] op regel 1;
  • regels 2-4: de te raadplegen URL-service wordt bepaald;
  • regels 5-8: de parameters van de POST, aangezien de URL [/authentifier-utilisateur] wacht op een POST met parameters [user, password];
  • regel 11: het verzoek wordt uitgevoerd. Ook hier wordt het antwoord van de server niet opgehaald. Het is de door [get_response] gegenereerde uitzondering die aangeeft of het al dan niet is gelukt;

[calculate_tax]


    def calculate_tax(self, taxpayer: TaxPayer):
        # service-URL
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['calculate-tax']}"
        # parameters van POST
        post_params = {
            "marié": taxpayer.marié,
            "enfants": taxpayer.enfants,
            "salaire": taxpayer.salaire
        }

        # verzoek uitvoeren
        response = self.get_response("POST", url_service, post_params)
        # bijwerken van TaxPayer met het antwoord
        taxpayer.fromdict(response)
  • met de methode [calculate_tax] kan de belasting worden berekend van een belastingplichtige [taxpayer] die als parameter wordt doorgegeven. Deze parameter wordt door de methode gewijzigd (regel 15) en vormt dus het resultaat van de methode;
  • regels 2-4: hier wordt de te raadplegen service URL gedefinieerd;
  • regels 6-10: de parameters van de POST die moeten worden verzonden. De URL-service van [/calculer-impot] verwacht namelijk een POST met de parameters [marié, enfants, salaire];
  • regels 12-13: het verzoek wordt uitgevoerd en het antwoord van de server wordt opgehaald. De URL-service van [/calculer-impot] retourneert een woordenboek met de belastingcodes [impôt, décôte, surcôte, réduction, taux];
  • regel 15: het verkregen woordenboek [response] wordt gebruikt om de belastingplichtige [taxpayer] bij te werken;

[calculate_tax_in_bulk_mode]


    # berekening van de belasting in bulkmodus
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        # uitzonderingen doorgeven

        # belastingplichtigen worden omgezet in een lijst met woordenboeken
        # alleen de eigenschappen bewaren [marié, enfants, salaire]
        list_dict_taxpayers = list(
            map(lambda taxpayer:
                taxpayer.asdict(included_keys=[
                    '_TaxPayer__marié',
                    '_TaxPayer__enfants',
                    '_TaxPayer__salaire']),
                taxpayers))

        # service-URL
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['calculate-tax-in-bulk-mode']}"

        # verzoek uitvoeren
        list_dict_taxpayers2 = self.get_response("POST", url_service, data_value=None, json_value=list_dict_taxpayers)
        # wanneer er slechts één belastingplichtige is en men zich in een XML-sessie bevindt, is [list_dict_taxpayers2] geen lijst
        # in dat geval maken we er een lijst van
        if not isinstance(list_dict_taxpayers2, list):
            list_dict_taxpayers2 = [list_dict_taxpayers2]
        # we werken de oorspronkelijke lijst van belastingplichtigen bij met de ontvangen resultaten
        for i in range(len(taxpayers)):
            # bijwerking van taxpayers[i]
            taxpayers[i].fromdict(list_dict_taxpayers2[i])
        # hier is de parameter [taxpayers] bijgewerkt met de resultaten van de server
  • regel 2: de methode ontvangt een lijst met belastingplichtigen van het type TaxPayer;
  • regels 7-13: deze lijst met elementen van het type [TaxPayer] wordt omgezet in een lijst met woordenboeken van het type [marié, enfants, salaire];
  • regels 15-17: de URL van de dienst wordt vastgelegd;
  • regels 19-20: er wordt een verzoek van het type POST uitgevoerd, waarvan de inhoud (jSON) bestaat uit de lijst met woordenboeken die in regel 7 is aangemaakt. Het antwoord van de server wordt opgehaald;
  • regels 23-24: uit tests is gebleken dat er een probleem optreedt wanneer de sessie van het type XML is:
    • als de oorspronkelijke lijst met belastingplichtigen N elementen bevat (N>1), krijgen we als resultaat een lijst met N woordenboeken van het type [OrderedDict];
    • als de oorspronkelijke lijst slechts één element bevat, krijgt men geen lijst maar één element van het type [OrderedDict];
  • regels 23-24: als dit laatste het geval is (1 element), wordt het resultaat omgezet in een lijst met 1 element;
  • regels 25-28: deze lijst met ontvangen woordenboeken bevat de belasting van elke belastingplichtige uit de oorspronkelijke lijst. Vervolgens wordt elk van deze woordenboeken bijgewerkt met de ontvangen resultaten;

[get_simulations]


    def get_simulations(self) -> list:
        # service-URL
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['get-simulations']}"

        # verzoek uitgevoerd
        return self.get_response("GET", url_service)
  • regel 1: de methode vraagt de lijst met simulaties die in de huidige sessie zijn uitgevoerd;
  • regel 2: de methode retourneert het antwoord van de server;

[delete_simulation]


    def delete_simulation(self, id: int) -> list:
        # service-URL
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['delete-simulation']}/{id}"

        # verzoek uitgevoerd
        return self.get_response("GET", url_service)
  • regel 1: de methode verwijdert de simulatie waarvan de ID wordt doorgegeven;
  • regel 7: de methode retourneert het antwoord van de server, de lijst met simulaties die overblijven na de gevraagde verwijdering;

[get-admindata]


    def get_admindata(self) -> AdminData:
        # uitzonderingen worden doorgegeven

        # service-URL
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['get-admindata']}"

        # verzoek uitvoeren
        résultat = self.get_response("GET", url_service)

        # het resultaat is een woordenboek met waarden van het type str als de sessie xml is
        if self.__session_type == 'xml':
            # nieuw woordenboek
            résultat2 = {}
            # alles wordt omgezet naar numerieke waarden
            for key, value in résultat.items():
                # sommige elementen van het woordenboek zijn lijsten
                if isinstance(value, list):
                    values = []
                    for value2 in value:
                        values.append(float(value2))
                    résultat2[key] = values
                else:
                    # andere zijn eenvoudige elementen
                    résultat2[key] = float(value)
        else:
            résultat2 = résultat
        # resultaat van het type AdminData
        return AdminData().fromdict(résultat2)
  • regel 1: de methode vraagt de server om de fiscale constanten waarmee de belasting kan worden berekend;
  • regel 29: de methode retourneert een type [AdminData];
  • regel 9: het antwoord van de server wordt opgehaald in de vorm van een woordenboek. Uit tests blijkt dat er een probleem optreedt wanneer de sessie een XML-sessie is: in plaats van numerieke waarden zijn de waarden in het woordenboek tekenreeksen. We hadden dit probleem al gemeld tijdens het onderzoek naar de module [xmltodict] en vastgesteld dat dit normaal was. [xmltodict] bevat geen typeninformatie in de stroom XML die eraan wordt doorgegeven. Dat gezegd hebbende, moeten in dit specifieke geval alle waarden van het ontvangen woordenboek naar numerieke waarden worden geconverteerd. Dit woordenboek bevat drie lijsten [limites, coeffr, coeffn] en een reeks numerieke eigenschappen;
  • regels 13-25: aanmaken van een woordenboek [résultat2] met numerieke waarden op basis van het woordenboek [résultat] met waarden van het type tekenreeks;
  • regel 29: het woordenboek [resultat2] wordt gebruikt om een type [AdminData] te initialiseren;

31.2.3. De factory van de laag [dao]

Onze clients zullen multithreaded zijn. Aangezien de laag [dao] is geïmplementeerd door een klasse met een lees-/schrijfstatus (= lees-/schrijf-eigenschappen), moet elke thread zijn eigen laag [dao] hebben, of moet de toegang tot de gedeelde gegevens tussen de threads worden gesynchroniseerd. Hier kiezen we voor de eerste oplossing. We gebruiken een klasse [ImpôtsDaoWithHttpSessionFactory] die instanties van de laag [dao] kan aanmaken:


from ImpôtsDaoWithHttpSession import ImpôtsDaoWithHttpSession

class ImpôtsDaoWithHttpSessionFactory:

    def __init__(self, config: dict):
        # de parameter wordt opgeslagen
        self.__config = config

    def new_instance(self):
        # er wordt een instantie van de laag geretourneerd [dao]
        return ImpôtsDaoWithHttpSession(self.__config)

31.3. Configuratie van de clients

Image

De clients worden geconfigureerd via de bestanden [config] en [config_layers]. Het bestand [config] ziet er als volgt uit:


def configure(config: dict) -> dict:
    import os

    # stap 1 ------

    # map van dit bestand
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # hoofdmap
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # absolute afhankelijkheden
    absolute_dependencies = [
        # projectmappen
        # 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",
        # Constanten, segmenten
        f"{root_dir}/impots/v05/entities",
        # ImpôtsDaoWithHttpSession, ImpôtsDaoWithHttpSessionFactory, InterfaceImpôtsDaoWithHttpSession
        f"{script_dir}/../services",
        # configuratiescripts
        script_dir,
        # Logger
        f"{root_dir}/impots/http-servers/02/utilities",
    ]

    # het syspath instellen
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # stap 2 ------
    # configuratie van de applicatie met constanten
    config.update({
        # bestand met belastingplichtigen
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        # resultatenbestand
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        # foutbestand
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        # logbestand
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # de server voor belastingberekening
        "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"
            }
        },
        # debugmodus
        "debug"True
    }
    )

    # stap 3 ------
    # instantiëren van de lagen
    import config_layers
    config['layers'] = config_layers.configure(config)

    # de configuratie wordt doorgegeven
    return config

Het bestand [config_layers] ziet er als volgt uit:


def configure(config: dict) -> dict:
    # instantie van de applicatielagen

    # laag [métier]
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    # factory van de DAO-laag
    from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
    dao_factory = ImpôtsDaoWithHttpSessionFactory(config)

    # de configuratie van de lagen wordt weergegeven
    return {
        "dao_factory": dao_factory,
        "métier": métier
    }
  • klanten hebben geen directe toegang tot de laag [dao]. Om hier toegang toe te krijgen, moeten ze via de factory van de laag [dao] gaan;

31.4. De client [main]

Image

Met de client [main] kunnen de URL en [/init-session, /authentifier-utilisateur, /calculer-impots, /fin-session] worden getest:


# er wordt een JSON- of XML-parameter verwacht
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()

# de applicatie wordt geconfigureerd
import config
config = config.configure({"session_type": session_type})

# afhankelijkheden
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger

# uitvoering van de laag [dao] in een thread
# taxpayers is een lijst met belastingplichtigen
def thread_function(config: dict, taxpayers: list):
    # de factory van de laag [dao] wordt opgehaald
    dao_factory = config['layers']['dao_factory']
    # we maken een instantie van de laag [dao] aan
    dao = dao_factory.new_instance()
    # type van de sessie
    session_type = config['session_type']
    # aantal belastingplichtigen
    nb_taxpayers = len(taxpayers)
    # logboek
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    # de sessie wordt geïnitialiseerd
    dao.init_session(session_type)
    # authenticatie
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    # de belasting van de belastingplichtigen wordt berekend
    dao.calculate_tax_in_bulk_mode(taxpayers)
    # einde van de sessie
    dao.end_session()
    # logboek
    logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")

# lijst met threads van de client
threads = []
logger = None
# code
try:
    # logger
    logger = Logger(config["logsFilename"])
    # wordt opgeslagen in de configuratie
    config["logger"] = logger
    # startlog
    logger.write("début du calcul de l'impôt des contribuables\n")
    # we halen de factory van de laag [dao] op
    dao_factory = config["layers"]["dao_factory"]
    # er wordt een instantie van de laag [dao] aangemaakt
    dao = dao_factory.new_instance()
    # het uitlezen van de gegevens van de belastingplichtigen
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # van de belastingplichtigen?
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # de belasting van de belastingplichtigen berekenen met meerdere threads
    i = 0
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        # elke thread verwerkt 1 tot 4 belastingplichtigen
        nb_taxpayers = min(l_taxpayers - i, random.randint(14))
        # de lijst met belastingplichtigen die door de thread zijn verwerkt
        thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
        # we verhogen i voor de volgende thread
        i += nb_taxpayers
        # de thread wordt aangemaakt
        thread = threading.Thread(target=thread_function, args=(config, thread_taxpayers))
        # de thread wordt toegevoegd aan de lijst met threads van het hoofdscript
        threads.append(thread)
        # de thread wordt gestart – deze bewerking is asynchroon – er wordt niet gewacht op het resultaat van de thread
        thread.start()
    # de hoofdthread wacht tot alle door hem gestarte threads zijn voltooid
    for thread in threads:
        thread.join()
    # hier hebben alle threads hun werk voltooid – elk heeft een of meer objecten gewijzigd [taxpayer]
    # de resultaten worden opgeslagen in het bestand jSON
    dao.write_taxpayers_results(taxpayers)
    # einde
except BaseException as erreur:
    # foutmelding weergeven
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # de logger wordt afgesloten
    if logger:
        # eindlog
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # logger wordt afgesloten
        logger.close()
    # klaar
    print("Travail terminé...")
    # einde van de threads die mogelijk nog bestaan als het proces bij een fout is gestopt
    sys.exit()
  • regels 4-11: de client wacht op een parameter die aangeeft welk sessietype, json of xml, met de server moet worden gebruikt;
  • regels 13-15: de client is geconfigureerd;
  • regels 48-104: deze code is bekend. Hij is al vele malen gebruikt. Hij verdeelt de belastingplichtigen voor wie de belasting moet worden berekend over meerdere threads;
  • regel 26: de methode [thread_function] is de methode die door elke thread wordt uitgevoerd om de belasting te berekenen van de belastingplichtigen die aan die thread zijn toegewezen;
  • regels 27-30: elke thread heeft zijn eigen laag [dao];
  • de berekening van de belasting verloopt in vier stappen:
    • regels 37-38: initialisatie van een sessie, json of xml, met de server;
    • regels 39-40: authenticatie bij de server;
    • regels 41-42: berekening van de belasting;
    • regels 43-44: afsluiten van de sessie met de server;

Wanneer deze code wordt uitgevoerd in de modus [json], krijgt men de volgende logbestanden:


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

Hierboven zien we het verloop van de thread [Thread-2].

Als we [main] uitvoeren in de modus XML, zijn de logbestanden als volgt:


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

Hierboven is het verloop van de thread [Thread-2] weergegeven.

31.5. De client [main2]

Image

Met de client [main2] kun je de URL en [/init-session, /authentifier-utilisateur, /get-admindata, /fin-session] testen:


# er wordt gewacht op een JSON- of XML-parameter
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()

# de applicatie wordt geconfigureerd
import config
config = config.configure({"session_type": session_type})

# afhankelijkheden
from ImpôtsError import ImpôtsError
from Logger import Logger

logger = None
# code
try:
    # logger
    logger = Logger(config["logsFilename"])
    # dit wordt opgeslagen in de configuratie
    config["logger"] = logger
    # startlog
    logger.write("début du calcul de l'impôt des contribuables\n")
    # we halen de factory van de laag [dao] op
    dao_factory = config['layers']['dao_factory']
    # er wordt een instantie van de laag [dao] aangemaakt
    dao = dao_factory.new_instance()
    # de belastingplichtigen worden opgehaald
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # van de belastingplichtigen?
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # type van de sessie
    session_type = config['session_type']
    # de sessie wordt geïnitialiseerd
    dao.init_session(session_type)
    # authenticatie
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    # gegevens van de belastingdienst ophalen
    admindata = dao.get_admindata()
    # einde van de sessie
    dao.end_session()
    # de belasting van de belastingplichtigen wordt berekend door de laag [métier]
    métier = config['layers']['métier']
    for taxpayer in taxpayers:
        métier.calculate_tax(taxpayer, admindata)
    # de resultaten worden opgeslagen in het bestand jSON
    dao.write_taxpayers_results(taxpayers)
 except BaseException as erreur:
     # weergave van de foutmelding
     print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # de logger wordt afgesloten
    if logger:
        # eindlog
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # logger wordt afgesloten
        logger.close()
    # klaar
    print("Travail terminé...")
  • regels 1-11: de parameter [json, xml] wordt opgehaald, die het type van de met de server te tot stand te brengen sessie bepaalt;
  • regels 13-15: de client wordt geconfigureerd;
  • regels 30-33: er wordt een laag [dao] aangemaakt;
  • regels 34-35: hiermee wordt de lijst met belastingplichtigen opgehaald waarvoor de belasting moet worden berekend;
  • vervolgens volgen de vier stappen van de communicatie met de server;
    • regels 41-42: er wordt een sessie met de server gestart;
    • regels 43-44: we authenticeren ons bij de server;
    • regels 45-46: de fiscale constanten die nodig zijn voor de berekening van de belasting worden bij de server opgevraagd;
    • regels 47-48: de sessie met de server wordt beëindigd;
  • regels 49-52: met deze constanten kunnen we de belasting van de belastingplichtigen berekenen met behulp van de lokale laag [métier] op de client;
  • regels 53-54: de verkregen resultaten worden opgeslagen;

Voor een XML-sessie zijn de resultaten als volgt:


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. De client [main3]

Met de client [main3] kun je de URL en [/init-session, /calculer-impots, /get-simulations, /delete-simulation, /fin-session] testen:

Image


# we wachten op een JSON- of XML-parameter
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()

# de applicatie wordt geconfigureerd
import config
config = config.configure({"session_type": session_type})

# afhankelijkheden
from ImpôtsError import ImpôtsError
import sys
from Logger import Logger

logger = None
# code
try:
    # logger
    logger = Logger(config["logsFilename"])
    # dit wordt opgeslagen in de configuratie
    config["logger"] = logger
    # startlog
    logger.write("début du calcul de l'impôt des contribuables\n")
    # we halen de factory van de laag [dao] op
    dao_factory = config["layers"]["dao_factory"]
    # we maken een instantie aan van de laag [dao]
    dao = dao_factory.new_instance()
    # het uitlezen van de gegevens van de belastingplichtigen
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # van de belastingplichtigen?
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # de belasting van de belastingplichtigen berekenen
    # aantal belastingplichtigen
    nb_taxpayers = len(taxpayers)
    # log
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    # de sessie wordt gestart
    dao.init_session(session_type)
    # authenticatie
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    # de belasting van de belastingplichtigen wordt berekend
    dao.calculate_tax_in_bulk_mode(taxpayers)
    # de lijst met simulaties wordt opgevraagd
    simulations = dao.get_simulations()
    # één op de twee wordt verwijderd
    for i in range(len(simulations)):
        if i % 2 == 0:
            # de simulatie wordt verwijderd
            dao.delete_simulation(simulations[i]['id'])
    # einde van de sessie
    dao.end_session()
    # raadpleeg de logbestanden om de verschillende resultaten te bekijken (debug-modus=True)
except BaseException as erreur:
    # foutmelding weergeven
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # de logger wordt afgesloten
    if logger:
        # eindlog
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # logger wordt afgesloten
        logger.close()
    # klaar
    print("Travail terminé...")
  • regels 1-11: het sessietype wordt opgehaald uit de scriptparameters;
  • regels 13-15: de applicatie wordt geconfigureerd;
  • regels 25-50: hier staat code die al eerder is uitgelegd;
  • regels 51-52: we vragen de lijst met simulaties die in de huidige sessie zijn uitgevoerd op;
  • regels 53-57: er wordt om de andere simulatie verwijderd;
  • regels 58-59: de sessie wordt beëindigd;

Tijdens een sessie jSON zijn de logbestanden als volgt:


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
  • regel 6: er zijn 11 simulaties;
  • regel 12: na de verschillende verwijderingen zijn er nog maar 5 over;

31.7. De testklasse [Test2HttpClientDaoWithSession]

Image

De klasse [Test2HttpClientDaoWithSession] test de laag [dao] van de clients als volgt:


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
        # er mogen geen fouten zijn
        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
        # er mogen geen fouten zijn
        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
        # er moet een fout zijn
        self.assertTrue(erreur)

    def test_authenticate_user_success(self) -> None:
        print('test_authenticate_user_success')
        # sessie starten
        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
        # er mag geen fout zijn
        self.assertFalse(erreur)

    def test_authenticate_user_failed(self) -> None:
        print('test_authenticate_user_failed')
        # sessie starten
        dao.init_session('json')
        # test
        erreur = False
        try:
            dao.authenticate_user('x''y')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # er moet een fout zijn
        self.assertTrue(erreur)

    def test_get_simulations(self) -> None:
        print('test_get_simulations')
        # sessie starten
        dao.init_session('json')
        # authenticatie
        dao.authenticate_user('admin''admin')
        # belastingberekening
        # {'getrouwd': 'ja', 'kinderen': 2, 'salaris': 55555,
        # 'belasting': 2814, 'toeslag': 0, 'korting': 0, 'vermindering': 0, 'tarief': 0,14}
        taxpayer = TaxPayer().fromdict({"marié""oui""enfants"2"salaire"55555})
        dao.calculate_tax(taxpayer)
        # get_simulations
        simulations = dao.get_simulations()
        # controles
        # er moet 1 simulatie zijn
        self.assertEqual(1, len(simulations))
        simulation = simulations[0]
        # controle van de berekende belasting
        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')
        # sessie starten
        dao.init_session('json')
        # authenticatie
        dao.authenticate_user('admin''admin')
        # belastingberekening
        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()
        # controle – er mogen geen simulaties meer zijn
        self.assertEqual(0, len(simulations))
        # we verwijderen een simulatie die niet bestaat
        erreur = False
        try:
            dao.delete_simulation(100)
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # er moet een fout zijn
        self.assertTrue(erreur)

if __name__ == '__main__':
    # de applicatie wordt geconfigureerd
    import config
    config = config.configure({})

    # loggen
    logger = Logger(config["logsFilename"])
    # wordt opgeslagen in de configuratie
    config["logger"] = logger

    # laag [dao]
    dao_factory = config['layers']['dao_factory']
    dao = dao_factory.new_instance()

    # de testmethoden worden uitgevoerd
    print("tests en cours...")
    unittest.main()
  • De laag [dao] stuurt een verzoek naar de server, ontvangt het antwoord en formatteert dit om het terug te sturen naar de aanroepende code. Wanneer de server een antwoord stuurt met een statuscode anders dan 200, genereert de laag [dao] een uitzondering. Een aantal tests is er dan ook op gericht om na te gaan of er al dan niet een uitzondering is opgetreden;
  • regels 9-18: er wordt een sessie jSON geïnitialiseerd. Er mag geen fout optreden;
  • regels 20-29: er wordt een sessie XML geïnitialiseerd. Er mag geen fout optreden;
  • regels 31-40: er wordt een sessie met een onjuist type geïnitialiseerd. Er moet een fout optreden;
  • regels 42-54: er wordt geauthenticeerd met de juiste inloggegevens. Er mag geen fout optreden;
  • regels 56-68: we authenticeren ons met onjuiste inloggegevens. Er moet een fout optreden;
  • regels 70-92: er wordt een belastingberekening uitgevoerd en vervolgens wordt de lijst met simulaties opgevraagd. Er moet er één zijn. Bovendien wordt gecontroleerd of deze simulatie inderdaad de gevraagde belasting bevat;
  • regels 94-119: er wordt een simulatie gemaakt die vervolgens wordt verwijderd. Daarna wordt geprobeerd een simulatie te verwijderen terwijl er geen meer is. Er moet een foutmelding verschijnen;
  • regels 121-137: de test wordt gestart als een klassiek consolescript;
  • regels 122-124: we configureren de applicatie;
  • regels 126-129: we configureren de logger. Zo kunnen we de logs volgen;
  • regels 131-133: we instantiëren de laag [dao] die getest gaat worden;
  • regels 135-137: de tests worden gestart;

De console-uitvoer is als volgt:


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