Skip to content

22. Webdienste mit dem Flask-Framework

Unter Webdienst verstehen wir hier jede Webanwendung, die Rohdaten liefert, die von einem Client verarbeitet werden – in den folgenden Beispielen oft ein Konsolenskript. Wir befassen uns nicht mit einer bestimmten Technologie wie REST (REpresentational State Transfer) oder SOAP (Simple Object Access Protocol), die mehr oder weniger Rohdaten in einem genau definierten Format liefern. REST gibt JSON zurück, während SOAP XML zurückgibt. Jede dieser Technologien beschreibt genau, wie der Client den Server abfragen muss und welches Format die Antwort des Servers haben muss. In diesem Kurs werden wir hinsichtlich der Art der Client-Anfrage und der Server-Antwort wesentlich flexibler sein. Die geschriebenen Skripte und die verwendeten Tools ähneln jedoch denen der REST-Technologie.

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 Anforderung der URL dieses Dienstes. Der Client kann in jeder beliebigen Sprache geschrieben sein, einschließlich Python. Im letzteren Fall nutzen wir die soeben behandelten Internetfunktionen. Wir müssen auch wissen, wie man mit einem Webdienst „kommuniziert“, d. h. das HTTP-Protokoll für die Kommunikation zwischen einem Webserver und seinen Clients verstehen. Das war der Zweck des Abschnitts über das HTTP-Protokoll. Die in diesem Teil des Kurses beschriebenen Web-Clients haben es uns ermöglicht, einen Teil des HTTP-Protokolls zu erkunden.

Image

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

  • Der Client öffnet eine Verbindung zu Port 80 auf dem Webserver;
  • er stellt eine Anfrage nach einem Dokument;
  • der Webserver sendet das angeforderte Dokument und schließt die Verbindung;
  • der Client schließt daraufhin die Verbindung;

Das Dokument kann verschiedene Formen annehmen: Text im HTML-Format, ein Bild, ein Video usw. 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 sprechen wir von Webprogrammierung. Das Skript zur dynamischen Generierung von Dokumenten kann in verschiedenen Sprachen geschrieben sein: PHP, Python, Perl, Java, Ruby, C#, VB.NET usw.

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 möglicherweise Parameter an dieses Skript;
  • In [3] führt der Webserver das Python-Skript mithilfe des Python-Interpreters aus. Das Skript generiert ein Dokument, das an den Client gesendet wird [2];
  • Der Server schließt die Verbindung. Der Client tut dasselbe;

Der Webserver kann mehrere Clients gleichzeitig bedienen.

Im Folgenden werden wir zwei Webserver verwenden:

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

Das Flask-Framework ist in Python geschrieben. Es handelt sich um ein Modul, das in einem PyCharm-Terminal installiert wird:


(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 Python-Webentwicklungs-Framework;
    • [Werkzeug-1.0.1]: ist der Webserver, der auf Client-Anfragen reagiert;
    • [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 Elemente der Webprogrammierung

Image

Unsere Beispiele werden in der folgenden Architektur ausgeführt:

Image

  • in [1] wird ein Python-Skript wie ein normales Konsolenskript ausgeführt;
  • in [2] wird transparent ein Webserver instanziiert, der auf Anfragen wartet. Tatsächlich akzeptiert er nur eine einzige URL;
  • in [3] fordert der Browser die einzige URL des Servers an;
  • in [4] führt der Server das von der Konsole [1] angegebene Python-Skript aus;
  • in [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 [Beispiel_01]: Grundlagen von HTML

Ein Webbrowser kann verschiedene Dokumente anzeigen, wobei HTML-Dokumente (HyperText Markup Language) am häufigsten vorkommen. Diese bestehen aus Text, der mit Tags in der Form <tag>text</tag> formatiert ist. So wird der Text <b>important</b> den Text „important“ in Fettdruck anzeigen. Es gibt eigenständige Tags, wie beispielsweise das <hr/>-Tag, das eine horizontale Linie anzeigt. Wir werden nicht alle Tags behandeln, die in HTML-Text vorkommen können. Es gibt viele WYSIWYG-Programme, mit denen Sie eine Webseite erstellen können, ohne eine einzige Zeile HTML-Code schreiben zu müssen. Diese Tools generieren automatisch den HTML-Code für ein Layout, das mit der Maus und vordefinierten Steuerelementen erstellt wurde. Sie können also (mit der Maus) eine Tabelle in die Seite einfügen und dann den von der Software generierten HTML-Code ansehen, um die Tags zu entdecken, die zur Definition einer Tabelle auf einer Webseite verwendet werden. So einfach ist das. Darüber hinaus sind HTML-Kenntnisse unerlässlich, da dynamische Webanwendungen den HTML-Code selbst generieren müssen, um ihn an Web-Clients zu senden. Dieser Code wird programmgesteuert generiert, und Sie müssen natürlich wissen, was zu generieren ist, damit der Client die gewünschte Webseite erhält.

Zusammenfassend lässt sich sagen, dass Sie nicht die gesamte HTML-Sprache beherrschen müssen, um mit der Webprogrammierung zu beginnen. Dieses Wissen ist jedoch notwendig und kann durch die Verwendung von WYSIWYG-Webseiten-Editoren wie DreamWeaver und Dutzenden anderen erworben werden. Eine weitere Möglichkeit, die Feinheiten von HTML zu entdecken, besteht darin, im Web zu surfen und den Quellcode von Seiten anzusehen, die interessante Elemente enthalten, denen Sie noch nicht begegnet sind.

Betrachten Sie das folgende Beispiel, das einige Elemente hervorhebt, die häufig in einem Webdokument vorkommen, wie zum Beispiel:

  • eine Tabelle;
  • ein Bild;
  • einem 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 liefert Informationen an den Browser, der das Dokument anzeigt. Er enthält häufig das Tag <title>…</title>, das den Text festlegt, der in der Titelleiste des Browsers erscheint. Er kann auch andere Tags enthalten, insbesondere solche, die die Schlüsselwörter des Dokuments definieren, die dann von Suchmaschinen verwendet werden. Dieser Abschnitt kann auch Skripte enthalten, die in der Regel in JavaScript oder VBScript geschrieben sind und vom Browser ausgeführt werden;
  • <body attributes>…</body>: Dies ist der Abschnitt, der vom Browser angezeigt wird. Die in diesem Abschnitt enthaltenen HTML-Tags teilen dem Browser das „gewünschte“ visuelle Layout des Dokuments mit. Jeder Browser interpretiert diese Tags auf seine eigene Weise. Infolgedessen können zwei Browser dasselbe Webdokument unterschiedlich darstellen. Dies ist im Allgemeinen eine der Herausforderungen, denen sich Webdesigner stellen müssen;

Der HTML-Code für unser Beispieldokument 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>
HTML
HTML-Tags und Beispiele
Dokumenttitel
<title>Einige HTML-Tags</title> (Zeile 5)
Der Text [Einige HTML-Tags] erscheint in der Titelleiste des Browsers, wenn das Dokument angezeigt wird
Horizontale Leiste
<hr />: zeigt eine horizontale Linie an (Zeile 10)
Tabelle
<table attributes>….</table>: zum Definieren der Tabelle (Zeilen 12, 32)
<thead>…</thead>: zum Definieren der Spaltenüberschriften (Zeilen 13, 19)
<tbody>…</tbody>: zum Definieren des Tabelleninhalts (Zeilen 20, 31)
<tr attributes>…</tr>: zum Definieren einer Zeile (Zeilen 21, 25)
<td attributes>…</td>: zum Definieren einer Zelle (Zeile 22)
Beispiele:
<table border="1">…</table>: Das Attribut „border“ definiert die Dicke des Tabellenrandes
<td style="text-align: center;">cell(1,2)</td> (Zeile 23): definiert eine Zelle, deren Inhalt cell(1,2) ist. Dieser Inhalt wird horizontal zentriert (text-align: center).
Bild
<img border="0" src="/static/images/cherrytree.jpg"/> (Zeile 38): definiert ein Bild ohne Rahmen (border="0"), dessen Quelldatei sich unter [/static/images/cherrytree.jpg] auf dem Webserver befindet (src="/static/images/cherrytree.jpg"). Befindet sich dieser Link in einem Webdokument, das über die URL [http://server/chemin/balises.html] erreichbar ist, fordert der Browser die URL [http://server/static/images/cherry-tree.jpg] an, um das hier referenzierte Bild abzurufen.
link
<a href="http://www.polytech-angers.fr/fr/index.html">here</a> (Zeile 43): Lässt den Text „here“ als Link zur URL http://www.polytech-angers.fr/fr/index.html fungieren.
Seitenhintergrund
<body style="background-image: url(/static/images/standard.jpg)"> (Zeile 8): gibt an, dass sich das Bild, das als Seitenhintergrund verwendet werden soll, unter der URL [/static/images/standard.jpg] auf dem Webserver befindet. In unserem Beispiel fordert der Browser die URL [http://server/static/images/standard.jpg] an, um dieses Hintergrundbild abzurufen.

An diesem einfachen Beispiel lässt sich erkennen, dass der Browser drei Anfragen an den Server senden muss, um das gesamte Dokument zu erstellen:

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

Das Skript [example_01] ermöglicht es uns, die vorherige statische Seite [tags.html] anzuzeigen:

Image

  • in [1] das Skript [example_01], das ausgeführt wird;
  • in [3] das HTML-Dokument, das vom Skript angezeigt wird;
  • in [2] die Bilder aus dem HTML-Dokument;

Das Skript [example_01] lautet wie folgt:

import os

from flask import Flask, make_response, render_template

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


#  Home URL
@app.route('/')
def index():
    #  page display
    return make_response(render_template("balises.html"))


#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeile 7: Wir instanziieren eine Flask-Anwendung. Eine Flask-Anwendung ist eine Webanwendung;
    • der erste Parameter ist der Name, der der Anwendung gegeben wird. Sie können einen beliebigen Namen wählen. Hier haben wir das vordefinierte Attribut [__name__] verwendet, das auf [__main__] gesetzt ist (Zeile 18);
    • der zweite Parameter ist ein benannter Parameter, was bedeutet, dass seine Position in der Parameterliste keine Rolle spielt. Der benannte Parameter [template_folder] gibt den Ordner an, in dem sich die statischen Seiten der Webanwendung befinden. Statische Seiten werden unverändert an den Browser übermittelt. Hier befinden sich die statischen Seiten im Ordner [templates] innerhalb der Projektverzeichnisstruktur. In Zeile 7 haben wir einen relativen Pfad zum Ordner [script_dir] angegeben, der das auszuführende Skript [example_01] enthält;
    • der dritte Parameter ist ebenfalls ein benannter Parameter. [static_folder] gibt den Ordner an, in dem sich die Ressourcen des HTML-Dokuments (Bilder, Videos usw.) befinden. Auch hier haben wir einen relativen Pfad zum Ordner [script_dir] angegeben, der das ausgeführte Skript [example_01] enthält;
  • Zeilen 10–14: Wir definieren die von der Webanwendung akzeptierten URLs. Jede URL ist mit einer Funktion verknüpft, die ausgeführt wird, wenn die URL von einem Webbrowser angefordert wird;
  • Zeile 11: Die einzige URL der Anwendung ist [/]. Beachten Sie, dass in [@app.route('/')] [app] die in Zeile 7 initialisierte Variable ist. Die Definition der Routen (die verschiedenen von der Anwendung verarbeiteten URLs) muss daher nach der Definition der Anwendung [app] erfolgen. Der Name der Anwendung ist beliebig;
  • Zeilen 12–14: Die Funktion, die ausgeführt wird, wenn die URL [/] von der Webanwendung [example_01] angefordert wird;
  • Zeile 12: Die einer URL zugeordnete Funktion kann einen beliebigen Namen haben. Sie kann manchmal Parameter enthalten, um Elemente aus der ihr zugeordneten URL abzurufen. Hier ist dies nicht der Fall;
  • Zeile 14:
    • Die Funktion [render_template] gibt eine Zeichenkette zurück, die dem durch ihren Parameter erzeugten Textdokument entspricht. In diesem Fall lautet der Parameter [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 HTTP-Antwort für den Browser, der die URL [/] angefordert hat. Wir haben im Abschnitt |Das HTTP-Protokoll| gesehen, dass eine HTTP-Antwort aus zwei Teilen besteht:
      • HTTP-Header;
      • das vom Browser angeforderte Dokument, in diesem Fall ein HTML-Dokument;

In Zeile 14 haben wir der Funktion [make_response] keine Parameter übergeben, um HTTP-Header zu generieren. Sie wird daher Standard-Header generieren. Wir werden später sehen, wie man diese HTTP-Header festlegt.

  • Wenn der Browser schließlich die URL / von der Flask-Anwendung anfordert, erhält er die Seite [tags.html];
  • Zeilen 17–20: Diese Zeilen dienen dazu, den Webserver zu starten, auf dem die Webanwendung [example_01] ausgeführt wird;
    • Zeile 18: Diese Bedingung ist nur dann wahr, wenn das Skript [example_01] in einer Konsole ausgeführt wird;
    • Zeile 19: Die Anwendung [app] aus Zeile 7 wird konfiguriert:
    • Der Parameter [ENV="development"] versetzt den Webserver in den Entwicklungsmodus: Sobald der Entwickler ein Element der Anwendung ändert, wird es neu generiert und an den Webserver übermittelt. Der Entwickler muss keine neue Ausführung anfordern;
    • Der Parameter [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 wird darauf bereitgestellt, um auf Anfragen von Web-Clients zu reagieren;

Hier ist ein Beispiel für einen Lauf:

Image

Die folgenden Protokolle erscheinen dann 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/ (Press CTRL+C to quit)
  • Zeile 2: Der Server zeigt das ausgeführte Skript an;
  • Zeile 3: Wir befinden uns im Entwicklungsmodus;
  • Zeilen 4–5: Der Server erkennt, dass er im [debug]-Modus gestartet wurde. Er startet daraufhin neu (Zeile 5). Der [debug]-Modus verlangsamt daher den Startvorgang geringfügig;
  • Zeile 8: Die URL, unter der die bereitgestellte Webanwendung [example_01] verfügbar ist;

Rufen wir die URL [http://127.0.0.1:5000/] über einen Webbrowser auf:

Image

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

22.2.2. Skript [example_02]: Dynamisches Erstellen eines HTML-Dokuments

Image

Das Skript [example_02] [1] generiert das folgende Dokument [example_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 dann vollständig bekannt ist, wenn der Webserver es bereitstellt. Insbesondere enthalten die Zeilen 5 und 8 zwei Elemente, die zum Zeitpunkt der Erstellung der Seite noch unbekannt 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 Zeichenfolgen handelt.

  • Zeilen 5, 8: Die Syntax {{expression}} ist Teil 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: Wir haben die Syntax [page.title] verwendet. Wir sind daher davon ausgegangen, dass bei der Generierung der Seite vor dem Senden eine Variable [page] bekannt ist; wir werden sehen, wie. In der Syntax {{Ausdruck}} können wir beliebige Variablennamen verwenden. In den Zeilen 5 und 8 könnten wir somit {{title}} und {{contents}} haben. Wir könnten dann 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 [page];
    • die Attribute dieses Wörterbuchs werden auf der Seite verwendet. Hier sind das [page.title] in Zeile 5 und [page.contents] in Zeile 8;

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

from flask import Flask, make_response, render_template

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


#  Home URL
@app.route('/')
def index():
    #  page content in the form of a dictionary
    page = {"title": "un titre", "contents": "un contenu"}
    #  page display
    return make_response(render_template("exemple_02.html", page=page))


#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Die Zeilen 4–5 und 18–20 haben wir bereits im vorherigen Beispiel erläutert. Wir werden diese Struktur in unseren Beispielen beibehalten;
  • Zeile 9: Die einzige von der Webanwendung bereitgestellte URL ist die URL /;
  • Zeile 14: Das unter der URL / bereitgestellte Dokument ist das soeben besprochene Dokument [example_02.html]. Wir wissen, dass es einen Parameter enthält, ein Wörterbuch namens [page];
  • Zeile 12: Wir definieren das Wörterbuch, das als Parameter an die Seite [example_02.html] übergeben wird. Es kann einen beliebigen Namen haben. Es muss jedoch die im HTML-Dokument verwendeten Attribute [title, contents] enthalten;
  • Zeile 14: Die Funktion [render_template] ist für die Darstellung der Zeichenfolge des Dokuments [example_02.html] zuständig. Da es sich um ein parametrisiertes Dokument handelt, übergeben wir den oder die erwarteten Parameter an die Funktion [render_template]. Dies geschieht hier durch die Zuweisung eines Werts an den Parameter [page]. In der Operation [page=page]:
    • Links vom =-Zeichen steht der Parameter [page], der im Dokument [example_02.html] verwendet wird;
    • rechts vom =-Zeichen steht der in Zeile 12 definierte Wert [page];
    • Im Allgemeinen gilt: Wenn ein HTML-Dokument die Parameter [param1, param2, …, paramn] enthält, übergeben wir deren Werte an die Funktion [render_template] in der Form [render_template(document, param1=value1, param2=value2, …] ;

Bevor wir [example_02] ausführen, müssen wir die Ausführung von [example_01] anhalten:

Image

Wenn es während der Ausführung von Skript 1 so aussieht, als würde Skript 2 laufen, liegt das wahrscheinlich daran, dass Skript 2 tatsächlich noch läuft. Um zu einem bekannten Zustand zurückzukehren, können Sie alle derzeit laufenden Prozesse in PyCharm anhalten (oben rechts im PyCharm-Fenster):

Image

Führen wir nun das Skript [example_02] aus:

Image

Die Konsolenprotokolle sehen dann wie folgt aus:


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/ (Press CTRL+C to quit)

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

Über einen Browser rufen wir die URL [http://localhost:5000/] auf:

Image

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

22.2.3. Skript [example_03]: Verwendung von Seitenfragmenten

Image

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

Das Dokument [example_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-Anweisung [include] verwendet, um externe Elemente in das Dokument einzubinden;
  • die Syntax lautet {% include … %}. Der Parameter der [include]-Direktive ist der Pfad zu dem einzubindenden Dokument. Dieser Pfad ist relativ zum Parameter [template_folder] der Flask-Anwendung:

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

Hier sind die Dokumentpfade also relativ zum Ordner [templates].

Das Fragment [fragment_01.html] (die Namen sind natürlich beliebig) sieht wie folgt aus:


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

Das Fragment [fragment_02.html] lautet wie folgt:


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

Wenn wir das Dokument [example_03.html] anhand dieser Fragmente rekonstruieren, erhalten wir 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 [example_02.html] identisch ist, aber aus Fragmenten zusammengesetzt wurde.

Das Webskript [example_03.py] lautet wie folgt:

import os

from flask import Flask, make_response, render_template

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


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


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

Der Code ähnelt dem in [example_02.py]. Zeile 16 zeigt, wie man auf Dokumente verweist, die sich in Unterordnern von [template_folder] aus Zeile 7 befinden.

Die Ausführung des Skripts [example_03.py] führt im Browser zu folgenden Ergebnissen:

Image

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

Image

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


<!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_time];

Der Webdienst [date_time_server.py] sieht wie folgt aus:

#  imports
import os
import time

from flask import Flask, make_response, render_template

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


#  Home URL
@app.route('/')
def index():
    #  dispatch time to customer
    #  time.localtime: number of milliseconds since 01/01/1970
    #  time.strftime formats time and date
    #  date-time display format
    #  d: 2-digit day
    #  m: 2-digit month
    #  y: 2-digit year
    #  H: hour 0.23
    #  M: minutes
    #  S: seconds

    #  current date / time
    time_of_day = time.strftime('%d/%m/%y %H:%M:%S', time.localtime())
    #  generate the document to be sent to the customer
    page = {"date_heure": time_of_day}
    document = render_template("date_time_server.html", page=page)
    print("document", type(document), document)
    #  HTTP response to customer
    response = make_response(document)
    print("response", type(response), response)
    return response


#  hand only
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeile 13: Die Webanwendung bedient nur die URL /;
  • Zeilen 15–24: Erläutern Sie, wie Datum und Uhrzeit abgerufen und angezeigt werden;
  • Zeile 27: Zeichenfolge, die das aktuelle Datum und die aktuelle Uhrzeit darstellt;
  • Zeilen 28–30: Erzeugt das dynamische Dokument [date_time_server.html], indem es das Wörterbuch [page] aus Zeile 29 übergibt;
  • Zeile 31: Zeigt den Typ von [document] und das Dokument selbst an. Wir wollen zeigen, dass es sich um eine Zeichenkette handelt;
  • Zeile 33: Erzeugt die HTTP-Antwort, die an den Client gesendet werden soll (sie wurde noch nicht gesendet);
  • Zeile 34: Wir zeigen ihren Typ und ihren Wert an;
  • Zeile 35: Die HTTP-Antwort wird an den Client gesendet;

Die Ausführung des Skripts führt im Browser zu folgendem 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/ (Press CTRL+C to quit)
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: Wir sehen, dass der Typ des von [render_template] zurückgegebenen Werts [str] ist. Diese Zeichenkette ist nichts anderes als das Dokument [date_time_server.html], nachdem es geparst wurde (Zeilen 10–19);
  • Zeile 20: Wir sehen, dass der Typ des von [make_response] zurückgegebenen Werts [flask.wrappers.Response] ist. Die Funktion [Response.__str__] wurde implizit aufgerufen, um das [Response]-Objekt anzuzeigen. Die von dieser Funktion zurückgegebene Zeichenkette liefert zwei Informationen über die HTTP-Antwort, die gesendet wird:
    • Das gesendete Dokument ist 195 Byte groß;
    • der HTTP-Antwortstatus 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 zurückgegeben hat:


<!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 Web-Client ist möglicherweise nur an den Informationen [page.date_time] in Zeile 8 interessiert und nicht am umgebenden HTML-Markup. Der Webdienst könnte diese Informationen als einfachen String zurückgeben. 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():
    #  absolute path configuration relative path reference
    rootDir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    #  application dependencies
    absolute_dependencies = [
        #  Person, Utilities, MyException
        f"{rootDir}/classes/02/entities",

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

    #  return the config
    return {}

Der Hauptzweck dieser Konfiguration besteht darin, den Python-Pfad für den Webdienst zu definieren. Wir müssen in der Lage sein, die Entitäten [2] (Zeile 8) zu finden.

Das Webskript [main_01] lautet wie folgt:

#  configure the application
import config
config=config.configure()

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

#  dependencies
from Personne import Personne

#  flask application (no static documents here)
app = Flask(__name__)


#  Home URL
@app.route('/')
def index():
    #  a person
    personne = Personne().fromdict({"prénom": "Aglaë", "nom": "de la Hûche", "âge": 87})
    #  answer HTTP
    response = make_response(str(personne))
    #  headers HTTP
    response.headers.set("Content-type", "application/json; charser=utf8")
    #  we return the answer HTTP
    return response, status.HTTP_200_OK


#  hand only
if __name__ == '__main__':
    #  start the server
    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 bedient nur die URL /;
  • Zeile 20: Es wird ein [Person]-Objekt erstellt;
  • Zeile 22: Es wird eine HTTP-Antwort mit der Zeichenkette erstellt, die die Person repräsentiert. Die Funktion [Person.__str__] wird aufgerufen. Diese gibt die JSON-Zeichenkette des [asdict]-Wörterbuchs der Person zurück (siehe |BaseEntity-Klasse|). Der Parameter der Funktion [make_response] ist das an den Client gesendete Textdokument, also hier die JSON-Zeichenkette einer Person;
  • Zeile 24: Wir fügen den HTTP-Headern der Antwort einen [Content-type]-Header hinzu, der dem Client mitteilt, welche Art von Dokument er erhalten wird – in diesem Fall ein in UTF-8 kodiertes JSON-Dokument;
  • Zeile 26: Wir geben ein Tupel mit zwei Elementen zurück:
    • die Antwort an den Client, einschließlich HTTP-Header und des Dokuments;
    • den Antwortstatuscode. Hier möchten wir den Statuscode [200 OK] zurückgeben. Die verschiedenen Statuscodes werden durch Konstanten im Modul [flask_api] definiert, das in Zeile 7 importiert wird;

Das Modul [flask_api] ist standardmäßig nicht verfügbar. Sie müssen es installieren. Dies können Sie in einem PyCharm-Terminal tun:


(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

Beim Ausführen des Webskripts [main_01] werden die folgenden Ergebnisse in einem Browser angezeigt:

Image

  • in [2] die empfangene JSON-Zeichenkette;
  • in [3-4] zeigen wir den Inhalt des empfangenen Dokuments an. Wir sehen, dass es kein HTML-Markup gibt, sondern nur die JSON-Zeichenkette;

Betrachten wir nun die Rolle des [Content-Type]-Headers, der vom Webdienst an den Client gesendet wird. Wir versetzen den Browser in den Entwicklermodus (normalerweise F12) und rufen dieselbe URL erneut auf. Unten sehen Sie einen Screenshot eines Chrome-Browsers:

Image

  • Wählen Sie in [1] die Registerkarte [Netzwerk] aus;
  • Bei [2, 4]: die vom Browser angeforderte URL;
  • Wählen Sie unter [3] die Registerkarte [Headers] (HTTP-Header) aus;
  • bei [5] den Statuscode der empfangenen HTTP-Antwort;
  • in [6] der Header, der dem Client mitteilt, dass er einen JSON-Text erhalten wird. Dadurch kann sich der Client an die Antwort anpassen. Daher ist die von Chrome zur Anzeige einer JSON-Antwort oder einer einfachen Textantwort verwendete Schriftart nicht dieselbe;

Image

  • Wählen Sie in [8] die Registerkarte [Response] aus, um auf das vom Webdienst gesendete Dokument zuzugreifen, in diesem Fall eine einfache JSON-Zeichenkette;

22.4.2. Postman

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

  • beliebige URLs verwenden: Diese werden manuell erstellt;
  • den Webserver mit GET, POST, PUT, OPTIONS usw. abzufragen;
  • die GET- oder POST-Parameter festlegen;
  • die HTTP-Header für die Anfrage festzulegen;
  • eine Antwort im JSON-, XML- oder HTML-Format empfangen;
  • auf die HTTP-Header der Antwort zugreifen. Dadurch erhalten Sie Zugriff auf die vollständige HTTP-Antwort des Servers;

[Postman] ist ein hervorragendes Lernwerkzeug, um die Client-Server-Kommunikation über das HTTP-Protokoll zu verstehen.

[Postman] ist unter der URL [https://www.getpostman.com/downloads/] verfügbar. Fahren Sie mit der Installation Ihrer Version von [Postman] fort. Während der Installation werden Sie aufgefordert, ein Konto zu erstellen: Dies wird hier nicht benötigt. Das [Postman]-Konto dient dazu, verschiedene Geräte zu synchronisieren, sodass die Konfiguration eines Geräts auf einem anderen repliziert wird. Nichts davon ist hier erforderlich.

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

Image

  • in [2-3] können Sie auf die Produkteinstellungen zugreifen;

Image

  • in [6] die in diesem Dokument verwendete Version;

Hier werden wir [Postman] verwenden, um den zuvor beschriebenen JSON-Webdienst zu testen:

  • Wir führen das Skript [flask/03/main_01] aus;
  • Anschließend senden wir mit Postman eine Anfrage an die URL [http://localhost:5000/]; Image
  • In [1] erstellen wir eine Anfrage;
  • in [2] handelt es sich um eine HTTP-GET-Anfrage;
  • in [3] die URL des abzufragenden Webdienstes;
  • In [4] senden wir die Anfrage an den Webdienst; Image
  • in [5] wählen wir die Registerkarte [Body] aus, auf der das empfangene Dokument angezeigt wird;
  • in [6] wählen Sie die Registerkarte [Pretty] aus, auf der das empfangene Dokument mit der entsprechenden Formatierung angezeigt wird, in diesem Fall formatiert als JSON-Zeichenkette;
  • in [7] das empfangene JSON-Dokument;
  • in [8-9] das empfangene Dokument ohne Formatierung; Image
  • in [10] werden die von Postman empfangenen HTTP-Header angezeigt;
  • in [11] der HTTP-Status der empfangenen Antwort;
  • in [12] die empfangenen HTTP-Header;
  • in [13] der [Content-type]-Header, der Postman erkennen ließ, dass es eine JSON-Zeichenkette empfangen würde. Postman nutzte diese Information, um das empfangene Dokument auf eine bestimmte Weise zu formatieren;

Es gibt noch eine weitere Möglichkeit, Postman zu nutzen. Dabei wird die Postman-Konsole (Strg-Alt-C) verwendet. So können Sie den Client-Server-Dialog einsehen. Neben der Tastenkombination Strg-Alt-C ist die Postman-Konsole auch über ein Symbol in der unteren linken Ecke des Postman-Hauptfensters erreichbar:

Image

Die Postman-Konsole protokolliert die Client-Server-Dialoge, die bei der Ausführung einer Postman-Anfrage stattfinden:

Image

  • in [3] die Liste der Anfragen, die Postman seit dem Start gestellt hat. Die neuesten befinden sich am Ende der Liste;
  • in [4] die von Postman gesendete HTTP-Anfrage;
  • in [5-6] die vom Webserver gesendete HTTP-Antwort;
  • in [7] können Sie die Protokolle im [Raw]-Modus anzeigen, d. h. ohne jegliche Formatierung;

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

Image

  • in [8] die von Postman an den Webserver gesendete HTTP-Anfrage;
  • in [9] die vom Webserver gesendete HTTP-Antwort;
  • in [10] können Sie zum Modus [pretty logs] zurückkehren;

Um die Erläuterungen verständlicher zu machen, nummerieren wir die Zeilen aus der Postman-Konsole.

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 [Postman]-Konsole im [Raw-Modus], um den Client-Server-Dialog zu erläutern;

22.4.3. Skript [main_02]

Image

Das Webskript [main_02] lautet wie folgt:

#  configure the application
import config
config=config.configure()

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

#  dependencies
from Personne import Personne

#  flask application
app = Flask(__name__)


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


#  hand only
if __name__ == '__main__':
    #  start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Das Skript [main_02] ähnelt dem Skript [main_01]. Es unterscheidet sich in zwei Punkten:
    • Zeile 22: Das an den Client gesendete Dokument ist eine Raw-Zeichenkette, keine JSON-Zeichenkette;
    • Zeile 24: Dies spiegelt sich im HTTP-Header [Content-Type] wider, der den Typ [text/plain] für das Dokument angibt;

Wir führen das Webskript [main_02] aus und fragen es anschließend mit [Postman] ab:

Image

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

Die Postman-Konsole zeigt die folgenden Protokolle an:

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 Webskript [main_03] lautet wie folgt:

#  configure the application
import config
config = config.configure()

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

#  dependencies
from MyException import MyException
from Personne import Personne

#  flask application
app = Flask(__name__)


#  Home URL
@app.route('/')
def index():
    #  an incorrect person
    msg_erreur = None
    try:
        personne = Personne().fromdict({"prénom": "", "nom": "", "âge": 87})
    except MyException as erreur:
        msg_erreur = f"{erreur}"
    #  mistake?
    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
    #  headers HTTP
    response.headers.set("Content-Type", "text/plain; charset=utf8")
    #  answer HTTP
    return response, status_code


#  hand only
if __name__ == '__main__':
    #  start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeile 23: Ein Fehler wird durch die Instanziierung einer falschen Person ausgelöst;
  • Zeilen 27–29: Aufgrund des Fehlers:
    • Zeile 28: Erstellen einer HTTP-Antwort mit der Fehlermeldung als Inhalt;
    • Zeile 29: Wir setzen den HTTP-Statuscode auf einen Fehlerwert [500 Internal Server Error];
  • Zeile 34: Wir teilen dem Client mit, dass wir Klartext senden;
  • Zeile 36: Wir senden die HTTP-Antwort an den Client;

Wir starten den Webdienst [main_03] und fragen ihn mit Postman ab:

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

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

In der Postman-Konsole sehen die Ergebnisse im [raw]-Modus wie folgt 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: 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] zeigt, 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 application
app = Flask(__name__)


#  Home URL
@app.route('/', methods=['GET', 'POST'])
def index():
    #  query parameters
    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
    #  answer HTTP
    response = make_response(request_data)
    #  headers HTTP
    response.headers["Content-Type"] = "application/json; charset=utf-8"
    #  send reply HTTP
    return response, status.HTTP_200_OK


#  hand
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 stellt die Liste bereit:

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 der URL zugelassen werden. Fehlt dieser Parameter, ist nur die Methode [GET] zulässig. Dies war bisher der Fall;

  • Zeile 12: Wir erstellen das [request_data]-Dictionary;
  • Zeile 13: Die Anfrage des Clients 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 [request]-Objekts im Detail zu beschreiben, führen wir diesen Code aus und sehen uns die Ergebnisse an. So werden wir die Bedeutung der verschiedenen angezeigten Attribute besser verstehen;
  • Zeile 42: Das Wörterbuch [request_data] wird den Inhalt der HTTP-Antwort bilden. Beachten Sie, dass dies Text sein muss. Flask konvertiert Wörterbücher automatisch in JSON-Zeichenfolgen;
  • Zeile 44: Wir teilen dem Client mit, dass er JSON erhalten wird;
  • Zeile 46: Wir senden die Antwort an den Client;

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

Image

  • in [1-2] die gesendete Anfrage;
  • in [2] die konfigurierte Anfrage. Die Parameter werden der URL in der Form [?param1=value1&param2=value2] angehängt. Es gibt zwei Möglichkeiten, diese Parameter in Postman einzugeben:
    • direkt in die URL eingeben;
    • Geben Sie sie in [3-4] ein;

Beide Methoden sind gleichwertig;

Wir fügen der Anfrage zusätzliche Parameter hinzu:

Image

  • in [5-7] fügen wir Parameter zum Hauptteil der Anfrage hinzu. Während URL-Parameter für den Benutzer in einem Webbrowser sichtbar sind, sind diejenigen im Hauptteil der Anfrage nicht sichtbar. Der Browser (oder in diesem Fall Postman) sendet sie nach den HTTP-Headern an den Server. Die Anfrage des Web-Clients hat dann dieselbe Struktur wie die Antwort des Web-Servers: HTTP-Header, gefolgt von einem Dokument. Dadurch werden zwei neue HTTP-Header in die Anfrage des Clients eingefügt:
    • [Content-Type]: Der Client teilt dem Server mit, um welche Art von Dokument es sich handelt;
    • [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;

Hier ist die Anfrage, die generiert wird:

Image

Die Antwort auf diese Anfrage lautet wie folgt:

Image

  • In [1-5] haben wir eine JSON-Zeichenkette [3] erhalten;
  • Für den Webdienst sind in der Regel die URL-Parameter [?param1=value1&param2=value2] sowie die im Request-Body (Dokument) übergebenen Parameter von Interesse. Auf diese Weise übermittelt der Client in der Regel Informationen an den Webdienst. Wie in [5] dargestellt, sind die URL-Parameter in [request.args] verfügbar;

Der Rest der Antwort sieht wie folgt aus:

Image

  • in [9] die Attribute der im Request-Body enthaltenen Parameter:
    • [content_type] ist der Typ des der Anfrage beigefügten Dokuments. Wir haben gesehen, dass dieses Dokument Informationen vom Typ [param=value] enthielt, die im Format [x-www-form-urlencoded] kodiert waren. Postman generierte daher einen HTTP-Header [Content-Type], der die Art des Dokuments angibt;
    • [content_length] ist die Größe dieses Dokuments in Byte;
  • In [10] enthält das Attribut [request.environ] eine Fülle von Informationen über die Umgebung, in der die Anfrage des Clients verarbeitet wird. Die meisten dieser Informationen finden sich in den anderen Attributen des [request]-Objekts;
  • in [11] sind die im Anfragetext vorhandenen Parameter im Attribut [request.form] verfügbar;
  • in [12] die Methode, die zum Senden der Anfrage verwendet wurde, hier die [GET]-Methode;
  • in [13] ist das Attribut [request.values] das Wörterbuch aller Parameter, einschließlich derer aus der URL und derer aus dem Dokumenttext. Um die Anfrageparameter abzurufen, verwenden Sie das Attribut:
    • [request.args], um die in der URL vorhandenen abzurufen;
    • [request.form], um die im Dokumenttext vorhandenen abzurufen;

In der Postman-Konsole sehen die Protokolle wie folgt aus:

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 wird;
  • 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 enden mit einer Leerzeile;
  • Zeilen 41–45: Zeichen mit Akzenten wurden UTF-8-kodiert;

Wenn wir nun die [POST]-Methode verwenden, um dieselbe Anfrage mit denselben Parametern zu senden, erhalten wir dieselbe Antwort, mit dem Unterschied, dass in [12] [‘method’: ‘POST’] steht.

Was ist also der Unterschied zwischen den Methoden GET und POST? Der Unterschied ist subtil und ergibt sich aus der Art und Weise, wie Browser sie historisch verwendet haben:

  • Parameter in der URL sind praktisch, da eine so konfigurierte URL als Link innerhalb eines HTML-Dokuments dienen kann. Der Benutzer kann die Parameter auch selbst ändern, um unterschiedliche Antworten vom Server zu erhalten. In diesem Fall verwenden Browser üblicherweise die [GET]-Methode, und die an den Webserver gesendete Anfrage enthält keinen Body (content_length=0) (keine versteckten Parameter);
  • Manchmal möchten wir nicht, dass Parameter in der URL angezeigt werden. Dies ist beispielsweise bei Passwörtern der Fall, die an den Server gesendet werden. Darüber hinaus ist der Platz, den URL-Parameter einnehmen, begrenzt (eine URL darf eine bestimmte Länge nicht überschreiten). Parameter im Request-Body unterliegen dieser Einschränkung nicht. Außerdem machen zu viele Parameter in der URL diese unlesbar. Betrachten wir den häufigen Fall eines Registrierungsformulars auf einer Website. Früher, als HTML-Seiten noch kein JavaScript enthielten, sendeten Browser die eingegebenen Informationen über eine POST-Anfrage. Dies wurde als „gepostete Werte“ bezeichnet;

In den Anfängen der Webprogrammierung war es also so:

  • Wurden GET-Methoden im Allgemeinen mit der Anforderung von Informationen von einem Webserver in Verbindung gebracht;
  • POST-Methoden wurden im Allgemeinen mit dem Senden von Informationen vom Browser an den Server in Verbindung gebracht. Der Server wurde dann durch diese Daten „angereichert“;

Seitdem ist JavaScript hinzugekommen. Während der Entwickler in den vorherigen Beispielen keine Kontrolle hatte (das Klicken auf einen Link löste unweigerlich einen GET-Aufruf aus, das Absenden eines Formulars unweigerlich einen POST-Aufruf), hat JavaScript ihm diese Kontrolle zurückgegeben. In diesem Modell ist die HTML-Seite mit JavaScript-Code verknüpft, der den Browser umgehen kann. So kann ein Klick auf einen Link vom JavaScript-Code abgefangen werden, der dann Code ausführen kann, der eine Anfrage an den Server sendet. Diese Anfrage ist für den Nutzer transparent. Er wird sie nicht sehen. Dieser Code fungiert als Web-Client, und genau wie bei Postman kann der Entwickler jede beliebige Anfrage erstellen. Um auf das Beispiel des Klickens auf einen Link zurückzukommen: Er kann eine POST-Anfrage ausführen, während der Browser standardmäßig eine GET-Anfrage ausgeführt hätte. Diese Entwicklungen haben die Unterschiede zwischen GET und POST weniger relevant gemacht.

Entwickler halten sich jedoch oft an folgende Regeln:

  • Eine GET-Anfrage darf den Zustand des Servers nicht verändern. Aufeinanderfolgende GET-Anfragen mit denselben Parametern in der URL müssen dasselbe Dokument zurückgeben. Darüber hinaus hat eine GET-Anfrage in der Regel keinen Body (kein zugehöriges Dokument), sondern nur Parameter in der URL;
  • Eine POST-Anfrage darf den Zustand des Servers verändern. Parameter werden meist im Request-Body gesendet. Diese werden als „gepostete Werte“ bezeichnet. Das Formularbeispiel ist am aussagekräftigsten: Die vom Benutzer eingegebenen Werte werden in den POST-Body gestellt, und der Server speichert sie an einem Ort, oft 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 vorherigen Client/Server-Beispielen funktionierte der Prozess wie folgt:

  • Der Client öffnet eine Verbindung zu Port 80 auf dem Webserver;
  • er sendet die Textsequenz: HTTP-Header, Leerzeile, [Dokument];
  • als Antwort sendet der Server eine Sequenz desselben Typs;
  • der Server schließt die Verbindung zum Client;
  • Der Client schließt 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 dies die erste Anfrage ist. Zwischen den Verbindungen „vergisst“ der Server seinen Client. Aus diesem Grund wird das HTTP-Protokoll als zustandsloses Protokoll bezeichnet. Für den Server ist es jedoch nützlich, sich an seine Clients zu erinnern. Wenn eine Anwendung beispielsweise sicher ist, sendet der Client dem Server einen Benutzernamen und ein Passwort, um sich zu authentifizieren. Würde der Server seinen Client zwischen den Verbindungen „vergessen“, müsste sich der Client bei jeder neuen Verbindung neu authentifizieren, was nicht praktikabel ist.

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

  1. Wenn ein Client eine erste Anfrage stellt, fügt der Server seiner Antwort eine Kennung hinzu, die der Client dann bei jeder neuen Anfrage zurücksenden muss. Anhand dieser Kennung, die für jeden Client einzigartig ist, kann der Server den Client erkennen. Er kann dann einen Cache für diesen Client in Form eines Caches verwalten, der eindeutig mit der Kennung des Clients verknüpft ist. So funktionieren beispielsweise PHP-Dienste;
  2. Wenn ein Client eine erste Anfrage stellt, fügt der Server seiner Antwort keine Kennung bei, sondern den Speicher des Benutzers selbst. Er speichert nichts auf der Serverseite. Um seinen Speicher zu erhalten, muss der Web-Client diesen Speicher mit jeder neuen Anfrage erneut senden. Dieser Speicher wird mit jeder neuen Anfrage geändert (oder auch nicht) und an den Client zurückgesendet (oder auch nicht). Dies ist die Methode, die vom Flask-Framework verwendet wird;

Die Unterschiede zwischen den beiden Methoden sind wie folgt:

  • Methode 1 verbraucht 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, wo der Speicher des Benutzers bei jeder Anfrage ausgetauscht wird und im Laufe mehrerer Anfragen anwachsen kann;
  • Methode 1 verbraucht mehr Speicherplatz. Dies liegt daran, dass der Server den Speicher des Benutzers auf seinen Dateisystemen speichert. Bei einer Million Benutzern könnte dies potenziell ein Problem darstellen. Bei Methode 2 wird nichts auf dem Server gespeichert;

Technisch gesehen funktioniert es bei beiden Methoden wie folgt:

  • In der Antwort an einen neuen Client fügt der Server den HTTP-Header [Set-Cookie: Key=ID] oder [Set-Cookie: memory] 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 – eine Kennung oder einen Speicherwert. Dies geschieht über den HTTP-Header [Cookie: Key=Value];

Man könnte sich fragen, woher der Server weiß, dass es sich um einen neuen und nicht um einen wiederkehrenden Client handelt. Das verrät ihm das Vorhandensein des HTTP-Cookie-Headers in den HTTP-Headern des Clients. Bei einem neuen Client fehlt dieser Header.

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

Der Server kann auch andere Arten von Speicher verwalten:

Image

  • in [1] ist der Speicher auf Anforderungsebene spezifisch. Er wird verwendet, wenn die Anforderung des Web-Clients nicht von einem einzelnen Dienst (oder einer einzelnen Anwendung), sondern von mehreren verarbeitet wird. Um Informationen an Dienst i+1 weiterzugeben, kann Dienst i die verarbeitete Anforderung mit diesen Informationen anreichern. Dies wird als Speicher auf Anforderungsebene bezeichnet. Wir werden diese Art von Speicher in diesem Dokument nicht verwenden;
  • in [2, 4] der soeben beschriebene Benutzerspeicher. Er kann lokal implementiert [2] oder über den Client verwaltet werden [4];
  • in [3] ist der Speicher auf „Anwendungsebene“ im Allgemeinen schreibgeschützt. Er wird von allen Benutzern gemeinsam genutzt. Er enthält oft 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 Anfragen eintreffen, wird es schwierig, in diesen Speicher zu schreiben. Wenn der Webserver mehrere Benutzer gleichzeitig bedient und zwei von ihnen versuchen, in den Speicher auf „Anwendungsebene“ zu schreiben, besteht die Gefahr, dass dieser Speicher beschädigt wird. Der Grund dafür ist, dass Benutzer 1 zwar mit dem Schreiben in den Speicher auf „Anwendungsebene“ begonnen hat, aber möglicherweise unterbrochen wird, bevor er fertig ist. Dies führt zu einem unvollständigen Anwendungsspeicher. Da dieser gemeinsam genutzt wird, kann Benutzer 2 ihn lesen und einen falschen Zustand erhalten;

22.6.2. Skript [session_scope_01]

Image

Die Skripte [session_scope_xx] veranschaulichen die Speicherverwaltung für Benutzer.

Das Skript [session_scope_01] lautet wie folgt:

#  configure the application
import config
config = config.configure()

#  dependencies
import json
from flask import Flask, make_response, session
from flask_api import status

#  flask application
app = Flask(__name__)

#  session secret key
app.secret_key = config["SECRET_KEY"]


@app.route('/set-session', methods=['GET'])
def set_session():
    #  put something in the session
    session['nom'] = 'séléné'
    #  send an empty response
    response = make_response()
    response.headers['Content-Length'] = 0
    return response, status.HTTP_200_OK


@app.route('/get-session', methods=['GET'])
def get_session():
    #  we retrieve the session and send the response
    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


#  hand only
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeile 11: Eine Flask-Anwendung wird instanziiert;
  • Zeile 14: Dem Attribut [secret_key] dieser Anwendung wird ein Wert zugewiesen, der aus der in den Zeilen 1–3 verwendeten Konfigurationsdatei stammt. Eine Flask-Sitzung ist nur möglich, wenn dieses Attribut initialisiert ist. Sie können darin beliebige Inhalte hinterlegen. Es wird verwendet, um einen Teil der „Benutzerdaten“ zu verschlüsseln, die an den Client gesendet werden. Wir geben in der Regel etwas ein, das schwer zu erraten ist. In der [config]-Datei ist der geheime Schlüssel wie folgt definiert:

    # on rend la config
    config = {
        # configuration Flask
        "SECRET_KEY": "vibnFfrdWYUp?*LQ"
    }
  • Zum ersten Mal definieren wir eine Webanwendung, die etwas anderes als die URL /
    • Zeile 17: Die URL [/set-session] wird verwendet, um die Sitzung des Benutzers zu initialisieren;
    • Zeile 27: Die URL [/get-session] wird verwendet, um die Sitzung des Benutzers abzurufen;
  • Zeile 20: Wir fügen etwas zur Sitzung des Benutzers hinzu, in diesem Fall einen Namen. Die Sitzung wird ähnlich wie ein Wörterbuch verwaltet. Man kann nicht einfach alles in die Sitzung einfügen. Die Werte, die man einfügt, müssen in JSON konvertiert werden können ( ). Bei den vordefinierten Typen von Python geschieht dies automatisch, ohne dass der Entwickler eingreifen muss . Bei benutzerdefinierten Objekten, die Python nicht erkennt, muss man die JSON-Konvertierung selbst durchführen;
  • Zeile 22: Wir erstellen eine HTTP-Antwort ohne Inhalt (es werden keine Parameter an `make_response` übergeben);
  • Zeile 23: Wir teilen dem Client mit, dass er ein leeres Dokument (0 Byte groß) erhalten wird;
  • Zeile 24: Wir senden die HTTP-Antwort an den Client. Die URL [/set-session] bewirkt daher nichts anderes als die Initialisierung einer Benutzersitzung;
  • Zeile 27: Über die URL [/get-session] kann der Benutzer einsehen, was sich in seiner Sitzung befindet;
  • Zeile 30: Wir erstellen eine HTTP-Antwort, die die JSON-Zeichenkette der Benutzersitzung enthält. Hier haben wir die JSON-Zeichenkette selbst erstellt, anstatt sie von Flask generieren zu lassen. Der Grund dafür ist, dass wir nicht möchten, dass Zeichen mit Akzenten maskiert werden (ensure_ascii=False);
  • Zeile 31: Wir teilen dem Client mit, dass wir JSON senden;
  • Zeile 32: Wir senden die HTTP-Antwort an den Client;

Der Zweck dieses Skripts ist es, zu demonstrieren, dass die Benutzersitzung es uns ermöglicht, die aufeinanderfolgenden Anfragen des Benutzers miteinander zu verknüpfen:

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

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

def configure():
    #  absolute path configuration relative path reference
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    #  application dependencies
    absolute_dependencies = [
        #  Person, Utilities, MyException
        f"{root_dir}/classes/02/entities",
    ]
    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  return the config
    config = {
        #  flask configuration
        "SECRET_KEY": "vibnFfrdWYUp?*LQ"
    }

    return config

Wir führen das Skript [session_scope_01] aus und verwenden anschließend Postman, um die URL [/set-session] abzufragen. Zuvor überprüfen wir einige Elemente der Anfrage, die gestellt wird:

Image

  • in [1] greifen wir auf die Cookies von Postman zu; Image
  • in [2–4] überprüfen wir die bekannten Cookies von Postman und löschen sie alle [4–5];

Sehen wir uns nun die HTTP-Anfrage an, die generiert wird:

Image

  • in [9]: einige der HTTP-Header, die Postman basierend auf der von uns vorgenommenen Konfiguration in die Anfrage einfügt. Mit dieser Überprüfung können Sie sicherstellen, dass Sie keine Parameter ausgelassen oder umgekehrt unnötige Parameter belassen haben;

Sobald dies erledigt ist, können wir die Anfrage ausführen:

Image

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

Image

  • in [1-2] die an den Webdienst gesendete Anfrage;
  • in [3-6] die HTTP-Header der Antwort;
  • in [4], da wir den Antworttyp im Code nicht angegeben haben, hat Flask den Standardtyp [text/html] verwendet;
  • in [5] weiß der Client, dass die Antwort kein Dokument enthält;
  • Zeile 6: Der [Set-Cookie]-Header wurde vom Flask-Server gesendet. Sein Wert wird als Session-Cookie bezeichnet. Er besteht aus drei Elementen:
    • [session=value]: value stellt die Sitzungsdaten des Benutzers in verschlüsselter Form dar. Diese Daten sind 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 Benutzer die empfangenen Daten jedoch nicht verändern und anschließend an den Server zurücksenden. Wenn der Server eine Sitzung empfängt, ist somit sichergestellt, dass er eine unverfälschte Sitzung erhält;
    • [HttpOnly]: Das Vorhandensein dieses Attributs teilt dem empfangenden Browser mit, dass das Cookie für kein JavaScript zugänglich sein darf, das die angezeigte Seite möglicherweise enthält;
    • [Path=/] ist der Pfad, für den das Sitzungs-Cookie zurückgesendet werden muss, was hier jeden Pfad innerhalb der Webanwendung bedeutet. Immer wenn der Benutzer explizit (durch Eingabe einer URL) oder implizit (durch Klicken auf einen Link) eine URL von dieser Domain anfordert, sendet der Browser automatisch das empfangene Sitzungs-Cookie zurück;

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

Image

  • In den HTTP-Headern [3-4] wird in [5] ein Session-Cookie angezeigt. Man könnte meinen, dass Postman ein Sitzungscookie in die Anfrage aufgenommen hat, aber das ist nicht der Fall. Die Header [3] stellen tatsächlich die HTTP-Header dar, die bei der nächsten Anfrage gesendet werden, so wie es derzeit konfiguriert ist. Postman hat gerade ein Sitzungscookie erhalten, das es bei der nächsten Anfrage zurücksenden wird. Deshalb haben wir [5];

Sie können den Client/Server-Dialog in der Postman-Konsole aufrufen, indem Sie Strg-Alt-C drücken:


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 Sitzungscookie;

Nun rufen wir die URL [/get-session] auf:

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 Sitzungs-Cookie an den Server zurückgesendet;
  • Zeile 18: die vom Server gesendete JSON-Zeichenkette;

Dieses Beispiel veranschaulicht mehrere Punkte:

  • Der Postman-Client sendet das vom Flask-Server empfangene Sitzungs-Cookie zurück. Webbrowser tun dies immer;
  • Wir sehen, dass Anfrage 2 [/get-session] Informationen abgerufen hat, die während Anfrage 1 [/set-session] erstellt wurden. Dies dient effektiv als Benutzerstatus;
  • Zeilen 11–16: Der Flask-Server hat kein Session-Cookie zurückgegeben. Dies ist nicht immer der Fall. Der Flask-Server gibt das Session-Cookie nur zurück, wenn die letzte Anfrage die Sitzung des Benutzers geändert hat;

22.6.3. Skript [session_scope_02]

Image

Das Skript [session_02] lautet wie folgt:

#  dependencies
import os

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

#  flask application
app = Flask(__name__)

#  session secret key
app.secret_key = os.urandom(12).hex()


#  Home URL
@app.route('/', methods=['GET'])
def index():
    #  we manage three meters
    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
    #  meter dictionary
    compteurs = {"n1": session['n1'], "n2": session['n2'], "n3": session['n3']}
    #  we send the answer
    response = make_response(compteurs)
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


#  hand
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 Zeichenfolge generiert. Beachten Sie, dass die Variable [app] die in Zeile 8 erstellte Instanz der Flask-Klasse ist;
  • Zeile 15: Diesmal gibt es nur eine Route, nämlich die Route „/“;
  • Zeilen 17–29: Wir verwalten eine Sitzung, die drei Zähler [n1, n2, n3] enthält. Beim ersten Aufruf des Benutzers ist [n1, n2, n3] = [0, 10, 100], und bei jedem folgenden Aufruf werden diese Zähler um 1 erhöht;
  • Zeile 18: Bei der ersten Anfrage ist die Anwendungssitzung leer. Der Ausdruck [session.get('key')] gibt den Wert [None] zurück. Bei nachfolgenden Anfragen gibt dieser Ausdruck den mit dem Schlüssel verknüpften Wert zurück;
  • Zeile 31: Diese Zähler werden in einem Wörterbuch abgelegt;
  • Zeile 33: Dieses Wörterbuch ist der HTTP-Antworttext. Beachten Sie, dass Flask Wörterbücher automatisch in JSON-Zeichenfolgen umwandelt;
  • Zeile 34: Dem Web-Client wird mitgeteilt, dass er JSON erhalten wird;
  • Zeile 35: Wir senden die HTTP-Antwort an den Client;

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

Image

In der Postman-Konsole sieht der Austausch zwischen Client und Server 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 Sitzungscookie;
  • in [18–22] die Antwort des Servers in Form einer JSON-Zeichenkette;

Führen wir dieselbe Anfrage ein zweites Mal durch. Die Protokolle ändern 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 Sitzungscookie zurück;
  • Zeile 15: In seiner Antwort sendet der Server ein neues Sitzungscookie, da die Anfrage des Clients den Status des Benutzers (= die Sitzung) geändert hat;
  • Zeilen 19–23: die neuen Zählerwerte;

22.6.4. Skript [session_scope_03]

Dieses neue Skript soll zeigen, dass verschiedene Python-Typen in einer Sitzung platziert werden können: Listen, Wörterbücher und Objekte. Die einzige Voraussetzung ist, dass Objekte, die in der Sitzung platziert werden, in JSON serialisierbar sein müssen. Wenn sie standardmäßig nicht serialisierbar sind (Listen, Wörterbücher), müssen Sie die Konvertierung in JSON selbst vornehmen.

#  configure the application
import config
config = config.configure()

#  dependencies
import json
import os

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

#  flask application
app = Flask(__name__)

#  session secret key
app.secret_key = os.urandom(12).hex()


#  Home URL
@app.route('/', methods=['GET'])
def index():
    #  list management
    liste = session.get('liste')
    if liste is None:
        #  1st request
        liste = [0, 10, 100]
    else:
        #  following requests
        for i in range(len(liste)):
            liste[i] += 1
    #  put the list back in the session
    session['liste'] = liste

    #  dictionary management
    dico = session.get('dico')
    if not dico:
        #  1st request
        dico = {"un": 0, "deux": 10, "trois": 100}
    else:
        #  following requests
        dico = session['dico']
        for key in dico.keys():
            dico[key] += 1
    #  put the dictionary back in the session
    session['dico'] = dico

    #  managing a person
    personne_json = session.get('personne')
    if personne_json is None:
        #  1st request
        personne = Personne().fromdict({"prénom": "aglaë", "nom": "séléné", "âge": 70})
    else:
        #  following requests
        personne = Personne().fromjson(personne_json)
        personne.âge += 1
    #  we put the person back in the session
    session['personne'] = personne.asjson()

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

    #  we send a jSON response
    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


#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeilen 1–3: Die Webanwendung wird konfiguriert;
  • Zeilen 5–11: 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. Wir haben Elemente darin abgelegt, die standardmäßig in JSON serialisierbar sind;
  • Zeilen 35–46: Verwaltung eines Dictionaries in der Session. Wir haben Elemente darin abgelegt, die standardmäßig in JSON serialisierbar sind;
  • Zeilen 48–58: Verwaltung einer Person. Ein [Person]-Objekt ist standardmäßig nicht in JSON serialisierbar. Daher müssen Vorsichtsmaßnahmen getroffen werden;
  • Zeile 58: Wir verwenden die Methode [BaseEntity.asjson], um die JSON-Zeichenkette der Person in der Sitzung zu speichern. Beachten Sie, dass wir auch [person.asdict] hätten verwenden können, da [person.asdict] ein Wörterbuch ist, das Werte enthält, die standardmäßig in JSON serialisierbar sind;
  • Zeile 55: Da wir eine JSON-Zeichenkette in der Sitzung gespeichert haben, rufen wir die Person daraus mit der Methode [BaseEntity.fromjson] ab;
  • Zeile 61: Wir erstellen das Wörterbuch [results], das als Antwort an den Client gesendet wird. Wir wissen, dass Flask in diesem Fall die JSON-Zeichenkette des Wörterbuchs sendet. Daher darf es nur Werte enthalten, die standardmäßig in JSON serialisierbar sind;
  • Zeile 64: Wir legen die JSON-Zeichenkette des [results]-Wörterbuchs in der HTTP-Antwort explizit fest. Flask hätte dies standardmäßig erledigt. Standardmäßig verwendet es jedoch den Parameter [ensure_ascii=True], was unseren Anforderungen nicht entsprach;
  • Zeile 65: Wir teilen dem Client mit, dass er JSON erhalten wird;
  • Zeile 66: Wir senden die Antwort an den Client;

Wir starten die Webanwendung. Wir löschen alle Cookies aus dem Postman-Client. Dann fordert der Client die 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 Sitzungscookie zurück;
  • Zeile 15: Der Server sendet ein neues zurück, da sich der Inhalt der Sitzung geändert hat (Zeile 19). Beachten Sie, dass dieser Inhalt in verschlüsselter Form im Sitzungscookie gespeichert wird;

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

22.7.1. Einleitung

Dieser Abschnitt soll zeigen, wie anwendungsweite Informationen, d. h. Informationen, die von allen Benutzern gemeinsam genutzt werden, verwaltet werden. Diese Informationen bestehen in der Regel aus Anwendungskonfigurationsdaten. Wir haben gesehen, dass eine Webanwendung verschiedene Arten von Speicher verwalten kann:

Image

Hier interessiert uns der Speicher der Anwendung [3].

22.7.2. Skript [application_scope_01]

Das Skript [application_scope_01] veranschaulicht eine Möglichkeit, Daten im Anwendungsbereich zu verwalten:

#  configure the application
import config
config = config.configure()

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

#  flask application
app = Flask(__name__)


#  Home URL
@app.route('/', methods=['GET'])
def index():
    #  we aim to show that the application remains in memory between requests from different clients
    #  every customer deals with the same application

    #  app_infos represents application-level information, not session-level information
    #  i.e. it concerns all users, not just one in particular
    #  this information is stored here in [config] (not mandatory)

    #  results dictionary
    résultats = {"config": config}

    #  we send the answer
    response = make_response(résultats)
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


#  hand
if __name__ == '__main__':
    #  check whether this code is executed several times
    print("application app lancée")
    #  launch the web application
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeilen 1–3: Wir rufen das Konfigurationswörterbuch ab. Wir werden zeigen, dass der Code außerhalb der Routing-Funktionen nur einmal ausgeführt wird. Die Flask-Anwendung verbleibt im Speicher. Alle außerhalb der Routen initialisierten Informationen sind für diese global und stehen ihnen daher zur Verfügung. Somit wird das [config]-Wörterbuch aus Zeile 3 von der Route / (Zeile 24) zurückgegeben. Wir werden zeigen, dass alle Web-Clients dasselbe Wörterbuch erhalten und dass es daher von allen Clients gemeinsam genutzt wird. Es handelt sich also um Informationen mit dem Geltungsbereich „Anwendung“;
  • Zeile 35: Wir fügen ein Log hinzu, um zu sehen, ob der Code in den Zeilen außerhalb der Routing-Funktion (Zeilen 1–10, 32–38) mehrfach ausgeführt wird;

Die Konfiguration [config] lautet wie folgt:

1
2
3
4
5
6
7
8
def configure():
    #  return the config
    config = {
        #  flask configuration
        "SECRET_KEY": "vibnFfrdWYUp?*LQ"
    }

    return config

Wir starten diese Anwendung. Die Protokolle in der PyCharm-Konsole lauten wie folgt:

Image

  • in [1], erster Start der Anwendung;
  • in [2], da wir den [Debug]-Modus angefordert haben, wird die Anwendung im [Debug]-Modus neu gestartet;

Nun geben wir in einem Browser (hier Chrome) die URL [http://127.0.0.1:5000/] ein:

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"
}

Kehren wir nun zur Pycharm-Konsole [Run] zurück:

Image

  • Die beiden Protokolleinträge [1, 2] sind noch vorhanden, aber es gibt keine weiteren, obwohl wir die drei vom Webserver empfangenen Anfragen sehen können;

Um ganz sicherzugehen, dass die Anwendung nicht bei jeder neuen Anfrage neu geladen wird, können wir der Konfiguration einen Zähler hinzufügen und diesen bei jeder neuen Anfrage erhöhen. Wir werden dann sehen, dass jeder Client den Zähler in dem Zustand sieht, den der vorherige Client hinterlassen hat. Es ist jedoch wichtig zu beachten, dass Clients keine Daten im Anwendungsbereich ändern sollten, da diese von allen Clients gemeinsam genutzt werden. In einem Szenario, in dem der Server mehrere Clients gleichzeitig bedient und keine Garantie dafür besteht, dass die Anfrage eines Clients vollständig und ohne Unterbrechung ausgeführt wird, kann ein Client 1, der eine Anfrage 1 gesendet hat, die vor der Fertigstellung unterbrochen wurde, die gemeinsam genutzten Daten für nachfolgende Clients in einem beschädigten Zustand hinterlassen.

22.7.3. Skript [application_scope_02]

Image

Das Skript [application_scope_02] tut genau das, was es nicht tun sollte: Es erlaubt Clients, Informationen zu ändern, die mit anderen Benutzern geteilt werden. Wir werden einen Zähler unter den Benutzern teilen, die ihn inkrementieren. Wir werden sehen, dass jeder Benutzer die Änderungen am Zähler sehen kann, die von anderen Benutzern vorgenommen wurden.

Das Skript lautet wie folgt:

#  dependencies

from flask import Flask, make_response
from flask_api import status

#  flask application
app = Flask(__name__)

#  application scope data
config = {
    "counter": 0
}


#  Home URL
@app.route('/', methods=['GET'])
def index():
    #  we aim to show that the [config] dictionary is shared by all clients
    #  web application

    #  increment the counter
    config["counter"] += 1
    #  we send the answer
    response = make_response(config)
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeilen 10–12: das von allen Benutzern gemeinsam genutzte [config]-Wörterbuch. Es enthält einen Zähler;
  • Zeile 22: Jedes Mal, wenn ein Benutzer die URL / aufruft, wird der Konfigurationszähler erhöht;
  • Zeilen 23–26: Die JSON-Zeichenkette des Wörterbuchs wird an jeden Client gesendet;

Wir führen dieses Skript aus. Anschließend rufen wir die URL [http://127.0.0.1:5000/] mit einem ersten Browser auf:

Image

Anschließend wiederholen wir den Vorgang mit einem zweiten Browser:

Image

Anschließend ein drittes Mal mit Postman:

Image

Wir sehen, dass jeder Client den Zähler in dem Zustand abruft, in dem der vorherige Client ihn hinterlassen hat. Sie haben also Zugriff auf dieselben Informationen.

22.7.4. Skript [application_scope_03]

Das Skript [application_scope_03] veranschaulicht, warum Informationen, die zwischen Benutzern geteilt werden, schreibgeschützt sein müssen.

Image

Das Skript lautet wie folgt:

#  dependencies
import threading
from time import sleep

from flask import Flask, make_response
from flask_api import status

#  flask application
app = Flask(__name__)

#  application scope data
config = {
    "counter": 0
}


#  Home URL
@app.route('/', methods=['GET'])
def index():
    #  we aim to show that the [config] dictionary is shared by all clients
    #  of the web application and must be read-only

    #  thread name
    thread_name = threading.current_thread().name
    #  read the counter
    counter = config["counter"]
    print(f"compteur lu : {counter}, par le thread {thread_name}")
    #  we stop for 5 seconds - so other customers will be served
    sleep(5)
    #  increment the configuration counter
    config["counter"] = counter + 1
    #  log
    print(f"compteur écrit : {config['counter']}, par le thread {thread_name}")
    #  we send the answer
    response = make_response(config)
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    return response, status.HTTP_200_OK


#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • Zeile 43: Wir haben den Ausführungsmodus der Webanwendung geändert. Wir haben [threaded=True] geschrieben, um anzugeben, dass die Anwendung mehrere Benutzer gleichzeitig bedienen soll. Dies geschieht mithilfe von Ausführungsthreads:
    • Es kann mehrere gleichzeitig laufende Ausführungsthreads geben, von denen 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: Wir rufen den Namen des Threads ab, der die Funktion [index] ausführt;
  • Zeile 26: Der Zählerwert wird gelesen. Für die Zwecke unserer Demonstration gliedern wir die Zählersteigerung wie folgt auf:
    • Schritt 1: Thread 1 liest den Zähler (zum Beispiel 1);
    • Schritt 2: Thread 1 pausiert für 5 Sekunden (Zeile 29). Da Thread 1 eine Pause angefordert hat, wird der Prozessor an einen anderen Thread, Thread 2, übergeben. Das Ziel ist, dass dieser neue Thread denselben Zählerwert (=1) liest. Dann pausiert auch er für 5 Sekunden und verliert den Prozessor;
    • Schritt 3: Inkrementiere den Zähler, Zeile 31, basierend auf dem in Schritt 1 gelesenen Wert (=1). Thread 1 ist der erste, der dies tut: Er setzt den Zähler auf 2 und beendet dann die Ausführung der Funktion [index]. Dann ist Thread 2 an der Reihe, zu aktivieren und den Zähler ebenfalls auf 2 zu setzen, basierend auf dem in Schritt 1 gelesenen Wert (=1). Letztendlich steht der Zähler, nachdem beide Threads gelaufen sind, auf 2, obwohl er auf 3 stehen sollte;
  • Zeile 33: Wir zeigen den Zählerwert zur Überprüfung an;

Wir führen das Skript aus und rufen dann die URL [http://loaclhost:5000/] mit zwei Browsern und anschließend mit Postman auf. Die Protokolle in der PyCharm-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/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/ (Press CTRL+C to quit)
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 aus dem Zähler;
  • Zeile 11: Thread 2 setzt den Zähler auf 1;
  • Zeile 13: Thread 4 erhöht den Zähler auf 1. Ab diesem Zeitpunkt ist der Zählerwert falsch;
  • Zeilen 15–16: Thread 5 wird nicht unterbrochen und verarbeitet den Zählerwert korrekt;

Die wichtigste Erkenntnis aus diesem Beispiel ist, dass der Code einer Webanwendung den Wert von Informationen, die von Benutzern geteilt werden, nicht verändern darf.

22.8. Skripte [flask/07]: Routenverwaltung

Image

Hier konzentrieren wir uns auf die Verwaltung der Routen einer Anwendung, d. h. der von der Webanwendung bereitgestellten URLs.

22.8.1. Skript [main_01]: Konfigurierte Routen

Das Skript [main_01] stellt die Möglichkeit vor, Routen zu konfigurieren:

from flask import Flask, make_response
from flask_api import status

#  flask application
app = Flask(__name__)


#  reply sent
def send_plain_response(réponse: str):
    #  we send the answer
    response = make_response(réponse)
    response.headers['Content-Type'] = 'text/plain; charset=utf-8'
    return response, status.HTTP_200_OK


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


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


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


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


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


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


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


#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeile 17: Wir geben den Typ der URL-Parameter an. Dies ermöglicht es Flask, Validierungen durchzuführen. Wenn der Parameter nicht den erwarteten Typ hat, wird die Anfrage des Clients abgelehnt (400 Bad Request-Fehler). Flask übernimmt also einen Teil der Arbeit, die wir sonst hätten erledigen müssen;
  • Zeile 18: Für die Parameter müssen wir die exakten Namen der Parameter aus Zeile 17 verwenden, 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 an den Client zu sendende Zeichenkette;
  • Zeile 11: Der Hauptteil der HTTP-Antwort wird aufgebaut;
  • Zeile 12: Wir teilen dem Client mit, dass wir Klartext senden;
  • Zeile 13: Die HTTP-Antwort wird gesendet;
  • Zeilen 23–62: zusätzliche konfigurierte Routen, die später in einer Anwendungsübung verwendet werden;

Wir führen das Skript aus und fragen es mit dem Postman-Client ab:

Image

22.8.2. Skript [main_02]: Auslagerung von Routen

Im vorherigen Skript [main_01] kann der Code bei vielen Routen recht lang werden. Das Skript [main_02] zeigt, wie man die Routen externalisiert.

Image

Das Skript [routes_02] fasst die Funktionen zusammen, die mit den Routen aus dem vorherigen Skript verbunden sind:

from flask import make_response
from flask_api import status


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


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


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


#  authenticate-user
def authentifier_utilisateur():
    #  answer
    return send_response("/authentifier-utilisateur")


#  calculate-tax
def calculer_impot():
    #  answer
    return send_response("/calculer-impot")


#  lister-simulations
def lister_simulations():
    #  answer
    return send_response("/lister-simulations")


#  delete-simulation
def supprimer_simulation(numero: int):
    #  answer
    return send_response(f"/supprimer-simulation/{numero}")


#  end of session
def fin_session():
    #  answer
    return send_response(f"/fin-session")

Beachten Sie, dass das Skript [routes_02] kein Routen-Skript ist. Es handelt sich um eine Liste von Funktionen. Das Hauptskript [main_02] verknüpft die Routen mit den Funktionen:

from flask import Flask

#  route functions are deported to their own script
import routes_02

#  flask application
app = Flask(__name__)

#  route/function associations
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)

#  hand
if __name__ == '__main__':
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • Zeile 4: Importiere das Skript, das die mit den Routen verbundenen Funktionen enthält;
  • 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 entsprechen denen, die mit dem vorherigen Skript [main_01] erzielt wurden.