23. Anwendungsübung: Version 6
23.1. Einleitung
Wir kehren nun zu unserer Anwendung zur Steuerberechnung zurück. Wir werden darauf aufbauend verschiedene Webanwendungen erstellen.
In Version 5 unserer Anwendungsübung waren die Daten der Steuerbehörde in einer Datenbank gespeichert. Diese Version 5 umfasste zwei separate Anwendungen, die jedoch gemeinsame Schichten hatten:
- eine Anwendung, die die Steuer im |Batch|-Modus für Steuerzahler berechnete, die in einer Textdatei gespeichert waren;
- eine Anwendung, die die Steuer im |interaktiven| Modus für Steuerzahler berechnete, deren Daten über die Tastatur eingegeben wurden;
Die Version 5 der Anwendung zur Steuerberechnung im Batch-Modus wies folgende Architektur auf:

Letztendlich wird die Webversion dieser Anwendung folgende Architektur aufweisen:

- Der Web-Client [1] wendet sich an den Web-Server [2], der wiederum mit den Servern SGBD und [3] kommuniziert;
- Der Webserver [2] behält die Schichten [métier], [8] sowie [dao] und [9] der ursprünglichen Anwendung bei;
- Die ursprüngliche Anwendung behält ihr Hauptskript [4] sowie ihre Schichten [métier] und [15] bei. Die Schichten [métier], [8] und [15] sind identisch;
- die Client-Server-Kommunikation erfordert zwei zusätzliche Schichten:
- die Schicht [web] [7], die die Webanwendung implementiert;
- die Schicht [dao] [5], die als Client der Webanwendung [7] fungiert;
In der endgültigen Version kann die Steuerberechnung im Batch-Verfahren auf zwei Arten erfolgen:
- Die fachliche Steuerberechnung erfolgt über die Serverschicht [métier]. Das Skript [main] wird diese Methode verwenden;
- Die fachliche Steuerberechnung erfolgt über die Client-Schicht [métier]. Das Skript [main2] wird diese Methode verwenden;
Von nun an werden wir mehrere Client-Server-Anwendungen des oben genannten Typs entwickeln, von denen jede eine oder mehrere neue Webentwicklungstechnologien veranschaulicht.
23.2. Der Webserver zur Steuerberechnung
23.2.1. Version 1

Das Skript [server_01] ist die folgende Webanwendung:

- In [1] wird ein konfiguriertes URL verwendet, an das drei Werte übergeben werden:
- [marié] (ja/nein), um anzugeben, ob der Steuerpflichtige verheiratet ist;
- [enfants]: die Anzahl der Kinder des Steuerpflichtigen;
- [salaire]: das Jahreseinkommen des Steuerpflichtigen;
- In [2] gibt der Webserver eine Zeichenfolge jSON zurück, die den zu zahlenden Steuerbetrag mit seinen verschiedenen Bestandteilen angibt;
Die Architektur der Anwendung sieht wie folgt aus:

- Der Browser [1] fragt den Server [2] ab. Das Skript [server_01] implementiert die Server-Schichten [web] und [2];
- die Schichten [3-8] sind diejenigen, die bereits in der |Version 5| der Steuerberechnungsanwendung verwendet wurden. Wir übernehmen sie unverändert;
- Die Schichten [métier] und [3] sind |hier| definiert;
- Die Schicht [dao] [4] ist |hier| definiert;
Die Webanwendung [server_01] wird mithilfe von drei Skripten konfiguriert:
- [config], das die gesamte Anwendung konfiguriert;
- [config_database], das den Zugriff auf die Datenbank konfiguriert. Wir werden mit den Skripten SGBD, MySQL und PostgreSQL arbeiten;
- [config_layers], das die Schichten der Anwendung konfiguriert;
Das Skript [config] lautet wie folgt:
def configure(config: dict) -> dict:
import os
# Schritt 1 ------
# Verzeichnis dieser Datei
script_dir = os.path.dirname(os.path.abspath(__file__))
# Stammverzeichnis
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# absolute Abhängigkeiten
absolute_dependencies = [
# Projektordner
# BaseEntity, MyException
f"{root_dir}/classes/02/entities",
# InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
f"{root_dir}/impots/v04/interfaces",
# AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
f"{root_dir}/impots/v04/services",
# ImpotsDaoWithAdminDataInDatabase
f"{root_dir}/impots/v05/services",
# AdminData, ImpôtsError, TaxPayer
f"{root_dir}/impots/v04/entities",
# Konstanten, Tranchen
f"{root_dir}/impots/v05/entities",
# IndexController
f"{script_dir}/../controllers",
# Skripte [config_database, config_layers]
script_dir,
]
# Der Syspath wird festgelegt
from myutils import set_syspath
set_syspath(absolute_dependencies)
# Schritt 2 ------
# Konfiguration der Anwendung
# Liste der Benutzer, die zur Nutzung der Anwendung berechtigt sind
config['users'] = [
{
"login": "admin",
"password": "admin"
}
]
# Schritt 3 ------
# Datenbankkonfiguration
import config_database
config["database"] = config_database.configure(config)
# Schritt 4 ------
# Instanziierung der Anwendungsschichten
import config_layers
config['layers'] = config_layers.configure(config)
# Die Konfiguration wird übernommen
return config
- Die Funktion [configure] erhält ein Wörterbuch [config] als Parameter (Zeile 1) und gibt es als Ergebnis zurück (Zeile 54), nachdem sie dessen Inhalt erweitert hat. Man hätte schon längst feststellen können, dass es nicht notwendig ist, das Ergebnis [config] zurückzugeben. Denn [config] ist eine Wörterbuchreferenz, die der aufrufende Code mit dem aufgerufenen Code teilt. Der aufrufende Code verfügt also bereits über diese Referenz (Zeile 1), und es ist unnötig, sie ihm erneut zu übergeben (Zeile 54). Daher sollte man schreiben:
config=[module].configure(config) (1)
ist überflüssig. Es reicht aus, Folgendes zu schreiben:
[module].configure(config) (2)
Dennoch habe ich die Schreibweise (1) beibehalten, da ich dachte, dass sie vielleicht besser verdeutlicht, dass der aufgerufene Code das Wörterbuch [config] ändert.
- Zeile 1: Das von der Funktion [configure] empfangene Wörterbuch [config] hat einen Schlüssel „sgbd“, dessen Wert aus der Liste [‘mysql’, ‘pgres’] stammt. [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: Hier werden alle Ordner aufgelistet, die für die Webanwendung erforderliche Elemente enthalten. Sie werden Teil des Python-Pfads der Anwendung sein (Zeilen 30–31);
- Zeilen 33–40: Es werden nur bestimmte Benutzer zum Zugriff auf die Anwendung berechtigt. Hier gibt es eine Liste mit einem einzigen Benutzer;
- Zeilen 43–46: Das Skript [config_database] erstellt die Konfiguration der verwendeten Datenbank;
- Zeile 46: Die vom Skript [config_database] erstellte Konfiguration ist ein Wörterbuch, das in der allgemeinen Konfiguration unter dem Schlüssel „database“ abgelegt wird;
- Zeilen 48–51: Das Skript [config_layers] instanziiert die Schichten der Webanwendung. Es gibt ein Dictionary zurück, das in der allgemeinen Konfiguration unter dem Schlüssel „layers“ abgelegt wird;
Das Skript [config_database] ist das bereits in |Version 5| verwendete. Zur Erinnerung geben wir es hier noch einmal wieder:
def configure(config: dict) -> dict:
# SQLAlchemy-Konfiguration
from sqlalchemy import create_engine, Table, Column, Integer, MetaData, Float
from sqlalchemy.orm import mapper, sessionmaker
# Verbindungsstrings zu den verwendeten Datenbanken
connection_strings = {
'mysql': "mysql+mysqlconnector://admimpots:mdpimpots@localhost/dbimpots-2019",
'pgres': "postgresql+psycopg2://admimpots:mdpimpots@localhost/dbimpots-2019"
}
# Verbindungszeichenfolge zur verwendeten Datenbank
engine = create_engine(connection_strings[config['sgbd']])
# Metadaten
metadata = MetaData()
# die Konstantentabelle
constantes_table = Table("tbconstantes", metadata,
Column('id', Integer, primary_key=True),
Column('plafond_qf_demi_part', Float, nullable=False),
Column('plafond_revenus_celibataire_pour_reduction', Float, nullable=False),
Column('plafond_revenus_couple_pour_reduction', Float, nullable=False),
Column('valeur_reduc_demi_part', Float, nullable=False),
Column('plafond_decote_celibataire', Float, nullable=False),
Column('plafond_decote_couple', Float, nullable=False),
Column('plafond_impot_celibataire_pour_decote', Float, nullable=False),
Column('plafond_impot_couple_pour_decote', Float, nullable=False),
Column('abattement_dixpourcent_max', Float, nullable=False),
Column('abattement_dixpourcent_min', Float, nullable=False)
)
# Steuerklassen-Tabelle
tranches_table = Table("tbtranches", metadata,
Column('id', Integer, primary_key=True),
Column('limite', Float, nullable=False),
Column('coeffr', Float, nullable=False),
Column('coeffn', Float, nullable=False)
)
# Mappings
from Tranche import Tranche
mapper(Tranche, tranches_table)
from Constantes import Constantes
mapper(Constantes, constantes_table)
# die Session-Factory
session_factory = sessionmaker()
session_factory.configure(bind=engine)
# eine Sitzung
session = session_factory()
# Bestimmte Informationen werden gespeichert und in einem Dictionary zurückgegeben
return {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
"constantes_table": constantes_table, "session": session}
Das Skript [config_layers] konfiguriert die Schichten des Webservers. Wir greifen auf ein bereits bekanntes |Skript| zurück:
def configure(config: dict) -> dict:
# Instanziierung der Anwendungsschichten
# DAO
from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
dao = ImpotsDaoWithAdminDataInDatabase(config)
# Geschäftslogik
from ImpôtsMétier import ImpôtsMétier
métier = ImpôtsMétier()
# Die Instanzen der Schichten werden in ein Dictionary gelegt, das an den aufrufenden Code zurückgegeben wird
return {
"dao": dao,
"métier": métier
}
- Zeile 6: Die Ebene [dao] wird mit einer Datenbank implementiert;
- [ImpotsDaoWithAdminDataInDatabase] wurde |hier| definiert;
- [ImpôtsMétier] wurde |hier| definiert;
Das Hauptskript [server_01] lautet wie folgt:
# Es wird ein Parameter „mysql“ oder „pgres“ erwartet
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
sgbd = sys.argv[1].lower()
erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# Die Anwendung wird konfiguriert
import config
config = config.configure({'sgbd': sgbd})
# Abhängigkeiten
from ImpôtsError import ImpôtsError
from TaxPayer import TaxPayer
import re
from flask import request
from myutils import json_response
from flask import Flask
from flask_api import status
# Abruf der Daten von der Steuerbehörde
try:
# „admindata“ wird ein schreibgeschützter Datensatz auf Anwendungsebene sein
admindata = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
print(f"L'erreur suivante s'est produite : {erreur}")
sys.exit(1)
# Flask-Anwendung
app = Flask(__name__)
# Startseite URL: /?verheiratet=xx&Kinder=yy&Gehalt=zz
@app.route('/', methods=['GET'])
def index():
# zunächst keine Fehler
erreurs = []
# Die Anfrage muss drei Parameter in der URL „URL“ enthalten
if len(request.args) != 3:
erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")
# Der Familienstand wird aus dem URL abgerufen
marié = request.args.get('marié')
if marié is None:
erreurs.append("paramètre [marié] manquant")
else:
marié = marié.strip().lower()
erreur = marié != "oui" and marié != "non"
if erreur:
erreurs.append(f"paramétre marié [{marié}] invalide")
# Die Anzahl der Kinder wird aus dem URL abgerufen
enfants = request.args.get('enfants')
if enfants is None:
erreurs.append("paramètre [enfants] manquant")
else:
enfants = enfants.strip()
match = re.match(r"^\d+", enfants)
if not match:
erreurs.append(f"paramétre enfants [{enfants}] invalide")
else:
enfants = int(enfants)
# Das Gehalt wird aus dem URL abgerufen
salaire = request.args.get('salaire')
if salaire is None:
erreurs.append("paramètre [salaire] manquant")
else:
salaire = salaire.strip()
match = re.match(r"^\d+", salaire)
if not match:
erreurs.append(f"paramétre salaire [{salaire}] invalide")
else:
salaire = int(salaire)
# Ungültige Parameter in der Tabelle URL?
for key in request.args.keys():
if key not in ['marié', 'enfants', 'salaire']:
erreurs.append(f"paramètre [{key}] invalide")
# Gibt es Fehler?
if erreurs:
# Es wird eine Fehlermeldung an den Kunden gesendet
résultats = {"réponse": {"erreurs": erreurs}}
return json_response(résultats, status.HTTP_400_BAD_REQUEST)
# Keine Fehler, es kann weitergearbeitet werden
# Steuerberechnung
taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
config["layers"]["métier"].calculate_tax(taxpayer, admindata)
# Die Antwort wird an den Kunden gesendet
return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)
# nur manuell
if __name__ == '__main__':
# Flask-Server wird gestartet
app.config.update(ENV="development", DEBUG=True)
app.run()
- Zeilen 1–10: Der Parameter wird abgerufen, der angibt, welches SGBD verwendet werden soll;
- Zeilen 12–14: Mit dieser Information kann die Anwendung konfiguriert werden. Insbesondere wird der Python-Pfad aufgebaut;
- Zeilen 16–23: Mit dem neuen Python-Pfad werden die benötigten Elemente importiert;
- Zeilen 25–31: Man ruft die Daten der Steuerbehörde ab, mit denen die Steuer berechnet werden kann;
- Zeilen 33–34: Instanziierung der Flask-Anwendung;
- Zeile 38: Die Flask-Anwendung bedient ausschließlich die URL [/]. Sie erwartet eine URL, die wie folgt konfiguriert ist: [/ ?marié=xx&enfants=yy&salaire=zz] mit:
- xx: ja / nein;
- yy: Anzahl der Kinder;
- zz: Jahresgehalt;
- Zeilen 40–89: Die Gültigkeit der Parameter von URL wird überprüft;
- Zeile 41: Die Fehlermeldungen werden in der Liste [erreurs] gesammelt;
- Zeile 43: Man erinnert sich vielleicht daran, dass die Parameter des konfigurierten URL in [request.args] zu finden sind (siehe |hier|):
- Das Objekt [request] ist das in Zeile 20 importierte Flask-Objekt;
- das Objekt [request.args] verhält sich wie ein Wörterbuch;
- Zeilen 43–44: Es wird überprüft, ob genau drei Parameter vorhanden sind (nicht weniger, nicht mehr);
- Zeilen 46–49: Es wird überprüft, ob der Parameter [marié] in URL vorhanden ist;
- Zeilen 50–54: Ist er vorhanden, wird überprüft, ob sein Wert in Kleinbuchstaben – ohne führende und nachgestellte Leerzeichen – „ja“ oder „nein“ lautet;
- Zeilen 56–59: Es wird überprüft, ob der Parameter [enfants] in URL enthalten ist;
- Zeilen 60–66: Falls vorhanden, wird überprüft, ob sein Wert eine positive ganze Zahl ist;
- Zeile 66: Es ist zu beachten, dass die Parameter von URL und ihre Werte Zeichenketten sind. Der Wert des Parameters [enfants] wird in einen „int“-Wert umgewandelt;
- Zeilen 68–78: Für den Parameter [salaire] werden dieselben Prüfungen durchgeführt wie für den Parameter [enfants];
- Zeilen 81–83: Es wird überprüft, ob in URL keine anderen Parameter als [‘marié, ‘enfants’, ‘salaire’] enthalten sind;
- Zeilen 85–89: Ist die Liste [erreurs] nach all diesen Überprüfungen nicht leer, so wird diese Fehlerliste in Form einer Zeichenkette jSON und des Statuscodes [400 Bad Request] an den Kunden gesendet;
Da wir im weiteren Verlauf häufig eine Zeichenkette jSON als Antwort an den Kunden senden müssen, wurden die dafür erforderlichen Zeilen in das Modul [myutils.py] ausgelagert, das wir bereits verwendet haben:

Das Skript [myutils.py] sieht nun wie folgt aus:
# Importe
import json
import os
import sys
from flask import make_response
def set_syspath(absolute_dependencies: list):
# absolute_dependencies: eine Liste absoluter Ordnernamen
….
# Erstellung einer Antwort HTTP jSON
def json_response(réponse: dict, status_code: int) -> tuple:
# Hauptteil der Antwort HTTP
response = make_response(json.dumps(réponse, ensure_ascii=False))
# Antworttext von HTTP stammt von jSON
response.headers['Content-Type'] = 'application/json; charset=utf-8'
# Die Antwort HTTP wird gesendet
return response, status_code
- Zeile 16: Die Funktion [json_response] erwartet zwei Parameter:
- [réponse]: das Wörterbuch, dessen Zeichenfolge jSON an den Web-Client gesendet werden soll;
- [status_code]: den Statuscode HTTP der Antwort;
- Zeile 18: Der Hauptteil der Antwort wird auf jSON festgelegt;
- Zeile 20: Der Header HTTP wird hinzugefügt, der dem Webclient mitteilt, dass er jSON erhalten wird;
- Zeile 22: Die Antwort HTTP wird an den aufrufenden Code gesendet. Dieser ist dafür verantwortlich, sie an den Web-Client weiterzuleiten;
Die Datei [__init__.py] entwickelt 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 Modulen im maschinenweiten Bereich 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: Man muss sich im Ordner „[packages]“ befinden, um diesen Befehl einzugeben;
Der Code des Skripts [server_01] setzt sich wie folgt fort:
…
# Fehler?
if erreurs:
# Es wird eine Fehlerantwort an den Client gesendet
résultats = {"réponse": {"erreurs": erreurs}}
return json_response(résultats, status.HTTP_400_BAD_REQUEST)
# keine Fehler, es kann weitergearbeitet werden
# Steuerberechnung
taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': enfants, 'salaire': salaire})
config["layers"]["métier"].calculate_tax(taxpayer, admindata)
# Die Antwort wird an den Kunden gesendet
return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)
- Zeile 10: An dieser Stelle sind die im Skript „URL“ erwarteten Parameter vorhanden und korrekt;
- Zeile 10: Es wird das Objekt [TaxPayer] angelegt, das den Steuerpflichtigen modelliert;
- Zeile 11: Die Schicht [métier] wird aufgefordert, die Steuer zu berechnen. Es sei daran erinnert, dass die von der Schicht [métier] berechneten Elemente in das als Parameter übergebene Objekt [taxpayer] eingefügt werden;
- Zeile 13: Die Antwort wird in Form einer Zeichenkette jSON an den Web-Client gesendet. Dabei handelt es sich um die Zeichenkette jSON aus einem Wörterbuch. Dem Schlüssel [result] wird das Wörterbuch des Objekts [taxpayer] zugeordnet. Das Objekt [taxpayer] selbst konnte nicht hinterlegt werden, da es nicht in jSON serialisierbar ist;
Es werden zwei Ausführungskonfigurationen angelegt, eine für MySQL, die andere für PostgreSQL:

Hier sind einige Ausführungsbeispiele (Sie haben die Anwendung [server_01] und die verwendete SGBD gestartet und rufen dann die URL unter http://localhost:5000/ mit einem Browser auf):


Hier ein Beispiel für die Ausführung 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: Ein ungültiger URL wird angefordert;
- Zeile 10: Der Server antwortet mit dem Status 400 BAD REQUEST;
23.2.2. Version 2

In Version 2 des Servers wird die Verarbeitung von URL in das Modul [index_controller] [5] ausgelagert:
# Import der Abhängigkeiten
import re
from flask_api import status
from werkzeug.local import LocalProxy
# URL mit folgenden Parametern: /?verheiratet=xx&Kinder=yy&Gehalt=zz
def execute(request: LocalProxy, config: dict) -> tuple:
# Abhängigkeiten
from TaxPayer import TaxPayer
# zunächst keine Fehler
erreurs = []
# Die Abfrage muss drei Parameter enthalten
if len(request.args) != 3:
erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")
# Der Familienstand von URL wird abgerufen
marié = request.args.get('marié')
if marié is None:
erreurs.append("paramètre [marié] manquant")
else:
marié = marié.strip().lower()
erreur = marié != "oui" and marié != "non"
if erreur:
erreurs.append(f"paramétre marié [{marié}] invalide")
# Die Anzahl der Kinder von URL wird abgerufen
enfants = request.args.get('enfants')
if enfants is None:
erreurs.append("paramètre [enfants] manquant")
else:
enfants = enfants.strip()
match = re.match(r"^\d+", enfants)
if not match:
erreurs.append(f"paramétre enfants {enfants} invalide")
else:
enfants = int(enfants)
# Das Gehalt von URL wird abgerufen
salaire = request.args.get('salaire')
if salaire is None:
erreurs.append("paramètre [salaire] manquant")
else:
salaire = salaire.strip()
match = re.match(r"^\d+", salaire)
if not match:
erreurs.append(f"paramétre salaire {salaire} invalide")
else:
salaire = int(salaire)
# Gibt es weitere Parameter in URL?
for key in request.args.keys():
if not key in ['marié', 'enfants', 'salaire']:
erreurs.append(f"paramètre [{key}] invalide")
# Gibt es Fehler?
if erreurs:
# Es wird eine Fehlermeldung an den Kunden gesendet
résultats = {"réponse": {"erreurs": erreurs}}
return résultats, status.HTTP_400_BAD_REQUEST
# keine Fehler, es kann weitergearbeitet werden
# Steuerberechnung
taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
config["layers"]["métier"].calculate_tax(taxpayer, config["admindata"])
# Die Antwort wird an den Kunden gesendet
return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
- Zeile 9: Die Funktion [execute] erhält zwei Parameter:
- [request]: die Anfrage HTTP des Clients;
- [config]: das Konfigurationswörterbuch der Anwendung;
Das Skript [server_02] lautet wie folgt:
# Es wird ein MySQL- oder PostgreSQL-Parameter erwartet
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
sgbd = sys.argv[1].lower()
erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# Die Anwendung wird konfiguriert
import config
config = config.configure({'sgbd': sgbd})
# Abhängigkeiten
from ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
import index_controller
# Abruf der Daten von der Steuerbehörde
try:
# „admindata“ wird ein schreibgeschützter Datensatz auf Anwendungsebene sein
config['admindata'] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
print(f"L'erreur suivante s'est produite : {erreur}")
sys.exit(1)
# Flask-Anwendung
app = Flask(__name__)
# Startseite URL: /?verheiratet=xx&Kind=yy&Gehalt=zz
@app.route('/', methods=['GET'])
def index():
# Die Anfrage wird ausgeführt
résultat, statusCode = index_controller.execute(request, config)
# die Antwort wird gesendet
return json_response(résultat, statusCode)
# nur Hauptprogramm
if __name__ == '__main__':
# Der Server wird gestartet
app.config.update(ENV="development", DEBUG=True)
app.run()
- Zeilen 36–41: Verarbeitung 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

In Version 3 wird das Konzept der Authentifizierung eingeführt.
Das Skript [server_03] sieht nun wie folgt aus:
# Warten auf einen MySQL- oder PostgreSQL-Parameter
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
sgbd = sys.argv[1].lower()
erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# Die Anwendung wird konfiguriert
import config
config = config.configure({'sgbd': sgbd})
# Abhängigkeiten
from ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
from flask_httpauth import HTTPBasicAuth
import index_controller
# Abruf der Daten von der Steuerbehörde
try:
# config[‘admindata’] wird eine schreibgeschützte Variable auf Anwendungsebene sein
config["admindata"] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
print(f"L'erreur suivante s'est produite : {erreur}")
sys.exit(1)
# Authentifizierungsmanager
auth = HTTPBasicAuth()
# Authentifizierungsmethode
@auth.verify_password
def verify_credentials(login: str, password: str) -> bool:
# Benutzerliste
users = config['users']
# Diese Liste wird durchlaufen
for user in users:
if user['login'] == login and user['password'] == password:
return True
# nicht gefunden
return False
# Flask-Anwendung
app = Flask(__name__)
# Startseite URL: /?verheiratet=xx&Kind=yy&Gehalt=zz
@app.route('/', methods=['GET'])
@auth.login_required
def index():
# Die Anfrage wird ausgeführt
résultat, statusCode = index_controller.execute(request, config)
# die Antwort wird gesendet
return json_response(résultat, statusCode)
# nur Hauptprogramm
if __name__ == '__main__':
# Der Server wird gestartet
app.config.update(ENV="development", DEBUG=True)
app.run()
- Zeile 21: Es wird ein Authentifizierungsmanager importiert. Es gibt verschiedene Arten der Authentifizierung bei einem Webserver. Die hier verwendete heißt [HTTP Basic]. Jede Art der Authentifizierung folgt einem bestimmten Client-Server-Dialog;
- Zeile 33: Es wird eine Instanz des Authentifizierungshandlers erstellt;
- Zeile 37: Die Annotation [@auth.verify_password] kennzeichnet die Funktion, die ausgeführt werden soll, wenn der Authentifizierungshandler die vom Client übermittelten Anmeldedaten gemäß dem Protokoll [HTTP Basic] überprüfen möchte;
- Zeile 55: Die Annotation [@auth.login_required] kennzeichnet eine Route, für die der Webclient authentifiziert werden muss. Hat der Webclient seine Anmeldedaten noch nicht gesendet, fordert der Webserver diese automatisch gemäß dem Basic-Protokoll HTTP 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 uns an, was in der Postman-Konsole passiert. Sie:
- Erstellen Sie eine Ausführungskonfiguration;
- starten Sie die Webanwendung;
- starten Sie das SGBD Ihrer Wahl;
- fordern Sie den URL [/] mit Postman an;
Der Client-Server-Dialog in der Postman-Konsole sieht wie folgt aus:
- Zeile 10: Der Server antwortet, dass wir keine Berechtigung zum Zugriff auf die URL und [/] haben;
- Zeile 13: Er gibt das zu verwendende Authentifizierungsprotokoll an, in diesem Fall das sogenannte Basic-Authentifizierungsprotokoll;
Es ist möglich, Postman so zu konfigurieren, dass es die Anmeldedaten des Benutzers gemäß dem Basic-Authentifizierungsprotokoll übermittelt:

- In „[6-7]“ tragen wir die Anmeldedaten aus dem Skript „[config]“ ein:
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 Anmeldedaten des Benutzers [admin / admin] in verschlüsselter Form;
- Zeile 17: Der Server antwortet korrekt. Er meldet Fehler, da die Parameter [marié, enfants, salaire] (Zeile 1) nicht gesendet wurden, meldet jedoch keinen Authentifizierungsfehler;
Nun rufen wir URL / mit einem Browser (unten Firefox) auf:

- Genau wie bei Postman hat Firefox die Antwort HTTP vom Server mit den Headern HTTP erhalten:
Firefox und andere Browser brechen den Dialog nicht ab, wenn sie diese Header erhalten. Sie fordern den Benutzer auf, die vom Server angeforderten Anmeldedaten einzugeben. Es reicht aus, oben „admin / admin“ einzugeben, um die Antwort des Servers zu erhalten:

23.3. Der Webclient des Steuerberechnungsservers
23.3.1. Einleitung
Im vorigen Abschnitt war der Webclient des Steuerberechnungsservers ein Browser. In diesem Abschnitt wird der Webclient ein Konsolenskript sein. Die Architektur sieht nun wie folgt aus:

- Der Web-Client besteht aus den Schichten [1-2];
- der Webserver besteht aus den Schichten [3-9]. Dies wurde im vorigen Abschnitt beschrieben;
Wir müssen also die Schichten [1-2] schreiben.
Die Schicht [dao] [2] muss mit dem Webserver [3] kommunizieren können. Wir kennen nun das Protokoll HTTP und könnten beispielsweise mit dem bereits behandelten Modul [pycurl] ein Skript schreiben, das mit dem Webserver [3] kommuniziert. Es gibt jedoch Module, die speziell auf Client-Server-Dialoge (HTTP) ausgerichtet sind. Wir werden eines davon verwenden, nämlich 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 Skriptstruktur des Web-Clients sieht wie folgt aus:

Das Skript implementiert die bereits ab |Version 1| beschriebene Anwendung zur Steuerberechnung im Batch-Modus. Die neueste Version dieser Anwendung ist |Version 5|. Zur Erinnerung:
- Die Steuerpflichtigen, für die die Steuer berechnet werden soll, sind in der Textdatei [taxpayersdata.txt] zusammengefasst:
- Die Ergebnisse werden in zwei Dateien gespeichert:
- Die Textdatei [errors.txt] enthält die in der Steuerpflichtigen-Datei festgestellten Fehler:
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 Datei jSON [résultats.json] enthält die Ergebnisse der Steuerberechnungen der verschiedenen Steuerpflichtigen:
[
{
"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 die gesamte Konfiguration außerhalb der Architektur-Schichten übernimmt;
- [config_layers], das die Konfiguration der Architektur-Schichten übernimmt;
Das Skript [config] lautet wie folgt:
def configure(config: dict) -> dict:
import os
# Schritt 1 ------
# Ordner dieser Datei
script_dir = os.path.dirname(os.path.abspath(__file__))
# Stammverzeichnis
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# Absolute Abhängigkeiten
absolute_dependencies = [
# Projektordner
# BaseEntity, MyException
f"{root_dir}/classes/02/entities",
# InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
f"{root_dir}/impots/v04/interfaces",
# AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
f"{root_dir}/impots/v04/services",
# ImpotsDaoWithAdminDataInDatabase
f"{root_dir}/impots/v05/services",
# AdminData, ImpôtsError, TaxPayer
f"{root_dir}/impots/v04/entities",
# Konstanten, Tranchen
f"{root_dir}/impots/v05/entities",
# ImpôtsDaoWithHttpClient
f"{script_dir}/../services",
# Konfigurationsskripte
script_dir,
]
# Festlegen des Syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# Schritt 2 ------
# Konfiguration der Anwendung mit Konstanten
config.update({
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
"resultsFilename": f"{script_dir}/../data/output/résultats.json",
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
"server": {
"urlServer": "http://127.0.0.1:5000/",
"authBasic": True,
"user": {
"login": "admin",
"password": "admin"
}
}
}
)
# Schritt 3 ------
# Instanziierung der Schichten
import config_layers
config['layers'] = config_layers.configure(config)
# Die Konfiguration wird übernommen
return config
- Zeile 1: Die Funktion [configure] erhält als Parameter das Wörterbuch, das mit den Konfigurationsinformationen gefüllt werden soll. Dieses kann bereits vorbelegt oder leer sein. Hier ist es leer;
- Zeilen 40–42: die absoluten Namen der drei Textdateien, die von der Schicht [dao] verwaltet werden;
- Zeilen 43–50: In Verbindung mit dem Schlüssel [server] die Informationen, die die Schicht [dao] über den Webserver kennen muss, mit dem sie kommunizieren soll:
- Zeile 44: der URL des Webdienstes;
- Zeile 45: Der Schlüssel [authBasic] hat den Wert „True“, wenn der Zugriff auf URL eine Authentifizierung vom Typ „Basic“ erfordert;
- Zeilen 46–49: die Anmeldedaten des Benutzers, der sich authentifizieren wird, falls eine Authentifizierung erforderlich ist;
- Zeilen 56–57: Die Schichten werden instanziiert, hier die einzige Schicht [dao], und die Referenzen der Schichten werden in [config] gespeichert, die dem Schlüssel [layers] zugeordnet sind;
Das Skript [config_layers] lautet wie folgt:
def configure(config: dict) -> dict:
# Instanziierung der Schichten der Anwendung
# DAO-Schicht
from ImpôtsDaoWithHttpClient import ImpôtsDaoWithHttpClient
dao = ImpôtsDaoWithHttpClient(config)
# Konfiguration der Schichten
return {
"dao": dao
}
- Zeile 1: Die Funktion [configure] erhält das Wörterbuch, das die Anwendung konfiguriert;
- Zeilen 4–6: Die Schicht [dao] wird instanziiert. In Zeile 6 wird ihr die Konfiguration der Anwendung übergeben, in der sie die benötigten Informationen findet;
- Zeilen 8–11: Es wird ein Dictionary zurückgegeben, in das die Referenz der Schicht [dao] aufgenommen wurde;
23.3.3. Das Hauptskript [main]
Das Hauptskript [main] ist eine Variante des Skripts aus |Version 5|:
# Konfiguration der Anwendung
import config
config = config.configure({})
# Abhängigkeiten
from ImpôtsError import ImpôtsError
# Code
try:
# die Ebene wird abgerufen [dao]
dao = config["layers"]["dao"]
# Auslesen der Steuerzahlerdaten
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# der Steuerzahler?
if not taxpayers:
raise ImpôtsError(f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
# Berechnung der Steuer der Steuerzahler
for taxpayer in taxpayers:
# „taxpayer“ ist sowohl ein Eingabe- als auch ein Ausgabeparameter
# „taxpayer“ wird geändert
dao.calculate_tax(taxpayer)
# Schreiben der Ergebnisse in eine Textdatei
dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
# Anzeige des Fehlers
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# abgeschlossen
print("Travail terminé...")
- Zeilen 2–3: Die Anwendung wird konfiguriert;
- Zeile 13: Die Ebene [dao] liefert die Liste der Steuerpflichtigen, für die die Steuer berechnet werden muss;
- Zeile 21: Die Ebene [dao] berechnet die Steuer für jeden einzelnen Steuerpflichtigen;
- Zeile 23: Die Ergebnisse werden in einer Datei mit dem Namen jSON gespeichert;
23.3.4. Implementierung der Schicht [dao]

Kommen wir noch einmal auf die verwendete Client-Server-Architektur zurück:

- In [2, 6] ist zu sehen, dass die Schicht [dao] zwei Aufgaben hat:
- Sie greift auf das Dateisystem zu, um sowohl die Daten der Steuerzahler zu lesen als auch die Ergebnisse der Steuerberechnungen zu schreiben. Wir haben bereits eine Klasse |AbstractImpôtsDao|, die dies beherrscht. Sie wurde bereits ab |Version 4| verwendet;
- sie kommuniziert mit dem Webserver [3];
In der |Version 5| kommunizierte das Hauptskript [main] [1] direkt mit der Schicht [métier] [4]. Dieses Skript soll nicht geändert werden. Zu diesem Zweck wird sichergestellt, dass die Schicht [dao] [2] die Schnittstelle der Schicht [métier] [4] implementiert. Auf diese Weise hat das Hauptskript [main] den Eindruck, direkt mit der Schicht [métier] [4] zu kommunizieren, und kann vollständig ignorieren, dass sich diese auf einem anderen Rechner befindet.
Eine Definition der Klasse, die die Schicht [dao] [2] implementiert, könnte wie folgt aussehen:
class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
- Die Klasse [ImpôtsDaoWithHttpClient]:
- erbt von der Klasse [AbstractImpôtsDao], was es ihr ermöglicht, die Kommunikation mit dem Dateisystem [6] zu verwalten;
- sie implementiert die Schnittstelle [InterfaceImpôtsMétier], um das Hauptskript [main] der |Version 5| nicht ändern zu müssen;
Der vollständige Code der Klasse [ImpôtsDaoWithHttpClient] lautet wie folgt:
# 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):
# Initialisierung des übergeordneten Objekts
AbstractImpôtsDao.__init__(self, config)
# Parameter speichern
self.__config_server = config["server"]
# nicht verwendete Methode von [AbstractImpôtsDao]
def get_admindata(self) -> AdminData:
pass
# Steuerberechnung
def calculate_tax(self: object, taxpayer: TaxPayer, admindata: AdminData = None):
# Ausnahmen werden weitergeleitet
# GET-Parameter
params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
# Verbindung mit Auth-Basic-Authentifizierung?
if self.__config_server['authBasic']:
response = requests.get(
# URL des abgefragten Servers
self.__config_server['urlServer'],
# Parameter des URL
params=params,
# Basic-Authentifizierung
auth=(
self.__config_server["user"]["login"],
self.__config_server["user"]["password"]))
else:
# Verbindung ohne Basic-Authentifizierung
response = requests.get(self.__config_server['urlServer'], params=params)
# Überprüfung
print(response.text)
# 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(87, résultat['réponse']['erreurs'])
# Es ist bekannt, dass das Ergebnis dem Schlüssel [result] der Antwort zugeordnet wurde
# Der Eingabeparameter wird mit diesem Ergebnis geändert
taxpayer.fromdict(résultat["réponse"]["result"])
- Zeilen 21–23: Die Klasse [AbstractImpôtsDao] (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 und nicht vom Client verwaltet);
- 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 als einzigen Parameter das Wörterbuch mit der Anwendungskonfiguration;
- Zeilen 16–17: Die übergeordnete Klasse [AbstractImpôtsDao] wird initialisiert, indem ihr ebenfalls die Anwendungskonfiguration übergeben wird. Darin findet sie die Namen der drei Textdateien, die sie verarbeiten muss;
- Zeilen 18–19: Die Informationen zum Webserver für die Steuerberechnung werden lokal in der Klasse gespeichert;
- Zeile 26: Die Methode [calculate_tax] erhält als Parameter ein Objekt vom Typ |Taxpayer|. Um die Signatur der Methode [InterfaceImpôtsMétier.calculate_tax] einzuhalten, erhält sie zusätzlich einen Parameter [admindata], der die Daten der Steuerbehörde kapseln soll. Auf der Client-Seite liegen diese Daten nicht vor. Dieser Parameter bleibt daher immer auf [None] gesetzt. Diese Umgehungslösung lässt darauf schließen, dass die Klasse [ImpôtsMétier] ursprünglich falsch geschrieben wurde:
- Die Signatur von [calculate_tax] hätte einfach lauten müssen:
def calculate_tax(self, taxpayer: TaxPayer)
und der Parameter [admindata : AdminData] hätte an den Konstruktor der Klasse übergeben werden müssen;
- Zeile 27: Der Code der Methode [calculate_tax] wurde nicht in einen try/catch/finally-Block gekapselt. Das bedeutet, dass eventuelle Ausnahmen nicht abgefangen werden und an den aufrufenden Code, in diesem Fall das Skript [main], weitergeleitet werden. Dieses fängt zwar alle Ausnahmen ab, die aus der Ebene [dao] weitergeleitet werden;
- Zeile 28: Die Steuerberechnung erfolgt serverseitig. Daher muss eine Kommunikation mit dem Server hergestellt werden. Dies geschieht über das in Zeile 2 importierte Modul [requests];
- Zeilen 31–43: Um eine Anfrage an den Webserver zu senden, wird die Methode [requests.get] verwendet:
- Zeilen 33–34: Der erste Parameter der Methode ist die zu kontaktierende URL;
- 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 das URL in der Form [/url ?param1=valeur1¶m2=valeur2&…] eingefügt werden sollen;
- Zeile 29: Das Wörterbuch enthält die drei Parameter [marié, enfants, salaire], die der Webserver erwartet. Man muss sich nicht um die Kodierung (sogenanntes „urlencoded“) kümmern, die diese Parameter durchlaufen müssen. [requests] übernimmt dies;
- Zeilen 37–40: Der Parameter mit dem Namen [auth] ist ein Tupel mit zwei Elementen (login, password). Er stellt die Anmeldedaten für eine Basic-Authentifizierung dar;
- Zeilen 44–45: Diese beiden Zeilen dienen lediglich zu Erklärungszwecken (sie werden nach Abschluss der Fehlersuche auskommentiert):
- [response] steht für die Antwort HTTP des Servers;
- [response.text] steht für den in dieser Antwort enthaltenen Dokumenttext. Während der Debugging-Phase ist es sinnvoll zu überprüfen, was der Server uns gesendet hat;
- Zeile 47: [response.status_code] ist der Statuscode HTTP der empfangenen Antwort. Unser Server sendet nur drei davon:
- 200 OK
- 400 BAD REQUEST
- 500 INTERNAL SERVER ERROR
- Zeile 49: Unser Server sendet immer „jSON“, auch im Fehlerfall. Die Funktion [response.json()] erstellt aus der empfangenen Zeichenfolge jSON ein Wörterbuch. Zur Erinnerung: Es gibt zwei mögliche Formen für die Zeichenfolge jSON:
{"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: Ist der Statuscode nicht 200, wird eine Ausnahme ausgelöst, wobei die Fehlermeldungen in die Antwort eingebettet werden;
- Zeile 56: Das bei der Steuerberechnung erzeugte Wörterbuch wird abgerufen und zur Aktualisierung des Eingabeparameters [taxpayer] verwendet;
23.3.5. Ausführung
So führen Sie den Client aus:
- Starten Sie den Server [server_03] mit dem SGBD 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 Schicht [dao]
Kehren wir zur Architektur der Client-Server-Anwendung zurück:
- Im Client-Code haben wir dafür gesorgt, dass die Schicht [dao] [1] dieselbe Schnittstelle bietet wie die Schicht [métier] [3]. Wir werden daher in [4] die bereits behandelte Testklasse |TestDaoMétier| verwenden, um die Schicht [métier] [3] zu testen;
Die Testklasse wird in der folgenden Umgebung ausgeführt:

- Die Konfiguration [2] ist identisch mit der Konfiguration [1], die wir gerade behandelt haben;
Die Testklasse [TestHttpClientDao] lautet wie folgt:
import unittest
class TestHttpClientDao(unittest.TestCase):
def test_1(self) -> None:
from TaxPayer import TaxPayer
# {'verheiratet': 'ja', 'Kinder': 2, 'Gehalt': 55555,
# 'Steuer': 2814, 'Zuschlag': 0, 'Abschlag': 0, 'Ermäßigung': 0, 'Satz': 0,14}
taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
dao.calculate_tax(taxpayer)
# Überprüfung
self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
self.assertEqual(taxpayer.décôte, 0)
self.assertEqual(taxpayer.réduction, 0)
self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
self.assertEqual(taxpayer.surcôte, 0)
…
def test_11(self) -> None:
from TaxPayer import TaxPayer
# {'verheiratet': 'ja', 'Kinder': 3, 'Gehalt': 200000,
# 'Steuer': 42842, 'Zuschlag': 17283, 'Abschlag': 0, 'Ermäßigung': 0, 'Satz': 0,41}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
dao.calculate_tax(taxpayer)
# Prüfungen
self.assertAlmostEqual(taxpayer.impôt, 42842, 1)
self.assertEqual(taxpayer.décôte, 0)
self.assertEqual(taxpayer.réduction, 0)
self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
self.assertAlmostEqual(taxpayer.surcôte, 17283, delta=1)
if __name__ == '__main__':
# Die Anwendung wird konfiguriert
import config
config = config.configure({})
# DAO-Schicht
dao = config['layers']['dao']
# Testmethoden werden ausgeführt
print("tests en cours...")
unittest.main()
Diese Klasse entspricht derjenigen, die bereits in Version 4 der Anwendung behandelt wurde.
- Zeilen 40–41: Hier wird die Testumgebung konfiguriert;
- Zeile 44: Es wird eine Referenz auf die Schicht [dao] abgerufen;
- Zeilen 47–48: Die Tests werden ausgeführt;
Um die Tests auszuführen, wird eine |Ausführungskonfiguration| erstellt:

- Es wird eine Ausführungskonfiguration für ein Konsolenskript erstellt, nicht für einen Test UnitTest;
Wenn man diese Konfiguration ausführt, erhält man folgende Ergebnisse:
Alle 11 Tests waren erfolgreich.