23. Übungsaufgabe: Version 6
23.1. Einführung
Wir kehren nun zu unserer Steuerberechnungsanwendung zurück. Wir werden verschiedene Webanwendungen darauf aufbauen.
In Version 5 unserer Anwendungsübung wurden die Daten der Steuerbehörde in einer Datenbank gespeichert. Diese Version 5 bestand aus zwei separaten Anwendungen, die gemeinsame Schichten nutzten:
- eine Anwendung, die Steuern im |Batch|-Modus für Steuerzahler berechnete, die in einer Textdatei gespeichert waren;
- eine Anwendung, die Steuern im |interaktiven| Modus für Steuerzahler berechnete, deren Daten über die Tastatur eingegeben wurden;
Die Version 5 der Anwendung zur Batch-Steuerberechnung wies folgende Architektur auf:

Letztendlich wird die Webversion dieser Anwendung die folgende Architektur aufweisen:

- Der Web-Client [1] kommuniziert mit dem Web-Server [2], der wiederum mit dem DBMS [3] kommuniziert;
- Der Webserver [2] behält die [Business]- [8]- und [DAO]- [9]-Schichten der ursprünglichen Anwendung bei;
- Die ursprüngliche Anwendung behält ihr Hauptskript [4] und ihre [Business-]Schicht [15] bei. Die [Business-]Schichten [8] und [15] sind identisch;
- die Client-Server-Kommunikation erfordert zwei zusätzliche Schichten:
- die [Web]-Schicht [7], die die Webanwendung implementiert;
- die [DAO]-Schicht [5], die als Client für die Webanwendung [7] fungiert;
In der endgültigen Version kann die Steuerberechnung im Batch-Verfahren auf zwei Arten durchgeführt werden:
- Die Geschäftslogik für die Steuerberechnung wird von der [Business]-Schicht des Servers übernommen. Das [main]-Skript wird diese Methode verwenden;
- Die Geschäftslogik für die Steuerberechnung wird von der [Business]-Schicht des Clients übernommen. Das Skript [main2] wird diese Methode verwenden;
Von nun an werden wir mehrere Client/Server-Anwendungen des oben beschriebenen Typs entwickeln, von denen jede eine oder mehrere neue Webentwicklungstechnologien veranschaulicht.
23.2. Der Webserver für die Steuerberechnung
23.2.1. Version 1

Das Skript [server_01] ist die folgende Webanwendung:

- In [1] verwenden wir eine parametrisierte URL, in der wir drei Werte übergeben:
- [married] (ja/nein), um anzugeben, ob der Steuerzahler verheiratet ist;
- [children]: die Anzahl der Kinder des Steuerpflichtigen;
- [salary]: das Jahresgehalt des Steuerzahlers;
- In [2] gibt der Webserver eine JSON-Zeichenkette zurück, die den fälligen Steuerbetrag zusammen mit dessen verschiedenen Bestandteilen angibt;
Die Anwendungsarchitektur sieht wie folgt aus:

- Der Browser [1] fragt den Server [2] ab. Das Skript [server_01] implementiert die [Web]-Schicht [2] des Servers;
- Die Schichten [3–8] sind diejenigen, die bereits in |Version 5| der Steuerberechnungsanwendung verwendet wurden. Wir verwenden sie unverändert wieder;
- Die [Business]-Schicht [3] ist |hier| definiert;
- die [DAO]-Schicht [4] ist |hier| definiert;
Die Webanwendung [server_01] wird mithilfe von drei Skripten konfiguriert:
- [config], das die gesamte Anwendung konfiguriert;
- [config_database], das den Datenbankzugriff konfiguriert. Wir werden mit den DBMS MySQL und PostgreSQL arbeiten;
- [config_layers], das die Anwendungsschichten konfiguriert;
Das Skript [config] sieht wie folgt aus:
- Die Funktion [configure] nimmt ein [config]-Wörterbuch als Argument entgegen (Zeile 1) und gibt es als Ergebnis zurück (Zeile 54), nachdem sie dessen Inhalt erweitert hat. Man hätte schon längst darauf hinweisen können, dass es nicht notwendig war, das Ergebnis [config] zurückzugeben . Tatsächlich ist [config] eine Wörterbuchreferenz, die sich der aufrufende Code mit dem aufgerufenen Code teilt. Der aufrufende Code verfügt daher bereits über diese Referenz (Zeile 1), und es besteht keine Notwendigkeit, sie erneut zurückzugeben (Zeile 54). Daher wäre die folgende Schreibweise:
config=[module].configure(config) (1)
ist überflüssig. Es reicht aus, zu schreiben:
[module].configure(config) (2)
Dennoch habe ich den Schreibstil (1) beibehalten, da ich dachte, dass er besser veranschaulicht, dass der aufgerufene Code das [config]-Wörterbuch modifiziert.
- Zeile 1: Das von der Funktion [configure] empfangene [config]-Wörterbuch enthält einen Schlüssel „sgbd“, dessen Wert aus der Liste [„mysql“, „pgres“] entnommen wird. [mysql] bedeutet, dass die verwendete Datenbank von MySQL verwaltet wird, während „pgres“ bedeutet, dass die verwendete Datenbank von PostgreSQL verwaltet wird;
- Zeilen 4–27: Wir listen alle Verzeichnisse auf, die für die Webanwendung notwendige Elemente enthalten. Sie werden Teil des Python-Pfads der Anwendung sein (Zeilen 30–31);
- Zeilen 33–40: Nur bestimmte Benutzer dürfen auf die Anwendung zugreifen. Hier haben wir eine Liste mit einem einzigen Benutzer;
- Zeilen 43–46: Das Skript [config_database] erstellt die Konfiguration für die verwendete Datenbank;
- Zeile 46: Die vom Skript [config_database] erstellte Konfiguration ist ein Wörterbuch, das wir in der allgemeinen Konfiguration unter dem Schlüssel „database“ speichern;
- Zeilen 48–51: Das Skript [config_layers] instanziiert die Schichten der Webanwendung. Es gibt ein Wörterbuch zurück, das in der allgemeinen Konfiguration unter dem Schlüssel „layers“ gespeichert wird;
Das Skript [config_database] ist dasjenige, das bereits in |Version 5| verwendet wurde. Wir fügen es hier zur Information ein:
Das Skript [config_layers] konfiguriert die Webserver-Ebenen. Wir verwenden ein Skript wieder, das wir bereits kennen:
- Zeile 6: Die [dao]-Schicht wird mithilfe einer Datenbank implementiert;
- [ImpotsDaoWithAdminDataInDatabase] wurde |hier| definiert;
- [BusinessTaxes] wurde |hier| definiert;
Das Hauptskript [server_01] lautet wie folgt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | |
- Zeilen 1–10: Abrufen des Parameters, der angibt, welches DBMS verwendet werden soll;
- Zeilen 12–14: Mit diesen Informationen können wir die Anwendung konfigurieren. Insbesondere wird der Python-Pfad aufgebaut;
- Zeilen 16–23: Mit dem neuen Python-Pfad importieren wir die erforderlichen Module;
- Zeilen 25–31: Abrufen von Daten von der Steuerbehörde zur Berechnung der Steuer;
- Zeilen 33–34: Instanziierung der Flask-Anwendung;
- Zeile 38: Die Flask-Anwendung bedient nur die URL [/]. Sie erwartet eine URL im folgenden Format: [/ ?married=xx&children=yy&salary=zz], wobei:
- xx: ja / nein;
- yy: Anzahl der Kinder;
- zz: Jahresgehalt;
- Zeilen 40–89: Wir prüfen die Gültigkeit der URL-Parameter;
- Zeile 41: Wir sammeln Fehlermeldungen in der Liste [errors];
- Zeile 43: Wie du dich vielleicht erinnerst, befinden sich die Parameter der URL in [request.args] (siehe |hier|):
- das [request]-Objekt ist das in Zeile 20 importierte Flask-Objekt;
- das Objekt [request.args] verhält sich wie ein Wörterbuch;
- Zeilen 43–44: Wir überprüfen, ob genau drei Parameter vorhanden sind (nicht weniger, nicht mehr);
- Zeilen 46–49: Wir prüfen, ob der Parameter [married] in der URL vorhanden ist;
- Zeilen 50–54: Wenn er vorhanden ist, prüfen wir, ob sein Wert in Kleinbuchstaben, ohne führende und nachfolgende Leerzeichen, „yes“ oder „no“ lautet;
- Zeilen 56–59: Wir prüfen, ob der Parameter [children] in der URL enthalten ist;
- Zeilen 60–66: Falls vorhanden, prüfen wir, ob sein Wert eine positive ganze Zahl ist;
- Zeile 66: Beachten Sie, dass URL-Parameter und ihre Werte Zeichenfolgen sind. Der Wert des Parameters [children] wird in einen „int“ umgewandelt;
- Zeilen 68–78: Für den Parameter [salary] führen wir dieselben Prüfungen durch wie für den Parameter [children];
- Zeilen 81–83: Wir prüfen, ob in der URL keine anderen Parameter als [‘married’, ‘children’, ‘salary’] vorhanden sind;
- Zeilen 85–89: Wenn nach all diesen Prüfungen die Liste [errors] nicht leer ist, senden wir diese Fehlerliste als JSON-Zeichenkette zusammen mit dem Statuscode [400 Bad Request] an den Client;
Da wir später häufig eine JSON-Zeichenkette als Antwort an den Client senden müssen, wurden die dafür erforderlichen wenigen Zeilen in das Modul [myutils.py] ausgelagert, das wir bereits verwendet haben:

