Skip to content

25. Anwendungsübung: Version 8

25.1. Einführung

Wir werden eine neue Client/Server-Anwendung schreiben. Die neue Funktion des Servers besteht darin, dass er eine Sitzung verwaltet. Anstatt die Steuerverwaltungsdaten in einem Objekt mit [Anwendungs-]Gültigkeitsbereich zu speichern, werden wir sie in einem Objekt mit [Sitzungs-]Gültigkeitsbereich speichern. Dies führt zu einer Verschlechterung der Code-Leistung. Wenn ein Objekt im schreibgeschützten Modus von allen Benutzern gemeinsam genutzt werden kann, ist es vorzuziehen, es als Objekt im [application]-Scope statt im [session]-Scope zu definieren. Wir gewinnen dadurch zumindest etwas Bandbreite, da dies die Größe des Session-Cookies verringert. Wir möchten jedoch eine Client/Server-Anwendung demonstrieren, bei der Client und Server ein Session-Cookie austauschen.

Die Anwendungsarchitektur bleibt unverändert:

Image

25.2. Der Webserver

Die Verzeichnisstruktur der Server-Skripte sieht wie folgt aus:

Image

Der Ordner [http-servers/03] wird zunächst durch Kopieren des Ordners [http-servers/02] erstellt. Anschließend werden Änderungen vorgenommen.

25.2.1. Die Konfiguration

Sie entspricht der |vorherigen Version| mit einigen Änderungen im Skript [config]:


# dépendances absolues
    absolute_dependencies = [
        # dossiers du projet
        # 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",
        # Constantes, 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",
    ]
  • Zeile 17: Wir werden einen Controller für die [index]-Funktion umschreiben, der die URL / verarbeitet;
  • Zeile 21: Wir verwenden die Dienstprogramme aus der |vorherigen Version|;

25.2.2. Das Hauptskript [main]

Das neue [main]-Skript führt einige Änderungen am Hauptskript [main] aus der vorherigen Version ein:


# l'application Flask peut démarrer
app = Flask(__name__)
# clé secrète de la session
app.secret_key = os.urandom(12).hex()
  • Zeile 4: Wir erstellen einen geheimen Schlüssel für die Anwendung. Wir wissen, dass dies für die Sitzungsverwaltung erforderlich ist;

Als Nächstes werden im [main]-Code keine Steuerdaten mehr abgefragt. Die folgenden Zeilen werden entfernt:

#  data recovery from tax authorities
erreur = False
try:
    #  admindata will be read-only application data
    config["admindata"] = config["layers"]["dao"].get_admindata()
    #  success log
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    #  we note the error
    erreur = True
    #  error log
    log = f"L'erreur suivante s'est produite : {ex}"
    #  console
    print(log)
    #  log file
    logger.write(f"{log}\n")
    #  mail to administrator
    send_adminmail(config, log)

Zusätzlich akzeptiert der [index_controller]-Controller einen weiteren Parameter, die Flask-Sitzung:

1
2
3
4
from flask import request, Flask, session
.        
        #  the request is executed by a controller
        résultat, status_code = index_controller.execute(request, session, config)

25.2.3. Der [index_controller]-Controller

Der [index_controller]-Controller verwaltet nun eine Sitzung:

#  import dependencies
import os
import re
import threading

from flask_api import status
from werkzeug.local import LocalProxy

