rest – Sam & Max http://sametmax.com Du code, du cul Wed, 23 Dec 2020 13:35:02 +0000 en-US hourly 1 https://wordpress.org/?v=4.9.7 32490438 Accepter un ID mais retourner un objet pour les liens de Django Rest Framework http://sametmax.com/accepter-un-id-mais-retourner-un-objet-pour-les-liens-de-django-rest-framework/ http://sametmax.com/accepter-un-id-mais-retourner-un-objet-pour-les-liens-de-django-rest-framework/#comments Thu, 08 Jun 2017 07:50:00 +0000 http://sametmax.com/?p=23385 DRF est une des perles de Django. De Python même. Comme marshmallow, requests, jupyter, pandas, SQLAlchemy ou l’admin Django. Python a tellement d’outils extraordinaires.

Mais aucune n’est parfaite, et une chose qui m’a toujours emmerdé avec celle-ci, c’est que si j’ai un modèle du genre:

class Foo(models.Model):
    name = models.CharField(max_length=64)
    bar = models.ForeignKey(Bar)

Et le serializer:

class FooSerialize(serilizers.ModelSerializer):

    class Meta:
        model = Foo

J’ai le choix entre soit avoir que des ID…

En lecture (chiant) :

GET /api/foos/1/

{
    name: "toto",
    bar: 2
}

Et en écriture (pratique) :

POST /api/foos/
{
    name: "tata",
    bar: 2
}

Soit avoir que des objets.

En lecture (pratique):

GET /api/foos/1/

{
    name: "toto",
    bar: {
       // tout l'objet bar disponible en lecture
    }
}
Et en écriture (chiant) :

POST /api/foos/
{
    name: "tata",
    bar: {
       // tout l'objet bar à se taper à écrire
    }
}

Il y a aussi la version hypermedia où l’id est remplacé par une URL. Mais vous voyez le genre : mon API REST est soit pratique en lecture mais relou à écrire, soit pratique en écriture (je fournis juste une référence), mais relou en lecture, puisque je dois ensuite fetcher chaque référence.

GraphQL répond particulièrement bien à ce problème, mais bon, la techno est encore jeune, et il y a encore plein d’API REST à coder pour les années à venir.

Comment donc résoudre ce casse-tête, Oh Sam! – sauveur de la pythonitude ?

Solution 1, utiliser un serializer à la place du field

class FooSerializer(serilizers.ModelSerializer):

    bar = BarSerializer()

    class Meta:
        model = Foo

Et là j’ai bien l’objet complet qui m’est retourné. Mais je suis en lecture seule, et il faut que je fasse l’écriture à la main. Youpi.

Pas la bonne solution donc.

Solution 2, écrire deux serializers

Ben ça marche mais il faut 2 routings, ça duplique l’API, la doc, les tests. Moche. Next.

Solution 3, un petit hack

En lisant le code source de DRF (ouais j’ai conscience que tout le monde à pas la foi de faire ça), j’ai noté que ModelSerializer génère automatiquement pour les relations un PrimaryKeyRelatedField, qui lui même fait le lien via l’ID. On a des classes similaires pour la version full de l’objet et celle avec l’hyperlien.

En héritant de cette classe, on peut créer une variante qui fait ce qu’on veut:

from collections import OrderedDict

from rest_framework import serializers


class AsymetricRelatedField(serializers.PrimaryKeyRelatedField):

    # en lecture, je veux l'objet complet, pas juste l'id
    def to_representation(self, value):
        # le self.serializer_class.serializer_class est redondant
        # mais obligatoire
        return self.serializer_class.serializer_class(value).data

    # petite astuce perso et pas obligatoire pour permettre de taper moins 
    # de code: lui faire prendre le queryset du model du serializer 
    # automatiquement. Je suis lazy
    def get_queryset(self):
        if self.queryset:
            return self.queryset
        return self.serializer_class.serializer_class.Meta.model.objects.all()

    # Get choices est utilisé par l'autodoc DRF et s'attend à ce que 
    # to_representation() retourne un ID ce qui fait tout planter. On 
    # réécrit le truc pour utiliser item.pk au lieu de to_representation()
    def get_choices(self, cutoff=None):
        queryset = self.get_queryset()
        if queryset is None:
            return {}

        if cutoff is not None:
            queryset = queryset[:cutoff]

        return OrderedDict([
            (
                item.pk,
                self.display_value(item)
            )
            for item in queryset
        ])

    # DRF saute certaines validations quand il n'y a que l'id, et comme ce 
    # n'est pas le cas ici, tout plante. On désactive ça.
    def use_pk_only_optimization(self):
        return False

    # Un petit constructeur pour générer le field depuis un serializer. lazy,
    # lazy, lazy...
    @classmethod
    def from_serializer(cls, serializer, name=None, args=(), kwargs={}):
        if name is None:
            name = f"{serializer.__class__.__name__}AsymetricAutoField"

        return type(name, (cls,), {"serializer_class": serializer})(*args, **kwargs)

Et du coup:

class FooSerializer(serializers.ModelSerializer):

    bar = AsymetricRelatedField.from_serializer(BarSerializer)

    class Meta:
        model = Foo

Et voilà, on peut maintenant faire:

GET /api/foos/1/

{
    name: "toto",
    bar: {
       // tout l'objet bar disponible en lecture
    }
}

POST /api/foos/
{
    name: "tata",
    bar: 2
}

Elle est pas belle la vie ?

Ca serait bien cool que ce soit rajouté officiellement dans DRF tout ça. Je crois que je vais ouvrir un ticket

]]>
http://sametmax.com/accepter-un-id-mais-retourner-un-objet-pour-les-liens-de-django-rest-framework/feed/ 13 23385
Qu’est-ce qu’une API ? http://sametmax.com/quest-ce-quune-api/ http://sametmax.com/quest-ce-quune-api/#comments Sat, 06 Sep 2014 19:13:58 +0000 http://sametmax.com/?p=12184 L’API, pour Application Programming Interface, est la partie du programme qu’on expose officiellement au monde extérieur pour manipuler celui-ci. L’API est au développeur ce que l’UI est à l’utilisateur : de quoi entrer des données et récupérer la sortie d’un traitement.

L’API au sens original

Initialement, une API regroupe un ensemble de fonctions ou méthodes, leurs signatures et ordre d’usage pour obtenir un résultat.

Par exemple, imaginons que je fasse une lib pour botter des culs en Python, bottage.py :

def senerver(moment):
    # ...

def botter(cul):
    # ...

def main():
    # ...

if __name__ == "__main__":
    main()

Je vais l’utiliser ainsi :

from bottage import senerver, botter

senerver(now)
botter(le_cul_de_ce_con)

Les deux fonctions senerver() et botter() sont mes points d’entrée pour cette action. Je n’utilise pas main(), qui est un code interne à la lib et ne me regarde pas. Il n’y a rien qui distingue cette fonction des autres dans cet exemple, mais je n’en ai pas besoin pour faire le boulot, c’est ma lib qui l’utilise automatiquement quelque part, je n’ai pas à la connaitre.

Donc leurs noms et leurs paramètres ainsi que leurs types sont l’API de ma lib, ce qui m’est exposé pour l’utiliser.

Si on veut rentrer dans des subtilités, on dira en fait que senerver() et botter() font partie de l’API publique, c’est à dire de ce qui est manipulable par un utilisateur de la lib. A l’inverse, main() fait partie de l’API privée, c’est à dire ce qui est manipulable par les développeurs de la lib. Mais quand on parle d’API sans préciser, on parle de l’API publique.

Changement d’API

En informatique, on peut généralement exposer les choses de plusieurs manières différentes. Je peux changer mon API :

import datetime

def senerver(moment=None):
    if not moment:
        moment = datetime.datetime.utcnow()
    # ...

def botter(cul):
    # ...

def init():
    # ...

if __name__ == "__main__":
    init()

Ici, j’ai changé mon API pour rendre le paramètre moment facultatif afin de faciliter la vie des utilisateurs de la libs.

Et là on aborde un point très important du concept : la stabilité d’une API.

Puisque l’API est ce qu’on expose au monde extérieur, le monde extérieur va l’utiliser d’une certaine façon. Si on change cette manière de l’utiliser dans une version suivante, au moment de la mise à jour, on va casser leur code si on ne fait pas attention.

Par exemple, ici je rends un paramètre facultatif : ça ne craint pas grand-chose. Mais si j’avais fait l’inverse ? J’avais un paramètre facultatif, et soudain je le rends obligatoire. Toutes les personnes qui n’ont pas passé le paramètre vont soudain avoir un plantage s’ils passent à la nouvelle version de la lib car l’API a changé.

C’est donc une seconde définition de l’API : l’API est une promesse, un contrat entre l’auteur d’un code et ceux qui vont utiliser ce code. Cette promesse est “voici ce que vous pouvez utiliser sereinement dans votre programme, je ne vais pas tout péter demain”.

Cette promesse est plus ou moins bien respectée selon les projets. Python, par exemple, a un historique exemplaire de stabilité d’API, et n’a cassé la compatibilité qu’une fois, avec Python 3, donnant 10 ans aux développeurs pour s’adapter.

Dans tous les cas, si une lib est beaucoup utilisée et que son développeur a le sens des responsabilités, elle évolue plus doucement. Pour cette raison, il faut faire attention au choix qu’on fait dans le style de son API, sous peine de ne pas pouvoir le changer plus tard.

En effet, on peut tout à faire écrire le même code dans des tas de styles différents. Ainsi, je pourrais botter des culs avec une API orientée objet :


class Colere:

    @classmethod
    def global_init():
        # ...

    def __init__(moment):
        # ...
        self._senerver(moment)

    def _senerver():
        # ...

    def botter(cul):
        # ...

if __name__ == "__main__":
    Colere.global_init()

Mon bottage de cul n’a plus du tout le même goût à l’usage :

from bottage import colere
c = Colere(now)
c.botter(un_aperi_cul)

Mon programme fait la même chose, mais mon API est différente. Notez le _senerver() qui est préfixé d’un underscore, une convention en Python pour dire que cette méthode doit être considérée comme ne faisant pas partie de l’API publique, donc à ne pas utiliser depuis l’extérieur. En effet, il n’y a pas de méthode privée en Python.

Qualités d’une API

On a vu que la stabilité était une qualité importante d’une API. Mais il y en a d’autres. Notamment la performance et l’ergonomie, généralement deux notions qui s’affrontent.

Pour l’ergonomie, il s’agit de rendre facile les usages qu’on fait le plus couramment, et rendre possible les usages les plus ardus. Prenez l’exemple d’une requête HTTP avec paramètre POST sur un site qui a besoin de cookies d’une requête précédente. Pas un usage incroyablement complexe a priori…

Avec la stdlib de Python, ça donne ça :

import urllib
import urllib2
import cookielib

cookie_jar = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie_jar))
urllib2.install_opener(opener)
urllib2.urlopen('http://site.com')

data = urllib.urlencode({"nom": "valeur"})
rsp = urllib2.urlopen('http://site.com/autre/page', data)
print(rsp.read())

La même chose avec la lib requests :

import requests

request.get('http://site.com')
res = request.post('http://site.com/autre/page', data={"nom": "valeur"})
print(res.content)

Il ne s’agit pas juste du fait qu’il y ait beaucoup moins de lignes pour le faire. La facilité à découvrir comment faire dans le deuxième cas est exemplaire : en lisant, on comprend le code. En bidouillant dans le shell, on peut sans doute trouver tout ça. On n’a pas besoin de se poser la question de ce qu’est une jar, l’url encoding, etc.

Le premier exemple est non seulement verbeux, mais très, très difficile à trouver soi-même. En fait, même avec la doc sous les yeux, ce n’est pas évident d’arriver à ce résultat, et on y arrivera après des essais douloureux.

Le deuxième exemple est plus ergonomique que le premier.

Mais l’ergonomie a généralement un coût : celui de la performance.

Imaginez que j’ai la lib de bottage de fion sous forme fonctionnelle :

import datetime

def senerver(moment=None):
    if not moment:
        moment = datetime.datetime.utcnow()
    # ...

def botter(cul):
    # ...

def init():
    # ...

if __name__ == "__main__":
    init()

Je veux la rendre plus ergonomique. Je sais qu’il faut obligatoirement s’énerver pour botter un cul, et je décide donc de cacher cette fonction et l’appeler automatiquement :

import moment

def _senerver(moment=None):
    if not moment:
        moment = datetime.datetime.utcnow()
    # ...

def botter(cul, moment=None):
    _senerver(moment)
    # ...

def init():
    # ...

if __name__ == "__main__":
    init()

Dans la plupart des cas, ça va aider mon public :

Au lieu de devoir savoir qu’il faut s’énerver avant de botter, ils ont juste à botter :

from bottage import botter
botter(cul_de_jatte)

J’ai identifié que c’était l’usage le plus courant, donc c’est une amélioration. Mais ça a un prix pour une petite partie de mes utilisateurs : les très gros botteurs de cul. Ceux qui bottent des culs par centaine.

Avant, ils pouvaient faire :

from bottage import senerver, botter
senerver()
for cul in rang_doigons:
    botter(cul)

Mais maintenant, faisant :

from bottage import botter
for cul in rang_doigons:
    botter(cul)

Ils ont un appel à _senerver() à chaque tour de boucle, et donc un appel à datetime.utcnow() aussi !

Bien entendu, il est possible de remédier à cette situation, mais cet article n’est pas là pour vous expliquer comment créer une belle API. Ce serait néanmoins un très bon article.

Ici, je vous montre simplement qu’en facilitant, on suppose d’un usage, et ça peut se faire au détriment des autres. L’automatisme a tendance à retirer de la marge de manœuvre.

