Skip to content

31. Web-Clients für die Dienste jSON und XML der Version 12

Wir werden drei Konsolen-Client-Anwendungen für die Dienste jSON und XML des soeben erstellten Webservers schreiben. Dabei greifen wir auf die Client-Server-Architektur der Version 11 zurück:

Image

Wir werden drei Konsolenskripte schreiben:

  • Die Skripte [main] und [main3] nutzen die Server-Schicht [métier];
  • das Skript [main2] nutzt die Client-Schicht [métier];

31.1. Die Skriptstruktur der Clients

Der Ordner „[http-clients/07]“ wird zunächst durch Kopieren des Ordners „[http-clients/06]“ erstellt. Anschließend wird er geändert.

Image

  • in [1]: die vom Kunden verarbeiteten oder erstellten Daten;
  • in [2] die Konfiguration und die Konsolenskripte des Kunden;
  • in [3] die Ebene [dao] des Kunden;
  • in [4], den Testordner der Kundenschicht [dao];

31.2. Die Kundenschicht [dao]

Image

Image

31.2.1. Schnittstelle

Die Schicht [dao] wird die folgende Schnittstelle [InterfaceImpôtsDaoWithHttpSession] implementieren:


from abc import abstractmethod

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

class InterfaceImpôtsDaoWithHttpSession(AbstractImpôtsDao):

    # Berechnung der Steuer pro Einheit
    @abstractmethod
    def calculate_tax(self, taxpayer: TaxPayer):
        pass

    # Berechnung der Steuer nach Chargen
    @abstractmethod
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        pass

    # Initialisierung einer Sitzung
    @abstractmethod
    def init_session(self, type_session: str):
        pass

    # Beendigung einer Sitzung
    @abstractmethod
    def end_session(self):
        pass

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

    # Liste der Simulationen
    @abstractmethod
    def get_simulations(self) -> list:
        pass

    # Simulation löschen
    @abstractmethod
    def delete_simulation(self, id: int) -> list:
        pass

    # Daten für die Steuerberechnung abrufen
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass

Jede Methode der Schnittstelle entspricht einer Servicemethode URL des Steuerberechnungsservers.

  • Zeile 7: Die Schnittstelle erweitert die Klasse [AbstractDao], die den Zugriff auf das Dateisystem verwaltet;

Die Zuordnung zwischen den Methoden und den Dienst-URL erfolgt in der Konfigurationsdatei [config]:


        # der Server zur Steuerberechnung
        "server": {
            "urlServer""http://127.0.0.1:5000",
            "user": {
                "login""admin",
                "password""admin"
            },
            "url_services": {
                "calculate-tax""/calculer-impot",
                "get-admindata""/get-admindata",
                "calculate-tax-in-bulk-mode""/calculer-impots",
                "init-session""/init-session",
                "end-session""/fin-session",
                "authenticate-user""/authentifier-utilisateur",
                "get-simulations""/lister-simulations",
                "delete-simulation""/supprimer-simulation"
            }

31.2.2. Implementierung

Die Schnittstelle [InterfaceImpôtsDaoWithHttpSession] wird durch die folgende Klasse [ImpôtsDaoWithHttpSession] implementiert:


# Importe
import json

import requests
import xmltodict
from flask_api import status

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDaoWithHttpSession import InterfaceImpôtsDaoWithHttpSession
from TaxPayer import TaxPayer

class ImpôtsDaoWithHttpSession(InterfaceImpôtsDaoWithHttpSession):

    # Hersteller
    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"]
        # Dienste
        self.__config_services = config["server"]['url_services']
        # Debug-Modus
        self.__debug = config["debug"]
        # Protokollierung
        self.__logger = None
        # Cookies
        self.__cookies = None
        # Sitzungstyp (json, xml)
        self.__session_type = None

        # Request-/Response-Schritt
     def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
        # [method]: Methode HTTP, GET oder POST
        # [url_service]: URL als Dienst
        # [data]: Parameter des POST im Format „x-www-form-urlencoded“
        # [json]: Parameter von POST im JSON-Format
        # [cookies]: In die Anfrage einzufügende Cookies

        # Es muss eine Sitzung mit XML oder JSON bestehen, andernfalls kann die Antwort nicht verarbeitet werden
        if self.__session_type not in ['json''xml']:
            raise ImpôtsError(73"il n'y a pas de session valide en cours")

        # Anmeldung
        if method == "GET":
            # GET
            response = requests.get(url_service, cookies=self.__cookies)
        else:
            # POST
            response = requests.post(url_service, data=data_value, json=json_value, cookies=self.__cookies)

        # Debug-Modus?
        if self.__debug:
            # Logger
            if not self.__logger:
                self.__logger = self.__config['logger']
            # Protokollierung läuft
            self.__logger.write(f"{response.text}\n")

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

        # Cookies aus der Antwort abrufen, falls vorhanden
        if response.cookies:
            self.__cookies = response.cookies

        # Statuscode
        status_code = response.status_code

        # wenn Statuscode ungleich 200 OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(35, résultat['réponse'])

        # das Ergebnis wird zurückgegeben
        return résultat['réponse']

    def init_session(self, session_type: str):
        # den Sitzungstyp notieren
        self.__session_type = session_type

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

        # Abfrageausführung
        self.get_response("GET", url_service)

  • Zeilen 16–34: der Konstruktor der Klasse;
  • Zeile 19: Die übergeordnete Klasse wird initialisiert;
  • Zeilen 21–28: Bestimmte Konfigurationsdaten werden gespeichert;
  • Zeilen 29–34: Es werden drei Eigenschaften angelegt, die in den Methoden der Klasse verwendet werden;
  • Zeilen 36–82: Die Methode [get_response] fasst die gemeinsamen Elemente aller Methoden der Schicht [dao] zusammen: das Senden einer Anfrage HTTP und das Abrufen der Antwort HTTP vom Server;
  • Zeilen 38–42: Definition der 5 Parameter der Methode [get_response];
  • Zeile 42: Da der Server eine Sitzung aufrechterhält, muss der Client Cookies lesen bzw. zurücksenden;
  • Zeilen 44–46: Es wird überprüft, ob tatsächlich eine gültige Sitzung läuft;
  • Zeile 51: Fall der Methode GET. Die empfangenen Cookies werden zurückgesendet;
  • Zeile 54: Fall von POST. Dieser kann zwei Arten von Parametern haben:
    • den Typ [x-www-form-urlencoded]. Dies ist bei den Typen URL, [/calculer-impot] und [/authentifier-utilisateur] der Fall. In diesem Fall wird der von der Methode empfangene Parameter [data_value] verwendet;
    • den Typ [json]. Dies gilt für die Typen URL und [/calculer-impots]. In diesem Fall wird der von der Methode empfangene Parameter [json_value] verwendet;

Auch hier wird das Sitzungs-Cookie zurückgesendet.

  • Zeilen 56–62: Befindet man sich im Modus [debug], wird die Antwort des Servers protokolliert. Dieses Protokoll ist wichtig, da es Aufschluss darüber gibt, was der Server genau zurückgesendet hat;
  • Zeilen 64–68: Je nachdem, ob man sich im JSON- oder XML-Modus befindet, wird die Textantwort des Servers in ein Dictionary umgewandelt. Nehmen wir das Beispiel URL [/init-session]:

Die Antwort „jSON“ lautet wie folgt:


2020-08-03 11:45:21.218116, MainThread : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}

