Skip to content

28. Anwendungsübung: Version 10

28.1. Einleitung

In den Client-Beispielen des Steuerberechnungsservers sendeten die Threads N Anfragen nacheinander, wenn sie N Steuerzahler bearbeiten mussten. Die Idee hier ist, eine einzige Anfrage zu senden, die die N Steuerzahler umfasst. Für jeden von ihnen müssen die Informationen [marié, enfants, salaire] gesendet werden. Diese können als Parameter gesendet werden:

  • als Parameter von URL. Man erhält dann eine lange, wenig aussagekräftige URL;
  • im Hauptteil (Body) der Anfrage HTTP. Es ist bekannt, dass dieser Hauptteil für den Nutzer, der einen Browser verwendet, verborgen ist;

In beiden Fällen kann man eine Anfrage [GET] oder [POST] verwenden. Wir werden eine Anfrage POST verwenden, wobei die Parameter im Hauptteil der Anfrage HTTP eingekapselt sind.

Die Client-Server-Architektur hat sich nicht geändert:

Image

28.2. Der Webserver

Image

Der Ordner [http-servers/05] wird zunächst durch Kopieren des Ordners [http-servers/02] erstellt. Wir kehren nun zu den Datenaustauschen jSON zwischen Client und Server zurück. Wir haben gesehen, dass der Übergang von jSON zu XML sehr einfach ist.

28.2.1. Konfiguration

Die Konfiguration von [config, config_database, config_layers] entspricht derjenigen der Vorgängerversionen. Wir gehen hier nicht noch einmal darauf ein.

28.2.2. Das Hauptskript [main]

Das Skript [main] ist identisch mit dem aus dem Ordner [http-servers/02], den wir kopiert haben. Nur ein Detail unterscheidet sich:


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

  • Zeile 2: Nun wird das URL über ein POST abgerufen;

28.2.3. Der Controller [index_controller]

Der Controller [index_controller] entwickelt sich wie folgt:


# Import der Abhängigkeiten

import json

