Skip to content

25. Anwendungsaufgabe: Version 8

25.1. Einleitung

Wir werden eine neue Client-Server-Anwendung schreiben. Die Neuerung beim Server besteht darin, dass er eine Sitzung verwalten wird. Anstatt die Daten der Steuerverwaltung in ein Objekt mit dem Geltungsbereich [application] zu speichern, werden wir sie in ein Objekt mit dem Geltungsbereich [session] speichern. Dadurch verschlechtert sich die Leistung des Codes. Wenn ein Objekt von allen Benutzern schreibgeschützt gemeinsam genutzt werden kann, ist es besser, es als Objekt mit dem Gültigkeitsbereich [application] statt mit dem Gültigkeitsbereich [session] zu definieren. Man spart zumindest Bandbreite, da dadurch die Größe des Session-Cookies verringert wird. Wir möchten jedoch eine Client-Server-Anwendung zeigen, bei der Client und Server ein Session-Cookie austauschen.

Die Architektur der Anwendung ändert sich nicht:

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


# absolute Abhängigkeiten
    absolute_dependencies = [
        # Projektordner
        # 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",
        # Konstanten, Tranchen
        f"{root_dir}/impots/v05/entities",
        # index_controller
        f"{script_dir}/../controllers",
        # Skripte [config_database, config_layers]
        script_dir,
        # Logger, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
    ]
  • Zeile 17: Wir schreiben einen Controller für die Funktion [index] neu, die die Datei URL verarbeitet;
  • Zeile 21: Wir verwenden die Hilfsprogramme der |vorherigen Version|;

25.2.2. Das Hauptskript [main]

Das neue Skript [main] enthält einige Änderungen gegenüber dem Hauptskript [main] der vorherigen Version:


# Die Flask-Anwendung kann gestartet werden
app = Flask(__name__)
# Geheimer Sitzungsschlüssel
app.secret_key = os.urandom(12).hex()
  • Zeile 4: Es wird ein geheimer Schlüssel für die Anwendung erstellt. Dieser ist bekanntlich für die Verwaltung der Sitzungen erforderlich;

Anschließend werden die Steuerdaten im Code von [main] nicht mehr abgefragt. Die folgenden Zeilen werden entfernt:


# Abruf der Daten der Steuerbehörde
erreur = False
try:
    # „admindata“ wird ein schreibgeschützter Datensatz im Anwendungsbereich sein
    config["admindata"] = config["layers"]["dao"].get_admindata()
    # Erfolgsprotokoll
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    # Fehler wird protokolliert
    erreur = True
    # Fehlerprotokoll
    log = f"L'erreur suivante s'est produite : {ex}"
    # Konsole
    print(log)
    # Protokolldatei
    logger.write(f"{log}\n")
    # E-Mail an den Administrator
    send_adminmail(config, log)

Außerdem akzeptiert der Controller [index_controller] einen zusätzlichen Parameter, die Flask-Sitzung:


from flask import request, Flask, session
….        
         # Die Abfrage wird von einem Controller ausgeführt
        résultat, status_code = index_controller.execute(request, session, config)

25.2.3. Der Controller [index_controller]

Der Controller [index_controller] verwaltet nun eine Sitzung:


# Import der Abhängigkeiten
import os
import re
import threading

from flask_api import status
from werkzeug.local import LocalProxy

