Skip to content

24. Übungsaufgabe: Version 7

24.1. Einführung

Version 7 der Steuerberechnungsanwendung ist bis auf die folgenden Details identisch mit Version 6:

  • Der Web-Client sendet mehrere HTTP-Anfragen gleichzeitig. In der vorherigen Version wurden diese Anfragen nacheinander gesendet. Der Server konnte daher jeweils nur eine einzige Anfrage bearbeiten;
  • Der Server wird multithreaded sein: Er wird in der Lage sein, mehrere Anfragen gleichzeitig zu verarbeiten;
  • Um die Ausführung dieser Anfragen zu verfolgen, wird der Webserver mit einem Logger ausgestattet, der wichtige Momente der Anfrageverarbeitung in einer Textdatei aufzeichnet;
  • Der Server sendet eine E-Mail an den Anwendungsadministrator, wenn er auf ein Problem stößt, das den Start verhindert, typischerweise ein Problem mit der dem Webserver zugeordneten Datenbank;

Die Anwendungsarchitektur bleibt unverändert:

Image

Die Verzeichnisstruktur der Skripte sieht wie folgt aus:

Image

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

24.2. Die Hilfsprogramme

Image

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:
    #  class attribute
    verrou = threading.RLock()

    #  manufacturer
    def __init__(self, logs_filename: str):
        try:
            #  open the file in append mode (a)
            self.__resource = codecs.open(logs_filename, "a", "utf-8")
        except BaseException as erreur:
            raise ImpôtsError(18, f"{erreur}")

    #  writing a log
    def write(self, message: str):
        #  current date / time
        today = date.today()
        now = datetime.time(datetime.now())
        #  thread name
        thread_name = current_thread().name
        #  you don't want to be disturbed while writing to the log file
        #  we request the class's synchronization object (= lock) - only one thread will get it
        Logger.verrou.acquire()
        try:
            #  log entry
            self.__resource.write(f"{today} {now}, {thread_name} : {message}")
            #  write immediately - otherwise the text will only be written when the write stream is closed
            #  we want to track logs over time
            self.__resource.flush()
        finally:
            #  release the synchronization object (= lock) so that another thread can obtain it
            Logger.verrou.release()

    #  freeing up resources
    def close(self):
        #  close file
        if self.__resource:
            self.__resource.close()
  • Zeilen 10–11: Wir definieren ein Klassenattribut. Ein Klassenattribut ist eine Eigenschaft, die allen Instanzen der Klasse gemeinsam ist. Es wird mit der Notation [Klasse.Klassenattribut] referenziert (Zeilen 30, 39). Das Klassenattribut [lock] dient als Synchronisationsobjekt für alle Threads, die den Code in den Zeilen 31–36 ausführen;
  • Zeilen 14–19: Der Konstruktor erhält den absoluten Pfad der Protokolldatei. Diese Datei wird dann geöffnet, und der abgerufene Dateideskriptor wird in der Klasse gespeichert;
  • Zeile 17: Die Protokolldatei wird im Modus „Anhängen“ (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 Nachricht in die Protokolldatei zu schreiben. Dieser Nachricht 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. Es ist wichtig, sich hier vor Augen zu halten, dass eine Webanwendung mehrere Benutzer gleichzeitig bedient. Jeder Anfrage wird ein Thread zugewiesen, um sie auszuführen. Wenn dieser Thread angehalten wird – typischerweise für eine E/A-Operation (Netzwerk, Dateien, Datenbank) – wird der Prozessor an einen anderen Thread übergeben. Aufgrund dieser möglichen Unterbrechungen können wir nicht sicher sein, dass es einem Thread gelingt, eine Zeile in die Protokolldatei zu schreiben, ohne unterbrochen zu werden. Es besteht daher das Risiko, dass Protokolle von zwei verschiedenen Threads durcheinander geraten. Das Risiko ist gering, vielleicht sogar gleich Null, aber wir haben uns dennoch entschlossen, zu zeigen, wie der Zugriff zweier Threads auf eine gemeinsam genutzte Ressource, in diesem Fall die Protokolldatei, synchronisiert wird;
  • Zeile 30: Vor dem Schreiben fordert der Thread den Schlüssel für die Eingangstür an. Der angeforderte Schlüssel ist derjenige, der in Zeile 11 erstellt wurde. Er ist tatsächlich eindeutig: Ein Klassenattribut ist für alle Instanzen der Klasse eindeutig;
    • Zum Zeitpunkt T1 erhält ein Thread namens Thread1 den Schlüssel. Er kann dann Zeile 33 ausführen;
    • Zum Zeitpunkt T2 wird der Thread Thread1 angehalten, noch bevor er das Schreiben des Protokolls beendet hat;
    • Zum Zeitpunkt T3 muss der Thread Thread2, der den Prozessor übernommen hat, ebenfalls ein Protokoll schreiben. Er erreicht somit Zeile 30, wo er den Schlüssel für die Eingangstür anfordert. Ihm wird mitgeteilt, dass ein anderer Thread diesen bereits besitzt. Er wird daraufhin automatisch angehalten. Dies gilt für alle Threads, die diesen Schlüssel anfordern;
    • Zum Zeitpunkt T4 erhält der Thread Thread1, der angehalten worden war, den Prozessor zurück. 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. Der [write]-Befehl in Zeile 33 schreibt in diesen Puffer, jedoch nicht direkt in die Datei. Der Puffer wird dann unter bestimmten Bedingungen in die Datei geschrieben:
        • der Puffer ist voll;
        • der Dateideskriptor wird mit [close] oder [flush] bearbeitet;
  • Zeile 36: Wir erzwingen das Schreiben der Protokollzeile in die Datei. Wir tun dies, weil wir die Protokolle der verschiedenen Threads miteinander vermischt sehen wollen. Wenn wir dies nicht tun, werden die Protokolle eines einzelnen Threads alle gleichzeitig geschrieben – nämlich wenn der Deskriptor in Zeile 45 geschlossen wird. Es wäre dann viel schwieriger zu erkennen, dass bestimmte Threads angehalten wurden: Wir müssten die Zeitstempel in den Protokollen überprüfen;
  • Zeile 39: Der Thread1 gibt die ihm zugewiesene Sperre zurück. Sie kann nun einem anderen Thread zugewiesen werden;
  • Zeile 22: Die [write]-Methode ist daher synchronisiert: Es schreibt jeweils nur ein Thread in die Logdatei. Der Schlüssel zu diesem Mechanismus ist Zeile 30: Egal, was passiert, nur ein Thread ruft den Schlüssel ab, um zur nächsten Zeile zu gelangen. Er behält ihn, bis er ihn zurückgibt (Zeile 39);
  • Zeilen 41–45: Die Methode [close] gibt die dem Log-Dateideskriptor zugewiesenen Ressourcen frei;

Die in die Protokolldatei geschriebenen Protokolle sehen wie folgt aus:

2020-07-22 20:03:52.992152, Thread-2 : …

24.2.2. Die Klasse [SendAdminMail]

Die Klasse [SendAdminMail] ermöglicht es Ihnen, bei einem Absturz der Anwendung eine Nachricht an den Anwendungsadministrator zu senden.

Image

Die Klasse [SendAdminMail] wird im Skript [config] [2] wie folgt konfiguriert:

        #  server config SMTP
        "adminMail": {
            #  server SMTP
            "smtp-server": "localhost",
            #  server port SMTP
            "smtp-port": "25",
            #  director
            "from": "guest@localhost.com",
            "to": "guest@localhost.com",
            #  mail subject
            "subject": "plantage du serveur de calcul d'impôts",
            #  tls to True if server SMTP requires authorization, False otherwise
            "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:

#  imports
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):
        #  sends message to smtp server config['smtp-server'] on port config[smtp-port]
        #  if config['tls'] is true, TLS support will be used
        #  mail is sent from config['from']
        #  for recipient config['to']
        #  message has subject config['subject']
        #  a logger reference can be found in config['logger']

        #  retrieve logger from config - can be None
        logger = config["logger"]
        #  server SMTP
        server = None
        #  we send the message
        try:
            #  the SMTP server
            server = smtplib.SMTP(config["smtp-server"])
            #  verbose mode
            server.set_debuglevel(verbose)
            #  secure connection?
            if config['tls']:
                #  start of safety dialogue
                server.starttls()
                #  authentication
                server.login(config["user"], config["password"])
            #  construction of a Multipart message - this is the message that will be sent
            msg = MIMEText(message)
            msg['From'] = config["from"]
            msg['To'] = config["to"]
            msg['Date'] = formatdate(localtime=True)
            msg['Subject'] = config["subject"]
            #  we send the message
            server.send_message(msg)
            #  log - the logger may not exist
            if logger:
                logger.write(f"[SendAdminMail] Message envoyé à [{config['to']}] : [{message}]\n")
        except BaseException as erreur:
            #  log- the logger may not exist
            if logger:
                logger.write(
                    f"[SendAdminMail] Erreur [{erreur}] lors de l'envoi à [{config['to']}] du message [{message}] : \n")
        finally:
            #  we're done - we release the resources mobilized by the function
            if server:
                server.quit()
  • Zeilen 24–54: Dies ist der Code, der bereits im Beispiel |smtp/02| behandelt wurde;
  • Zeile 20: Wir rufen die Referenz eines Loggers ab. Diese wird in den Zeilen 45 und 49 verwendet;

24.3. Der Webserver

24.3.1. Konfiguration

Die Serverkonfiguration ist der zuvor besprochenen sehr ähnlich. Lediglich die Datei [config.py] hat sich geringfügig geändert:

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

    #  step 1 ------

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

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

    #  absolute dependencies
    absolute_dependencies = [
        #  project files
        #  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",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  IndexController
        f"{root_dir}/impots/http-servers/01/controllers",
        #  scripts [config_database, config_layers]
        script_dir,
        #  Logger, SendAdminMail
        f"{script_dir}/../utilities",
    ]
    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration
    config.update({
        #  users authorized to use the application
        "users": [
            {
                "login": "admin",
                "password": "admin"
            }
        ],
        #  log file
        "logsFilename": f"{script_dir}/../data/logs/logs.txt",
        #  server config SMTP
        "adminMail": {
            #  server SMTP
            "smtp-server": "localhost",
            #  server port SMTP
            "smtp-port": "25",
            #  director
            "from": "guest@localhost.com",
            "to": "guest@localhost.com",
            #  mail subject
            "subject": "plantage du serveur de calcul d'impôts",
            #  tls to True if server SMTP requires authorization, False otherwise
            "tls": False
        },
        #  thread pause time in seconds
        "sleep_time": 0
    })

    #  step 3 ------
    #  database configuration
    import config_database
    config["database"] = config_database.configure(config)

    #  step 4 ------
    #  instantiation of application layers
    import config_layers
    config['layers'] = config_layers.configure(config)

    #  we return the configuration
    return config
  • Zeilen 40–66: Wir fügen dem Konfigurationswörterbuch des Servers Elemente hinzu, die sich auf den Logger (Zeile 49) und auf das Versenden einer Warn-E-Mail an den Anwendungsadministrator (Zeilen 51–63) beziehen;
  • Zeile 65: Um die Threads besser in Aktion zu sehen, werden wir einige von ihnen zwingen, eine Pause einzulegen. [sleep_time] ist die Dauer der Pause in Sekunden;
  • Zeilen 27–28: Beachten Sie, dass wir den [index_controller] aus der vorherigen Version 6 verwenden;

24.3.2. Das Hauptskript [main]

Das Hauptskript [main] lautet wie folgt:

#  a mysql or pgres parameter is expected
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()

#  configure the application
import config
config = config.configure({'sgbd': sgbd})

#  dependencies
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

#  authentication manager
auth = HTTPBasicAuth()


@auth.verify_password
def verify_password(login, password):
    #  user list
    users = config['users']
    #  browse this list
    for user in users:
        if user['login'] == login and user['password'] == password:
            return True
    #  we didn't find
    return False


#  send an e-mail to the administrator
def send_adminmail(config: dict, message: str):
    #  send an e-mail to the application administrator
    config_mail = config["adminMail"]
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)