from flask_api import status
from werkzeug.local import LocalProxy


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

    # Der Textkörper des Beitrags wird abgerufen – es wird eine Liste von Wörterbüchern erwartet
    msg_erreur = None
    list_dict_taxpayers = None
    # der Textkörper jSON des POST
    request_text = request.data
    try:
        #, den man in eine Liste von Wörterbüchern umwandelt
        list_dict_taxpayers = json.loads(request_text)
    except BaseException as erreur:
        # der Fehler wird vermerkt
        msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
    # Haben wir eine nicht leere Liste?
    if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
        # Der Fehler wird vermerkt
        msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
    # Haben wir eine Liste von Dictionaries?
    if not msg_erreur:
        erreur = False
        i = 0
        while not erreur and i < len(list_dict_taxpayers):
            erreur = not isinstance(list_dict_taxpayers[i], dict)
            i += 1
        # Fehler?
        if erreur:
            msg_erreur = "le corps du POST doit être une liste de dictionnaires"
    # Fehler?
    if msg_erreur:
        # Es wird eine Fehlermeldung an den Client gesendet
        résultats = {"réponse": {"erreurs": [msg_erreur]}}
        return résultats, status.HTTP_400_BAD_REQUEST

    # Die TaxPayers werden nacheinander überprüft
    # zunächst keine Fehler
    list_erreurs = []
    for dict_taxpayer in list_dict_taxpayers:
        # Es wird ein TaxPayer aus dict_taxpayer erstellt
        msg_erreur = None
        try:
            # Der folgende Schritt filtert die Fälle heraus, bei denen die Parameter nicht
            # Eigenschaften der Klasse TaxPayer sind sowie Fälle, in denen deren Werte
            # falsch sind
            TaxPayer().fromdict(dict_taxpayer)
        except BaseException as erreur:
            msg_erreur = f"{erreur}"
        # bestimmte Schlüssel müssen im Dictionary vorhanden sein
        if not msg_erreur:
            # Die Schlüssel [marié, enfants, salaire] müssen im Wörterbuch vorhanden sein
            keys = dict_taxpayer.keys()
            if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
                msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
        # Fehler?
        if msg_erreur:
            # Der Fehler wird im TaxPayer selbst vermerkt
            dict_taxpayer['erreur'] = msg_erreur
            # TaxPayer wird zur Fehlerliste hinzugefügt
            list_erreurs.append(dict_taxpayer)

    # Alle Steuerzahler wurden verarbeitet – gibt es Fehler?
    if list_erreurs:
        # Es wird eine Fehlermeldung an den Kunden gesendet
        résultats = {"réponse": {"erreurs": list_erreurs}}
        return résultats, status.HTTP_400_BAD_REQUEST

    # Keine Fehler, es kann weitergearbeitet werden
    # Abruf der Daten von der Steuerbehörde
    admindata = config["admindata"]
    métier = config["layers"]["métier"]
    try:
        # Die TaxPayer-Dateien werden einzeln verarbeitet
        list_taxpayers = []
        for dict_taxpayer in list_dict_taxpayers:
            # Steuerberechnung
            taxpayer = TaxPayer().fromdict(
                {'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
                 '„Gehalt“: dict_taxpayer['salaire']})
            métier.calculate_tax(taxpayer, admindata)
            # Das Ergebnis wird als Dictionary gespeichert
            list_taxpayers.append(taxpayer.asdict())
        # die Antwort wird an den Client gesendet
        return {"réponse": {"results": list_taxpayers}}, status.HTTP_200_OK
    except ImpôtsError as erreur:
        # Eine Fehlermeldung wird an den Client gesendet
        return {"réponse": {"erreurs": f"[{erreur}]"}}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • Zeile 9: Der Controller empfängt:
      • die Anfrage [request] vom Client;
      • die Serverkonfiguration [config];
  • Zeilen 14–18: Der Hauptteil von POST wird abgerufen. Die im Hauptteil der Anfrage HTTP gekapselten Parameter können auf verschiedene Arten kodiert sein. Eine davon haben wir bereits kennengelernt: [x-www-form-urlencoded]. Wir werden hier eine andere Kodierung verwenden: jSON;
  • Zeile 18: Mit „[request.data]“ lässt sich der Hauptteil (Body) der Anfrage „HTTP“ abrufen. Hier wird Text abgerufen, und wir wissen, dass dieser Text aus jSON stammt, das eine Liste von Wörterbüchern [marié, enfants, salaire] darstellt;
  • Zeilen 19–24: Diese Wörterbuchliste wird abgerufen;
  • Zeilen 22–24: Wenn das Abrufen von jSON fehlgeschlagen ist, wird der Fehler protokolliert;
  • Zeilen 26–28: Wenn festgestellt wird, dass das abgerufene Objekt keine Liste ist oder dass es sich um eine leere Liste handelt, wird der Fehler protokolliert;
  • Zeilen 29–38: Wenn eine Liste erfolgreich abgerufen wurde, wird überprüft, ob es sich tatsächlich um eine Liste von Wörterbüchern handelt;
  • Zeilen 40–43: Wenn ein Fehler aufgetreten ist, wird der Vorgang an dieser Stelle abgebrochen und eine Fehlermeldung an den Client gesendet;
  • Zeilen 45–69: Nun wird jedes einzelne Wörterbuch überprüft:
    • Sie müssen die Schlüssel [marié, enfants, salaire] enthalten;
    • sie müssen die Erstellung eines gültigen Objekts [TaxPayer] ermöglichen;
  • Zeilen 65–69: Wenn in einem Wörterbuch ein Fehler festgestellt wurde, wird dieser in dasselbe Wörterbuch unter dem Schlüssel „Fehler“ eingetragen;
  • Zeilen 72–75: Die fehlerhaften Wörterbücher wurden in der Liste [list_erreurs] zusammengefasst. Ist diese Liste nicht leer, wird sie in einer Fehlerantwort an den Client gesendet;
  • Zeile 77: An dieser Stelle ist bekannt, dass aus dem Hauptteil der vom Kunden gesendeten Anfrage eine Liste von Objekten des Typs [TaxPayer] erstellt werden kann;
  • Zeilen 84–91: Die Liste der empfangenen Wörterbücher wird ausgewertet;
  • Zeile 86: Aus einem Dictionary wird ein Objekt vom Typ [TaxPayer] erstellt;
  • Zeile 89: Die Steuer für dieses [TaxPayer] wird berechnet;
  • Zeile 91: Da bekannt ist, dass das Objekt „[taxpayer]“ durch die Steuerberechnung verändert wurde, wird es in ein Wörterbuch umgewandelt und einer Ergebnisliste hinzugefügt;
  • Zeile 93: Diese Ergebnisliste wird an den Client gesendet;

28.2.4. Server-Tests

Wir werden den Server mit einem Postman-Client testen:

  • Wir starten den Webserver SGBD und den Mailserver [hMailServer];
  • Wir starten den Postman-Client sowie dessen Konsole (Strg-Alt-C);

Image

  • an [1]: Wir senden eine Anfrage an [POST];
  • in [2]: die Antwort URL des Servers;
  • in [3]: der Hauptteil der Anfrage HTTP;
  • in [5]: Es wird angegeben, dass dieser Hauptteil in Form einer Zeichenkette jSON gesendet werden soll;
  • in [4]: Man wechselt in den Modus [raw], um eine Zeichenkette jSON kopieren und einfügen zu können;
  • in [6]: Man fügt die Zeichenfolge jSON ein, die aus einer der Dateien [résultats.json] der verschiedenen Versionen stammt. Anschließend behält man für jeden Steuerzahler nur die Eigenschaften [marié, salaire, enfants] bei;

Image

  • In [7] werden die Header HTTP betrachtet, die der Postman-Client an den Server senden wird;
  • in [8] sehen wir, dass er ihm einen Header [Content-Type] sendet, der angibt, dass die Anfrage einen in jSON kodierten Hauptteil enthält. Dies ergibt sich aus der zuvor getroffenen Auswahl [5];

Image

  • in [9-12]: Wir fügen die vom Server erwarteten Identifikatoren in die Anfrage ein;

Diese Anfrage wird gesendet. Die Antwort des Servers lautet wie folgt:

Image

  • in [3], wir haben von jSON erhalten;
  • in [4] die Steuern der Steuerzahler;

Sehen wir uns in der Postman-Konsole (Strg-Alt-C) den stattgefundenen Client-Server-Dialog an:

Der Postman-Client hat folgenden Text gesendet:

POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 03c4aa28-5a5d-4bb5-ac51-7ad51968c71d
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 824

[
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 55555
  },
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 50000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 50000
  },
  {
    "marié": "non",
    "enfants": 2,
    "salaire": 100000
  },
  {
    "marié": "non",
    "enfants": 3,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 5,
    "salaire": 100000
  },
  {
    "marié": "non",
    "enfants": 0,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 30000
  },
  {
    "marié": "non",
    "enfants": 0,
    "salaire": 200000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 200000
  }
]
  • Zeile 1: den POST an den Server;
  • Zeile 2: der Authentifizierungsheader HTTP;
  • Zeile 3: Der Client teilt dem Server mit, dass er ihm eine Zeichenkette mit dem Namen jSON sendet und dass diese Zeichenkette 824 Byte lang ist (Zeile 11);
  • Zeilen 13–69: der Hauptteil der Anfrage jSON;