Une bonne API va donc proposer un moyen automatique de faire les opérations de tous les jours, va lui donner une forme (nom, ordre des actions, organisation, etc) ET un moyen de faire des choses compliquées ou performantes. Ce qui va rendre l’API plus riche, donc plus lourde, avec une plus grosse doc, etc.

Tout a un coût.

API Web

Jusqu’ici vous avez vu l’API d’une bibliothèque, mais il existe d’autres genres d’API. L’un est devenu particulièrement populaire depuis le milieu des années 2000 : l’API Web.

L’API Web est comme l’API précédente ce qui est exposé à l’extérieur pour manipuler un programme. Entrées. Sorties. Mais il y a plusieurs différences :

  • Ce n’est pas une lib qu’on manipule, c’est en général un service complet.
  • On n’utilise pas le langage dudit code pour le manipuler, mais un protocole Web.
  • Les appels passent par le réseau.

Il existe de nombreux protocoles qui permettent de faire une API Web : SOAP, REST, XML RPC, WAMP etc.

Aujourd’hui, les API Web les plus populaires utilisent majoritairement un protocole pseudo-REST (techniquement REST est plus une archi qu’un protocole, mais fuck) avec en encoding JSON.

Hum, je vous vois sourciller.

Oui, c’est clair que la phrase est un peu tordue du cul, comme si elle avait été bottée.

Prenons donc un exemple : un service Web de bottage de cul !

Vous êtes donc asskicker.io, leader mondial du bottage de cul en ligne. Et vous exposez votre processus de bottage de cul exclusif à tous les programmeurs.

Pour ce faire, vous mettez à disposition une API WEB sous forme pseudo REST. Au lieu d’appeler des méthodes, les développeurs vont envoyer des requêtes HTTP Get et Post à des URLs représentant les culs à botter.

Je ne vais pas rentrer dans les détails de ce qu’est du (pseudo) REST ou JSON exactement, mais un exemple de requêtes à faire pour botter des culs via notre API Web serait :

import json
import requests

# On fait une requête GET vers l'URL du service pour obtenir de quoi s’énerver
colere = requests.get('http://asskicker.io/colere/')

# Je créer un nouveau bottage de cul
data = json.dumps({'cul': 'de bouteille', 'colere': colere['id']})
headers = {'content-type': 'application/json'}
res = requests.post('http://asskicker.io/bottage/', data=data, headers=headers)

Et supposons qu’on veuille connaître le dernier bottage de cul fait :

res = requests.get('http://asskicker.io/bottage/last')
print(res.json()) # et la réponse JSON du service :
# {
#     "bottage": 89080,
#     "cul": "de bouteille",
#     "colere": 99943,
#     "date": "2014-09-06 20:38:11"
# }

Les URLs sont fictives, complètement inventées, et ne correspondent à rien.

Ici, notre API est donc la collections d’URLs (http://asskicker.io/bottage/, http://asskicker.io/bottage/last, etc.) qui permet de manipuler notre service, ainsi que les noms et types des paramètres à envoyer via data et le contenu de la réponse.

Le but de l’API Web est de permettre de manipuler du code sur une machine distante à travers le Web, depuis n’importe quel langage capable d’envoyer une requête HTTP. L’API Twitter permet de de lister des tweets et en envoyer. Par exemple, si on est authentifié, faire une requête GET sur http://api.twitter.com/1.1statuses/show/787998 permet d’obtenir en retour un JSON contenant les informations sur le tweet numéro 787998

L’API Google permet de faire des recherches. L’API flicker permet d’uploader des photos. Toutes les APIS ont des formes différentes, certaines sont plus ou moins faciles, plus ou moins efficaces, utilisent tels ou tels formats, mais au final, c’est la même chose : un moyen de manipuler le service en faisant des requêtes.

La manière classique de créer un site est de générer le HTML final sur le serveur. Or, comme il est possible d’envoyer des requêtes HTTP depuis une page Web en utilisant Ajax, on voit aujourd’hui des sites codés en Javascript qui vont chercher leurs données sur le serveur via l’API du site. Le navigateur reçoit ainsi un HTML incomplet, et le JS appelle l’API pour reconstruire la page.

Ainsi, on code la logique une seule fois : récupérer les informations, effectuer des actions… Et on utilise l’API pour tout ça, que ce soit pour faire le site Web, ou pour laisser d’autres programmeurs utiliser le site.

Évidemment, une vraie API Web est complexe, possède des problématiques de sécurité, d’authentification… Encore un bon article à écrire.

]]>
http://sametmax.com/quest-ce-quune-api/feed/ 18 12184