:orphan: ========= API Web ========= .. role:: en .. role:: lat Motivation ========== Arichitecture client-serveur ---------------------------- .. figure:: client-server.* :height: 8ex Source image http://commons.wikimedia.org/wiki/File:Client-server-model.svg 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 `:doc:. Définition ---------- * API = `Application Programming Interface`:en: * Permet donc de *programmer* une application s'appuyant sur le service Web * (par opposition à une utilisation "manuelle" à travers un navigateur). .. _clients-rest: Comment tester une API ? ------------------------ * RestClient_ (extension Firefox) * ARC_ (Advanced REST client) .. _RestClient: https://addons.mozilla.org/fr/firefox/addon/restclient/ .. _Arc: https://install.advancedrestclient.com/ 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 ----------------------------------------- .. code-block:: python { '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`:en:. .. 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`:en:) plutôt que de liste, * on parle d'*objet* plutôt que de dictionnaire. Exemple précédent dans la syntaxe JSON -------------------------------------- .. code-block:: 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 : .. code-block:: python 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) .. nextslide:: 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.`:lat: ne modifie pas l'état de la ressource) * rend possible l'utilisation des caches HEAD ---- * a la même sémantique que GET * mais ne réclame *que* les en-têtes * et *pas* la représentation de la ressource .. note:: Flask gène automatiquement le verbe `HEAD` pour toute route qui supporte le verbe `GET`. 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`__. __ https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm 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: # ... .. nextslide:: 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`:lat:). * 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`:en:) 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 ----------- .. seqdiag:: :figclass: scroll seqdiag { C [label=Client] S [label=Server] C -> S [label="GET /foo"] ; C <- S [label="200 OK\nEtag: \"abc\""] ; C -> S [label="GET /foo\nIf-none-match: \"abc\""] ; C <-- S [label="304 Not Modified", leftnote="Reuses\ncached\nversion"] ; === Resource changes === C -> S [label="GET /foo\nIf-none-match: \"abc\""] ; C <- S [label="200 OK\nEtag: \"cde\""] ; } Etag et PUT - Concurrence optimiste ----------------------------------- .. seqdiag:: :figclass: scroll seqdiag { A [label="Client A"] S [label=Server] B [label="Client B"] A -> S [label="GET /foo"] ; A <- S [label="200 OK\nEtag: \"abc\""] ; B -> S [label="GET /foo"] ; B <- S [label="200 OK\nEtag: \"abc\""] ; B -> S [label="PUT /foo\nIf-match: \"abc\"", leftnote="Resource\nchanged"] ; B <- S [label="200 OK\nEtag: \"def\""] ; A -> S [label="PUT /foo\nIf-match: \"abc\""] ; A <-- S [label="409 Conflict"] ; A -> S [label="GET /foo"] ; A <- S [label="200 OK\nEtag: \"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.