#  check log file
logger = None
erreur = False
message_erreur = None
try:
    #  logger
    logger = Logger(config["logsFilename"])
except BaseException as exception:
    #  log console
    print(f"L'erreur suivante s'est produite : {exception}")
    #  we note the error
    erreur = True
    message_erreur = f"{exception}"
#  store the logger in the config
config['logger'] = logger
#  error handling
if erreur:
    #  mail to administrator
    send_adminmail(config, message_erreur)
    #  end of application
    sys.exit(1)

#  start-up log
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)

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

#  the main thread no longer needs the logger
logger.close()

#  if there has been an error, we stop
if erreur:
    sys.exit(2)

#  the Flask application can be started
app = Flask(__name__)


#  Home URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    


#  hand only
if __name__ == '__main__':
    #  start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • Zeilen 1–10: Das Skript erwartet einen Parameter [mysql / pgres], der das zu verwendende DBMS angibt;
  • Zeilen 12–14: Die Anwendung wird konfiguriert (Python-Pfad, Layers, Datenbank);
  • Zeilen 16–28: Von der Anwendung benötigte Abhängigkeiten;
  • Zeilen 30–43: Authentifizierungsmanagement;
  • Zeilen 46–51: Eine Funktion, die eine E-Mail an den Anwendungsadministrator sendet;
  • Die Funktion erwartet zwei Parameter:
      • config: ein Wörterbuch mit den Schlüsseln [adminMail] und [logger];
      • die zu versendende Nachricht;
    • Zeilen 49–50: Wir bereiten die E-Mail-Konfiguration vor;
    • wir versenden die E-Mail;
  • Zeilen 54–74: Wir prüfen, ob die Protokolldatei vorhanden ist;
  • Zeilen 70–74: Wenn wir die Protokolldatei nicht öffnen konnten, senden wir eine E-Mail an den Administrator und beenden das Programm;
  • Zeilen 76–79: Protokollierung des Serverstarts;
  • Zeilen 81–98: Wir rufen die Steuerverwaltungsdaten aus der Datenbank ab;
  • Zeilen 88–98: Wenn wir diese Daten nicht abrufen konnten, protokollieren wir den Fehler sowohl auf der Konsole als auch in der Protokolldatei;
  • Zeilen 100–101: Der Hauptthread protokolliert nicht mehr (die erstellten Threads verwenden nicht denselben Dateideskriptor);
  • Zeilen 103–105: Wenn wir keine Verbindung zur Datenbank herstellen konnten, brechen wir ab;
  • Zeile 122: Der Server wird im Multithread-Modus gestartet;

