Mon début d’API REST en Python avec Bottle et MongoDB

closeCet article a été publié il y a 9 ans 17 jours, il est donc possible qu’il ne soit plus à jour. Les informations proposées sont donc peut-être expirées.

Ça faisait longtemps que je n’avais pas retouché à ma base de données. J’avais d’autres priorités (manger, faire évoluer les performances du blog), et il faut bien le dire, mon application PHP/MySQL n’a pas pour autant cessé de fonctionner après avoir migré et adapté ma base. Maintenant que celle-ci est en service, j’ai ressorti mes doigts de là où ils traînaient et commencé à trifouiller du serpent pour sortir des infos de ma nouvelle base.

Ceci n’est pas non plus un cours sur les API REST. L’idée, c’est qu’on demande (ou envoie) des informations, le tout au travers de ce bon vieux protocole HTTP, et on y répond évidemment de la même manière. Je vous laisse lire cet article pour vous faire une idée plus précise.

Sur le papier, il doit être possible de coder une telle application dans pratiquement n’importe quel langage. Aussi, une API est généralement conçue pour renvoyer les données de la manière la plus légère possible, et surtout, juste les données, de manière plus ou moins structurée. Deux écoles s’affrontent sur le format de sortie, qui n’est que du texte dans les deux cas : XML, que Yahoo utilise pour la météo par exemple (utilisée dans Domohouse), et JSON, plus léger, plus « compressible ».

MongoDB utilisant le BSON, qui dans la pratique n’est à peine plus que du JSON, j’ai décidé de m’en tenir à celui-ci. En effet, je compte partir sur du Python pour le développement, et on a déjà vu que les chaînes JSON sont très faciles à manipuler, car elles sont représentées sous forme de dictionnaires.

Un framework adapté pour gagner du temps

Parce que je ne me vois pas coder un micro-serveur Web dans la journée, et que des gens ont déjà fait le travail, j’ai décidé d’utiliser Bottle. Un cadriciel en bon français, qui regroupe une série de classes, de fonctions, permettant de gagner du temps, en particulier si l’on développe une application Web, que ce soit une API ou carrément un site complet tel qu’un blog.

Ce framework est très pratique pour du développement, car il permet, suivant les options de lancement, de recharger l’application automatiquement quand il détecte des modifications dans le code. Le système de routage est très pratique pour définir de manière très lisible quelles fonctions exécuter en fonction de l’URL demandée.

Pour installer Bottle sur votre machine, le plus simple est de passer par Python-pip :

Que veut-on faire exactement ?

Aujourd’hui, je ne vais pas couvrir la totalité des fonctions, et pour démarrer « doucement », on va essayer d’au moins reproduire les mêmes comportements que dans l’application actuelle. On a donc les « fonctions » suivantes :

  • afficher les dix derniers films ajoutés
  • afficher la liste complète
  • afficher la liste des films prêtés
  • afficher le détail d’un des films
  • rechercher le titre d’un film
  • ajouter un film
  • modifier un film (qui permet de gérer les prêts)
  • supprimer un film

Aujourd’hui, on va s’attarder sur les fonctions de consultation, soit nos quatre premières lignes. Les fonctions de modifications de la base demanderont encore un peu de réflexion avant d’être mises au point.

Une petite fonction d’adaptation

Avant d’attaquer les routes de consultation, j’ai besoin d’une transformation. Lors de mes premiers essais, je me mangeais une erreur sur le fait de ne pas pouvoir convertir l’ObjectId renvoyé par MongoDB. Je vais donc forcer cette conversion au format texte, une opération que je saurais faire dans l’autre sens lorsque j’aborderais la modification (mais pas seulement).

Cette classe sera donc ajoutée au début du script :

Au moment d’envoyer la chaîne JSON au client, j’appellerais MongoEncoder() qui fera son travail.

Notre application va tenir dans un seul script. Voici donc le « décor » dans lequel on va ajouter nos routes :

Notre API écoutera donc sur le port 8080. Les paramètres debug et reloader afficheront quantité de messages intéressants et  permettront de recharger à chaque modification. Par exemple, on aura les messages suivant dans la console après l’avoir lancé, et que des requêtes seront exécutées :

Évidemment, une fois en production, il est inutile de dire qu’il faudra éviter ces deux paramètres (on pourra éventuellement gérer un log des requêtes effectuées).

Nos fonctions d’aujourd’hui font partie des routes de consultations, vous savez donc ou ajouter les blocs.

Les dix derniers films ajoutés

C’est ce qui s’affiche en premier lorsqu’on arrive sur la page d’accueil de l’application. Il est donc naturel de se pencher dessus en premier. Dans MongoDB, pour avoir les dix derniers films classés par ordre d’ajout, la commande est la suivante :

