Erstellen einer Rest-API mit dem Bottle Framework

Veröffentlicht: 2022-03-11

REST-APIs sind zu einer gängigen Methode geworden, um eine Schnittstelle zwischen Web-Back-Ends und -Front-Ends sowie zwischen verschiedenen Webdiensten einzurichten. Die Einfachheit dieser Art von Schnittstelle und die allgegenwärtige Unterstützung der HTTP- und HTTPS-Protokolle über verschiedene Netzwerke und Frameworks hinweg machen sie zu einer einfachen Wahl, wenn es um Fragen der Interoperabilität geht.

Bottle ist ein minimalistisches Python-Webframework. Es ist leichtgewichtig, schnell und benutzerfreundlich und eignet sich gut zum Erstellen von RESTful-Diensten. Ein von Andriy Kornatskyy durchgeführter Bare-Bones-Vergleich platzierte es in Bezug auf Reaktionszeit und Durchsatz (Anfragen pro Sekunde) unter den Top-3-Frameworks. In meinen eigenen Tests auf den von DigitalOcean erhältlichen virtuellen Servern fand ich heraus, dass die Kombination aus dem uWSGI-Server-Stack und Bottle einen Overhead von nur 140 μs pro Anfrage erreichen konnte.

In diesem Artikel gebe ich eine exemplarische Vorgehensweise zum Erstellen eines RESTful-API-Dienstes mit Bottle.

Flasche: Ein schnelles und leichtes Python-Webframework

Installation und Konfiguration

Das Flaschengerüst erreicht seine beeindruckende Leistung unter anderem durch sein geringes Gewicht. Tatsächlich wird die gesamte Bibliothek als Ein-Datei-Modul vertrieben. Das bedeutet, dass es Ihnen nicht so sehr in die Hände fällt wie andere Frameworks, aber es ist auch flexibler und kann so angepasst werden, dass es in viele verschiedene Tech-Stacks passt. Bottle eignet sich daher am besten für Projekte, bei denen Leistung und Anpassbarkeit im Vordergrund stehen und bei denen die zeitsparenden Vorteile schwererer Rahmen weniger in Betracht gezogen werden.

Die Flexibilität von Bottle macht eine ausführliche Beschreibung der Einrichtung der Plattform etwas sinnlos, da sie möglicherweise nicht Ihren eigenen Stack widerspiegelt. Ein kurzer Überblick über die Optionen und wo Sie mehr darüber erfahren können, wie Sie sie einrichten, ist jedoch hier angebracht:

Installation

Die Installation von Bottle ist so einfach wie die Installation jedes anderen Python-Pakets. Ihre Optionen sind:

  • Installieren Sie auf Ihrem System mit dem Paketmanager des Systems. Debian Jessie (aktuell stabil) verpackt die Version 0.12 als python-bottle .
  • Installieren Sie auf Ihrem System mithilfe des Python-Paketindex mit pip install bottle .
  • Installation in einer virtuellen Umgebung (empfohlen).

Um Bottle in einer virtuellen Umgebung zu installieren, benötigen Sie die Tools virtualenv und pip . Um sie zu installieren, lesen Sie bitte die virtualenv- und Pip-Dokumentation, obwohl Sie sie wahrscheinlich bereits auf Ihrem System haben.

Erstellen Sie in Bash eine Umgebung mit Python 3:

 $ virtualenv -p `which python3` env

Das Unterdrücken des Parameters -p `which python3` führt zur Installation des auf dem System vorhandenen Standard-Python-Interpreters – normalerweise Python 2.7. Python 2.7 wird unterstützt, aber dieses Tutorial geht von Python 3.4 aus.

Aktiviere nun die Umgebung und installiere Bottle:

 $ . env/bin/activate $ pip install bottle

Das ist es. Die Flasche ist installiert und einsatzbereit. Wenn Sie mit virtualenv oder pip nicht vertraut sind, ist deren Dokumentation erstklassig. Schau mal! Sie sind es wert.

Server

Bottle entspricht Pythons standardmäßigem Web Server Gateway Interface (WSGI), was bedeutet, dass es mit jedem WSGI-kompatiblen Server verwendet werden kann. Dazu gehören uWSGI, Tornado, Gunicorn, Apache, Amazon Beanstalk, Google App Engine und andere.

Die korrekte Einrichtung variiert leicht mit jeder Umgebung. Bottle macht ein Objekt verfügbar, das der WSGI-Schnittstelle entspricht, und der Server muss für die Interaktion mit diesem Objekt konfiguriert werden.

Um mehr darüber zu erfahren, wie Sie Ihren Server einrichten, lesen Sie hier die Dokumentation des Servers und die Dokumentation von Bottle.