Die Funktion [index] (Zeile 114) lautet wie folgt:

#  Home URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    logger = None
    try:
        #  logger
        logger = Logger(config["logsFilename"])
        #  we store it in a config associated with the thread
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        #  log the request
        logger.write(f"[index] requête : {request}\n")
        #  the thread is interrupted if requested
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            #  pause is randomized so that some threads are interrupted and others not
            aléa = randint(0, 1)
            if aléa == 1:
                #  log before break
                logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                #  break
                time.sleep(sleep_time)
        #  the request is executed by a controller
        résultat, status_code = index_controller.execute(request, config)
        #  was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            #  send an e-mail to the application administrator
            config_mail = config["adminMail"]
            config_mail["logger"] = logger
            SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
        #  we log the answer
        logger.write(f"[index] {résultat}\n")
        #  we send the answer
        return json_response(résultat, status_code)
    except BaseException as erreur:
        #  log the error if possible
        if logger:
            logger.write(f"[index] {erreur}")
        #  we prepare the response to the customer
        résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
        #  we send the answer
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        #  close the log file if it has been opened
        if logger:
            logger.close()
  • Zeile 4: Die Funktion, die ausgeführt wird, wenn ein Benutzer die URL / anfordert. 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. Behalte dies immer im Hinterkopf, wenn der Code auf eine Ressource zugreift, die von allen Threads gemeinsam genutzt wird. In diesem Fall ist diese Ressource die Protokolldatei: Alle Threads schreiben in sie;
  • Zeile 8: Wir erstellen eine Instanz des Loggers. Somit verfügen alle Threads über eine eigene Instanz des Loggers. Alle diese Logger verweisen jedoch auf dieselbe Logdatei. Es ist dennoch wichtig zu beachten, dass das Schließen des Loggers durch einen Thread keine Auswirkungen auf die Logger der anderen Threads hat;
  • Zeilen 9–12: Wir speichern den Logger im [config]-Wörterbuch der Anwendung unter einem Schlüssel, der nach dem Thread benannt ist. Wenn also n Threads gleichzeitig laufen, werden n Einträge im [config]-Wörterbuch erstellt. [config] ist eine Ressource, die von allen Threads gemeinsam genutzt wird. 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 ihre Einträge in der [config]-Datei 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 diese Annahme im Test als falsch erweisen, müsste der Zugriff auf Zeile 12 synchronisiert werden;
  • Zeile 10: Wir legen den Logger in einem Wörterbuch ab;
  • Zeile 11: [threading.current_thread()] ist der Thread, der diese Zeile ausführt, und somit der Thread, der die Funktion [index] ausführt. Wir speichern seinen Namen. Jeder Thread hat einen eindeutigen Namen;
  • Zeile 12: Wir speichern die Konfiguration des Threads. Von nun an gehen wir immer wie folgt vor: Wenn es Informationen gibt, die nicht zwischen Threads geteilt werden können, werden sie dennoch in der allgemeinen Konfiguration abgelegt, jedoch mit dem Namen des Threads verknüpft;
  • Zeile 14: Wir protokollieren die Anfrage, die wir gerade ausführen;
  • Zeilen 15–24: Wir halten bestimmte Threads nach dem Zufallsprinzip an, damit sie den Prozessor an einen anderen Thread abgeben;
    • Zeile 16: Wir lesen die Pausendauer (in Sekunden) aus der Konfiguration aus;
    • Zeile 17: Eine Pause tritt nur auf, wenn die Pausendauer ungleich 0 ist;
    • Zeile 19: eine zufällige ganze Zahl im Bereich [0, 1]. Daher sind nur die Werte 0 und 1 möglich;
    • Zeile 20: Der Thread wird nur angehalten, wenn die Zufallszahl 1 ist;
    • Zeile 22: Wir protokollieren, dass der Thread kurz vor einer Pause steht;
    • Zeile 24: Der Thread wird für [sleep_time] Sekunden angehalten;
  • Zeile 26: Wenn der Thread wieder aktiv wird, lässt er das Modul [index_controller] die Anfrage ausführen;
  • Zeilen 28–32: Wenn diese Ausführung einen [500 INTERNAL SERVER ERROR] verursacht, wird eine E-Mail an den Administrator gesendet;
    • Zeilen 30–31: Wir konfigurieren das [config_mail]-Wörterbuch, das wir an die [SendAdminMail]-Klasse übergeben werden;
    • Zeile 32: Die an den Administrator gesendete Nachricht ist die JSON-Zeichenkette des Ergebnisses, das an den Client gesendet wird;
  • Zeilen 33–34: Wir protokollieren die Antwort, die an den Client gesendet wird (Zeile 36);
  • Zeilen 37–44: Behandlung etwaiger Ausnahmen;
  • Zeilen 39–40: Wenn der Logger existiert, protokollieren wir den aufgetretenen Fehler;
  • Zeilen 47–48: Wir schließen den Logger, falls er existiert. Letztendlich erstellt der Thread zu Beginn der Anfrage einen Logger und schließt ihn, sobald die Anfrage verarbeitet wurde;

