Creazione di un'API Rest con Bottle Framework
Pubblicato: 2022-03-11Le API REST sono diventate un modo comune per stabilire un'interfaccia tra back-end e front-end Web e tra diversi servizi Web. La semplicità di questo tipo di interfaccia e l'onnipresente supporto dei protocolli HTTP e HTTPS su reti e framework diversi, lo rendono una scelta facile quando si considerano i problemi di interoperabilità.
Bottle è un framework Web Python minimalista. È leggero, veloce e facile da usare ed è adatto alla creazione di servizi RESTful. Un semplice confronto fatto da Andriy Kornatskyy lo colloca tra i primi tre framework in termini di tempo di risposta e throughput (richieste al secondo). Nei miei test sui server virtuali disponibili da DigitalOcean, ho scoperto che la combinazione dello stack del server uWSGI e Bottle poteva raggiungere un sovraccarico di 140μs per richiesta.
In questo articolo, fornirò una procedura dettagliata su come creare un servizio API RESTful utilizzando Bottle.
Installazione e configurazione
La struttura della bottiglia raggiunge le sue prestazioni impressionanti in parte grazie al suo peso leggero. Infatti l'intera libreria è distribuita come un modulo a un file. Ciò significa che non tiene la mano tanto quanto altri framework, ma è anche più flessibile e può essere adattato per adattarsi a molti stack tecnologici diversi. Bottle è quindi più adatto per progetti in cui le prestazioni e la personalizzazione sono un premio e dove i vantaggi di risparmio di tempo di strutture più pesanti sono meno importanti.
La flessibilità di Bottle rende un po' futile una descrizione approfondita della configurazione della piattaforma, poiché potrebbe non riflettere il tuo stack. Tuttavia, una rapida panoramica delle opzioni e dove saperne di più su come configurarle è appropriata qui:
Installazione
Installare Bottle è facile come installare qualsiasi altro pacchetto Python. Le tue opzioni sono:
- Installa sul tuo sistema usando il gestore di pacchetti del sistema. Debian Jessie (attuale stabile) impacchetta la versione 0.12 come python-bottle .
- Installa sul tuo sistema usando Python Package Index con
pip install bottle
. - Installa in un ambiente virtuale (consigliato).
Per installare Bottle su un ambiente virtuale, avrai bisogno degli strumenti virtualenv e pip . Per installarli, fai riferimento alla documentazione virtualenv e pip, anche se probabilmente li hai già sul tuo sistema.
In Bash, crea un ambiente con Python 3:
$ virtualenv -p `which python3` env
La soppressione del parametro -p `which python3`
porterà all'installazione dell'interprete Python predefinito presente sul sistema, solitamente Python 2.7. Python 2.7 è supportato, ma questo tutorial presuppone Python 3.4.
Ora attiva l'ambiente e installa Bottle:
$ . env/bin/activate $ pip install bottle
Questo è tutto. La bottiglia è installata e pronta per l'uso. Se non hai familiarità con virtualenv o pip , la loro documentazione è di prim'ordine. Guarda! Ne vale la pena.
server
Bottle è conforme allo standard Web Server Gateway Interface (WSGI) di Python, il che significa che può essere utilizzato con qualsiasi server conforme a WSGI. Ciò include uWSGI, Tornado, Gunicorn, Apache, Amazon Beanstalk, Google App Engine e altri.
Il modo corretto per configurarlo varia leggermente a seconda dell'ambiente. Bottle espone un oggetto conforme all'interfaccia WSGI e il server deve essere configurato per interagire con questo oggetto.
Per ulteriori informazioni su come configurare il server, fare riferimento alla documentazione del server e alla documentazione di Bottle, qui.
Banca dati
Bottle è indipendente dal database e non si preoccupa da dove provengono i dati. Se desideri utilizzare un database nella tua app, Python Package Index ha diverse opzioni interessanti, come SQLAlchemy, PyMongo, MongoEngine, CouchDB e Boto per DynamoDB. Hai solo bisogno dell'adattatore appropriato per farlo funzionare con il database di tua scelta.
Nozioni di base sulla struttura delle bottiglie
Ora vediamo come creare un'app di base in Bottle. Per esempi di codice, assumerò Python >= 3.4. Tuttavia, la maggior parte di ciò che scriverò qui funzionerà anche su Python 2.7.
Un'app di base in Bottle si presenta così:
import bottle app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)
Quando dico di base, intendo dire che questo programma non ti "Hello World". (Quando è stata l'ultima volta che hai effettuato l'accesso a un'interfaccia REST che ha risposto "Hello World?") Tutte le richieste HTTP a 127.0.0.1:8000
riceveranno uno stato di risposta 404 Non trovato.
App in bottiglia
Bottle può avere diverse istanze di app create, ma per comodità la prima istanza viene creata per te; questa è l'app predefinita. Bottle mantiene queste istanze in uno stack interno al modulo. Ogni volta che fai qualcosa con Bottle (come eseguire l'app o allegare un percorso) e non specifichi di quale app stai parlando, fa riferimento all'app predefinita. In effetti, la riga app = application = bottle.default_app()
non ha nemmeno bisogno di esistere in questa app di base, ma è lì in modo che possiamo facilmente invocare l'app predefinita con Gunicorn, uWSGI o qualche server WSGI generico.
La possibilità di più app può sembrare confusa all'inizio, ma aggiungono flessibilità a Bottle. Per diversi moduli della tua applicazione, puoi creare app Bottle specializzate istanziando altre classi Bottle e configurandole con configurazioni diverse secondo necessità. È possibile accedere a queste diverse app da URL diversi, tramite il router URL di Bottle. Non lo approfondiremo in questo tutorial, ma sei incoraggiato a dare un'occhiata alla documentazione di Bottle qui e qui.
Invocazione del server
L'ultima riga dello script esegue Bottle utilizzando il server indicato. Se non viene indicato alcun server, come nel caso qui, il server predefinito è il server di riferimento WSGI integrato in Python, che è adatto solo per scopi di sviluppo. È possibile utilizzare un server diverso in questo modo:
bottle.run(server='gunicorn', host = '127.0.0.1', port = 8000)
Questo è lo zucchero sintattico che ti consente di avviare l'app eseguendo questo script. Ad esempio, se questo file è denominato main.py
, puoi semplicemente eseguire python main.py
per avviare l'app. Bottle contiene un elenco piuttosto ampio di adattatori per server che possono essere utilizzati in questo modo.
Alcuni server WSGI non hanno adattatori Bottle. Questi possono essere avviati con i comandi di esecuzione del server. Su uWSGI, ad esempio, tutto ciò che dovresti fare sarebbe chiamare uwsgi
in questo modo:
$ uwsgi --http :8000 --wsgi-file main.py
Una nota sulla struttura dei file
Bottle lascia la struttura dei file della tua app interamente a te. Ho scoperto che le mie politiche sulla struttura dei file si evolvono da progetto a progetto, ma tendono a essere basate su una filosofia MVC.
Costruire la tua API REST
Naturalmente, nessuno ha bisogno di un server che restituisca solo 404 per ogni URI richiesto. Ti ho promesso che avremmo creato un'API REST, quindi facciamolo.
Supponiamo di voler costruire un'interfaccia che manipola un insieme di nomi. In un'app reale probabilmente utilizzeresti un database per questo, ma per questo esempio utilizzeremo solo la struttura dei dati del set
in memoria.
Lo scheletro della nostra API potrebbe assomigliare a questo. Puoi inserire questo codice ovunque nel progetto, ma la mia raccomandazione sarebbe un file API separato, come 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
Instradamento
Come possiamo vedere, il routing in Bottle viene eseguito utilizzando i decoratori. I decoratori importati post
, get
, put
ed delete
gestori di registro per queste quattro azioni. Capire come funzionano questi possono essere suddivisi come segue:
- Tutti i decoratori di cui sopra sono una scorciatoia per i decoratori di routing
default_app
. Ad esempio, il decoratore@get()
applicabottle.default_app().get()
al gestore. - I metodi di routing su
default_app
sono tutte scorciatoie perroute()
. Quindidefault_app().get('/')
equivale adefault_app().route(method='GET', '/')
.
Quindi @get('/')
è lo stesso di @route(method='GET', '/')
, che è lo stesso di @bottle.default_app().route(method='GET', '/')
, e questi possono essere usati in modo intercambiabile.
Una cosa utile del decoratore @route
è che se desideri, ad esempio, utilizzare lo stesso gestore per gestire sia gli aggiornamenti che le eliminazioni degli oggetti, puoi semplicemente passare un elenco di metodi che gestisce in questo modo:
@route('/names/<name>', method=['PUT', 'DELETE']) def update_delete_handler(name): '''Handles name updates and deletions''' pass
Bene, allora implementiamo alcuni di questi gestori.
POST: Creazione di risorse
Il nostro gestore POST potrebbe assomigliare a questo:
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})
Beh, è parecchio. Esaminiamo questi passaggi parte per parte.
Analisi del corpo
Questa API richiede all'utente di POST una stringa JSON nel corpo con un attributo chiamato "nome".

L'oggetto request
importato in precedenza dalla bottle
punta sempre alla richiesta corrente e contiene tutti i dati della richiesta. Il suo attributo body
contiene un flusso di byte del corpo della richiesta, a cui può accedere qualsiasi funzione in grado di leggere un oggetto flusso (come leggere un file).
Il metodo request.json()
controlla le intestazioni della richiesta per il tipo di contenuto "application/json" e analizza il corpo se è corretto. Se Bottle rileva un corpo non corretto (es: vuoto o con tipo di contenuto errato), questo metodo restituisce None
e quindi solleviamo un ValueError
. Se il contenuto JSON non corretto viene rilevato dal parser JSON; solleva un'eccezione che catturiamo e rilanciamo, sempre come ValueError
.
Analisi e convalida degli oggetti
Se non ci sono errori, abbiamo convertito il corpo della richiesta in un oggetto Python a cui fa riferimento la variabile data
. Se abbiamo ricevuto un dizionario con una chiave "name", saremo in grado di accedervi tramite data['name']
. Se abbiamo ricevuto un dizionario senza questa chiave, provare ad accedervi ci porterà a un'eccezione KeyError
. Se abbiamo ricevuto qualcosa di diverso da un dizionario, otterremo un'eccezione TypeError
. Se si verifica uno di questi errori, ancora una volta, lo rilanciamo come ValueError
, indicando un input errato.
Per verificare se la chiave del nome ha il formato corretto, dovremmo testarla con una maschera regex, come la maschera namepattern
che abbiamo creato qui. Se il name
della chiave non è una stringa, namepattern.match()
solleverà un TypeError
e se non corrisponde restituirà None
.
Con la maschera in questo esempio, un nome deve essere un alfanumerico ASCII senza spazi da 1 a 64 caratteri. Questa è una semplice convalida e non verifica un oggetto con dati inutili, ad esempio. Una convalida più complessa e completa può essere ottenuta tramite l'uso di strumenti come FormEncode.
Test di esistenza
L'ultimo test prima di soddisfare la richiesta è se il nome dato esiste già nel set. In un'app più strutturata, quel test dovrebbe probabilmente essere eseguito da un modulo dedicato e segnalato alla nostra API tramite un'eccezione specializzata, ma poiché stiamo manipolando direttamente un set, dobbiamo farlo qui.
Segnaliamo l'esistenza del nome sollevando un KeyError
.
Risposte di errore
Proprio come l'oggetto della richiesta contiene tutti i dati della richiesta, l'oggetto della risposta fa lo stesso per i dati della risposta. Esistono due modi per impostare lo stato della risposta:
response.status = 400
e:
response.status = '400 Bad Request'
Per il nostro esempio, abbiamo optato per il modulo più semplice, ma il secondo modulo può essere utilizzato per specificare la descrizione del testo dell'errore. Internamente, Bottle dividerà la seconda stringa e imposterà il codice numerico in modo appropriato.
Risposta di successo
Se tutti i passaggi hanno esito positivo, soddisfiamo la richiesta aggiungendo il nome al set _names
, impostando l'intestazione della risposta Content-Type
e restituendo la risposta. Qualsiasi stringa restituita dalla funzione verrà trattata come il corpo della risposta di una risposta 200 Success
, quindi ne generiamo semplicemente una con json.dumps
.
OTTIENI: Elenco risorse
Passando dalla creazione del nome, implementeremo il gestore dell'elenco dei nomi:
@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)})
Elencare i nomi era molto più semplice, vero? Rispetto alla creazione del nome non c'è molto da fare qui. Basta impostare alcune intestazioni di risposta e restituire una rappresentazione JSON di tutti i nomi e il gioco è fatto.
PUT: aggiornamento delle risorse
Ora, vediamo come implementare il metodo di aggiornamento. Non è molto diverso dal metodo create, ma usiamo questo esempio per introdurre i parametri URI.
@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})
Lo schema del corpo per l'azione di aggiornamento è lo stesso dell'azione di creazione, ma ora abbiamo anche un nuovo parametro oldname
nell'URI, come definito da route @put('/names/<oldname>')
.
Parametri URI
Come puoi vedere, la notazione di Bottle per i parametri URI è molto semplice. Puoi creare URI con tutti i parametri che desideri. Bottle li estrae automaticamente dall'URI e li passa al gestore della richiesta:
@get('/<param1>/<param2>') def handler(param1, param2): pass
Utilizzando i decoratori di percorsi a cascata, puoi creare URI con parametri opzionali:
@get('/<param1>') @get('/<param1>/<param2>') def handler(param1, param2 = None) pass
Inoltre, Bottle consente i seguenti filtri di routing negli URI:
-
int
Corrisponde solo ai parametri che possono essere convertiti in
int
e passa il valore convertito al gestore:@get('/<param:int>') def handler(param): pass
-
float
Lo stesso di
int
, ma con valori in virgola mobile:@get('/<param:float>') def handler(param): pass
-
re
(espressioni regolari)
Corrisponde solo ai parametri che corrispondono all'espressione regolare data:
@get('/<param:re:^[az]+$>') def handler(param): pass
-
path
Abbina i sottosegmenti del percorso URI in modo flessibile:
@get('/<param:path>/id>') def handler(param): pass
Partite:
/x/id
, passandox
comeparam
./x/y/id
, passandox/y
comeparam
.
DELETE: Eliminazione delle risorse
Come il metodo GET, il metodo DELETE ci porta poche novità. Tieni presente che restituire None
senza impostare uno stato restituisce una risposta con un corpo vuoto e un codice di stato 200.
@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
Passaggio finale: attivazione dell'API
Supponendo di aver salvato la nostra API dei nomi come api/names.py
, ora possiamo abilitare questi percorsi nel file dell'applicazione principale 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)
Si noti che abbiamo importato solo il modulo dei names
. Poiché abbiamo decorato tutti i metodi con i relativi URI allegati all'app predefinita, non è necessario eseguire ulteriori impostazioni. I nostri metodi sono già in atto, pronti per essere consultati.
Puoi utilizzare strumenti come Curl o Postman per utilizzare l'API e testarla manualmente. (Se stai usando Curl, puoi usare un formattatore JSON per rendere la risposta meno disordinata.)
Bonus: condivisione delle risorse tra origini (CORS)
Un motivo comune per creare un'API REST è comunicare con un front-end JavaScript tramite AJAX. Per alcune applicazioni, queste richieste dovrebbero essere autorizzate a provenire da qualsiasi dominio, non solo dal dominio principale dell'API. Per impostazione predefinita, la maggior parte dei browser disabilita questo comportamento, quindi lascia che ti mostri come impostare la condivisione delle risorse cross-origin (CORS) in Bottle per consentirlo:
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
Il decoratore hook
ci consente di chiamare una funzione prima o dopo ogni richiesta. Nel nostro caso, per abilitare CORS dobbiamo impostare le intestazioni Access-Control-Allow-Origin
, -Allow-Methods
e -Allow-Headers
per ciascuna delle nostre risposte. Questi indicano al richiedente che serviremo le richieste indicate.
Inoltre, il client può effettuare una richiesta HTTP OPTIONS al server per vedere se può effettivamente effettuare richieste con altri metodi. Con questo esempio di esempio, rispondiamo a tutte le richieste OPTIONS con un codice di stato 200 e un corpo vuoto.
Per abilitarlo, salvalo e importalo dal modulo principale.
Incartare
Questo è tutto quello che c'è da fare!
Con questo tutorial, ho provato a coprire i passaggi di base per creare un'API REST per un'app Python con il framework Web Bottle.
Puoi approfondire le tue conoscenze su questo piccolo ma potente framework visitando il suo tutorial e i documenti di riferimento API.