Die Antwort „XML“ lautet wie folgt:


2020-08-03 11:45:54.671871, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>

Der Code in den Zeilen 64–68 stellt sicher, dass man in beiden Fällen in [résultat] ein Wörterbuch mit den Schlüsseln [action, état, réponse] erhält;

  • Zeilen 70–72: Wenn die Antwort Cookies enthält, werden diese abgerufen. Sie müssen bei der nächsten Anfrage erneut gesendet werden;
  • Zeilen 74–79: Wenn der Status HTTP der Antwort nicht 200 ist, wird eine Ausnahme mit der in „result[’réponse’]“ enthaltenen Fehlermeldung ausgelöst. Dabei kann es sich um einen einzelnen Fehler oder eine Liste von Fehlern handeln;
  • Zeilen 81–82: Die Antwort des Servers wird an den aufrufenden Code zurückgegeben;

[init_session]

  • Zeile 84: Die Methode [init_session] dient dazu, den Typ der JSON- oder XML-Sitzung festzulegen, die der Client mit dem Server starten möchte;
  • Zeile 86: Der gewünschte Sitzungstyp wird innerhalb der Klasse vermerkt. Tatsächlich benötigen alle Methoden diese Information, um die Antwort des Servers korrekt zu dekodieren;
  • Zeilen 88–90: Anhand der Anwendungskonfiguration wird bestimmt, welche Dienstmethode „URL“ abgefragt werden muss;
  • Zeile 93: Der Dienst URL wird abgefragt. Das Ergebnis der Methode [get_response] wird nicht abgerufen:
    • Wenn sie eine Ausnahme auslöst, ist der Vorgang fehlgeschlagen. Die Ausnahme wird hier nicht abgefangen und wird direkt an den aufrufenden Code weitergeleitet, der den Client dann mit einer Fehlermeldung beendet;
    • Wenn keine Ausnahme ausgelöst wird, bedeutet dies, dass die Initialisierung der Sitzung erfolgreich war;

[authenticate_user]


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

        # Abfrageausführung
        self.get_response("POST", url_service, post_params)
  • Die Methode [authenticate_user] dient zur Authentifizierung beim Server. Dazu erhält sie in Zeile 1 die Anmeldedaten [user, password];
  • Zeilen 2–4: Es wird der abzufragende Dienst URL ermittelt;
  • Zeilen 5–8: die Parameter des POST, da der URL [/authentifier-utilisateur] einen POST mit den Parametern [user, password] erwartet;
  • Zeile 11: Die Anfrage wird ausgeführt. Auch hier wird die Antwort des Servers nicht abgerufen. Die von [get_response] ausgelöste Ausnahme gibt an, ob der Vorgang erfolgreich war oder nicht;