24.3.3. Der Controller [index_controller]

Der Controller [index_controller], der die Anfragen ausführt, ist derselbe wie in der vorherigen Version:

Image

24.3.4. Ausführung

Wir starten den Flask-Server, den E-Mail-Server |hMailServer| und den E-Mail-Client |Thunderbird|. Das DBMS starten wir nicht. Der Server stoppt mit den folgenden Konsolenprotokollen:


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


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

Überprüfen Sie mit Thunderbird die E-Mails des Administrators [guest@localhost.com]:

Image

Starten Sie anschließend das DBMS und rufen Sie die URL [http://127.0.0.1:5000/?mari%C3%A9=oui&enfants=3&salaire=200000] auf. Die Protokolle sehen dann 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/?marié=oui&enfants=3&salaire=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: Beachten Sie, dass der Server zweimal startet, da der Modus [Debug=True] einen zweiten Start auslöst;
  • Zeilen 5–6: Die Protokolle geben uns einen Eindruck von der Ausführungszeit einer Anfrage, hier 2,293 Millisekunden;

24.4. Der Web-Client

Image

Das Verzeichnis [http-clients/02] wird durch Kopieren des Verzeichnisses [http-clients/01] erstellt. Anschließend nehmen wir einige Änderungen vor.

24.4.1. Die Konfiguration

Die [config]-Konfiguration der Anwendung [http-clients/02] entspricht der der Anwendung [http-clients/01], mit einigen geringfügigen Unterschieden:

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

    #  step 1 ------

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

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

    #  absolute dependencies
    absolute_dependencies = [
        #  project files
        #  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",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        #  configuration scripts
        script_dir,
        #  Logger
        f"{root_dir}/impots/http-servers/02/utilities",
    ]

    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration with constants
    config.update({
        #  taxpayer file
        "taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
        #  results file
        "resultsFilename": f"{script_dir}/../data/output/résultats.json",
        #  error file
        "errorsFilename": f"{script_dir}/../data/output/errors.txt",
        #  log file
        "logsFilename": f"{script_dir}/../data/logs/logs.txt",
        #  tax calculation server
        "server": {
            "urlServer": "http://127.0.0.1:5000/",
            "authBasic": True,
            "user": {
                "login": "admin",
                "password": "admin"
            }
        },
        #  debug mode
        "debug": True
    }
    )

    #  step 3 ------
    #  layer instantiation
    import config_layers
    config['layers'] = config_layers.configure(config)

    #  we return the configuration
    return config
  • Zeilen 31–32: Wir verwenden denselben Logger |Logger| wie für den Server;
  • Zeile 49: der absolute Pfad zur Protokolldatei;
  • Zeile 60: Der Modus [debug=True] wird verwendet, um die Antworten des Webservers in die Logdatei zu schreiben;

24.4.2. Die [dao]-Schicht

Der Code für die Klasse [ImpôtsDaoWithHttpClient] ändert sich geringfügig:

#  imports

import requests
from flask_api import status




class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):

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

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

    #  tAX CALCULATION
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        #  we let the exceptions rise
        
        #  debug mode?
        if self.__debug:
            #  logger
            if not self.__logger:
                self.__logger = self.__config['logger']
            #  log on
            self.__logger.write(f"{response.text}\n")
        #  response status code HTTP
        status_code = response.status_code
        
  • Zeile 17: Wir speichern die allgemeine Konfiguration. Wir werden später sehen, dass beim Ausführen des Konstruktors der Klasse [ImpôtsDaoWithHttpClient] das Wörterbuch [config] den in Zeile 37 verwendeten Schlüssel [logger] noch nicht enthält. Deshalb können wir [self.__logger] (Zeile 23) im Konstruktor nicht initialisieren;
  • Zeile 21: Wir haben der Konfiguration einen [debug]-Schlüssel hinzugefügt, der die Protokollierung in den Zeilen 33–39 steuert;
  • Zeile 34: wenn wir uns im [debug]-Modus befinden;
  • Zeilen 36–37: Optionale Initialisierung der Eigenschaft [self.__logger]. Wenn die Methode [calculate_tax] verwendet wird, ist der Schlüssel [logger] Teil des Wörterbuchs [config];
  • Zeile 39: Wir protokollieren das Textdokument, das mit der HTTP-Antwort des Servers verknüpft ist;

