Skip to content

22. Webdienste mit dem Flask-Framework

Unter einem Webdienst versteht man hier jede Webanwendung, die Rohdaten bereitstellt, die von einem Client genutzt werden – in den folgenden Beispielen oft ein Konsolenskript. Wir befassen uns hier nicht mit einer bestimmten Technologie, wie beispielsweise REST (REpresentational State Transfer) oder SOAP (Simple Object Access Protocol), die mehr oder weniger Rohdaten in einem genau definierten Format bereitstellen. REST liefert jSON, während es bei SOAP XML ist. Jede dieser Technologien beschreibt genau, wie der Client den Server abfragen muss und wie die Antwort des Servers aussehen muss. In diesem Kurs werden wir hinsichtlich der Art der Client-Anfrage und der Server-Antwort wesentlich flexibler vorgehen. Die geschriebenen Skripte und die verwendeten Tools ähneln jedoch denen der Technologie REST.

22.1. Einführung

Python-Skripte können von einem Webserver ausgeführt werden. Ein solches Skript wird zu einem Serverprogramm, das mehrere Clients bedienen kann. Aus Sicht des Clients entspricht der Aufruf eines Webdienstes der Abfrage des URL dieses Dienstes. Der Client kann in jeder beliebigen Sprache geschrieben sein, insbesondere in Python. Im letzteren Fall werden dann die Internetfunktionen verwendet, die wir gerade kennengelernt haben. Außerdem müssen wir wissen, wie man mit einem Webdienst „kommuniziert“, d. h. das Kommunikationsprotokoll HTTP zwischen einem Webserver und seinen Clients verstehen. Das war das Ziel des Abschnitts |das HTTP-Protokoll|. Die in diesem Teil des Kurses beschriebenen Web-Clients haben es uns ermöglicht, einen Teil des HTTP-Protokolls kennenzulernen.

Image

In ihrer einfachsten Form verläuft der Austausch zwischen Client und Server wie folgt:

  • Der Client baut eine Verbindung zum Port 80 des Webservers auf;
  • er stellt eine Anfrage nach einem Dokument;
  • der Webserver sendet das angeforderte Dokument und schließt die Verbindung;
  • der Client schließt seinerseits die Verbindung;

Das Dokument kann unterschiedlicher Art sein: ein Text im Format HTML, ein Bild, ein Video, … Es kann sich um ein bereits vorhandenes Dokument (statisches Dokument) oder um ein Dokument handeln, das von einem Skript dynamisch generiert wird (dynamisches Dokument). Im letzteren Fall spricht man von Webprogrammierung. Das Skript zur dynamischen Dokumentgenerierung kann in verschiedenen Sprachen geschrieben sein: PHP, Python, Perl, Java, Ruby, C#, VB.net, …

Im Folgenden werden wir Python-Skripte verwenden, um Textdokumente dynamisch zu generieren.

Image

  • In [1] stellt der Client eine Verbindung zum Server her, fordert ein Python-Skript an und sendet gegebenenfalls Parameter an dieses Skript;
  • in [3] lässt der Webserver das Python-Skript vom Python-Interpreter ausführen. Das Skript generiert ein Dokument, das an den Client gesendet wird ([2]);
  • Der Server beendet die Verbindung. Der Client tut dasselbe;

Der Webserver kann mehrere Clients gleichzeitig bedienen.