# URL mit folgenden Parametern: /?verheiratet=xx&Kinder=yy&Gehalt=zz
from AdminData import AdminData
from ImpôtsError import ImpôtsError


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

    # zunächst keine Fehler
    erreurs = []
    

    # Fehler?
    if erreurs:
        # dem Kunden wird eine Fehlermeldung zurückgegeben
        return {"réponse": {"erreurs": erreurs}}, status.HTTP_400_BAD_REQUEST

    # keine Fehler, es kann weitergearbeitet werden
    # Die zum Thread gehörende Konfiguration wird abgerufen
    thread_name = threading.current_thread().name
    logger = config[thread_name]["config"]["logger"]
    # Die Abfrage wird ausgeführt
    réponse = None
    try:
        # Der einfachste Fall – „admindata“ ist bereits angemeldet
        if session.get('client_id') is not None:
            # Die Informationen der Sitzung werden abgerufen
            client_id = session.get('client_id')
            admindata = AdminData().fromdict(session.get('admindata'))
            # Protokoll
            logger.write(f"[index_controller] client [{client_id}], données fiscales prises en session\n")
        else:
            # Abruf der Daten der Steuerbehörde
            admindata = config["layers"]["dao"].get_admindata()
            # Anmeldung von „admindata“
            session['admindata'] = admindata.asdict()
            # Dem Kunden wird eine Nummer zugewiesen und diese Nummer wird in die Sitzung aufgenommen
            # Dadurch können wir ihn in den Server-Protokollen nachverfolgen
            client_id = os.urandom(12).hex()
            session['client_id'] = client_id
            # Protokoll
            logger.write(f"[index_controller] client [{client_id}], données fiscales prises dans la couche dao\n")
        # Steuerberechnung
        taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
        config["layers"]["métier"].calculate_tax(taxpayer, admindata)
        # Die Antwort wird an den Kunden zurückgesendet
        return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
    except (BaseException, ImpôtsError) as erreur:
        # die Antwort wird an den Kunden zurückgesendet
        return {"réponse": {"erreurs": [f"{erreur}"]}}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • Zeile 14: Der Controller empfängt die aktuelle Sitzung des Web-Clients;
  • Zeilen 35–38: Wenn der Client über eine Sitzung verfügt, enthält diese zwei Schlüssel:
    • [client_id]: eine Kundennummer (Zeile 37);
    • [admindata]: die Daten der Steuerbehörde in Form eines Dictionaries (Zeile 38);
  • Zeile 35: Es wird geprüft, ob die Sitzung einen der beiden erwarteten Schlüssel enthält;
  • Zeilen 42–51: Fall, in dem die Sitzung des Kunden noch nicht initialisiert wurde;
    • Zeile 43: Die Daten der Steuerbehörde werden aus der Ebene [dao] abgerufen;
    • Zeile 45: Diese Daten werden in Form eines Dictionaries in die Sitzung geschrieben;
    • Zeile 48: Dem Kunden wird eine Zufallsnummer zugewiesen. Diese Nummer ist für jeden Kunden unterschiedlich;
    • Zeile 49: Diese Nummer wird in die Sitzung geschrieben;
    • Zeile 51: Es wird protokolliert, dass die Daten der Steuerbehörde über die Schicht [dao] abgerufen wurden. Zugriffe auf die Schicht [dao] sind in der Regel ressourcenintensiv. Deshalb sollten sie begrenzt werden. Die Idee dabei ist, die Steuerdaten einmalig über die Schicht [dao] abzurufen, sie in die Sitzung zu übernehmen und sie bei nachfolgenden Anfragen desselben Kunden von dort abzurufen. Es sei daran erinnert, dass dies nicht die beste Lösung ist. Da die Steuerdaten der Finanzbehörde für alle Kunden gleich sind, gehören sie in ein Objekt mit Anwendungsbereich;
  • Zeilen 35–40: Fall, in dem die Sitzung des Kunden bei einer vorherigen Anfrage initialisiert wurde;
    • Zeile 37: Die Kundennummer wird aus der Sitzung abgerufen;
    • Zeile 38: Die Steuerdaten der Behörde werden aus der Sitzung abgerufen;
    • Zeile 40: Es wird protokolliert, dass der Kunde die Steuerdaten der Behörde aus der Sitzung abgerufen hat;

25.3. Der Web-Client

Image

25.3.1. Die Schicht [dao]

25.3.1.1. Die Klasse [ImpôtsDaoWithHttpSession]

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