Datenbank

Bottle ist datenbankunabhängig und kümmert sich nicht darum, woher die Daten kommen. Wenn Sie eine Datenbank in Ihrer App verwenden möchten, bietet der Python-Paketindex mehrere interessante Optionen, wie SQLAlchemy, PyMongo, MongoEngine, CouchDB und Boto für DynamoDB. Sie benötigen nur den passenden Adapter, um es mit der Datenbank Ihrer Wahl zum Laufen zu bringen.

Flaschen-Framework-Grundlagen

Sehen wir uns nun an, wie man eine einfache App in Bottle erstellt. Für Codebeispiele gehe ich von Python >= 3.4 aus. Das meiste, was ich hier schreibe, funktioniert jedoch auch mit Python 2.7.

Eine einfache App in Bottle sieht so aus:

 import bottle app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)

Wenn ich grundlegend sage, meine ich, dass dieses Programm Ihnen nicht einmal „Hallo Welt“ sagt. (Wann haben Sie das letzte Mal auf eine REST-Schnittstelle zugegriffen, die mit „Hello World“ geantwortet hat?) Alle HTTP-Anforderungen an 127.0.0.1:8000 erhalten den Antwortstatus 404 Not Found.

Apps in der Flasche

Mit Bottle können mehrere Instanzen von Apps erstellt werden, aber der Einfachheit halber wird die erste Instanz für Sie erstellt. das ist die Standard-App. Bottle hält diese Instanzen in einem internen Stack des Moduls. Immer wenn Sie etwas mit Bottle tun (z. B. die App ausführen oder eine Route anhängen) und nicht angeben, von welcher App Sie sprechen, bezieht sich dies auf die Standard-App. Tatsächlich muss die Zeile app = application = bottle.default_app() in dieser Basis-App nicht einmal vorhanden sein, aber sie ist vorhanden, damit wir die Standard-App einfach mit Gunicorn, uWSGI oder einem generischen WSGI-Server aufrufen können.

Die Möglichkeit mehrerer Apps mag zunächst verwirrend erscheinen, aber sie verleihen Bottle Flexibilität. Für verschiedene Module Ihrer Anwendung können Sie spezielle Bottle-Apps erstellen, indem Sie andere Bottle-Klassen instanziieren und sie nach Bedarf mit unterschiedlichen Konfigurationen einrichten. Auf diese verschiedenen Apps könnte über verschiedene URLs über den URL-Router von Bottle zugegriffen werden. Wir werden in diesem Tutorial nicht darauf eingehen, aber Sie werden ermutigt, sich die Dokumentation von Bottle hier und hier anzusehen.

Serveraufruf

Die letzte Zeile des Skripts führt Bottle unter Verwendung des angegebenen Servers aus. Wenn kein Server angegeben ist, wie hier der Fall, ist der Standardserver Pythons eingebauter WSGI-Referenzserver, der nur für Entwicklungszwecke geeignet ist. Ein anderer Server kann wie folgt verwendet werden:

 bottle.run(server='gunicorn', host = '127.0.0.1', port = 8000)

Dies ist syntaktischer Zucker, mit dem Sie die App starten können, indem Sie dieses Skript ausführen. Wenn diese Datei beispielsweise main.py heißt, können Sie einfach python main.py , um die App zu starten. Bottle führt eine ziemlich umfangreiche Liste von Serveradaptern, die auf diese Weise verwendet werden können.

Einige WSGI-Server haben keine Flaschenadapter. Diese können mit servereigenen Run-Kommandos gestartet werden. Auf uWSGI müssten Sie zum Beispiel nur uwsgi so aufrufen:

 $ uwsgi --http :8000 --wsgi-file main.py

Eine Anmerkung zur Dateistruktur

Bottle überlässt die Dateistruktur Ihrer App ganz Ihnen. Ich habe festgestellt, dass sich meine Dateistrukturrichtlinien von Projekt zu Projekt weiterentwickeln, aber eher auf einer MVC-Philosophie basieren.

Aufbau Ihrer REST-API

Natürlich braucht niemand einen Server, der für jede angeforderte URI nur 404 zurückgibt. Ich habe Ihnen versprochen, dass wir eine REST-API erstellen würden, also machen wir es.

Angenommen, Sie möchten eine Schnittstelle erstellen, die eine Reihe von Namen manipuliert. In einer echten App würden Sie dafür wahrscheinlich eine Datenbank verwenden, aber für dieses Beispiel verwenden wir nur die In-Memory- set -Datenstruktur.