On connaît déjà la commande find(), mais ici, on fait pas mal de travail pour avoir une requête potable. On trie les films avec une chaîne JSON indiquant qu’on ne récupère que le titre, la date d’ajout et l’identifiant. On demande dans le même temps à sort() de trier les résultats de manière descendante en fonction de l’identifiant. Celui-ci est créé par défaut en fonction du temps, ce qui est parfait dans le cas présent. Enfin, on limite aux dix premiers résultats. Exploiter cette commande dans une application en Python, avec PyMongo, demandera un poil d’adaptation, rien de très difficile ceci dit.

Par quelle URL va-t-on passer pour demander ces fameux films ? Très peu original j’en ai peur, mais avant tout efficace. Je vais aussi prendre en compte le fait que certains s’amuseront à ajouter un petit truc au bout, même s’il n’y a rien après. Les deux chemins/routes pour obtenir notre information seront donc les suivants :

Je crois qu’on a quasiment tout, donc sans plus de chichis, voici le bloc de code :

On voit qu’on peut utiliser la même fonction pour plusieurs routes en les « empilant » juste avant la définition en question. Une particularité que je réutiliserais. On peut aussi forcer une route en fonction de la commande HTTP (GET ou POST principalement). On peut voir également la modification nécessaire sur sort(), telle qu’on doit l’utiliser avec PyMongo. La ligne « retour = … » interprète l’objet stocké dans resultat, pour avoir notre dictionnaire de dictionnaires. On force le type de contenu et le jeu de caractères de la réponse HTTP, puis on retourne notre liste passée à la moulinette du convertisseur d’ObjectId. Résultat, en ouvrant par exemple un onglet de notre navigateur préféré avec l’adresse suivante http://192.168.1.10/last :

Pas de panique, les caractères unicode sont très bien gérés dans un autre langage, j’ai pu tester avec succès en PHP.

Le détail d’un film

Je ne vais pas m’attarder sur la liste complète, la fonction est pratiquement la même que pour les dix derniers, seuls changent la requête à MongoDB (beaucoup plus simple), et les routes (/films et /films/). Vous devriez donc vous en sortir seul, y compris pour la liste des prêts.

Pour afficher le détail d’un film en particulier, il nous faut son identifiant. Celui-ci devra donc être indiqué dans l’URL. C’est la première fois qu’on va recevoir une donnée de la part d’un utilisateur, et vous savez ce qu’on dit quand on parle de développement un peu sécurisé :

Ne jamais faire confiance à l’utilisateur.

Surtout dans le cas présent où aucune authentification est en place. L’id sera dès lors converti en chaînes de caractères avant d’être manipulé (un peu à la manière d’un html_entities() en PHP).

Cette fonction sera donc l’occasion d’introduire la récupération de paramètres dans la route. En effet, en définissant cette dernière, on peut indiquer d’une part qu’on en attend une partie en particulier, et surtout, lui donner un nom pour la manipuler dans notre fonction.

Ici, on reprendra donc le modèle de la liste complète, mais si on ajoute l’id (après un / nécessaire), la route sera différente, et signifiera qu’on cherche les détails d’un film particulier. Voici donc le code à ajouter :

On force toujours la méthode GET, et ce coup-ci, on attend l’id dans l’URL, qui sera passé à la fonction (et transformé grâce à str() ). J’utilise la fonction find.one(), plus rapide quand on cherche un document en particulier, en transformant la chaîne de caractères en ObjectId avec la fonction « kivabien ».

En l’état, si aucun film ne correspond, je renvoie une erreur 404. Sinon, les deux moulinettes habituelles (type/charset et convertisseur), qui seront de la partie dans chacune de nos fonctions, terminent le travail, exemple avec l’identifiant de World War Z :

Rechercher un film

C’est le dernier morceau de la partie consultation. On a déjà vu comment extraire un paramètre de l’URL juste au dessus, ce coup-ci, c’est du côté de MongoDB que ça va coincer un peu pour la recherche.

Les plus avisés auront remarqué l’importation du module re. Oui, j’ai encore souffert pendant près d’une heure pour trouver le résultat de la bonne vieille expression régulière qu’il nous faut. Sans oublier de « sécuriser » le titre, et en prenant soin de faire fi de la casse (majuscule/minuscule). Je fais encore appel à sort() pour trier les résultats, mais ce coup-ci en fonction du titre, et de manière ascendante.

Prochaine étape : les fonctions de modifications

C’est tout pour aujourd’hui. On sait déjà consulter les infos, mais si on veut au minimum une parité avec l’application actuelle, je dois pouvoir ajouter de nouveaux films, modifier ceux qui existent (notamment pour la gestion des prêts), et aussi, même si c’est plus rare, supprimer un film. Avec une plus grande attention encore sur la sécurité des données.

La suite au prochain numéro comme on dit.