Skip to content

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:

Image

Letztendlich wird die Webversion dieser Anwendung folgende Architektur aufweisen:

Image

  • 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

Image

Das Skript [server_01] ist die folgende Webanwendung:

Image

  • 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:

Image

  • 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:

Image

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:

Image

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

Image

Image

Hier ein Beispiel für die Ausführung in der Postman-Konsole:

Image


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

Image

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

Image

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:

GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: e65e2a28-4fe3-423b-88b3-b3e5a83092b1
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 401 UNAUTHORIZED
Content-Type: text/html; charset=utf-8
Content-Length: 19
WWW-Authenticate: Basic realm="Authentication Required"
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 07:05:37 GMT

Unauthorized Access
  • 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:

Image

  • In „[6-7]“ tragen wir die Anmeldedaten aus dem Skript „[config]“ ein: Image

    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:

Image

  • Genau wie bei Postman hat Firefox die Antwort HTTP vom Server mit den Headern HTTP erhalten:
1
2
3
4
HTTP/1.0 401 UNAUTHORIZED
WWW-Authenticate: Basic realm="Authentication Required"

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:

Image

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:

Image

  • 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:

Image

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:
# Gültige Daten: ID, verheiratet, Kinder, Gehalt
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# Es werden fehlerhafte Zeilen erstellt
# zu wenige Werte
11,12
# Falsche Werte
x,x,x,x
  • 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

Image

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]

Image

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

Image

  • 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&param2=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:

Image

  • 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:

Image

  • 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:

C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-clients/01/tests/TestHttpClientDao.py
tests en cours...
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
....{"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}}}
...{"réponse": {"result": {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}}}
....
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}}}
----------------------------------------------------------------------
Ran 11 tests in 0.130s

OK

Process finished with exit code 0

Alle 11 Tests waren erfolgreich.