Simple REST-Anwendung mit Python und Flask

Neuerdings widme ich mich verstärkt der Automatisierung von komplexen Programmabläufen und habe mich zu Lernzwecken auch mit der Entwicklung einer eigenen REST-Anwendung beschäftigt. Daraus ist ein kleines Tutorial entstanden, welches alle Stufen der Entwicklung einer kleinen "RESTful" Anwendung abdeckt:

  1. Konzeption/Definition
  2. Entwicklung
  3. Testen und Dokumentieren

Aber zuerst mal zu den Grundlagen...

Crashkurs: REST in Peace

REST wurde zur Kommunikation zwischen Anwendungsdiensten konzipiert und soll so eine leicht zu verwendende Alternative zu SOAP (Simple Object Access Protocol), WSDL (Web Services Description Language) und co. darstellen.

Ein besonderes Kommunikationsmerkmal ist, dass REST stets zustandslos ist. Das bedeutet, dass jede Nachricht alle benötigten Informationen enthält und somit in sich abgeschlossen ist. Eine sauber implementierte REST-Anwendung muss also keine weiteren benötigten Parameter in einem weiteren Aufruf anfordern oder nachladen. Diese Zustandslosigkeit kommt vor allem der Skalierbarkeit zugute - anspruchsvolle Anwendungen können sich so über mehrere Server hinter einem Loadbalancer erstrecken. Da jede Nachricht in sich abgeschlossen ist, können aufeinanderfolgende Nachrichten von mehreren Servern abgearbeitet werden - ohne, dass dies für den Anwender ersichtlich ist.

Erfahrungsgemäß werden REST-Dienste vor allem in JSON oder XML dargestellt. Diese eignen sich zur Integration in weitere "maschinengesteuerte" Applikationen, da sie leicht automatisiert ausgelesen und verarbeitet werden können. Hinsichtlich der Datenübertragung kommen HTTP oder bevorzugt - da REST-Anwendungen in der Regel keine eigene Verschlüsselung implementieren - HTTPS zum Einsatz.

Ein weiteres Merkmal von REST ist die Anwendung des KISS-Prinzips. Anwendungen sollen so simpel wie möglich gestaltet werden - dies erstreckt sich vom selbsterklärenden URI-Schema bis hin zur Menge an Parametern. Um die Anwendung zu erleichtern, ist es empfehlenswert, sich auf das Minimum zu beschränken.

Für den Rest des Artikels orientieren wir uns an einem kleinen Beispiel - einer Anwendung, die Benutzer mit einer eindeutigen ID, sowie Benutzernamen und E-Mail-Adresse speichert.

URI-Konvention

REST-Anwendungen verwenden leicht zu merkende URIs, die die Funktion des Aufrufs treffend umschreiben und den Zusammenhang zwischen Ressourcen klar darstellen. Dabei ist es auch möglich, alternative API-Versionen einer REST-Schnittstelle anzugeben. Einige Beispiele:

URIBeschreibung
http://server/api/erer/1234Bezeichnet eine konkrete Ressource (bestimmter Benutzer anhand der ID)
http://server/api/v2/user/6667Bezeichnet eine konkrete Ressource, spezielle API-Version wird verwendet
http://server/api/userBezeichnet eine Ressourcen-Struktur ohne konkrete Ressource, liefert i.d.r. alle Ressourcen (hier: alle Benutzer)
http://server/api/user/1337/address/2Bezeichnet eine konkrete Unterressource einer eindeutigen Ressource (hier: zweite Adresse eines Benutzers)

Diese URIs lassen sich einfach merken - und so sollte es auf keinen Fall aussehen:

URIBeschreibung
http://server/api/user/get?id=1234Verwendung von GET-Parametern und Aktionen als Parameter; Kätzchen werden sterben!
http://server/api/?version=2&get=6667
http://server/api/create?type=user&id=1337&name=paul
http://server/api/action=getaddr&user=1337

Befehle

URIs lassen sich mehrfach belegen, sofern unterschiedliche HTTP-Befehle zum Einsatz kommen. Unter anderem werden die folgenden Befehle in REST verwendet:

BefehlBeschreibung
GETLiest Informationen zu einer Ressource aus
POSTErstellt eine neue Ressource oder fügt zu einer Ressource eine neue Unter-Ressource hinzu
PUTAktualisieren einer bestehenden Ressource
DELETELöschen einer vorhandenen Ressource
PATCHEin Teil einer Ressource wird geändert, beispielsweise ein einzelnes Attribut
HEADLiefert Header-Informationen über eine Ressource aus - meistens analog zu GET, jedoch ohne wirklichen Inhalt

PUT und PATCH scheinen auf den ersten Blick die gleichen Dinge zu tun. Nehmen wir zur Verdeutlichung wieder oben stehendes Beispiel. Soll die E-Mail-Adresse eines bestehenden Benutzers verändert werden, wäre PATCH der richtige Aufruf. Sollen jedoch mehrere Felder aktualisiert werden (beispielsweise die E-Mail-Adresse und der Benutzername), wäre es sinnvoller, PUT zu verwenden.

Weitere Informationen finden sich auf folgender Webseite: [REST API Tutorial].

Flask

Flask ist ein Micro-Framework für Python zur Implementation von Web-Anwendungen. Es ist simpel zu implementieren und eignet sich deswegen hervorragend für kleine Anwendungen wie in diesem Beispiel. Eine einfache Flask-Anwendung lässt sich schon in 5 Zeilen definieren:

1from flask import Flask
2app = Flask(__name__)
3
4@app.route("/")
5def hello():
6  return "Hello World!"

Hier wird lediglich für den Aufruf der Hauptseite die Ausgabe des altbekannten "Hello Worlds" definiert.

Starten lässt sich die Anwendung wie folgt:

1$ ./hello.py
2 * Running on http://localhost:5000/

Alternativ ist auch folgender Aufruf möglich:

1$ FLASK_APP=hello.py flask run
2 * Running on http://localhost:5000/

Weitere Beispiele gibt es in der Projekt-Dokumentation.

Beispiel-Anwendung

Wir erinnern uns an das oben genannte Beispiel und die erwähnten REST-Aufrufe. Die folgenden Befehle müssen in der Anwendung implementiert werden:

URLBefehlBeschreibung
/api/user/POSTErstellt einen Benutzer; Parameter per JSON
/api/user/<id>GETLiest Benutzerinformationen aus
PUTAktualisiert einen Benutzer; Parameter per JSON
DELETELöscht einen Benutzer

Beim Erstellen und Aktualisieren von Benutzern müssen Parameter über JSON übergeben werden: die ID, ein Benutzername und eine E-Mail-Adresse. In weiser Voraussicht, dass die Anwendung in Zukunft mehrere Einträge auf einmal erstellen und anpassen könnte, ist es ratsam, die Parameter nicht einzeln zu übergeben. Sinnvoller ist es, die Parameter als eindeutiges Element (item) zu bündeln, diese könnte man dann in einem Array weiter bündeln und an die Anwendung übergeben. Somit wäre es möglich, mehrere Benutzer auf einmal anzulegen.

Ein Parameter-Beispiel würde wie folgt aussehen:

1{ "item": {"id": 1, "name": "Simone Giertz", "mail": "giertz@shittyrobots.loc"} }

Oder in grafischer Form:

item
id1
nameSimone Giertz
mailgiertz@shittyrobots.loc

Das Erstellen, Anpassen oder Löschen von Benutzern soll mit einer kurzen Nachricht bestätigt werden. Neben einem Returncode (0 = erfolgreich, 1 = nicht erfolgreich) soll auch eine kurze Status-Meldung angezeigt werden (SUCCESS oder FAILURE). Diese Information könnte eine Client-Anwendung später auswerten, um den Benutzer entsprechend zu informieren.

Anwendung

Vorab sei darauf hingewiesen, dass der Quellcode dieser Anwendung auf meinem GitHub-Profil zu finden ist. Ich werde in diesem Artikel nicht den gesamten Quellcode kommentieren, sondern lediglich sektionsweise auf Besonderheiten hinweisen. Der vollständige Quellcode ist auf GitHub entsprechend dokumentiert.