[calculate_tax]


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

        # Abfrageausführung
        response = self.get_response("POST", url_service, post_params)
        # Aktualisierung von TaxPayer mit der Antwort
        taxpayer.fromdict(response)
  • Die Methode [calculate_tax] ermöglicht die Berechnung der Steuer eines als Parameter übergebenen Steuerpflichtigen [taxpayer]. Dieser Parameter wird von der Methode geändert (Zeile 15) und stellt somit das Ergebnis der Methode dar;
  • Zeilen 2–4: Es wird die abzufragende Service-ID URL definiert;
  • Zeilen 6–10: Die Parameter des zu sendenden POST. Tatsächlich erwartet der Dienst URL vom Dienst [/calculer-impot] einen POST mit den Parametern [marié, enfants, salaire];
  • Zeilen 12–13: Die Anfrage wird ausgeführt und die Antwort des Servers abgerufen. Der Dienst URL des Dienstes [/calculer-impot] gibt ein Wörterbuch mit den Steuerschlüsseln [impôt, décôte, surcôte, réduction, taux] zurück;
  • Zeile 15: Das erhaltene Wörterbuch [response] wird verwendet, um den Steuerpflichtigen [taxpayer] zu aktualisieren;

[calculate_tax_in_bulk_mode]


    # Steuerberechnung im Bulk-Modus
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        # Ausnahmen werden weitergeleitet

        # Steuerpflichtige werden in eine Liste von Wörterbüchern umgewandelt
        # nur die Eigenschaften werden beibehalten [marié, enfants, salaire]
        list_dict_taxpayers = list(
            map(lambda taxpayer:
                taxpayer.asdict(included_keys=[
                    '_TaxPayer__marié',
                    '_TaxPayer__enfants',
                    '_TaxPayer__salaire']),
                taxpayers))

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

        # Abfrageausführung
        list_dict_taxpayers2 = self.get_response("POST", url_service, data_value=None, json_value=list_dict_taxpayers)
        # Wenn es nur einen Steuerzahler gibt und man sich in einer XML-Sitzung befindet, ist [list_dict_taxpayers2] keine Liste
        # In diesem Fall wird daraus eine Liste erstellt
        if not isinstance(list_dict_taxpayers2, list):
            list_dict_taxpayers2 = [list_dict_taxpayers2]
        # Die ursprüngliche Liste der Steuerzahler wird mit den erhaltenen Ergebnissen aktualisiert
        for i in range(len(taxpayers)):
            # Aktualisierung der Steuerzahler [i]
            taxpayers[i].fromdict(list_dict_taxpayers2[i])
        # Hier wurde der Parameter [taxpayers] mit den Ergebnissen vom Server aktualisiert
  • Zeile 2: Die Methode erhält eine Liste von Steuerpflichtigen vom Typ TaxPayer;
  • Zeilen 7–13: Diese Liste von Elementen vom Typ [TaxPayer] wird in eine Liste von Dictionaries vom Typ [marié, enfants, salaire] umgewandelt;
  • Zeilen 15–17: Der Dienst-URL wird festgelegt;
  • Zeilen 19–20: Es wird eine Anfrage vom Typ POST ausgeführt, deren Hauptteil jSON aus der in Zeile 7 erstellten Liste der Wörterbücher besteht. Die Antwort des Servers wird abgerufen;
  • Zeilen 23–24: Die Tests haben ein Problem aufgezeigt, wenn die Sitzung vom Typ XML ist:
    • Wenn die ursprüngliche Liste der Steuerzahler N Elemente enthält (N > 1), erhält man als Ergebnis eine Liste von N Wörterbüchern vom Typ [OrderedDict];
    • Wenn die ursprüngliche Liste nur ein Element enthält, erhält man keine Liste, sondern ein Element vom Typ [OrderedDict];
  • Zeilen 23–24: Befindet man sich im letztgenannten Fall (1 Element), wandelt man das Ergebnis in eine Liste mit 1 Element um;
  • Zeilen 25–28: Diese Liste der empfangenen Wörterbücher enthält die Steuer jedes Steuerpflichtigen aus der ursprünglichen Liste. Anschließend wird jedes einzelne mit den empfangenen Ergebnissen aktualisiert;

[get_simulations]


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

        # Abfrageausführung
        return self.get_response("GET", url_service)
  • Zeile 1: Die Methode fordert die Liste der in der aktuellen Sitzung durchgeführten Simulationen an;
  • Zeile 2: Die Methode gibt die Antwort des Servers zurück;

[delete_simulation]


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

        # Anfrageausführung
        return self.get_response("GET", url_service)
  • Zeile 1: Die Methode löscht die Simulation, deren ID übergeben wird;
  • Zeile 7: Die Methode gibt die Antwort des Servers zurück, nämlich die Liste der nach dem angeforderten Löschen verbleibenden Simulationen;

