Skip to content

25. Toepassingsopdracht: versie 8

25.1. Inleiding

We gaan een nieuwe client/server-toepassing schrijven. Het nieuwe aan de server is dat deze een sessie gaat beheren. In plaats van de gegevens van de belastingdienst in een object met bereik [application] te plaatsen, gaan we ze in een object met bereik [session] plaatsen. Hierdoor gaat de prestatie van de code achteruit. Wanneer een object door alle gebruikers op alleen-lezenbasis kan worden gedeeld, verdient het de voorkeur om er een object met bereik [application] van te maken in plaats van een object met bereik [session]. Dit levert op zijn minst een besparing op de bandbreedte op, aangezien de grootte van de sessiecookie hierdoor wordt verkleind. Maar we willen een client/server-toepassing laten zien waarbij de client en de server een sessiecookie uitwisselen.

De architectuur van de applicatie verandert niet:

Image

25.2. De webserver

De structuur van de scripts op de server is als volgt:

Image

De map [http-servers/03] wordt in eerste instantie aangemaakt door de map [http-servers/02] te kopiëren. Vervolgens worden er wijzigingen aangebracht.

25.2.1. De configuratie

Deze is hetzelfde als in de |vorige versie|, met enkele wijzigingen in het script [config]:


# 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, Tranches
        f"{root_dir}/impots/v05/entities",
        # index_controller
        f"{script_dir}/../controllers",
        # scripts [config_database, config_layers]
        script_dir,
        # Logger, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
    ]
  • regel 17: we gaan een controller herschrijven voor de functie [index] die het bestand URL verwerkt / ;
  • regel 21: we gebruiken de hulpprogramma's van de |vorige versie|;

25.2.2. Het hoofdscript [main]

Het nieuwe script [main] brengt enkele wijzigingen aan in het hoofdscript [main] van de vorige versie:


# de Flask-applicatie kan worden gestart
app = Flask(__name__)
# geheime sessiesleutel
app.secret_key = os.urandom(12).hex()
  • regel 4: er wordt een geheime sleutel voor de applicatie aangemaakt. We weten dat deze nodig is voor het beheer van de sessies;

Vervolgens worden de fiscale gegevens niet meer opgevraagd in de code van [main]. De volgende regels worden verwijderd:


# ophalen van gegevens van de belastingdienst
erreur = False
try:
    # admindata wordt een alleen-lezen gegeven met toepassingsbereik
    config["admindata"] = config["layers"]["dao"].get_admindata()
    # succeslogboek
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    # de fout wordt geregistreerd
    erreur = True
    # foutlogboek
    log = f"L'erreur suivante s'est produite : {ex}"
    # console
    print(log)
    # logbestand
    logger.write(f"{log}\n")
    # e-mail naar de beheerder
    send_adminmail(config, log)

Bovendien accepteert de controller [index_controller] een extra parameter, de Flask-sessie:


from flask import request, Flask, session
….        
         # de aanvraag wordt door een controller uitgevoerd
        résultat, status_code = index_controller.execute(request, session, config)

25.2.3. De controller [index_controller]

De controller [index_controller] ondersteunt nu een sessie:


# afhankelijkheden importeren
import os
import re
import threading

from flask_api import status
from werkzeug.local import LocalProxy

# URL geconfigureerd: /?getrouwd=xx&kinderen=yy&salaris=zz
from AdminData import AdminData
from ImpôtsError import ImpôtsError