Für jeden API-Aufruf werden einzelne Funktionen erstellt und mithilfe einer Flask-Route mit einer URI verknüpft - beispielsweise für das Auflisten eines Benutzers:

1@app.route("/api/user/<int:user_id>", methods=["GET"])
2def user_show(user_id):
3  """
4  This function shows a particular user.
5  """
6  # return a particular user
7  print("Retrieve user #{}".format(user_id))
8  result = user_get(user_id)
9  return Response(json.dumps(result), mimetype="application/json")

Die Funktion user_show() kümmert sich um die Anzeige eines Benutzers; sie wird ausgelöst, wenn ein GET-Aufruf (mit dem methods-Parameter können die HTTP-Befehle, auf die reagiert werden sollen, definiert werden) auf die URL /api/user/ erfolgt. Wichtig ist hierbei, dass der URL eine Ganzzahl (<int:user_id>) folgen muss - diese wird dann in der Variablen user_id zwischengespeichert. Die Funktion user_get() erhält diese Variable als Parameter und liest daraufhin den entsprechenden Benutzer aus einer SQLite-Datenbank aus. Anschließend übergibt die Anwendung die bezogenen Informationen als JSON-Antwort. Hierzu implementiert Flask einen eigenen Objekttyp Response, der mit einem Mimetype näher spezifiziert werden kann - in diesem Fall wird application/json definiert.

Etwas komplexer ist das Erstellen von Benutzern - hier sieht der Code wie folgt aus:

 1@app.route("/api/user", methods=["POST"])
 2def user_add():
 3  """
 4  This function creates a new user.
 5  """
 6  # execute and return result
 7  json_data = get_data(request.data)
 8  print("Create user #{}".format(json_data["item"]["name"]))
 9  result = user_create(
10  json_data["item"]["id"], json_data["item"]["name"],
11  json_data["item"]["mail"])
12  return Response(return_result(result), mimetype="application/json")

Die Funktion erfordert den POST-Befehl. Die Funktion get_data() bezieht die übermittelten Informationen und deserialisiert diese. Wir erinnern uns - JSON-Daten stellen im Wesentlichen Objekte mit dazugehörigen Informationen dar, die in einen JavaScript-konformen String "zerlegt" wurden. Die Funktion ist recht simpel, sie wird im Quellcode wie folgt definiert:

 1def get_data(data):
 2  """
 3  This function deserializes an JSON object.
 4
 5  :param data: JSON data
 6  :type data: str
 7  """
 8  json_data = json.loads(data)
 9  print("Deserialized data: {}".format(data))
10  return json_data

Die Funktion macht also nichts anderes als den String wieder in ein Objekt zu verwandeln und dieses anschließend zu übergeben. Weiter im Text: nachdem die Eingaben als Objekt entgegengenommen wurden werden diese der Funktion user_create() übergeben. Diese Funktion dient dazu, den Benutzer in der Datenbank anzulegen - als Return-Code wird entweder ein True (erfolgreich) oder False (fehlerhaft) übergeben. Dieser Wert wird anschließend an die Funktion return_result() übergeben. Diese wiederum hat lediglich die Aufgabe, Return-Code und Statusmeldung je nach True oder False zu definieren:

 1def return_result(result):
 2  """
 3  This function simply returns an operation's status in JSON.
 4
 5  :param result: boolean whether successful
 6  :type result: bool
 7  """
 8  ret = {}
 9  if result:
10    ret["code"] = 0
11    ret["message"] = "SUCCESS"
12  else:
13    ret["code"] = 1
14    ret["message"] = "FAILURE"
15  return json.dumps(ret)

Das Resultat ist ein Dictionary, welches den Return-Code und die Statusmeldung enthält - dieses wird nun per Flask-Aufruf als JSON-Antwort zurückgesendet.

Der vollständige Quellcode auf meinem GitHub-Profil wurde auch um eine Web-Oberfläche erweitert, die dazu verwendet werden kann, Benutzer zu verwalten.