Die [dao]-Schicht wird von mehreren Threads gleichzeitig ausgeführt. Hier erstellen wir jedoch eine einzige Instanz dieser Schicht (siehe config_layers). Wir müssen daher sicherstellen, dass der Code keinen Schreibzugriff auf gemeinsam genutzte Daten beinhaltet, typischerweise die Eigenschaften der Klasse [ImpôtsDaoWithHttpClient], die die [dao]-Schicht implementiert. Im obigen Code ändert Zeile 37 jedoch eine Eigenschaft der Klasseninstanz. Hier hat dies keine Konsequenzen, da alle Threads denselben Logger 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] verläuft wie folgt:

#  configure the application

import config
config = config.configure({})

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


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


#  list of client threads
threads = []
logger = None
#  code
try:
    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger
    #  we recover the [dao] layer
    dao = config["layers"]["dao"]
    #  reading taxpayer data
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #  taxpayers?
    if not taxpayers:
        raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #  multi-threaded tax calculation for taxpayers
    i = 0
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        #  each thread will process from 1 to 4 contributors
        nb_taxpayers = min(l_taxpayers - i, random.randint(1, 4))
        #  the list of taxpayers processed by the thread
        thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
        #  increment i for the next thread
        i += nb_taxpayers
        #  create the thread
        thread = threading.Thread(target=thread_function, args=(dao, logger, thread_taxpayers))
        #  we add it to the list of threads in the main script
        threads.append(thread)
        #  we launch the thread - this operation is asynchronous - we don't wait for the thread's result
        thread.start()
    #  the main thread waits for all threads it has launched to finish
    for thread in threads:
        thread.join()
    #  here all threads have finished their work - each has modified one or more objects [taxpayer]
    #  save the results in the jSON file
    dao.write_taxpayers_results(taxpayers)
