Une API avec R en deux temps, trois mouvements.

Créer une API en faisant un peu de plomberie avec R et {plumber}, ça vous tente ? Bien. Amusons nous à monter une API depuis R, avec les tweets envoyés entre le 20 janvier et le 06 février 2018 et contenant le hashtag #RStudioConf.

API, vous avez dit API ?

Si vous passez par ici et n’avez aucune idée de ce qu’est une API, voici plusieurs liens qui pourront vous rafraîchir la mémoire :

En quelques mots, une API est une suite de règles, définissant des méthodes permettant d’interagir avec un logiciel. Et donc, dans notre cas, nous allons voir comment créer une suite de règles permettant d’interagir avec R via l’Hypertext Transfer Protocol (aussi connu sous le petit nom de http).

Notes : ce billet de blog n’est pas destiné à expliquer comment faire des requêtes sur une API depuis R, mais se concentre sur comment créer l’API. Pour savoir comment faire une requête, nous vous conseillons la lecture des deux billets de blogs ci-dessus.

À propos du jeu de données

Les tweets ont été collectés entre le 20 janvier et le 06 février avec un Raspberry Pi, avec une fréquence d’une requête toutes les 30 minutes. Pour cela, nous avons utilisé le package {rtweet}.

Vous pouvez retrouver ce jeu de données sur notre GitHub !

Une API avec {plumber}

Pour créer une API avec R, nous allons avoir besoin de faire appel à {plumber}, un package permettant de définir les règles avec lesquelles nous pourrons converser, via http, avec R.

Mais pour quoi faire ? Grâce à {plumber}, R peut-être appelé depuis d’autres services : ces autres services envoient une requête http vers l’API, qui communique avec R, exécute le code, et renvoie le résultat.

Cela permet par exemple d‘intégrer R dans des softwares écrits dans d’autres langages : plot, modèles, data.frame… ces formats sont intégrable très facilement dans d’autres applications, grâce à {plumber}.

Alors, en clair, comment fait-on une API avec {plumber} ? Tour d’horizon en 2 temps, 3 mouvements.

Créer des endpoints

« Wow. D’entrée de jeu, comme ça, le jargon ! » Rassures-toi lecteur, il s’agit là d’un terme assez simple à comprendre, si on le tradécompose (non, ce mot n’est pas dans le wiktionnaire 😉 ) :

  • end réfère à une extrémité, un « bout » de l’API.
  • point réfère à… un point.

En clair, avec un endpoint, on définit un point d’accès dans l’API. Lorsque la requête arrive sur le routeur {plumber}, ce dernier va rechercher un endpoint correspondant, et s’il le trouve, il exécutera le code R et renverra le résultat. S’il n’en trouve pas, vous rencontrerez la bien connue erreur 404. À noter que ces endpoints doivent être uniques : lorsque la requête pointe son nez, {plumber} va chercher le premier endpoint satisfaisant la requête, et l’exécuter.

Comme tout point d’accès, pour le retrouver, il va falloir lui donner un petit nom. Et pour y aller, un méthode. C’est quoi une méthode ? Eh bien, si vous vous posez la question, c’est que vous n’avez pas lu le billet que nous avons mis en introduction 🙂 Heureusement, il n’est pas trop tard pour vous rattraper : Les API, un enfeR ?.

Les méthodes disponibles pour générer des endpoints sont : get, post, put, delete et head. Un même endpoint peut avoir plusieurs méthodes.

Pour créer ces endpoints, commençons pas créer un fichier .R, que l’on appelera plumber.R (file.create('plumber.R')).

C’est bon, vous y êtes ? Première étape, écrire la ligne de code qui chargera le jeu de données. Dans notre cas, le jeu de données, c’est #RStudioConf.RDS. Vous pouvez le télécharger ici.

df <- readRDS("#RStudioConf.RDS")

Nous aurons besoin de quelques packages un peu plus tard, assurons nous qu’ils soient bien chargés :

library(dplyr)
library(attempt)

On va créer une colonne en plus :

df <- mutate(df, date = as.Date(created_at))

