302 lines
8.3 KiB
TeX
302 lines
8.3 KiB
TeX
|
\chapter{Application Programming Interface}
|
||
|
|
||
|
|
||
|
\url{https://news.ycombinator.com/item?id=30221016\&utm_term=comment} vs
|
||
|
Django Rest Framework
|
||
|
|
||
|
Expliquer pourquoi une API est intéressante/primordiale/la première
|
||
|
chose à réaliser/le cadet de nos soucis.
|
||
|
|
||
|
Voir peut-être aussi
|
||
|
\url{https://christophergs.com/python/2021/12/04/fastapi-ultimate-tutorial/}
|
||
|
|
||
|
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
|
||
|
d'affectation. Quelque chose comme ceci:
|
||
|
|
||
|
\begin{minted}{python}
|
||
|
# 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
|
||
|
)
|
||
|
\end{minted}
|
||
|
|
||
|
|
||
|
|
||
|
\includegraphics{images/rest/models.png}
|
||
|
|
||
|
|
||
|
\section{Mise en place}
|
||
|
|
||
|
La configuration des points de terminaison de notre API est relativement
|
||
|
touffue. Il convient de:
|
||
|
|
||
|
\begin{enumerate}
|
||
|
\item
|
||
|
Configurer les sérialiseurs, càd. les champs que nous souhaitons
|
||
|
exposer au travers de l'API,
|
||
|
\item
|
||
|
Configurer les vues, càd le comportement de chacun des points de
|
||
|
terminaison,
|
||
|
\item
|
||
|
Configurer les points de terminaison eux-mêmes, càd les URLs
|
||
|
permettant d'accéder aux ressources.
|
||
|
\item
|
||
|
Et finalement ajouter quelques paramètres au niveau de notre
|
||
|
application.
|
||
|
\end{enumerate}
|
||
|
|
||
|
\subsection{Serialiseurs}
|
||
|
|
||
|
\begin{minted}{python}
|
||
|
# 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",)
|
||
|
\end{minted}
|
||
|
|
||
|
\subsection{Vues}
|
||
|
|
||
|
\begin{minted}{python}
|
||
|
# 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]
|
||
|
\end{minted}
|
||
|
|
||
|
\subsection{URLs}
|
||
|
|
||
|
\begin{minted}{python}
|
||
|
# 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),
|
||
|
]
|
||
|
\end{minted}
|
||
|
|
||
|
\begin{minted}{python}
|
||
|
# settings.py
|
||
|
|
||
|
INSTALLED_APPS = [
|
||
|
...
|
||
|
"rest_framework",
|
||
|
...
|
||
|
]
|
||
|
|
||
|
...
|
||
|
|
||
|
REST_FRAMEWORK = {
|
||
|
'DEFAULT_PAGINATION_CLASS':
|
||
|
'rest_framework.pagination.PageNumberPagination',
|
||
|
'PAGE_SIZE': 10
|
||
|
}
|
||
|
\end{minted}
|
||
|
|
||
|
\subsection{Résultat}
|
||
|
|
||
|
En nous rendant sur l'URL \texttt{http://localhost:8000/api/v1}, nous obtiendrons ceci:
|
||
|
|
||
|
\includegraphics{images/rest/api-first-example.png}
|
||
|
|
||
|
\section{Modéles et relations}
|
||
|
|
||
|
Plus haut, nous avons utilisé une relation de type \texttt{HyperlinkedModelSerializer}. C'est une bonne manière pour autoriser des relations entre vos instances à partir de l'API, mais il faut reconnaître que cela reste assez limité. Pour palier à ceci, il existe {[}plusieurs manières de représenter ces
|
||
|
\url{https://www.django-rest-framework.org/api-guide/relations/}:
|
||
|
|
||
|
\begin{enumerate}
|
||
|
\item Soit \textbf{via} un hyperlien, comme ci-dessus,
|
||
|
\item Soit en utilisant les clés primaires, soit en utilisant l'URL canonique permettant d'accéder à la ressource.
|
||
|
\end{enumerate}
|
||
|
|
||
|
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):
|
||
|
|
||
|
\begin{minted}{js}
|
||
|
{
|
||
|
"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/"
|
||
|
]
|
||
|
}
|
||
|
]
|
||
|
}
|
||
|
\end{minted}
|
||
|
|
||
|
à ceci:
|
||
|
|
||
|
\begin{minted}{js}
|
||
|
{
|
||
|
"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/"
|
||
|
}
|
||
|
]
|
||
|
}
|
||
|
]
|
||
|
}
|
||
|
\end{minted}
|
||
|
|
||
|
La modification se limite à \textbf{surcharger} la propriété, pour
|
||
|
indiquer qu'elle consiste en une instance d'un des sérialiseurs
|
||
|
existants. Nous passons ainsi de ceci
|
||
|
|
||
|
\begin{minted}{python}
|
||
|
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")
|
||
|
\end{minted}
|
||
|
|
||
|
à ceci:
|
||
|
|
||
|
\begin{minted}{python}
|
||
|
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")
|
||
|
\end{minted}
|
||
|
|
||
|
|
||
|
Nous ne faisons donc bien que redéfinir la propriété
|
||
|
\texttt{contract\_set} et indiquons qu'il s'agit à présent d'une
|
||
|
instance de \texttt{ContractSerializer}, et qu'il est possible d'en
|
||
|
avoir plusieurs. C'est tout.
|