except BaseException as erreur:
    #  error display
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  close the logger
    if logger:
        logger.close()
    #  we're done
    print("Travail terminé...")
    #  end of threads that might still exist if we stopped on error
    sys.exit()
  • Das Hauptskript unterscheidet sich von dem des vorherigen Clients dadurch, dass es mehrere Ausführungsthreads generiert, um Anfragen an den Server zu senden. Der Client in Version 6 sendete alle seine Anfragen nacheinander. Anfrage #i wurde erst gestellt, nachdem die Antwort auf Anfrage #[i-1] empfangen wurde. Hier wollen wir sehen, wie sich der Server verhält, wenn er mehrere gleichzeitige Anfragen erhält. Dazu benötigen wir Threads;
  • Zeile 21: Die generierten Threads werden in einer Liste abgelegt. Es ist wichtig zu verstehen, dass das [main]-Skript ebenfalls von einem Thread namens [MainThread] ausgeführt wird. Dieser Haupt-Thread erstellt weitere Threads, die für die Berechnung der Steuer für einen oder mehrere Steuerzahler zuständig sind;
  • Zeile 26: Wir erstellen einen Logger. Dieser wird von allen Threads gemeinsam genutzt;
  • Zeile 32: Wir rufen alle Steuerzahler ab, deren Steuern berechnet werden müssen;
  • Zeilen 39–51: Wir verteilen diese Steuerzahler auf mehrere Threads;
  • Zeilen 40–41: Jeder Thread verarbeitet 1 bis 4 Steuerzahler. Diese Anzahl wird zufällig bestimmt;
    • [random.randint(1, 4)] generiert zufällig eine Zahl aus der Liste [1, 2, 3, 4];
    • Der Thread darf nicht mehr als [l-i] Steuerzahler umfassen, wobei [l-i] die Anzahl der Steuerzahler angibt, denen noch kein Thread zugewiesen wurde;
    • wir nehmen daher den kleineren der beiden Werte;
  • Zeile 43: Sobald [nb_taxpayers], die Anzahl der vom Thread verarbeiteten Steuerzahler, bekannt ist, entnehmen wir diese der Liste der Steuerzahler:
    • [slice(10,12)] ist die Menge der Indizes [10, 11, 12];
    • [taxpayers[slice(10,12)]] ist die Liste [taxpayers[10], taxpayers[11], taxpayers[12] ;
  • Zeile 45: Wir erhöhen den Wert von i, der die Schleife in Zeile 39 steuert;
  • Zeile 47: Wir erstellen einen Thread:
    • [target=thread_function] legt die Funktion fest, die der Thread ausführen wird. Dies ist die Funktion aus 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 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 läuft dann parallel zu den anderen aktiven Threads. Hier führt er die [thread_function] mit den ihm ü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 gestartet [th1, th2, th3];
    • Der Hauptthread wartet auf jeden der Threads (Zeilen 53–54) in der Reihenfolge der for-Schleife: [th1, th2, th3];
    • Angenommen, die Threads werden in der Reihenfolge [th2, th1, th3] beendet;
    • Der Hauptthread wartet darauf, dass th1 beendet wird. Wenn th2 beendet wird, passiert nichts;
    • Wenn th1 beendet ist, wartet der Hauptthread auf th2. th2 ist jedoch bereits beendet. Der Hauptthread fährt dann mit dem nächsten Thread fort und wartet auf th3;
    • Wenn th3 beendet ist, hat der Hauptthread das Warten beendet und fährt mit der Ausführung von Zeile 57 fort;
  • Zeile 57 schreibt die 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;
    • wir wissen, dass die Steuerberechnung die Objekte ändern wird, auf die die Referenzen in der Liste [thread_payers] verweisen. Diese Objekte werden mit den Ergebnissen der Steuerberechnung aktualisiert. Die Referenzen selbst werden jedoch nicht geändert. Daher „sehen“ die Referenzen in der ursprünglichen Liste [taxpayers] die geänderten Objekte oder „verweisen“ auf sie;

Die von den Threads ausgeführte [thread_function] lautet wie folgt:

#  executing the [dao] layer in a thread
#  taxpayers is a list of taxpayers
def thread_function(dao, logger, taxpayers: list):
    #  log thread start
    thread_name = threading.current_thread().name
    logger.write(f"début du thread [{thread_name}] avec {len(taxpayers)} contribuable(s)\n")
    #  taxpayers' taxes are calculated
    for taxpayer in taxpayers:
        #  log
        logger.write(f"début du calcul de l'impôt de {taxpayer}\n")
        #  synchronous tax calculation
        dao.calculate_tax(taxpayer)
        #  log
        logger.write(f"fin du calcul de l'impôt de {taxpayer}\n")
    #  log end of thread
    logger.write(f"fin du thread [{thread_name}]\n")
  • Funktionen, die von mehreren Threads gleichzeitig ausgeführt werden, sind oft schwierig zu schreiben: Sie müssen stets sicherstellen, dass der Code nicht versucht, Daten zu ändern, die von mehreren Threads gemeinsam genutzt werden. In diesem Fall müssen Sie einen synchronisierten Zugriff auf die gemeinsam genutzten Daten implementieren, die geändert werden sollen;
  • Zeile 3: Die Funktion erhält drei Parameter:
    • [dao]: eine Referenz auf die [dao]-Schicht. 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] zeigte, eine Referenz [self.__logger] hatte, die von den Threads geändert wurde, allerdings um einen Wert festzulegen, der für alle Threads gleich ist;
    • Die Referenz [logger] verweist auf einen Dateideskriptor. Wir haben gesehen, dass es beim Schreiben von Protokollen in die Datei zu Problemen kommen kann. Aus diesem Grund wurde das Schreiben in die Datei synchronisiert;
  • Zeilen 5–6: Wir protokollieren den Namen des Threads und die Anzahl der Steuerzahler, die er verwalten muss;
  • Zeilen 8–14: Berechnung der Steuern der Steuerzahler;
  • Zeile 16: Protokollierung des Endes des Threads;