#  URL set: /?married=xx&children=yy&salary=zz
from AdminData import AdminData
from ImpôtsError import ImpôtsError


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

    #  initially no errors
    erreurs = []
    

    #  mistakes?
    if erreurs:
        #  an error response is returned to the client
        return {"réponse": {"erreurs": erreurs}}, status.HTTP_400_BAD_REQUEST

    #  no mistakes, we can work
    #  retrieve the config associated with the thread
    thread_name = threading.current_thread().name
    logger = config[thread_name]["config"]["logger"]
    #  execute the query
    réponse = None
    try:
        #  the simplest case - admindata is already in session
        if session.get('client_id') is not None:
            #  retrieve session information
            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:
            #  data recovery from tax authorities
            admindata = config["layers"]["dao"].get_admindata()
            #  admindata session
            session['admindata'] = admindata.asdict()
            #  we give the customer a number and put it in the session
            #  this will allow us to track it in the server logs
            client_id = os.urandom(12).hex()
            session['client_id'] = client_id
            #  log
            logger.write(f"[index_controller] client [{client_id}], données fiscales prises dans la couche dao\n")
        #  tAX CALCULATION
        taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
        config["layers"]["métier"].calculate_tax(taxpayer, admindata)
        #  we return the answer to the customer
        return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
    except (BaseException, ImpôtsError) as erreur:
        #  we return the answer to the customer
        return {"réponse": {"erreurs": [f"{erreur}"]}}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • Zeile 14: Der Controller empfängt die aktuelle Sitzung vom Web-Client;
  • Zeilen 35–38: Wenn der Client eine Sitzung hat, enthält diese zwei Schlüssel:
    • [client_id]: eine Client-ID (Zeile 37);
    • [admindata]: Steuerverwaltungsdaten in Form eines Wörterbuchs (Zeile 38);
  • Zeile 35: Wir prüfen, ob die Sitzung einen der beiden erwarteten Schlüssel enthält;
  • Zeilen 42–51: Fall, in dem die Sitzung des Clients noch nicht initialisiert wurde;
    • Zeile 43: Abrufen der Steuerbehörden-Daten aus der [dao]-Schicht;
    • Zeile 45: Diese Daten werden in Form eines Wörterbuchs in die Sitzung eingefügt;
    • Zeile 48: Dem Client wird eine Zufallszahl zugewiesen. Diese Zahl ist für jeden Client unterschiedlich;
    • Zeile 49: Diese Zahl wird in der Sitzung gespeichert;
    • Zeile 51: Wir protokollieren, dass die Daten der Steuerbehörde von der [dao]-Schicht abgerufen wurden. Der Zugriff auf die [dao]-Schicht ist in der Regel aufwendig. Deshalb muss er begrenzt werden. Die Idee dabei ist, die Steuerdaten einmalig aus der [dao]-Schicht abzurufen, sie in der Sitzung zu speichern und sie bei nachfolgenden Anfragen desselben Clients von dort abzurufen. Beachten Sie, dass dies nicht die beste Lösung ist. Da die Steuerdaten der Verwaltung für alle Clients gleich sind, gehören sie in ein Objekt im Anwendungsbereich;
  • Zeilen 35–40: Fall, in dem die Sitzung des Clients bei einer vorherigen Anfrage initialisiert wurde;
    • Zeile 37: Abrufen der Client-ID aus der Sitzung;
    • Zeile 38: Wir rufen die Steuerdaten der Verwaltung aus der Sitzung ab;
    • Zeile 40: Wir protokollieren, dass der Client die Steuerdaten der Verwaltung aus der Sitzung abgerufen hat;

25.3. Der Web-Client

Image

25.3.1. Die [dao]-Schicht

25.3.1.1. Die Klasse [ImpôtsDaoWithHttpSession]

Die [dao]-Schicht wird durch die folgende Klasse [ImpôtsDaoWithHttpSession] implementiert:

#  imports

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

    #  manufacturer
    def __init__(self, config: dict):
        #  parent initialization
        AbstractImpôtsDao.__init__(self, config)
        #  saving configuration items
        #  general configuration
        self.__config = config
        #  server
        self.__config_server = config["server"]
        #  debug mode
        self.__debug = config["debug"]
        #  logger
        self.__logger = None
        #  cookies
        self.__cookies = None

    #  unused method
    def get_admindata(self) -> AdminData:
        pass

    #  tAX CALCULATION
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        #  we let the exceptions rise
        #  get parameters
        params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
        #  connection with Auth Basic authentication?
        if self.__config_server['authBasic']:
            response = requests.get(
                #  URL of the queried server
                self.__config_server['urlServer'],
                #  URL parameters
                params=params,
                #  basic authentication
                auth=(
                    self.__config_server["user"]["login"],
                    self.__config_server["user"]["password"]),
                cookies=self.__cookies)

        else:
            #  connection without Auth Basic authentication
            response = requests.get(self.__config_server['urlServer'], params=params, cookies=self.__cookies)
        #  retrieve response cookies, if any
        if response.cookies:
            self.__cookies = response.cookies
            #  retrieve the session cookie
            session_cookie = response.cookies.get('session')
            #  we decode it to log it
            if session_cookie:
                #  logger
                if not self.__logger:
                    self.__logger = self.__config['logger']
                #  log on
                self.__logger.write(f"cookie de session={decode_flask_session(session_cookie)}\n")

        #  debug mode?
        if self.__debug:
            #  logger
            if not self.__logger:
                self.__logger = self.__config['logger']
            #  log on
            self.__logger.write(f"{response.text}\n")
        #  response status code HTTP
        status_code = response.status_code
        #  we put the response jSON in a dictionary
        résultat = response.json()
        #  error if status code other than 200 OK
        if status_code != status.HTTP_200_OK:
            #  we know that the errors have been associated with the [errors] key in the response
            raise ImpôtsError(87, résultat['réponse']['erreurs'])
        #  we know that the result has been associated with the [result] key in the response
        #  modify the input parameter with this result
        taxpayer.fromdict(résultat["réponse"]["result"])
  • Zeile 30: Die [dao]-Schicht verwaltet ein Wörterbuch mit Cookies;
  • Zeile 58: Die Eigenschaft [response.cookies] ist ein Wörterbuch, das die vom Server in den [Set-Cookie]-HTTP-Headern gesendeten Cookies enthält;
  • Zeile 59: Diese Cookies werden in der [dao]-Schicht gespeichert. Sie werden bei nachfolgenden Anfragen desselben Clients an den Server zurückgesendet;
  • Zeilen 60–68: Obwohl dies nicht unbedingt erforderlich ist, rufen wir das Sitzungscookie ab. Im vom Server gesendeten Cookie-Dictionary ist das Sitzungscookie dem Schlüssel [session] zugeordnet;
  • Zeilen 62–68: Wir entschlüsseln das Session-Cookie, um den Benutzer anzumelden;
  • Zeile 68: Wir werden später auf die Funktion [decode_flask_session] zurückkommen, die das Session-Cookie entschlüsselt;
  • Zeilen 52 und 57: Bei jeder Anfrage desselben Clients werden die vom Server gesendeten Cookies an diesen zurückgesendet. Auf diese Weise wird die Flask-Sitzung zwischen Client und Server aufrechterhalten;

Wir müssen nun bedenken, dass die [dao]-Schicht von mehreren Threads gleichzeitig ausgeführt wird. Wir müssen daher alle Eigenschaften der Klasseninstanz untersuchen und prüfen, ob der gleichzeitige Zugriff auf diese Eigenschaften ein Problem darstellt. Hier haben wir in Zeile 30 die Eigenschaft [self.__cookies] hinzugefügt. Diese Eigenschaft wird in Zeile 59 geändert. Wir haben also Schreibzugriff auf Daten, die von allen Threads gemeinsam genutzt werden. Dieser Zugriff stellt jedoch ein Problem dar: Jeder Thread, der einen bestimmten Client repräsentiert, hat sein eigenes Session-Cookie. Tatsächlich enthält es für jeden Client eine eindeutige Client-ID (=Thread). Wenn wir nichts unternehmen, kann Thread T2 die Cookies von Thread T1 überschreiben.

Wir haben bereits eine Methode gesehen, um dieses Problem zu lösen: Wir können für jeden Thread unterschiedliche Schlüssel in der [config]-Datei erstellen, die als Parameter an den Konstruktor übergeben wird (Zeile 17). Beispielsweise können wir den Thread-Namen als Schlüssel verwenden:

  • In Zeile 59 könnten wir schreiben:

config[thread_name][‘cookies’]=cookies
  • In Zeile 52 könnten wir dann schreiben:

cookies=config[thread_name][‘cookies’]

Hier verwenden wir eine andere Technik: Jeder Thread (=Client) verfügt über eine eigene [dao]-Schicht. Auf diese Weise ist Zeile 59 kein Problem mehr, da die verwendeten Cookies die eines einzelnen Clients sind.

Dazu erstellen wir eine neue Klasse [ImpôtsDaoWithHttpSessionFactory].

25.3.1.2. Die Flask-Funktion zur Dekodierung der Sitzung

Die Funktion [decode_flask_session] ist im Skript [myutils] definiert:

Image