[get-admindata]


    def get_admindata(self) -> AdminData:
        # Ausnahmen werden weitergeleitet

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

        # Abfrageausführung
        résultat = self.get_response("GET", url_service)

        # Das Ergebnis ist ein Wörterbuch mit Werten vom Typ „str“, wenn es sich um eine XML-Sitzung handelt
        if self.__session_type == 'xml':
            # neues Wörterbuch
            résultat2 = {}
            # alles wird in numerische Werte umgewandelt
            for key, value in résultat.items():
                # Einige Elemente des Wörterbuchs sind Listen
                if isinstance(value, list):
                    values = []
                    for value2 in value:
                        values.append(float(value2))
                    résultat2[key] = values
                else:
                    # andere sind einfache Elemente
                    résultat2[key] = float(value)
        else:
            résultat2 = résultat
        # Ergebnis vom Typ AdminData
        return AdminData().fromdict(résultat2)
  • Zeile 1: Die Methode fragt beim Server die Steuerkonstanten ab, die zur Berechnung der Steuer benötigt werden;
  • Zeile 29: Sie gibt einen Typ [AdminData] zurück;
  • Zeile 9: Die Antwort des Servers wird in Form eines Dictionaries abgerufen. Tests zeigen, dass ein Problem auftritt, wenn es sich um eine XML-Sitzung handelt: Anstelle von numerischen Werten sind die Werte des Dictionaries Zeichenfolgen. Wir hatten dieses Problem bereits bei der Untersuchung des Moduls [xmltodict] gemeldet und festgestellt, dass es sich um ein normales Verhalten handelt. [xmltodict] enthält keine Typinformationen in dem Datenstrom XML, der ihm übergeben wird. In diesem speziellen Fall müssen jedoch alle Werte des empfangenen Wörterbuchs in numerische Werte umgewandelt werden. Dieses enthält drei Listen [limites, coeffr, coeffn] und eine Reihe numerischer Eigenschaften;
  • Zeilen 13–25: Erstellung eines Wörterbuchs [résultat2] mit numerischen Werten auf der Grundlage des Wörterbuchs [résultat] mit Zeichenfolgenwerten;
  • Zeile 29: Das Wörterbuch [resultat2] wird zur Initialisierung eines Typs [AdminData] verwendet;

31.2.3. Die Factory der Schicht [dao]

Unsere Clients werden multithreaded sein. Da die Schicht [dao] durch eine Klasse implementiert ist, die über einen Lese-/Schreibzustand verfügt (= Lese-/Schreib-Eigenschaften), muss jeder Thread über eine eigene Schicht [dao] verfügen, oder aber der Zugriff auf die gemeinsam genutzten Daten muss zwischen den Threads synchronisiert werden. Hier entscheiden wir uns für die erste Lösung. Wir verwenden eine Klasse [ImpôtsDaoWithHttpSessionFactory], die Instanzen der Schicht [dao] erstellen kann:


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)

31.3. Konfiguration der Clients

Image

Die Clients werden über die Dateien [config] und [config_layers] konfiguriert. Die Datei [config] sieht wie folgt aus:


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

    # Schritt 1 ------

    # Ordner dieser Datei
    script_dir = os.path.dirname(os.path.abspath(__file__))

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

    # 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",
        # ImpôtsDaoWithHttpSession, ImpôtsDaoWithHttpSessionFactory, InterfaceImpôtsDaoWithHttpSession
        f"{script_dir}/../services",
        # Konfigurationsskripte
        script_dir,
        # Logger
        f"{root_dir}/impots/http-servers/02/utilities",
    ]

    # Festlegen des Syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # Schritt 2 ------
    # Konfiguration der Anwendung mit Konstanten
    config.update({
        # Steuerzahlerdatei
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        # Ergebnisdatei
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        # Fehlerdatei
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        # Protokolldatei
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # Steuerberechnungsserver
        "server": {
            "urlServer""http://127.0.0.1:5000",
            "user": {
                "login""admin",
                "password""admin"
            },
            "url_services": {
                "calculate-tax""/calculer-impot",
                "get-admindata""/get-admindata",
                "calculate-tax-in-bulk-mode""/calculer-impots",
                "init-session""/init-session",
                "end-session""/fin-session",
                "authenticate-user""/authentifier-utilisateur",
                "get-simulations""/lister-simulations",
                "delete-simulation""/supprimer-simulation"
            }
        },
        # Debug-Modus
        "debug"True
    }
    )

    # Schritt 3 ------
    # Instanziierung der Schichten
    import config_layers
    config['layers'] = config_layers.configure(config)

    # Konfiguration wird zurückgesetzt
    return config

Die Datei [config_layers] lautet wie folgt:


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

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

    # Factory der DAO-Schicht
    from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
    dao_factory = ImpôtsDaoWithHttpSessionFactory(config)

    # Die Konfiguration der Schichten wird zurückgegeben
    return {
        "dao_factory": dao_factory,
        "métier": métier
    }
  • Die Clients haben keinen direkten Zugriff auf die Schicht [dao]. Um einen solchen Zugriff zu erhalten, müssen sie über die Factory der Schicht [dao] gehen;

31.4. Der Client [main]

Image

Der Client [main] ermöglicht das Testen von URL und [/init-session, /authentifier-utilisateur, /calculer-impots, /fin-session]:


# Es wird ein JSON- oder XML-Parameter erwartet
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# Die Anwendung wird konfiguriert
import config
config = config.configure({"session_type": session_type})

# 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(config: dict, taxpayers: list):
    # Die Factory der Schicht [dao] wird abgerufen
    dao_factory = config['layers']['dao_factory']
    # Es wird eine Instanz der Schicht [dao] erstellt
    dao = dao_factory.new_instance()
    # Art der Sitzung
    session_type = config['session_type']
    # Anzahl der Steuerzahler
    nb_taxpayers = len(taxpayers)
    # Protokoll
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    # Die Sitzung wird initialisiert
    dao.init_session(session_type)
    # Authentifizierung
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    # Berechnung der Steuer der Steuerzahler
    dao.calculate_tax_in_bulk_mode(taxpayers)
    # Ende der Sitzung
    dao.end_session()
    # Protokoll
    logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")