def execute(request: LocalProxy, session: LocalProxy, config: dict) -> tuple:
    # afhankelijkheden
    from TaxPayer import TaxPayer

    # in eerste instantie geen fouten
    erreurs = []
    

    # fouten?
    if erreurs:
        # er wordt een foutmelding teruggestuurd naar de klant
        return {"réponse": {"erreurs": erreurs}}, status.HTTP_400_BAD_REQUEST

    # geen fouten, we kunnen aan de slag
    # de configuratie die bij de thread hoort wordt opgehaald
    thread_name = threading.current_thread().name
    logger = config[thread_name]["config"]["logger"]
    # de aanvraag wordt uitgevoerd
    réponse = None
    try:
        # het eenvoudigste geval – admindata is al ingelogd
        if session.get('client_id') is not None:
            # we halen de sessiegegevens op
            client_id = session.get('client_id')
            admindata = AdminData().fromdict(session.get('admindata'))
            # log
            logger.write(f"[index_controller] client [{client_id}], données fiscales prises en session\n")
        else:
            # ophalen van gegevens van de belastingdienst
            admindata = config["layers"]["dao"].get_admindata()
            # admindata wordt aangemeld
            session['admindata'] = admindata.asdict()
            # we kennen een nummer toe aan de klant en voegen dit nummer toe aan de sessie
            # hierdoor kunnen we de klant volgen in de serverlogs
            client_id = os.urandom(12).hex()
            session['client_id'] = client_id
            # logboek
            logger.write(f"[index_controller] client [{client_id}], données fiscales prises dans la couche dao\n")
        # berekening van de belasting
        taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
        config["layers"]["métier"].calculate_tax(taxpayer, admindata)
        # we sturen het antwoord terug naar de klant
        return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
    except (BaseException, ImpôtsError) as erreur:
        # we sturen het antwoord naar de klant
        return {"réponse": {"erreurs": [f"{erreur}"]}}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • regel 14: de controller ontvangt de huidige sessie van de webclient;
  • regels 35-38: als de client een sessie heeft, dan bevat deze twee sleutels:
    • [client_id]: een klantnummer (regel 37);
    • [admindata]: de gegevens van de belastingdienst in de vorm van een woordenboek (regel 38);
  • regel 35: er wordt gecontroleerd of de sessie een van de twee verwachte sleutels bevat;
  • regels 42-51: het geval waarin de sessie van de klant nog niet is geïnitialiseerd;
    • regel 43: de gegevens van de belastingdienst worden opgehaald uit de laag [dao];
    • regel 45: deze gegevens worden in de sessie opgeslagen in de vorm van een woordenboek;
    • regel 48: er wordt een willekeurig nummer aan de klant toegewezen. Dit nummer is voor elke klant anders;
    • regel 49: dit nummer wordt in de sessie opgeslagen;
    • regel 51: er wordt gelogd dat de gegevens van de belastingdienst zijn verkregen via de laag [dao]. Toegang tot de laag [dao] is doorgaans kostbaar. Daarom moet deze worden beperkt. Het idee hier is om de belastinggegevens eenmalig op te halen uit de laag [dao], deze in de sessie op te slaan en ze daar op te halen bij latere verzoeken van dezelfde klant. We wijzen erop dat dit niet de beste oplossing is. Aangezien de belastinggegevens van de belastingdienst voor alle klanten hetzelfde zijn, horen ze thuis in een object met applicatiebereik;
  • regels 35-40: voor het geval de sessie van de klant bij een eerdere verzoek is geïnitialiseerd;
    • regel 37: het klantnummer wordt uit de sessie opgehaald;
    • regel 38: de fiscale gegevens van de overheid worden uit de sessie opgehaald;
    • regel 40: er wordt gelogd dat de klant de fiscale gegevens van de overheid in de sessie heeft ontvangen;

25.3. De webclient

Image

25.3.1. De laag [dao]

25.3.1.1. De klasse [ImpôtsDaoWithHttpSession]

De laag [dao] wordt geïmplementeerd door de volgende klasse [ImpôtsDaoWithHttpSession]:


# import

import requests
from flask_api import status
from myutils import decode_flask_session

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsMétier import InterfaceImpôtsMétier
from TaxPayer import TaxPayer