Wir haben das Skript |myutils| bereits behandelt. Dieses Skript ist ein Modul im Maschinenbereich, das die verschiedenen Skripte in diesem Kurs mit der Anweisung importieren können:

import myutils

Darin definieren wir die Funktion [decode_flask_session] wie folgt:

def decode_flask_session(cookie: str) -> str:
    #  source : 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")
  • Zeile 2: Die URL, unter der ich diese Funktion gefunden habe;
  • Zeile 1: Der Parameter [cookie] ist die Zeichenkette, die dem Schlüssel [session] im Cookie-Wörterbuch eines Web-Clients zugeordnet ist;
  • Zeilen 3–16: Ich werde diesen Code nicht kommentieren, da ich ihn nicht vollständig verstehe;

Wir fügen der Datei [__init__.py] einen neuen Import hinzu:


from .myutils import set_syspath, json_response, decode_flask_session

Die neue Version von [myutils] wird mit dem Befehl [pip install .] in einem PyCharm-Terminal als systemweites Modul installiert:


(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
  • Zeile 1: Sie müssen sich im Ordner [packages] befinden, um diesen Befehl einzugeben;

25.3.1.3. Die Klasse [ImpôtsDaoWithHttpSessionFactory]

Die Klasse [ImpôtsDaoWithHttpSessionFactory] sieht wie folgt aus:

from ImpôtsDaoWithHttpSession import ImpôtsDaoWithHttpSession


class ImpôtsDaoWithHttpSessionFactory:

    def __init__(self, config: dict):
        #  the parameter
        self.__config = config

    def new_instance(self):
        #  render an instance of the [dao] layer
        return ImpôtsDaoWithHttpSession(self.__config)
  • Die Klasse [ImpôtsDaoWithHttpSessionFactory] ermöglicht es uns, mithilfe der Methode [new_instance] in den Zeilen 10–12 eine neue Implementierung der [dao]-Schicht zu erstellen;

25.3.2. Konfiguration

Das Skript [config_layers], das die Web-Client-Schichten konfiguriert, wird wie folgt geändert:

def configure(config: dict) -> dict:
    #  instantiation of applicatuon layers

    #  dao layer
    from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
    dao_factory = ImpôtsDaoWithHttpSessionFactory(config)

    #  make the layer configuration
    return {
        "dao_factory": dao_factory
    }
  • Zeilen 5–6: Anstatt wie zuvor eine einzelne [DAO]-Schicht zu instanziieren, instanziieren wir eine „Factory“ für diese Schicht (Factory = Objekt-Produktionsfabrik, in diesem Fall die [DAO]-Schicht);
  • Zeilen 9–11: Wir geben die Layer-Konfiguration zurück;

25.3.3. Das Hauptskript des Clients

Das [main]-Skript hat sich im Vergleich zur vorherigen Version wie folgt geändert:

#  configure the application

import config
config = config.configure({})

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


#  executing the [dao] layer in a thread
#  taxpayers is a list of taxpayers
def thread_function(thread_dao, logger, taxpayers: list):
    


#  list of client threads
threads = []
logger = None
#  code
try:
    
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        
        #  each thread must have its own [dao] layer to properly manage its session cookie
        thread_dao = dao_factory.new_instance()
        #  create the thread
        thread = threading.Thread(target=thread_function, args=(thread_dao, logger, thread_taxpayers))
        #  we add it to the list of threads in the main script
        threads.append(thread)
        #  we launch the thread - this operation is asynchronous - we don't wait for the thread's result
        thread.start()
    #  the main thread waits for all threads it has launched to finish
    
except BaseException as erreur:
    #  error display
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  close the logger
    if logger:
        logger.close()
    #  we're done
    print("Travail terminé...")
    #  end of threads that might still exist if we stopped on error
    sys.exit()
  • Zeilen 29–30: Jeder Thread hat seine eigene [dao]-Schicht;

25.3.4. Ausführung auf dem Client

Der Webserver wird gestartet, das DBMS wird gestartet, der Mailserver [hMailServer] wird gestartet. Anschließend starten wir das [main]-Skript des Webclients. Die Ausführungsprotokolle in [data/logs/logs.txt] sehen dann wie folgt aus:


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]
  • Es gibt insgesamt 6 Threads, d. h. 6 Clients (Zeilen 1, 3, 4, 6, 8, 9), die gleichzeitig den Steuerberechnungsserver abfragen;
  • wir verfolgen den Thread [Thread-4], der 3 Steuerzahler (Zeile 6) bearbeitet. Er wird drei aufeinanderfolgende Anfragen an den Steuerberechnungsserver stellen;
  • Zeile 10: Die erste Anfrage von [Thread-4];
  • Zeile 13: [Thread-4] hat die Antwort auf seine erste Anfrage erhalten. Darin findet er ein Sitzungscookie, das die Nummer [fa3c83b82761c83e13217967] enthält, die ihm vom Server zugewiesen wurde;
  • Zeile 14: die Steuer für den ersten Steuerzahler;
  • Zeile 16: [Thread-4] stellt eine Anfrage für den zweiten Steuerzahler;
  • Zeile 43: [Thread-4] erhält den Steuerbetrag für den zweiten Steuerzahler;
  • Zeile 45: [Thread-4] stellt eine Anfrage für den dritten Steuerzahler;
  • Zeile 49: [Thread-4] erhält den Steuerbetrag für den dritten Steuerzahler;
  • Zeile 51: [Thread-4] hat seine Arbeit beendet;