Der Server hat ihm folgenden Text zurückgesendet:


HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 1461
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:16:34 GMT

{"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}]}}
  • Zeile 1: Die Anfrage war erfolgreich;
  • Zeile 2: Der Hauptteil der Serverantwort ist eine Zeichenkette jSON. Diese ist 1461 Byte lang (Zeile 3);
  • Zeile 7: Die Antwort des Servers lautet jSON;

Testen wir nun einige Fehlerfälle.

Fall 1: Wir senden beliebige Daten


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 47652706-9744-46a0-a682-de010e5406c0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 3

abc

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 125
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:43:27 GMT

{"réponse": {"erreurs": ["le corps du POST n'est pas une chaîne jSON valide : Expecting value: line 1 column 1 (char 0)"]}}
  • Zeile 13: Wir haben die Zeichenfolge [abc] gesendet, die keine gültige jSON-Zeichenfolge ist (Zeile 3);
  • Zeile 15: Der Server antwortet mit einem Fehlercode 400;
  • Zeile 21: Die Antwort jSON vom Server;

Fall 2: Senden wir eine gültige Zeichenfolge jSON, die keine Liste ist


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 03b64735-9239-47b3-b92d-be7c9ebc7559
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 17

{"att1":"value1"}

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 97
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:50:11 GMT

{"réponse": {"erreurs": ["le corps du POST n'est pas une liste ou alors cette liste est vide"]}}

Fall 3: Senden wir eine Zeichenfolge jSON, die eine Liste ist, deren Elemente nicht alle Wörterbücher sind


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: a1528a5f-777c-413f-b3be-7d4e9955b12a
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 7

[0,1,2]

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 85
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:52:10 GMT

{"réponse": {"erreurs": ["le corps du POST doit être une liste de dictionnaires"]}}

Fall 4: Senden wir eine Liste von Dictionaries, wobei ein Dictionary nicht die richtigen Schlüssel enthält


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: ba964d81-c9d9-46ff-a521-b4c4e5639484
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 19

[{"att1":"value1"}]

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 112
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:54:33 GMT

{"réponse": {"erreurs": [{"att1": "value1", "erreur": "MyException[2, la clé [att1] n'est pas autorisée]"}]}}

Fall 5: Senden wir eine Liste von Dictionaries, in der ein Dictionary fehlende Schlüssel enthält:


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 98aec51d-f37d-4c14-81cd-c7ffcbbcdc65
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 18

[{"marié":"oui"}]

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 125
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:56:40 GMT

{"réponse": {"erreurs": [{"marié": "oui", "erreur": "le dictionnaire doit inclure les clés [marié, enfants, salaire]"}]}}

Fall 6: Senden einer Liste von Dictionaries mit einem Dictionary, das zwar die richtigen Schlüssel enthält, bei dem jedoch einige Werte fehlerhaft sind:


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 3083e601-dee4-4e15-9ea4-fc0328d0fcf0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 46

[{"marié":"x", "enfants":"x", "salaire":"x"}]

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 167
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:59:32 GMT

{"réponse": {"erreurs": [{"marié": "x", "enfants": "x", "salaire": "x", "erreur": "MyException[31, l'attribut marié [x] doit avoir l'une des valeurs oui / non]"}]}}

28.3. Der Webclient

Image

Der Ordner [http-clients/05] (Version 10) wird zunächst durch Kopieren des Ordners [http-clients/02] (Version 7) erstellt. Anschließend wird er geändert.

28.3.1. Die Schicht [dao]

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


# Importe

import requests
from flask_api import status

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsMétier import InterfaceImpôtsMétier
from TaxPayer import TaxPayer


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

    # Konstruktor
    def __init__(self, config: dict):
        

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

    # Steuerberechnung
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        

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

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

        # Verbindung zum Server
        config_server = self.__config_server
        if config_server['authBasic']:
            response = requests.post(config_server['urlServer'], json=list_dict_taxpayers,
                                     auth=(config_server["user"]["login"],
                                           config_server["user"]["password"]))
        else:
            response = requests.post(config_server['urlServer'], json=list_dict_taxpayers)
        # Debug-Modus?
        if self.__debug:
            # Logger
            if not self.__logger:
                self.__logger = self.__config['logger']
            # Anmeldung läuft
            self.__logger.write(f"{response.text}\n")
        # Statuscode der Antwort HTTP
        status_code = response.status_code
        # Die Antwort jSON wird in ein Wörterbuch aufgenommen
        résultat = response.json()
        # Fehler, wenn der Statuscode nicht 200 ist OK
        if status_code != status.HTTP_200_OK:
            # Es ist bekannt, dass die Fehler dem Schlüssel [erreurs] der Antwort zugeordnet wurden
            raise ImpôtsError(93, résultat['réponse']['erreurs'])
        # Es ist bekannt, dass das Ergebnis dem Schlüssel [results] aus der Antwort zugeordnet wurde
        list_dict_taxpayers2 = résultat['réponse']['results']
        # Die ursprüngliche Liste der Steuerzahler wird mit den erhaltenen Ergebnissen aktualisiert
        for i in range(len(taxpayers)):
            # Aktualisierung der Steuerzahler [i]
            taxpayers[i].fromdict(list_dict_taxpayers2[i])
        # Hier wurde der Parameter [taxpayers] mit den Ergebnissen des Servers aktualisiert
  • Zeilen 1–26: Der Code bleibt unverändert gegenüber Version 7 und anderen Versionen;
  • Zeilen 27–70: Es wird eine neue Methode [calculate_tax_in_bulk_mode] eingeführt, deren Aufgabe es ist, die Steuer für eine Liste von Steuerpflichtigen zu berechnen;
  • Zeile 28: [taxpayers] ist diese Liste der Steuerpflichtigen;
  • Zeilen 31–39: Mithilfe der Funktion [map] wird eine Liste von Objekten vom Typ [TaxPayer] in eine Liste von Dictionaries umgewandelt;
  • Zeilen 34–38: Die verwendete Lambda-Funktion wandelt ein Objekt vom Typ [TaxPayer] in ein Wörterbuch vom Typ [dict] um, das ausschließlich die Schlüssel [marié, enfants, salaire] enthält. Dazu wird der Parameter mit dem Namen [included_keys] der Methode [BaseEntity.asdict] verwendet. Zur Erinnerung: Um die genauen Namen der Eigenschaften zu erfahren, die in die Parameter [excluded_keys, included_keys] eingegeben werden müssen, muss das vordefinierte Wörterbuch [taxpayer.__dict__] verwendet werden;
  • Zeilen 41–48: Verbindung zum Server herstellen und dessen Antwort abrufen (HTTP);
  • Zeilen 44, 48:
    • Es wird die statische Methode [requests.post] verwendet, um einen POST an den Server zu senden;
    • der Parameter mit dem Namen [json] wird verwendet, um anzugeben, dass der Hauptteil des POST eine Zeichenkette jSON ist. Dies hat zwei Konsequenzen:
      • Das dem Parameter „[json]“ zugewiesene Objekt – in diesem Fall eine Liste von Dictionaries – wird in die Zeichenkette „jSON“ umgewandelt;
      • der Header
Content-Type: application/json

wird in die Header HTTP von POST aufgenommen;

  • Zeile 59: Die Antwort jSON vom Server wird in das Wörterbuch [résultat] deserialisiert;
  • Zeilen 61–63: Ein eventuell vom Server gesendeter Fehler wird behandelt;
  • Zeile 65: Die Ergebnisse der Steuerberechnung befinden sich in einer Liste von Dictionaries;
  • Zeilen 67–69: Diese Ergebnisse werden verwendet, um die ursprüngliche Liste der Steuerzahler [taxpayers] zu aktualisieren, die ursprünglich von der Methode in Zeile 28 empfangen wurde;
  • Zeile 70: Hier wurde die ursprüngliche Liste der Steuerzahler mit den Ergebnissen der Steuerberechnung aktualisiert;

28.3.2. Das Hauptskript [main]

Das Hauptskript [main] ändert sich wie folgt: Es wird lediglich die Funktion [thread_function] geändert, die von den vom Client erstellten Threads ausgeführt wird. Der übrige Code bleibt unverändert.


# Ausführung der Ebene [dao] in einem Thread
# „taxpayers“ ist eine Liste von Steuerzahlern
def thread_function(dao: ImpôtsDaoWithHttpClient, logger: Logger, taxpayers: list):
    # Protokoll: Start des Threads
    thread_name = threading.current_thread().name
    nb_taxpayers = len(taxpayers)
    # Protokoll
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    # Die Steuer der Steuerzahler wird berechnet
    dao.calculate_tax_in_bulk_mode(taxpayers)
    # Protokoll
    logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")
  • Zeilen 9–10: Während zuvor eine Schleife vorhanden war, die nacheinander jeden Steuerzahler an die Methode [dao.calculate_tax] übergab, erfolgt hier nur ein einziger Aufruf der Methode [dao.calculate_tax_in_bulk_mode], an die alle Steuerzahler übergeben werden;

28.3.3. Ausführung des Clients

Wir werden die Ausführungszeiten der Versionen vergleichen:

  • 7, bei der für jeden Steuerpflichtigen eine Abfrage HTTP durchgeführt wird;
  • 10 (diese hier), bei der mehrere Steuerzahler in einer einzigen Abfrage HTTP zusammengefasst werden;

Zunächst Version 6. Um die beiden Versionen zu vergleichen, setzen wir die Eigenschaft [sleep_time] des Servers auf Null, damit keine erzwungene Wartezeit der Threads auftritt. Die Protokolle des Clients lauten wie folgt:


2020-07-28 14:20:45.811347, Thread-1 : début du thread [Thread-1] avec 4 contribuable(s)
2020-07-28 14:20:45.811347, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}

