drf – Sam & Max http://sametmax.com Du code, du cul Tue, 10 Sep 2019 09:14:50 +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
Look it up http://sametmax.com/look-it-up/ http://sametmax.com/look-it-up/#comments Tue, 08 Mar 2016 08:58:49 +0000 http://sametmax.com/?p=18545 un projet magique. Chaque jour est une nouvelle découverte, une aventure !]]> Je travaille sur un projet magique. Chaque jour est une nouvelle découverte, une aventure !

Par exemple, c’est un projet utilisant l’excellent Django Rest Framework, une app Django très puissante qui permet de créer des API succulentes.

DRF est très flexible, et permet de régler tout un tas de paramètres à l’aide de classes de configuration. Par exemple, il extrait automatiquement tout champ lookup_field sur ses classes Serializer afin de choisir sur quel champ filtrer les données.

L’auteur du code que j’ai sous les yeux, je crois, a voulu être vraiment, mais alors vraiment sûr d’avoir un look up:

class FooViewSet(ModelViewSet):

    class Meta:
        model = Foo
        lookup_field = 'pk'
        lookup_fields = ('pk', 'data_id')
        extra_lookup_fields = None

En soi, c’est très drôle.

Et je pourrais arrêter l’article ici.

Mais non.

En effet, y a pas un truc qui vous choque ?

Je veux dire, autre que la sainte trinité des lookup fields…

Allez, relisez l’article depuis le début, je vous laisse une chance.


J’ai dit que DRF extrayait un champ lookup_field sur les classes Serializer, et comme vous pouvez le constater, l’auteur ici hérite joyeusement de ModelViewSet, mais pas du tout de Serializer.

Oui, parce qu’on est en pleine exploration de Fistland (Au fond du fun !™), ces 3 champs ne sont en aucun cas exploités automatiquement par DRF… car sur les Viewset, lookup_field est utilisé pour générer des URLs, et mes prédécesseurs ont créé un router custo qui override ceci. Mais si on retire les champs, ça pète tout car il y a des bouts de leur code qui supposent l’existence de ce champ.

Néanmoins, ne soyons pas complètement négatif, certaines classes héritent bien de Serialiser, et définissent aussi lookup_field. D’ailleurs une part de mon job est de migrer tout ça. Car la petite touche humoristique finale, c’est que lookup_field est deprecated depuis 3 releases dans DRF \o/ Mais deprecated sur les Serializer uniquement hein, pas les Viewset. Enfin je dis ça…

]]>
http://sametmax.com/look-it-up/feed/ 7 18545