24.4.4. Ausführung

Starten wir den Webserver wie im vorherigen Abschnitt beschrieben (Webserver, DBMS, hMailServer, Thunderbird) und führen wir dann das [main]-Skript des Clients aus. In den Dateien [data/output/errors.txt, data/output/results.json] erhalten wir die gleichen Ergebnisse wie in der vorherigen Version. In der Datei [data/logs/logs.txt] finden wir folgende Protokolle:


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 Steuern für 11 Steuerzahler zu berechnen. Diese fünf Threads haben gleichzeitig Anfragen an den Steuerberechnungsserver gesendet. Es ist wichtig zu verstehen, wie dies funktioniert:
    • Thread [Thread-1] wird als erster gestartet. Wenn er die CPU-Ressourcen hat, führt er den Code aus, bis er seine HTTP-Anfrage sendet. Da er auf das Ergebnis dieser Anfrage warten muss, wird er automatisch in den Wartezustand versetzt. Er verliert dann die CPU-Ressourcen, und ein anderer Thread übernimmt;
    • Zeilen 1–10: Der gleiche Vorgang wiederholt sich für jeden der 5 Threads. Somit werden die 5 Threads gestartet, noch bevor Thread [Thread-1] in Zeile 11 überhaupt seine Antwort erhalten hat;
  • Die Threads werden nicht in der Reihenfolge beendet, in der sie gestartet wurden. Somit endet Thread [Thread-3] als erster, Zeile 23;

