24. Anwendungsübung: Version 7
24.1. Einleitung
Version 7 der Steuerberechnungsanwendung entspricht bis auf die folgenden Details der Version 6:
- Der Webclient wird mehrere Anfragen HTTP gleichzeitig starten. In der vorherigen Version wurden diese Anfragen nacheinander gestartet. Der Server bearbeitete zu jedem Zeitpunkt nur eine einzige Anfrage;
- Der Server wird multithreaded sein: Er kann mehrere Anfragen gleichzeitig bearbeiten;
- Um die Ausführung dieser Anfragen zu verfolgen, wird der Webserver mit einem Logger ausgestattet, mit dem wichtige Zeitpunkte der Anfrageverarbeitung in einer Textdatei protokolliert werden;
- Der Server sendet eine E-Mail an den Administrator der Anwendung, wenn er auf ein Problem stößt, das den Start verhindert, typischerweise ein Problem mit der dem Webserver zugeordneten Datenbank;
Die Architektur der Anwendung bleibt unverändert:
Die Verzeichnisstruktur der Skripte sieht wie folgt aus:

Der Ordner „[http-servers/02]“ wird zunächst durch Kopieren des Ordners „[http-servers/01]“ erstellt. Anschließend werden darin Änderungen vorgenommen.
24.2. Die Dienstprogramme

24.2.1. Die Klasse [Logger]
Die Klasse [Logger] ermöglicht es, bestimmte Aktionen des Webservers in einer Textdatei zu protokollieren:
import codecs
import threading
from datetime import date, datetime
from threading import current_thread
from ImpôtsError import ImpôtsError
class Logger:
# Klassenattribut
verrou = threading.RLock()
# Konstruktor
def __init__(self, logs_filename: str):
try:
# Die Datei wird im Anfüge-Modus (a) geöffnet
self.__resource = codecs.open(logs_filename, "a", "utf-8")
except BaseException as erreur:
raise ImpôtsError(18, f"{erreur}")
# Schreiben eines Protokolls
def write(self, message: str):
# Datum/Uhrzeit des aktuellen Zeitpunkts
today = date.today()
now = datetime.time(datetime.now())
# Name des Threads
thread_name = current_thread().name
# Man möchte während des Schreibvorgangs in die Protokolldatei nicht gestört werden
# Das Synchronisationsobjekt (= die Sperre) der Klasse wird angefordert – nur ein Thread erhält es
Logger.verrou.acquire()
try:
# Schreiben des Logs
self.__resource.write(f"{today} {now}, {thread_name} : {message}")
# Sofortiges Schreiben – andernfalls wird der Text erst beim Schließen des Schreibstroms geschrieben
# oder man möchte die Protokolle im Zeitverlauf verfolgen
self.__resource.flush()
finally:
# Das Synchronisationsobjekt (= die Sperre) wird freigegeben, damit ein anderer Thread es erhalten kann
Logger.verrou.release()
# Freigabe der Ressourcen
def close(self):
# Schließen der Datei
if self.__resource:
self.__resource.close()
- Zeilen 10–11: Hier wird ein Klassenattribut definiert. Ein Klassenattribut ist eine Eigenschaft, die von allen Instanzen der Klasse gemeinsam genutzt wird. Es wird mit der Notation [Classe.attribut_de_classe] referenziert (Zeilen 30, 39). Das Klassenattribut [verrou] dient als Synchronisationsobjekt für alle Threads, die den Code der Zeilen 31–36 ausführen;
- Zeilen 14–19: Der Konstruktor erhält den absoluten Pfadnamen der Protokolldatei. Diese Datei wird daraufhin geöffnet und der abgerufene Dateideskriptor in der Klasse gespeichert;
- Zeile 17: Die Protokolldatei wird im Modus „Append“ (a) geöffnet. Jede geschriebene Zeile wird an das Ende der Datei angehängt;
- Zeilen 22–39: Die Methode [write] ermöglicht es, eine als Parameter übergebene Meldung in die Protokolldatei zu schreiben. An diese werden zwei Informationen angehängt:
- Zeile 24: das aktuelle Datum;
- Zeile 25: die aktuelle Uhrzeit;
- Zeile 27: der Name des Threads, der das Protokoll schreibt. Dabei darf nicht vergessen werden, dass eine Webanwendung mehrere Benutzer gleichzeitig bedient. Jeder Anfrage wird ein Thread zugewiesen, um sie auszuführen. Wird dieser Thread angehalten – typischerweise für einen Ein-/Ausgabevorgang (Netzwerk, Dateien, Datenbank) –, wird der Prozessor einem anderen Thread zugewiesen. Aufgrund dieser möglichen Unterbrechungen kann man nicht sicher sein, dass es einem Thread gelingt, eine Zeile in die Protokolldatei zu schreiben, ohne unterbrochen zu werden. Es besteht daher die Gefahr, dass sich Protokolle von zwei verschiedenen Threads vermischen. Das Risiko ist gering, vielleicht sogar gleich null, aber wir haben uns dennoch entschlossen, zu zeigen, wie man den Zugriff zweier Threads auf eine gemeinsame Ressource – in diesem Fall die Protokolldatei – synchronisiert;
- Zeile 30: Vor dem Schreiben fordert der Thread den Schlüssel für die Eingangstür an. Der angeforderte Schlüssel ist der in Zeile 11 erstellte. Er ist tatsächlich eindeutig: Ein Klassenattribut ist für alle Instanzen der Klasse eindeutig;
- Zum Zeitpunkt T1 erhält der Thread „Thread1“ den Schlüssel. Er kann nun Zeile 33 ausführen;
- zum Zeitpunkt T2 wird der Thread „Thread1“ angehalten, noch bevor er das Schreiben des Protokolls abgeschlossen hat;
- Zum Zeitpunkt T3 muss auch der Thread „Thread2“, der die Prozessorausführung erhalten hat, ein Protokoll schreiben. Er gelangt dabei zu Zeile 30, wo er den Schlüssel für die Eingangstür anfordert. Es wird ihm mitgeteilt, dass ein anderer Thread diesen bereits besitzt. Daraufhin wird er automatisch angehalten. Dies gilt für alle Threads, die diesen Schlüssel anfordern;
- Zum Zeitpunkt T4 erhält der Thread Thread1, der zuvor angehalten worden war, wieder die Prozessorsteuerung. Er beendet daraufhin das Schreiben des Protokolls;
- Zeilen 32–36: Das Schreiben in die Protokolldatei erfolgt in zwei Schritten:
- Zeile 33: Der in Zeile 17 erhaltene Dateideskriptor arbeitet mit einem Puffer. Die Operation [write] in Zeile 33 schreibt in diesen Puffer, jedoch nicht direkt in die Datei. Der Puffer wird anschließend unter bestimmten Bedingungen in die Datei entleert:
- der Puffer ist voll;
- der Dateideskriptor ist Gegenstand einer Operation [close] oder [flush];
- Zeile 36: Das Schreiben der Protokollzeile in die Datei wird erzwungen. Dies geschieht, weil wir sehen wollen, wie sich die Protokolle der verschiedenen Threads untereinander einfügen. Würden wir dies nicht tun, würden die Protokolle eines Threads alle gleichzeitig beim Schließen des Dateideskriptors (Zeile 45) geschrieben. Es wäre dann viel schwieriger zu erkennen, dass bestimmte Threads beendet wurden: Man müsste die Zeitangaben in den Protokollen überprüfen;
- Zeile 39: Der Thread „Thread1“ gibt den ihm zugewiesenen Schlüssel zurück. Dieser kann nun einem anderen Thread zugewiesen werden;
- Zeile 22: Die Methode [write] ist somit synchronisiert: Es schreibt jeweils nur ein Thread in die Logdatei. Der Schlüssel des Mechanismus ist Zeile 30: Was auch immer geschieht, nur ein Thread erhält den Schlüssel für den Übergang zur nächsten Zeile. Er behält ihn, bis er ihn zurückgibt (Zeile 39);
- Zeilen 41–45: Die Methode [close] ermöglicht es, die dem Deskriptor der Protokolldatei zugewiesenen Ressourcen freizugeben;
Die in die Logdatei geschriebenen Logs sehen wie folgt aus:
24.2.2. Die Klasse [SendAdminMail]
Die Klasse [SendAminMail] ermöglicht es, eine Nachricht an den Administrator der Anwendung zu senden, wenn diese „abstürzt“.

