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:

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.

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


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

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]

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(36, f"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(1, 4))
# 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]

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(36, f"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:

# 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(36, f"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]

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