# Liste der Client-Threads
threads = []
logger = None
# Code
try:
    # Logger
    logger = Logger(config["logsFilename"])
    # wird in der Konfiguration gespeichert
    config["logger"] = logger
    # Startprotokoll
    logger.write("début du calcul de l'impôt des contribuables\n")
    # die Factory der Schicht [dao] wird abgerufen
    dao_factory = config["layers"]["dao_factory"]
    # Es wird eine Instanz der Schicht [dao] erstellt
    dao = dao_factory.new_instance()
    # Lesen der Steuerzahlerdaten
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # der Steuerzahler?
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # Berechnung der Steuer der Steuerzahler mit mehreren Threads
    i = 0
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        # Jeder Thread verarbeitet 1 bis 4 Steuerzahler
        nb_taxpayers = min(l_taxpayers - i, random.randint(14))
        # die Liste der vom Thread bearbeiteten Steuerzahler
        thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
        # i wird für den nächsten Thread erhöht
        i += nb_taxpayers
        # Der Thread wird erstellt
        thread = threading.Thread(target=thread_function, args=(config, thread_taxpayers))
        # Er wird zur Liste der Threads des Hauptskripts hinzugefügt
        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
    for thread in threads:
        thread.join()
    # Hier haben alle Threads ihre Arbeit beendet – jeder hat ein oder mehrere Objekte geändert [taxpayer]
    # Die Ergebnisse werden in der Datei jSON gespeichert
    dao.write_taxpayers_results(taxpayers)
    # Ende
except BaseException as erreur:
    # Anzeige des Fehlers
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # Der Logger wird geschlossen
    if logger:
        # Abschlussprotokoll
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # Logger wird geschlossen
        logger.close()
    # Fertig
    print("Travail terminé...")
    # Beenden von Threads, die möglicherweise noch bestehen, wenn der Vorgang aufgrund eines Fehlers abgebrochen wurde
    sys.exit()
  • Zeilen 4–11: Der Client erwartet einen Parameter, der ihm den mit dem Server zu verwendenden Sitzungstyp (json oder xml) angibt;
  • Zeilen 13–15: Der Client wird konfiguriert;
  • Zeilen 48–104: Dieser Code ist bekannt. Er wurde bereits mehrfach verwendet. Er verteilt die Steuerzahler, für die die Steuer berechnet werden soll, auf mehrere Threads;
  • Zeile 26: Die Methode [thread_function] wird von jedem Thread ausgeführt, um die Steuer der ihm zugewiesenen Steuerzahler zu berechnen;
  • Zeilen 27–30: Jeder Thread verfügt über eine eigene Schicht [dao];
  • Die Steuerberechnung erfolgt in vier Schritten:
    • Zeilen 37–38: Initialisierung einer Sitzung (JSON oder XML) mit dem Server;
    • Zeilen 39–40: Authentifizierung beim Server;
    • Zeilen 41–42: Berechnung der Steuer;
    • Zeilen 43–44: Beenden der Sitzung mit dem Server;

Wenn man diesen Code im Modus [json] ausführt, erhält man folgende Protokolle:


2020-08-03 14:28:34.320751, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:28:34.328749, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.328749, Thread-2 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.333592, Thread-3 : début du calcul de l'impôt des 3 contribuables
2020-08-03 14:28:34.368651, Thread-2 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.375699, Thread-1 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.377432, Thread-3 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.385653, Thread-2 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.392656, Thread-1 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.396377, Thread-3 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.406528, Thread-2 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}]}
2020-08-03 14:28:34.413837, Thread-1 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}]}
2020-08-03 14:28:34.416695, Thread-3 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 3}]}
2020-08-03 14:28:34.425747, Thread-2 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.425747, Thread-2 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.428956, Thread-1 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.428956, Thread-1 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.428956, Thread-3 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.428956, Thread-3 : fin du calcul de l'impôt des 3 contribuables
2020-08-03 14:28:34.428956, MainThread : fin du calcul de l'impôt des contribuables

Oben ist der Ablauf des Threads [Thread-2] dargestellt.

Führt man [main] im Modus XML aus, lauten die Protokolle wie folgt:


2020-08-03 14:32:48.495316, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:32:48.496452, Thread-1 : début du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.498992, Thread-2 : début du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.498992, Thread-3 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:32:48.498992, Thread-4 : début du calcul de l'impôt des 3 contribuables
2020-08-03 14:32:48.538637, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.540783, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.547811, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.547811, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.555184, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.564793, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.564793, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.568333, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.568333, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>2</enfants><salaire>55555</salaire><impôt>2814</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>oui</marié><enfants>2</enfants><salaire>50000</salaire><impôt>1384</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>384</décôte><réduction>347</réduction><id>2</id></réponse></root>
2020-08-03 14:32:48.579205, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>3</enfants><salaire>50000</salaire><impôt>0</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>720</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>non</marié><enfants>2</enfants><salaire>100000</salaire><impôt>19884</impôt><surcôte>4480</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse></root>
2020-08-03 14:32:48.579205, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>non</marié><enfants>3</enfants><salaire>100000</salaire><impôt>16782</impôt><surcôte>7176</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>oui</marié><enfants>3</enfants><salaire>100000</salaire><impôt>9200</impôt><surcôte>2180</surcôte><taux>0.3</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse><réponse><marié>oui</marié><enfants>5</enfants><salaire>100000</salaire><impôt>4230</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>0</décôte><réduction>0</réduction><id>3</id></réponse><réponse><marié>non</marié><enfants>0</enfants><salaire>100000</salaire><impôt>22986</impôt><surcôte>0</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>4</id></réponse></root>
2020-08-03 14:32:48.588051, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>2</enfants><salaire>30000</salaire><impôt>0</impôt><surcôte>0</surcôte><taux>0.0</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>non</marié><enfants>0</enfants><salaire>200000</salaire><impôt>64210</impôt><surcôte>7498</surcôte><taux>0.45</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse><réponse><marié>oui</marié><enfants>3</enfants><salaire>200000</salaire><impôt>42842</impôt><surcôte>17283</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>3</id></réponse></root>
2020-08-03 14:32:48.594058, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-1 : fin du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.595198, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-2 : fin du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.595198, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-3 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:32:48.603351, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.603351, Thread-4 : fin du calcul de l'impôt des 3 contribuables
2020-08-03 14:32:48.603351, MainThread : fin du calcul de l'impôt des contribuables

Oben ist der Ablauf des Threads [Thread-2] dargestellt.

31.5. Der Client [main2]

Image

Mit dem Client [main2] lassen sich die Clients URL und [/init-session, /authentifier-utilisateur, /get-admindata, /fin-session] testen:


# Es wird ein JSON- oder XML-Parameter erwartet
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# Die Anwendung wird konfiguriert
import config
config = config.configure({"session_type": session_type})

# Abhängigkeiten
from ImpôtsError import ImpôtsError
from Logger import Logger

logger = None
# Code
try:
    # Logger
    logger = Logger(config["logsFilename"])
    # wird in der Konfiguration gespeichert
    config["logger"] = logger
    # Startprotokoll
    logger.write("début du calcul de l'impôt des contribuables\n")
    # die Factory der Schicht [dao] wird abgerufen
    dao_factory = config['layers']['dao_factory']
    # Es wird eine Instanz der Schicht [dao] erstellt
    dao = dao_factory.new_instance()
    # Die Steuerzahler werden abgerufen
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # der Steuerzahler?
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # Art der Sitzung
    session_type = config['session_type']
    # Die Sitzung wird initialisiert
    dao.init_session(session_type)
    # Authentifizierung
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    # Daten der Steuerbehörde werden abgerufen
    admindata = dao.get_admindata()
    # Ende der Sitzung
    dao.end_session()
    # Berechnung der Steuer der Steuerpflichtigen durch die Schicht [métier]
    métier = config['layers']['métier']
    for taxpayer in taxpayers:
        métier.calculate_tax(taxpayer, admindata)
    # Die Ergebnisse werden in der Datei jSON gespeichert
    dao.write_taxpayers_results(taxpayers)
 except BaseException as erreur:
     # Anzeige des Fehlers
     print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # Der Logger wird geschlossen
    if logger:
        # Abschlussprotokoll
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # Logger wird geschlossen
        logger.close()
    # Fertig
    print("Travail terminé...")
  • Zeilen 1–11: Der Parameter [json, xml] wird abgerufen, der den Typ der mit dem Server herzustellenden Sitzung festlegt;
  • Zeilen 13–15: Der Client wird konfiguriert;
  • Zeilen 30–33: Es wird eine Schicht [dao] angelegt;
  • Zeilen 34–35: Mit dieser Schicht wird die Liste der Steuerpflichtigen abgerufen, für die die Steuer berechnet werden muss;
  • Anschließend folgen die vier Schritte des Dialogs mit dem Server;
    • Zeilen 41–42: Es wird eine Sitzung mit dem Server gestartet;
    • Zeilen 43–44: Man authentifiziert sich beim Server;
    • Zeilen 45–46: Vom Server werden die Steuerkonstanten angefordert, die zur Berechnung der Steuer erforderlich sind;
    • Zeilen 47–48: Die Sitzung mit dem Server wird beendet;
  • Zeilen 49–52: Mit diesen Konstanten kann die Steuer der Steuerzahler mithilfe der clientseitigen Schicht [métier] berechnet werden;
  • Zeilen 53–54: Die erzielten Ergebnisse werden gespeichert;

Für eine Sitzung mit der Schicht XML lauten die Ergebnisse wie folgt:


2020-08-03 14:44:43.194294, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:44:43.231633, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:44:43.240872, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:44:43.250061, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>get-admindata</action><état>1000</état><réponse><limites>9964.0</limites><limites>27519.0</limites><limites>73779.0</limites><limites>156244.0</limites><limites>93749.0</limites><coeffr>0.0</coeffr><coeffr>0.14</coeffr><coeffr>0.3</coeffr><coeffr>0.41</coeffr><coeffr>0.45</coeffr><coeffn>0.0</coeffn><coeffn>1394.96</coeffn><coeffn>5798.0</coeffn><coeffn>13913.7</coeffn><coeffn>20163.4</coeffn><abattement_dixpourcent_min>437.0</abattement_dixpourcent_min><plafond_impot_couple_pour_decote>2627.0</plafond_impot_couple_pour_decote><plafond_decote_couple>1970.0</plafond_decote_couple><valeur_reduc_demi_part>3797.0</valeur_reduc_demi_part><plafond_revenus_celibataire_pour_reduction>21037.0</plafond_revenus_celibataire_pour_reduction><id>1</id><abattement_dixpourcent_max>12502.0</abattement_dixpourcent_max><plafond_impot_celibataire_pour_decote>1595.0</plafond_impot_celibataire_pour_decote><plafond_decote_celibataire>1196.0</plafond_decote_celibataire><plafond_revenus_couple_pour_reduction>42074.0</plafond_revenus_couple_pour_reduction><plafond_qf_demi_part>1551.0</plafond_qf_demi_part></réponse></root>
2020-08-03 14:44:43.269850, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:44:43.269850, MainThread : fin du calcul de l'impôt des contribuables

31.6. Der Client [main3]

Mit dem Client [main3] lassen sich die Clients URL und [/init-session, /calculer-impots, /get-simulations, /delete-simulation, /fin-session] testen:

Image


# Es wird auf einen JSON- oder XML-Parameter gewartet
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# Die Anwendung wird konfiguriert
import config
config = config.configure({"session_type": session_type})

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

logger = None
# Code
try:
    # Logger
    logger = Logger(config["logsFilename"])
    # wird in der Konfiguration gespeichert
    config["logger"] = logger
    # Startprotokoll
    logger.write("début du calcul de l'impôt des contribuables\n")
    # die Factory der Schicht [dao] wird abgerufen
    dao_factory = config["layers"]["dao_factory"]
    # Es wird eine Instanz der Schicht [dao] erstellt
    dao = dao_factory.new_instance()
    # Lesen der Daten der Steuerzahler
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # der Steuerzahler?
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # Berechnung der Steuer der Steuerzahler
    # Anzahl der Steuerzahler
    nb_taxpayers = len(taxpayers)
    # Protokoll
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    # Die Sitzung wird initialisiert
    dao.init_session(session_type)
    # Authentifizierung
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    # Die Steuer der Steuerzahler wird berechnet
    dao.calculate_tax_in_bulk_mode(taxpayers)
    # Liste der Simulationen wird angefordert
    simulations = dao.get_simulations()
    # jede zweite wird gelöscht
    for i in range(len(simulations)):
        if i % 2 == 0:
            # Die Simulation wird gelöscht
            dao.delete_simulation(simulations[i]['id'])
    # Ende der Sitzung
    dao.end_session()
    # Sehen Sie sich die Protokolle an, um die verschiedenen Ergebnisse zu sehen (Debug-Modus = True)
except BaseException as erreur:
    # Anzeige des Fehlers
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # Der Logger wird geschlossen
    if logger:
        # Abschlussprotokoll
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # Schließen des Loggers
        logger.close()
    # Fertig
    print("Travail terminé...")
  • Zeilen 1–11: Der Sitzungstyp wird aus den Skriptparametern abgerufen;
  • Zeilen 13–15: Die Anwendung wird konfiguriert;
  • Zeilen 25–50: Hier findet sich Code, der bereits an anderer Stelle erläutert wurde;
  • Zeilen 51–52: Die Liste der in der aktuellen Sitzung durchgeführten Simulationen wird abgefragt;
  • Zeilen 53–57: Jede zweite Simulation wird gelöscht;
  • Zeilen 58–59: Die Sitzung wird beendet;

In einer Sitzung mit der ID jSON lauten die Protokolle wie folgt:


2020-08-03 15:01:52.702297, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 15:01:52.702297, MainThread : début du calcul de l'impôt des 11 contribuables
2020-08-03 15:01:52.734806, MainThread : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 15:01:52.747961, MainThread : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 15:01:52.765721, MainThread : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 5}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 6}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 7}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 8}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 9}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 10}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 11}]}
2020-08-03 15:01:52.785505, MainThread : {"action": "lister-simulations", "état": 500, "réponse": [{"décôte": 0, "enfants": 2, "id": 1, "impôt": 2814, "marié": "oui", "réduction": 0, "salaire": 55555, "surcôte": 0, "taux": 0.14}, {"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 720, "enfants": 3, "id": 3, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.801475, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 720, "enfants": 3, "id": 3, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.810129, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.818803, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.834604, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.843803, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.851855, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}]}
2020-08-03 15:01:52.863165, MainThread : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 15:01:52.863165, MainThread : fin du calcul de l'impôt des contribuables
  • Zeile 6: Es liegen 11 Simulationen vor;
  • Zeile 12: Nach den verschiedenen Löschungen sind nur noch 5 übrig;

31.7. Die Testklasse [Test2HttpClientDaoWithSession]

Image

Die Klasse [Test2HttpClientDaoWithSession] testet die Schicht [dao] der Clients wie folgt:


import unittest

from ImpôtsError import ImpôtsError
from Logger import Logger
from TaxPayer import TaxPayer

class Test2HttpClientDaoWithSession(unittest.TestCase):

    def test_init_session_json(self) -> None:
        print('test_init_session_json')
        erreur = False
        try:
            dao.init_session('json')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # Es darf kein Fehler vorliegen
        self.assertFalse(erreur)

    def test_init_session_xml(self) -> None:
        print('test_init_session_xml')
        erreur = False
        try:
            dao.init_session('xml')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # Es darf kein Fehler vorliegen
        self.assertFalse(erreur)

    def test_init_session_xxx(self) -> None:
        print('test_init_session_xxx')
        erreur = False
        try:
            dao.init_session('xxx')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # Es muss ein Fehler vorliegen
        self.assertTrue(erreur)

    def test_authenticate_user_success(self) -> None:
        print('test_authenticate_user_success')
        # Sitzung initialisieren
        dao.init_session('json')
        # Test
        erreur = False
        try:
            dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # Es darf kein Fehler vorliegen
        self.assertFalse(erreur)

    def test_authenticate_user_failed(self) -> None:
        print('test_authenticate_user_failed')
        # Sitzung initialisieren
        dao.init_session('json')
        # Test
        erreur = False
        try:
            dao.authenticate_user('x''y')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # Es muss ein Fehler auftreten
        self.assertTrue(erreur)

    def test_get_simulations(self) -> None:
        print('test_get_simulations')
        # Sitzung initialisieren
        dao.init_session('json')
        # Authentifizierung
        dao.authenticate_user('admin''admin')
        # Steuerberechnung
        # {'verheiratet': 'ja', 'Kinder': 2, 'Gehalt': 55555,
        # 'Steuer': 2814, 'Zuschlag': 0, 'Abzug': 0, 'Ermäßigung': 0, 'Satz': 0,14}
        taxpayer = TaxPayer().fromdict({"marié""oui""enfants"2"salaire"55555})
        dao.calculate_tax(taxpayer)
        # get_simulations
        simulations = dao.get_simulations()
        # Prüfungen
        # Es muss 1 Simulation geben
        self.assertEqual(1, len(simulations))
        simulation = simulations[0]
        # Überprüfung der berechneten Steuer
        self.assertAlmostEqual(simulation['impôt']2815, delta=1)
        self.assertEqual(simulation['décôte']0)
        self.assertEqual(simulation['réduction']0)
        self.assertAlmostEqual(simulation['taux']0.14, delta=0.01)
        self.assertEqual(simulation['surcôte']0)

    def test_delete_simulation(self) -> None:
        print('test_delete_simulation')
        # Sitzung initialisieren
        dao.init_session('json')
        # Authentifizierung
        dao.authenticate_user('admin''admin')
        # Steuerberechnung
        taxpayer = TaxPayer().fromdict({"marié""oui""enfants"2"salaire"55555})
        dao.calculate_tax(taxpayer)
        # get_simulations
        simulations = dao.get_simulations()
        # delete_simulation
        dao.delete_simulation(simulations[0]['id'])
        # get_simulations
        simulations = dao.get_simulations()
        # Überprüfung – es dürfen keine Simulationen mehr vorhanden sein
        self.assertEqual(0, len(simulations))
        # Eine nicht vorhandene Simulation wird gelöscht
        erreur = False
        try:
            dao.delete_simulation(100)
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # Es muss ein Fehler vorliegen
        self.assertTrue(erreur)

if __name__ == '__main__':
    # Die Anwendung wird konfiguriert
    import config
    config = config.configure({})

    # Protokollieren
    logger = Logger(config["logsFilename"])
    # wird in der Konfiguration gespeichert
    config["logger"] = logger

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

    # die Testmethoden werden ausgeführt
    print("tests en cours...")
    unittest.main()
  • Die Schicht [dao] sendet eine Anfrage an den Server, empfängt dessen Antwort und formatiert diese, um sie an den aufrufenden Code zurückzugeben. Wenn der Server eine Antwort mit einem anderen Statuscode als 200 sendet, löst die Schicht [dao] eine Ausnahme aus. Daher besteht eine Reihe von Tests darin, festzustellen, ob eine Ausnahme aufgetreten ist oder nicht;
  • Zeilen 9–18: Es wird eine Sitzung mit jSON initialisiert. Es dürfen keine Fehler auftreten;
  • Zeilen 20–29: Es wird eine Sitzung XML initialisiert. Es darf kein Fehler auftreten;
  • Zeilen 31–40: Es wird eine Sitzung mit einem falschen Typ initialisiert. Es muss ein Fehler auftreten;
  • Zeilen 42–54: Man authentifiziert sich mit den richtigen Anmeldedaten. Es darf kein Fehler auftreten;
  • Zeilen 56–68: Man authentifiziert sich mit falschen Zugangsdaten. Es muss ein Fehler auftreten;
  • Zeilen 70–92: Es wird eine Steuerberechnung durchgeführt und anschließend die Liste der Simulationen abgefragt. Es muss eine vorhanden sein. Außerdem wird überprüft, ob diese Simulation tatsächlich die angeforderte Steuer enthält;
  • Zeilen 94–119: Es wird eine Simulation erstellt, die anschließend gelöscht wird. Danach wird versucht, eine Simulation zu löschen, obwohl keine mehr vorhanden ist. Es muss ein Fehler auftreten;
  • Zeilen 121–137: Der Test wird wie ein klassisches Konsolenskript gestartet;
  • Zeilen 122–124: Wir konfigurieren die Anwendung;
  • Zeilen 126–129: Der Logger wird konfiguriert. So können wir die Protokolle verfolgen;
  • Zeilen 131–133: Die zu testende Schicht [dao] wird instanziiert;
  • Zeilen 135–137: Die Tests werden gestartet;

Die Konsolenergebnisse 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/07/tests/Test2HttpClientDaoWithSession.py
tests en cours...
test_authenticate_user_failed
..MyException[35, ["Echec de l'authentification"]]
test_authenticate_user_success
test_delete_simulation
MyException[35, ["la simulation n° [100] n'existe pas"]]
test_get_simulations
test_init_session_json
test_init_session_xml
test_init_session_xxx
MyException[73, il n'y a pas de session valide en cours]
----------------------------------------------------------------------
Ran 7 tests in 0.171s

OK

Process finished with exit code 0