Die Klasse [SendAdminMail] wird im Skript [config] [2] wie folgt konfiguriert:
# Serverkonfiguration SMTP
"adminMail": {
# Server SMTP
"smtp-server": "localhost",
# Server-Port SMTP
"smtp-port": "25",
# Administrator
"from": "guest@localhost.com",
"to": "guest@localhost.com",
# Betreff der E-Mail
"subject": "plantage du serveur de calcul d'impôts",
# tls auf „True“, wenn der Server SMTP eine Autorisierung erfordert, andernfalls auf „False“
"tls": False
}
Die Klasse [SendAdminMail] erhält das Wörterbuch aus den Zeilen 2–13 sowie die Konfiguration für den E-Mail-Versand. Die Klasse sieht wie folgt aus:
# Importe
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate
class SendAdminMail:
# -----------------------------------------------------------------------
@staticmethod
def send(config: dict, message: str, verbose: bool = False):
# sendet eine Nachricht an den SMTP-Server config['smtp-server'] auf dem Port config[smtp-port]
# Wenn config['tls'] wahr ist, wird das Format TLS verwendet
# Die E-Mail wird im Namen von config['from'] versendet
# an den Empfänger config['to']
# Die Nachricht hat den Betreff „config['subject']“
# In der Konfiguration findet man die Referenz eines Loggers: config['logger']
# Der Logger wird aus der Konfiguration abgerufen – kann auch „None“ lauten
logger = config["logger"]
# Server SMTP
server = None
# Die Nachricht wird gesendet
try:
# der Server SMTP
server = smtplib.SMTP(config["smtp-server"])
# Ausführlicher Modus
server.set_debuglevel(verbose)
# Sichere Verbindung?
if config['tls']:
# Start des Sicherheitsdialogs
server.starttls()
# Authentifizierung
server.login(config["user"], config["password"])
# Erstellung einer Multipart-Nachricht – diese Nachricht wird gesendet
msg = MIMEText(message)
msg['From'] = config["from"]
msg['To'] = config["to"]
msg['Date'] = formatdate(localtime=True)
msg['Subject'] = config["subject"]
# Die Nachricht wird gesendet
server.send_message(msg)
# Protokoll – der Logger ist möglicherweise nicht vorhanden
if logger:
logger.write(f"[SendAdminMail] Message envoyé à [{config['to']}] : [{message}]\n")
except BaseException as erreur:
# Protokoll – der Logger existiert möglicherweise nicht
if logger:
logger.write(
f"[SendAdminMail] Erreur [{erreur}] lors de l'envoi à [{config['to']}] du message [{message}] : \n")
finally:
# Fertig – die von der Funktion belegten Ressourcen werden freigegeben
if server:
server.quit()
- Zeilen 24–54: Hier findet sich der bereits im Beispiel |smtp/02| behandelte Code;
- Zeile 20: Hier wird die Referenz eines Loggers abgerufen. Diese wird in den Zeilen 45 und 49 verwendet;
24.3. Der Webserver

24.3.1. Konfiguration