class ImpôtsDaoWithHttpSession(AbstractImpôtsDao, InterfaceImpôtsMétier):

    # constructor
    def __init__(self, config: dict):
        # initialisatie van bovenliggend element
        AbstractImpôtsDao.__init__(self, config)
        # opslag van configuratie-elementen
        # algemene configuratie
        self.__config = config
        # server
        self.__config_server = config["server"]
        # debugmodus
        self.__debug = config["debug"]
        # logger
        self.__logger = None
        # cookies
        self.__cookies = None

    # ongebruikte methode
    def get_admindata(self) -> AdminData:
        pass

    # belastingberekening
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        # uitzonderingen doorgeven
        # GET-parameters
        params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
        # verbinding met Auth Basic-authenticatie?
        if self.__config_server['authBasic']:
            response = requests.get(
                # URL van de opgevraagde server
                self.__config_server['urlServer'],
                # parameters van de URL
                params=params,
                # Basic-authenticatie
                auth=(
                    self.__config_server["user"]["login"],
                    self.__config_server["user"]["password"]),
                cookies=self.__cookies)

        else:
            # verbinding zonder Basic-authenticatie
            response = requests.get(self.__config_server['urlServer'], params=params, cookies=self.__cookies)
        # de cookies uit het antwoord worden opgehaald, indien aanwezig
        if response.cookies:
            self.__cookies = response.cookies
            # de sessiecookie wordt opgehaald
            session_cookie = response.cookies.get('session')
            # deze wordt gedecodeerd om in te loggen
            if session_cookie:
                # inlogfunctie
                if not self.__logger:
                    self.__logger = self.__config['logger']
                # er wordt ingelogd
                self.__logger.write(f"cookie de session={decode_flask_session(session_cookie)}\n")

        # debugmodus?
        if self.__debug:
            # logboek
            if not self.__logger:
                self.__logger = self.__config['logger']
            # loggen
            self.__logger.write(f"{response.text}\n")
        # statuscode van het antwoord HTTP
        status_code = response.status_code
        # het antwoord jSON wordt in een woordenboek opgeslagen
        résultat = response.json()
        # fout als de statuscode niet 200 is OK
        if status_code != status.HTTP_200_OK:
            # we weten dat de fouten zijn gekoppeld aan de sleutel [erreurs] van het antwoord
            raise ImpôtsError(87, résultat['réponse']['erreurs'])
        # het is bekend dat het resultaat is gekoppeld aan de sleutel [result] van het antwoord
        # de invoerparameter wordt aangepast met dit resultaat
        taxpayer.fromdict(résultat["réponse"]["result"])
  • regel 30: de laag [dao] beheert een woordenboek met cookies;
  • regel 58: de eigenschap [response.cookies] is een woordenboek waarin de cookies worden verzameld die door de server in de headers HTTP en [Set-Cookie] worden verzonden;
  • regel 59: deze cookies worden opgeslagen in de laag [dao]. Ze worden bij volgende verzoeken van dezelfde client teruggestuurd naar de server;
  • regels 60-68: hoewel dit niet noodzakelijk is, halen we de sessiecookie op. In het woordenboek met door de server verzonden cookies is de sessiecookie gekoppeld aan de sleutel [session];
  • regels 62-68: we decoderen de sessiecookie om de gebruiker in te loggen;
  • regel 68: we komen later terug op de functie [decode_flask_session] die het sessiecookie decodeert;
  • regels 52 en 57: bij elk verzoek van dezelfde client worden de door de server verzonden cookies naar de client teruggestuurd. Op deze manier wordt de Flask-sessie tussen de client en de server in stand gehouden;

We moeten er nu rekening mee houden dat de laag [dao] gelijktijdig door meerdere threads zal worden uitgevoerd. We moeten dus alle eigenschappen van de klasse-instantie bekijken en nagaan of gelijktijdige toegang tot deze eigenschappen problemen oplevert. Hier hebben we de eigenschap [self.__cookies] toegevoegd, op regel 30. Deze eigenschap wordt gewijzigd op regel 59. We hebben dus schrijftoegang tot gegevens die door alle threads worden gedeeld. Deze toegang levert echter een probleem op: elke thread die een bepaalde client vertegenwoordigt, heeft zijn eigen sessiecookie. Daarin staat namelijk een uniek klantnummer (=thread) voor elke klant. Als we niets doen, kan de thread T2 de cookies van de thread T1 overschrijven.

We hebben al een methode gezien om dit probleem op te lossen: we kunnen in het bestand [config], dat als parameter aan de constructor wordt doorgegeven (regel 17), voor elke thread verschillende sleutels aanmaken. We kunnen bijvoorbeeld de naam van de thread als sleutel gebruiken:

  • in regel 59 zou je dan kunnen schrijven:

config[thread_name][‘cookies’]=cookies
  • op regel 52 zou je dan kunnen schrijven:

cookies=config[thread_name][‘cookies’]

We gaan hier een andere techniek gebruiken: elke thread (=client) krijgt zijn eigen laag [dao]. Zo vormt regel 59 geen probleem meer, omdat de gebruikte cookies dan die van één enkele client zijn.

Hiervoor gaan we een nieuwe klasse [ImpôtsDaoWithHttpSessionFactory] aanmaken.

25.3.1.2. De decoderingsfunctie voor de Flask-sessie

De functie [decode_flask_session] is gedefinieerd in het script [myutils]:

Image

We hebben het script |myutils| al besproken. Dit script is een module met machinebereik die de verschillende scripts in deze cursus kunnen importeren met de instructie:

import myutils

Daarin wordt de functie [decode_flask_session] als volgt gedefinieerd:


def decode_flask_session(cookie: str) -> str:
    # bron: https://www.kirsle.net/wizards/flask-session.cgi
    compressed = False
    payload = cookie

    if payload.startswith('.'):
        compressed = True
        payload = payload[1:]

    data = payload.split(".")[0]

    data = base64_decode(data)
    if compressed:
        data = zlib.decompress(data)

    return data.decode("utf-8")
  • regel 2: de URL waarin ik deze functie heb gevonden;
  • regel 1: de parameter [cookie] is de tekenreeks die gekoppeld is aan de sleutel [session] in het woordenboek van cookies die door een webclient zijn ontvangen;
  • regels 3-16: ik zal geen commentaar geven op deze code, omdat ik er geen verstand van heb;

We voegen een nieuwe import toe aan het bestand [__init__.py]:


from .myutils import set_syspath, json_response, decode_flask_session

De nieuwe versie van [myutils] wordt geïnstalleerd onder de modules op machineniveau met het commando [pip install .] in een Pycharm-terminal:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install .
Processing c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\packages
Using legacy setup.py install for myutils, since package 'wheel' is not installed.
Installing collected packages: myutils
  Attempting uninstall: myutils
    Found existing installation: myutils 0.1
    Uninstalling myutils-0.1:
      Successfully uninstalled myutils-0.1
    Running setup.py install for myutils ... done
Successfully installed myutils-0.1
  • regel 1: je moet in de map [packages] zijn om deze instructie in te voeren;

25.3.1.3. De klasse [ImpôtsDaoWithHttpSessionFactory]

De klasse [ImpôtsDaoWithHttpSessionFactory] ziet er als volgt uit:


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 [dao] geretourneerd
        return ImpôtsDaoWithHttpSession(self.__config)
  • Met de klasse [ImpôtsDaoWithHttpSessionFactory] kun je een nieuwe implementatie van de laag [dao] maken met de methode [new_instance] uit de regels 10-12;

25.3.2. De configuratie

Het script [config_layers], dat de lagen van de webclient configureert, wordt als volgt gewijzigd:


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

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

    # we voeren de configuratie van de lagen uit
    return {
        "dao_factory": dao_factory
    }
  • regels 5-6: in plaats van één enkele laag [dao] te instantiëren zoals voorheen, wordt een ‘factory’ van deze laag geïnstantieerd (factory = objectfabriek, in dit geval de laag [dao]);
  • regels 9-11: de configuratie van de lagen wordt teruggegeven;

25.3.3. Het hoofdscript van de client

Het script [main] is ten opzichte van de vorige versie als volgt gewijzigd:


# de applicatie configureren

import config
config = config.configure({})

# 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(thread_dao, logger, taxpayers: list):
    


# lijst met threads van de client
threads = []
logger = None
# code
try:
    
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        
        # elke thread moet zijn eigen laag [dao] hebben om zijn sessiecookie correct te beheren
        thread_dao = dao_factory.new_instance()
        # we maken de thread aan
        thread = threading.Thread(target=thread_function, args=(thread_dao, logger, thread_taxpayers))
        # we voegen deze toe aan de lijst met threads van het hoofdscript
        threads.append(thread)
        # de thread wordt gestart – dit is een asynchrone bewerking – er wordt niet gewacht op het resultaat van de thread
        thread.start()
    # de hoofdthread wacht tot alle door hem gestarte threads zijn voltooid
    
except BaseException as erreur:
    # de fout wordt weergegeven
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # de logger wordt gesloten
    if logger:
        logger.close()
    # we zijn klaar
    print("Travail terminé...")
    # einde van de threads die nog zouden kunnen bestaan als we bij een fout zijn gestopt
    sys.exit()
  • regels 29-30: elke thread heeft zijn eigen laag [dao];

25.3.4. Uitvoering van de client

De webserver wordt gestart, SGBD wordt gestart, de e-mailserver [hMailServer] wordt gestart. Vervolgens wordt het script [main] van de webclient gestart. De uitvoeringslogs in [data/logs/logs.txt] zien er dan als volgt uit:


2020-07-25 10:21:46.478511, Thread-1 : début du thread [Thread-1] avec 1 contribuable(s)
2020-07-25 10:21:46.479111, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-25 10:21:46.479111, Thread-2 : début du thread [Thread-2] avec 1 contribuable(s)
2020-07-25 10:21:46.480195, Thread-3 : début du thread [Thread-3] avec 2 contribuable(s)
2020-07-25 10:21:46.480195, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-25 10:21:46.481137, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
2020-07-25 10:21:46.481137, Thread-3 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-25 10:21:46.482279, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
2020-07-25 10:21:46.482622, Thread-6 : début du thread [Thread-6] avec 1 contribuable(s)
2020-07-25 10:21:46.482622, Thread-4 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-5 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-6 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-25 10:21:46.725910, Thread-4 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"fa3c83b82761c83e13217967"}
2020-07-25 10:21:46.725910, Thread-4 : {"réponse": {"result": {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:46.725910, Thread-4 : fin du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:46.726960, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:47.514108, Thread-3 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,24999.5],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"700e3f5dc808c7c48f0c9007"}
2020-07-25 10:21:47.514610, Thread-3 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}}}
2020-07-25 10:21:47.514939, Thread-3 : fin du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}
2020-07-25 10:21:47.514939, Thread-3 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000}
2020-07-25 10:21:47.527211, Thread-5 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"9e14a5d4a3057f69ab95ab2d"}
2020-07-25 10:21:47.527211, Thread-2 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,22500.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"a06e8fd70a44c9e311f4dce0"}
2020-07-25 10:21:47.527211, Thread-5 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.527211, Thread-1 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"28c38df998f67685b3a482b8"}
2020-07-25 10:21:47.527211, Thread-2 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}}}
2020-07-25 10:21:47.528341, Thread-5 : fin du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.528341, Thread-1 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.528842, Thread-2 : fin du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}
2020-07-25 10:21:47.529349, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000}
2020-07-25 10:21:47.529699, Thread-1 : fin du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.529699, Thread-2 : fin du thread [Thread-2]
2020-07-25 10:21:47.531905, Thread-1 : fin du thread [Thread-1]
2020-07-25 10:21:47.536121, Thread-6 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,93749.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"38499b63076516c02f2770ec"}
2020-07-25 10:21:47.537161, Thread-3 : {"réponse": {"result": {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.537161, Thread-6 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.538156, Thread-3 : fin du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.538557, Thread-6 : fin du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.538828, Thread-3 : fin du thread [Thread-3]
2020-07-25 10:21:47.538828, Thread-6 : fin du thread [Thread-6]
2020-07-25 10:21:47.546198, Thread-5 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.546198, Thread-5 : fin du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.546198, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000}
2020-07-25 10:21:47.739643, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.739643, Thread-4 : fin du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.740668, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000}
2020-07-25 10:21:48.557469, Thread-5 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:48.558715, Thread-5 : fin du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}
2020-07-25 10:21:48.558715, Thread-5 : fin du thread [Thread-5]
2020-07-25 10:21:48.753025, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:48.753318, Thread-4 : fin du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-25 10:21:48.753540, Thread-4 : fin du thread [Thread-4]
  • er zijn in totaal 6 threads, dus 6 klanten (regels 1, 3, 4, 6, 8, 9) die tegelijkertijd de belastingberekeningsserver opvragen;
  • we volgen de thread [Thread-4], die 3 belastingplichtigen beheert (regel 6). Deze thread voert achtereenvolgens drie verzoeken uit naar de server voor belastingberekening;
  • regel 10: het eerste verzoek van [Thread-4];
  • regel 13: [Thread-4] heeft het antwoord op zijn eerste verzoek ontvangen. Daarin vindt hij een sessiecookie met het nummer [fa3c83b82761c83e13217967] dat de server hem heeft toegewezen;
  • regel 14: de belasting van de eerste belastingplichtige;
  • regel 16: [Thread-4] doet een verzoek voor de tweede belastingplichtige;
  • regel 43: [Thread-4] ontvangt de belasting van de tweede belastingplichtige;
  • regel 45: [Thread-4] doet een aanvraag voor de derde belastingplichtige;
  • regel 49: [Thread-4] ontvangt de belasting van de derde belastingplichtige;
  • regel 51: [Thread-4] heeft zijn taak voltooid;

Laten we nu eens kijken hoe de drie verzoeken van [Thread-4] aan de serverzijde zijn verwerkt. We kunnen dit volgen in de serverlogs aan de hand van zijn klantnummer [fa3c83b82761c83e13217967].

De serverlogs van [data/logs/logs.txt] zijn als volgt:


2020-07-25 10:21:39.187366, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:40.439093, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:46.502011, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=ja&kinderen=3&salaris=50000' [GET]>
2020-07-25 10:21:46.504049, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.505452, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=ja&kinderen=2&salaris=55555' [GET]>
2020-07-25 10:21:46.506257, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.507292, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=nee&kinderen=0&salaris=100000' [GET]>
2020-07-25 10:21:46.507292, Thread-4 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.508301, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=ja&kinderen=2&salaris=50000' [GET]>
2020-07-25 10:21:46.509293, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.511808, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=nee&kinderen=3&salaris=100000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=ja&kinderen=3&salaris=200000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.719504, Thread-6 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises dans la couche dao
2020-07-25 10:21:46.720003, Thread-6 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 3, 'salaire': 100000, 'impôt': 16782, 'surcôte': 7176, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:46.736108, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=ja&kinderen=3&salaris=100000' [GET]>
2020-07-25 10:21:46.736108, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.506709, Thread-2 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises dans la couche dao
2020-07-25 10:21:47.507216, Thread-2 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 50000, 'impôt': 0, 'surcôte': 0, 'taux': 0.14, 'décôte': 720, 'réduction': 0}}}
2020-07-25 10:21:47.507216, Thread-3 : [index_controller] client [28c38df998f67685b3a482b8], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508442, Thread-4 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508940, Thread-3 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 55555, 'impôt': 2814, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.510506, Thread-4 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0, 'salaire': 100000, 'impôt': 22986, 'surcôte': 0, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.511513, Thread-5 : [index_controller] client [a06e8fd70a44c9e311f4dce0], données fiscales prises dans la couche dao
2020-07-25 10:21:47.514939, Thread-5 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'taux': 0.14, 'décôte': 384, 'réduction': 347}}}
2020-07-25 10:21:47.520727, Thread-7 : [index_controller] client [38499b63076516c02f2770ec], données fiscales prises dans la couche dao
2020-07-25 10:21:47.523162, Thread-7 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.530835, Thread-9 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=nee&kinderen=2&salaris=100000' [GET]>
2020-07-25 10:21:47.531736, Thread-9 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises en session
2020-07-25 10:21:47.531905, Thread-9 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.541899, Thread-10 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=ja&kinderen=2&salaris=30000' [GET]>
2020-07-25 10:21:47.542488, Thread-10 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:47.542488, Thread-10 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'taux': 0.0, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.553628, Thread-11 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=nee&kinderen=0&salaris=200000' [GET]>
2020-07-25 10:21:47.553628, Thread-11 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.736910, Thread-8 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:47.737191, Thread-8 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'taux': 0.3, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.748226, Thread-12 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=ja&kinderen=5&salaris=100000' [GET]>
2020-07-25 10:21:47.748226, Thread-12 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:48.554695, Thread-11 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:48.555070, Thread-11 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0, 'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'taux': 0.45, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:48.748753, Thread-12 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:48.748753, Thread-12 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
  • de klant [fa3c83b82761c83e13217967] komt voor het eerst voor op regel 14: om de belasting te berekenen, moest de server de gegevens van de belastingdienst uit de database ophalen;
  • vervolgens zien we de client [fa3c83b82761c83e13217967] terug in regel 36. Deze keer vindt de server de gegevens van de belastingdienst in de sessie, waardoor hij een mogelijk kostbare toegang tot de laag [dao] kan vermijden;
  • de client [fa3c83b82761c83e13217967] komt voor de derde keer voor op regel 42, waar de server opnieuw gebruikmaakt van de sessie van de client;

Dit voorbeeld illustreert duidelijk het nut van de sessie voor een client: hierin worden gegevens opgeslagen die door alle verzoeken van deze client worden gedeeld en die kostbaar zijn om te verkrijgen.

Aan de kant van de client zijn de resultaten in het bestand [data/output/résultats.json] dezelfde als bij de vorige versies.

25.4. Tests van de laag [dao]

Net zoals we in de |vorige versies| hebben gedaan, testen we de clientlaag [dao]:

Image

De testklasse wordt uitgevoerd in de volgende omgeving:

Image

  • de configuratie [2] is identiek aan de configuratie [1] die we zojuist hebben bekeken;

De testklasse [TestHttpClientDao] is als volgt:


import unittest

from Logger import Logger


class TestHttpClientDao(unittest.TestCase):

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

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



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

    # loggen
    logger = Logger(config["logsFilename"])
    # opslaan in de configuratie
    config["logger"] = logger
    # de factory van de laag [dao] wordt opgehaald
    dao_factory = config["layers"]["dao_factory"]
    # er wordt een instantie van de laag [dao] aangemaakt
    dao = dao_factory.new_instance()

    # we voeren de testmethoden uit
    print("tests en cours...")
    unittest.main()
  • we maken een |uitvoeringsconfiguratie| aan voor deze test;
  • we starten de webserver met de volledige omgeving;
  • we voeren de test uit;

De resultaten zijn 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/03/tests/TestHttpClientDao.py
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 3.392s

OK

Process finished with exit code 0