Nous voilà donc fin prêts. Définissons maintenant quelques points d’accès à l’API. Lorsque l’on interrogera l’API créée avec {plumber} sur les endpoints, nous lancerons les fonctions, et le résultat sera renvoyé comme résultat de la requête. Oui, c’est aussi simple que ça.

Et le truc encore plus cool ? On documente nos endpoints avec du {roxygen2}. Elle est pas belle la vie ?

#' @apiTitle RStudioConf tweets
#' @apiDescription Tweets collected with {rtweet} during the 2018 RStudio Conference

#' An endpoint to test the API
#' 
#' @param q Something to print back
#' @get /test
 
function(q=""){
  list(msg = paste0("You entered: '", q, "'"), 
       class = class(q))
}

 

Nous venons ici de créer un endpoint test, qui renvoie le contenu tapé par l’utilisateur. Les habitués de {roxygen}, reconnaîtrons leur format favoris : en première ligne, le titre du endpoint. On retrouve ensuite le célèbre @param, définissant les paramètres de la fonction, et par extension, du endpoint. Enfin, nouveauté ici, le @get, suivi de /test. On donne tout simplement l’information que lorsqu’une requête GET sera effectuée sur le endpoint /test, c’est cette fonction qui sera éxécutée.

Définissons d’autres méthodes :

#' Search tweets from one user
#' 
#' @param q The user to look for. Default is "rstudio".
#' @param rt Wether or not (1 or 0) to include the Retweets. Default is 1. 
#' @get /who

function(q = "rstudio", rt = 0){
  stop_if_not(rt %in% c(0,1), msg = "rt should be 0 or 1")
  if (rt == 0){
    df <- filter(df, ! grepl("RT", df$text))
  }
  filter(df, screen_name == q)
}

#' Search for a given period (between 2018-01-20 and 2018-02-06)
#' 
#' @param from the range of the period to look for. Default is "2018-01-31". 
#' @param to the range of the period to look for. Default is "2018-02-06". 
#' @param rt Wether or not (1 or 0) to include the Retweets. Default is 1. 
#' @get /when

function(from = "2018-01-31", to = "2018-02-06", rt = 1){
  stop_if_not(rt %in% c(0,1), msg = "rt should be 0 or 1")
  if (rt == 0){
    df <- filter(df, ! grepl("RT", df$text))
  }
  filter(df, date > as.Date(from), date < as.Date(to))
}

#' Search for one word (for everybody or for a specific user)
#' 
#' @param q the word to look for in the tweets. Default is "package". 
#' @param user restrict search to one user.
#' @param rt Wether or not (1 or 0) to include the Retweets. Default is 1. 
#' @get /what

function(q = "package", user = NULL, rt = 1){
  stop_if_not(rt %in% c(0,1), msg = "rt should be 0 or 1")
  if (rt == 0){
    df <- filter(df, ! grepl("RT", df$text))
  }
  if (!is.null(user)){
    df <- filter(df, screen_name == user)
  }
  filter(df, grepl(q, df$text))
}

À noter que, par défaut, les résultats sont renvoyés au format JSON. Il est cependant possible de définir d’autres format, avec un @format :

Annotation Content Type Description/References
@json application/json jsonlite::toJSON()
@html text/html; charset=utf-8 Passes response through without any additional serialization
@jpeg image/jpeg jpeg()
@png image/png png()
@serializer htmlwidget text/html; charset=utf-8 htmlwidgets::saveWidget()
@serializer unboxedJSON application/json jsonlite::toJSON(unboxed=TRUE)

 

Il est possible de paramétrer chacun de ces formats, par exemple avec : @png (width = 1200, height = 600).

Lancer l’API

Bien, maintenant que nous avons nos méthodes, lançons l’API. Pour ça, on va garder le code dans un autre fichier .R, que l’on appelera torun.R (file.create('torun.R')).

Dans ce fichier R, on écrira d’abord :

pr <- plumber::plumb("plumber.R")

Cette première commande transforme le script défini plus haut en API. Nous avons ici un objet, pr, qui contient notre interface vers l’API.

Pour lancer l’API, nous aurons besoin de :

pr$run()

Dans votre console, vous trouverez un message du type :