# Importe

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

    # Konstruktor
    def __init__(self, config: dict):
        # Initialisierung des übergeordneten Elements
        AbstractImpôtsDao.__init__(self, config)
        # Speicherung der Konfigurationselemente
        # Allgemeine Konfiguration
        self.__config = config
        # Server
        self.__config_server = config["server"]
        # Debug-Modus
        self.__debug = config["debug"]
        # Logger
        self.__logger = None
        # Cookies
        self.__cookies = None

    # nicht verwendete Methode
    def get_admindata(self) -> AdminData:
        pass

    # Steuerberechnung
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        # Ausnahmen werden weitergeleitet
        # GET-Parameter
        params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
        # Verbindung mit Auth-Basic-Authentifizierung?
        if self.__config_server['authBasic']:
            response = requests.get(
                # URL des abgefragten Servers
                self.__config_server['urlServer'],
                # Parameter des URL
                params=params,
                # Basic-Authentifizierung
                auth=(
                    self.__config_server["user"]["login"],
                    self.__config_server["user"]["password"]),
                cookies=self.__cookies)

        else:
            # Verbindung ohne Basic-Authentifizierung
            response = requests.get(self.__config_server['urlServer'], params=params, cookies=self.__cookies)
        # Cookies aus der Antwort werden abgerufen, sofern vorhanden
        if response.cookies:
            self.__cookies = response.cookies
            # Das Sitzungs-Cookie wird abgerufen
            session_cookie = response.cookies.get('session')
            # es wird entschlüsselt, um sich anzumelden
            if session_cookie:
                # Anmeldeprogramm
                if not self.__logger:
                    self.__logger = self.__config['logger']
                # Protokollierung
                self.__logger.write(f"cookie de session={decode_flask_session(session_cookie)}\n")

        # Debug-Modus?
        if self.__debug:
            # Protokollierung
            if not self.__logger:
                self.__logger = self.__config['logger']
            # Protokollierung
            self.__logger.write(f"{response.text}\n")
        # Statuscode der Antwort HTTP
        status_code = response.status_code
        # Die Antwort jSON wird in ein Wörterbuch gespeichert
        résultat = response.json()
        # Fehler, wenn der Statuscode nicht 200 ist OK
        if status_code != status.HTTP_200_OK:
            # Es ist bekannt, dass die Fehler dem Schlüssel [erreurs] der Antwort zugeordnet wurden
            raise ImpôtsError(87, résultat['réponse']['erreurs'])
        # Es ist bekannt, dass das Ergebnis dem Schlüssel [result] der Antwort zugeordnet wurde
        # Der Eingabeparameter wird mit diesem Ergebnis geändert
        taxpayer.fromdict(résultat["réponse"]["result"])
  • Zeile 30: Die Schicht [dao] verwaltet ein Cookie-Wörterbuch;
  • Zeile 58: Die Eigenschaft [response.cookies] ist ein Wörterbuch, das die vom Server in den Headern HTTP und [Set-Cookie] gesendeten Cookies sammelt;
  • Zeile 59: Diese Cookies werden in der Ebene [dao] gespeichert. Sie werden bei späteren Anfragen desselben Clients an den Server zurückgesendet;
  • Zeilen 60–68: Obwohl dies nicht unbedingt erforderlich ist, wird das Sitzungs-Cookie abgerufen. Im Verzeichnis der vom Server gesendeten Cookies ist das Sitzungs-Cookie dem Schlüssel [session] zugeordnet;
  • Zeilen 62–68: Das Sitzungs-Cookie wird entschlüsselt, um den Benutzer anzumelden;
  • Zeile 68: Wir werden später noch auf die Funktion [decode_flask_session] zurückkommen, die das Sitzungs-Cookie entschlüsselt;
  • Zeilen 52 und 57: Bei jeder Anfrage desselben Clients werden die vom Server gesendeten Cookies an ihn zurückgesendet. Auf diese Weise wird die Flask-Sitzung zwischen Client und Server aufrechterhalten;

Man muss nun bedenken, dass die Schicht [dao] gleichzeitig von mehreren Threads ausgeführt wird. Daher müssen alle Eigenschaften der Klasseninstanz überprüft werden, um festzustellen, ob der gleichzeitige Zugriff auf diese Eigenschaften Probleme verursacht. Hier haben wir die Eigenschaft [self.__cookies] in Zeile 30 hinzugefügt. Diese Eigenschaft wird in Zeile 59 geändert. Wir haben also einen Schreibzugriff auf eine Daten, die von allen Threads gemeinsam genutzt wird. Dieser Zugriff ist jedoch problematisch: Jeder Thread, der einen bestimmten Client repräsentiert, hat sein eigenes Sitzungs-Cookie. Darin ist nämlich für jeden Kunden eine eindeutige Kunden-Nr. (=Thread) enthalten. Wenn nichts unternommen wird, kann der Thread „T2“ die Cookies des Threads „T1“ überschreiben.

