Wrapping web APIs with Tortilla - Damien Accorsi

Wrapping web APIs
with Tortilla


wrapping web APIs made easy
(and more...)


Damien Accorsi — damien.accorsi@algoo.fr

Qui suis-je ?

Damien Accorsi

Freelance architecture et dév. backend

Créateur de Tracim — http://tracim.fr

Contributeur LinuxFR — http://linuxfr.org/users/lebouquetin

Les APIs web

Qu'est-ce qu'une API web

Les types d'APIs web

Une API web, c'est quoi ?

Application Programming Interface

interface de programmation pour accéder à un ensemble de fonctionnalités

API web

interface de programmation pour interagir avec un système/service distant, avec ou sans authentification

Types classiques d'API web

  • SOAP
  • XML/RPC et JSON/RPC (RPC pour Remote Procedure Call)
  • REST/XML, REST/JSON

SOAP — des API typées, mais complexes

Avantages

  • Auto-documenté via le fichier WSDL (définition de l'API)
  • Manipulation de types complexes
  • Pas limité à HTTP (existe aussi au dessus de SMTP, par exemple)

Inconvénients

  • lib client/serveur pas toujours compatibles
  • pas trivial à utiliser

SOAP n'est pas simple (et donc pas ciblé par Tortilla)

XML-RPC (ou JSON-RPC) — Appels de procédures distantes

Avantages

  • RPC: concept simple à comprendre pour les programmeurs
  • Existe depuis longtemps, de bonnes lib dispo
  • Utilisé par de nombreux logiciels open-source et services en ligne
    ex. : DokuWiki, Programmation de l'API Gandi...
  • Variante JSON-RPC moins verbeuse

Inconvénients

  • XML est verbeux... XML-RPC l'est donc aussi
  • Orienté "procédures", ce qui n'est pas le besoin le plus courant aujourd'hui
  • Sujet à des vulnérabilités XML

XML-RPC en python : utilisez le module xmlrpclib (xmlrpc.client en python 3)

API REST/JSON (ou REST/XML) — API orientée ressource

Avantages

  • Simple et adapté à la manipulation de données stockées en base de données
  • Utilise les verbes définis par HTTP : GET, PUT, PATCH, DELETE, POST
  • REST est un principe d'architecture, pas une technologie -> rien n'est imposé

Inconvénients

  • Rien n'est imposé, on trouve donc du bon et du moins bon

Exemple d'une api REST

Définition

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

Utilisation

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

Tortilla — Introduction

Généralités

Débuter avec Tortilla

Du "wrapping", et juste ça

Tortilla, c'est quoi ?

La page github du projet dit...

« Wrapping web APIs made easy. »

En réalité, c'est plutôt « Wrapping REST APIs made easy. »

Tortilla est

Débuter avec Tortilla

Installation

pip install tortilla

Exemple d'appel sur une api REST/JSON

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'})
              

Débuter avec Tortilla — REST/XML

Installation

pip install tortilla
pip install xmltodict

Exemple d'appel sur une api REST/XML

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})

Du wrapping et juste ça

Tortilla ne masque pas la complexité, il se contente de la "wrapper"

L'accès à des APIs REST simples

donnera du code python simple

L'accès à des APIs (REST) complexes

donnera du code python... complexe ;)

Dans tous les cas, l'écriture du code sera simplifiée

mais parfois il restera difficile à lire ;)

Illustration simple avec l'API Redmine

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

Illustration complexe avec l'API Redmine

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

Tortilla — Principes

Ecriture du code

Options surchargeables

Verbes HTTP standards

Ecriture du code

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 options sont surchargeables

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)

Verbes standards HTTP — Rappel

Les verbes HTTP standards sont supportés :
GET, POST, PUT, PATCH, DELETE, HEAD
via les méthodes get(), post(), put(), patch(), delete() et head()

Signification des verbes (convention)

  • GET : récupération d'une ressource
  • POST : création d'une ressource
  • PUT : modification intégrale d'une ressource
  • PATCH : modification partielle d'une ressource
  • DELETE : suppression d'une ressource
  • HEAD : récupération des métadonnées d'une ressource

Tortilla — Mise en oeuvre

Ressource manipulée

Données envoyées

Paramètres d'url

Ajout de headers

Extension d'url

Format de données

Authentification

Optimisation des performance

Ressource manipulée : premier paramètre

# GET http://api.example.org/videos/33
api.videos.get(33)

Données envoyées : paramètre data

# POST http://api.example.org/videos/33/comments
api.videos(33).comments.post(data={'pseudo': 'fantomass', 
                                   'message': 'Hey! This guy is really AWESOME!'})

Paramètres d'url : paramètre params

# GET http://api.example.org/videos/33/comments?order_by=pub_date
api.videos(33).comments.get(params={'order_by': 'pub_date'})

Ajout de headers

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'

Ajout d'une extension à l'url

# 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

Format de données (par défault : json)

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

Authentification 'Basic Authentication'

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'))

Optimisation des ressources réseau

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)

Utilisation détournée de Tortilla...

"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