Im weiteren Verlauf werden wir zwei Webserver verwenden:

  • den leichtgewichtigen Werkzeug-Server [https://werkzeug.palletsprojects.com/en/1.0.x/]. Dieser Server wird vom Web-Framework Flask [https://flask.palletsprojects.com/en/1.1.x/] verwendet. Wir werden ihn im Folgenden meist als Flask-Server bezeichnen;
  • den Apache-2-Server [https://httpd.apache.org/];

Der Flask-Server wird in allen Beispielen verwendet. Der Apache-Server dient zum Hosten der Webanwendung, die wir entwickeln werden.

Das Flask-Framework wurde in Python entwickelt. Es handelt sich um ein Modul, das man in einem Terminal installiert: PyCharm:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>pip install flask
Collecting flask
  Downloading Flask-1.1.2-py2.py3-none-any.whl (94 kB)
     || 94 kB 1.1 MB/s
Collecting click>=5.1
  Downloading click-7.1.2-py2.py3-none-any.whl (82 kB)
     || 82 kB 5.8 MB/s
Collecting itsdangerous>=0.24
  Downloading itsdangerous-1.1.0-py2.py3-none-any.whl (16 kB)
Collecting Jinja2>=2.10.1
  Downloading Jinja2-2.11.2-py2.py3-none-any.whl (125 kB)
     || 125 kB 6.4 MB/s
Collecting Werkzeug>=0.15
  Downloading Werkzeug-1.0.1-py2.py3-none-any.whl (298 kB)
     || 298 kB 6.4 MB/s
Collecting MarkupSafe>=0.23
  Downloading MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl (16 kB)
Installing collected packages: click, itsdangerous, MarkupSafe, Jinja2, Werkzeug, flask
Successfully installed Jinja2-2.11.2 MarkupSafe-1.1.1 Werkzeug-1.0.1 click-7.1.2 flask-1.1.2 itsdangerous-1.1.0
  • Zeile 1: der ausgeführte Befehl;
  • Zeile 19: die installierten Komponenten:
    • [flask-1.1.2]: ist ein Webentwicklungs-Framework in Python;
    • [Werkzeug-1.0.1]: ist der Webserver, der die Anfragen der Clients beantwortet;
    • [Jinja2-2.11.2]: ist ein Tool, mit dem dynamische Elemente in Seiten eingefügt werden können, die ansonsten statisch wären;

22.2. Skripte [flask/01]: erste Grundlagen der Webprogrammierung

Image

Unsere Beispiele werden in der folgenden Architektur ausgeführt:

Image

  • In [1] wird ein Python-Skript wie ein herkömmliches Konsolenskript ausgeführt;
  • in [2] wird transparent ein Webserver instanziiert, der auf Anfragen wartet. Tatsächlich akzeptiert er nur eine einzige Anfrage, nämlich URL;
  • Bei [3] fordert der Browser die einzige Anfrage URL vom Server an;
  • In [4] lässt der Server das von der Konsole [1] angegebene Python-Skript ausführen;
  • Bei [5] gibt das Skript seine Ergebnisse an den Webserver zurück, ein Textdokument;
  • in [6] sendet der Webserver dieses Textdokument an den Browser;

22.2.1. Skript [exemple_01]: Grundlagen der Sprache HTML

Ein Webbrowser kann verschiedene Dokumente anzeigen, wobei das häufigste das HTML-Dokument (HyperText Markup Language) ist. Dabei handelt es sich um einen Text, der mit Tags der Form <balise>texte</balise> formatiert ist. So wird der Text „<b>important</b>“ den wichtigen Text in Fettdruck anzeigen. Es gibt auch einzelne Tags, wie beispielsweise das Tag „<hr/>“, das eine horizontale Linie anzeigt. Wir werden hier nicht auf alle Tags eingehen, die in einem HTML-Text vorkommen können. Es gibt zahlreiche WYSIWYG-Programme, mit denen man eine WEB-Seite erstellen kann, ohne eine einzige Zeile HTML-Code schreiben zu müssen. Diese Tools generieren automatisch den Code HTML für ein Layout, das mit der Maus und vordefinierten Steuerelementen erstellt wurde. So kann man (mit der Maus) eine Tabelle in die Seite einfügen und anschließend den von der Software generierten Code HTML einsehen, um die Tags zu ermitteln, die zur Definition einer Tabelle in einer WEB-Seite verwendet werden müssen. Einfacher geht es nicht. Außerdem sind Kenntnisse der Sprache HTML unerlässlich, da dynamische Webanwendungen den an die Web-Clients zu sendenden Code HTML selbst generieren müssen. Dieser Code wird programmgesteuert generiert, und man muss natürlich wissen, was generiert werden muss, damit der Client die gewünschte Webseite erhält.

Zusammenfassend lässt sich sagen, dass man keineswegs die gesamte Sprache HTML beherrschen muss, um mit der Webprogrammierung zu beginnen. Allerdings sind diese Kenntnisse notwendig und können durch die Verwendung von WYSIWYG-Software zur Erstellung von WEB-Seiten wie DreamWeaver und Dutzenden anderer erworben werden. Eine weitere Möglichkeit, die Feinheiten der Sprache HTML zu entdecken, besteht darin, im Internet zu stöbern und den Quellcode von Seiten anzuzeigen, die interessante und Ihnen noch unbekannte Merkmale aufweisen.

Betrachten wir das folgende Beispiel, das einige Elemente zeigt, die in einem Webdokument vorkommen können, wie zum Beispiel:

  • eine Tabelle;
  • ein Bild;
  • einen Link;

Image

Ein HTML-Dokument wird von den Tags <html>…</html> umschlossen. Es besteht aus zwei Teilen:

  • <head>…</head>: Dies ist der nicht sichtbare Teil des Dokuments. Er enthält Informationen für den Browser, der das Dokument anzeigen wird. Oft findet sich hier das Tag <title>…</title>, das den Text festlegt, der in der Titelleiste des Browsers angezeigt wird. Außerdem können hier weitere Tags vorkommen, insbesondere solche, die die Schlüsselwörter des Dokuments definieren – Schlüsselwörter, die anschließend von Suchmaschinen verwendet werden. In diesem Teil können sich auch Skripte befinden, die meist in JavaScript oder VBScript geschrieben sind und vom Browser ausgeführt werden;
  • <body-Attribute>…</body>: Dies ist der Teil, der vom Browser angezeigt wird. Die in diesem Abschnitt enthaltenen Tags HTML geben dem Browser die „gewünschte“ visuelle Darstellung des Dokuments vor. Jeder Browser interpretiert diese Tags auf seine eigene Weise. Zwei Browser können daher ein und dasselbe Webdokument unterschiedlich darstellen. Dies stellt für Webdesigner in der Regel eine Herausforderung dar;

Der Code HTML unseres Beispieldokuments lautet wie folgt:


<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>Quelques balises HTML</title>
</head>

<body style="background-image: url(/static/images/standard.jpg)">
  <h1 style="text-align: left">Quelques balises HTML</h1>
  <hr />

  <table border="1">
    <thead>
      <tr>
        <th>Colonne 1</th>
        <th>Colonne 2</th>
        <th>Colonne 3</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>cellule(1,1)</td>
        <td style="text-align: center;">cellule(1,2)</td>
        <td>cellule(1,3)</td>
      </tr>
      <tr>
        <td>cellule(2,1)</td>
        <td>cellule(2,2)</td>
        <td>cellule(2,3</td>
      </tr>
    </tbody>
  </table>
  <br /><br />
  <table border="0">
    <tr>
      <td>Une image</td>
      <td>
        <img border="0" src="/static/images/cerisier.jpg" />
      </td>
    </tr>
    <tr>
      <td>Le site de Polytech'Angers</td>
      <td><a href="http://www.polytech-angers.fr/fr/index.html">ici</a></td>
    </tr>
  </table>
</body>
</html>
Element
Tags und Beispiele HTML
titre du document
<title>Einige Tags HTML</title> (Zeile 5)
Der Text [Quelques balises HTML] erscheint in der Titelleiste des Browsers, der das Dokument anzeigt
barre horizontale
<hr />: Zeigt eine horizontale Linie an (Zeile 10)
tableau
<Tabellenattribute>….</table>: zum Definieren der Tabelle (Zeilen 12, 32)
<thead>…</thead>: zur Definition der Spaltenüberschriften (Zeilen 13, 19)
<tbody>…</tbody>: zum Definieren des Tabelleninhalts (Zeile 20, 31)
<tr-Attribute>…</tr>: zur Definition einer Zeile (Zeilen 21, 25)
<td Attribute>…</td>: zum Definieren einer Zelle (Zeile 22)
Beispiele:
<table border="1">…</table>: Das Attribut „border“ legt die Dicke des Tabellenrandes fest
<td style="text-align: center;">Zelle(1,2)</td> (Zeile 23): Definiert eine Zelle, deren Inhalt „Zelle(1,2)“ lautet. Dieser Inhalt wird horizontal zentriert (text-align: center).
image
<img border="0" src="/static/images/cerisier.jpg"/> (Zeile 38): Definiert ein Bild ohne Rahmen („border=0“), dessen Quelldatei [/static/images/cerisier.jpg] auf dem Webserver ist (src="/static/images/cerisier.jpg"). Befindet sich dieser Link in einem Webdokument, das mit dem URL [http://server/chemin/balises.html] erstellt wurde, fordert der Browser das URL [http://server/ static/images/cerisier.jpg] an, um das hier referenzierte Bild zu erhalten.
lien
<a href="http://www.polytech-angers.fr/fr/index.html">hier</a> (Zeile 43): bewirkt, dass der Text „ici“ als Link zu „URL http://www.polytech-angers.fr/fr/index.html“ dient.
fond de page
<body style="background-image: url(/static/images/standard.jpg)"> (Zeile 8): Gibt an, dass sich das Bild, das als Seitenhintergrund dienen soll, unter der Adresse URL [/static/images/standard.jpg] auf dem Webserver befindet. In unserem Beispiel fordert der Browser die Dateien URL und [http://server/static/images/standard.jpg] an, um dieses Hintergrundbild abzurufen.

An diesem einfachen Beispiel wird deutlich, dass der Browser drei Anfragen an den Server stellen muss, um das gesamte Dokument aufzubauen:

  • [http://server/chemin/balises.html], um den Quellcode HTML des Dokuments zu erhalten;
  • [http://server/static/images/cerisier.jpg], um das Bild cerisier.jpg abzurufen;
  • [http://server/static/images/standard.jpg], um das Hintergrundbild standard.jpg abzurufen;

Mit dem Skript [exemple_01] können wir die vorherige statische Seite [balises.html] anzeigen:

Image

  • in [1] das Skript [exemple_01], das ausgeführt wird;
  • in [3] das Dokument HTML, das vom Skript angezeigt wird;
  • in [2] die Bilder des Dokuments HTML;

Das Skript [exemple_01] lautet wie folgt:


import os

from flask import Flask, make_response, render_template

# Flask-Anwendung
script_dir = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, template_folder=f"{script_dir}/../templates", static_folder=f"{script_dir}/../static")


# Startseite URL
@app.route('/')
def index():
    # Anzeige der Seite
    return make_response(render_template("balises.html"))


# main
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeile 7: Es wird eine Flask-Anwendung instanziiert. Eine Flask-Anwendung ist eine Webanwendung;
    • Der erste Parameter ist der Name der Anwendung. Man kann einen beliebigen Namen vergeben. Hier wurde das vordefinierte Attribut [__name__] verwendet, dessen Wert [__main__] ist (Zeile 18);
    • Der zweite Parameter ist ein benannter Parameter, d. h., seine Position in der Reihenfolge der Parameter spielt keine Rolle. Der benannte Parameter [template_folder] gibt den Ordner an, in dem sich die statischen Seiten der Webanwendung befinden. Die statischen Seiten werden unverändert an den Browser ausgeliefert. Hier befinden sich die statischen Seiten im Ordner [templates] der Projektstruktur. In Zeile 7 haben wir einen relativen Pfad zum Ordner [script_dir] angegeben, der das ausgeführte Skript [exemple_01] enthält;
    • Der dritte Parameter ist ebenfalls ein benannter Parameter. [static_folder] bezeichnet den Ordner, in dem sich die Ressourcen des Dokuments HTML (Bilder, Videos usw.) befinden. Auch hier haben wir einen relativen Pfad zum Ordner [script_dir] angegeben, der das ausgeführte Skript [exemple_01] enthält;
  • Zeilen 10–14: Hier werden die von der Webanwendung akzeptierten URL definiert. Jedes URL ist mit einer Funktion verknüpft, die ausgeführt wird, wenn das URL von einem Webbrowser angefordert wird;
  • Zeile 11: Das einzige URL der Anwendung ist das URL [/]. Beachten Sie, dass in [@app.route('/')] die in Zeile 7 initialisierte Variable [app] verwendet wird. Die Definition der Routen (die verschiedenen von der Anwendung verwalteten URL) erfolgt daher zwangsläufig nach der Definition der Anwendung [app]. Letzterer Name ist frei wählbar;
  • Zeilen 12–14: Die Funktion, die ausgeführt wird, wenn die URL [/] bei der Webanwendung [exemple_01] angefordert wird;
  • Zeile 12: Die mit einem URL verknüpfte Funktion kann einen beliebigen Namen tragen. Sie kann manchmal Parameter haben, um Elemente aus dem damit verknüpften URL abzurufen. Hier hat sie keine;
  • Zeile 14:
    • Die Funktion [render_template] gibt eine Zeichenkette zurück, die dem durch ihren Parameter erzeugten Textdokument entspricht. Dieser ist hier [balises.html]. Aufgrund des [template_folder] in Zeile 7 wird dieses Dokument im Ordner [f"{script_dir}/../templates"] gesucht. Dort befindet es sich tatsächlich;
    • die Funktion [make_response] generiert eine Antwort HTTP für den Browser, der sie um die URL [/] gebeten hat. Im Abschnitt |Das HTTP-Protokoll| haben wir gesehen, dass eine Antwort HTTP zwei Elemente enthält:
      • Kopfzeilen HTTP;
      • das vom Browser angeforderte Dokument, in diesem Fall ein Dokument HTML;

In Zeile 14 wurden der Funktion [make_response] keine Parameter zur Generierung von HTTP-Header übergeben. Sie generiert daher standardmäßige Header. Wir werden später sehen, wie diese HTTP-Header festgelegt werden können.

  • Wenn der Browser schließlich die URL URL von der Flask-Anwendung anfordert, erhält er die Seite [balises.html];
  • Zeilen 17–20: Diese Zeilen dienen dazu, den Webserver zu starten, der die Webanwendung [exemple_01] ausführt;
    • Zeile 18: Diese Bedingung ist nur dann erfüllt, wenn das Skript „[exemple_01]“ in einer Konsole ausgeführt wird;
    • Zeile 19: Die Anwendung [app] aus Zeile 7 wird konfiguriert:
    • Der Parameter mit dem Namen [ENV="development"] versetzt den Webserver in den Entwicklungsmodus: Sobald der Entwickler ein Element der Anwendung ändert, wird diese neu generiert und an den Webserver übermittelt. Der Entwickler muss keine neue Ausführung anfordern;
    • Der Parameter mit dem Namen [DEBUG=True] ermöglicht es dem Entwickler, Haltepunkte im Anwendungscode zu setzen;
    • Zeile 20: Die Webanwendung wird gestartet: Ein Webserver wird instanziiert und die Webanwendung darauf bereitgestellt, um Anfragen von Web-Clients zu beantworten;

Hier ein Ausführungsbeispiel:

Image

Die folgenden Protokolleinträge erscheinen daraufhin in der Ausführungskonsole:


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/flask/01/main/exemple_01.py
 * Serving Flask app "exemple_01" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 334-263-283
* Running on http://127.0.0.1:5000/ (Drücken Sie CTRL+C, um das Programm zu beenden)
  • Zeile 2: Der Server zeigt das ausgeführte Skript an;
  • Zeile 3: Es befindet sich im Entwicklungsmodus;
  • Zeilen 4–5: Der Server erkennt, dass er im Modus [debug] gestartet wurde. Er startet daraufhin neu (Zeile 5). Der Modus [debug] verlangsamt den Start also etwas;
  • Zeile 8: Der URL, in dem die bereitgestellte Webanwendung [exemple_01] verfügbar ist;

Rufen wir mit einem Webbrowser die Seite URL [http://127.0.0.1:5000/] auf:

Image

Wir erhalten tatsächlich das erwartete Dokument [balises.html].

22.2.2. Skript [exemple_02]: Dynamisches Erstellen eines Dokuments HTML

Image

Das Skript [exemple_02] [1] generiert das folgende Dokument [exemple_02.html] [2]:


<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>{{page.title}}</title>
</head>
<body>
    <b>{{page.contents}}</b>
</body>
</html>

Dieses Dokument ist dynamisch, da sein Inhalt erst in dem Moment vollständig bekannt ist, in dem der Webserver es bereitstellt. In den Zeilen 5 und 8 befinden sich nämlich zwei Elemente, die zum Zeitpunkt der Erstellung der Seite noch nicht bekannt sind. Sie werden erst bekannt, wenn die Seite an einen Client gesendet wird. Dann werden sie durch ihre Werte ersetzt, bei denen es sich um Zeichenketten handelt.

  • Zeilen 5, 8: Die Syntax {{Ausdruck}} ist eine Syntax der Jinja2-Vorlagensprache [https://jinja.palletsprojects.com/en/2.11.x/]. Bevor die Seite an einen Client gesendet wird, werden die dynamischen Elemente der Seite (Zeilen 5 und 8) ausgewertet und durch ihre Werte ersetzt;
  • Zeile 5: Es wurde die Syntax [page.title] verwendet. Man ist also davon ausgegangen, dass bei der Generierung der Seite vor ihrer Übermittlung eine Variable [page] bekannt ist – wir werden sehen, wie das funktioniert. In der Syntax {{Ausdruck}} können beliebige Variablennamen verwendet werden. In den Zeilen 5 und 8 könnten wir somit {{title}} und {{contents}} haben. Man könnte also sagen, dass [title] und [contents] Parameter der Seite sind. Im Folgenden werden wir immer dieselbe Technik anwenden:
    • Der einzige Parameter der Seite ist ein Wörterbuch mit dem Namen [page];
    • die Attribute dieses Wörterbuchs werden auf der Seite verwendet. Hier [page.title] in Zeile 5 und [page.contents] in Zeile 8;

Die Webanwendung [exemple_02.py] sieht wie folgt aus:


from flask import Flask, make_response, render_template

# Flask-Anwendung
script_dir = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, template_folder=f"{script_dir}/../templates", static_folder=f"{script_dir}/../static")


# Startseite URL
@app.route('/')
def index():
    # Seiteninhalt in Form eines Wörterbuchs
    page = {"title": "un titre", "contents": "un contenu"}
    # Anzeige der Seite
    return make_response(render_template("exemple_02.html", page=page))


# main
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Wir haben dies bereits im vorherigen Beispiel in den Zeilen 4–5 und 18–20 erläutert. Wir werden dieses Schema in unseren Beispielen weiterhin verwenden;
  • Zeile 9: Das einzige von der Webanwendung bereitgestellte URL ist das URL /;
  • Zeile 14: Das an die Seite URL / übergebene Dokument ist das Dokument [exemple_02.html], das wir gerade erläutert haben. Wir wissen, dass es einen Parameter enthält, nämlich ein Wörterbuch namens [page];
  • Zeile 12: Wir definieren das Wörterbuch, das als Parameter an die Seite [exemple_02.html] übergeben wird. Es kann einen beliebigen Namen tragen. Es muss jedoch die Attribute [title, contents] aufweisen, die im Dokument HTML verwendet werden;
  • Zeile 14: Die Funktion [render_template] hat die Aufgabe, die Zeichenkette des Dokuments [exemple_02.html] zu rendern. Da es sich hierbei um ein parametrisiertes Dokument handelt, werden der Funktion [render_template] die erwarteten Parameter übergeben. Dies geschieht hier, indem wir dem Parameter mit dem Namen [page] einen Wert zuweisen. Im Vorgang [page=page]:
    • links vom Gleichheitszeichen steht der Parameter [page], der im Dokument [exemple_02.html] verwendet wird;
    • rechts vom Gleichheitszeichen steht der Wert [page], der in Zeile 12 definiert wurde;
    • Allgemein gilt: Wenn ein Dokument HTML die Parameter [param1, param2, …, paramn] enthält, werden deren Werte in der Form [render_template(document, param1=valeur1, param2=valeur2, …] an die Funktion [render_template] übergeben;

Bevor wir [exemple_02] ausführen, müssen wir die Ausführung von [exemple_01] beenden:

Image

Wenn Sie bei der Ausführung von Skript 1 den Eindruck haben, dass Skript 2 ausgeführt wird, liegt das wahrscheinlich daran, dass dieses noch läuft. Um zu einem bekannten Zustand zurückzukehren, können Sie alle laufenden Prozesse in PyCharm (oben rechts im Fenster PyCharm) beenden:

Image

Führen wir das Skript [exemple_02] aus:

Image

Die Konsolenprotokolle lauten dann wie folgt:


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/flask/01/main/exemple_02.py
 * Serving Flask app "exemple_02" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 334-263-283
* Running on http://127.0.0.1:5000/ (Drücken Sie CTRL+C, um das Programm zu beenden)

Zeile 8 gibt den Deployment-Port (5000) der Anwendung [exemple_02] (Zeile 1) auf dem Rechner [localhost] an. Da die vorherigen Zeilen immer gleich sind, werden wir sie nicht erneut anzeigen.

Mit einem Browser rufen wir die Seiten URL und [http://localhost:5000/] auf:

Image

  • Der Ausdruck {{page.title}} ergab [1];
  • der Ausdruck {{page.contents}} ergab [2];

22.2.3. Skript [exemple_03]: Seitenfragmente verwenden

Image

  • In [1] generiert das Skript [exemple_03.py] das dynamische Dokument [exemple_03.html] [2]. Dieses wird aus den Seitenfragmenten [fragment_01.html, fragment_02.html] und [3] zusammengesetzt;

Das Dokument [exemple_03.html] sieht wie folgt aus:


<!DOCTYPE html>
<html lang="fr">
{% include "fragments/fragment_01.html" %}
<body>
{% include "fragments/fragment_02.html" %}
</body>
</html>
  • In den Zeilen 3 und 5 wird die Jinja2-Direktive [include] verwendet, um externe Elemente in das Dokument einzubinden;
  • Die Syntax lautet {% include … %}. Der Parameter der Direktive [include] ist der Pfad des einzubindenden Dokuments. Dieser Pfad ist relativ zum Parameter [template_folder] der Flask-Anwendung:

app = Flask(__name__, template_folder="../templates", static_folder="../static")

Die Pfade der Dokumente werden also hier relativ zum Ordner „[templates]“ angegeben.

Das Fragment [fragment_01.html] (die Namen sind natürlich frei wählbar) lautet wie folgt:


<meta charset="UTF-8">
<title>{{page.title}}</title>

Das Fragment [fragment_02.html] lautet wie folgt:


<b>{{page.contents}}</b>

Wenn man das Dokument [exemple_03.html] aus diesen Fragmenten rekonstruiert, erhält man den folgenden Code:


<!DOCTYPE html>
<html lang="fr">
<meta charset="UTF-8">
<title>{{page.title}}</title>
<body>
<b>{{page.contents}}</b>
</body>
</html>

Wir haben also ein Dokument, das mit [exemple_02.html] identisch ist, jedoch aus Fragmenten zusammengesetzt wurde.

Das Webskript [exemple_03.py] lautet wie folgt:


import os

from flask import Flask, make_response, render_template

# Flask-Anwendung
script_dir = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, template_folder=f"{script_dir}/../templates", static_folder=f"{script_dir}/../static")


# Startseite URL
@app.route('/')
def index():
    # Seiteninhalt
    page = {"title": "un autre titre", "contents": "un autre contenu"}
    # Seitenanzeige
    return make_response(render_template("views/exemple_03.html", page=page))


# main
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()

Der Code entspricht dem von [exemple_02.py]. In Zeile 16 wird gezeigt, wie man auf Dokumente verweisen kann, die sich in Unterordnern von [template_folder] aus Zeile 7 befinden.

Die Ausführung des Skripts [exemple_03.py] liefert im Browser folgende Ergebnisse:

Image

22.3. Skripte [flask/02]: Webdienst für Datum und Uhrzeit

Image

Das Dokument [date_time_server.html] lautet wie folgt:


<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Date et heure du moment</title>
</head>
<body>
    <b>Date et heure du moment : {{page.date_heure}}</b>
</body>
</html>
  • Zeile 8: Die Seite akzeptiert den Parameter [page.date_heure];

Der Webdienst [date_time_server.py] lautet wie folgt:


# Importe
import os
import time

from flask import Flask, make_response, render_template

# Flask-Anwendung
script_dir = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, template_folder=f"{script_dir}")


# Startseite URL
@app.route('/')
def index():
    # Zeitangabe an den Kunden
    # time.localtime: Anzahl der Millisekunden seit dem 01.01.1970
    # time.strftime ermöglicht die Formatierung von Uhrzeit und Datum
    # Format für die Anzeige von Datum und Uhrzeit
    # d: zweistelliger Tag
    # m: zweistelliger Monat
    # y: Jahr (2 Ziffern)
    # H: Stunde 0,23
    # M: Minuten
    # S: Sekunden

    # Datum/Uhrzeit des aktuellen Zeitpunkts
    time_of_day = time.strftime('%d/%m/%y %H:%M:%S', time.localtime())
    # Das an den Kunden zu sendende Dokument wird generiert
    page = {"date_heure": time_of_day}
    document = render_template("date_time_server.html", page=page)
    print("document", type(document), document)
    # Antwort HTTP an den Kunden
    response = make_response(document)
    print("response", type(response), response)
    return response


# nur manuell
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeile 13: Die Webanwendung stellt nur den Parameter URL bereit;
  • Zeilen 15–24: Erläutern, wie Datum und Uhrzeit abgerufen und angezeigt werden;
  • Zeile 27: Zeichenkette, die das aktuelle Datum und die aktuelle Uhrzeit darstellt;
  • Zeilen 28–30: Das dynamische Dokument [date_time_server.html] wird generiert, indem ihm das Wörterbuch [page] aus Zeile 29 übergeben wird;
  • Zeile 31: Der Typ von [document] und das Dokument selbst werden angezeigt. Damit soll gezeigt werden, dass es sich um eine Zeichenkette handelt;
  • Zeile 33: Die Antwort HTTP wird generiert, die an den Client gesendet wird (sie wurde noch nicht gesendet);
  • Zeile 34: Der Typ und der Wert werden angezeigt;
  • Zeile 35: Die Antwort „HTTP“ wird an den Client gesendet;

Die Ausführung des Skripts liefert in einem Browser folgendes Ergebnis:

Image

Die Protokolleinträge in der Konsole lauten wie folgt:


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\flask\02\date_time_server.py
 * Serving Flask app "date_time_server" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 334-263-283
* Running on http://127.0.0.1:5000/ (Drücken Sie CTRL+C, um das Programm zu beenden)
127.0.0.1 - - [10/Jul/2020 09:32:09] "GET / HTTP/1.1" 200 -
document <class 'str'> <!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Date et heure du moment</title>
</head>
<body>
    <b>Date et heure du moment : 10/07/20 09:42:33</b>
</body>
</html>
response <class 'flask.wrappers.Response'> <Response 195 bytes [200 OK]>
  • Zeile 10: Man sieht, dass der Typ des von [render_template] zurückgegebenen Werts vom Typ [str] ist. Diese Zeichenkette ist nichts anderes als das Dokument [date_time_server.html] nach seiner Interpretation (Zeilen 10–19);
  • Zeile 20: Man sieht, dass der Typ des von [make_response] zurückgegebenen Werts vom Typ [flask.wrappers.Response] ist. Die Funktion [Response.__str__] wurde implizit aufgerufen, um das Objekt [Response] anzuzeigen. Die von dieser Funktion zurückgegebene Zeichenkette liefert zwei Informationen über die Antwort HTTP, die ausgegeben wird:
    • Das gesendete Dokument ist 195 Byte groß;
    • der Status der Antwort HTTP lautet [200 OK]. Wir werden später sehen, dass wir Zugriff auf diesen Statuscode haben;

22.4. Skripte [flask/03]: Webdienste, die Klartext generieren

In einem früheren Beispiel haben wir gesehen, dass der Webdienst das folgende Dokument lieferte:


<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Date et heure du moment</title>
</head>
<body>
    <b>Date et heure du moment : {{page.date_heure}}</b>
</body>
</html>

Ein Webclient könnte nur an der Information [page.date_heure] in Zeile 8 interessiert sein und nicht an der umgebenden Formatierung HTML. Der Webdienst könnte diese Information als einfache Zeichenkette ausgeben. Wir werden hier Beispiele für diese Art von Webdienst vorstellen.

22.4.1. Skript [main_01]

Image

  • [main_01] ist der Webdienst;
  • [config] ist das Konfigurationsskript der Webanwendung;
  • der Webdienst verwendet einige der in [2] definierten Entitäten;

Das Skript [config] lautet wie folgt:


def configure():
    # Absoluter Pfad als Referenz für die relativen Pfade der Konfiguration
    rootDir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # Abhängigkeiten der Anwendung
    absolute_dependencies = [
        # Personen, Dienstprogramme, MyException
        f"{rootDir}/classes/02/entities",

    ]
    # Der Syspath wird festgelegt
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # Die Konfiguration wird übernommen
    return {}

Die Hauptaufgabe dieser Konfiguration besteht darin, den Python-Pfad des Webdienstes festzulegen. Die Entitäten [2] (Zeile 8) müssen auffindbar sein.

Das Webskript [main_01] lautet wie folgt:


# Die Anwendung wird konfiguriert
import config
config=config.configure()

# Importe
from flask import Flask, make_response
from flask_api import status

# Abhängigkeiten
from Personne import Personne

# Flask-Anwendung (hier keine statischen Dokumente)
app = Flask(__name__)


# Startseite URL
@app.route('/')
def index():
    # eine Person
    personne = Personne().fromdict({"prénom": "Aglaë", "nom": "de la Hûche", "âge": 87})
    # Antwort HTTP
    response = make_response(str(personne))
    # Header HTTP
    response.headers.set("Content-type", "application/json; charser=utf8")
    # die Antwort wird zurückgegeben: HTTP
    return response, status.HTTP_200_OK


# nur Hauptprogramm
if __name__ == '__main__':
    # Der Server wird gestartet
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeilen 1–3: Der Python-Pfad der Anwendung wird festgelegt;
  • Zeilen 5–10: Die vom Skript benötigten Elemente werden importiert;
  • Zeile 17: Der Webdienst stellt ausschließlich URL / bereit;
  • Zeile 20: Es wird ein Objekt [Personne] erstellt;
  • Zeile 22: Es wird eine Antwort HTTP mit der Zeichenkette erstellt, die die Person repräsentiert. Die Funktion [Personne.__str__] wird aufgerufen. Diese gibt die Zeichenkette jSON aus dem Wörterbuch [asdict] der Person zurück (siehe |Klasse BaseEntity|). Der Parameter der Funktion [make_response] ist das an den Client gesendete Textdokument, also hier die Zeichenfolge jSON einer Person;
  • Zeile 24: In die Kopfzeilen HTTP der Antwort wird eine Kopfzeile [Content-type] eingefügt, die dem Kunden angibt, welche Art von Dokument er erhalten wird, in diesem Fall ein Dokument jSON, das in UTF-8 kodiert ist;
  • Zeile 26: Es wird ein Tupel mit zwei Elementen zurückgegeben:
    • die Antwort an den Client, die Header HTTP und das Dokument;
    • den Statuscode der Antwort. Hier soll der Statuscode [200 OK] ausgegeben werden. Die verschiedenen Statuscodes sind durch Konstanten im Modul [flask_api] definiert, das in Zeile 7 importiert wird;

Das Modul [flask_api] ist nicht standardmäßig verfügbar. Es muss installiert werden. Dies erfolgt in einem Terminal PyCharm:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>pip install flask_api
Collecting flask_api
  Downloading Flask_API-2.0-py3-none-any.whl (119 kB)
     || 119 kB 544 kB/s
Requirement already satisfied: Flask>=1.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from flask_api) (1.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>=1.1->flask_api) (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>=1.1->flask_api) (1.0.1)
Requirement already satisfied: click>=5.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask>=1.1->flask_api) (7.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>=1.1->flask_api) (1.1.0)
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>=1.1->flask_api) (1.1.1
)
Installing collected packages: flask-api
Successfully installed flask-api-2.0

Wenn man das Webskript [main_01] ausführt, erhält man in einem Browser folgende Ergebnisse:

Image

  • in [2] die empfangene Zeichenfolge jSON;
  • in [3-4] wird der Inhalt des empfangenen Dokuments angezeigt. Man sieht, dass es kein Layout HTML gibt, sondern nur die Zeichenfolge jSON;

Betrachten wir nun die Rolle des Headers [Content-Type], der vom Webdienst an den Client gesendet wird. Wir versetzen den Browser in den Entwicklermodus (in der Regel F12) und fordern dieselbe URL erneut an. Nachfolgend ein Screenshot eines Chrome-Browsers:

Image

  • in [1] die Registerkarte [Network] auswählen;
  • bei [2, 4]: die vom Browser angeforderte URL;
  • in [3] die Registerkarte [Headers] auswählen (Header HTTP);
  • in [5] den Statuscode der empfangenen Antwort HTTP;
  • in [6] den Header, der dem Client mitteilt, dass er einen Text jSON erhalten wird. Dadurch kann sich der Client auf die Antwort einstellen. Daher ist die von Chrome zur Anzeige einer Antwort mit dem Statuscode jSON verwendete Schriftart nicht dieselbe wie bei einer einfachen Textantwort;

Image

  • Bei [8] wählt man die Registerkarte [Response] aus, um Zugriff auf das vom Webdienst gesendete Dokument zu erhalten, in diesem Fall eine einfache Zeichenfolge jSON;

22.4.2. Postman

[Postman] ist das Tool, mit dem wir die verschiedenen URL einer Webanwendung abfragen können. Es ermöglicht uns:

  • beliebige URL zu verwenden: Diese werden manuell erstellt;
  • den Webserver über ein GET, POST, PUT, OPTIONS… abzufragen;
  • die Parameter von GET oder POST anzugeben;
  • die Kopfzeilen HTTP der Anfrage festzulegen;
  • eine Antwort im Format jSON, XML, HTML zu erhalten,
  • Zugriff auf die Header HTTP der Antwort zu erhalten. Auf diese Weise erhält man somit Zugriff auf die vollständige Antwort HTTP des Servers;

[Postman] ist ein hervorragendes Lehrmittel, um die Client-Server-Kommunikation des Protokolls HTTP zu verstehen.

[Postman] ist unter URL [https://www.getpostman.com/downloads/] verfügbar. Führen Sie die Installation Ihrer Version von [Postman] durch. Während der Installation werden Sie aufgefordert, ein Konto zu erstellen: Dieses wird hier nicht benötigt. Das Konto [Postman] dient dazu, verschiedene Geräte zu synchronisieren, damit die Konfiguration eines Geräts auf ein anderes übertragen wird. All dies ist hier nicht erforderlich.

Nach der Installation zeigt [Postman] die folgende Benutzeroberfläche an:

Image

  • Unter [2-3] hat man Zugriff auf die Produkteinstellungen;

Image

  • in [6], der in diesem Dokument verwendeten Version;

Wir werden hier [Postman] verwenden, um den zuvor genannten Webdienst jSON zu testen:

  • Wir führen das Skript [flask/03/main_01] aus;
  • anschließend rufen wir URL [http://localhost:5000/] mit Postman auf; Image
  • in [1] erstellen wir eine Anfrage;
  • in [2] handelt es sich um eine Anfrage an HTTP und GET;
  • in [3] ist das URL des abgefragten Webdienstes;
  • In [4] wird die Anfrage an den Webdienst gesendet; Image
  • in [5] wählt man die Registerkarte [Body] aus, auf der das empfangene Dokument angezeigt wird;
  • In [6] wählt man die Registerkarte [Pretty] aus, die das empfangene Dokument in einer geeigneten Formatierung anzeigt, in diesem Fall in einer für die Zeichenfolge jSON geeigneten Formatierung;
  • in [7] das empfangene Dokument jSON;
  • in [8-9] das empfangene Dokument ohne Formatierung; Image
  • in [10] werden die von Postman empfangenen Header HTTP angezeigt;
  • in [11] der Status HTTP der empfangenen Antwort;
  • in [12] die empfangenen Header HTTP;
  • in [13] den Header [Content-type], durch den Postman wusste, dass es eine Zeichenfolge jSON erhalten würde. Postman nutzte diese Information, um das empfangene Dokument in gewisser Weise zu formatieren;

Es gibt noch eine weitere Möglichkeit, Postman zu nutzen. Dabei wird die Postman-Konsole (Strg-Alt-C) verwendet. Diese ermöglicht es, den Client-Server-Dialog einzusehen. Neben der Tastenkombination Strg-Alt-C ist die Postman-Konsole auch über ein Symbol unten links im Hauptfenster von Postman erreichbar:

Image

Die Postman-Konsole speichert den Client-Server-Dialog, der bei der Ausführung einer Postman-Anfrage stattfindet:

Image

  • In [3] finden Sie die Liste der Anfragen, die Postman seit seinem Start gestellt hat. Die neuesten befinden sich am Ende der Liste;
  • in [4] die von Postman gesendete Anfrage HTTP;
  • in [5-6] die vom Webserver bereitgestellte Antwort HTTP;
  • unter [7] sind die Protokolle im Modus [raw] zu sehen, d. h. ohne Darstellungsaufbereitung;

Im Modus [raw] sieht das Konsolenfenster wie folgt aus:

Image

  • in [8] die von Postman an den Webserver gesendete Anfrage HTTP;
  • in [9] die vom Webserver gesendete Antwort HTTP;
  • in [10] kann man zum Modus [pretty logs] zurückkehren;

Zur Vereinfachung der Erläuterungen nummerieren wir die Zeilen, die wir über die Postman-Konsole erhalten haben.

Für den Client:

1
2
3
4
5
6
7
8
GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 70e2acaa-b3e5-46f6-8375-989e6b94e694
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

Für den Server:

1
2
3
4
5
6
HTTP/1.0 200 OK
Content-type: application/json; charser=utf8
Content-Length: 56
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Mon, 13 Jul 2020 17:19:56 GMT
{"prénom": "Aglaë", "nom": "de la Hûche", "âge": 87}

Von nun an werden wir hauptsächlich Folgendes verwenden:

  • [Postman] als Web-Client;
  • die Konsole [Postman] in [raw mode], um den Client-Server-Dialog zu erläutern;

22.4.3. Skript [main_02]

Image

Das Web-Skript [main_02] lautet wie folgt:


# die Anwendung wird konfiguriert
import config
config=config.configure()

# Importe
from flask import Flask, make_response
from flask_api import status

# Abhängigkeiten
from Personne import Personne

# Flask-Anwendung
app = Flask(__name__)


# Startseite URL
@app.route('/')
def index():
    # eine Person
    personne = Personne().fromdict({"prénom": "Aglaë", "nom": "de la Hûche", "âge": 87})
    # Inhalt
    response = make_response(f"personne[{personne.prénom}, {personne.nom}, {personne.âge}]")
    # Header HTTP
    response.headers.set("Content-Type", "text/plain; charset=utf8")
    # Antwort HTTP
    return response, status.HTTP_200_OK


# nur Hauptteil
if __name__ == '__main__':
    # Der Server wird gestartet
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Das Skript [main_02] ist analog zum Skript [main_01]. Es unterscheidet sich in zwei Punkten:
    • Zeile 22: Das an den Client gesendete Dokument ist eine rohe Zeichenkette, keine jSON-Zeichenkette;
    • Zeile 24: Dies spiegelt sich im Header HTTP [Content-Type] wider, der den Typ [text/plain] für das Dokument angibt;

Wir führen das Webskript [main_02] aus und verwenden anschließend [Postman], um es abzufragen:

Image

  • Bei [1-3] wird die Anfrage an den Webdienst gesendet;
  • in [5] den Status OK der Antwort;
  • in [4, 6] die Header HTTP der Antwort;
  • in [7] den Header [Content-Type];
  • in [8-10] das vom Webdienst gesendete Dokument, eine Zeichenfolge;

Die Postman-Konsole gibt folgende Protokolle aus:

Client-Anfrage:

1
2
3
4
5
6
7
8
GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 7c7fc9f3-8df8-49ae-9dc8-53c2d87d111a
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

Serverantwort:


HTTP/1.0 200 OK
Content-Type: text/plain; charset=utf8
Content-Length: 34
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Mon, 13 Jul 2020 17:34:22 GMT

personne[Aglaë, de la Hûche, 87]

22.4.4. Skript [main_03]

Image

Das Web-Skript [main_03] lautet wie folgt:


# die Anwendung wird konfiguriert
import config
config = config.configure()

# Importe
from flask import Flask, make_response
from flask_api import status

# Abhängigkeiten
from MyException import MyException
from Personne import Personne

# Flask-Anwendung
app = Flask(__name__)


# Startseite URL
@app.route('/')
def index():
    # eine falsche Person
    msg_erreur = None
    try:
        personne = Personne().fromdict({"prénom": "", "nom": "", "âge": 87})
    except MyException as erreur:
        msg_erreur = f"{erreur}"
    # Fehler?
    if msg_erreur:
        response = make_response(msg_erreur)
        status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
    else:
        response = make_response(f"personne[{personne.prénom}, {personne.nom}, {personne.âge}]")
        status_code = status.HTTP_200_OK
    # Header HTTP
    response.headers.set("Content-Type", "text/plain; charset=utf8")
    # Antwort HTTP
    return response, status_code


# nur Hauptteil
if __name__ == '__main__':
    # Der Server wird gestartet
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeile 23: Es wird ein Fehler ausgelöst, indem eine falsche Person instanziiert wird;
  • Zeilen 27–29: Aufgrund des Fehlers:
    • Zeile 28: Es wird eine Antwort HTTP vorbereitet, deren Inhalt die Fehlermeldung ist;
    • Zeile 29: Dem Statuscode HTTP wird der Fehlerwert [500 Internal Server Error] zugewiesen;
  • Zeile 34: Dem Client wird mitgeteilt, dass ihm ein Klartext gesendet wird;
  • Zeile 36: Die Antwort HTTP wird an den Client gesendet;

Wir starten den Webdienst [main_03] und verwenden Postman, um ihn abzufragen:

Image

  • In [1-3] senden wir die Anfrage;
  • In [4] erhalten wir eine Antwort mit dem Statuscode [500 INTERNAL SERVER ERROR];
  • in [5-7]: Die Antwort ist ein Text, der den aufgetretenen Fehler beschreibt;

Image

  • bei [8-10] die Header HTTP der Antwort des Webdienstes;

In der Postman-Konsole lauten die Ergebnisse im Modus [raw] wie folgt:

Client-Anfrage:

1
2
3
4
5
6
7
8
GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 925ff036-a360-47af-adf6-78173c01a247
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

Serverantwort:


HTTP/1.0 500 INTERNAL SERVER ERROR
Content-Type: text/plain; charset=utf8
Content-Length: 74
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Mon, 13 Jul 2020 17:39:24 GMT

MyException[11, Le prénom doit être une chaîne de caractères non vide]

22.5. Skripte [flask/04]: in der Anfrage enthaltene Informationen

Image

Das Skript [request_parameters.py] soll zeigen, dass der Webdienst Zugriff auf verschiedene Informationen hat, die in der Anfrage eines Webclients enthalten sind. Der Code lautet wie folgt:


# Import
from flask import Flask, make_response, request
from flask_api import status
# Flask-Anwendung
app = Flask(__name__)


# Startseite URL
@app.route('/', methods=['GET', 'POST'])
def index():
    # Anfrageparameter
    request_data = {}
    request_data["environ"] = f"{request.environ}"
    request_data["path"] = request.path
    request_data["full_path"] = request.full_path
    request_data["script_root"] = request.script_root
    request_data["url"] = request.url
    request_data["base_url"] = request.base_url
    request_data["url_root"] = request.url_root
    request_data["accept_charsets"] = request.accept_charsets
    request_data["accept_encodings"] = request.accept_encodings
    request_data["accept_languages"] = request.accept_languages
    request_data["accept_mimetypes"] = request.accept_mimetypes
    request_data["args"] = request.args
    request_data["content_encoding"] = request.content_encoding
    request_data["content_length"] = request.content_length
    request_data["content_type"] = request.content_type
    request_data["endpoint"] = request.endpoint
    request_data["files"] = request.files
    request_data["form"] = request.form
    request_data["host"] = request.host
    request_data["method"] = request.method
    request_data["query_string"] = request.query_string.decode()
    request_data["referrer"] = request.referrer
    request_data["remote_addr"] = request.remote_addr
    request_data["remote_user"] = request.remote_user
    request_data["scheme"] = request.scheme
    request_data["script_root"] = request.script_root
    request_data["user_agent"] = f"{request.user_agent}"
    request_data["values"] = request.values
    # Antwort HTTP
    response = make_response(request_data)
    # Header HTTP
    response.headers["Content-Type"] = "application/json; charset=utf-8"
    # Antwort senden HTTP
    return response, status.HTTP_200_OK


# Hauptprogramm
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeile 9: Wir nehmen eine Änderung vor. Wir legen fest, welche Verben in der Anfrage des Clients zulässig sind. Postman liefert die Liste:

Image

Die ersten beiden, [GET, POST], werden am häufigsten verwendet und sind auch die einzigen, die in diesem Dokument zum Einsatz kommen. Zurück zu Zeile 9 des Codes: Der Parameter [methods] enthält die Liste der Methoden aus der obigen Liste, die von URL zugelassen sind. Ohne diesen Parameter ist nur die Methode [GET] zulässig. So war es bisher;

  • Zeile 12: Wir werden das Dictionary [request_data] anlegen;
  • Zeile 13: Die Client-Anfrage ist in einem vordefinierten Objekt [request] verfügbar, das in Zeile 2 importiert wurde und vom Typ [werkzeug.local.LocalProxy] ist. Die folgenden Zeilen rufen verschiedene Attribute dieses Objekts ab;
  • Anstatt jedes Attribut des Objekts [request] im Detail zu erläutern, werden wir diesen Code ausführen und uns die Ergebnisse ansehen. So lässt sich die Bedeutung der verschiedenen angezeigten Attribute besser nachvollziehen;
  • Zeile 42: Das Wörterbuch [request_data] wird der Inhalt der Antwort HTTP sein. Wir erinnern uns, dass es sich dabei um Text handeln muss. Flask wandelt Wörterbücher automatisch in Zeichenketten um, wie z. B. jSON;
  • Zeile 44: Dem Client wird mitgeteilt, dass er jSON erhalten wird;
  • Zeile 46: Die Antwort wird an den Client gesendet;

Mit dem Postman-Client senden wir die folgende Anfrage an den zuvor genannten Webdienst:

Image

  • in [1-2]: die gesendete Anfrage;
  • in [2] ist die Anfrage konfiguriert. Die Parameter werden an URL in der Form [ ?param1=valeur1&param2=valeur2] angehängt. Es gibt zwei Möglichkeiten, diese Parameter in Postman einzugeben:
    • sie direkt in URL eingeben;
    • sie in die Datei „[3-4]“ einzutragen;

Beide Methoden sind gleichwertig;

Wir fügen der Anfrage weitere Parameter hinzu:

Image

  • In [5-7] fügen wir Parameter im Hauptteil (=Body) der Anfrage hinzu. Während die Parameter von URL für den Nutzer eines Webbrowsers sichtbar sind, sind diejenigen, die Teil des Hauptteils der Anfrage sind, nicht sichtbar. Der Browser (oder hier Postman) sendet sie nach den Headern HTTP an den Server. Die Anfrage des Web-Clients hat nun dieselbe Struktur wie die Antwort des Web-Servers: HTTP-Header, gefolgt von einem Dokument. Dadurch erscheinen zwei neue HTTP-Header in der Anfrage des Clients:
    • [Content-Type]: Der Client teilt dem Server mit, welchen Dokumenttyp er sendet;
    • [Content-Length]: die Größe des Dokuments in Byte;
  • in [6] die Kodierung, die für die in [7] deklarierten Parameter verwendet werden soll. Diese können auf verschiedene Arten kodiert werden. [x-www-form-urlencoded] ist eine von Browsern häufig verwendete Methode;

Man sieht die Anfrage, die generiert wird:

Image

Die Antwort auf diese Anfrage lautet wie folgt:

Image

  • Bei [1-5] wurde eine Zeichenfolge jSON [3] empfangen;
  • Was den Webdienst in der Regel interessiert, sind die Parameter von URL und [ ?param1=valeur1&param2=valeur2] sowie diejenigen, die im Hauptteil der Anfrage (Dokument) übermittelt wurden. Auf diese Weise übermittelt der Client ihm in der Regel Informationen. In [5] ist zu sehen, dass die Parameter von URL in [request.args] verfügbar sind;

Der Rest der Antwort lautet wie folgt:

Image

  • In [9] sind die Attribute der im Anfragetext enthaltenen Parameter aufgeführt:
    • [content_type] ist der Typ des der Anfrage beigefügten Dokuments. Wir haben gesehen, dass dieses Dokument Informationen vom Typ [param=valeur] enthält, die in der Form [x-www-form-urlencoded] kodiert sind. Postman hat daher einen Header HTTP [Content-Type] generiert, der die Art des Dokuments angibt;
    • [content_length] ist die Größe dieses Dokuments in Byte;
  • In [10] enthält das Attribut [request.environ] zahlreiche Informationen über die Umgebung, in der die Anfrage des Clients verarbeitet wird. Die meisten dieser Informationen finden sich in den anderen Attributen des Objekts [request] wieder;
  • in [11] sind die im Hauptteil der Anfrage enthaltenen Parameter im Attribut [request.form] verfügbar;
  • in [12] die zum Senden der Anfrage verwendete Methode, hier die Methode [GET];
  • in [13] ist das Attribut [request.values] das Wörterbuch aller Parameter, sowohl der aus URL als auch der aus dem Dokumenttext. Um die Parameter der Anfrage abzurufen, verwendet man das Attribut:
    • [request.args], um die in URL enthaltenen Parameter abzurufen;
    • [request.form], um die im Dokumentkörper vorhandenen Parameter abzurufen;

In der Postman-Konsole lauten die Protokolle wie folgt:

Client-Anfrage:

GET /?param1=valeur1&param2=valeur2 HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: cbfac6aa-71a0-4076-a0c3-91d36d74a4c0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 60

nom=s%C3%A9l%C3%A9n%C3%A9&pr%C3%A9nom=agla%C3%AB&%C3%A2ge=77
  • Zeile 9: Der Typ des Dokuments, das in Zeile 12 an den Server gesendet wurde;
  • Zeile 11: Die HTTP-Header der Anfrage sind durch eine Leerzeile vom gesendeten Dokument getrennt. Auf diese Weise erkennt der Server das Ende der HTTP-Header des Clients;
  • Zeile 12: das „URL-kodierte“ Dokument. Alle Zeichen mit Akzenten wurden kodiert;

Die Antwort des Clients lautet wie folgt:


HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 2433
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:09:09 GMT

{
  "accept_charsets": [], 
  "accept_encodings": [
    [
      "gzip", 
      1
    ], 
    [
      "deflate", 
      1
    ], 
    [
      "br", 
      1
    ]
  ], 
  "accept_languages": [], 
  "accept_mimetypes": [
    [
      "*/*", 
      1
    ]
  ], 
  "args": {
    "param1": "valeur1", 
    "param2": "valeur2"
  }, 
  "base_url": "http://localhost:5000/", 
  "content_encoding": null, 
  "content_length": 60, 
  "content_type": "application/x-www-form-urlencoded", 
  "endpoint": "index", 
  "environ": "{'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.input': <_io.BufferedReader name=908>, 'wsgi.errors': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>, 'wsgi.multithread': True, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'werkzeug.server.shutdown': <function WSGIRequestHandler.make_environ.<locals>.shutdown_server at 0x00000173CA6E5160>, 'SERVER_SOFTWARE': 'Werkzeug/1.0.1', 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'PATH_INFO': '/', 'QUERY_STRING': 'param1=valeur1&param2=valeur2', 'REQUEST_URI': '/?param1=valeur1&param2=valeur2', 'RAW_URI': '/?param1=valeur1&param2=valeur2', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': 50592, 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '5000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'HTTP_USER_AGENT': 'PostmanRuntime/7.26.1', 'HTTP_ACCEPT': '*/*', 'HTTP_CACHE_CONTROL': 'no-cache', 'HTTP_POSTMAN_TOKEN': 'cbfac6aa-71a0-4076-a0c3-91d36d74a4c0', 'HTTP_HOST': 'localhost:5000', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'HTTP_CONNECTION': 'keep-alive', 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 'CONTENT_LENGTH': '60', 'werkzeug.request': <Request 'http://localhost:5000/?param1=valeur1&param2=valeur2' [GET]>}", 
  "files": {}, 
  "form": {
    "nom": "s\u00e9l\u00e9n\u00e9", 
    "pr\u00e9nom": "agla\u00eb", 
    "\u00e2ge": "77"
  }, 
  "full_path": "/?param1=valeur1&param2=valeur2", 
  "host": "localhost:5000", 
  "method": "GET", 
  "path": "/", 
  "query_string": "param1=valeur1&param2=valeur2", 
  "referrer": null, 
  "remote_addr": "127.0.0.1", 
  "remote_user": null, 
  "scheme": "http", 
  "script_root": "", 
  "url": "http://localhost:5000/?param1=valeur1&param2=valeur2", 
  "url_root": "http://localhost:5000/", 
  "user_agent": "PostmanRuntime/7.26.1", 
  "values": {
    "nom": "s\u00e9l\u00e9n\u00e9", 
    "param1": "valeur1", 
    "param2": "valeur2", 
    "pr\u00e9nom": "agla\u00eb", 
    "\u00e2ge": "77"
  }
}
  • Zeilen 1–5: Die HTTP-Header der Antwort, die mit einer Leerzeile enden;
  • Zeilen 41–45: Die Zeichen mit Akzenten wurden kodiert: UTF-8;

Wenn man nun die Methode [POST] verwendet, um dieselbe Anfrage mit denselben Parametern zu senden, erhält man dieselbe Antwort, nur dass man bei [12] die Antwort [‘method’ : ‘POST’] erhält.

Was ist also der Unterschied zwischen den Methoden GET und POST? Der Unterschied ist gering und ergab sich aus der historischen Verwendung durch die Browser:

  • Die Parameter in URL sind praktisch, da ein so konfiguriertes URL als Link in einem HTML-Dokument dienen kann. Der Benutzer kann die Parameter auch selbst ändern, um andere Antworten vom Server zu erhalten. In diesem Fall verwenden Browser üblicherweise die Methode [GET], und die an den Webserver gesendete Anfrage enthält keinen Hauptteil (content_length=0) (keine versteckten Parameter);
  • manchmal möchte man nicht, dass die Parameter im URL angezeigt werden. Dies ist beispielsweise bei Passwörtern der Fall, die an den Server gesendet werden. Außerdem ist die Größe, die die Parameter im URL einnehmen, begrenzt (ein URL darf eine bestimmte Größe nicht überschreiten). Die Parameter im Hauptteil der Anfrage unterliegen dieser Beschränkung nicht. Zudem machen zu viele Parameter im URL diesen unlesbar. Nehmen wir den häufigen Fall eines Anmeldeformulars auf einer Website. Früher, als HTML-Seiten noch kein JavaScript enthielten, sendeten die Browser die eingegebenen Informationen über ein POST. Man sprach damals von „geposteten Werten“;

Zu Beginn der Webprogrammierung galten

  • wurden die Methoden GET eher mit der Anforderung von Informationen von einem Webserver in Verbindung gebracht;
  • während die Methoden POST eher mit dem Senden von Informationen vom Browser an den Server in Verbindung standen. Der Server wurde dadurch sozusagen „angereichert“;

Seitdem hat JavaScript Einzug gehalten. Während der Entwickler in den vorherigen Beispielen keinen Einfluss hatte (das Anklicken eines Links löste zwangsläufig einen GET aus, das Absenden eines Formulars erfolgte zwangsläufig über einen POST), hat JavaScript ihm die Kontrolle zurückgegeben. In diesem Modell ist die Seite HTML mit JavaScript-Code verknüpft, der den Browser umgehen kann. So kann der Klick auf einen Link vom JavaScript-Code abgefangen werden, der anschließend einen Code ausführen kann, der eine Anfrage an den Server sendet. Diese Anfrage ist für den Nutzer nicht erkennbar. Er wird sie nicht sehen. Dieser Code ist ein Webclient, und wie wir es mit Postman getan haben, kann der Entwickler die gewünschte Anfrage erstellen. Um auf den Klick auf einen Link zurückzukommen: Er kann einen POST ausführen, während der Browser standardmäßig einen GET ausgeführt hätte. Durch diese Weiterentwicklungen haben die Unterschiede zwischen GET und POST an Bedeutung verloren.

Entwickler halten sich jedoch häufig an die folgenden Regeln:

  • Ein GET darf den Status des Servers nicht verändern. Aufeinanderfolgende GET, die mit denselben Parametern wie im URL ausgeführt werden, müssen dasselbe Dokument zurückgeben. Zudem hat der GET meist keinen Hauptteil (kein zugehöriges Dokument), sondern nur Parameter im URL;
  • die POST kann den Status des Servers ändern. Die Parameter werden meist im Hauptteil der Anfrage gesendet. Man spricht dann von „POST“-Werten. Das Beispiel des Formulars verdeutlicht dies am besten: Die vom Benutzer eingegebenen Werte werden in den Hauptteil des POST eingefügt, und der Server speichert sie an einem bestimmten Ort, häufig in einer Datenbank;

Im weiteren Verlauf dieses Dokuments halten wir uns nicht an bestimmte Regeln.

22.6. Skripte [flask-05]: Verwaltung des Benutzerspeichers

22.6.1. Einleitung

In den vorangegangenen Client-Server-Beispielen galt folgender Ablauf:

  • Der Client öffnet eine Verbindung zum Port 80 des Webserver-Rechners;
  • er sendet die Textsequenz: Header HTTP, Leerzeile, [document];
  • als Antwort sendet der Server eine Sequenz desselben Typs;
  • der Server beendet die Verbindung zum Client;
  • Der Client beendet die Verbindung zum Server;

Wenn derselbe Client kurz darauf eine neue Anfrage an den Webserver stellt, wird eine neue Verbindung zwischen dem Client und dem Server hergestellt. Der Server kann nicht erkennen, ob der sich verbindende Client bereits zuvor da war oder ob es sich um eine erste Anfrage handelt. Zwischen zwei Verbindungen „vergisst“ der Server seinen Client. Aus diesem Grund wird das Protokoll HTTP als zustandsloses Protokoll bezeichnet. Es ist jedoch sinnvoll, dass sich der Server an seine Clients erinnert. Wenn eine Anwendung gesichert ist, sendet der Client dem Server beispielsweise einen Benutzernamen und ein Passwort, um sich zu identifizieren. Wenn der Server seinen Client zwischen zwei Verbindungen „vergisst“, müsste sich dieser bei jeder neuen Verbindung erneut identifizieren, was nicht praktikabel ist.

Um einen Client nachzuverfolgen, kann der Server auf verschiedene Weise vorgehen:

  1. Bei einer ersten Anfrage eines Clients fügt er seiner Antwort eine Kennung hinzu, die der Client ihm anschließend bei jeder neuen Anfrage zurücksenden muss. Dank dieser Kennung, die für jeden Kunden unterschiedlich ist, kann der Server einen Kunden erkennen. Er kann dann einen Speicher für diesen Kunden in Form eines Speichers verwalten, der eindeutig der Kennung des Kunden zugeordnet ist. So funktionieren beispielsweise die Dienste PHP;
  2. Bei einer ersten Anfrage eines Kunden fügt der Server in seine Antwort keine Kennung ein, sondern den Speicher des Nutzers selbst. Auf der Serverseite wird nichts gespeichert. Um seinen Speicher zu erhalten, muss der Webclient diesen bei jeder neuen Anfrage erneut senden. Dieser wird bei jeder neuen Anfrage geändert (oder auch nicht) und an den Client zurückgesendet (oder auch nicht). Diese Methode wird vom Flask-Framework verwendet;

Die Unterschiede zwischen den beiden Methoden sind folgende:

  • Methode 1 beansprucht weniger Bandbreite. Zwischen Client und Server wird lediglich eine Kennung ausgetauscht. Wenn der Speicher des Benutzers wächst, hat dies keine Auswirkungen auf die Kennung, die unverändert bleibt. Dies ist bei Methode 2 nicht der Fall, bei der der Speicher des Benutzers bei jeder Anfrage ausgetauscht wird und im Laufe der Anfragen anwachsen kann;
  • Methode 1 beansprucht mehr Speicherplatz. Der Server speichert den Speicher des Benutzers nämlich in seinen Dateisystemen. Bei einer Million Benutzern könnte dies möglicherweise ein Problem darstellen. Bei Methode 2 wird nichts auf dem Server gespeichert;

Technisch läuft es bei beiden Methoden wie folgt ab:

  • In der Antwort an einen neuen Client fügt der Server den Header „HTTP“, „[Set-Cookie : MotClé=Identifiant]“ oder „[Set-Cookie : mémoire]“ ein. Bei Methode 1 geschieht dies nur bei der ersten Anfrage. Bei Methode 2 geschieht dies jedes Mal, wenn sich der Speicher des Benutzers ändert;
  • in seinen Anfragen sendet der Client systematisch das zurück, was er erhalten hat, nämlich eine Kennung oder einen Speicher. Dies geschieht über den Header HTTP [Cookie : MotClé=Valeur];

Man könnte sich fragen, wie der Server erkennen kann, dass es sich um einen neuen Client handelt und nicht um einen bereits bekannten. Dies wird durch das Vorhandensein des Headers HTTP Cookie in den Headern HTTP des Clients angezeigt. Bei einem neuen Client fehlt dieser Header.

Die Gesamtheit der Verbindungen eines bestimmten Kunden wird als Sitzung bezeichnet.

Der Server kann weitere Speichertypen verwalten:

Image

  • In [1] ist der Request-Speicher von besonderer Bedeutung. Er kommt zum Einsatz, wenn die Anfrage des Web-Clients nicht von einem einzigen Dienst (oder einer einzigen Anwendung), sondern von mehreren verarbeitet wird. Um Informationen an den Dienst i+1 weiterzugeben, kann der Dienst i die verarbeitete Anfrage (Request) um diese Informationen ergänzen. Dies wird als Request-Speicher bezeichnet. Wir werden diese Art von Speicher in diesem Dokument nicht verwenden;
  • in [2, 4] ist der soeben beschriebene Benutzerspeicher. Er kann lokal implementiert werden ([2]) oder mithilfe des Clients verwaltet werden ([4]);
  • In [3] ist der Speicher auf „Anwendungsebene“ in der Regel ein schreibgeschützter Speicher. Er wird von allen Benutzern gemeinsam genutzt. Dort befinden sich häufig Elemente der Konfiguration der Webanwendung, die von allen Benutzern der Anwendung gemeinsam genutzt wird. Bei dieser Art von Speicher ist Vorsicht geboten: Das Schreiben in diesen Speicher muss zu einem Zeitpunkt erfolgen, zu dem die Benutzer noch keine Anfragen gesendet haben, meist beim Start der Anwendung. Sobald dann Anfragen eingehen, ist es schwierig, in diesen Speicher zu schreiben. Wenn der Webserver mehrere Benutzer gleichzeitig bedient und zwei von ihnen in den Speicher auf „Anwendungsebene“ schreiben wollen, besteht die Gefahr, dass dieser Speicher beschädigt wird. Denn während Benutzer 1 begonnen hat, in den Speicher auf „Anwendungsebene“ zu schreiben, kann er unterbrochen werden, noch bevor er fertig ist. Man hat dann einen unvollständigen Anwendungsspeicher. Da dieser gemeinsam genutzt wird, kann ein Benutzer 2 ihn lesen und einen falschen Zustand erhalten;

22.6.2. Skript [session_scope_01]

Image

Die Skripte [session_scope_xx] veranschaulichen die Verwaltung von Benutzerspeichern.

Das Skript [session_scope_01] lautet wie folgt:


# die Anwendung wird konfiguriert
import config
config = config.configure()

# Abhängigkeiten
import json
from flask import Flask, make_response, session
from flask_api import status

# Flask-Anwendung
app = Flask(__name__)

# geheimer Sitzungsschlüssel
app.secret_key = config["SECRET_KEY"]


@app.route('/set-session', methods=['GET'])
def set_session():
    # man fügt etwas zur Sitzung hinzu
    session['nom'] = 'séléné'
    # Eine leere Antwort wird gesendet
    response = make_response()
    response.headers['Content-Length'] = 0
    return response, status.HTTP_200_OK


@app.route('/get-session', methods=['GET'])
def get_session():
    # Die Sitzung wird abgerufen und die Antwort gesendet
    response = make_response(json.dumps({"nom": session['nom']}, ensure_ascii=False))
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


# nur manuell
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeile 11: Eine Flask-Anwendung wird instanziiert;
  • Zeile 14: Das Attribut [secret_key] dieser Anwendung erhält einen Wert aus der in den Zeilen 1–3 verwendeten Konfigurationsdatei. Eine Flask-Sitzung ist nur möglich, wenn dieses Attribut initialisiert ist. Man kann darin beliebige Inhalte hinterlegen. Es dient dazu, einen Teil des „Benutzerspeichers“ zu verschlüsseln, der an den Client gesendet wird. In der Regel wird etwas verwendet, das schwer zu erraten ist. In der Datei [config] ist der geheime Schlüssel wie folgt definiert:

    # die Konfiguration wird zurückgegeben
    config = {
        # Flask-Konfiguration
        "SECRET_KEY": "vibnFfrdWYUp?*LQ"
    }
  • Zum ersten Mal definieren wir eine Webanwendung, die etwas anderes bedient als URL /
    • Zeile 17: URL [/set-session] dient dazu, die Sitzung des Benutzers zu initialisieren;
    • Zeile 27: URL [/get-session] dient dazu, den Speicher des Benutzers (oder die Benutzersitzung) abzurufen;
  • Zeile 20: Man speichert etwas im Speicher (= der Sitzung) des Benutzers, in diesem Fall einen Namen. Die Sitzung funktioniert ähnlich wie ein Wörterbuch. Man kann nicht beliebige Daten in die Sitzung schreiben. Die dort gespeicherten Werte müssen in jSON umgewandelt werden können. Bei vordefinierten Python-Typen geschieht dies ohne Eingreifen des Entwicklers. Bei benutzerdefinierten Objekten, die Python nicht kennt, muss man die Konvertierung jSON selbst vornehmen;
  • Zeile 22: Man erstellt eine HTTP-Antwort ohne Inhalt (kein Parameter für make_response);
  • Zeile 23: Dem Client wird mitgeteilt, dass er ein leeres Dokument (Größe 0 Byte) erhalten wird;
  • Zeile 24: Die Antwort HTTP wird an den Client gesendet. URL und [/set-session] dienen somit lediglich dazu, eine Benutzersitzung zu initialisieren;
  • Zeile 27: Mit den Antworten „URL“ und „[/get-session]“ kann der Benutzer erkennen, was sich in seiner Sitzung befindet;
  • Zeile 30: Es wird eine Antwort HTTP erstellt, die die Zeichenkette jSON aus der Benutzersitzung enthält. Hier haben wir die Zeichenfolge jSON selbst erstellt, anstatt sie von Flask generieren zu lassen. Denn wir möchten nicht, dass Zeichen mit Akzenten escaped werden (ensure_ascii=False);
  • Zeile 31: Wir teilen dem Client mit, dass wir ihm jSON senden;
  • Zeile 32: Wir senden die Antwort HTTP an den Client;

Das Ziel dieses Skripts ist es zu zeigen, dass die Benutzersitzung es ermöglicht, eine Verbindung zwischen den aufeinanderfolgenden Anfragen des Benutzers herzustellen:

  • Anfrage 1 fordert URL und [/set-session] an;
  • Anfrage 2 fordert die URL und [/get-session] an und ruft den Namen ab, den Anfrage 1 initialisiert hat;

Das Skript [config], das die Skripte im Ordner [flask/05] konfiguriert, lautet wie folgt:


def configure():
    # Absoluter Pfad als Referenz für die relativen Pfade der Konfiguration
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # Abhängigkeiten der Anwendung
    absolute_dependencies = [
        # Personen, Tools, MyException
        f"{root_dir}/classes/02/entities",
    ]
    # Der Syspath wird festgelegt
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # Die Konfiguration wird bereitgestellt
    config = {
        # Flask-Konfiguration
        "SECRET_KEY": "vibnFfrdWYUp?*LQ"
    }

    return config

Wir starten das Skript [session_scope_01] und fordern anschließend mit Postman die URL und [/set-session] ab. Zuvor überprüfen wir einige Elemente der Anfrage, die gestellt werden soll:

Image

  • In [1] rufen wir die Cookies von Postman ab; Image
  • bei [2-4] überprüfen wir die bekannten Postman-Cookies und löschen sie alle ([4-5]);

Nun überprüfen wir die Anfrage HTTP, die generiert wird:

Image

  • in [9]: ein Teil der Header HTTP, die Postman anhand der von uns vorgenommenen Konfiguration in die Anfrage einfügt. Anhand dieser Überprüfung können Sie sicherstellen, dass Sie keine Parameter ausgelassen oder im Gegenteil unnötige Parameter belassen haben;

Nachdem dies erledigt ist, kann die Abfrage ausgeführt werden:

Image

Es gibt verschiedene Möglichkeiten, das Ergebnis zu überprüfen. Man kann zunächst einen Blick auf das Hauptfenster werfen:

Image

  • in [1-2] die an den Webdienst gesendete Anfrage;
  • in [3-6] die Header der Antwort;
  • in [4]: Da im Code der Typ der Antwort nicht angegeben wurde, hat Flask standardmäßig den Typ [text/html] verwendet;
  • in [5] weiß der Client, dass die Antwort kein Dokument enthält;
  • Zeile 6: Der Header [Set-Cookie] wurde vom Flask-Server gesendet. Sein Wert wird als Session-Cookie bezeichnet. Er besteht aus drei Elementen:
    • [session=valeur]: Der Wert stellt den Speicher des Benutzers in verschlüsselter Form dar. Dieser Speicher ist entschlüsselbar (siehe |https://blog.miguelgrinberg.com/post/how-secure-is-the-flask-user-session|). Aufgrund des vom Server verwendeten geheimen Schlüssels kann der Nutzer die empfangene Speicherkapazität jedoch nicht verändern, um sie anschließend an den Server zurückzusenden. Wenn der Server eine Sitzung empfängt, ist somit sichergestellt, dass er eine unverfälschte Sitzung erhält;
    • [HttpOnly]: Das Vorhandensein dieses Elements weist den Browser, der es empfängt, darauf hin, dass das Cookie für das JavaScript, das die angezeigte Seite möglicherweise enthält, nicht zugänglich sein darf;
    • [Path=/] ist der Pfad, an den das Sitzungs-Cookie zurückgesendet werden muss, also in diesem Fall jeder Pfad der Webanwendung. Jedes Mal, wenn der Benutzer über die Tastatur explizit (er gibt ein URL ein) oder implizit (er klickt auf einen Link) ein URL dieser Domain anfordert, sendet der Browser automatisch das empfangene Sitzungs-Cookie zurück;

Der Nachteil des Hauptfensters besteht darin, dass man keinen Zugriff auf die vollständige Anfrage hat, die zu dieser Antwort geführt hat. Was in diesem Fenster angezeigt wird, ist verwirrend:

Image

  • In den Headern HTTP und [3-4] wird ein Sitzungscookie mit der Kennung [5] angezeigt. Man könnte daher annehmen, dass Postman ein Sitzungscookie in die Anfrage eingefügt hat, was jedoch nicht der Fall ist. Die Header [3] stellen tatsächlich die Header HTTP dar, die bei der nächsten Anfrage gesendet werden, so wie diese derzeit konfiguriert ist. Postman hat gerade ein Session-Cookie empfangen, das es bei der nächsten Anfrage zurücksenden wird. Deshalb haben wir [5];

Man kann den Client-Server-Dialog in der Postman-Konsole aufrufen, der mit Strg-Alt-C geöffnet wird:


GET /set-session HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 3673b73f-7600-4df4-8c4b-c37973e50df8
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 0
Vary: Cookie
Set-Cookie: session=eyJub20iOiJzXHUwMGU5bFx1MDBlOW5cdTAwZTkifQ.Xw6jGQ.y5Icu70wTIN-B0o_hwx0xDH247I; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:32:57 GMT
  • Zeile 14: das vom Server gesendete Sitzungs-Cookie;

Nun fordern wir URL und [/get-session] an:

GET /get-session HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: ce991398-2d9a-46d0-9ccd-c7ff3c7f4d6d
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: session=eyJub20iOiJzXHUwMGU5bFx1MDBlOW5cdTAwZTkifQ.Xw6jGQ.y5Icu70wTIN-B0o_hwx0xDH247I

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 20
Vary: Cookie
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:36:52 GMT

{"nom": "séléné"}
  • Zeile 9: Der Postman-Client hat das empfangene Session-Cookie an den Server zurückgesendet;
  • Zeile 18: Die vom Server gesendete Zeichenfolge jSON;

Dieses Beispiel verdeutlicht verschiedene Punkte:

  • Der Postman-Client sendet das Sitzungs-Cookie zurück, das er vom Flask-Server erhält. Webbrowser verfahren immer so;
  • Wir sehen, dass die Anfrage 2 ([/get-session]) es ermöglicht hat, eine Information abzurufen, die bei der Anfrage 1 ([/set-session]) erstellt wurde. Wir haben es hier also mit einem Benutzerspeicher zu tun;
  • Zeilen 11–16: Der Flask-Server hat kein Session-Cookie zurückgesendet. Dies geschieht nicht systematisch. Der Flask-Server sendet das Session-Cookie nur dann zurück, wenn die letzte Anfrage den Benutzerspeicher verändert hat;

22.6.3. Skript [session_scope_02]

Image

Das Skript [session_02] lautet wie folgt:


# Abhängigkeiten
import os

from flask import Flask, make_response, session
from flask_api import status

# Flask-Anwendung
app = Flask(__name__)

# Geheimer Sitzungsschlüssel
app.secret_key = os.urandom(12).hex()


# Startseite URL
@app.route('/', methods=['GET'])
def index():
    # Es werden drei Zähler verwaltet
    if session.get('n1') is None:
        session['n1'] = 0
    else:
        session['n1'] = session['n1'] + 1
    if session.get('n2') is None:
        session['n2'] = 10
    else:
        session['n2'] = session['n2'] + 1
    if session.get('n3') is None:
        session['n3'] = 100
    else:
        session['n3'] = session['n3'] + 1
    # Zählerverzeichnis
    compteurs = {"n1": session['n1'], "n2": session['n2'], "n3": session['n3']}
    # Die Antwort wird gesendet
    response = make_response(compteurs)
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


# Hauptprogramm
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeile 11: Hier wird der geheime Schlüssel mithilfe einer Funktion generiert. Der Vorteil dieser Funktion besteht darin, dass sie zufällig eine komplexe Zeichenkette erzeugt. Zur Erinnerung: Die Variable [app] ist die in Zeile 8 erstellte Instanz der Flask-Klasse;
  • Zeile 15: Diesmal gibt es nur eine Route, nämlich die Route „/“;
  • Zeilen 17–29: Es wird eine Sitzung verwaltet, die drei Zähler [n1, n2, n3] enthält. Beim ersten Aufruf des Benutzers gilt [n1, n2, n3] = [0, 10, 100], und bei jedem weiteren Aufruf werden diese Zähler um 1 erhöht;
  • Zeile 18: Bei der ersten Abfrage ist die Anwendungssitzung leer. Der Ausdruck [session.get(‘clé’)] liefert den Wert [None]. Bei den folgenden Abfragen liefert dieser Ausdruck den dem Schlüssel zugeordneten Wert;
  • Zeile 31: Diese Zähler werden in ein Wörterbuch gespeichert;
  • Zeile 33: Dieses Wörterbuch ist das Antwortdokument HTTP. Zur Erinnerung: Flask wandelt Wörterbücher automatisch in die Zeichenkette jSON um;
  • Zeile 34: Dem Web-Client wird mitgeteilt, dass er jSON erhalten wird;
  • Zeile 35: Die Antwort HTTP wird an den Client gesendet;

Führen wir dieses Skript aus und fragen wir die so erstellte Webanwendung mit Postman ab, nachdem wir alle Cookies des Postman-Clients [1-3] gelöscht haben:

Image

In der Postman-Konsole sehen die Client-Server-Kommunikationen wie folgt aus:


GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: c7db536d-9352-4aa6-9877-04560e03d935
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 41
Vary: Cookie
Set-Cookie: session=eyJuMSI6MCwibjIiOjEwLCJuMyI6MTAwfQ.Xw6nLg.v49CeDWwqP-6Dp9Qt330GAe-dNA; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:50:22 GMT

{
"n1": 0, 
"n2": 10, 
"n3": 100
}
  • in [14], das vom Server gesendete Sitzungs-Cookie;
  • in [18-22] die Antwort des Servers in Form einer Zeichenkette jSON;

Führen wir dieselbe Anfrage ein zweites Mal durch. Die Protokolle entwickeln sich wie folgt:


GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 8205ad85-37b3-41f2-a171-70dd3b3a1679
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: session=eyJuMSI6MCwibjIiOjEwLCJuMyI6MTAwfQ.Xw6nLg.v49CeDWwqP-6Dp9Qt330GAe-dNA

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 41
Vary: Cookie
Set-Cookie: session=eyJuMSI6MSwibjIiOjExLCJuMyI6MTAxfQ.Xw6nsw.OuxIQnGhmhSsan5Qu_FL3Iyu-9k; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:52:35 GMT

{
"n1": 1, 
"n2": 11, 
"n3": 101
}
  • Zeile 9: Der Postman-Client sendet das empfangene Sitzungs-Cookie zurück;
  • Zeile 15: In seiner Antwort sendet der Server ein neues Sitzungs-Cookie, da die Anfrage des Clients den Benutzerspeicher (= die Sitzung) verändert hat;
  • Zeilen 19–23: die neuen Werte der Zähler;

22.6.4. Skript [session_scope_03]

Dieses neue Skript soll zeigen, dass man verschiedene Python-Typen in eine Sitzung einfügen kann: Listen, Wörterbücher, Objekte. Die einzige Einschränkung besteht darin, dass die in die Sitzung aufgenommenen Objekte in jSON serialisierbar sein müssen. Sind sie dies standardmäßig nicht (Listen, Dictionaries), muss die Konvertierung in jSON selbst vorgenommen werden.


# Die Anwendung wird konfiguriert
import config
config = config.configure()

# Abhängigkeiten
import json
import os

from flask import Flask, make_response, session
from flask_api import status
from Personne import Personne

# Flask-Anwendung
app = Flask(__name__)

# Geheimer Sitzungsschlüssel
app.secret_key = os.urandom(12).hex()


# Startseite URL
@app.route('/', methods=['GET'])
def index():
    # Verwaltung einer Liste
    liste = session.get('liste')
    if liste is None:
        # 1. Anfrage
        liste = [0, 10, 100]
    else:
        # folgende Anfragen
        for i in range(len(liste)):
            liste[i] += 1
    # Die Liste wird wieder in die Sitzung aufgenommen
    session['liste'] = liste

    # Verwaltung eines Wörterbuchs
    dico = session.get('dico')
    if not dico:
        # 1. Abfrage
        dico = {"un": 0, "deux": 10, "trois": 100}
    else:
        # folgende Abfragen
        dico = session['dico']
        for key in dico.keys():
            dico[key] += 1
    # Das Wörterbuch wird wieder in die Sitzung geladen
    session['dico'] = dico

    # Verwaltung einer Person
    personne_json = session.get('personne')
    if personne_json is None:
        # 1. Abfrage
        personne = Personne().fromdict({"prénom": "aglaë", "nom": "séléné", "âge": 70})
    else:
        # folgende Abfragen
        personne = Personne().fromjson(personne_json)
        personne.âge += 1
    # Die Person wird wieder in die Sitzung aufgenommen
    session['personne'] = personne.asjson()

    # Ergebnisverzeichnis
    résultats = {"liste": liste, "dict": dico, "personne": personne.asdict()}

    # Eine Antwort wird gesendet jSON
    response = make_response(json.dumps(résultats, ensure_ascii=False))
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


# Hauptprogramm
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeilen 1–3: Die Webanwendung wird konfiguriert;
  • Zeilen 5–11: Die Abhängigkeiten werden importiert;
  • Zeile 14: Die Flask-Anwendung wird instanziiert;
  • Zeile 17: Das Attribut [secret_key] wird initialisiert. Dies ermöglicht die Verwendung von Sitzungen;
  • Zeile 21: Die einzige Route der Anwendung;
  • Zeilen 23–33: Verwaltung einer Liste in der Sitzung. Diese enthält standardmäßig in jSON serialisierbare Elemente;
  • Zeilen 35–46: Verwaltung eines Wörterbuchs in der Sitzung. Darin wurden standardmäßig serialisierbare Elemente in jSON abgelegt;
  • Zeilen 48–58: Verwaltung einer Person. Ein Objekt [Personne] ist standardmäßig nicht in jSON serialisierbar. Daher müssen Vorsichtsmaßnahmen getroffen werden;
  • Zeile 58: Es wird die Methode [BaseEntity.asjson] verwendet, um die Zeichenkette jSON der Person in der Sitzung zu speichern. Beachten Sie, dass man auch [personne.asdict] hätte verwenden können, da [personne.asdict] ein Wörterbuch ist, das Werte enthält, die standardmäßig in jSON serialisierbar sind;
  • Zeile 55: Da wir eine Zeichenkette jSON in der Sitzung gespeichert haben, rufen wir die Person daraus mithilfe der Methode [BaseEntity.fromjson] ab;
  • Zeile 61: Es wird das Wörterbuch [résultats] erstellt, das als Antwort an den Client gesendet wird. Wir wissen, dass Flask in diesem Fall die Zeichenfolge jSON aus dem Wörterbuch sendet. Daher darf dieses standardmäßig nur serialisierbare Werte in jSON enthalten;
  • Zeile 64: Wir fügen explizit die Zeichenfolge jSON aus dem Wörterbuch [résultats] in die Antwort HTTP ein. Flask hätte dies standardmäßig getan. Allerdings verwendet es standardmäßig den Parameter [ensure_ascii=True], was für uns nicht in Ordnung war;
  • Zeile 65: Dem Client wird mitgeteilt, dass er jSON erhalten wird;
  • Zeile 66: Wir senden ihm die Antwort;

Wir starten die Webanwendung. Wir löschen alle Cookies des Postman-Clients. Dann fordert dieser das URL [http://localhost:5000] 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: 5f8b7c63-aa8a-4429-a2fa-62141423d933
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 135
Vary: Cookie
Set-Cookie: session=.eJw9isEKwyAQRH-lzHkPm15K91dqD2mzBMFq0AgF8d-jsRQG9u3MK1jsO0AKFs1fyMSEPQabOjbOHsKV4GzaFfJgmnr4Sdg0puB9a1EMtmgys959-BjIxWBe3XxWLwNq_39IQ3Q_f5zhnHxdtYs3rqgH4gQvMg.Xw6yGw.Bwpt3q-sH03gFLmg2FIPXV_ZNt8; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 07:36:59 GMT

{"liste": [0, 10, 100], "dict": {"un": 0, "deux": 10, "trois": 100}, "personne": {"prénom": "aglaë", "nom": "séléné", "âge": 70}}

Wir senden die Anfrage ein zweites Mal:


GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 40fd00ea-d45c-46b7-a51e-d4d433a37b5c
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: session=.eJw9isEKwyAQRH-lzHkPm15K91dqD2mzBMFq0AgF8d-jsRQG9u3MK1jsO0AKFs1fyMSEPQabOjbOHsKV4GzaFfJgmnr4Sdg0puB9a1EMtmgys959-BjIxWBe3XxWLwNq_39IQ3Q_f5zhnHxdtYs3rqgH4gQvMg.Xw6yGw.Bwpt3q-sH03gFLmg2FIPXV_ZNt8

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 135
Vary: Cookie
Set-Cookie: session=.eJw9isEKwyAQRH-lzHkP2kupv9LtIW2WIBgNGqEg_nu3seQ0b2Zew-zfCa5hlvqBs5aw5-SLolGuUaETgi-7wD0sqaHPk7BJLilGXdEYW-ZqjNxjWhnuwpiWMB3Ti0Haz6MMMfz9EcM5-LrIT7zZjv4F5NYvOQ.Xw6ydQ.PMWRCqKx9HNnb_DyK-ha-9pCF7M; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 07:38:29 GMT

{"liste": [1, 11, 101], "dict": {"deux": 11, "trois": 101, "un": 1}, "personne": {"prénom": "aglaë", "nom": "séléné", "âge": 71}}
  • Zeile 9: Der Client sendet das empfangene Sitzungs-Cookie zurück;
  • Zeile 15: Der Server sendet ihm ein neues zurück, da sich der Inhalt der Sitzung geändert hat (Zeile 19). Zur Erinnerung: Dieser Inhalt ist in verschlüsselter Form im Sitzungs-Cookie enthalten;

22.7. Skripte [flask/06]: Informationen, die von allen Benutzern gemeinsam genutzt werden

22.7.1. Einleitung

Dieser Abschnitt soll zeigen, wie Informationen mit Anwendungsumfang verwaltet werden, d. h. Informationen, die von allen Benutzern gemeinsam genutzt werden. Bei diesen Informationen handelt es sich typischerweise um Konfigurationsdaten der Anwendung. Wir haben gesehen, dass eine Webanwendung verschiedene Arten von Speicher verwalten kann:

Image

Wir befassen uns hier mit dem Speicher der Anwendung [3].

22.7.2. Skript [application_scope_01]

Image

Das Skript [application_scope_01] zeigt eine Möglichkeit, Daten mit dem Geltungsbereich „Anwendung“ zu verwalten:


# Die Anwendung wird konfiguriert
import config
config = config.configure()

# Abhängigkeiten
from flask import Flask, make_response
from flask_api import status

# Flask-Anwendung
app = Flask(__name__)


# Startseite URL
@app.route('/', methods=['GET'])
def index():
    # Wir wollen zeigen, dass die Anwendung zwischen den Anfragen der verschiedenen Clients im Speicher verbleibt
    # Jeder Client kommuniziert mit derselben Anwendung

    # app_infos steht für Informationen auf Anwendungsebene und nicht auf Sitzungsebene
    # Das heißt, sie betrifft alle Benutzer und nicht einen bestimmten
    # Diese Information wird hier in [config] gespeichert (nicht zwingend erforderlich)

    # Ergebniswörterbuch
    résultats = {"config": config}

    # Die Antwort wird gesendet
    response = make_response(résultats)
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


# main
if __name__ == '__main__':
    # Es wird überprüft, ob dieser Code mehrmals ausgeführt wird
    print("application app lancée")
    # Die Webanwendung wird gestartet
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeilen 1–3: Das Konfigurationswörterbuch wird abgerufen. Wir werden zeigen, dass der Code außerhalb der Routing-Funktionen nur einmal ausgeführt wird. Die Flask-Anwendung bleibt im Speicher. Alle außerhalb der Routen initialisierten Informationen sind für diese global und stehen ihnen somit zur Verfügung. Somit wird das Dictionary [config] aus Zeile 3 von der Route / (Zeile 24) gerendert. Wir werden zeigen, dass alle Web-Clients dasselbe Dictionary erhalten und dass dieses somit von allen Clients gemeinsam genutzt wird. Es handelt sich also um eine Information mit dem Geltungsbereich „Anwendung“;
  • Zeile 35: Wir fügen ein Log ein, um zu überprüfen, ob der Code der Zeilen außerhalb der Routing-Funktion (Zeilen 1–10, 32–38) mehrfach ausgeführt wird;

Die Konfiguration [config] lautet wie folgt:


def configure():
    # Konfiguration wird zurückgegeben
    config = {
        # Flask-Konfiguration
        "SECRET_KEY""vibnFfrdWYUp?*LQ"
    }

    return config

Wir starten diese Anwendung. Die Protokolleinträge in der Konsole PyCharm lauten wie folgt:

Image

  • in [1], erster Start der Anwendung;
  • in [2]: Da der Modus [Debug] angefordert wurde, wird die Anwendung im Modus [Debug] neu gestartet;

Nun wird mit einem Browser (im Folgenden Chrome) der Modus URL [http://127.0.0.1:5000/] angefordert:

Image

Nun mit dem Firefox-Browser:

Image

Nun mit dem Postman-Client:

GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 51e75099-8ecb-4f27-ae3b-9386e982ede4
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 39
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 10:34:26 GMT

{
"SECRET_KEY": "vibnFfrdWYUp?*LQ"
}

Nun kehren wir zur Konsole [Run] in PyCharm zurück:

Image

  • Die beiden Protokolleinträge [1, 2] sind immer noch vorhanden, es gibt jedoch keine weiteren, obwohl die drei vom Webserver empfangenen Anfragen sichtbar sind;

Um ganz sicherzugehen, dass die Anwendung nicht bei jeder neuen Anfrage neu geladen wird, kann man in der Konfiguration einen Zähler einrichten und diesen bei jeder neuen Anfrage erhöhen. Man wird dann feststellen, dass jeder Client den Zähler in dem Zustand sieht, in dem ihn der vorherige Client hinterlassen hat. Es sei jedoch daran erinnert, dass Clients keine Daten im Anwendungsbereich ändern sollten, da diese von allen Clients gemeinsam genutzt werden und in einem Kontext, in dem der Server mehrere Clients gleichzeitig bedient, ohne dass garantiert ist, dass die Anfrage eines Clients vollständig und ohne Unterbrechung ausgeführt wird, ein Client 1, dessen Anfrage 1 vor ihrem Abschluss unterbrochen wurde, die gemeinsam genutzten Daten für nachfolgende Clients in einem beschädigten Zustand hinterlassen kann.

22.7.3. Skript [application_scope_02]

Image

Das Skript [application_scope_02] tut genau das, was man nicht tun sollte: Es ermöglicht den Clients, gemeinsam genutzte Informationen zu ändern. Wir werden einen Zähler zwischen den Benutzern teilen, die ihn jeweils erhöhen. Wir werden sehen, dass jeder Benutzer die Änderungen sieht, die andere Benutzer am Zähler vorgenommen haben.

Das Skript lautet wie folgt:


# Abhängigkeiten

from flask import Flask, make_response
from flask_api import status

# Flask-Anwendung
app = Flask(__name__)

# Anwendungsdaten
config = {
    "counter": 0
}


# Startseite URL
@app.route('/', methods=['GET'])
def index():
    # soll zeigen, dass das Wörterbuch [config] von allen Clients gemeinsam genutzt wird
    # der Webanwendung

    # Der Zähler wird erhöht
    config["counter"] += 1
    # Die Antwort wird gesendet
    response = make_response(config)
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


# main
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeilen 10–12: Das von den Benutzern gemeinsam genutzte Wörterbuch [config]. Es enthält einen Zähler;
  • Zeile 22: Jedes Mal, wenn ein Benutzer „URL /“ anfordert, wird der Zähler in der Konfiguration erhöht;
  • Zeilen 23–26: Die Zeichenfolge jSON aus dem Wörterbuch wird an jeden Client gesendet;

Wir starten dieses Skript. Anschließend rufen wir mit einem ersten Browser die Seiten URL und [http://127.0.0.1:5000/] auf:

Image

Anschließend wird dasselbe mit einem zweiten Browser durchgeführt:

Image

Anschließend ein drittes Mal mit Postman:

Image

Man sieht, dass jeder Client den Zähler in dem Zustand abruft, in dem der vorherige Client ihn hinterlassen hat. Sie haben also tatsächlich Zugriff auf dieselbe Information.

22.7.4. Skript [application_scope_03]

Das Skript [application_scope_03] zeigt, warum die zwischen Benutzern geteilten Informationen schreibgeschützt sein müssen.

Image

Das Skript lautet wie folgt:


# Abhängigkeiten
import threading
from time import sleep

from flask import Flask, make_response
from flask_api import status

# Flask-Anwendung
app = Flask(__name__)

# Anwendungs-Scope-Daten
config = {
    "counter": 0
}


# Startseite URL
@app.route('/', methods=['GET'])
def index():
    # soll zeigen, dass das Wörterbuch [config] von allen Clients gemeinsam genutzt wird
    # der Webanwendung gemeinsam genutzt wird und schreibgeschützt sein muss

    # Name des Threads
    thread_name = threading.current_thread().name
    # Der Zähler wird ausgelesen
    counter = config["counter"]
    print(f"compteur lu : {counter}, par le thread {thread_name}")
    # Es wird für 5 Sekunden angehalten – dadurch werden andere Clients bedient
    sleep(5)
    # Der Zähler der Konfiguration wird erhöht
    config["counter"] = counter + 1
    # Protokoll
    print(f"compteur écrit : {config['counter']}, par le thread {thread_name}")
    # Antwort wird gesendet
    response = make_response(config)
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


# main
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • Zeile 43: Der Ausführungsmodus der Webanwendung wurde geändert. Es wurde „[threaded=True]“ eingegeben, um anzugeben, dass die Anwendung mehrere Benutzer gleichzeitig bedienen soll. Dies geschieht mithilfe von Ausführungsthreads:
    • Es können mehrere Ausführungs-Threads gleichzeitig laufen, wobei jeder einen Benutzer bedient;
    • der Prozessor des Rechners wird von diesen Threads gemeinsam genutzt;
    • ein Thread kann unterbrochen werden, bevor er seine Arbeit beendet hat. Er wird später fortgesetzt;
  • Zeile 19: Die Funktion [index] kann von mehreren Threads gleichzeitig ausgeführt werden;
  • Zeile 24: Der Name des Threads, der die Funktion [index] ausführt, wird abgerufen;
  • Zeile 26: Der Wert des Zählers wird ausgelesen. Für die Zwecke unserer Demonstration gliedern wir die Inkrementierung des Zählers wie folgt auf:
    • Schritt 1: Auslesen des Zählers (z. B. 1) durch Thread 1;
    • Schritt 2: Thread 1 pausiert für 5 Sekunden (Zeile 29). Da Thread 1 eine Pause angefordert hat, wird die Prozessorausführung an einen anderen Thread, nämlich Thread 2, übergeben. Das Ziel ist, dass dieser neue Thread denselben Zählerwert (= 1) ausliest. Anschließend pausiert auch er für 5 Sekunden und verliert die Prozessorausführung;
    • Schritt 3: Inkrementierung des Zählers, Zeile 31, ausgehend von dem in Schritt 1 gelesenen Wert (= 1). Thread 1 ist der erste, der dies tut: Er setzt den Zähler auf 2 und beendet anschließend die Ausführung der Funktion [index]. Dann ist Thread 2 an der Reihe, zu erwachen und den Zähler ebenfalls auf 2 zu setzen, ausgehend von dem in Schritt 1 gelesenen Wert (=1). Letztendlich steht der Zähler nach der Ausführung beider Threads auf 2, obwohl er eigentlich bei 3 stehen sollte;
  • Zeile 33: Der Wert des Zählers wird zur Überprüfung angezeigt;

Wir starten das Skript und rufen dann die URL [http://loaclhost :5000/] zunächst mit zwei Browsern und anschließend mit Postman auf. Die Protokolle in der Konsole PyCharm lauten dann wie folgt:


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/flask/06/application_scope_03.py
 * Serving Flask app "application_scope_03" (lazy loading)
 * Environment: development
 * Debug mode: on
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 334-263-283
* Running on http://127.0.0.1:5000/ (Drücken Sie CTRL+C, um das Programm zu beenden)
compteur lu : 0, par le thread Thread-2
compteur lu : 0, par le thread Thread-4
compteur écrit : 1, par le thread Thread-2
127.0.0.1 - - [16/Jul/2020 08:55:37] "GET / HTTP/1.1" 200 -
compteur écrit : 1, par le thread Thread-4
127.0.0.1 - - [16/Jul/2020 08:55:40] "GET / HTTP/1.1" 200 -
compteur lu : 1, par le thread Thread-5
compteur écrit : 2, par le thread Thread-5
127.0.0.1 - - [16/Jul/2020 08:55:46] "GET / HTTP/1.1" 200 -
  • Zeilen 9–10: Die ersten beiden Threads 2 und 4 lesen denselben Wert 0 vom Zähler;
  • Zeile 11: Thread 2 setzt den Zähler auf 1;
  • Zeile 13: Thread 4 setzt den Zähler auf 1. Ab diesem Zeitpunkt ist der Wert des Zählers falsch;
  • Zeilen 15–16: Thread 5 wird nicht unterbrochen und verarbeitet den Wert des Zählers korrekt;

Aus diesem Beispiel lässt sich ableiten, dass der Code einer Webanwendung den Wert von Informationen, die von den Benutzern gemeinsam genutzt werden, nicht verändern darf.

22.8. Skripte [flask/07]: Routenverwaltung

Image

Wir befassen uns hier mit der Verwaltung der Routen einer Anwendung, d. h. den von der Webanwendung bereitgestellten URL.

22.8.1. Skript [main_01]: Konfigurierte Routen

Das Skript [main_01] führt die Möglichkeit ein, Routen zu konfigurieren:


from flask import Flask, make_response
from flask_api import status

# Flask-Anwendung
app = Flask(__name__)


# Antwort wird gesendet
def send_plain_response(réponse: str):
    # Die Antwort wird gesendet
    response = make_response(réponse)
    response.headers['Content-Type'] = 'text/plain; charset=utf-8'
    return response, status.HTTP_200_OK


# /Nachname/Vorname
@app.route('/<string:nom>/<string:prenom>', methods=['GET'])
def index(nom, prenom):
    # Antwort
    return send_plain_response(f"{prenom} {nom}")


# Sitzung initialisieren
@app.route('/init-session/<string:type>', methods=['GET'])
def init_session(type: str):
    # Antwort
    return send_plain_response(f"/init-session/{type}")


# Benutzer-Authentifizierung
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur():
    # Antwort
    return send_plain_response("/authentifier-utilisateur")


# Steuerberechnung
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot():
    # Antwort
    return send_plain_response("/calculer-impot")


# Simulationen auflisten
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations():
    # Antwort
    return send_plain_response("/lister-simulations")


# Simulation löschen
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int):
    # Antwort
    return send_plain_response(f"/supprimer-simulation/{numero}")


# Sitzung beenden
@app.route('/fin-session', methods=['GET'])
def fin_session():
    # Antwort
    return send_plain_response(f"/fin-session")


# Hauptprogramm
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeile 17: Hier wird der Typ der Parameter von URL angegeben. Dies ermöglicht es Flask, Überprüfungen durchzuführen. Ist der Parameter nicht vom erwarteten Typ, wird die Anfrage des Clients abgelehnt (Fehler 400 Bad Request). Flask übernimmt also einen Teil der Arbeit, die wir sonst hätten erledigen müssen;
  • Zeile 18: Bei den Parametern müssen die genauen Namen der Parameter aus Zeile 17 übernommen werden, jedoch nicht unbedingt deren Reihenfolge;
  • Zeile 20: Wir verwenden die Funktion [send_plain_response], um die Antwort an den Web-Client zu senden;
  • Zeile 9: Die Funktion [send_plain_response] empfängt die Zeichenkette, die an den Client gesendet werden soll;
  • Zeile 11: Der Hauptteil der Antwort HTTP wird erstellt;
  • Zeile 12: Dem Client wird mitgeteilt, dass ihm Klartext gesendet wird;
  • Zeile 13: Die Antwort HTTP wird gesendet;
  • Zeilen 23–62: weitere konfigurierte Routen, die später in einer Anwendungsübung verwendet werden;

Wir starten das Skript und rufen es mit dem Postman-Client auf:

Image

22.8.2. Skript [main_02]: Auslagerung der Routen

Im vorherigen Skript [main_01] kann der Code bei einer großen Anzahl von Routen sehr umfangreich werden. Das Skript [main_02] zeigt, wie man die Routen auslagert.

Image

Das Skript [routes_02] fasst die Funktionen zusammen, die im vorherigen Skript mit den Routen verbunden waren:


from flask import make_response
from flask_api import status


def send_response(réponse: str):
    # Antwort wird gesendet
    response = make_response(réponse)
    response.headers['Content-Type'] = 'text/plain; charset=utf-8'
    return response, status.HTTP_200_OK


# Startseite URL
def index(nom, prenom):
    # Antwort
    return send_response(f"{prenom} {nom}")


# Sitzung einleiten
def init_session(type: str):
    # Antwort
    return send_response(f"/init-session/{type}")


# Benutzer-Authentifizierung
def authentifier_utilisateur():
    # Antwort
    return send_response("/authentifier-utilisateur")


# Steuerberechnung
def calculer_impot():
    # Antwort
    return send_response("/calculer-impot")


# Simulationen auflisten
def lister_simulations():
    # Antwort
    return send_response("/lister-simulations")


# Simulation löschen
def supprimer_simulation(numero: int):
    # Antwort
    return send_response(f"/supprimer-simulation/{numero}")


# Sitzung beenden
def fin_session():
    # Antwort
    return send_response(f"/fin-session")

Es ist zu beachten, dass das Skript [routes_02] kein Routenskript ist. Es handelt sich um eine Liste von Funktionen. Das Hauptskript [main_02] stellt die Verbindung zwischen Routen und Funktionen her:


from flask import Flask

# Die Funktionen der Routen werden in ein eigenes Skript ausgelagert
import routes_02

# Flask-Anwendung
app = Flask(__name__)

# Zuordnung von Routen und Funktionen
app.add_url_rule('/<string:nom>/<string:prenom>', methods=['GET'], view_func=routes_02.index)
app.add_url_rule('/init-session/<string:type>', methods=['GET'], view_func=routes_02.init_session)
app.add_url_rule('/authentifier-utilisateur', methods=['POST'], view_func=routes_02.authentifier_utilisateur)
app.add_url_rule('/calculer-impot', methods=['POST'], view_func=routes_02.calculer_impot)
app.add_url_rule('/lister-simulations', methods=['GET'], view_func=routes_02.lister_simulations)
app.add_url_rule('/supprimer-simulation/<int:numero>', methods=['GET'], view_func=routes_02.supprimer_simulation)
app.add_url_rule('/fin-session', methods=['GET'], view_func=routes_02.fin_session)

# main
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeile 4: Das Skript mit den den Routen zugeordneten Funktionen wird importiert;
  • Zeilen 9–16: Zuordnung von Routen und Funktionen;

Mit dieser Methode kann jede einer Route zugeordnete Funktion bei Bedarf Gegenstand eines separaten Skripts sein.

Die Ergebnisse sind dieselben wie bei dem zuvor verwendeten Skript [main_01].