Wir haben bereits eine Methode zur Lösung dieses Problems kennengelernt: In der Datei „[config]“, die als Parameter an den Konstruktor übergeben wird (Zeile 17), können für jeden Thread unterschiedliche Schlüssel angelegt werden. Als Schlüssel kann beispielsweise der Name des Threads verwendet werden:

  • In Zeile 59 könnte man schreiben:

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

cookies=config[thread_name][‘cookies’]

Wir werden hier eine andere Technik anwenden: Jeder Thread (=Client) erhält seine eigene Ebene [dao]. Somit stellt Zeile 59 kein Problem mehr dar, da die verwendeten Cookies dann die eines einzelnen Clients sind.

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

25.3.1.2. Die Funktion zur Dekodierung der Flask-Sitzung

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

Image

Wir haben das Skript |myutils| bereits behandelt. Dieses Skript ist ein Modul mit maschinenweitem Geltungsbereich, das die verschiedenen Skripte dieses Kurses mit der Anweisung importieren können:

import myutils

Darin wird die Funktion [decode_flask_session] wie folgt definiert:


def decode_flask_session(cookie: str) -> str:
    # Quelle: 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: URL, wo ich diese Funktion gefunden habe;
  • Zeile 1: Der Parameter [cookie] ist die Zeichenkette, die dem Schlüssel [session] im Cookie-Verzeichnis eines Web-Clients zugeordnet ist;
  • Zeilen 3–16: Ich werde diesen Code nicht kommentieren, da ich ihn nicht verstehe;

Wir fügen einen neuen Import in die Datei [__init__.py] 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 unter den Modulen im Maschinenbereich 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: Man muss 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):
        # Der Parameter wird gespeichert
        self.__config = config

    def new_instance(self):
        # eine Instanz der Schicht [dao] wird zurückgegeben
        return ImpôtsDaoWithHttpSession(self.__config)
  • Die Klasse [ImpôtsDaoWithHttpSessionFactory] ermöglicht es, eine neue Implementierung der Schicht [dao] mit der Methode [new_instance] aus den Zeilen 10–12 zu erstellen;

25.3.2. Die Konfiguration

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


def configure(config: dict) -> dict:
    # Instanziierung der Anwendungsschichten

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

    # die Konfiguration der Ebenen
    return {
        "dao_factory": dao_factory
    }
  • Zeilen 5–6: Anstatt wie bisher eine einzelne Schicht [dao] zu instanziieren, wird eine „Factory“ dieser Schicht instanziiert (Factory = Objektfabrik, hier die Schicht [dao]);
  • Zeilen 9–11: Die Konfiguration der Schichten wird zurückgegeben;

25.3.3. Das Hauptskript des Clients

Das Skript [main] hat sich im Vergleich zur vorherigen Version wie folgt weiterentwickelt:


# Konfiguration der Anwendung

import config
config = config.configure({})

# Abhängigkeiten
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger


# Ausführung der Schicht [dao] in einem Thread
# „taxpayers“ ist eine Liste von Steuerzahlern
def thread_function(thread_dao, logger, taxpayers: list):
    


# Liste der Client-Threads
threads = []
logger = None
# Code
try:
    
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        
        # Jeder Thread muss über eine eigene Schicht [dao] verfügen, um sein Sitzungs-Cookie korrekt zu verwalten
        thread_dao = dao_factory.new_instance()
        # Der Thread wird erstellt
        thread = threading.Thread(target=thread_function, args=(thread_dao, logger, thread_taxpayers))
        # man fügt ihn zur Thread-Liste des Hauptskripts hinzu
        threads.append(thread)
        # Der Thread wird gestartet – dieser Vorgang erfolgt asynchron – das Ergebnis des Threads wird nicht abgewartet
        thread.start()
    # Der Haupt-Thread wartet auf das Ende aller von ihm gestarteten Threads
    
except BaseException as erreur:
    # Anzeige des Fehlers
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # Der Logger wird geschlossen
    if logger:
        logger.close()
    # Fertig
    print("Travail terminé...")
    # Beendigung der Threads, die möglicherweise noch bestehen, falls der Vorgang aufgrund eines Fehlers abgebrochen wurde
    sys.exit()
  • Zeilen 29–30: Jeder Thread verfügt über seine eigene Schicht [dao];

25.3.4. Ausführung des Clients

Der Webserver wird gestartet, das Skript SGBD wird gestartet, der Mailserver [hMailServer] wird gestartet. Anschließend wird das Skript [main] des Web-Clients gestartet. Die Ausführungsprotokolle in [data/logs/logs.txt] lauten dann wie folgt:


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, also 6 Clients (Zeilen 1, 3, 4, 6, 8, 9), die gleichzeitig den Steuerberechnungsserver abfragen;
  • Wir werden den Thread [Thread-4] verfolgen, der drei Steuerzahler verwaltet (Zeile 6). Er wird nacheinander drei 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 Sitzungs-Cookie, das die Nummer [fa3c83b82761c83e13217967] enthält, die ihm vom Server zugewiesen wurde;
  • Zeile 14: Die Steuer des ersten Steuerpflichtigen;
  • Zeile 16: [Thread-4] stellt eine Anfrage für den zweiten Steuerzahler;
  • Zeile 43: [Thread-4] erhält die Steuer des zweiten Steuerpflichtigen;
  • Zeile 45: [Thread-4] stellt eine Anfrage für den dritten Steuerpflichtigen;
  • Zeile 49: [Thread-4] erhält die Steuer des dritten Steuerpflichtigen;
  • Zeile 51: [Thread-4] hat seine Arbeit abgeschlossen;

Sehen wir uns nun an, wie die drei Anfragen von [Thread-4] serverseitig verarbeitet wurden. Wir können dies anhand seiner Kundennummer [fa3c83b82761c83e13217967] in den Server-Logs nachverfolgen.

Die serverseitigen Protokolle für [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/?verheiratet=ja&Kinder=3&Gehalt=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/?verheiratet=ja&Kinder=2&Gehalt=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/?verheiratet=nein&Kinder=0&Gehalt=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/?verheiratet=ja&Kinder=2&Gehalt=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/?verheiratet=nein&Kinder=3&Gehalt=100000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?verheiratet=ja&Kinder=3&Gehalt=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/?verheiratet=ja&Kinder=3&Gehalt=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/?verheiratet=nein&Kinder=2&Gehalt=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/?verheiratet=ja&Kinder=2&Gehalt=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/?verheiratet=nein&Kinder=0&Gehalt=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/?verheiratet=ja&Kinder=5&Gehalt=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: Zur Berechnung der Steuer musste der Server die Daten der Steuerbehörde aus der Datenbank abrufen;
  • Anschließend taucht der Client [fa3c83b82761c83e13217967] in Zeile 36 wieder auf. Diesmal findet der Server die Daten der Steuerbehörde bereits in der Sitzung, wodurch er sich einen möglicherweise kostspieligen Zugriff auf die Ebene [dao] erspart;
  • Der Client [fa3c83b82761c83e13217967] taucht ein drittes Mal in Zeile 42 auf, wo der Server erneut die Sitzung des Clients nutzt;

Dieses Beispiel verdeutlicht den Nutzen der Sitzung für einen Client: Dort werden Daten gespeichert, 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/résultats.json] dieselben wie bei den vorherigen Versionen.

25.4. Tests der Schicht [dao]

Wie bereits in den |vorherigen Versionen| testen wir die Client-Schicht [dao]:

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] lautet wie folgt:


import unittest

from Logger import Logger


class TestHttpClientDao(unittest.TestCase):

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

        # {'verheiratet': 'ja', 'Kinder': 2, 'Gehalt': 55555,
        # 'Steuer': 2814, 'Zuschlag': 0, 'Abschlag': 0, 'Ermäßigung': 0, 'Satz': 0,14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        # Überprüfung
        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__':
    # die Anwendung wird konfiguriert
    import config
    config = config.configure({})

    # Protokollierung
    logger = Logger(config["logsFilename"])
    # in der Konfiguration speichern
    config["logger"] = logger
    # die Factory der Schicht [dao] wird abgerufen
    dao_factory = config["layers"]["dao_factory"]
    # eine Instanz der Schicht [dao] wird erstellt
    dao = dao_factory.new_instance()

    # die Testmethoden werden ausgeführt
    print("tests en cours...")
    unittest.main()
  • Wir erstellen eine |Ausführungskonfiguration| für diesen Test;
  • Wir starten den Webserver mit seiner gesamten Umgebung;
  • Wir 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