Die Konfiguration des Servers ist der des zuvor untersuchten Servers sehr ähnlich. Lediglich die Datei [config.py] weicht geringfügig ab:
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",
# IndexController
f"{root_dir}/impots/http-servers/01/controllers",
# Skripte [config_database, config_layers]
script_dir,
# Logger, SendAdminMail
f"{script_dir}/../utilities",
]
# Der Syspath wird festgelegt
from myutils import set_syspath
set_syspath(absolute_dependencies)
# Schritt 2 ------
# Konfiguration der Anwendung
config.update({
# Benutzer, die zur Nutzung der Anwendung berechtigt sind
"users": [
{
"login": "admin",
"password": "admin"
}
],
# Protokolldatei
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# Serverkonfiguration SMTP
"adminMail": {
# Server SMTP
"smtp-server": "localhost",
# Port des Servers SMTP
"smtp-port": "25",
# Administrator
"from": "guest@localhost.com",
"to": "guest@localhost.com",
# Betreff der E-Mail
"subject": "plantage du serveur de calcul d'impôts",
# TLS auf „True“, wenn der Server SMTP eine Autorisierung erfordert, andernfalls auf „False“
"tls": False
},
# Pausenzeit des Threads in Sekunden
"sleep_time": 0
})
# Schritt 3 ------
# Datenbankkonfiguration
import config_database
config["database"] = config_database.configure(config)
# Schritt 4 ------
# Instanziierung der Anwendungsschichten
import config_layers
config['layers'] = config_layers.configure(config)
# Die Konfiguration wird zurückgegeben
return config
- Zeilen 40–66: Im Konfigurationswörterbuch des Servers werden die Elemente zum Logger (Zeile 49) sowie diejenigen zum Versand einer Warn-E-Mail an den Anwendungsadministrator (Zeilen 51–63) hinzugefügt;
- Zeile 65: Um die Threads besser in Aktion beobachten zu können, werden einige davon zwangsweise angehalten. [sleep_time] ist die Dauer der Unterbrechung in Sekunden;
- Zeilen 27–28: Beachten Sie, dass wir den Controller [index_controller] aus der vorherigen Version 6 verwenden;
24.3.2. Das Hauptskript [main]
Das Hauptskript [main] lautet wie folgt:
# Es wird ein MySQL- oder PostgreSQL-Parameter erwartet
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
sgbd = sys.argv[1].lower()
erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# Die Anwendung wird konfiguriert
import config
config = config.configure({'sgbd': sgbd})
# Abhängigkeiten
from flask import request, Flask
from flask_httpauth import HTTPBasicAuth
import json
import index_controller
from flask_api import status
from SendAdminMail import SendAdminMail
from myutils import json_response
from Logger import Logger
import threading
import time
from random import randint
from ImpôtsError import ImpôtsError
# Authentifizierungsmanager
auth = HTTPBasicAuth()
@auth.verify_password
def verify_password(login, password):
# Benutzerliste
users = config['users']
# Diese Liste wird durchlaufen
for user in users:
if user['login'] == login and user['password'] == password:
return True
# Es wurde nichts gefunden
return False
# E-Mail an den Administrator senden
def send_adminmail(config: dict, message: str):
# Es wird eine E-Mail an den Administrator der Anwendung gesendet
config_mail = config["adminMail"]
config_mail["logger"] = config['logger']
SendAdminMail.send(config_mail, message)
# Überprüfung der Protokolldatei
logger = None
erreur = False
message_erreur = None
try:
# Protokollierung
logger = Logger(config["logsFilename"])
except BaseException as exception:
# Konsolenprotokoll
print(f"L'erreur suivante s'est produite : {exception}")
# Der Fehler wird notiert
erreur = True
message_erreur = f"{exception}"
# Logger in der Konfiguration speichern
config['logger'] = logger
# Fehlerbehandlung
if erreur:
# E-Mail an den Administrator
send_adminmail(config, message_erreur)
# Beenden der Anwendung
sys.exit(1)
# Startprotokoll
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)
# Abruf der Daten von der Steuerbehörde
erreur = False
try:
# „admindata“ wird zu einer schreibgeschützten, anwendungsweiten Variable
config["admindata"] = config["layers"]["dao"].get_admindata()
# Erfolgsprotokoll
logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
# Fehler wird protokolliert
erreur = True
# Fehlerprotokoll
log = f"L'erreur suivante s'est produite : {ex}"
# Konsole
print(log)
# Protokolldatei
logger.write(f"{log}\n")
# E-Mail an den Administrator
send_adminmail(config, log)
# Der Hauptthread benötigt den Logger nicht mehr
logger.close()
# Bei einem Fehler wird das Programm beendet
if erreur:
sys.exit(2)
# Die Flask-Anwendung kann gestartet werden
app = Flask(__name__)
# Startseite URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
…
# nur main
if __name__ == '__main__':
# Der Server wird gestartet
app.config.update(ENV="development", DEBUG=True)
app.run(threaded=True)
- Zeilen 1–10: Das Skript erwartet einen Parameter [mysql / pgres], der ihm angibt, welches SGBD verwendet werden soll;
- Zeilen 12–14: Die Anwendung wird konfiguriert (Python-Pfad, Schichten, Datenbank);
- Zeilen 16–28: Die für die Anwendung erforderlichen Abhängigkeiten;
- Zeilen 30–43: Verwaltung der Authentifizierung;
- Zeilen 46–51: Eine Funktion, die eine E-Mail an den Administrator der Anwendung sendet;
- Die Funktion erwartet zwei Parameter:
- config: ein Wörterbuch mit den Schlüsseln [adminMail] und [logger];
- die zu versendende Nachricht;
- Zeilen 49–50: Vorbereitung der Versandkonfiguration;
- die E-Mail wird versendet;
- Zeilen 54–74: Es wird überprüft, ob die Protokolldatei vorhanden ist;
- Zeilen 70–74: Wenn die Logdatei nicht geöffnet werden konnte, wird eine E-Mail an den Administrator gesendet und der Vorgang abgebrochen;
- Zeilen 76–79: Der Start des Servers wird protokolliert;
- Zeilen 81–98: Die Daten der Steuerbehörde werden aus der Datenbank abgerufen;
- Zeilen 88–98: Wenn es nicht gelungen ist, diese Daten abzurufen, wird der Fehler sowohl auf der Konsole als auch in der Protokolldatei protokolliert;
- Zeilen 100–101: Der Haupt-Thread führt keine Protokollierung mehr durch (die erstellten Threads verwenden nicht denselben Dateideskriptor);
- Zeilen 103–105: Wenn keine Verbindung zur Datenbank hergestellt werden konnte, wird der Vorgang abgebrochen;
- Zeile 122: Der Server wird im Multithread-Modus gestartet;
Die Funktion [index] (Zeile 114) lautet wie folgt:
# Startseite URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
logger = None
try:
# Protokollieren
logger = Logger(config["logsFilename"])
# wird in einer dem Thread zugeordneten Konfiguration gespeichert
thread_config = {"logger": logger}
thread_name = threading.current_thread().name
config[thread_name] = {"config": thread_config}
# Die Anfrage wird protokolliert
logger.write(f"[index] requête : {request}\n")
# Der Thread wird unterbrochen, falls dies angefordert wurde
sleep_time = config["sleep_time"]
if sleep_time != 0:
# Die Pause erfolgt zufällig, damit einige Threads unterbrochen werden und andere nicht
aléa = randint(0, 1)
if aléa == 1:
# Protokollierung vor der Pause
logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
# Pause
time.sleep(sleep_time)
# Die Anfrage wird von einem Controller ausgeführt
résultat, status_code = index_controller.execute(request, config)
# Ist ein schwerwiegender Fehler aufgetreten?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# Es wird eine E-Mail an den Administrator der Anwendung gesendet
config_mail = config["adminMail"]
config_mail["logger"] = logger
SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
# Die Antwort wird protokolliert
logger.write(f"[index] {résultat}\n")
# Die Antwort wird gesendet
return json_response(résultat, status_code)
except BaseException as erreur:
# Der Fehler wird protokolliert, sofern dies möglich ist
if logger:
logger.write(f"[index] {erreur}")
# Die Antwort an den Kunden wird vorbereitet
résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
# die Antwort wird gesendet
return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
finally:
# Die Protokolldatei wird geschlossen, falls sie geöffnet war
if logger:
logger.close()
- Zeile 4: Die Funktion, die ausgeführt wird, wenn ein Benutzer die Funktion URL aufruft. Da der Server multithreaded ist (Zeile 112), wird ein Thread erstellt, um die Funktion auszuführen. Dieser Thread kann jederzeit unterbrochen und angehalten werden, um die Ausführung etwas später fortzusetzen. Diesen Punkt muss man immer im Hinterkopf behalten, wenn der Code auf eine Ressource zugreift, die von allen Threads gemeinsam genutzt wird. Eine solche Ressource ist hier die Protokolldatei: Alle Threads schreiben in sie hinein;
- Zeile 8: Es wird eine Instanz des Loggers erstellt. Somit verfügen alle Threads über eine eigene Instanz des Loggers. Dennoch verweisen alle diese Logger auf dieselbe Logdatei. Es ist jedoch wichtig zu beachten, dass das Schließen des Loggers durch einen Thread keine Auswirkungen auf die Logger der anderen Threads hat;
- Zeilen 9–12: Der Logger wird im Wörterbuch [config] der Anwendung gespeichert, zugeordnet einem Schlüssel mit dem Namen des Threads. Wenn also n Threads gleichzeitig ausgeführt werden, werden n Einträge im Wörterbuch [config] angelegt. [config] ist eine von allen Threads gemeinsam genutzte Ressource. Daher kann eine Synchronisation erforderlich sein. Ich bin hier von einer Annahme ausgegangen. Ich bin davon ausgegangen, dass es keine Auswirkungen hätte, wenn zwei Threads gleichzeitig ihren Eintrag in der Datei „[config]“ erstellen würden und einer von ihnen durch den anderen unterbrochen würde. Der unterbrochene Thread könnte die Erstellung des Eintrags später abschließen. Sollte sich im Test herausstellen, dass diese Annahme falsch ist, müsste der Zugriff auf Zeile 12 synchronisiert werden;
- Zeile 10: Der Logger wird in ein Wörterbuch aufgenommen;
- Zeile 11: [threading.current_thread()] ist der Thread, der diese Zeile ausführt, also der Thread, der die Funktion [index] ausführt. Sein Name wird notiert. Jeder Thread hat einen eindeutigen Namen;
- Zeile 12: Die Konfiguration des Threads wird gespeichert. Von nun an werden wir immer so vorgehen: Wenn es Informationen gibt, die nicht zwischen den Threads geteilt werden können, werden sie dennoch in die allgemeine Konfiguration aufgenommen, jedoch dem Namen des Threads zugeordnet;
- Zeile 14: Die gerade ausgeführte Anfrage wird protokolliert;
- Zeilen 15–24: Wir setzen bestimmte Threads nach dem Zufallsprinzip in den Pausenmodus, damit sie den Prozessor einem anderen Thread überlassen;
- Zeile 16: Die Dauer der Pause (in Sekunden) wird aus der Konfiguration abgerufen;
- Zeile 17: Es erfolgt nur dann eine Pause, wenn die Pausendauer ungleich 0 ist;
- Zeile 19: Eine Zufallszahl im Intervall [0, 1]. Es sind also nur die Werte 0 und 1 möglich;
- Zeile 20: Der Thread wird nur angehalten, wenn die Zufallszahl 1 ist;
- Zeile 22: Es wird protokolliert, dass der Thread unterbrochen wird;
- Zeile 24: Der Thread wird für [sleep_time] Sekunden unterbrochen;
- Zeile 26: Wenn der Thread wieder aktiv wird, lässt er die Abfrage durch das Modul [index_controller] ausführen;
- Zeilen 28–32: Wenn diese Ausführung einen Fehler vom Typ [500 INTERNAL SERVER ERROR] verursacht, wird eine E-Mail an den Administrator gesendet;
- Zeilen 30–31: Das Wörterbuch [config_mail] wird konfiguriert und anschließend an die Klasse [SendAdminMail] übergeben;
- Zeile 32: Die an den Administrator gesendete Nachricht ist die Zeichenkette jSON aus dem Ergebnis, das an den Kunden gesendet wird;
- Zeilen 33–34: Die Antwort, die an den Kunden gesendet wird (Zeile 36), wird protokolliert;
- Zeilen 37–44: Behandlung einer möglichen Ausnahme;
- Zeilen 39–40: Falls der Logger vorhanden ist, wird der aufgetretene Fehler protokolliert;
- Zeilen 47–48: Der Logger wird geschlossen, sofern er vorhanden ist. Letztendlich erstellt der Thread zu Beginn der Anfrage einen Logger und schließt ihn, sobald diese bearbeitet wurde;
24.3.3. Der Controller [index_controller]
Der Controller [index_controller], der die Anfragen ausführt, ist der aus der vorherigen Version:

24.3.4. Ausführung
Wir starten den Flask-Server, den Mailserver |hMailServer| sowie das E-Mail-Programm |Thunderbird|. SGBD wird nicht gestartet. Der Server wird mit den folgenden Konsolenprotokollen beendet:
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-servers/02/flask/main.py mysql
[serveur] démarrage du serveur
L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]
Process finished with exit code 2
Die Protokolldatei [logs.txt] sieht wie folgt aus:
2020-07-23 11:51:38.324752, MainThread : [serveur] démarrage du serveur
2020-07-23 11:51:40.355510, MainThread : L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]
2020-07-23 11:51:42.464206, MainThread : [SendAdminMail] Message envoyé à [guest@localhost.com] : [L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]]
Mit Thunderbird werden die E-Mails des Administrators [guest@localhost.com] überprüft:

Anschließend starten wir SGBD und fordern URL sowie [http://127.0.0.1:5000/?mari%C3%A9=oui&enfants=3&salaire=200000] an. Die Protokolle sehen nun wie folgt aus:
2020-07-23 11:56:38.891753, MainThread : [serveur] démarrage du serveur
2020-07-23 11:56:38.987999, MainThread : [serveur] connexion à la base de données réussie
2020-07-23 11:56:40.586747, MainThread : [serveur] démarrage du serveur
2020-07-23 11:56:40.655254, MainThread : [serveur] connexion à la base de données réussie
2020-07-23 11:56:54.528360, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?verheiratet=ja&Kinder=3&Gehalt=200000' [GET]>
2020-07-23 11:56:54.530653, Thread-2 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
- Zeilen 1–4: Zur Erinnerung: Es gibt zwei Serverstarts, da der Modus [Debug=True] einen zweiten Start auslöst;
- Zeilen 5–6: Die Protokolle geben Aufschluss über die Ausführungszeit einer Anfrage, hier 2,293 Millisekunden;
24.4. Der Webclient


Die Datei [http-clients/02] wird durch Kopieren der Datei [http-clients/01] erstellt. Anschließend werden einige Änderungen vorgenommen.
24.4.1. Die Konfiguration
Die Konfiguration [config] der Anwendung [http-clients/02] entspricht bis auf wenige Details der Konfiguration der Anwendung [http-clients/01]:
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ôtsDaoWithHttpClient
f"{script_dir}/../services",
# Konfigurationsskripte
script_dir,
# Logger
f"{root_dir}/impots/http-servers/02/utilities",
]
# Syspath festlegen
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/",
"authBasic": True,
"user": {
"login": "admin",
"password": "admin"
}
},
# Debug-Modus
"debug": True
}
)
# Schritt 3 ------
# Instanziierung der Schichten
import config_layers
config['layers'] = config_layers.configure(config)
# Konfiguration wird zurückgesetzt
return config
- Zeilen 31–32: Wir verwenden denselben Logger |Logger| wie für den Server;
- Zeile 49: Der absolute Pfad zur Logdatei;
- Zeile 60: Der Modus [debug=True] dient dazu, die Antworten des Webservers in die Logdatei zu schreiben;
24.4.2. Die Schicht [dao]
Der Code der Klasse [ImpôtsDaoWithHttpClient] ändert sich geringfügig:
# Importe
import requests
from flask_api import status
…
class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
# Konstruktor
def __init__(self, config: dict):
# Initialisierung des übergeordneten Objekts
AbstractImpôtsDao.__init__(self, config)
# Speicherung der Konfigurationselemente
# allgemeine Konfiguration
self.__config = config
# Server
self.__config_server = config["server"]
# Debug-Modus
self.__debug = config["debug"]
# Logger
self.__logger = None
# nicht verwendete Methode
def get_admindata(self) -> AdminData:
pass
# Steuerberechnung
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
# Ausnahmen werden weitergeleitet
…
# Debug-Modus?
if self.__debug:
# Logger
if not self.__logger:
self.__logger = self.__config['logger']
# Protokollierung
self.__logger.write(f"{response.text}\n")
# Antwort-Statuscode HTTP
status_code = response.status_code
…
- Zeile 17: Die allgemeine Konfiguration wird gespeichert. Wir werden später sehen, dass beim Aufruf des Konstruktors der Klasse [ImpôtsDaoWithHttpClient] das Wörterbuch [config] den in Zeile 37 verwendeten Schlüssel [logger] noch nicht enthält. Aus diesem Grund kann [self.__logger] (Zeile 23) im Konstruktor nicht initialisiert werden;
- Zeile 21: In der Konfiguration wurde ein Schlüssel [debug] hinzugefügt, der das Protokoll der Zeilen 33–39 steuert;
- Zeile 34: wenn man sich im Modus [debug] befindet;
- Zeilen 36–37: Gegebenenfalls Initialisierung der Eigenschaft [self.__logger]. Bei Verwendung der Methode [calculate_tax] ist der Schlüssel [logger] Teil des Wörterbuchs [config];
- Zeile 39: Das mit der Antwort HTTP des Servers verbundene Textdokument wird protokolliert;
Die Schicht [dao] wird gleichzeitig von mehreren Threads ausgeführt. Hier wird jedoch nur eine einzige Instanz dieser Schicht erstellt (siehe config_layers). Es muss daher überprüft werden, ob der Code keinen Schreibzugriff auf gemeinsam genutzte Daten beinhaltet, typischerweise die Eigenschaften der Klasse [ImpôtsDaoWithHttpClient], die die Schicht [dao] implementiert. In Zeile 37 wird jedoch eine Eigenschaft der Klasseninstanz geändert. Hier hat dies keine Auswirkungen, da alle Threads denselben Logger gemeinsam nutzen. Wäre dies nicht der Fall gewesen, hätte der Zugriff auf Zeile 37 synchronisiert werden müssen.
24.4.3. Das Hauptskript
Das Hauptskript [main] entwickelt sich wie folgt:
# Anwendung wird konfiguriert
import config
config = config.configure({})
# Abhängigkeiten
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger
# Ausführung der Schicht [dao] in einem Thread
# „taxpayers“ ist eine Liste von Steuerzahlern
def thread_function(dao, logger, taxpayers: list):
…
# Liste der Client-Threads
threads = []
logger = None
# Code
try:
# Logger
logger = Logger(config["logsFilename"])
# wird in der Konfiguration gespeichert
config["logger"] = logger
# die Ebene wird abgerufen [dao]
dao = config["layers"]["dao"]
# Auslesen 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=(dao, logger, thread_taxpayers))
# Er wird zur Liste der Threads des Hauptskripts hinzugefügt
threads.append(thread)
# Der Thread wird gestartet – dieser Vorgang ist 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)
except BaseException as erreur:
# Anzeige des Fehlers
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# Das Logger-Programm wird geschlossen
if logger:
logger.close()
# Fertig
print("Travail terminé...")
# Beendigung der Threads, die möglicherweise noch bestehen, falls der Vorgang aufgrund eines Fehlers abgebrochen wurde
sys.exit()
- Das Hauptskript unterscheidet sich von dem des vorherigen Clients dadurch, dass es mehrere Ausführungsthreads generiert, um die Anfragen an den Server zu stellen. Der Client der Version 6 führte alle seine Anfragen nacheinander aus. Die Anfrage Nr. i wurde erst gestellt, nachdem die Antwort auf die Anfrage Nr. [i-1] eingegangen war. Hier wollen wir untersuchen, wie sich der Server verhält, wenn er mehrere gleichzeitige Anfragen erhält. Dazu benötigen wir Threads;
- Zeile 21: Die erzeugten Threads werden in eine Liste aufgenommen. Dabei ist zu beachten, dass das Skript [main] ebenfalls von einem Thread namens [MainThread] ausgeführt wird. Dieser Haupt-Thread erstellt weitere Threads, die für die Berechnung der Steuer eines oder mehrerer Steuerzahler zuständig sind;
- Zeile 26: Es wird ein Logger erstellt. Dieser wird von allen Threads gemeinsam genutzt;
- Zeile 32: Es werden alle Steuerpflichtigen abgerufen, deren Steuer berechnet werden muss;
- Zeilen 39–51: Diese Steuerzahler werden auf mehrere Threads verteilt;
- Zeilen 40–41: Jeder Thread bearbeitet 1 bis 4 Steuerzahler. Diese Anzahl wird zufällig festgelegt;
- [random.randint(1, 4)] gibt zufällig eine Zahl aus der Liste [1, 2, 3, 4] aus;
- Der Thread darf nicht mehr als [l-i] Steuerzahler haben, wobei [l-i] die Anzahl der Steuerzahler angibt, denen noch kein Thread zugewiesen wurde;
- daher wird der kleinere der beiden Werte genommen;
- Zeile 43: Sobald [nb_taxpayers], die Anzahl der vom Thread bearbeiteten Steuerzahler, bekannt ist, werden diese aus der Liste der Steuerzahler entnommen:
- [slice(10,12)] ist die Menge der Indizes [10, 11, 12];
- [résultat["réponse"]] ist die Liste [taxpayers[10], taxpayers[11], taxpayers[12];
- Zeile 45: Der Wert von i, der die Schleife in Zeile 39 steuert, wird erhöht;
- Zeile 47: Es wird ein Thread erstellt:
- [target=thread_function] legt die Funktion fest, die der Thread ausführen wird. Es handelt sich um die Funktion in den Zeilen 16–17. Sie erwartet drei Parameter;
- [ags] ist die Liste der drei Parameter, die von der Funktion [thread_function] erwartet werden;
Das Erstellen eines Threads führt diesen noch nicht aus. Es wird lediglich ein Objekt erstellt;
- Zeilen 48–49: Der soeben erstellte Thread wird der Liste der vom Hauptthread erstellten Threads hinzugefügt;
- Zeile 51: Der Thread wird gestartet. Er wird nun parallel zu den anderen aktiven Threads ausgeführt. Hier führt er die Funktion [thread_function] mit den übergebenen Argumenten aus;
- Zeilen 53–54: Der Hauptthread wartet auf jeden der von ihm gestarteten Threads. Nehmen wir ein Beispiel:
- Der Hauptthread hat drei Threads vom Typ [th1, th2, th3] gestartet;
- der Hauptthread wartet auf jeden einzelnen Thread (Zeilen 53–54) in der Reihenfolge der for-Schleife: [th1, th2, th3];
- Nehmen wir an, die Threads enden in der Reihenfolge [th2, th1, th3];
- Der Hauptthread wartet auf das Ende von th1. Wenn th2 endet, passiert nichts;
- Wenn th1 endet, wartet der Hauptthread auf th2. Dieser ist jedoch bereits beendet. Der Hauptthread wechselt dann zum nächsten Thread und wartet auf th3;
- Wenn th3 beendet ist, hat der Haupt-Thread das Warten beendet und fährt mit der Ausführung von Zeile 57 fort;
- Zeile 57 schreibt die erzielten Ergebnisse in die Ergebnisdatei. Dies ist ein gutes Beispiel für Objektreferenzen:
- Zeile 43: Die einem Thread zugeordnete Liste [thread_payers] enthält Kopien der Objektreferenzen, die in der Liste [taxpayers] enthalten sind;
- Es ist bekannt, dass die Steuerberechnung die Objekte verändert, auf die die Referenzen der Liste [thread_payers] verweisen. Diese Objekte werden mit den Ergebnissen der Steuerberechnung ergänzt. Die Referenzen selbst werden jedoch nicht verändert. Die Referenzen der ursprünglichen Liste [taxpayers] „sehen“ also die geänderten Objekte oder „verweisen“ auf diese;
Die von den Threads ausgeführte Funktion [thread_function] lautet wie folgt:
# Ausführung der Ebene [dao] in einem Thread
# „taxpayers“ ist eine Liste von Steuerzahlern
def thread_function(dao, logger, taxpayers: list):
# Protokoll: Start des Threads
thread_name = threading.current_thread().name
logger.write(f"début du thread [{thread_name}] avec {len(taxpayers)} contribuable(s)\n")
# Die Steuern der Steuerzahler werden berechnet
for taxpayer in taxpayers:
# Protokoll
logger.write(f"début du calcul de l'impôt de {taxpayer}\n")
# Synchrone Steuerberechnung
dao.calculate_tax(taxpayer)
# Protokoll
logger.write(f"fin du calcul de l'impôt de {taxpayer}\n")
# Protokoll: Ende des Threads
logger.write(f"fin du thread [{thread_name}]\n")
- Funktionen, die gleichzeitig von mehreren Threads ausgeführt werden, sind oft schwierig zu programmieren: Man muss stets sicherstellen, dass der Code nicht versucht, gemeinsam genutzte Daten zwischen den Threads zu ändern. In diesem Fall muss ein synchronisierter Zugriff auf die gemeinsam genutzten Daten eingerichtet werden, die geändert werden sollen;
- Zeile 3: Die Funktion erhält drei Parameter:
- [dao]: eine Referenz auf die Ebene [dao]. Diese Daten werden gemeinsam genutzt;
- [logger]: eine Referenz auf den Logger. Diese Daten werden gemeinsam genutzt;
- [taxpayers]: eine Liste von Steuerzahlern. Diese Daten werden nicht gemeinsam genutzt: Jeder Thread verwaltet eine eigene Liste;
- Betrachten wir die beiden Referenzen [dao, logger]:
- Wir haben gesehen, dass das Objekt, auf das die Referenz [dao] verweist, eine Referenz [self.__logger] hatte, die von den Threads geändert wurde, allerdings nur, um einen für alle Threads gemeinsamen Wert einzutragen;
- Die Referenz [logger] verweist auf einen Dateideskriptor. Wir haben gesehen, dass es beim Schreiben der Protokolle in die Datei zu einem Problem kommen kann. Aus diesem Grund wurde das Schreiben in die Datei synchronisiert;
- Zeilen 5–6: Der Name des Threads und die Anzahl der Steuerzahler, die er verwalten muss, werden protokolliert;
- Zeilen 8–14: Berechnung der Steuern der Steuerzahler;
- Zeile 16: Das Ende des Threads wird protokolliert;
24.4.4. Ausführung
Starten wir den Webserver wie im vorherigen Abschnitt beschrieben (Webserver, SGBD, hMailServer, Thunderbird) und führen wir anschließend das Skript [main] auf dem Client aus. In den Dateien [data/output/errors.txt, data/output/résultats.json] erhalten wir dieselben Ergebnisse wie in der vorherigen Version. In der Datei [data/logs/logs.txt] finden sich folgende Protokolleinträge:
2020-07-24 10:05:20.942404, Thread-1 : début du thread [Thread-1] avec 1 contribuable(s)
2020-07-24 10:05:20.943458, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-24 10:05:20.943458, Thread-2 : début du thread [Thread-2] avec 3 contribuable(s)
2020-07-24 10:05:20.946502, Thread-3 : début du thread [Thread-3] avec 1 contribuable(s)
2020-07-24 10:05:20.946502, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-24 10:05:20.947003, Thread-3 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000}
2020-07-24 10:05:20.947003, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
2020-07-24 10:05:20.950324, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000}
2020-07-24 10:05:20.948449, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
2020-07-24 10:05:20.953645, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000}
2020-07-24 10:05:20.976143, Thread-1 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:20.976695, Thread-1 : fin du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-24 10:05:20.976695, Thread-1 : fin du thread [Thread-1]
2020-07-24 10:05:21.973914, Thread-2 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}}}
2020-07-24 10:05:21.973914, Thread-2 : fin du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}
2020-07-24 10:05:21.973914, Thread-2 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-24 10:05:21.977130, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:21.977130, Thread-4 : fin du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}
2020-07-24 10:05:21.977130, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000}
2020-07-24 10:05:21.982634, Thread-3 : {"réponse": {"result": {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:21.982634, Thread-5 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:21.983134, Thread-3 : fin du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-24 10:05:21.983134, Thread-5 : fin du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}
2020-07-24 10:05:21.983134, Thread-3 : fin du thread [Thread-3]
2020-07-24 10:05:21.983763, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000}
2020-07-24 10:05:22.008562, Thread-5 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:22.008562, Thread-5 : fin du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}
2020-07-24 10:05:22.009062, Thread-5 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-24 10:05:22.016848, Thread-5 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:22.017349, Thread-5 : fin du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-24 10:05:22.017349, Thread-5 : fin du thread [Thread-5]
2020-07-24 10:05:23.008486, Thread-2 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}}}
2020-07-24 10:05:23.008486, Thread-2 : fin du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}
2020-07-24 10:05:23.009749, Thread-2 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000}
2020-07-24 10:05:23.011722, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:23.013723, Thread-4 : fin du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-24 10:05:23.013723, Thread-4 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000}
2020-07-24 10:05:23.024135, Thread-2 : {"réponse": {"result": {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:23.024135, Thread-2 : fin du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-24 10:05:23.025178, Thread-2 : fin du thread [Thread-2]
2020-07-24 10:05:23.025178, Thread-4 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-24 10:05:23.026191, Thread-4 : fin du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-24 10:05:23.026191, Thread-4 : fin du thread [Thread-4]
- Diese Protokolle zeigen, dass fünf Threads gestartet wurden, um die Steuer für 11 Steuerzahler zu berechnen. Diese fünf Threads haben gleichzeitig Anfragen an den Steuerberechnungsserver gesendet. Man muss verstehen, wie das funktioniert:
- Der Thread [Thread-1] wird als erster gestartet. Sobald er die Prozessorausführung erhält, durchläuft er den Code, bis er seine Anfrage HTTP sendet. Da er auf das Ergebnis dieser Anfrage warten muss, wird er automatisch in den Wartezustand versetzt. Er verliert daraufhin die Prozessorausführung, und ein anderer Thread erhält diese;
- Zeilen 1–10: Der gleiche Vorgang wiederholt sich für jeden der 5 Threads. Somit werden die 5 Threads gestartet, noch bevor der Thread [Thread-1] seine Antwort erhalten hat (Zeile 11);
- Die Threads werden nicht in der Reihenfolge beendet, in der sie gestartet wurden. Somit wird der Thread [Thread-3] als erster beendet (Zeile 23);
Auf der Serverseite lauten die Protokolleinträge in der Datei [data/logs/logs.txt] wie folgt:
2020-07-24 10:05:01.692980, MainThread : [serveur] démarrage du serveur
2020-07-24 10:05:01.877251, MainThread : [serveur] connexion à la base de données réussie
2020-07-24 10:05:03.596162, MainThread : [serveur] démarrage du serveur
2020-07-24 10:05:03.661160, MainThread : [serveur] connexion à la base de données réussie
2020-07-24 10:05:20.968053, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?verheiratet=ja&Kinder=2&Gehalt=50000' [GET]>
2020-07-24 10:05:20.969132, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.970316, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?verheiratet=ja&Kinder=3&Gehalt=100000' [GET]>
2020-07-24 10:05:20.970316, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.971335, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?verheiratet=ja&Kinder=2&Gehalt=55555' [GET]>
2020-07-24 10:05:20.972563, Thread-4 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 55555, 'impôt': 2814, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:20.974796, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?verheiratet=nein&Kinder=3&Gehalt=100000' [GET]>
2020-07-24 10:05:20.974796, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.976143, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?verheiratet=ja&Kinder=2&Gehalt=30000' [GET]>
2020-07-24 10:05:20.976143, Thread-6 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:21.970615, Thread-2 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'taux': 0.14, 'décôte': 384, 'réduction': 347}}}
2020-07-24 10:05:21.973914, Thread-3 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'taux': 0.3, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:21.977130, Thread-6 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'taux': 0.0, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:21.977130, Thread-5 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 3, 'salaire': 100000, 'impôt': 16782, 'surcôte': 7176, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:22.001693, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?verheiratet=ja&Kinder=3&Gehalt=50000' [GET]>
2020-07-24 10:05:22.003013, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:22.003013, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?verheiratet=ja&Kinder=5&Gehalt=100000' [GET]>
2020-07-24 10:05:22.003013, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:22.005871, Thread-9 : [index] requête : <Request 'http://127.0.0.1:5000/?verheiratet=nein&Kinder=0&Gehalt=200000' [GET]>
2020-07-24 10:05:22.006370, Thread-9 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0, 'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'taux': 0.45, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:22.014170, Thread-10 : [index] requête : <Request 'http://127.0.0.1:5000/?verheiratet=ja&Kinder=3&Gehalt=200000' [GET]>
2020-07-24 10:05:22.014170, Thread-10 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:23.003533, Thread-7 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 50000, 'impôt': 0, 'surcôte': 0, 'taux': 0.14, 'décôte': 720, 'réduction': 0}}}
2020-07-24 10:05:23.006434, Thread-8 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:23.018026, Thread-11 : [index] requête : <Request 'http://127.0.0.1:5000/?verheiratet=nein&Kinder=2&Gehalt=100000' [GET]>
2020-07-24 10:05:23.019074, Thread-11 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-24 10:05:23.021447, Thread-12 : [index] requête : <Request 'http://127.0.0.1:5000/?verheiratet=nein&Kinder=0&Gehalt=100000' [GET]>
2020-07-24 10:05:23.022447, Thread-12 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0, 'salaire': 100000, 'impôt': 22986, 'surcôte': 0, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
- Man sieht, dass 11 Threads die 11 Steuerzahler bearbeitet haben;
- Einige Threads wurden in die Warteschlange gestellt (Zeilen 6, 8, 12, 14, 20, 22), andere nicht (Zeilen 9, 23, 25, 29, 31);
24.5. Tests der Schicht [dao]
Wie bereits in der |vorherigen Version| testen wir die Client-Schicht [dao]. Das Prinzip ist genau dasselbe:

Die Testklasse wird in der folgenden Umgebung ausgeführt:

- Die Konfiguration [2] ist identisch mit der Konfiguration [1], die wir gerade untersucht haben;
Die Testklasse [TestHttpClientDao] lautet wie folgt:
import unittest
from Logger import Logger
class TestHttpClientDao(unittest.TestCase):
def test_1(self) -> None:
from TaxPayer import TaxPayer
# {'verheiratet': 'ja', 'Kinder': 2, 'Gehalt': 55555,
# 'Steuer': 2814, 'Zuschlag': 0, 'Abschlag': 0, 'Ermäßigung': 0, 'Satz': 0,14}
taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
dao.calculate_tax(taxpayer)
# Überprüfung
self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
self.assertEqual(taxpayer.décôte, 0)
self.assertEqual(taxpayer.réduction, 0)
self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
self.assertEqual(taxpayer.surcôte, 0)
…
if __name__ == '__main__':
# Die Anwendung wird konfiguriert
import config
config = config.configure({})
# Logger
logger = Logger(config["logsFilename"])
# wird in der Konfiguration gespeichert
config["logger"] = logger
# die Ebene [dao] wird abgerufen
dao = config["layers"]["dao"]
# die Testmethoden werden ausgeführt
print("tests en cours...")
unittest.main()
- Wir erstellen eine |Ausführungskonfiguration| für diesen Test;
- Wir starten den Webserver mit seiner gesamten Umgebung;
- Wir führen den Test aus;
Die Ergebnisse lauten wie folgt:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-clients/02/tests/TestHttpClientDao.py
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 6.128s
OK
Process finished with exit code 0