Postman

Postman

Wenn es darum geht, API-Schnittstellen zu testen, ist Postman der de facto-Standard. Der stark-anpassbare HTTP-Client, der für alle nennenswerte Plattform erhältlich ist, liefert zahlreiche Möglichkeiten, um das Generieren von Webserver-Anfragen zu erleichtern - um einige Beispiele zu nennen:

  • Benutzerfreundliches Generieren von Anfragen durch Klicken und Auswählen benötigter Parameter (Header-Informationen, Body,...)
  • Speichern, Gruppieren und Dokumentieren von Anfragen
  • Syntax-Highlighting
  • Automatisiertes Testen von ganzen API-Abläufen

Postman kann auf der Projekt-Webseite kostenlos heruntergeladen werden.

Nachdem wir nun einige API-Aufrufe implementiert haben, ist es Zeit diese zu überprüfen. Auf GitHub habe ich einen entsprechenden Postman-Katalog vorbereitet - im Wesentlichen besteht dieser ausfolgenden Aufrufen:

URIBefehlBeschreibung
/api/user/0GETAnzeigen aller Benutzer
/api/user/1GETAnzeigen eines Benutzers anhand der ID
/api/userPOSTErstellen eines neuen Benutzers (Parameter per JSON)
/api/user/1PUTAktualisieren aller Angaben eines Benutzers (Parameter per JSON)
/api/user/1337DELETEEntfernen eines Benutzers anhand der ID

Beim Starten der jeweiligen Befehle sollten entsprechende Ausgaben in Postman angezeigt werden, beispielsweise beim Anzeigen eines Benutzers:

1{
2 "results": [
3 {
4 "mail": "giertz@shittyrobots.loc",
5 "id": 1,
6 "name": "Simone Giertz"
7 }
8 ]
9}

Ein anderes Beispiel: Löschen eines Benutzers:

1{
2 "message": "SUCCESS",
3 "code": 0
4}

JSON-Parser

Wenn wir alles richtig gemacht haben, liefern die oben genannten Aufrufe entsprechende Informationen als JSON zurück. Diese mögen in diesem Beispiel noch recht einfach zu lesen sein - bei komplexeren Anwendungen ist dies jedoch nicht mehr der Fall. In diesem Fall kann es helfen, einen grafischen JSON-Parser zu verwenden, wenn Probleme nachvollzogen werden sollen. Im Internet gibt es zahlreiche Online-Parser, beispielsweise JSON Editor Online oder JSON Parser Online. Wenn hier die Ausgaben unserer Anwendung eingefügt werden, können diese grafisch dargestellt werden:

JSON Parser

Mit Tools wie JSONMate.com können sogar Hierarchien abgebildet werden - mit steigender Komplexität nimmt der Nutzen hiervon jedoch deutlich ab:

Ausblick

Wir haben soeben unsere erste REST-Anwendung entwickelt - Wahnsinn! 🙂

Natürlich gibt es noch zahlreiche Möglichkeiten, das Anwendungsbeispiel zu optimieren. So könnte man beispielsweise mehr Informationen hinzufügen (Adresse, Telefonnummer,...) oder die Anwendung etwas intelligenter gestalten. Die Angabe der Benutzer-ID ist seitens der Datenbank eine optionale Angabe - es kann auch eine fortlaufende Nummer verwendet werden; wird ein Benutzer über die API angelegt ist die Angabe einer ID jedoch zwingend notwendig.

Mit Swagger gibt es ein sehr interessantes Framework, um das Entwickeln von APIs und Clients zu vereinfachen. Hier werden lediglich Aufrufe, sowie Inhalte und Werte definiert, die konkrete Implementation in Python oder anderen Sprachen übernimmt das Toolkit. Swagger kommt bei vielen Open Source-Projekten und auch in kommerziellen Produkten (wie dem VMware vRealize Orchestrator) zum Einsatz. Vielleicht widme ich mich in einem der nächsten Artikel diesem Thema.. 🙂

Übersetzungen: