API Web§

Motivation§

Arichitecture client-serveur§

Hétérogénéité des clients§

  • client mobile
    • minimiser les échanges (bande passante, batterie, forfait...)
    • gérer l’affichage coté client
  • autre serveur
    • exemple : agence de voyage / SNCF
    • pas besoin d’affichage

Note

Même dans le cas d’une application HTML “classique”, on peut bénéficier d’une API grâce à Javascript.

Définition§

  • API = Application Programming Interface
  • Permet donc de programmer une application s’appuyant sur le service Web
  • (par opposition à une utilisation “manuelle” à travers un navigateur).

Comment tester une API ?§

Le format JSON§

Présentation§

  • JavaScript Object Notation (JSON) est un format largement utilisé pour échanger des données sur le Web.
  • Inspiré du langage Javascript, il est en également bien adapté à de nombreux langages, dont Python.

Sémantique§

JSON permet de représenter les données Python suivantes :

  • None,
  • des chaînes de caractères,
  • des nombres (entiers et flottants),
  • des booléens,
  • des listes,
  • des dictionnaires dont les clés sont des chaînes de caractères.

Exemple de données représentables en JSON§

{
  'id': 1,
  'label': "File",
  'tooltip': None,
  'items': [
    {'label': "New",   'visible': True},
    {'label': "Open",  'visible': True},
    {'label': "Close", 'visible': False},
  ],
}

Syntaxe§

Très similaire à Python, à quelques exceptions près :

  • None s’écrit null,
  • True s’écrit true,
  • False s’écrit false,
  • les chaînes de caractères sont obligatoirement encadrées par des double quotes.

Note

Ces variantes proviennent principalement du langage Javascript, mais aussi d’une volonté de garder le langage JSON simple.

En terme de vocabulaire,

  • on parle de tableau (array) plutôt que de liste,
  • on parle d’objet plutôt que de dictionnaire.

Exemple précédent dans la syntaxe JSON§

{
  "id": 1,
  "label": "File",
  "tooltip": null,
  "items": [
    {"label": "New",   "visible": true},
    {"label": "Open",  "visible": true},
    {"label": "Close", "visible": false}
  ]
}

Utilisation en python§

En Python standard :

import json
# avec des fichiers
data = json.load(open("file.json"))
json.dump(data, open("file2.json", "w"))
# avec des chaînes de caractères
txt = json.dumps(data)
data2 = json.loads(txt)

Dans Flask:

  • request.get_json() fournit le contenu JSON de la requête (le cas échéant), ou None si le contenu est absent ou dans autre format.
  • flask.jsonify(data) produit un objet réponse dont
    • l’en-tête content-type est application/json, et
    • le contenu est la sérialisation JSON des données passées en paramètre.

Verbes HTTP§

Introduction§

Avec HTML, on utilise exclusivement les verbes GET et POST, mais HTTP définit d’autres verbes, qui sont particulièrement utiles lors de la définition d’APIs.

GET§

  • n’attend pas contenu
  • réclame une représentation de la ressource
  • est sans effet de bord (i.e. ne modifie pas l’état de la ressource)
  • rend possible l’utilisation des caches

PUT§

  • attend un contenu
  • demande la création ou la modification de la ressource
  • de sorte que le nouvel état de la ressource corresponde au contenu fourni
  • a donc un effet de bord idempotent

DELETE§

  • n’attend pas de contenu
  • demande la suppression de la ressource
  • a donc un effet de bord idempotent

POST§

  • attent un contenu
  • peut avoir n’importe quel effet de bord (y compris non-idempotent)
  • est souvent utiliser pour créer une ressource sans connaître a priori son URL

API RESTful§

  • On affecte à chaque ressource une URL qui l’identifie,
  • et chaque URL identifie une ressource.
  • Les manipulations se font en utilisant les verbes HTTP appropriés.

Note

L’acronyme REST et ses dérivés (comme “RESTful”) a été inventé par Roy Fieldings dans sa thèse.