Auf der Serverseite sehen die Protokolle in der Datei [data/logs/logs.txt] wie folgt aus:


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/?marié=oui&enfants=2&salaire=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/?marié=oui&enfants=3&salaire=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/?marié=oui&enfants=2&salaire=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/?marié=non&enfants=3&salaire=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/?marié=oui&enfants=2&salaire=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/?marié=oui&enfants=3&salaire=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/?marié=oui&enfants=5&salaire=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/?marié=non&enfants=0&salaire=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/?marié=oui&enfants=3&salaire=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/?marié=non&enfants=2&salaire=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/?marié=non&enfants=0&salaire=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}}}
  • Wir sehen, dass 11 Threads die 11 Steuerzahler bearbeitet haben;
  • einige Threads wurden angehalten (Zeilen 6, 8, 12, 14, 20, 22) und andere nicht (Zeilen 9, 23, 25, 29, 31);

24.5. [DAO]-Schicht-Tests

Wie bereits in der |vorherigen Version| testen wir die [DAO]-Schicht des Clients. Das Prinzip ist genau dasselbe:

Image

Die Testklasse wird in der folgenden Umgebung ausgeführt:

Image

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

Die Testklasse [TestHttpClientDao] sieht wie folgt aus:

import unittest

from Logger import Logger


class TestHttpClientDao(unittest.TestCase):

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

        #  { 'married': 'yes', 'children': 2, 'salary': 55555,
        #  tax': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        #  check
        self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    


if __name__ == '__main__':
    #  configure the application
    import config
    config = config.configure({})

    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger
    #  we recover the [dao] layer
    dao = config["layers"]["dao"]

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

Die Ergebnisse lauten wie folgt:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-clients/02/tests/TestHttpClientDao.py
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 6.128s
 
OK
 
Process finished with exit code 0