Das Skelett unserer API könnte so aussehen. Sie können diesen Code überall im Projekt platzieren, aber meine Empfehlung wäre eine separate API-Datei, z. B. api/names.py .

 from bottle import request, response from bottle import post, get, put, delete _names = set() # the set of names @post('/names') def creation_handler(): '''Handles name creation''' pass @get('/names') def listing_handler(): '''Handles name listing''' pass @put('/names/<name>') def update_handler(name): '''Handles name updates''' pass @delete('/names/<name>') def delete_handler(name): '''Handles name deletions''' pass

Routing

Wie wir sehen können, erfolgt das Routing in Bottle mit Decorators. Die importierten Decorators post , get , put und delete registrieren Handler für diese vier Aktionen. Um zu verstehen, wie diese Arbeit wie folgt aufgeschlüsselt werden kann:

  • Alle oben genannten Decorators sind eine Verknüpfung zu den default_app Routing-Decorators. Der @get() Dekorator wendet bottle.default_app().get() auf den Handler an.
  • Die Routing-Methoden auf default_app sind alle Abkürzungen für route() . default_app().get('/') entspricht default_app().route(method='GET', '/') .

@get('/') ist also dasselbe wie @route(method='GET', '/') , was dasselbe ist wie @bottle.default_app().route(method='GET', '/') , und diese können austauschbar verwendet werden.

Eine hilfreiche Sache am @route-Decorator ist, dass Sie, wenn Sie beispielsweise denselben Handler verwenden möchten, um sowohl @route als auch -löschungen zu verarbeiten, einfach eine Liste von Methoden übergeben können, die er wie folgt behandelt:

 @route('/names/<name>', method=['PUT', 'DELETE']) def update_delete_handler(name): '''Handles name updates and deletions''' pass

In Ordnung, lassen Sie uns einige dieser Handler implementieren.

Stellen Sie Ihre perfekte REST-API mit Bottle Framework zusammen.

RESTful-APIs sind ein fester Bestandteil der modernen Webentwicklung. Servieren Sie Ihren API-Clients ein starkes Gebräu mit einem Bottle-Back-End.
Twittern

POST: Ressourcenerstellung

Unser POST-Handler könnte so aussehen:

 import re, json namepattern = re.compile(r'^[a-zA-Z\d]{1,64}$') @post('/names') def creation_handler(): '''Handles name creation''' try: # parse input data try: data = request.json() except: raise ValueError if data is None: raise ValueError # extract and validate name try: if namepattern.match(data['name']) is None: raise ValueError name = data['name'] except (TypeError, KeyError): raise ValueError # check for existence if name in _names: raise KeyError except ValueError: # if bad request data, return 400 Bad Request response.status = 400 return except KeyError: # if name already exists, return 409 Conflict response.status = 409 return # add name _names.add(name) # return 200 Success response.headers['Content-Type'] = 'application/json' return json.dumps({'name': name})

Nun, das ist ziemlich viel. Sehen wir uns diese Schritte Stück für Stück an.

Body-Parsing

Diese API erfordert, dass der Benutzer eine JSON-Zeichenfolge im Text mit einem Attribut namens „Name“ POSTET.

Das zuvor aus der bottle importierte request -Objekt zeigt immer auf den aktuellen Request und enthält alle Daten des Requests. Sein body -Attribut enthält einen Byte-Stream des Request-Body, auf den von jeder Funktion zugegriffen werden kann, die ein Stream-Objekt lesen kann (wie das Lesen einer Datei).

Die Methode „ request.json() “ prüft die Header der Anfrage auf den Inhaltstyp „application/json“ und parst den Text, wenn er korrekt ist. Wenn Bottle einen fehlerhaften Körper erkennt (z. B. leer oder mit falschem Inhaltstyp), gibt diese Methode None zurück und wir lösen einen ValueError . Wenn fehlerhafter JSON-Inhalt vom JSON-Parser erkannt wird; Es löst eine Ausnahme aus, die wir abfangen und erneut auslösen, wiederum als ValueError .

Objektanalyse und -validierung

Wenn keine Fehler vorliegen, haben wir den Text der Anfrage in ein Python-Objekt konvertiert, auf das von der data verwiesen wird. Wenn wir ein Wörterbuch mit einem „Name“-Schlüssel erhalten haben, können wir über data['name'] darauf zugreifen. Wenn wir ein Wörterbuch ohne diesen Schlüssel erhalten haben, führt der Versuch, darauf zuzugreifen, zu einer KeyError Ausnahme. Wenn wir etwas anderes als ein Wörterbuch erhalten haben, erhalten wir eine TypeError Ausnahme. Wenn einer dieser Fehler erneut auftritt, lösen wir ihn erneut als ValueError , was auf eine fehlerhafte Eingabe hinweist.

Um zu überprüfen, ob der Namensschlüssel das richtige Format hat, sollten wir ihn mit einer Regex-Maske testen, wie z. B. der namepattern , die wir hier erstellt haben. Wenn der Schlüsselname kein String ist, löst name namepattern.match() einen TypeError , und wenn er nicht übereinstimmt, gibt er None zurück.

Mit der Maske in diesem Beispiel muss ein Name ein alphanumerischer ASCII-Wert ohne Leerzeichen von 1 bis 64 Zeichen sein. Dies ist eine einfache Validierung und testet beispielsweise nicht auf ein Objekt mit Datenmüll. Eine komplexere und vollständigere Validierung kann durch die Verwendung von Tools wie FormEncode erreicht werden.

Prüfung auf Existenz

Der letzte Test vor der Erfüllung der Anfrage ist, ob der angegebene Name bereits in der Menge existiert. In einer strukturierteren App sollte dieser Test wahrscheinlich von einem dedizierten Modul durchgeführt und unserer API durch eine spezielle Ausnahme signalisiert werden, aber da wir einen Satz direkt manipulieren, müssen wir ihn hier tun.

Wir signalisieren die Existenz des Namens, indem wir einen KeyError .

Fehlerantworten

So wie das Request-Objekt alle Request-Daten enthält, tut das Response-Objekt dasselbe für die Response-Daten. Es gibt zwei Möglichkeiten, den Antwortstatus einzustellen:

 response.status = 400

und:

 response.status = '400 Bad Request'

Für unser Beispiel haben wir uns für die einfachere Form entschieden, aber die zweite Form kann verwendet werden, um die Textbeschreibung des Fehlers anzugeben. Intern teilt Bottle die zweite Zeichenfolge auf und setzt den numerischen Code entsprechend.

Erfolgsantwort

Wenn alle Schritte erfolgreich sind, erfüllen wir die Anforderung, indem wir den Namen zum Satz _names , den Content-Type Antwortheader setzen und die Antwort zurückgeben. Jede von der Funktion zurückgegebene Zeichenfolge wird als Antworttext einer 200 Success -Antwort behandelt, also generieren wir einfach eine mit json.dumps .

GET: Ressourcenliste

Nach der Namenserstellung implementieren wir den Handler für die Namensliste:

 @get('/names') def listing_handler(): '''Handles name listing''' response.headers['Content-Type'] = 'application/json' response.headers['Cache-Control'] = 'no-cache' return json.dumps({'names': list(_names)})

Die Namen aufzulisten war viel einfacher, nicht wahr? Im Vergleich zur Namenserstellung gibt es hier nicht viel zu tun. Legen Sie einfach einige Antwortheader fest und geben Sie eine JSON-Darstellung aller Namen zurück, und wir sind fertig.

PUT: Ressourcenaktualisierung

Sehen wir uns nun an, wie die Update-Methode implementiert wird. Sie unterscheidet sich nicht sehr von der create-Methode, aber wir verwenden dieses Beispiel, um URI-Parameter einzuführen.

 @put('/names/<oldname>') def update_handler(name): '''Handles name updates''' try: # parse input data try: data = json.load(utf8reader(request.body)) except: raise ValueError # extract and validate new name try: if namepattern.match(data['name']) is None: raise ValueError newname = data['name'] except (TypeError, KeyError): raise ValueError # check if updated name exists if oldname not in _names: raise KeyError(404) # check if new name exists if name in _names: raise KeyError(409) except ValueError: response.status = 400 return except KeyError as e: response.status = e.args[0] return # add new name and remove old name _names.remove(oldname) _names.add(newname) # return 200 Success response.headers['Content-Type'] = 'application/json' return json.dumps({'name': newname})

Das Body-Schema für die Aktualisierungsaktion ist das gleiche wie für die Erstellungsaktion, aber jetzt haben wir auch einen neuen oldname Parameter im URI, wie durch die Route @put('/names/<oldname>') definiert.

URI-Parameter

Wie Sie sehen können, ist die Notation von URI-Parametern in Bottle sehr einfach. Sie können URIs mit beliebig vielen Parametern erstellen. Bottle extrahiert sie automatisch aus dem URI und übergibt sie an den Request-Handler:

 @get('/<param1>/<param2>') def handler(param1, param2): pass

Mit kaskadierenden Route-Decorators können Sie URIs mit optionalen Parametern erstellen:

 @get('/<param1>') @get('/<param1>/<param2>') def handler(param1, param2 = None) pass

Außerdem ermöglicht Bottle die folgenden Routing-Filter in URIs:

  • int

Stimmt nur mit Parametern überein, die in int konvertiert werden können, und übergibt den konvertierten Wert an den Handler:

 @get('/<param:int>') def handler(param): pass
  • float

Dasselbe wie int , aber mit Fließkommawerten:

 @get('/<param:float>') def handler(param): pass
  • re (reguläre Ausdrücke)

Entspricht nur Parametern, die mit dem angegebenen regulären Ausdruck übereinstimmen:

 @get('/<param:re:^[az]+$>') def handler(param): pass
  • path

Passt Untersegmente des URI-Pfads flexibel an:

 @get('/<param:path>/id>') def handler(param): pass

Streichhölzer:

  • /x/id , wobei x als param .
  • /x/y/id , wobei x/y als param .

LÖSCHEN: Ressourcenlöschung

Wie die GET-Methode bringt uns die DELETE-Methode wenig Neues. Beachten Sie nur, dass die Rückgabe von None ohne Festlegen eines Status eine Antwort mit einem leeren Text und einem Statuscode 200 zurückgibt.

 @delete('/names/<name>') def delete_handler(name): '''Handles name updates''' try: # Check if name exists if name not in _names: raise KeyError except KeyError: response.status = 404 return # Remove name _names.remove(name) return

Letzter Schritt: Aktivieren der API

Angenommen, wir haben unsere Namens-API als api/names.py , können wir diese Routen jetzt in der Hauptanwendungsdatei main.py .

 import bottle from api import names app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)

Beachten Sie, dass wir nur das names importiert haben. Da wir alle Methoden mit ihren URIs versehen haben, die an die Standard-App angehängt sind, ist keine weitere Einrichtung erforderlich. Unsere Methoden sind bereits vorhanden und bereit für den Zugriff.

Nichts macht ein Front-End so glücklich wie eine gut gemachte REST-API. Klappt wunderbar!

Sie können Tools wie Curl oder Postman verwenden, um die API zu nutzen und manuell zu testen. (Wenn Sie Curl verwenden, können Sie einen JSON-Formatierer verwenden, damit die Antwort weniger überladen aussieht.)

Bonus: Cross-Origin Resource Sharing (CORS)

Ein häufiger Grund für die Erstellung einer REST-API ist die Kommunikation mit einem JavaScript-Front-End über AJAX. Bei einigen Anwendungen sollten diese Anfragen von jeder Domäne kommen dürfen, nicht nur von der Heimatdomäne Ihrer API. Standardmäßig verbieten die meisten Browser dieses Verhalten, also lassen Sie mich Ihnen zeigen, wie Sie Cross-Origin Resource Sharing (CORS) in Bottle einrichten, um dies zu ermöglichen:

 from bottle import hook, route, response _allow_origin = '*' _allow_methods = 'PUT, GET, POST, DELETE, OPTIONS' _allow_headers = 'Authorization, Origin, Accept, Content-Type, X-Requested-With' @hook('after_request') def enable_cors(): '''Add headers to enable CORS''' response.headers['Access-Control-Allow-Origin'] = _allow_origin response.headers['Access-Control-Allow-Methods'] = _allow_methods response.headers['Access-Control-Allow-Headers'] = _allow_headers @route('/', method = 'OPTIONS') @route('/<path:path>', method = 'OPTIONS') def options_handler(path = None): return

Der hook -Decorator ermöglicht es uns, eine Funktion vor oder nach jeder Anfrage aufzurufen. In unserem Fall müssen wir zum Aktivieren von CORS die Header Access-Control-Allow-Origin , -Allow-Methods und -Allow-Headers Headers für jede unserer Antworten festlegen. Diese zeigen dem Anfragenden an, dass wir die angegebenen Anfragen bedienen werden.

Außerdem kann der Client eine OPTIONS-HTTP-Anforderung an den Server senden, um zu sehen, ob er wirklich Anforderungen mit anderen Methoden stellen kann. Mit diesem beispielhaften Auffangbeispiel antworten wir auf alle OPTIONS-Anforderungen mit einem 200-Statuscode und leerem Text.

Um dies zu aktivieren, speichern Sie es einfach und importieren Sie es aus dem Hauptmodul.

Einpacken

Das ist alles dazu!

Mit diesem Tutorial habe ich versucht, die grundlegenden Schritte zum Erstellen einer REST-API für eine Python-App mit dem Bottle-Webframework abzudecken.

Sie können Ihr Wissen über dieses kleine, aber leistungsstarke Framework vertiefen, indem Sie das Tutorial und die API-Referenzdokumente besuchen.

Siehe auch: Erstellen einer Node.js/TypeScript-REST-API, Teil 1: Express.js