2020-07-28 14:20:45.913065, Thread-3 : 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-28 14:20:45.913065, Thread-3 : fin du thread [Thread-3]

Die Ausführungszeit des Clients für die Berechnung der Steuer von 11 Steuerzahlern beträgt somit [913065-811347= 101718], d. h. etwa 102 Millisekunden.

Machen wir dasselbe mit Version 10 (sleep_time des Servers mit Null). Die Protokolle des Clients lauten dann wie folgt:


2020-07-28 14:25:31.871428, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-07-28 14:25:31.873594, Thread-2 : début du calcul de l'impôt des 3 contribuables
2020-07-28 14:25:31.877429, Thread-3 : début du calcul de l'impôt des 3 contribuables
2020-07-28 14:25:31.882855, Thread-4 : début du calcul de l'impôt des 1 contribuables
2020-07-28 14:25:31.930723, Thread-2 : {"réponse": {"results": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}]}}
….
2020-07-28 14:25:31.935958, Thread-4 : fin du calcul de l'impôt des 1 contribuables
2020-07-28 14:25:31.935958, Thread-1 : fin du calcul de l'impôt des 4 contribuables

Die Ausführungszeit des Clients für die Berechnung der Steuer von 11 Steuerzahlern beträgt somit [935958-871428= 64530 ns] (Zeile 8 – Zeile 1), d. h. etwa 65 Millisekunden. Diese neue Version 10 bringt somit eine Zeitersparnis von etwa 57 % gegenüber Version 7.

28.3.4. Tests der Client-Ebene [dao]

Image

Der [TestHttpClientDao]-Test des Clients der Version 10 liegt sehr nahe an dem der Version 7:


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, 'Abzug': 0, 'Ermäßigung': 0, 'Satz': 0,14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax_in_bulk_mode([taxpayer])
        # Überprüfung
        self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    

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

    # Protokollierung
    logger = Logger(config["logsFilename"])
    # in der Konfiguration speichern
    config["logger"] = logger
    # die Schicht wird abgerufen [dao]
    dao = config["layers"]["dao"]

    # die Testmethoden werden ausgeführt
    print("tests en cours...")
    unittest.main()
  • Zeile 14: Anstatt die Methode [dao.calculate_tax] aufzurufen, wird die Methode [dao.calculate_tax_in_bulk_mode] aufgerufen, an die eine Liste (durch eckige Klammern gekennzeichnet) eines Steuerpflichtigen übergeben wird;

Alle Tests werden bestanden.