wrapping web APIs made easy
(and more...)
Damien Accorsi — damien.accorsi@algoo.fr
⚝ Damien Accorsi
⚝ Freelance architecture et dév. backend
⚝ Créateur de Tracim — http://tracim.fr
⚝ Contributeur LinuxFR — http://linuxfr.org/users/lebouquetin
⚝ Qu'est-ce qu'une API web
⚝ Les types d'APIs web
interface de programmation pour accéder à un ensemble de fonctionnalités
interface de programmation pour interagir avec un système/service distant, avec ou sans authentification
SOAP n'est pas simple (et donc pas ciblé par Tortilla)
XML-RPC en python : utilisez le module xmlrpclib (xmlrpc.client en python 3)
Server: api.service.com
Endpoints:
GET /videos # retourne la liste des videos
POST /videos # ajout d'une nouvelle vidéo
GET /videos/<id> # retourne la video #id
GET /videos/<id>/comments # retourne la liste de commentaires de la video #id
POST /videos/<id>/comments # ajout d'un commentaire à la video #id
Ajouter un commentaire sur la vidéo #33 se fera via un POST sur l'url
http://api.service.com/videos/33/comments
Récupérer la liste des vidéos se fera via un GET sur l'url
http://api.service.com/videos
« Wrapping web APIs made easy. »
En réalité, c'est plutôt « Wrapping REST APIs made easy. »
pip install tortilla
Recherche sur Github des projets incluant le mot-clé "tortilla" :
# executes a GET request on the following url:
# https://api.github.com/search/repositories?q=tortilla
import tortilla
github = tortilla.wrap('https://api.github.com')
github.search.get('repositories', params={'q': 'tortilla'})
pip install tortilla
pip install xmltodict
Recherche sur OpenStreetMap d'une note avec la discussion associée :
# executes a GET request on the following url:
# http://api.openstreetmap.org/api/0.6/notes/326316?include_discussion=true
import tortilla
import xmltodict
tortilla.formats.register('xml', xmltodict.parse, xmltodict.unparse)
osm = tortilla.wrap('http://api.openstreetmap.org', format='xml')
osm.api('0.6').notes.get(326316, params={'include_discussion': True})
Tortilla ne masque pas la complexité, il se contente de la "wrapper"
donnera du code python simple
donnera du code python... complexe ;)
mais parfois il restera difficile à lire ;)
Récupération du détail du bug #19920
# Redmine project web API
# Get a given issue
#
# The API is at http://www.redmine.org/issues/19920?format=json
import tortilla
redmine = tortilla.wrap('http://www.redmine.org/')
response = redmine.issues.get(19920, params={'format': 'json'})
print response.issue
Dauns un cas simple... le code est simple
Objectif : récupérer la liste des bugs urgents encore ouverts sur le projet Redmine
# Redmine project web API
# Get list of urgent issues still open
#
# The API is at http://www.redmine.org/projects/redmine/issues
# Complexity is about filtering data
import tortilla
redmine = tortilla.wrap('http://www.redmine.org/')
response = redmine.projects('redmine').issues.get(params={'set_filter': '1',
'f[]': ['status_id',
'priority_id'],
'op[status_id]': 'o',
'op[priority_id]': '=',
'v[priority_id][]': 6,
'format': 'json',
'limit': 100})
print str(response.total_count)+'issue(s)'
for issue in response.issues:
print '{}\t{}\t{}'.format(issue.created_on, issue.id, issue.subject)
# Will execute a GET on the following url:
# http://www.redmine.org/projects/redmine/issues?set_filter=1&f[]=status_id&f[]=priority_id&op[status_id]=o&op[priority_id]=%3D&v[priority_id][]=6&format=json&limit=100
Dans un cas plus compliqué, la complexité... est toujours là ;)
la lisibilité est (éventuellement) améliorée
Chaque méthode ou attribut représente un segment de l'URL
id, count = 71, 20
api = tortilla.wrap('https://api.example.org')
api.video(id).comments.get(count)
peut se décomposer en :
api -> https://api.example.org
.video -> /video
(id) -> /71
.comments -> /comments
.get(count) -> /20 (et méthode GET)
Le dernier appel chaîné correspond à la méthode HTTP, ici un GET.
GET https://api.example.org/video/71/comments/20
Les différentes options peuvent être définies à la création du wrapper, et surchargées à l'appel
api = tortilla.wrap('https://api.example.org', debug=True)
# do not show debug messages
api.video(71).comments.get(20, debug=False)
# show debug messages
api.video(71).comments.get(20)
Autre exemple avec les headers
api = tortilla.wrap('https://api.example.org', debug=True)
# define default header
api.config.headers.authorization = 'Basic oirzegnuirzenuioernv'
# no headers
api.video(71).comments.get(20, headers={})
# default headers will be sent
api.video(71).comments.get(20)
Les verbes HTTP standards sont supportés :
GET, POST, PUT, PATCH, DELETE, HEAD
via les méthodes get(), post(), put(), patch(), delete() et head()
# GET http://api.example.org/videos/33
api.videos.get(33)
# POST http://api.example.org/videos/33/comments
api.videos(33).comments.post(data={'pseudo': 'fantomass',
'message': 'Hey! This guy is really AWESOME!'})
# GET http://api.example.org/videos/33/comments?order_by=pub_date
api.videos(33).comments.get(params={'order_by': 'pub_date'})
soit via le paramètre headers
# GET http://api.example.org/videos/33 with following header:
# AUTHORIZATION: BASIC DHJh327tOiE0uKJYcmtycght
api.videos.get(33, headers={'AUTHORIZATION': 'BASIC DHJh327tOiE0uKJYcmtycght'})
soit par configuration du wrapper
api = tortilla.wrap('http://api.service.org')
api.config.headers.authorization = 'BASIC DHJh327tOiE0uKJYcmtycght'
# GET http://api.example.org/videos/33/comments.json?order_by=pub_date
api.videos(33).comments.get(params={'order_by': 'pub_date'}, extension='json')
donne l'url
http://api.example.org/videos/33/comments.json?order_by=pub_date
définit le parsing des réponses (et l'encodage des données envoyées)
import tortilla
import xmltodict
# Register a new type of data (with associated encode/decode methods)
tortilla.formats.register('xml', xmltodict.parse, xmltodict.unparse)
# Now, tortilla can wrap a RSS which is pure XML
gandi_news = tortilla.wrap('http://www.gandi.net/feed/news/fr/block/hp/', format='xml')
gandi_news.get()
Intérêt : possibilité de définir un "parser" vraiment spécifique
Tortilla supporte l'authentification HTTP via la méthode Basic :
AUTHORIZATION: Basic ZmFudG9tYXM6bXlfc2VjcmV0X3Bhc3N3b3JkX2duYXJrX2duYXJrX2duYXJr
En python 2.7, cela donne quelque chose comme :
import base64
import tortilla
username = 'fantomas'
password = 'my_secret_password_gnark_gnark_gnark'
user_and_pass = '%s:%s' % (username, password)
auth_string = 'Basic %s' % (base64.b64encode(user_and_pass))
# auth_string now contains something like:
# Basic ZmFudG9tYXM6bXlfc2VjcmV0X3Bhc3N3b3JkX2duYXJrX2duYXJrX2duYXJr
github = tortilla.wrap('https://api.github.com', debug=True)
github.config.headers.authorization = auth_string # Set authorization header for all requests
github.search.repositories.get(params=dict(q='python,calendar,server',
sort='stars'))
Mise en cache des requêtes et résultats pour éviter de refaire des requêtes identiques (durée de vie du cache paramétrable en secondes) :
api = tortilla.wrap('http://localhost:5000', cache_lifetime=60)
api.live.get(37) # HTTP request is executed
api.live.get(37) # Result is taken from cache
# Force not using cache with ignore_cache
api.live.get(37, ignore_cache=True) # HTTP request is executed
Bridage du taux de requêtage pour respecter les contraintes imposées côté serveur (et ne pas se faire black-lister) :
# Force a 3 seconds delay between requests
api = tortilla.wrap('http://localhost:5000', delay=3)
"Construction" d'une api de recherche sur le site web leboncoin
http://www.leboncoin.fr/voitures/offres/rhone_alpes/?f=a&th=1&q=bmw&location=Grenoble 38100,Grenoble 38000
peut se décomposer en :
http://www.leboncoin.fr -> racine
/ventes_immobilieres -> catégorie recherchée
/offres -> élément statique
/rhone_alpes -> région ciblée
?f=a&th=1 -> paramètres cabalistiques (on les reprendra tel-quels)
&q=bmw -> paramètre de recherche de mots-clés
&location=Grenoble 38100,Grenoble 38000 -> localisation précise, ville avec code postal, séparées par une virgule