Skip to content

30. Praktische Übung: Version 12

In diesem Kapitel werden wir eine Webanwendung nach der MVC-Architektur (Model-View-Controller) schreiben. Die Anwendung wird in der Lage sein, Antworten in drei Formaten zurückzugeben: JSON, XML und HTML. Der Komplexitätsgrad ist bei dem, was wir nun tun werden, deutlich höher als bei dem, was wir bisher gemacht haben. Wir werden die meisten der bisher behandelten Konzepte wiederverwenden und alle Schritte, die zur endgültigen Anwendung führen, detailliert beschreiben.

30.1. MVC-Architektur

Wir werden das MVC-Architekturmuster (Model–View–Controller) wie folgt implementieren:

Image

Die Verarbeitung einer Client-Anfrage läuft wie folgt ab:

  • 1 – Anfrage

Die angeforderten URLs haben die Form http://machine:port/action/param1/param2/… Der [Haupt-Controller] verwendet eine Konfigurationsdatei, um die Anfrage an den richtigen Controller weiterzuleiten. Dazu nutzt er das Feld [action] der URL. Der Rest der URL [param1/param2/…] besteht aus optionalen Parametern, die an die Aktion übergeben werden. Das „C“ in MVC bezieht sich hier auf die Kette [Hauptcontroller, Controller / Aktion]. Wenn kein Controller die angeforderte Aktion verarbeiten kann, antwortet der Webserver, dass die angeforderte URL nicht gefunden wurde.

  • 2 – Verarbeitung
  • Die ausgewählte Aktion [2a] kann die Parameter verwenden, die ihr vom [Haupt-Controller] übergeben wurden. Diese können aus zwei Quellen stammen:
      • dem Pfad [/param1/param2/…] der URL,
      • aus Parametern, die im Body der Client-Anfrage gesendet wurden;
    • Bei der Verarbeitung der Benutzeranfrage benötigt die Aktion möglicherweise die [Business]-Schicht [2b]. Sobald die Anfrage des Clients verarbeitet wurde, kann dies verschiedene Antworten auslösen. Ein klassisches Beispiel ist:
      • eine Fehlerantwort, wenn die Anfrage nicht korrekt verarbeitet werden konnte;
      • ansonsten eine Bestätigungsantwort;
    • Der [Controller / Action] sendet seine Antwort [2c] zusammen mit einem Statuscode an den Hauptcontroller zurück. Diese Statuscodes geben den aktuellen Zustand der Anwendung eindeutig wieder. Es handelt sich entweder um einen Erfolgscode oder einen Fehlercode;
  • 3 – Antwort
    • Je nachdem, ob der Client eine JSON-, XML- oder HTML-Antwort angefordert hat, instanziiert der [Hauptcontroller] [3a] den entsprechenden Antworttyp und weist ihn an, die Antwort an den Client zu senden. Der [Hauptcontroller] übergibt ihm sowohl die Antwort als auch den Statuscode, der von dem ausgeführten [Controller/Action] bereitgestellt wurde;
    • Wenn die gewünschte Antwort vom Typ JSON oder XML ist, formatiert die ausgewählte Antwort die Antwort des [Controllers/der Aktion], die ihr übermittelt wurde, und sendet sie [3c]. Der Client, der diese Antwort verarbeiten kann, kann ein Python-Konsolenskript oder ein in eine HTML-Seite eingebettetes JavaScript-Skript sein;
    • Wenn die gewünschte Antwort vom Typ HTML ist, wählt die ausgewählte Antwort [3b] anhand des ihr übermittelten Statuscodes eine der HTML-Ansichten [Vuei] aus. Dies ist das V in MVC. Eine einzelne Ansicht entspricht einem einzelnen Statuscode. Diese Ansicht V zeigt die Antwort des [Controllers/der Aktion] an, die ausgeführt wurde. Sie verpackt die Daten dieser Antwort in HTML, CSS und JavaScript. Diese Daten werden als View-Modell bezeichnet. Dies ist das M in MVC. Der Client ist meist ein Browser;

Lassen Sie uns nun die Beziehung zwischen der MVC-Webarchitektur und der Schichtenarchitektur klären. Je nachdem, wie das Modell definiert ist, können diese beiden Konzepte miteinander in Verbindung stehen oder auch nicht. Betrachten wir eine einschichtige MVC-Webanwendung:

Image

Im obigen Beispiel enthalten der [Controller / die Aktion] jeweils Teile der [Business-] und [DAO-]Schichten. In der [Web-]Schicht haben wir zwar eine MVC-Architektur, aber die Anwendung als Ganzes verfügt nicht über eine Schichtenarchitektur. Hier gibt es nur eine Schicht – die Web-Schicht –, die alles abwickelt.

Betrachten wir nun eine mehrschichtige Webarchitektur:

Image

Die [Web]-Schicht kann implementiert werden, ohne dem MVC-Modell zu folgen. Wir haben dann eine mehrschichtige Architektur, aber die Web-Schicht implementiert das MVC-Modell nicht.

In der .NET-Welt kann die oben genannte [Web]-Schicht beispielsweise mit ASP.NET MVC implementiert werden, was zu einer mehrschichtigen Architektur mit einer [Web]-Schicht im MVC-Stil führt. Anschließend können wir diese ASP.NET MVC-Schicht durch eine klassische ASP.NET-Schicht (WebForms) ersetzen, während der Rest (Geschäftslogik, DAO, Treiber) unverändert bleibt. Wir haben dann eine Schichtenarchitektur mit einer [Web]-Schicht, die nicht mehr MVC-basiert ist.

In MVC haben wir gesagt, dass das M-Modell das der V-Ansicht ist, d. h. die Menge der von der V-Ansicht angezeigten Daten. Es gibt eine weitere Definition des M-Modells in MVC:

Image

Viele Autoren sind der Ansicht, dass das, was rechts von der [Web]-Schicht liegt, das M-Modell von MVC bildet. Um Mehrdeutigkeiten zu vermeiden, können wir uns auf Folgendes beziehen:

  • das Domänenmodell, wenn wir uns auf alles rechts von der [Web-]Schicht beziehen;
  • das View-Modell, wenn wir uns auf die von einer View V angezeigten Daten beziehen;

Im Folgenden beziehen wir uns mit dem Begriff „Modell“ immer auf das View-Modell.

30.2. Client/Server-Anwendungsarchitektur

Die Webanwendung wird die folgende Architektur aufweisen:

Image

  • In [1] wird der Webserver zwei Arten von Clients haben:
    • in [2] einen Konsolen-Client, der JSON und XML mit dem Server austauscht;
    • in [3] einen Browser, der HTML vom Server empfängt und anzeigt;
  • Der Webserver [1] behält die [Business]- und [DAO]-Schichten aus früheren Versionen bei;
  • Der Web-Client [2] wird aktualisiert, um die neuen Service-URLs der Webanwendung zu berücksichtigen;
  • Die vom Browser angezeigte HTML-Anwendung muss von Grund auf neu geschrieben werden;

Wir werden die Anwendung in mehreren Phasen entwickeln:

  • Wir werden die JSON-Version des Servers entwickeln. Wir werden die Service-URLs des Servers nacheinander mit einem Postman-Client testen. Diese Methode ermöglicht es uns, das Framework des Webservers aufzubauen, ohne uns um die Ansichten der Anwendung (=HTML) kümmern zu müssen;
  • Nachdem wir den JSON-Server mit Postman getestet haben, werden wir ihn mit einem Konsolen-Client testen;
  • Anschließend wenden wir uns der XML-Version des Servers zu. Wir haben gesehen, dass der Wechsel von JSON zu XML unkompliziert ist;
  • schließlich werden wir uns der HTML-Version des Servers zuwenden. Wir werden eine MVC-Architektur aufbauen und die anzuzeigenden Ansichten definieren. Die HTML-Anwendung wird sowohl mit dem Postman-Client als auch mit einem Standardbrowser getestet;

30.3. Die Verzeichnisstruktur des Servercodes

