Construire une API Rest avec le framework Bottle
Publié: 2022-03-11Les API REST sont devenues un moyen courant d'établir une interface entre les back-ends et les front-ends Web, et entre différents services Web. La simplicité de ce type d'interface et la prise en charge omniprésente des protocoles HTTP et HTTPS sur différents réseaux et frameworks en font un choix facile lorsque l'on considère les problèmes d'interopérabilité.
Bottle est un framework Web Python minimaliste. Il est léger, rapide et facile à utiliser, et convient parfaitement à la création de services RESTful. Une comparaison simple faite par Andriy Kornatskyy l'a placé parmi les trois meilleurs frameworks en termes de temps de réponse et de débit (requêtes par seconde). Lors de mes propres tests sur les serveurs virtuels disponibles auprès de DigitalOcean, j'ai constaté que la combinaison de la pile de serveurs uWSGI et de Bottle pouvait atteindre une surcharge de 140 μs par requête.
Dans cet article, je vais vous expliquer comment créer un service d'API RESTful à l'aide de Bottle.
Installation et configuration
Le cadre Bottle atteint ses performances impressionnantes en partie grâce à son poids léger. En fait, toute la bibliothèque est distribuée sous la forme d'un module à un seul fichier. Cela signifie qu'il ne vous tient pas autant la main que d'autres frameworks, mais il est également plus flexible et peut être adapté pour s'adapter à de nombreuses piles technologiques différentes. Bottle est donc le mieux adapté aux projets où les performances et la personnalisation sont primordiales, et où les avantages de gain de temps des cadres plus lourds sont moins pris en compte.
La flexibilité de Bottle rend une description détaillée de la configuration de la plate-forme un peu vaine, car elle peut ne pas refléter votre propre pile. Cependant, un aperçu rapide des options, et où en savoir plus sur la façon de les configurer, est approprié ici :
Installation
L'installation de Bottle est aussi simple que l'installation de n'importe quel autre package Python. Vos options sont :
- Installez sur votre système à l'aide du gestionnaire de packages du système. Debian Jessie (actuellement stable) empaquete la version 0.12 en tant que python-bottle .
- Installez sur votre système à l'aide de Python Package Index avec
pip install bottle
. - Installer sur un environnement virtuel (recommandé).
Pour installer Bottle sur un environnement virtuel, vous aurez besoin des outils virtualenv et pip . Pour les installer, veuillez vous référer à la documentation virtualenv et pip, bien que vous les ayez probablement déjà sur votre système.
Dans Bash, créez un environnement avec Python 3 :
$ virtualenv -p `which python3` env
La suppression du paramètre -p `which python3`
conduira à l'installation de l'interpréteur Python par défaut présent sur le système - généralement Python 2.7. Python 2.7 est pris en charge, mais ce didacticiel suppose Python 3.4.
Activez maintenant l'environnement et installez Bottle :
$ . env/bin/activate $ pip install bottle
C'est ça. La bouteille est installée et prête à l'emploi. Si vous n'êtes pas familier avec virtualenv ou pip , leur documentation est de premier ordre. Regarde! Ils valent la peine.
Serveur
Bottle est conforme à la norme Web Server Gateway Interface (WSGI) de Python, ce qui signifie qu'il peut être utilisé avec n'importe quel serveur compatible WSGI. Cela inclut uWSGI, Tornado, Gunicorn, Apache, Amazon Beanstalk, Google App Engine et autres.
La manière correcte de le configurer varie légèrement avec chaque environnement. Bottle expose un objet conforme à l'interface WSGI, et le serveur doit être configuré pour interagir avec cet objet.
Pour en savoir plus sur la configuration de votre serveur, reportez-vous à la documentation du serveur et à la documentation de Bottle, ici.
Base de données
Bottle est indépendant de la base de données et ne se soucie pas de la provenance des données. Si vous souhaitez utiliser une base de données dans votre application, Python Package Index propose plusieurs options intéressantes, telles que SQLAlchemy, PyMongo, MongoEngine, CouchDB et Boto pour DynamoDB. Vous n'avez besoin que de l'adaptateur approprié pour le faire fonctionner avec la base de données de votre choix.
Principes de base du cadre de bouteille
Voyons maintenant comment créer une application de base dans Bottle. Pour les exemples de code, je supposerai que Python >= 3.4. Cependant, la plupart de ce que je vais écrire ici fonctionnera également sur Python 2.7.
Une application de base dans Bottle ressemble à ceci :
import bottle app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)
Quand je dis basique, je veux dire que ce programme ne vous dit même pas "Hello World". (Quand avez-vous accédé pour la dernière fois à une interface REST qui a répondu "Hello World?") Toutes les requêtes HTTP à 127.0.0.1:8000
recevront un état de réponse 404 Not Found.
Applications en bouteille
Bottle peut avoir plusieurs instances d'applications créées, mais pour des raisons de commodité, la première instance est créée pour vous ; c'est l'application par défaut. Bottle conserve ces instances dans une pile interne au module. Chaque fois que vous faites quelque chose avec Bottle (comme exécuter l'application ou attacher un itinéraire) et que vous ne spécifiez pas de quelle application vous parlez, cela fait référence à l'application par défaut. En fait, la ligne app = application = bottle.default_app()
n'a même pas besoin d'exister dans cette application de base, mais elle est là pour que nous puissions facilement invoquer l'application par défaut avec Gunicorn, uWSGI ou un serveur WSGI générique.
La possibilité de plusieurs applications peut sembler déroutante au début, mais elles ajoutent de la flexibilité à Bottle. Pour différents modules de votre application, vous pouvez créer des applications Bottle spécialisées en instanciant d'autres classes Bottle et en les configurant avec différentes configurations selon les besoins. Ces différentes applications sont accessibles par différentes URL, via le routeur URL de Bottle. Nous n'aborderons pas cela dans ce didacticiel, mais nous vous encourageons à jeter un coup d'œil à la documentation de Bottle ici et ici.
Appel du serveur
La dernière ligne du script exécute Bottle en utilisant le serveur indiqué. Si aucun serveur n'est indiqué, comme c'est le cas ici, le serveur par défaut est le serveur de référence WSGI intégré de Python, qui ne convient qu'à des fins de développement. Un serveur différent peut être utilisé comme ceci :
bottle.run(server='gunicorn', host = '127.0.0.1', port = 8000)
C'est du sucre syntaxique qui vous permet de démarrer l'application en exécutant ce script. Par exemple, si ce fichier est nommé main.py
, vous pouvez simplement exécuter python main.py
pour démarrer l'application. Bottle contient une liste assez complète d'adaptateurs de serveur pouvant être utilisés de cette façon.
Certains serveurs WSGI n'ont pas d'adaptateurs Bottle. Ceux-ci peuvent être démarrés avec les propres commandes d'exécution du serveur. Sur uWSGI, par exemple, tout ce que vous auriez à faire serait d'appeler uwsgi
comme ceci :
$ uwsgi --http :8000 --wsgi-file main.py
Remarque sur la structure des fichiers
Bottle laisse la structure de fichiers de votre application entièrement à vous. J'ai constaté que mes politiques de structure de fichiers évoluent d'un projet à l'autre, mais ont tendance à être basées sur une philosophie MVC.
Construire votre API REST
Bien sûr, personne n'a besoin d'un serveur qui ne renvoie que 404 pour chaque URI demandé. Je vous ai promis que nous construirions une API REST, alors faisons-le.
Supposons que vous souhaitiez créer une interface qui manipule un ensemble de noms. Dans une application réelle, vous utiliseriez probablement une base de données pour cela, mais pour cet exemple, nous utiliserons simplement la structure de données de l' set
en mémoire.
Le squelette de notre API pourrait ressembler à ceci. Vous pouvez placer ce code n'importe où dans le projet, mais ma recommandation serait un fichier API séparé, tel que 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
Routage
Comme nous pouvons le voir, le routage dans Bottle se fait à l'aide de décorateurs. Les décorateurs importés post
, get
, put
et delete
des gestionnaires de registre pour ces quatre actions. Comprendre comment ces travaux peuvent être décomposés comme suit :
- Tous les décorateurs ci-dessus sont un raccourci vers les décorateurs de routage
default_app
. Par exemple, le décorateur@get()
appliquebottle.default_app().get()
au gestionnaire. - Les méthodes de routage sur
default_app
sont toutes des raccourcis pourroute()
. Ainsi,default_app().get('/')
est équivalent àdefault_app().route(method='GET', '/')
.
Donc @get('/')
est identique à @route(method='GET', '/')
, qui est identique à @bottle.default_app().route(method='GET', '/')
, et ceux-ci peuvent être utilisés de manière interchangeable.
Une chose utile à propos du décorateur @route
est que si vous souhaitez, par exemple, utiliser le même gestionnaire pour gérer à la fois les mises à jour et les suppressions d'objets, vous pouvez simplement passer une liste de méthodes qu'il gère comme ceci :
@route('/names/<name>', method=['PUT', 'DELETE']) def update_delete_handler(name): '''Handles name updates and deletions''' pass
Très bien, implémentons certains de ces gestionnaires.
POST : création de ressources
Notre gestionnaire POST pourrait ressembler à ceci :
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})
Eh bien, c'est beaucoup. Passons en revue ces étapes partie par partie.

Analyse du corps
Cette API nécessite que l'utilisateur poste une chaîne JSON dans le corps avec un attribut nommé "name".
L'objet de request
importé plus tôt à partir de bottle
pointe toujours vers la requête actuelle et contient toutes les données de la requête. Son attribut body
contient un flux d'octets du corps de la requête, accessible par toute fonction capable de lire un objet de flux (comme la lecture d'un fichier).
La méthode request.json()
vérifie les en-têtes de la requête pour le type de contenu « application/json » et analyse le corps s'il est correct. Si Bottle détecte un corps malformé (ex : vide ou avec un mauvais type de contenu), cette méthode retourne None
et donc on lève une ValueError
. Si un contenu JSON malformé est détecté par l'analyseur JSON ; il lève une exception que nous attrapons et relançons, encore une fois en tant que ValueError
.
Analyse et validation d'objets
S'il n'y a pas d'erreurs, nous avons converti le corps de la requête en un objet Python référencé par la variable data
. Si nous avons reçu un dictionnaire avec une clé "name", nous pourrons y accéder via data['name']
. Si nous avons reçu un dictionnaire sans cette clé, essayer d'y accéder nous conduira à une exception KeyError
. Si nous avons reçu autre chose qu'un dictionnaire, nous aurons une exception TypeError
. Si l'une de ces erreurs se produit, une fois de plus, nous la relancéons en tant que ValueError
, indiquant une mauvaise entrée.
Pour vérifier si la clé de nom a le bon format, nous devons la tester par rapport à un masque regex, tel que le masque de namepattern
de nom que nous avons créé ici. Si le name
de la clé n'est pas une chaîne, namepattern.match()
lèvera une TypeError
, et s'il ne correspond pas, il renverra None
.
Avec le masque dans cet exemple, un nom doit être un alphanumérique ASCII sans espaces entre 1 et 64 caractères. Il s'agit d'une validation simple et elle ne teste pas un objet avec des données parasites, par exemple. Une validation plus complexe et complète peut être obtenue grâce à l'utilisation d'outils tels que FormEncode.
Tester l'existence
Le dernier test avant de répondre à la demande est de savoir si le nom donné existe déjà dans l'ensemble. Dans une application plus structurée, ce test devrait probablement être effectué par un module dédié et signalé à notre API via une exception spécialisée, mais puisque nous manipulons un ensemble directement, nous devons le faire ici.
Nous signalons l'existence du nom en levant une KeyError
.
Réponses d'erreur
Tout comme l'objet de requête contient toutes les données de la requête, l'objet de réponse fait de même pour les données de réponse. Il existe deux manières de définir l'état de la réponse :
response.status = 400
et:
response.status = '400 Bad Request'
Pour notre exemple, nous avons opté pour la forme la plus simple, mais la seconde forme peut être utilisée pour spécifier la description textuelle de l'erreur. En interne, Bottle divisera la deuxième chaîne et définira le code numérique de manière appropriée.
Réponse réussie
Si toutes les étapes réussissent, nous répondons à la demande en ajoutant le nom à l'ensemble _names
, en définissant l'en-tête de réponse Content-Type
et en renvoyant la réponse. Toute chaîne renvoyée par la fonction sera traitée comme le corps de la réponse d'une réponse 200 Success
, nous en générons donc simplement une avec json.dumps
.
GET : Liste des ressources
Passant à la création de noms, nous allons implémenter le gestionnaire de liste de noms :
@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)})
Lister les noms était beaucoup plus facile, n'est-ce pas ? Comparé à la création de noms, il n'y a pas grand-chose à faire ici. Définissez simplement des en-têtes de réponse et renvoyez une représentation JSON de tous les noms, et nous avons terminé.
PUT : Mise à jour des ressources
Voyons maintenant comment implémenter la méthode de mise à jour. Ce n'est pas très différent de la méthode create, mais nous utilisons cet exemple pour introduire les paramètres 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})
Le schéma du corps pour l'action de mise à jour est le même que pour l'action de création, mais nous avons maintenant également un nouveau paramètre oldname
dans l'URI, tel que défini par la route @put('/names/<oldname>')
.
Paramètres URI
Comme vous pouvez le voir, la notation de Bottle pour les paramètres URI est très simple. Vous pouvez créer des URI avec autant de paramètres que vous le souhaitez. Bottle les extrait automatiquement de l'URI et les transmet au gestionnaire de requête :
@get('/<param1>/<param2>') def handler(param1, param2): pass
En utilisant des décorateurs de route en cascade, vous pouvez créer des URI avec des paramètres facultatifs :
@get('/<param1>') @get('/<param1>/<param2>') def handler(param1, param2 = None) pass
De plus, Bottle autorise les filtres de routage suivants dans les URI :
-
int
Correspond uniquement aux paramètres pouvant être convertis en
int
et transmet la valeur convertie au gestionnaire :@get('/<param:int>') def handler(param): pass
-
float
Identique à
int
, mais avec des valeurs à virgule flottante :@get('/<param:float>') def handler(param): pass
-
re
(expressions régulières)
Correspond uniquement aux paramètres qui correspondent à l'expression régulière donnée :
@get('/<param:re:^[az]+$>') def handler(param): pass
-
path
Correspond aux sous-segments du chemin URI de manière flexible :
@get('/<param:path>/id>') def handler(param): pass
Allumettes:
/x/id
, en passantx
commeparam
./x/y/id
, en passantx/y
commeparam
.
SUPPRIMER : suppression de ressources
Comme la méthode GET, la méthode DELETE nous apporte peu de nouveautés. Notez simplement que renvoyer None
sans définir de statut renvoie une réponse avec un corps vide et un code de statut 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
Dernière étape : activation de l'API
En supposant que nous ayons enregistré notre API de noms sous api/names.py
, nous pouvons maintenant activer ces routes dans le fichier d'application principal 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)
Notez que nous avons seulement importé le module names
. Étant donné que nous avons décoré toutes les méthodes avec leurs URI attachés à l'application par défaut, il n'est pas nécessaire de procéder à une configuration supplémentaire. Nos méthodes sont déjà en place, prêtes à être consultées.
Vous pouvez utiliser des outils comme Curl ou Postman pour utiliser l'API et la tester manuellement. (Si vous utilisez Curl, vous pouvez utiliser un formateur JSON pour rendre la réponse moins encombrée.)
Bonus : Partage de ressources inter-origines (CORS)
Une raison courante de créer une API REST est de communiquer avec un frontal JavaScript via AJAX. Pour certaines applications, ces requêtes doivent pouvoir provenir de n'importe quel domaine, pas seulement du domaine d'accueil de votre API. Par défaut, la plupart des navigateurs interdisent ce comportement, alors laissez-moi vous montrer comment configurer le partage de ressources cross-origin (CORS) dans Bottle pour permettre ceci :
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
Le décorateur de hook
nous permet d'appeler une fonction avant ou après chaque requête. Dans notre cas, pour activer CORS, nous devons définir les en-têtes Access-Control-Allow-Origin
, -Allow-Methods
et -Allow-Headers
pour chacune de nos réponses. Ceux-ci indiquent au demandeur que nous servirons les demandes indiquées.
De plus, le client peut faire une requête HTTP OPTIONS au serveur pour voir s'il peut vraiment faire des requêtes avec d'autres méthodes. Avec cet exemple d'exemple fourre-tout, nous répondons à toutes les requêtes OPTIONS avec un code d'état 200 et un corps vide.
Pour l'activer, il suffit de l'enregistrer et de l'importer depuis le module principal.
Emballer
C'est tout ce qu'on peut en dire!
Avec ce tutoriel, j'ai essayé de couvrir les étapes de base pour créer une API REST pour une application Python avec le framework Web Bottle.
Vous pouvez approfondir vos connaissances sur ce framework petit mais puissant en visitant son didacticiel et ses documents de référence sur l'API.