Starting server to listen on port 8048
Running the swagger UI at http://127.0.0.1:8048/__swagger__/

Ici, l’API s’est donc lancée en localhost (sur ma machine), et j’ai accès à son interface (son swagger) sur http://127.0.0.1:8048/__swagger__/ :

On retrouve dans nos différentes méthodes GET les éléments que nous avons définis plus haut :

La console nous permet également de tester nos requêtes :

Ici, nous retrouvons dans l’onglet « Curl », la requête à réaliser sur l’API. Si nous la passons dans un terminal :

Et donc… voilà, c’est aussi simple, vous avez une API locale « up and running » 🙂

À propos des paramètres

Lors de la requêtes, les paramètres à passer aux fonctions doivent être passés comme « query strings », que vous pouvez mettre dans l’url. C’est-à-dire qu’on appellera http://127.0.0.1:8048/who?q=hadleywickham pour retourner les tweets d’Hadley Wickham. Pour passer plusieurs paramètres, on construira ainsi : http://127.0.0.1:8048/when?to=2018-02-02&from=2018-01-31

À noter que les paramètres sont reçus en tant que chaîne de caractères, comme nous l’indique le endpoint de test :

À noter, également, que le titre et la description du swagger se personnalisent avec :


#' @apiTitle Title text
#' @apiDescription Description text

Une API {plumber} avec Docker

Bien, c’est chouette tout ça, mais nous avons peut-être envie d’autre chose que de simplement faire tourner cette API en local… Ça se comprend 😉

Voici donc comment créer une image Docker qui contiendra notre API. Mais avant ça, quatre petites modifications dans torun.R.

library(plumber)
pr <- plumb("/usr/scripts/plumber.R")
pr$run(host='0.0.0.0', port = 8000, swagger = TRUE)

Et une dans plumber.R :

df <- readRDS("/usr/scripts/#RStudioConf.RDS")

Ensuite, créons notre Dockerfile avec {dockerfiler} (oui, nous pourrions aussi le faire à la main) :

# install.packages("remotes")
remotes::install_github("colinfay/dockerfiler")

library(dockerfiler)
# On va créer une nouvelle dockerfile
my_dock <- Dockerfile$new()
# Ajout du maintainer
my_dock$MAINTAINER("Colin FAY", "[email protected]")
my_dock$RUN("apt-get update -qq && apt-get install -y \\
  git-core \\
  libssl-dev \\
  libcurl4-gnutls-dev
")
# On aura besoin d'installer plumber, et d'autres packages 
my_dock$RUN(r(install.packages("attempt", repo = "http://cran.irsn.fr/")))
my_dock$RUN(r(install.packages("dplyr", repo = "http://cran.irsn.fr/")))
my_dock$RUN(r(install.packages("remotes", repo = "http://cran.irsn.fr/")))
# Seulement la version dev exporte le swagger
my_dock$RUN(r(remotes::install_github("trestletech/plumber", ref="do-swagger")))
# On va créer un dossier qui contiendra nos scripts
my_dock$RUN("mkdir /usr/scripts")
# On transfère les documents dans ce dossier 
my_dock$COPY("torun.R", "/usr/scripts/torun.R")
my_dock$COPY("#RStudioConf.RDS", "/usr/scripts/#RStudioConf.RDS")
my_dock$COPY("plumber.R", "/usr/scripts/plumber.R")
# On ouvre le port 8000
my_dock$EXPOSE(8000)
# Lorsqu'on lancera l'image Docker, le script torun.R sera lancé
my_dock$CMD("Rscript /usr/scripts/torun.R")
# On sauvegarde 
my_dock$write()

 

Ensuite, rendez-vous dans votre Terminal de prédilection (vous savez que vous pouvez utiliser le terminal directement dans Rstudio ?) pour lancer :

docker build . -t apiplumber

Un fois que c’est fait, direction :

docker run -d -p 8000:8000 apiplumber

Et voilà, direction http://127.0.0.1:8000/  !

Amusez-vous bien !


À propos de l'auteur

Colin Fay

Colin Fay

Data scientist & R Hacker


Commentaires


À lire également