Patron “collection/élément”§

  • une collection contient une liste d’éléments
  • la collection supporte
    • GET pour obtenir la liste de ses éléments (avec leurs URLs)
    • POST pour créer un nouvel élément
  • chaque élément supporte
    • GET pour en obtenir une description détaillée
    • PUT pour le modifier (éventuellement le créer)
    • DELETE pour le supprimer

Mise en œuvre en Flask§

Il est possible d’implémenter différents verbes dans la même vue :

@app.route("/bidules/", methods=['GET', 'POST'])
def bidules():
    if request.method == 'GET':
      # ...
    else:
      # ...

Mais il est également possible de les implémenter dans différentes vues, (et donc d’avoir plusieurs routes ayant la même URL, mais des verbes différents) :

@app.route("/bidules/", methods=['GET'])
def bidules_list():
    # ...

@app.route("/bidules/", methods=['POST'])
def bidules_new():
    # ...

Le choix entre les deux options dépend de la quantité de code commun aux traitements des différents verbes.

Gestion du cache§

Introduction§

  • HTTP est conçu pour tirer parti des caches.
  • Cette gestion de cache n’est pas spécifique au développement d’APIs, elle est également utile pour tous les fichiers réutilisés dans plusieurs pages (images, feuilles de styles, etc).
  • Mais elle est d’autant plus importante avec les APIs : une machine peut lancer des requêtes beaucoup plus vite qu’un humain.

En-tête Cache-control§

Voici deux valeurs typiques pour cache-control dans une réponse :

  • max-age=T (ou T est une durée en secondes) : informe le client sur la durée pendant laquelle il est pertinent de garder l’information en cache.
  • no-cache informe le client qu’il ne doit pas conserver cette réponse en cache, pour le forcer à ré-interroger le serveur à la prochaine requête.

En-tête ETag§

  • Un etag (entity tag) est une chaîne de caractère identifiant une version de la ressource. L’etag change chaque fois que la ressource est modifiée.
  • Elle permet au client d’indiquer au serveur la dernière version de la ressource dont il a connaissance.
  • Ceci ce fait en conditionnant l’exécution d’une requête à certaines versions de la ressource, avec les en-têtes
    • If-Match: seulement si la ressource correspond à l’un des etags fournis
    • If-None-Match: seulement si la ressource ne correspond à aucun des etags fournis

Etag et GET§

blockdiag Client Server Reuses cached version GET /foo 200 OK Etag: "abc" GET /foo If-none-match: "abc" 304 Not Modified GET /foo If-none-match: "abc" 200 OK Etag: "cde" Resource changes

Etag et PUT - Concurrence optimiste§

blockdiag Client A Server Client B Resource changed GET /foo 200 OK Etag: "abc" GET /foo 200 OK Etag: "abc" PUT /foo If-match: "abc" 200 OK Etag: "def" PUT /foo If-match: "abc" 409 Conflict GET /foo 200 OK Etag: "def"

Mise en œuvre en Flask§

  • L’objet Response a une méthode set_etag qui permet de lui associer un etag.

    Note

    Il ne faut pas utiliser resp.headers["etag"] = my_etag car les etags doivent être encodés d’une manière spécifique.

  • L’objet request a des attributs if_match et if_none_match qui contiennent la liste des etags fournis par le client (correctement décodés).

  • Exemple d’utilisation :

    @app.route('/my/url')
    def smart_view():
        etag = get_etag()
        if etag in request.if_none_match:
            resp = make_response("", 304) # 304 not modified
            resp.set_etag(etag)
            return resp
        # here, make the "normal" response
    

    Note

    Flask offre une manière plus intégrée de gérer les requêtes GET conditionnelles :

    @app.route('/my/url')
    def smart_view():
        # here, make the "normal" response
        resp.set_etag(get_etag())
        resp.make_conditional(request)
        # transform the response according to conditional request
        return resp
    

    La réponse sera modifiée pour tenir compte de l’en-tête If-None-March, mais également de l’en-tête Range (RFC 7233). Cette solution est donc plus concise et plus puissante, mais elle oblige à construire systématiquement la réponse, ce qui peut avoir un coût significatif (accès à la base de données).

    Le choix de l’une ou l’autre solution est donc une question de compromis.