gwift-book/source/part-4-services-oriented-ap.../rest.adoc

13 KiB
Executable File
Raw Blame History

Application Programming Interface

Note
Expliquer pourquoi une API est intéressante/primordiale/la première chose à réaliser/le cadet de nos soucis.

Au niveau du modèle, nous allons partir de quelque chose de très simple: des personnes, des contrats, des types de contrats, et un service daffectation. Quelque chose comme ceci:

# models.py

from django.db import models


class People(models.Model):
    CIVILITY_CHOICES = (
        ("M", "Monsieur"),
        ("Mme", "Madame"),
        ("Dr", "Docteur"),
        ("Pr", "Professeur"),
        ("", "")
    )

    last_name = models.CharField(max_length=255)
    first_name = models.CharField(max_length=255)
    civility = models.CharField(
        max_length=3,
        choices=CIVILITY_CHOICES,
        default=""
    )

    def __str__(self):
        return "{}, {}".format(self.last_name, self.first_name)


class Service(models.Model):
    label = models.CharField(max_length=255)

    def __str__(self):
        return self.label


class ContractType(models.Model):
    label = models.CharField(max_length=255)
    short_label = models.CharField(max_length=50)

    def __str__(self):
        return self.short_label


class Contract(models.Model):
    people = models.ForeignKey(People, on_delete=models.CASCADE)
    date_begin = models.DateField()
    date_end = models.DateField(blank=True, null=True)
    contract_type = models.ForeignKey(ContractType, on_delete=models.CASCADE)
    service = models.ForeignKey(Service, on_delete=models.CASCADE)

    def __str__(self):
        if self.date_end is not None:
            return "A partir du {}, jusqu'au {}, dans le service {} ({})".format(
                self.date_begin,
                self.date_end,
                self.service,
                self.contract_type
            )

        return "A partir du {}, à durée indéterminée, dans le service {} ({})".format(
                self.date_begin,
                self.service,
                self.contract_type
            )
models

Configuration

La configuration des points de terminaison de notre API est relativement touffue. Il convient de:

  1. Configurer les sérialiseurs, càd. les champs que nous souhaitons exposer au travers de lAPI,

  2. Configurer les vues, càd le comportement de chacun des points de terminaison,

  3. Configurer les points de terminaison eux-mêmes, càd les URLs permettant daccéder aux ressources.

  4. Et finalement ajouter quelques paramètres au niveau de notre application.

Sérialiseurs

# serializers.py

from django.contrib.auth.models import User, Group
from rest_framework import serializers

from .models import People, Contract, Service


class PeopleSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = People
        fields = ("last_name", "first_name", "contract_set")


class ContractSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Contract
        fields = ("date_begin", "date_end", "service")


class ServiceSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Service
        fields = ("name",)

Vues

# views.py

from django.contrib.auth.models import User, Group
from rest_framework import viewsets
from rest_framework import permissions

from .models import People, Contract, Service
from .serializers import PeopleSerializer, ContractSerializer, ServiceSerializer


class PeopleViewSet(viewsets.ModelViewSet):
    queryset = People.objects.all()
    serializer_class = PeopleSerializer
    permission_class = [permissions.IsAuthenticated]


class ContractViewSet(viewsets.ModelViewSet):
    queryset = Contract.objects.all()
    serializer_class = ContractSerializer
    permission_class = [permissions.IsAuthenticated]


class ServiceViewSet(viewsets.ModelViewSet):
    queryset = Service.objects.all()
    serializer_class = ServiceSerializer
    permission_class = [permissions.IsAuthenticated]

URLs

# urls.py

from django.contrib import admin
from django.urls import path, include

from rest_framework import routers

from core import views


router = routers.DefaultRouter()
router.register(r"people", views.PeopleViewSet)
router.register(r"contracts", views.ContractViewSet)
router.register(r"services", views.ServiceViewSet)

urlpatterns = [
    path("api/v1/", include(router.urls)),
    path('admin/', admin.site.urls),
]

Paramètres

# settings.py

INSTALLED_APPS = [
    ...
    "rest_framework",
    ...
]

...

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10
}

A ce stade, en nous rendant sur lURL http://localhost:8000/api/v1, nous obtiendrons ceci:

api first example

Modèles et relations