Sehen wir uns nun an, wie die drei Anfragen von [Thread-4] auf der Serverseite verarbeitet wurden. Wir können sie in den Serverprotokollen anhand ihrer Client-ID [fa3c83b82761c83e13217967] nachverfolgen.

Die serverseitigen Protokolle [data/logs/logs.txt] lauten wie folgt:


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/?marié=oui&enfants=3&salaire=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/?marié=oui&enfants=2&salaire=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/?marié=non&enfants=0&salaire=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/?marié=oui&enfants=2&salaire=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/?marié=non&enfants=3&salaire=100000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=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/?marié=oui&enfants=3&salaire=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/?marié=non&enfants=2&salaire=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/?marié=oui&enfants=2&salaire=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/?marié=non&enfants=0&salaire=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/?marié=oui&enfants=5&salaire=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}}}
  • Der Client [fa3c83b82761c83e13217967] taucht zum ersten Mal in Zeile 14 auf: Um die Steuer zu berechnen, musste der Server Daten aus der Datenbank der Steuerbehörde abrufen;
  • dann sehen wir den Client [fa3c83b82761c83e13217967] erneut in Zeile 36. Diesmal ruft der Server die Daten der Steuerbehörde aus der Sitzung ab, was ihm einen potenziell kostspieligen Zugriff auf die [DAO]-Schicht erspart;
  • Wir begegnen dem Client [fa3c83b82761c83e13217967] ein drittes Mal in Zeile 42, wo der Server erneut die Sitzung des Clients nutzt;

Dieses Beispiel veranschaulicht deutlich den Nutzen der Sitzung für einen Client: Sie speichert Daten, die von allen Anfragen dieses Clients gemeinsam genutzt werden und deren Abruf mit hohem Aufwand verbunden ist.

Auf der Client-Seite sind die Ergebnisse in der Datei [data/output/results.json] dieselben wie in früheren Versionen.

25.4. Testen der [DAO]-Schicht

Wie in den |vorherigen Versionen| testen wir die [dao]-Schicht des Clients:

Image

Die Testklasse wird in der folgenden Umgebung ausgeführt:

Image

  • Die Konfiguration [2] ist identisch mit der Konfiguration [1], die wir gerade untersucht haben;

Die Testklasse [TestHttpClientDao] sieht wie folgt aus:

import unittest

from Logger import Logger


class TestHttpClientDao(unittest.TestCase):

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

        #  { 'married': 'yes', 'children': 2, 'salary': 55555,
        #  tax': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        #  check
        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__':
    #  configure the application
    import config
    config = config.configure({})

    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger
    #  retrieve the [dao] layer factory
    dao_factory = config["layers"]["dao_factory"]
    #  create an instance of the [dao] layer
    dao = dao_factory.new_instance()

    #  test methods are executed
    print("tests en cours...")
    unittest.main()
  • Wir erstellen eine |Ausführungskonfiguration| für diesen Test;
  • Wir starten den Webserver mit seiner gesamten Umgebung;
  • führen den Test aus;

Die Ergebnisse lauten wie folgt:


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