Das Skript [myutils.py] sieht nun wie folgt aus:
- Zeile 16: Die Funktion [json_response] erwartet zwei Parameter:
- [response]: das Wörterbuch, das die an den Web-Client zu sendende JSON-Zeichenkette enthält;
- [status_code]: der HTTP-Statuscode der Antwort;
- Zeile 18: Wir legen den JSON-Body der Antwort fest;
- Zeile 20: Wir fügen den HTTP-Header hinzu, der dem Web-Client mitteilt, dass er JSON erhalten wird;
- Zeile 22: Wir senden die HTTP-Antwort an den aufrufenden Code. Es ist Aufgabe des aufrufenden Codes, diese an den Web-Client weiterzuleiten;
Die Datei [__init__.py] ändert sich wie folgt:
from .myutils import set_syspath, json_response
Die neue Version von [myutils] wird mit dem Befehl [pip install .] in einem PyCharm-Terminal unter den systemweiten Modulen installiert:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install .
Processing c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\packages
Using legacy setup.py install for myutils, since package 'wheel' is not installed.
Installing collected packages: myutils
Attempting uninstall: myutils
Found existing installation: myutils 0.1
Uninstalling myutils-0.1:
Successfully uninstalled myutils-0.1
Running setup.py install for myutils ... done
Successfully installed myutils-0.1
- Zeile 1: Sie müssen sich im Ordner [packages] befinden, um diesen Befehl einzugeben;
Der Code für das Skript [server_01] lautet wie folgt:
- Zeile 10: Zu diesem Zeitpunkt sind die erwarteten Parameter in der URL vorhanden und korrekt;
- Zeile 10: Wir erstellen das [TaxPayer]-Objekt, das den Steuerzahler modelliert;
- Zeile 11: Wir beauftragen die [business]-Schicht, die Steuer zu berechnen. Beachten Sie, dass die von der [business]-Schicht berechneten Elemente in das als Parameter übergebene [taxpayer]-Objekt eingefügt werden;
- Zeile 13: Die Antwort wird als JSON-String an den Web-Client gesendet. Dies ist der JSON-String eines Dictionaries. Dem Schlüssel [result] ordnen wir das Dictionary des [taxpayer]-Objekts zu. Wir konnten das [taxpayer]-Objekt selbst nicht zuweisen, da es in JSON nicht serialisierbar ist;
Wir erstellen zwei Ausführungskonfigurationen, eine für MySQL und eine für PostgreSQL:

Hier sind einige Ausführungsbeispiele (Sie haben die Anwendung [server_01] und das DBMS gestartet und dann über einen Browser die URL http://localhost:5000/ aufgerufen):


Hier ist ein Beispiel für die Anfrage in der Postman-Konsole:

GET /?mari%C3%A9=xx&enfants=yy&salaire=zz HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: e4c5df8c-4bd6-4250-b789-b7b164db4eff
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 134
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 06:15:44 GMT
{"réponse": {"erreurs": ["paramètre marié [xx] invalide", "paramètre enfants [yy] invalide", "paramètre salaire [zz] invalide"]}}
- Zeile 1: Es wird eine falsche URL angefordert;
- Zeile 10: Der Server antwortet mit dem Status 400 BAD REQUEST;
23.2.2. Version 2

In Version 2 des Servers wird die URL-Verarbeitung im Modul [index_controller] isoliert [5]:
- Zeile 9: Die Funktion [execute] erhält zwei Parameter:
- [request]: die HTTP-Anfrage des Clients;
- [config]: das Konfigurationswörterbuch der Anwendung;
Das Skript [server_02] lautet wie folgt:
- Zeilen 36–41: Behandlung der Route /;
- Zeile 39: Verwendung der Funktion [IndexController.execute];
Wir werden nun diese Technik anwenden: Jede Route wird von einem eigenen Modul verarbeitet.
Die Ausführungsergebnisse sind dieselben wie bei Version 1.
23.2.3. Version 3
Version 3 führt das Konzept der Authentifizierung ein.
Das Skript [server_03] sieht nun wie folgt aus:
- Zeile 21: Importieren eines Authentifizierungshandlers. Es gibt verschiedene Arten der Authentifizierung für einen Webserver. Die hier verwendete heißt [HTTP Basic]. Jede Art der Authentifizierung folgt einem bestimmten Client-Server-Dialog;
- Zeile 33: Erstellen einer Instanz des Authentifizierungshandlers;
- Zeile 37: Die Annotation [@auth.verify_password] kennzeichnet die Funktion, die ausgeführt werden soll, wenn der Authentifizierungshandler den vom Client gemäß dem [HTTP Basic]-Protokoll gesendeten Benutzernamen und das Passwort überprüfen möchte;
- Zeile 55: Die Annotation [@auth.login_required] kennzeichnet eine Route, für die der Web-Client authentifiziert werden muss. Wenn der Web-Client seine Anmeldedaten noch nicht gesendet hat, fordert der Webserver diese automatisch über das HTTP-Basic-Protokoll an;
Das Modul [flask_httpauth] muss installiert sein:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\01\flask>pip install flask_httpauth
Collecting flask_httpauth
Downloading Flask_HTTPAuth-4.1.0-py2.py3-none-any.whl (5.8 kB)
Requirement already satisfied: Flask in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from flask_httpauth) (1.1.2)
Requirement already satisfied: itsdangerous>=0.24 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (1.1.0)
Requirement already satisfied: click>=5.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (7.1.2)
Requirement already satisfied: Jinja2>=2.10.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (2.11.2)
Requirement already satisfied: Werkzeug>=0.15 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (1.0.1)
Requirement already satisfied: MarkupSafe>=0.23 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Jinja2>=2.10.1->Flask->flask_httpauth) (1.1.1
)
Installing collected packages: flask-httpauth
Successfully installed flask-httpauth-4.1.0
Schauen wir mal, was in der Postman-Konsole passiert. Sie:
- Erstellen Sie eine Laufkonfiguration;
- Starten Sie die Webanwendung;
- Starten Sie die Datenbank Ihrer Wahl;
- Rufen Sie die URL [/] mit Postman auf;
Der Client-Server-Dialog in der Postman-Konsole sieht wie folgt aus:
- Zeile 10: Der Server antwortet, dass wir nicht berechtigt sind, auf die URL [/] zuzugreifen;
- Zeile 13: Es teilt uns mit, welches Authentifizierungsprotokoll wir verwenden sollen, in diesem Fall das Basic-Authentifizierungsprotokoll;
Es ist möglich, Postman so zu konfigurieren, dass die Benutzeranmeldedaten gemäß dem Basic-Authentifizierungsprotokoll gesendet werden:

- In [6-7] geben wir die Anmeldedaten ein, die im Skript [config] vorhanden sind:
config['users'] = [
{
"login": "admin",
"password": "admin"
}
]
Der Client-Server-Dialog in der Postman-Konsole sieht nun wie folgt aus:
GET / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 5ce20822-e87c-4eef-a2f4-b9eaec38d881
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 203
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 07:20:01 GMT
{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]", "paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
- Zeile 2: Der Postman-Client sendet die Benutzeranmeldedaten [admin / admin] in verschlüsselter Form;
- Zeile 17: Der Server antwortet korrekt. Er meldet Fehler, da die Parameter [verheiratet, Kinder, Gehalt] nicht gesendet wurden (Zeile 1), meldet jedoch keinen Authentifizierungsfehler;
Rufen wir nun die URL / über einen Browser auf (unten Firefox):

- Wie bei Postman hat Firefox die HTTP-Antwort vom Server mit den folgenden HTTP-Headern erhalten:
Firefox stoppt, wie andere Browser auch, den Dialog nicht, wenn er diese Header erhält. Er fordert den Benutzer auf, die vom Server angeforderten Anmeldedaten einzugeben. Im obigen Beispiel erhält man durch einfache Eingabe von „admin / admin“ die Antwort des Servers:

23.3. Der Web-Client des Steuerberechnungsservers
23.3.1. Einleitung
Im vorherigen Abschnitt war der Web-Client für den Steuerberechnungsserver ein Browser. In diesem Abschnitt wird der Web-Client ein Konsolenskript sein. Die Architektur sieht dann wie folgt aus:

- Der Web-Client besteht aus den Schichten [1–2];
- der Webserver besteht aus den Schichten [3–9]. Wie im vorherigen Abschnitt erwähnt;
müssen wir daher die Schichten [1-2] schreiben.
Die Schicht [dao] [2] muss mit dem Webserver [3] kommunizieren können. Wir verstehen nun das HTTP-Protokoll und könnten beispielsweise mit dem bereits behandelten Modul [pycurl] ein Skript schreiben, das mit dem Webserver [3] kommuniziert. Es gibt jedoch Module, die auf die HTTP-Client-Server-Kommunikation spezialisiert sind. Wir werden eines davon verwenden, das Modul [requests]:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\01\flask>pip install requests
Collecting requests
Downloading requests-2.24.0-py2.py3-none-any.whl (61 kB)
|| 61 kB 137 kB/s
Collecting idna<3,>=2.5
Downloading idna-2.10-py2.py3-none-any.whl (58 kB)
|| 58 kB 692 kB/s
Collecting chardet<4,>=3.0.2
Downloading chardet-3.0.4-py2.py3-none-any.whl (133 kB)
|| 133 kB 1.3 MB/s
Collecting urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1
Downloading urllib3-1.25.9-py2.py3-none-any.whl (126 kB)
|| 126 kB 1.1 MB/s
Collecting certifi>=2017.4.17
Downloading certifi-2020.6.20-py2.py3-none-any.whl (156 kB)
|| 156 kB 1.1 MB/s
Installing collected packages: idna, chardet, urllib3, certifi, requests
Successfully installed certifi-2020.6.20 chardet-3.0.4 idna-2.10 requests-2.24.0 urllib3-1.25.9
Die Verzeichnisstruktur für die Web-Client-Skripte sieht wie folgt aus:

Das Skript implementiert die in |Version 1| beschriebene Anwendung zur Steuerberechnung im Batch-Modus. Die neueste Version dieser Anwendung ist |Version 5|. Hier noch einmal zur Erinnerung, wie sie funktioniert:
- Die Steuerzahler, für die die Steuer berechnet wird, sind in der Textdatei [taxpayersdata.txt] aufgeführt:
- Die Ergebnisse werden in zwei Dateien gespeichert:
- Die Textdatei [errors.txt] listet die in der Steuerzahlerdatei festgestellten Fehler auf:
Analyse du fichier C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-clients\01\main/../data/input/taxpayersdata.txt
Ligne 15, not enough values to unpack (expected 4, got 2)
Ligne 17, MyException[1, L'identifiant d'une entité <class 'TaxPayer.TaxPayer'> doit être un entier >=0]
- (Fortsetzung)
- Die JSON-Datei [results.json] enthält die Ergebnisse der Steuerberechnung für die verschiedenen Steuerzahler:
[
{
"id": 0,
"marié": "oui",
"enfants": 2,
"salaire": 55555,
"impôt": 2814,
"surcôte": 0,
"taux": 0.14,
"décôte": 0,
"réduction": 0
},
{
"id": 1,
"marié": "oui",
"enfants": 2,
"salaire": 50000,
"impôt": 1384,
"surcôte": 0,
"taux": 0.14,
"décôte": 384,
"réduction": 347
},
…
]
23.3.2. Konfiguration des Web-Clients

Die Konfiguration erfolgt mithilfe von zwei Skripten:
- [config], das alle Konfigurationen außerhalb der Architekturschichten übernimmt;
- [config_layers], das die Konfiguration der Architekturschichten übernimmt;
Das Skript [config] lautet wie folgt:
- Zeile 1: Die Funktion [configure] nimmt als Parameter das Wörterbuch entgegen, das mit Konfigurationsinformationen gefüllt werden soll. Dieses Wörterbuch kann bereits vorbelegt oder leer sein. Hier ist es leer;
- Zeilen 40–42: die absoluten Pfade der drei Textdateien, die von der [dao]-Schicht verwaltet werden;
- Zeilen 43–50: dem Schlüssel [server] zugeordnet, die Informationen, die die [dao]-Schicht über den Webserver benötigt, mit dem sie kommunizieren muss:
- Zeile 44: die URL des Webdienstes;
- Zeile 45: Der Schlüssel [authBasic] wird auf „True“ gesetzt, wenn der Zugriff auf die URL eine Basic-Authentifizierung erfordert;
- Zeilen 46–49: die Anmeldedaten des Benutzers, der sich authentifizieren wird, falls eine Authentifizierung erforderlich ist;
- Zeilen 56–57: Wir instanziieren die Schichten – in diesem Fall die einzelne [dao]-Schicht – und platzieren die Schichtenreferenzen in [config] unter dem Schlüssel [layers];
Das Skript [config_layers] lautet wie folgt:
- Zeile 1: Die Funktion [configure] erhält das Wörterbuch, das die Anwendung konfiguriert;
- Zeilen 4–6: Die [dao]-Schicht wird instanziiert. In Zeile 6 übergeben wir ihr die Anwendungskonfiguration, in der sie die benötigten Informationen findet;
- Zeilen 8–11: Es wird ein Wörterbuch zurückgegeben, das den Verweis auf die [dao]-Schicht enthält;
23.3.3. Das Hauptskript [main]
Das Hauptskript [main] ist eine Variante des Skripts aus |Version 5|:
- Zeilen 2–3: Die Anwendung wird konfiguriert;
- Zeile 13: Die [dao]-Schicht liefert die Liste der Steuerzahler, für die Steuern berechnet werden müssen;
- Zeile 21: Die [dao]-Schicht berechnet die Steuer für jeden einzelnen;
- Zeile 23: Die Ergebnisse werden in einer JSON-Datei gespeichert;
23.3.4. Implementierung der [dao]-Schicht

Werfen wir noch einmal einen Blick auf die verwendete Client/Server-Architektur:

- In [2, 6] sehen wir, dass die [dao]-Schicht zwei Aufgaben hat:
- Sie greift auf das Dateisystem zu, um sowohl Steuerzahlerdaten zu lesen als auch die Ergebnisse der Steuerberechnungen zu schreiben. Wir verfügen bereits über eine |AbstractImpôtsDao|-Klasse, die dies leisten kann. Sie ist seit |Version 4| im Einsatz;
- sie kommuniziert mit dem Webserver [3];
In |Version 5| kommunizierte das Hauptskript [main] [1] direkt mit der [Business]-Schicht [4]. Wir möchten dieses Skript lieber nicht ändern. Um dies zu erreichen, stellen wir sicher, dass die [DAO]-Schicht [2] die Schnittstelle der [business]-Schicht [4] implementiert. Auf diese Weise scheint das Hauptskript [main] direkt mit der [business]-Schicht [4] zu kommunizieren und kann die Tatsache, dass sich diese auf einem anderen Rechner befindet, vollständig ignorieren.
Eine Definition der Klasse, die die [DAO]-Schicht [2] implementiert, könnte wie folgt lauten:
class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
- Die Klasse [TaxDaoWithHttpClient]:
- erbt von der Klasse [AbstractTaxDao], wodurch sie die Kommunikation mit dem Dateisystem [6] handhaben kann;
- implementiert die Schnittstelle [InterfaceImpôtsMétier], um das Hauptskript [main] von |Version 5| nicht ändern zu müssen;
Der vollständige Code für die Klasse [TaxDaoWithHttpClient] lautet wie folgt:
- Zeilen 21–23: Die Klasse [AbstractTaxDao] (Zeile 12) verfügt über eine abstrakte Methode [get_admindata]. Wir müssen diese implementieren, auch wenn wir sie nicht verwenden (admindata wird vom Server verwaltet, nicht vom Client);
- Zeile 26: Die Methode [calculate_tax] gehört zur Schnittstelle [InterfaceImpôtsMétier] (Zeile 12). Wir müssen sie implementieren;
- Zeile 15: Der Konstruktor erhält das Konfigurationswörterbuch der Anwendung als einzigen Parameter;
- Zeilen 16–17: Die übergeordnete Klasse [AbstractTaxDao] wird initialisiert, indem ihr – auch hier – die Anwendungskonfiguration übergeben wird. Dort findet sie die Namen der drei Textdateien, die sie verwalten muss;
- Zeilen 18–19: Informationen zum Webserver für die Steuerberechnung werden lokal innerhalb der Klasse gespeichert;
- Zeile 26: Die Methode [calculate_tax] erhält ein Objekt vom Typ |Taxpayer| als Parameter. Um der Signatur der Methode [InterfaceImpôtsMétier.calculate_tax] zu entsprechen, erhält sie zudem einen Parameter [admindata], der die Daten der Steuerverwaltung kapseln soll. Auf der Client-Seite verfügen wir nicht über diese Daten. Dieser Parameter bleibt immer [None]. Diese Umgehungslösung deutet darauf hin, dass die Klasse [ImpôtsMétier] ursprünglich schlecht konzipiert war:
- Die Signatur von [calculate_tax] hätte einfach lauten sollen:
def calculate_tax(self, taxpayer: TaxPayer)
und der Parameter [admindata: AdminData] hätte an den Klassenkonstruktor übergeben werden sollen;
- Zeile 27: Der Code für die Methode [calculate_tax] wurde nicht in einen try/catch/finally-Block gekapselt. Das bedeutet, dass etwaige Ausnahmen nicht abgefangen werden und an den aufrufenden Code, in diesem Fall das Skript [main], weitergeleitet werden. Dieses Skript fängt alle Ausnahmen ab, die aus der [dao]-Schicht weitergeleitet werden;
- Zeile 28: Die Steuerberechnung erfolgt serverseitig. Wir müssen daher mit dem Server kommunizieren. Dazu verwenden wir das in Zeile 2 importierte [requests]-Modul;
- Zeilen 31–43: Um eine GET-Anfrage an den Webserver zu senden, verwenden wir die Methode [requests.get]:
- Zeilen 33–34: Der erste Parameter der Methode ist die URL, die aufgerufen werden soll;
- Zeilen 35–40: Die beiden anderen Parameter sind benannte Parameter, deren Reihenfolge keine Rolle spielt;
- Zeilen 35–36: Der Wert des benannten Parameters [params] muss ein Wörterbuch sein, das die Informationen enthält, die in der URL in der Form [/url?param1=value1¶m2=value2&…] enthalten sein sollen;
- Zeile 29: Das Wörterbuch enthält die drei Parameter [married, children, salary], die der Webserver erwartet. Wir müssen uns keine Gedanken über die Kodierung (sogenanntes URL-Encoding) machen, die diese Parameter durchlaufen müssen. [requests] übernimmt dies;
- Zeilen 37–40: Der Parameter [auth] ist ein Tupel aus zwei Elementen (login, password). Er stellt die Anmeldedaten für die Basic-Authentifizierung dar;
- Zeilen 44–45: Diese beiden Zeilen dienen nur zu Lehrzwecken (wir werden sie auskommentieren, sobald das Debugging abgeschlossen ist):
- [response] steht für die HTTP-Antwort des Servers;
- [response.text] stellt den Text des in dieser Antwort enthaltenen Dokuments dar. Während des Debuggens ist es nützlich, zu überprüfen, was der Server uns gesendet hat;
- Zeile 47: [response.status_code] ist der HTTP-Statuscode der empfangenen Antwort. Unser Server sendet nur drei:
- 200 OK
- 400 BAD REQUEST
- 500 INTERNALER SERVERFEHLER
- Zeile 49: Unser Server sendet immer JSON, auch im Falle eines Fehlers. Die Funktion [response.json()] erstellt aus der empfangenen JSON-Zeichenkette ein Dictionary. Sehen wir uns die beiden möglichen Formen der JSON-Zeichenkette an:
{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]", "paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
{"réponse": {"result": {"id": 0, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
- Zeilen 51–53: Wenn der Statuscode nicht 200 ist, wird eine Ausnahme mit den in der Antwort enthaltenen Fehlermeldungen ausgelöst;
- Zeile 56: Das durch die Steuerberechnung erzeugte Wörterbuch abrufen und damit den Eingabeparameter [taxpayer] aktualisieren;
23.3.5. Ausführung
So führen Sie den Client aus:
- Starten Sie den Server [server_03] mit dem DBMS Ihrer Wahl;
- Führen Sie das Skript [main] des Clients aus;
Die Ergebnisse finden Sie im Ordner [data/output]. Sie entsprechen denen der Version 5.
23.4. Tests der [dao]-Schicht
Kehren wir zur Client-Server-Anwendungsarchitektur zurück:

- Im Client-Code haben wir sichergestellt, dass die [dao]-Schicht [1] dieselbe Schnittstelle bereitstellt wie die [business]-Schicht [3]. Wir werden daher die Testklasse |TestDaoMétier| verwenden, die wir zuvor behandelt haben, um die [business]-Schicht [3] zu testen;
Die Testklasse wird in der folgenden Umgebung ausgeführt:

- Die Konfiguration [2] ist identisch mit der Konfiguration [1], die wir gerade betrachtet haben;
Die Testklasse [TestHttpClientDao] sieht wie folgt aus:
Diese Klasse ähnelt derjenigen, die bereits in Version 4 der Anwendung behandelt wurde.
- Zeilen 40–41: Konfiguration der Testumgebung;
- Zeile 44: Wir rufen eine Referenz auf die [DAO]-Schicht ab;
- Zeilen 47–48: Wir führen die Tests aus;
Um die Tests auszuführen, erstellen wir eine |Ausführungskonfiguration|:

- Wir erstellen eine Ausführungskonfiguration für ein Konsolenskript, nicht für einen UnitTest;
Beim Ausführen dieser Konfiguration werden die folgenden Ergebnisse erzielt:
Alle 11 Tests bestanden.