Plus haut, nous avons utilisé une relation de type HyperlinkedModelSerializer. Cest une bonne manière pour autoriser des relations entre vos instances à partir de lAPI, mais il faut reconnaître que cela reste assez limité. Pour palier à ceci, il existe [plusieurs manières de représenter ces relations](https://www.django-rest-framework.org/api-guide/relations/): soit via un hyperlien, comme ci-dessus, soit en utilisant les clés primaires, soit en utilisant lURL canonique permettant daccéder à la ressource. La solution la plus complète consiste à intégrer la relation directement au niveau des données sérialisées, ce qui nous permet de passer de ceci (au niveau des contrats):

{
    "count": 1,
    "next": null,
    "previous": null,
    "results": [
        {
            "last_name": "Bond",
            "first_name": "James",
            "contract_set": [
                "http://localhost:8000/api/v1/contracts/1/",
                "http://localhost:8000/api/v1/contracts/2/"
            ]
        }
    ]
}

à ceci:

{
    "count": 1,
    "next": null,
    "previous": null,
    "results": [
        {
            "last_name": "Bond",
            "first_name": "James",
            "contract_set": [
                {
                    "date_begin": "2019-01-01",
                    "date_end": null,
                    "service": "http://localhost:8000/api/v1/services/1/"
                },
                {
                    "date_begin": "2009-01-01",
                    "date_end": "2021-01-01",
                    "service": "http://localhost:8000/api/v1/services/1/"
                }
            ]
        }
    ]
}

La modification se limite à surcharger la propriété, pour indiquer quelle consiste en une instance dun des sérialiseurs existants. Nous passons ainsi de ceci

class ContractSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Contract
        fields = ("date_begin", "date_end", "service")


class PeopleSerializer(serializers.HyperlinkedModelSerializer):

    class Meta:
        model = People
        fields = ("last_name", "first_name", "contract_set")

à ceci:

class ContractSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Contract
        fields = ("date_begin", "date_end", "service")


class PeopleSerializer(serializers.HyperlinkedModelSerializer):
    contract_set = ContractSerializer(many=True, read_only=True)

    class Meta:
        model = People
        fields = ("last_name", "first_name", "contract_set")

Nous ne faisons donc bien que redéfinir la propriété contract_set et indiquons quil sagit à présent dune instance de ContractSerializer, et quil est possible den avoir plusieurs. Cest tout.

Filtres et recherches

A ce stade, nous pouvons juste récupérer des informations présentes dans notre base de données, mais à part les parcourir, il est difficile den faire quelque chose.

Il est possible de jouer avec les URLs en définissant une nouvelle route ou avec les paramètres de lURL, ce qui demanderait alors de programmer chaque cas possible - sans que le consommateur ne puisse les déduire lui-même. Une solution élégante consiste à autoriser le consommateur à filtrer les données, directement au niveau de lAPI. Ceci peut être fait. Il existe deux manières de restreindre lensemble des résultats retournés:

  1. Soit au travers dune recherche, qui permet deffectuer une recherche textuelle, globale et par ensemble à un ensemble de champs,

  2. Soit au travers dun filtre, ce qui permet de spécifier une valeur précise à rechercher.

Dans notre exemple, la première possibilité sera utile pour rechercher une personne répondant à un ensemble de critères. Typiquement, /api/v1/people/?search=raymond bond ne nous donnera aucun résultat, alors que /api/v1/people/?search=james bond nous donnera le célèbre agent secret (qui a bien entendu un contrat chez nous…).

Le second cas permettra par contre de préciser que nous souhaitons disposer de toutes les personnes dont le contrat est ultérieur à une date particulière.

Utiliser ces deux mécanismes permet, pour Django-Rest-Framework, de proposer immédiatement les champs, et donc dinformer le consommateur des possibilités:

drf filters and searches

Recherches

La fonction de recherche est déjà implémentée au niveau de Django-Rest-Framework, et aucune dépendance supplémentaire nest nécessaire. Au niveau du viewset, il suffit dajouter deux informations:

...
from rest_framework import filters, viewsets
...

class PeopleViewSet(viewsets.ModelViewSet):
    ...
    filter_backends = [filters.SearchFilter]
    search_fields = ["last_name", "first_name"]
    ...

Filtres

Nous commençons par installer [le paquet django-filter](https://www.django-rest-framework.org/api-guide/filtering/#djangofilterbackend) et nous lajoutons parmi les applications installées:

λ pip install django-filter
Collecting django-filter
  Downloading django_filter-2.4.0-py3-none-any.whl (73 kB)
     |████████████████████████████████| 73 kB 2.6 MB/s
Requirement already satisfied: Django>=2.2 in c:\users\fred\sources\.venvs\rps\lib\site-packages (from django-filter) (3.1.7)
Requirement already satisfied: asgiref<4,>=3.2.10 in c:\users\fred\sources\.venvs\rps\lib\site-packages (from Django>=2.2->django-filter) (3.3.1)
Requirement already satisfied: sqlparse>=0.2.2 in c:\users\fred\sources\.venvs\rps\lib\site-packages (from Django>=2.2->django-filter) (0.4.1)
Requirement already satisfied: pytz in c:\users\fred\sources\.venvs\rps\lib\site-packages (from Django>=2.2->django-filter) (2021.1)
Installing collected packages: django-filter
Successfully installed django-filter-2.4.0

Une fois linstallée réalisée, il reste deux choses à faire:

  1. Ajouter django_filters parmi les applications installées:

  2. Configurer la clé DEFAULT_FILTER_BACKENDS à la valeur ['django_filters.rest_framework.DjangoFilterBackend'].

Vous avez suivi les étapes ci-dessus, il suffit dadapter le fichier settings.py de la manière suivante:

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10,
    'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend']
}

Au niveau du viewset, il convient dajouter ceci:

...
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets
...

class PeopleViewSet(viewsets.ModelViewSet):
    ...
    filter_backends = [DjangoFilterBackend]
    filterset_fields = ('last_name',)
    ...

A ce stade, nous avons deux problèmes:

  1. Le champ que nous avons défini au niveau de la propriété filterset_fields exige une correspondance exacte. Ainsi, /api/v1/people/?last_name=Bon ne retourne rien, alors que /api/v1/people/?last_name=Bond nous donnera notre agent secret préféré.

  2. Il nest pas possible daller appliquer un critère de sélection sur la propriété dune relation. Notre exemple proposant rechercher uniquement les relations dans le futur (ou dans le passé) tombe à leau.

Pour ces deux points, nous allons définir un nouveau filtre, en surchargeant une nouvelle classe dont la classe mère serait de type django_filters.FilterSet.

TO BE CONTINUED.

A noter quil existe un paquet [Django-Rest-Framework-filters](https://github.com/philipn/django-rest-framework-filters), mais il est déprécié depuis Django 3.0, puisquil se base sur django.utils.six qui nexiste à présent plus. Il faut donc le faire à la main (ou patcher le paquet…).