Image

  • in [1: der Webserver als Ganzes;
  • in [2]: Vorerst ignorieren wir die Ordner [static, templates, tests_views], die zur HTML-Version des Servers gehören. Außerhalb dieses Ordners finden wir das Hauptskript [main] und dessen Konfiguration;
  • in [3] die Webserver-Controller. Dabei handelt es sich um Klasseninstanzen;
 
  • in [4] wird die HTTP-Antwort des Servers von Klassen verarbeitet;
  • in [5] behalten wir die Protokolldatei der vorherigen Server bei;

Wenn wir die HTML-Version des Servers erstellen, kommen weitere Ordner ins Spiel:

 
  • in [6] die statischen Elemente der HTML-Anwendung;
  • in [7] die HTML-Anwendungsvorlagen, unterteilt in Ansichten [9] und Ansichtsfragmente [8];
  • in [9] die Klassen, die die Ansichtsmodelle implementieren;

30.4. Die Service-URLs der Anwendung

Um den Webserver zu erstellen, gehen wir wie folgt vor:

  • Auf der Grundlage der Ansichten der HTML-Anwendung definieren wir die Aktionen, die die Webanwendung implementieren muss. Wir verwenden hier die tatsächlichen Ansichten, es könnten aber auch einfach Ansichten auf Papier sein;
  • Basierend auf diesen Aktionen definieren wir die Service-URLs der HTML-Anwendung;
  • Wir werden diese Service-URLs mithilfe eines Servers implementieren, der JSON zurückgibt. So können wir das Framework des Webservers definieren, ohne uns um die bereitzustellenden HTML-Seiten kümmern zu müssen. Wir werden diese Service-URLs mit Postman testen;
  • Anschließend testen wir unseren JSON-Server mit einem Konsolen-Client;
  • Sobald der JSON-Server validiert wurde, fahren wir mit dem Schreiben der HTML-Anwendung fort;

Die erste Ansicht wird die Authentifizierungsansicht sein:

Image

  • Die Aktion, die zu dieser ersten Ansicht führt, heißt [init-session] [1];
  • Ein Klick auf die Schaltfläche [Validate] löst die Aktion [authenticate-user] mit zwei übermittelten Parametern aus [2-3];

Die Ansicht zur Steuerberechnung:

Image

  • In [1] die Aktion [authenticate-user], die zu dieser Ansicht geführt hat;
  • Bei [2] löst ein Klick auf die Schaltfläche [Validate] die Ausführung der Aktion [calculate-tax] mit drei übermittelten Parametern aus [2-5];
  • Ein Klick auf den Link [6] löst die Aktion [list-simulations] ohne Parameter aus;
  • Ein Klick auf den Link [7] löst die Aktion [end-session] ohne Parameter aus;

Die dritte Ansicht zeigt die vom authentifizierten Benutzer durchgeführten Simulationen an:

Image

  • in [3] die Aktion [list-simulations], die zu dieser Ansicht geführt hat;
  • in [2] löst das Klicken auf den Link [Löschen] die Aktion [delete-simulation] mit einem Parameter aus: der Nummer der Simulation, die aus der Liste gelöscht werden soll;
  • Ein Klick auf den Link [3] löst die Aktion [display-tax-calculation] ohne Parameter aus, wodurch die Ansicht zur Steuerberechnung erneut angezeigt wird;
  • Ein Klick auf den Link [4] löst die Aktion [end-session] ohne Parameter aus;

Mit diesen ersten Informationen können wir die verschiedenen Service-URLs des Servers definieren:

Aktion
Rolle
Ausführungskontext
/init-session
Wird verwendet, um den Typ (json, xml, html) der gewünschten Antworten festzulegen
GET-Anfrage
Kann jederzeit gesendet werden
/authenticate-user
Autorisiert oder verweigert die Anmeldung eines Benutzers
POST-Anfrage.
Die Anfrage muss zwei übermittelte Parameter enthalten [user, password]
Kann nur gesendet werden, wenn der Sitzungstyp (json, xml, html) bekannt ist
/calculate-tax
Führt eine Simulation der Steuerberechnung durch
POST-Anfrage.
Die Anfrage muss drei übermittelte Parameter enthalten [verheiratet, Kinder, Gehalt]
Kann nur ausgegeben werden, wenn der Sitzungstyp (json, xml, html) bekannt ist und der Benutzer authentifiziert ist
/list-simulations
Anfrage zum Anzeigen der Liste der seit Beginn der Sitzung durchgeführten Simulationen
GET-Anfrage.
Kann nur gesendet werden, wenn der Sitzungstyp (json, xml, html) bekannt ist und der Benutzer authentifiziert ist
/delete-simulation/number
Löscht eine Simulation aus der Liste der Simulationen
GET-Anfrage.
Kann nur gesendet werden, wenn der Sitzungstyp (json, xml, html) bekannt ist und der Benutzer authentifiziert ist
/display-tax-calculation
Zeigt die HTML-Seite für die Steuerberechnung an
GET-Anfrage.
Kann nur ausgegeben werden, wenn der Sitzungstyp (json, xml, html) bekannt ist und der Benutzer authentifiziert ist
/end-session
Beendet die Simulationssitzung.
Technisch gesehen wird die alte Web-Sitzung gelöscht und eine neue Sitzung erstellt
Kann nur ausgegeben werden, wenn der Sitzungstyp (json, xml, html) bekannt ist und der Benutzer authentifiziert ist

Diese verschiedenen Service-URLs werden sowohl für den HTML-Server als auch für die JSON- oder XML-Server verwendet. Zwei URLs werden ausschließlich für die beiden letztgenannten Server verwendet: Es handelt sich um die URLs aus der vorherigen Version des Client-/Webservers, die wir hier wiederverwenden:

Aktion
Rolle
Ausführungskontext
/get-admindata
Gibt Steuerdaten zurück, die zur Berechnung von der Steuer
GET-Anfrage.
Wird nur verwendet, wenn der Sitzungstyp „json“ oder „xml“ ist. Der Benutzer muss authentifiziert sein
/calculate-taxes
Berechnet die Steuer für eine Liste von Steuerzahlern, die in einer JSON-
GET-Anfrage übermittelt wurde.
Wird nur verwendet, wenn der Sitzungstyp „json“ oder „xml“ ist. Der Benutzer muss authentifiziert sein

Alle mit diesen Aktionen verbundenen Controller verfahren auf die gleiche Weise:

  • Sie überprüfen ihre Parameter. Diese befinden sich im Objekt:
    • [request.path] für Parameter, die in der URL in der Form [/action/param1/param2/…] vorhanden sind;
    • im Objekt [request.form] für diejenigen, die als [x-www-form-urlencoded] im Request-Body übermittelt werden;
    • im Objekt [request.data] für diejenigen, die als JSON im Request-Body übertragen werden;
  • Ein Controller ähnelt einer Funktion oder Methode, die die Gültigkeit ihrer Parameter überprüft. Für den Controller ist es jedoch etwas komplizierter:
    • Die erwarteten Parameter fehlen möglicherweise;
    • Die vom Controller abgerufenen Parameter sind Zeichenfolgen. Wenn der erwartete Parameter eine Zahl ist, muss der Controller überprüfen, ob die Zeichenfolge des Parameters tatsächlich eine Zahl darstellt;
    • Sobald überprüft wurde, dass die erwarteten Parameter vorhanden und syntaktisch korrekt sind, müssen Sie sicherstellen, dass sie im aktuellen Ausführungskontext gültig sind. Dieser Kontext ist in der Sitzung vorhanden. Das Authentifizierungsbeispiel ist ein Beispiel für einen Ausführungskontext. Bestimmte Aktionen sollten erst verarbeitet werden, wenn der Client authentifiziert wurde. Im Allgemeinen gibt ein Schlüssel in der Sitzung an, ob diese Authentifizierung stattgefunden hat oder nicht;
    • Sobald die vorangegangenen Prüfungen abgeschlossen sind, kann der sekundäre Controller fortfahren. Dieser Prozess der Parameterüberprüfung ist sehr wichtig. Wir können nicht akzeptieren, dass ein Client uns zu irgendeinem Zeitpunkt während des Lebenszyklus der Anwendung beliebige Daten sendet. Wir müssen die volle Kontrolle über den Lebenszyklus der Anwendung behalten;
    • Sobald seine Arbeit erledigt ist, gibt der sekundäre Controller ein Wörterbuch mit den Schlüsseln [action, state, response] an den Hauptcontroller zurück, der ihn aufgerufen hat:
      • [action] ist die Aktion, die gerade ausgeführt wurde;
      • [state] ist eine dreistellige Zahl, die das Ergebnis der Verarbeitung der Aktion angibt:
    • [x00] steht für eine erfolgreiche Verarbeitung;
    • [x01] zeigt einen Verarbeitungsfehler an;
  • [response] ist das Wörterbuch der Ergebnisse in der Form {‘response’:object}. Das Objekt hat je nach der verarbeiteten Aktion unterschiedliche Strukturen;

Wir werden nun die verschiedenen Controller – oder, mit anderen Worten, die verschiedenen Aktionen, die diese Controller verarbeiten – betrachten, die den Workflow der Webanwendung steuern.

30.5. Serverkonfiguration

Image

Die Datenbankkonfiguration [config_database] und die Konfiguration der Server-Ebene [config_layers] sind identisch mit denen in früheren Versionen. Die Datei [config] enthält nun neue Informationen:

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

    #  step 1 ------

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

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

    #  dependencies
    absolute_dependencies = [
        #  project files
        #  BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        #  InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        #  AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        #  ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        #  AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  Logger, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
        #  scripts [config_database, config_layers]
        script_dir,
        #  controllers
        f"{script_dir}/../controllers",
        #  answers HTTP
        f"{script_dir}/../responses",
        #  view models
        f"{script_dir}/../models_for_views",
    ]

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

    #  web server dependencies

    #  controllers
    from AfficherCalculImpotController import AfficherCalculImpotController
    from AuthentifierUtilisateurController import AuthentifierUtilisateurController
    from CalculerImpotController import CalculerImpotController
    from CalculerImpotsController import CalculerImpotsController
    from FinSessionController import FinSessionController
    from GetAdminDataController import GetAdminDataController
    from InitSessionController import InitSessionController
    from ListerSimulationsController import ListerSimulationsController
    from MainController import MainController
    from SupprimerSimulationController import SupprimerSimulationController

    #  answers HTTP
    from HtmlResponse import HtmlResponse
    from JsonResponse import JsonResponse
    from XmlResponse import XmlResponse

    #  view models
    from ModelForAuthentificationView import ModelForAuthentificationView
    from ModelForCalculImpotView import ModelForCalculImpotView
    from ModelForErreursView import ModelForErreursView
    from ModelForListeSimulationsView import ModelForListeSimulationsView

    #  step 2 ------
    #  application configuration
    config.update({
        #  users authorized to use the application
        "users": [
            {
                "login": "admin",
                "password": "admin"
            }
        ],

        #  log file
        "logsFilename": f"{script_dir}/../data/logs/logs.txt",

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

        #  thread pause time in seconds
        "sleep_time": 0,

        #  authorized shares and their auditors
        "controllers": {
            #  initialization of a calculation session
            "init-session": InitSessionController(),
            #  user authentication
            "authentifier-utilisateur": AuthentifierUtilisateurController(),
            #  tax calculation in individual mode
            "calculer-impot": CalculerImpotController(),
            #  batch mode tax calculation
            "calculer-impots": CalculerImpotsController(),
            #  list of simulations
            "lister-simulations": ListerSimulationsController(),
            #  deleting a simulation
            "supprimer-simulation": SupprimerSimulationController(),
            #  end of calculation session
            "fin-session": FinSessionController(),
            #  display tax calculation view
            "afficher-calcul-impot": AfficherCalculImpotController(),
            #  obtaining data from tax authorities
            "get-admindata": GetAdminDataController(),
            #  main controller
            "main-controller": MainController()
        },

        #  different response types (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

        #  HTML views and their models depend on the state rendered by the controller
        "views": [
            {
                #  authentication view
                "états": [
                    #  /init-session success
                    700,
                    #  /authentifier-user failure
                    201
                ],
                "view_name": "views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                #  tax calculation
                "états": [
                    #  /authentifier-user success
                    200,
                    #  /calculate-tax-success
                    300,
                    #  /calculate-tax failure
                    301,
                    #  /show-tax-calculation
                    800
                ],
                "view_name": "views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                #  view of simulation list
                "états": [
                    #  /lister-simulations
                    500,
                    #  /suppress-simulation
                    600
                ],
                "view_name": "views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],

        #  view of unexpected errors
        "view-erreurs": {
            "view_name": "views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },

        #  redirections
        "redirections": [
            {
                "états": [
                    400,  #  /end-session successful
                ],
                #  redirection to
                "to": "/init-session/html",
            }
        ],
    }
    )

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

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

    #  we return the configuration
    return config
  • Bis Zeile 41 sehen wir Standardelemente;
  • Zeilen 43–66: In Zeile 43 wird der Python-Pfad des Servers definiert. Anschließend können wir die Abhängigkeiten des Projekts importieren:
    • Zeilen 45–55: die Liste der Controller;
    • Zeilen 57–60: die Liste der HTTP-Antworten;
    • Zeilen 62–66: die Liste der View-Vorlagen;
  • Zeilen 68–189: die Anwendungskonfiguration mit einer Reihe von Konstanten;
    • Zeilen 71–98: Diese Zeilen kennen wir bereits aus früheren Versionen;
    • Zeilen 101–122: das Controller-Wörterbuch:
      • Die Schlüssel sind die Namen der Aktionen;
      • die Werte sind eine Instanz des Controllers, der für die Abwicklung dieser Aktion zuständig ist. Jeder Controller wird als einzelne Instanz (Singleton) instanziiert. Dieselbe Instanz wird von verschiedenen Server-Threads ausgeführt. Daher muss bei gemeinsam genutzten Daten, die jeder Controller möglicherweise ändern möchte, Vorsicht walten;
    • Zeilen 125–129: das Wörterbuch der drei möglichen HTTP-Antworten:
      • Die Schlüssel sind die vom Client angeforderten Antworttypen (JSON, XML, HTML);
      • Die Werte sind eine Instanz der HTTP-Antwort. Jeder Antwortgenerator wird als einzelne Instanz (Singleton) instanziiert. Derselbe Generator wird von verschiedenen Server-Threads ausgeführt. Daher ist bei gemeinsam genutzten Daten, die jeder Generator möglicherweise ändern möchte, Vorsicht geboten;
    • Zeilen 132–186: Konfiguration der HTML-Ansichten. Diese Zeilen lassen wir vorerst außer Acht;
  • Zeilen 191–202: Diese Zeilen sind uns bereits aus früheren Versionen bekannt;

30.6. Der Pfad einer Client-Anfrage innerhalb des Servers

Image

Wir werden den Weg einer Client-Anfrage, die beim Server eintrifft, bis hin zur zurückgesendeten HTTP-Antwort verfolgen. Er folgt dem Ablauf des MVC-Servers.

30.6.1. Das [main]-Skript

Das [main]-Skript ist in vielerlei Hinsicht identisch mit dem der vorherigen Versionen. Wir stellen es dennoch vollständig zur Verfügung, um sicherzustellen, dass wir auf dem richtigen Fuß starten:

#  a mysql or pgres parameter is expected
import sys

syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

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

#  dependencies
from flask import request, Flask, session, url_for, redirect
from flask_api import status
from SendAdminMail import SendAdminMail
from myutils import json_response
from Logger import Logger
import threading
import time
from random import randint
from ImpôtsError import ImpôtsError
import os

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

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

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

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

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

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

#  flask application
app = Flask(__name__, template_folder="templates", static_folder="static")
#  session secret key
app.secret_key = os.urandom(12).hex()

#  the front controller
def front_controller() -> tuple:
    #  the request is processed
    logger = None
    

@app.route('/', methods=['GET'])
def index() -> tuple:
    #  redirect to /init-session/html
    return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)

#  init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  authenticate-user
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  calculate-tax
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  lister-simulations
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  delete-simulation
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  end of session
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  display-calculation-tax
@app.route('/afficher-calcul-impot', methods=['GET'])
def afficher_calcul_impot() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  get-admindata
@app.route('/get-admindata/<int:numero>', methods=['GET'])
def get_admindata() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  hand only
if __name__ == '__main__':
    #  start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • Zeilen 1–92: Alle diese Zeilen wurden bereits behandelt und erklärt;
  • Zeile 92: Der Server verwaltet eine Sitzung. Wir benötigen daher einen geheimen Schlüssel. Für jeden Benutzer speichern wir zwei Informationen in der Sitzung:
    • ob sich der Benutzer erfolgreich authentifiziert hat;
    • Jedes Mal, wenn eine Steuerberechnung durchgeführt wird, werden die Ergebnisse dieser Berechnung in eine Liste namens „Simulationsliste des Benutzers“ aufgenommen. Diese Liste wird in der Sitzung gespeichert;
  • Zeilen 100–151: die Liste der Service-URLs des Servers. Die zugehörigen Funktionen dienen als Filter: Alle URLs, die nicht in dieser Liste enthalten sind, werden vom Flask-Server mit einem [404 NOT FOUND]-Fehler zurückgewiesen. Sobald diese Filterung abgeschlossen ist, wird die Anfrage systematisch an einen „Front Controller“ weitergeleitet, der durch die Funktion [front_controller] in den Zeilen 94–98 implementiert wird, auf die wir gleich noch eingehen werden;
  • Zeilen 100–103: Behandlung der Route [/]. Der Einstiegspunkt für die Webanwendung ist die URL in Zeile 107. Daher leiten wir in Zeile 103 den Client zu dieser URL weiter:
  • Die Funktion [url_for] wird in Zeile 18 importiert. Sie hat hier zwei Parameter:
      • Der erste Parameter ist der Name einer der Routing-Funktionen, in diesem Fall die in Zeile 107. Wir sehen, dass diese Funktion einen Parameter [type_response] erwartet, bei dem es sich um den vom Client angeforderten Antworttyp (json, xml, html) handelt;
      • der zweite Parameter übernimmt den Namen des Parameters aus Zeile 107, [type_response], und weist ihm einen Wert zu. Gäbe es weitere Parameter, würden wir den Vorgang für jeden einzelnen wiederholen;
      • sie gibt die URL zurück, die mit der Funktion verknüpft ist, die durch die beiden ihr übergebenen Parameter bezeichnet wird. Hier gibt dies die URL aus Zeile 106 zurück, wobei der Parameter durch seinen Wert [/init-session/html] ersetzt wird;
    • Die Funktion [redirect] wurde in Zeile 18 importiert. Ihre Aufgabe ist es, einen HTTP-Redirect-Header an den Client zu senden:
      • Der erste Parameter ist die URL, zu der der Client umgeleitet werden soll;
      • der zweite Parameter ist der Statuscode der an den Client gesendeten HTTP-Antwort. Der Code [status.HTTP_302_FOUND] entspricht einer HTTP-Weiterleitung;

Die Funktion [ front_controller] in den Zeilen 94–98 führt die erste Verarbeitung der Anfrage des Clients durch:

#  the front controller
def front_controller() -> tuple:
    #  we process the request
    logger = None
    try:
        #  logger
        logger = Logger(config["logsFilename"])
        #  we store it in a config associated with the thread
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        #  log the request
        logger.write(f"[ front_controller] requête : {request}\n")
        #  the thread is interrupted if requested
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            #  pause is randomized so that some threads are interrupted and others not
            aléa = randint(0, 1)
            if aléa == 1:
                #  log before break
                logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                #  break
                time.sleep(sleep_time)
        #  forward the request to the main controller
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        #  we log the result sent to the customer
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        #  was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            #  send an e-mail to the application administrator
            send_adminmail(config, log)
        #  determine the desired type of response
        if session.get('typeResponse') is None:
            #  the session type has not yet been set - it will be jSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        #  build the response to be sent
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        #  we send the answer
        return response, status_code
    except BaseException as erreur:
        #  it's an unexpected error - log the error if possible
        if logger:
            logger.write(f"[ front_controller] {erreur}")
        #  we prepare the response to the customer
        résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
        #  we send a response in jSON
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        #  close the log file if it has been opened
        if logger:
            logger.close()
  • Zeilen 1–57: Dieser Code ist uns bekannt. So war dies beispielsweise der Code für die Funktion namens [main] im Skript [main] der vorherigen Version. Zu beachten ist der in den Zeilen 25–26 verwendete Controller:
  • Zeile 25: Wir rufen die Controller-Instanz mit dem Namen [main-controller] aus der Konfiguration ab. Dies sind die folgenden Zeilen:
    #  web server dependencies
    #  controllers
    
    from MainController import MainController

    #  authorized shares and their controllers
        "controllers": {
            ,
            #  main controller
            "main-controller": MainController()
        },
  • (Fortsetzung)
    • Zeile 10 oben: Beachten Sie, dass wir eine Instanz der Klasse abrufen;
  • Zeile 26: Wir weisen den Controller [MainController] an, die Anfrage zu bearbeiten;
  • Zeilen 30–45: Die vom [MainController] zurückgegebene Antwort wird an den Client gesendet. Wir werden etwas später auf diese Zeilen zurückkommen;

Die Aufgabe der Funktion [front_controller] und anschließend der Klasse [MainController] besteht darin, die für alle Anfragen gemeinsamen Aufgaben zu bearbeiten:

Im obigen Diagramm befinden wir uns noch in Phase 1 der Anforderungsverarbeitung. Der Hauptcontroller [MainController] fährt mit Schritt 1 fort.

Image

30.6.2. Der Hauptcontroller [MainController]

Der Hauptcontroller [MainController] setzt die von der Funktion [front_controller] begonnene Arbeit fort:

Alle Controller implementieren die folgende Schnittstelle [InterfaceController] [2]:

Image


from abc import ABC, abstractmethod
 
from werkzeug.local import LocalProxy
 
class InterfaceController(ABC):
 
    @abstractmethod
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        pass
  • Die Schnittstelle [InterfaceController] definiert in Zeile 8 lediglich die einzige Methode [execute]. Diese Methode nimmt drei Parameter entgegen:
    • [request]: die Anfrage des Clients;
    • [session]: die Sitzung des Clients;
    • [config]: die Anwendungskonfiguration;

Die Methode [execute] gibt ein Tupel mit zwei Elementen zurück:

  • Das erste Element ist das Ergebniswörterbuch in der Form {‘action’: action, ‘status’: status, ‘response’: results};
  • das zweite ist der HTTP-Statuscode, der an den Client zurückgegeben wird;

Der Hauptcontroller [MainController] [1] implementiert die Schnittstelle [InterfaceController] wie folgt:

#  import dependencies

from flask_api import status
from werkzeug.local import LocalProxy

#  web application controllers
from InterfaceController import InterfaceController

class MainController(InterfaceController):
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  retrieve path elements
        params = request.path.split('/')
        action = params[1]

        #  errors
        erreur = False
        #  session type must be known prior to certain actions
        type_response = session.get('typeResponse')
        if type_response is None and action != "init-session":
            #  we note the error
            résultat = {"action": action, "état": 101,
                        "réponse": ["pas de session en cours. Commencer par action [init-session]"]}
            erreur = True
        #  some actions require authentication
        user = session.get('user')
        if not erreur and user is None and action not in ["init-session", "authentifier-utilisateur"]:
            #  we note the error
            résultat = {"action": action, "état": 101,
                        "réponse": [f"action [{action}] demandée par utilisateur non authentifié"]}
            erreur = True
        #  are there any mistakes?
        if erreur:
            #  an error msg is returned
            return résultat, status.HTTP_400_BAD_REQUEST
        else:
            #  execute the controller associated with the action
            controller = config["controllers"][action]
            résultat, status_code = controller.execute(request, session, config)
            return résultat, status_code

Der [MainController] führt die ersten Prüfungen durch, um die Anfrage zu validieren.

  • Zeilen 11–13: Der Controller beginnt damit, die vom Client angeforderte Aktion abzurufen. Zur Erinnerung: Service-URLs haben die Form [/action/param1/param2/…] und diese URL befindet sich in [request.path];
  • Zeilen 17–23: Die Aktion [init-session] wird verwendet, um den vom Client angeforderten Antworttyp (json, xml, html) zu initialisieren. Diese Information wird in der Sitzung unter dem Schlüssel [responseType] gespeichert. Wenn die Aktion also nicht [init-session] ist, muss die Sitzung den Schlüssel [responseType] enthalten; andernfalls ist die Anfrage ungültig;
  • Zeilen 21–22: Die Struktur des von jedem Controller zurückgegebenen Ergebnisses, in diesem Fall ein Fehlerergebnis:
    • [action]: ist der Name der aktuellen Aktion. Dies ermöglicht es uns, den Namen bei der Protokollierung des Anforderungsergebnisses abzurufen;
    • [status]: ist ein dreistelliger Statuscode:
        • [x00] für einen Erfolg;
        • [x01] für einen Fehler;
  • [response]: ist die Antwort auf die Anfrage. Ihre Art ist spezifisch für jede Anfrage;
  • Zeilen 24–30: Die Aktion [authenticate-user] dient zur Authentifizierung des Benutzers. Bei Erfolg wird der Schlüssel [user=True] zur Sitzung des Benutzers hinzugefügt. Bestimmte Service-URLs sind nur für authentifizierte Benutzer zugänglich. Dies wird hier überprüft;
  • Zeile 26: Nur die Aktionen [init-session] und [authenticate-user] können von einem noch nicht authentifizierten Benutzer ausgeführt werden;
  • Zeilen 28–29: Die im Fehlerfall zu sendende Antwort;
  • Zeilen 32–34: Wenn einer der beiden vorherigen Fehler aufgetreten ist, wird die Fehlerantwort mit dem HTTP-Status 400 BAD REQUEST an den Client gesendet;
  • Zeilen 35–39: Wenn kein Fehler aufgetreten ist, wird die Steuerung an den Controller übergeben, der für die Bearbeitung der aktuellen Aktion zuständig ist. Seine Instanz ist in der Anwendungskonfiguration zu finden;

Die Klasse [MainController] setzt die Arbeit der Funktion [front_controller] fort: Gemeinsam bearbeiten sie alles, was aus der Anforderungsverarbeitung herausgelöst werden kann, und warten bis zum letzten Moment, um die Anforderung an einen bestimmten Controller weiterzuleiten. Die Aufteilung des Codes zwischen der Funktion [front_controller] und der Klasse [MainController] ist rein subjektiv. Hier wollte ich die Struktur der vorherigen Version beibehalten: Die Funktion [front_controller] existierte bereits unter dem Namen [main]. In der Praxis könnte man:

  • alles in die Funktion [front_controller] packen und die Klasse [MainController] entfernen;
  • alles in die Klasse [MainController] packen und die Funktion [front_controller] entfernen. Ich würde eher diese Lösung wählen, da sie den Vorteil hat, den Code des Hauptskripts [main] zu straffen;

30.7. Aktionsspezifische Verarbeitung

Kehren wir zur MVC-Architektur der Anwendung zurück:

Image

Wir befinden uns immer noch bei Schritt 1 oben. Wenn keine Fehler aufgetreten sind, beginnt Schritt 2. Die Anfrage wurde an den Controller weitergeleitet, der für die in der Anfrage angeforderte Aktion zuständig ist. Nehmen wir an, diese Aktion ist [/init-session], definiert durch die Route:

1
2
3
4
5
#  init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

Diese Aktion ist in der Konfiguration [config] mit einem Controller verknüpft:


        # actions autorisées et leurs contrôleurs
        "controllers": {
            # initialisation d'une session de calcul
            "init-session": InitSessionController(),
            
        },

Dann übernimmt der [InitSessionController] (Zeile 4). Sein Code lautet wie folgt:

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class InitSessionController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action, type_response = request.path.split('/')

        #  initially no error
        erreur = False
        #  check response type
        if type_response not in config['responses'].keys():
            erreur = True
            résultat = {"action": action, "état": 701,
                        "réponse": [f"paramètre [type={type_response}] invalide"]}
        #  if no error
        if not erreur:
            #  set the session type in the flask session
            session['typeResponse'] = type_response
            résultat = {"action": action, "état": 700,
                        "réponse": [f"session démarrée avec le type de réponse {type_response}"]}
            return résultat, status.HTTP_200_OK
        else:
            return résultat, status.HTTP_400_BAD_REQUEST
  • Zeile 6: Wie die anderen Controller implementiert auch der [InitSessionController] die Schnittstelle [InterfaceController];
  • Zeile 10: Die URL hat den Typ [/init-session/type_response]. Wir rufen die Aktion [init-session] und den gewünschten Antworttyp ab;
  • Zeile 15: Der gewünschte Antworttyp kann nur einer der in der Antwortkonfiguration vorhandenen sein:

        # les différents types de réponse (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },
  • ist dies nicht der Fall, wird eine Fehlerantwort 701 vorbereitet (Zeile 17);
  • Zeilen 20–25: Fall, in dem der gewünschte Antworttyp gültig ist;
  • Zeile 22: Der gewünschte Antworttyp wird in der Sitzung gespeichert. Dies geschieht, da wir ihn für nachfolgende Anfragen benötigen;
  • Zeilen 23–24: Eine 700-Erfolgsantwort vorbereiten;
  • Zeile 25: Die Erfolgsmeldung wird an den Aufrufer zurückgegeben;
  • Zeile 27: Wenn ein Fehler aufgetreten ist, wird die Fehlerantwort an den Aufrufer zurückgegeben;

30.8. Generieren der HTTP-Antwort des Servers

Kehren wir zur MVC-Architektur der Anwendung zurück:

Image

Wir haben gerade die Schritte 1 und 2 behandelt. Dabei sind wir auf drei Statuscodes gestoßen:

  • 700: /init-session erfolgreich;
  • 701: /init-session fehlgeschlagen;
  • 101: ungültige Anfrage, entweder weil die Sitzung nicht initialisiert wurde oder weil der Benutzer nicht authentifiziert ist;

Schauen wir uns an, wie die Antwort des Servers im obigen Schritt 3 an den Client gesendet wird. Dies geschieht in der Funktion [front_controller] des Skripts [main]:

#  the front controller
def front_controller() -> tuple:
    #  the request is processed
    logger = None
    try:
        #  logger
        logger = Logger(config["logsFilename"])
        #  we store it in a config associated with the thread
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        #  log the request
        logger.write(f"[ front_controller] requête : {request}\n")
        #  the thread is interrupted if requested
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            #  pause is randomized so that some threads are interrupted and others not
            aléa = randint(0, 1)
            if aléa == 1:
                #  log before break
                logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                #  break
                time.sleep(sleep_time)
        #  forward the request to the main controller
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        #  we log the result sent to the customer
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        #  was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            #  send an e-mail to the application administrator
            send_adminmail(config, log)
        #  determine the desired type of response
        if session.get('typeResponse') is None:
            #  the session type has not yet been set - it will be jSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        #  build the response to be sent
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        #  we send the answer
        return response, status_code
    except BaseException as erreur:
        #  it's an unexpected error - log the error if possible
        if logger:
            logger.write(f"[ front_controller] {erreur}")
        #  we prepare the response to the customer
        résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
        #  we send a response in jSON
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        #  close the log file if it has been opened
        if logger:
            logger.close()
  • Wir befinden uns nun in Zeile 26: Der Hauptcontroller hat seine Fehlerantwort zurückgegeben;
  • Zeilen 27–29: Unabhängig von der Antwort des Hauptcontrollers (erfolgreich oder fehlgeschlagen) wird diese Antwort in der Protokolldatei protokolliert;
  • Zeilen 30–33: Wie in früheren Versionen senden wir, wenn der HTTP-Status [500 INTERNAL SERVER ERROR] lautet, eine E-Mail mit dem Fehlerprotokoll an den Anwendungsadministrator;
  • Zeilen 34–39: Wir senden die HTTP-Antwort, und das vom Controller zurückgegebene Ergebnis wird in den Hauptteil dieser Antwort eingefügt. Wir müssen wissen, in welchem Format (JSON, XML, HTML) der Client diese Antwort wünscht. Wir suchen in der Sitzung nach dem gewünschten Antworttyp. Ist dieser nicht vorhanden, setzen wir diesen Typ willkürlich auf JSON;
  • Zeilen 40–43: Die HTTP-Antwort wird erstellt;

In der Konfigurationsdatei wurde jeder Antworttyp (json, xml, html) einer Klasseninstanz zugeordnet:


        # les différents types de réponse (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

Die Antwortklassen befinden sich im Ordner [responses] der Server-Verzeichnisstruktur:

Image

Jede Antwortklasse implementiert die folgende [InterfaceResponse]-Schnittstelle:


from abc import ABC, abstractmethod
 
from flask.wrappers import Response
from werkzeug.local import LocalProxy
 
class InterfaceResponse(ABC):
 
    @abstractmethod
    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        pass
  • Zeilen 8–11: Die Schnittstelle [InterfaceResponse] definiert eine einzige Methode [build_http_response] mit den folgenden Parametern:
    • [request, session, config]: Dies sind die vom Aktionscontroller empfangenen Parameter;
    • [result, status_code]: Dies sind die vom Aktionscontroller erzeugten Ergebnisse;

Wir stellen nun die JSON-Antwort vor. Sie wird von der folgenden Klasse [JsonResponse] generiert:

import json

from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy

from InterfaceResponse import InterfaceResponse

class JsonResponse(InterfaceResponse):

    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        #  results: the results dictionary
        #  status_code: status code of the HTTP response

        #  we return the answer HTTP
        response = make_response(json.dumps(résultat, ensure_ascii=False))
        response.headers['Content-Type'] = 'application/json; charset=utf-8'
        return response, status_code

Dieser Code ist uns bekannt, da wir ihm schon oft begegnet sind. Es handelt sich um den Code für die Funktion [json_response] im Modul [myutils].

30.9. Erste Tests

In dem von uns untersuchten Code sind wir auf drei Statuscodes gestoßen:

  • 700: /init-session erfolgreich;
  • 701: /init-session fehlgeschlagen;
  • 101: Ungültige Anfrage, entweder weil die Sitzung nicht initialisiert wurde oder weil der Benutzer nicht authentifiziert ist;

Wir werden versuchen, diese mit einer JSON-Sitzung auszulösen.

  • Wir starten den Webserver, das DBMS und den Mailserver;
  • Wir starten einen Postman-Client;

Test 1

Zunächst demonstrieren wir eine ungültige Anfrage, da die Sitzung noch nicht initialisiert wurde:

Image

#  authenticate-user
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

Dies wird jedoch nur akzeptiert, wenn die Sitzung zuvor mit der Aktion [/init-session] initialisiert wurde.

Führen wir die Anfrage aus und sehen wir uns das vom Server gesendete Ergebnis an:

Image

  • [1-2]: Wir haben eine JSON-Antwort erhalten. Wenn der Antworttyp vom Client noch nicht angegeben wurde, verwendet der Server JSON für die Antwort;
  • [3-5]: das JSON-Dictionary der Antwort;
    • [action]: die ausgeführte Aktion;
    • [status]: der Antwortstatuscode. Ein Code [x01] weist auf einen Fehler hin;
    • [response]: ist auf jede Aktion zugeschnitten. Hier enthält er eine Fehlermeldung;

Initialisieren wir nun eine Sitzung mit einem falschen Antworttyp:

Image

  • [1-2] ist eine gültige Route:
#  init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

Dadurch gelangt die Anfrage in die Verarbeitungs-Pipeline des MVC-Servers. Sie sollte jedoch während dieser Verarbeitung abgelehnt werden, da der angeforderte Sitzungstyp falsch ist.

Die Antwort lautet wie folgt:

Image

  • in [4] ein Fehlercode [x01];
  • in [5] die Fehlerbeschreibung;

Nun initialisieren wir eine JSON-Sitzung:

Image

Die Antwort lautet wie folgt:

Image

Initialisieren wir nun eine XML-Sitzung. Die JSON-Antwort wird durch eine XML-Antwort ersetzt, die von der folgenden [XmlResponse]-Klasse generiert wird:

import xmltodict
from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy

from InterfaceResponse import InterfaceResponse
from Logger import Logger

class XmlResponse(InterfaceResponse):

    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        #  results: the results dictionary
        #  status_code: status code of the HTTP response

        #  result: the dictionary to be transformed into the XML string
        xml_string = xmltodict.unparse({"root": résultat})
        #  we return the answer HTTP
        response = make_response(xml_string)
        response.headers['Content-Type'] = 'application/xml; charset=utf-8'
        return response, status_code

Das ist uns bekannter Code – er stammt aus der Funktion [xml_response] im gemeinsam genutzten Modul [myutils].

Wir initialisieren eine XML-Sitzung:

Image

Die Antwort des Servers lautet dann wie folgt:

Image

Wir erhalten dieselbe Antwort wie in JSON, doch diesmal ist die Antwort als XML formatiert.

30.10. Die Aktion [authenticate-user]

Die Aktion [authenticate-user] ermöglicht es Ihnen, einen Benutzer zu authentifizieren, der die Steuerberechnungsanwendung nutzen möchte. Ihre Route ist im Skript [main] wie folgt definiert:

1
2
3
4
5
#  authenticate-user
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

Der Server erwartet zwei POST-Parameter:

  • [user]: die ID des Benutzers;
  • [password]: das Passwort des Benutzers;

Die Liste der autorisierten Benutzer ist in der Konfiguration [config] definiert:


        # utilisateurs autorisés à utiliser l'application
        "users"[
            {
                "login""admin",
                "password""admin"
            }
        ],

Hier haben wir eine Liste mit einem einzigen Element.

Die Aktion [authenticate-user] wird vom folgenden Controller [AuthentifierUtilisateurController] verarbeitet:

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController
from Logger import Logger

class AuthentifierUtilisateurController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  POST parameters
        post_params = request.form
        #  response status code HTTP
        status_code = None
        #  initially no errors
        erreur = False
        erreurs = []
        #  you need a POST with two parameters
        if len(post_params) != 2:
            erreur = True
            status_code = status.HTTP_400_BAD_REQUEST
            erreurs.append("méthode POST requise, paramètre [action] dans l'URL, paramètres postés [user, password]")
        if not erreur:
            #  retrieve POST parameters
            #  parameter [user]
            user = post_params.get("user")
            if user is None:
                erreur = True
                erreurs.append("paramètre [user] manquant")
            #  parameter [password]
            password = post_params.get("password")
            if password is None:
                erreur = True
                erreurs.append("paramètre [password] manquant")
            #  mistake?
            if erreur:
                status_code = status.HTTP_400_BAD_REQUEST
        #  mistake?
        if not erreur:
            #  check the validity of the (user, password) pair
            users = config['users']
            i = 0
            nbusers = len(users)
            trouvé = False
            while not trouvé and i < nbusers:
                trouvé = user == users[i]["login"] and password == users[i]["password"]
                i += 1
            #  found?
            if not trouvé:
                #  we note the error
                erreur = True
                status_code = status.HTTP_401_UNAUTHORIZED
                erreurs.append(f"Echec de l'authentification")
            else:
                #  note in the session that the user has been found
                session["user"] = True
        #  it's over
        if not erreur:
            #  error-free return
            résultat = {"action": action, "état": 200, "réponse": f"Authentification réussie"}
            return résultat, status.HTTP_200_OK
        else:
            #  return with error
            return {"action": action, "état": 201, "réponse": erreurs}, status_code
  • Zeile 14: Abrufen der POST-Parameter;
  • Zeile 19: die Liste der in der Anfrage gefundenen Fehler;
  • Zeilen 20–24: Wir überprüfen, ob tatsächlich zwei Parameter gesendet wurden;
  • Zeilen 27–31: Prüfen, ob ein [users]-Parameter vorhanden ist;
  • Zeilen 32–36: Prüfen, ob ein [password]-Parameter vorhanden ist;
  • Zeilen 38–39: Wenn die übermittelten Parameter falsch sind, bereite eine HTTP-400-BAD-REQUEST-Antwort vor;
  • Zeilen 40–58: Überprüfen, ob die Anmeldedaten [user, password] zu einem Benutzer gehören, der zur Nutzung der Anwendung berechtigt ist;
  • Zeilen 51–55: Wenn der Benutzer (user, password) nicht zur Nutzung der Anwendung berechtigt ist, bereite eine HTTP-401-UNAUTHORIZED-Antwort vor;
  • Zeilen 56–58: Wenn der Benutzer autorisiert ist, speichern wir in der Sitzung unter dem Schlüssel [user], dass er sich authentifiziert hat;

Beachten Sie: Wenn der Benutzer mit den Anmeldedaten [Anmeldedaten1] authentifiziert wurde und die Authentifizierung mit den Anmeldedaten [Anmeldedaten2] fehlschlägt, bleibt er mit den Anmeldedaten [Anmeldedaten1] authentifiziert.

Führen wir einige Postman-Tests durch:

  • Wir starten den Webserver, das DBMS und den Mailserver;
  • Verwenden des Postman-Clients:
    • starten wir eine JSON-Sitzung;
    • dann authentifizieren;

Hier sind verschiedene Szenarien.

Fall 1: POST ohne übermittelte Parameter

Image

  • In [3-5] hat der POST-Request keinen Body;

Das Ergebnis der Anfrage ist wie folgt:

Image

  • In [2] erhielten wir eine HTTP-400-BAD-REQUEST-Antwort;
  • In [5] erhielten wir den Fehlercode [201];

Fall 2: POST mit falschen Anmeldedaten

Image

  • In [6] sind die Anmeldedaten falsch;

Der Server sendet die folgende Antwort:

Image

  • in [2] die HTTP-Antwort 401 UNAUTHORIZED;
  • In [5] die Fehlerantwort;

Fall 2: POST mit korrekten Anmeldedaten

Image

  • In [6] sind die Anmeldedaten korrekt;

Die Antwort des Servers lautet wie folgt:

  • in [2] eine HTTP-200-OK-Antwort; Image
  • in [5] die Erfolgsmeldung;

30.11. Die Aktion [calculate_tax]

Die Aktion [calculate_tax] berechnet die Steuer eines Steuerzahlers. Ihre Route ist im Skript [main] wie folgt definiert:

1
2
3
4
5
#  calculate-tax
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

Der Server erwartet drei POST-Parameter:

  • [married]: ja / nein;
  • [children]: Anzahl der Kinder des Steuerpflichtigen;
  • [salary]: Jahresgehalt des Steuerpflichtigen;

Der Controller [CalculateTaxController] verarbeitet die Aktion [calculate_tax]:

import re

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController
from TaxPayer import TaxPayer

class CalculerImpotController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  no error at start
        erreur = False
        erreurs = []
        #  POST parameters
        post_params = request.form
        #  you need a POST with three parameters
        if len(post_params) != 3:
            erreur = True
            erreurs.append(
                "méthode POST requise avec les paramètres postés [marié, enfants, salaire]")
        #  analyze posted parameters
        if not erreur:
            #  married parameter
            marié = post_params.get("marié")
            if marié is None:
                erreurs.append("paramètre [marié] manquant")
            else:
                #  is the parameter valid?
                marié = marié.lower()
                if marié != "oui" and marié != "non":
                    erreur = True
                    erreurs.append(f"valeur [{marié}] invalide pour le paramètre [marié (oui/non)]")
            #  children] parameter
            enfants = post_params.get("enfants")
            if enfants is None:
                erreur = True
                erreurs.append("paramètre [enfants] manquant")
            else:
                #  is the parameter valid?
                enfants = enfants.strip()
                match = re.match(r"\d+", enfants)
                if not match:
                    erreur = True
                    erreurs.append(f"valeur [{enfants}] invalide pour le paramètre [enfants (entier>=0)]")
            #  salary parameter
            salaire = post_params.get("salaire")
            if salaire is None:
                erreur = True
                erreurs.append("paramètre [salaire] manquant")
            else:
                #  is the parameter valid?
                salaire = salaire.strip()
                match = re.match(r"\d+", salaire)
                if not match:
                    erreur = True
                    erreurs.append(f"valeur [{salaire}] invalide pour le paramètre [salaire (entier>=0)]")
        #  mistake?
        if erreur:
            status_code = status.HTTP_400_BAD_REQUEST
            résultat = {"action": action, "état": 301, "réponse": erreurs}
            #  we return the result
            return résultat, status_code

        #  tAX CALCULATION
        #  retrieve the [business] layer and the [adminData] dictionary
        métier = config["layers"]["métier"]
        admin_data = config["admindata"]
        #  tAX CALCULATION
        taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
        métier.calculate_tax(taxpayer, admin_data)
        #  simulation no
        id_simulation = session.get('id_simulation', 0)
        id_simulation += 1
        session['id_simulation'] = id_simulation
        #  we put the result in session in the form of a TaxPayer dictionary
        simulation = taxpayer.fromdict({'id': id_simulation}).asdict()
        #  we add the result to the list of simulations already carried out and put it in session
        simulations = session.get("simulations", [])
        simulations.append(simulation)
        session["simulations"] = simulations
        #  result
        résultat = {"action": action, "état": 300, "réponse": simulation}
        status_code = status.HTTP_200_OK

        #  we return the result
        return résultat, status_code
  • Zeile 13: Wir rufen den Namen der aktuellen Aktion ab;
  • Zeile 17: Wir sammeln die Fehler in einer Liste;
  • Zeile 19: Die übermittelten Parameter abrufen. Diese werden im Format [x-www-form-urlencoded] übermittelt, weshalb wir sie aus [request.form] abrufen. Wären sie als JSON übermittelt worden, hätten wir sie aus [request.data] abgerufen;
  • Zeilen 21–24: Wir überprüfen, ob tatsächlich drei übermittelte Parameter vorhanden sind;
  • Zeilen 27–36: Wir prüfen auf das Vorhandensein und die Gültigkeit des übermittelten Parameters [married];
  • Zeilen 37–48: Überprüfung auf das Vorhandensein und die Gültigkeit des übermittelten Parameters [children];
  • Zeilen 49–60: Überprüfung auf Vorhandensein und Gültigkeit des gesendeten Parameters [salary];
  • Zeilen 62–66: Wenn ein Fehler aufgetreten ist, wird eine 400 BAD REQUEST-Fehlerantwort mit dem Statuscode [301] gesendet;
  • Zeilen 69–71: Wenn kein Fehler aufgetreten ist, Vorbereitung der Steuerberechnung. Dazu
    • Zeile 70: Abruf einer Referenz aus der [business]-Schicht;
    • Zeile 71: Abrufen von Daten von der Steuerbehörde in der Serverkonfiguration;
  • Zeilen 72–74: Die Steuer des Steuerpflichtigen wird berechnet;
  • Zeilen 75–77: Wir zählen die Anzahl der vom Benutzer durchgeführten Steuerberechnungen;
    • Zeile 76: Rufen Sie die Nummer der zuletzt durchgeführten Berechnung aus der Sitzung ab. Hier bezeichnen wir das Ergebnis einer Berechnung als [Simulation];
    • Zeile 77: Die Nummer der letzten Simulation wird erhöht;
    • Zeile 78: Diese Nummer wird in der Sitzung gespeichert;
  • Zeilen 79–84: Um die vom Benutzer durchgeführten Berechnungen nachzuverfolgen, speichern wir die Liste der von ihm durchgeführten Simulationen in seiner Sitzung;
  • Zeile 80: Eine Simulation ist das Wörterbuch eines TaxPayer-Objekts, dessen [id]-Eigenschaft den Wert der Simulationsnummer hat;
  • Zeilen 82–84: Die aktuelle Simulation wird zur Liste der Simulationen in der Sitzung hinzugefügt;
  • Zeilen 86–87: Wir bereiten eine erfolgreiche HTTP-Antwort vor;
  • Zeile 90: Wir geben das Ergebnis zurück;

Führen wir einige Tests durch: Der Webserver, das DBMS, der Mailserver und ein Postman-Client werden gestartet.

Fall 1: Durchführung einer Steuerberechnung, während die Sitzung nicht initialisiert ist

Image

Die Antwort lautet wie folgt:

Image

Fall 2: Durchführung einer Steuerberechnung ohne Authentifizierung

Zunächst starten wir eine JSON-Sitzung mit [/init-session/json]. Dann stellen wir dieselbe Anfrage wie zuvor. Die Antwort lautet wie folgt:

Image

Fall 3: Durchführung einer Steuerberechnung mit fehlenden Parametern

Wir initialisieren eine JSON-Sitzung, authentifizieren uns und stellen dann die folgende Anfrage:

Image

  • In [5] fehlt der Parameter [married];

Die Antwort lautet wie folgt:

Fall 4: Steuerberechnung mit falschen Parametern

Image

Image

Die Antwort des Servers lautet wie folgt:

Image

Fall 4: Durchführung einer Steuerberechnung mit korrekten Parametern

Image

Die Antwort des Servers lautet wie folgt:

Image

30.12. Die Aktion [list-simulations]

Die Aktion [list-simulations] ermöglicht es einem Benutzer, die Liste der Simulationen anzuzeigen, die er seit Beginn der Sitzung durchgeführt hat. Ihre Route ist im Skript [main] wie folgt definiert:

1
2
3
4
5
#  lister-simulations
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

Der Server erwartet keine Parameter. Die Aktion [lister-simulations] wird vom folgenden [ListerSimulationsController] verarbeitet:

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class ListerSimulationsController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  retrieve the list of simulations in the session
        simulations = session.get("simulations", [])
        #  we return the result
        return {"action": action, "état": 500,
                "réponse": simulations}, status.HTTP_200_OK
  • Zeile 13: Die Liste der Simulationen wird aus der Sitzung abgerufen;
  • Zeilen 15–16: Eine Erfolgsmeldung wird zurückgegeben;

Führen wir den folgenden Postman-Test aus:

  • Wir starten eine JSON-Sitzung;
  • Wir authentifizieren uns;
  • Führen zwei Steuerberechnungen durch;
  • Wir fordern die Liste der Simulationen an;

Die Anfrage lautet wie folgt:

  • In [3] gibt es keine Parameter; Image

Die Antwort des Servers lautet wie folgt:

Image

  • in [4] die Liste der Simulationen des Benutzers;

30.13. Die Aktion [delete-simulation]

Die Aktion [delete-simulation] ermöglicht es einem Benutzer, eine der Simulationen aus seiner Simulationsliste zu löschen. Ihre Route ist im Skript [main] wie folgt definiert:

1
2
3
4
5
#  delete-simulation
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

Der Server erwartet einen einzigen Parameter: die Nummer der zu löschenden Simulation. Die Aktion [delete-simulation] wird vom folgenden [DeleteSimulationController] verarbeitet:

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class SupprimerSimulationController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action, numéro = request.path.split('/')

        #  parameter [number] is a positive integer or zero according to its route
        numéro = int(numéro)
        #  the simulation id=number must exist in the simulation list
        simulations = session.get("simulations", [])
        liste_simulations = list(filter(lambda simulation: simulation['id'] == numéro, simulations))
        if not liste_simulations:
            msg_erreur = f"la simulation n° [{numéro}] n'existe pas"
            #  we return the error
            return {"action": action, "état": 601, "réponse": [msg_erreur]}, status.HTTP_400_BAD_REQUEST
        #  delete simulation id=number
        simulation = liste_simulations.pop(0)
        simulations.remove(simulation)
        #  put the simulations back in the session
        session["simulations"] = simulations
        #  we return the result
        return {"action": action, "état": 600, "réponse": simulations}, status.HTTP_200_OK
  • Zeile 10: Abrufen der beiden Elemente des Anfragepfads. Sie werden als Zeichenfolgen abgerufen;
  • Zeile 13: Der Parameter [number] wird in eine Ganzzahl umgewandelt. Dass dies möglich ist, wissen wir aufgrund der Signatur der Route,

@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])

Wir wissen auch, dass es sich um eine ganze Zahl >=0 handelt. Tatsächlich kann es keine URL wie [/delete-simulation/-4] geben. Diese wird vom Flask-Server abgelehnt;

  • Zeile 15: Wir rufen die Liste der Simulationen aus der Sitzung ab;
  • Zeile 16: Mit der Funktion [filter] suchen wir nach der Simulation mit id==number. Wir erhalten ein [filter]-Objekt, das wir in eine [list] umwandeln;
  • Zeilen 17–20: Wenn der Filter nichts zurückgibt, existiert die zu löschende Simulation nicht. Wir geben eine Fehlermeldung zurück, die darauf hinweist;
  • Zeilen 21–23: Wir löschen die vom Filter zurückgegebene Simulation;
  • Zeile 25: Wir stellen die neue Liste der Simulationen in der Sitzung wieder her;
  • Zeile 27: Wir geben die neue Liste der Simulationen in der Antwort zurück;

Wir führen einen Erfolgstest und einen Fehlertest durch. Wir führen Simulationen durch und fordern anschließend die Liste der Simulationen an:

Image

  • Die Simulationen hier haben die Nummern 2 und 3;

Wir fordern an, dass die Simulation mit der Nummer 3 entfernt wird.

Image

Die Antwort lautet wie folgt:

Wiederholen wir nun denselben Vorgang (Löschen der Simulation mit der ID 3). Die Antwort lautet dann wie folgt:

Image

Image

30.14. Die Aktion [end-session]

Die Aktion [end-session] ermöglicht es einem Benutzer, seine Simulationssitzung zu beenden. Ihr Pfad ist im Skript [main] wie folgt definiert:

1
2
3
4
5
#  end of session
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

Der Server erwartet keine Parameter. Die Aktion wird vom folgenden [FinSessionController] verarbeitet:

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class FinSessionController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  delete all keys in the current session
        session.clear()
        #  we return the result
        return {"action": action, "état": 400, "réponse": "session réinitialisée"}, status.HTTP_200_OK
  • Zeile 13: Alle Schlüssel aus der Sitzung löschen. Dies löscht:
    • [typeResponse]: den Typ der HTTP-Antworten (json, xml, html);
    • [simulation_id]: die ID der zuletzt durchgeführten Simulation;
    • [simulations]: die Liste der Simulationen des Benutzers;
    • [user]: die Kennung, dass der Benutzer authentifiziert wurde;
  • die Antwort zurückgeben;

Man könnte sich fragen, wie die HTTP-Antwort aus Zeile 15 zurückgegeben wird, da der Antworttyp nun nicht mehr in der Sitzung vorhanden ist. Um dies herauszufinden, müssen wir zur Funktion |front_controller| im Hauptskript [main] zurückkehren und sie wie folgt ändern:


…        
         # on not# note the type of response required if this information is in the session
        type_response1 = session.get('typeResponse'None)
        # forward the request to the main controller
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        # we log the result sent to the customer
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        # was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # send an e-mail to the application administrator
            send_adminmail(config, log)
        # determine the desired type of response
        type_response2=session.get('typeResponse')
        if  type_response2 is None and type_response1 is None:
            # the session type has not yet been set - it will be jSON
            type_response = 'json'
        elif type_response2 is not None:
            # the type of response is known and in the session
            type_response = type_response2
        else:
            type_response=type_response1
        # build the response to be sent
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        # we send the answer
        return response, status_code
  • Zeile 3: Der Typ der aktuell in der Sitzung befindlichen Antwort wird gespeichert;
  • Zeile 6: Die Aktion wird ausgeführt. Wenn es sich um
    • [end-session], ist der Schlüssel [typeResponse] nicht mehr in der Sitzung vorhanden;
    • [init-session], hat der Schlüssel [typeResponse] in der Sitzung möglicherweise seinen Wert geändert;
  • Zeilen 14–20: Die HTTP-Antwort muss gesendet werden. Wir müssen wissen, in welcher Form:
    • Zeilen 16–18: Wenn der Antworttyp weder durch [type_response1] in Zeile 3 noch durch [type_response2] in Zeile 15 definiert ist, wurde der Antworttyp weder vor noch nach der Aktion definiert. Wir verwenden dann JSON (Zeile 18);
    • Zeilen 19–21: Wenn [type_response2] vorhanden ist – der Antworttyp in der Sitzung nach der Aktion –, dann ist dies der zu verwendende Typ;
    • Zeilen 22–23: Andernfalls ist [type_response1], der Antworttyp vor der Aktion (der [end-session] sein muss), der zu verwendende Typ;

30.15. Die Aktion [get-admindata]

Wir werden nun die beiden URLs besprechen, die für die JSON- und XML-Dienste reserviert sind:

Aktion
Rolle
Ausführungskontext
/get-admindata
Gibt die Steuerdaten zurück, die zur Berechnung der Steuer verwendet werden
.
Wird nur verwendet, wenn der Sitzungstyp „json“ oder „xml“ ist. Der Benutzer muss authentifiziert sein
/calculate-taxes
Berechnet die Steuer für eine Liste von Steuerzahlern, die im JSON-Format übermittelt wurde
GET-Anfrage.
Wird nur verwendet, wenn der Sitzungstyp „json“ oder „xml“ ist. Der Benutzer muss authentifiziert sein

Die URL [/get-admindata] ist in den Routen des Hauptskripts [main] wie folgt definiert:

1
2
3
4
5
#  get-admindata
@app.route('/get-admindata', methods=['GET'])
def get_admindata() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

Die Route [/get-admindata] wird vom folgenden [GetAdminDataController] verarbeitet:

#  import dependencies

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class GetAdminDataController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action = request.path.split('/')
        #  only json and xml sessions are accepted
        type_response = session.get('typeResponse')
        if type_response != 'json' and type_response != 'xml':
            #  an error response is returned
            return {
                       "action": action,
                       "état": 1001,
                       "réponse": ["cette action n'est possible que pour les sessions json ou xml"]
                   }, status.HTTP_400_BAD_REQUEST
        else:
            #  a success answer is returned
            return {"action": action, "état": 1000, "réponse": config["adminData"].asdict()}, status.HTTP_200_OK
  • Zeilen 13–21: Wir prüfen, ob wir uns in einer JSON- oder XML-Sitzung befinden;
  • Zeile 24: Das Datenwörterbuch der Steuerverwaltung, das beim Start des Servers in die Konfiguration aufgenommen wurde, wird zurückgegeben:

    # admindata sera une donnée de portée application en lecture seule
    config["admindata"] = config["layers"]["dao"].get_admindata()

Verwenden wir einen Postman-Client und senden wir eine Anfrage an die URL [/get-admindata], nachdem wir eine JSON-Sitzung gestartet und uns authentifiziert haben:

Image

Die Serverantwort lautet wie folgt:

Image

30.16. Die Aktion [calculate-taxes]

Die Aktion [calculate-taxes] berechnet die Steuern für eine Liste von Steuerzahlern, die im Request-Body als JSON-String enthalten ist. Diese Aktion ist uns bereits bekannt: In der vorherigen Version hieß sie [calculate_tax_in_bulk_mode].

Ihre Route lautet wie folgt:

1
2
3
4
5
#  batch tax calculation
@app.route('/calculer-impots', methods=['POST'])
def calculer_impots():
    #  execute the controller associated with the action
    return front_controller()

Diese Aktion wird vom folgenden [CalculateTaxesController] verarbeitet:

import json

from flask_api import status
from werkzeug.local import LocalProxy

from ImpôtsError import ImpôtsError
from InterfaceController import InterfaceController
from TaxPayer import TaxPayer

class CalculerImpotsController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  only json and xml sessions are accepted
        type_response = session.get('typeResponse')
        if type_response != 'json' and type_response != 'xml':
            #  an error response is returned
            return {
                       "action": action,
                       "état": 1501,
                       "réponse": ["cette action n'est possible que pour les sessions json ou xml"]
                   }, status.HTTP_400_BAD_REQUEST

        #  retrieve the body of the post - wait for a list of dictionaries
        msg_erreur = None
        list_dict_taxpayers = None
        #  the jSON body of POST
        request_text = request.data
        try:
            #  which we transform into a list of dictionaries
            list_dict_taxpayers = json.loads(request_text)
        except BaseException as erreur:
            #  we note the error
            msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
        #  do we have a non-empty list?
        if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
            #  we note the error
            msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
        #  do we have a list of dictionaries?
        if not msg_erreur:
            erreur = False
            i = 0
            while not erreur and i < len(list_dict_taxpayers):
                erreur = not isinstance(list_dict_taxpayers[i], dict)
                i += 1
            #  mistake?
            if erreur:
                msg_erreur = "le corps du POST doit être une liste de dictionnaires"
        #  mistake?
        if msg_erreur:
            #  an error response is sent to the client
            résultats = {"action": action, "état": 1501, "réponse": [msg_erreur]}
            return résultats, status.HTTP_400_BAD_REQUEST

        #  check TaxPayers one by one
        #  initially no errors
        list_erreurs = []
        for dict_taxpayer in list_dict_taxpayers:
            #  we create a TaxPayer from dict_taxpayer
            msg_erreur = None
            try:
                #  the following operation will eliminate cases where the parameters are not
                #  properties of the TaxPayer class as well as the cases where their values
                #  are incorrect
                TaxPayer().fromdict(dict_taxpayer)
            except BaseException as erreur:
                msg_erreur = f"{erreur}"
            #  certain keys must be present in the dictionary
            if not msg_erreur:
                #  the keys [married, children, salary] must be present in the dictionary
                keys = dict_taxpayer.keys()
                if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
                    msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
            #  mistakes?
            if msg_erreur:
                #  we note the error in the TaxPayer itself
                dict_taxpayer['erreur'] = msg_erreur
                #  add the TaxPayer to the error list
                list_erreurs.append(dict_taxpayer)

        #  we've processed all the taxpayers - are there any mistakes?
        if list_erreurs:
            #  an error response is sent to the client
            résultats = {"action": action, "état": 1501, "réponse": list_erreurs}
            return résultats, status.HTTP_400_BAD_REQUEST

        #  no mistakes, we can work
        #  data recovery from tax authorities
        admindata = config["admindata"]
        métier = config["layers"]["métier"]
        try:
            #  process the TaxPayer one by one
            list_taxpayers = []
            for dict_taxpayer in list_dict_taxpayers:
                #  tAX CALCULATION
                taxpayer = TaxPayer().fromdict(
                    {'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
                     'salaire': dict_taxpayer['salaire']})
                métier.calculate_tax(taxpayer, admindata)
                #  the result is stored as a dictionary
                list_taxpayers.append(taxpayer.asdict())
            #  we add list_taxpayers to the current simulations, giving each simulation a number
            simulations = session.get("simulations", [])
            id_simulation = session.get("id_simulation", 0)
            for simulation in list_taxpayers:
                #  each simulation is given a number
                id_simulation += 1
                simulation['id'] = id_simulation
                #  we add it to the current list of simulations
                simulations.append(simulation)
            #  we put everything back in session
            session["simulations"] = simulations
            session["id_simulation"] = id_simulation
            #  we send the response to the client
            return {"action": action, "état": 1500, "réponse": list_taxpayers}, status.HTTP_200_OK
        except ImpôtsError as erreur:
            #  an error response is sent to the client
            return {"action": action, "état": 1501, "réponse": [f"{erreur}"]}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • Zeilen 16–24: Wir überprüfen, ob wir uns tatsächlich in einer JSON- oder XML-Sitzung befinden
  • Zeilen 26–120: Dieser Code ist uns im Allgemeinen bekannt. Er stammt aus der Funktion |index_controller| in Version 10 der Anwendung, die an die Spezifikationen der implementierten Schnittstelle [InterfaceController] angepasst wurde;
  • Zeilen 104–115: Code, der hinzugefügt wurde, um der neuen Umgebung dieses Controllers Rechnung zu tragen. Wir haben soeben Steuerberechnungen durchgeführt. Wir müssen die Ergebnisse in der Liste der Simulationen speichern, die in der Sitzung verwaltet wird;
  • Zeile 105: Wir rufen die Liste der Simulationen in der Sitzung ab;
  • Zeile 106: Wir rufen die Nummer der zuletzt durchgeführten Simulation ab;
  • Zeilen 107–112: Wir durchlaufen die Liste der Wörterbücher, die die Ergebnisse der Steuerberechnung enthalten; wir weisen jedem eine Simulations-ID zu, und jedes Wörterbuch wird der Liste der Simulationen hinzugefügt;
  • Zeilen 113–115: Die neue Liste der Simulationen und die Nummer der zuletzt durchgeführten Simulation werden an die Sitzung zurückgegeben;

Nach der Initialisierung einer JSON-Sitzung und der Authentifizierung führen wir den folgenden Postman-Test durch:

Image

Image

Die Serverantwort lautet wie folgt:

Image

Wenn wir nun die Liste der Simulationen abfragen:

Beachten Sie, dass in der Ergebnisliste für [/calcul-impots] Steuerzahler kein [id]-Attribut haben, während in der Liste der Simulationen jede Simulation eine Nummer hat, die sie identifiziert.

Image