Compare commits
210 Commits
239 changed files with 12268 additions and 4558 deletions
@ -1,7 +0,0 @@ |
|||
compile_pdf: |
|||
image: ddidier/sphinx-doc |
|||
script: |
|||
- make latexpdf |
|||
artifacts: |
|||
paths: |
|||
- build/latex/Gwift.pdf |
@ -0,0 +1,36 @@ |
|||
# Contibuer à Gwift |
|||
|
|||
(Grossièrement traduit et adapté de [ProGit](https://raw.githubusercontent.com/progit/progit2/main/CONTRIBUTING.md)) |
|||
|
|||
## Licence |
|||
|
|||
Quand vous ouvrez une *pull request*, vous acceptez d'appliquer la même licence que le projet à votre travail. |
|||
Aussi, vous acceptez de céder votre travail sous cette même licence. |
|||
Si vos modifications devaient apparaitre dans une version publiée, vous apparaitrez dans la [liste des contributeurs](book/contributors.adoc). |
|||
|
|||
## Signaler un problème |
|||
|
|||
Vérifiez avant tout s'il n'existe pas déjà un problème similaire, avant de créer un nouveau ticket. |
|||
|
|||
Aussi, vérifiez si ce même problème n'a pas déjà été corrigé dans les fichiers sources, mais n'aurait pas encore été pris en compte dans une version ultérieure du fichier PDF. |
|||
|
|||
## Petites corrections |
|||
|
|||
Les errata et clarifications basiques seront acceptés si nous sommes d'accord sur le fait qu'ils améliorent le contenu. |
|||
Vous pouvez ouvrir un ticket, de manière à ce que nous discutions de la manière dont il faut adresser le changement. |
|||
|
|||
Si vous n'avez jamais réalisé ceci, le [flow guide](https://guides.github.com/introduction/flow/) peut être utile au début. |
|||
|
|||
## Gros changements |
|||
|
|||
Ouvrez d'abord une discussion, de manière à démarrer. |
|||
Une grosse modification tend à être très subjective, et ne vise souvent qu'un petit nombre de lecteurs/utilisateurs. |
|||
|
|||
## Images et schéma |
|||
|
|||
Les images de ce livre sont générées en utilisant [Draw.io](draw.io). |
|||
|
|||
## Traductions |
|||
|
|||
Il n'y en a pas pour le moment 😉. |
|||
|
@ -0,0 +1,14 @@ |
|||
FROM miktex/miktex |
|||
|
|||
LABEL Description="Docker container from MiKTeX, Ubuntu 20.04, with Pygments" Version="1.0" |
|||
|
|||
RUN rm /etc/apt/sources.list.d/miktex.list |
|||
|
|||
RUN apt-get update |
|||
RUN apt-get install python3-pip -y |
|||
RUN pip install pygments |
|||
|
|||
WORKDIR /miktex/work |
|||
|
|||
CMD ["bash"] |
|||
|
@ -0,0 +1,55 @@ |
|||
\chapter{Gilded Roses} |
|||
|
|||
\url{https://github.com/emilybache/GildedRose-Refactoring-Kata} |
|||
|
|||
\begin{listing}[H] |
|||
\begin{minted}[tabsize=4]{python} |
|||
|
|||
# -*- coding: utf-8 -*- |
|||
|
|||
class GildedRose(object): |
|||
|
|||
def __init__(self, items): |
|||
self.items = items |
|||
|
|||
def update_quality(self): |
|||
for item in self.items: |
|||
if item.name != "Aged Brie" and item.name != "Backstage passes to a TAFKAL80ETC concert": |
|||
if item.quality > 0: |
|||
if item.name != "Sulfuras, Hand of Ragnaros": |
|||
item.quality = item.quality - 1 |
|||
else: |
|||
if item.quality < 50: |
|||
item.quality = item.quality + 1 |
|||
if item.name == "Backstage passes to a TAFKAL80ETC concert": |
|||
if item.sell_in < 11: |
|||
if item.quality < 50: |
|||
item.quality = item.quality + 1 |
|||
if item.sell_in < 6: |
|||
if item.quality < 50: |
|||
item.quality = item.quality + 1 |
|||
if item.name != "Sulfuras, Hand of Ragnaros": |
|||
item.sell_in = item.sell_in - 1 |
|||
if item.sell_in < 0: |
|||
if item.name != "Aged Brie": |
|||
if item.name != "Backstage passes to a TAFKAL80ETC concert": |
|||
if item.quality > 0: |
|||
if item.name != "Sulfuras, Hand of Ragnaros": |
|||
item.quality = item.quality - 1 |
|||
else: |
|||
item.quality = item.quality - item.quality |
|||
else: |
|||
if item.quality < 50: |
|||
item.quality = item.quality + 1 |
|||
|
|||
|
|||
class Item: |
|||
def __init__(self, name, sell_in, quality): |
|||
self.name = name |
|||
self.sell_in = sell_in |
|||
self.quality = quality |
|||
|
|||
def __repr__(self): |
|||
return "%s, %s, %s" % (self.name, self.sell_in, self.quality) |
|||
\end{minted} |
|||
\end{listing} |
@ -0,0 +1,71 @@ |
|||
\chapter{Monitoring Stack} |
|||
|
|||
InfluxDB ? https://www.influxdata.com/ |
|||
|
|||
\section{Visualisation} |
|||
|
|||
\begin{quote} |
|||
Grafana allows you to query, visualize, alert on and understand your metrics no matter where they are stored. |
|||
Create, explore, and share beautiful dashboards with your team and foster a data driven culture. |
|||
\end{quote} |
|||
|
|||
\section{Métriques} |
|||
|
|||
\begin{quote} |
|||
Graphite is an enterprise-ready monitoring tool that runs equally well on cheap hardware or Cloud infrastructure. |
|||
Teams use Graphite to track the performance of their websites, applications, business services, and networked servers. |
|||
It marked the start of a new generation of monitoring tools, making it easier than ever to store, retrieve, share, and visualize time-series data. |
|||
|
|||
Graphite was originally designed and written by Chris Davis at Orbitz in 2006 as side project that ultimately grew to be their foundational monitoring tool. |
|||
In 2008, Orbitz allowed Graphite to be released under the open source Apache 2.0 license. |
|||
Numerous large companies have deployed Graphite to production where it helps them to monitor their production e-commerce services and plan for growth. |
|||
\end{quote} |
|||
|
|||
Graphite does two things: |
|||
|
|||
\begin{enumerate} |
|||
\item |
|||
Store numeric time-series data |
|||
\item |
|||
Render graphs of this data on demand |
|||
\end{enumerate} |
|||
|
|||
What Graphite does not do is collect data for you, however there are some tools out there that know how to send data to graphite. Even though it often requires a little code, sending data to Graphite is very simple. |
|||
|
|||
Graphite consists of 3 software components: |
|||
|
|||
\begin{enumerate} |
|||
\item |
|||
\textbf{carbon} - a Twisted daemon that listens for time-series data |
|||
\item |
|||
\textbf{whisper} - a simple database library for storing time-series data (similar in design to RRD) |
|||
\item |
|||
\textbf{graphite webapp} - A Django webapp that renders graphs on-demand using Cairo |
|||
\end{enumerate} |
|||
|
|||
Feeding in your data is pretty easy, typically most of the effort is in collecting the data to begin with. As you send datapoints to Carbon, they become immediately available for graphing in the webapp. The webapp offers several ways to create and display graphs including a simple URL API for rendering that makes it easy to embed graphs in other webpages. |
|||
|
|||
\section{Logs} |
|||
|
|||
\begin{quote} |
|||
Loki brings together logs from all your applications and infrastructure in a single place. |
|||
By using the exact same service discovery and label model as Prometheus, Grafana Logs can systematically guarantee your logs have consistent metadata with your metrics, making it easy to move from one to the other. |
|||
\end{quote} |
|||
|
|||
Loki est l'équivalent (développé directement par Grafana) de Prometheus. |
|||
Il sera donc toujours nécessaire d'accumuler des logs au travers d'exporters. |
|||
|
|||
\begin{quote} |
|||
Loki se comporte comme Prometheus : c'est un logiciel que vous allez installer sur votre machine qui sert pour le monitoring de votre infrastructure et le laisser vivre sa vie. Comme son mentor, ou presque, il va falloir lui associer des exporters pour le gaver de données : Promtail. |
|||
|
|||
-- https://www.dadall.info/article698/loki-jouer-avec-ses-logs-dans-grafana |
|||
\end{quote} |
|||
|
|||
\section{Traces} |
|||
|
|||
Unlike other tracing tools, Grafana Traces does not index the traces which makes it possible to store orders of magnitude more trace data for the same cost, and removes the need for sampling. |
|||
|
|||
Stores orders of magnitude more trace data for the same cost, and removes the need for sampling. |
|||
Reduces the TCO by an order of magnitude and makes the system overall much easier to use. |
|||
|
|||
Grafana Traces is available as a containerized application, and you can run it on any orchestration engine like Kubernetes, Mesos, etc. The various services can be horizontally scaled depending on the workload on the ingest/query path. You can also use cloud native object storage, such as Google Cloud Storage, Amazon S3, or Azure Blob Storage. |
@ -0,0 +1,5 @@ |
|||
\chapter{Snippets} |
|||
|
|||
\section{Nettoyage de chaînes de caractères} |
|||
|
|||
Basically, NFC is how you normalize text that's meant to be displayed to a user on the web, and NFKC is how you normalize text that's used to for searching and guaranteeing uniqueness. \cite{django_for_startup_founders} |
@ -0,0 +1,2 @@ |
|||
\chapter{SonarQube} |
|||
|
@ -0,0 +1,316 @@ |
|||
\chapter{Administration} |
|||
|
|||
Cette partie est tellement puissante et performante, qu'elle pourrait laisser penser qu'il est possible de réaliser une application complète rien qu'en configurant l'administration. |
|||
C'est faux. |
|||
|
|||
L'administration est une sorte de tour de contrôle évoluée, un \emph{back office} sans transpirer; elle se base sur le modèle de données programmé et construit dynamiquement les formulaires qui lui est associé. |
|||
Elle joue avec les clés primaires, étrangères, les champs et types de champs par \href{https://fr.wikipedia.org/wiki/Introspection}{introspection}, et présente tout ce qu'il faut pour avoir du \href{https://fr.wikipedia.org/wiki/CRUD}{CRUD} \index{CRUD} \footnote{\emph{Create-Read-Update-Delete}, c'est-à-dire le fonctionnement par défaut de beaucoup d'applications}, c'est-à-dire tout ce qu'il faut pour ajouter, lister, modifier ou supprimer des informations. |
|||
|
|||
Son problème est qu'elle présente une courbe d'apprentissage asymptotique. |
|||
Il est \textbf{très} facile d'arriver rapidement à un bon résultat, au travers d'un périmètre de configuration relativement restreint. |
|||
Quoi que vous fassiez, il y a un moment où la courbe de paramétrage sera tellement ardue que vous aurez plus facile à développer ce que vous souhaitez ajouter en utilisant les autres concepts de Django. |
|||
|
|||
Cette interface doit rester dans les mains d'administrateurs ou de gestionnaires, et dans leurs mains à eux uniquement: il n'est pas question de donner des droits aux utilisateurs finaux (même si c'est extrêment tentant durant les premiers tours de roues). |
|||
Indépendamment de la manière dont vous allez l'utiliser et la configurer, vous finirez par devoir développer une "vraie" application, destinée aux utilisateurs classiques, et répondant à leurs besoins uniquement. |
|||
|
|||
Une bonne idée consiste à développer l'administration dans un premier temps, en \textbf{gardant en tête qu'il sera nécessaire de développer des concepts spécifiques}. |
|||
Dans cet objectif, l'administration est un outil exceptionel, qui permet de valider un modèle, de créer des objets rapidement et de valider les liens qui existent entre eux. |
|||
|
|||
C'est aussi un excellent outil de prototypage et de preuve de concept. |
|||
|
|||
Elle se base sur plusieurs couches que l'on a déjà (ou on va bientôt) aborder (suivant le sens de lecture que vous préférez): |
|||
|
|||
\begin{enumerate} |
|||
\item |
|||
Le modèle de données |
|||
\item |
|||
Les validateurs |
|||
\item |
|||
Les formulaires |
|||
\item |
|||
Les widgets |
|||
\end{enumerate} |
|||
|
|||
\section{Le modèle de données} |
|||
|
|||
Comme expliqué ci-dessus, le modèle de données est constité d'un ensemble de champs typés et de relations. |
|||
L'administration permet de décrire les données qui peuvent être modifiées, en y associant un ensemble (basique) de permissions. |
|||
|
|||
Si vous vous rappelez de l'application que nous avions créée dans la première partie, les URLs reprenaient déjà la partie suivante: |
|||
|
|||
\begin{minted}[tabsize=4]{python} |
|||
from django.contrib import admin |
|||
from django.urls import path |
|||
from gwift.views import wish_details |
|||
|
|||
urlpatterns = [ |
|||
path('admin/', admin.site.urls), |
|||
[...] |
|||
] |
|||
\end{minted} |
|||
|
|||
Cette URL signifie que la partie \texttt{admin} est déjà active et accessible à l'URL \texttt{\textless{}mon\_site\textgreater{}/admin}. |
|||
C'est le seul prérequis pour cette partie. |
|||
|
|||
Chaque application nouvellement créée contient par défaut un fichier \texttt{admin.py}, dans lequel il est possible de déclarer les ensembles de données seront accessibles ou éditables. |
|||
Ainsi, si nous partons du modèle basique que nous avions détaillé plus tôt, avec des souhaits et des listes de souhaits: |
|||
|
|||
\begin{minted}[tabsize=4]{python} |
|||
# gwift/wish/models.py |
|||
|
|||
from django.db import models |
|||
|
|||
|
|||
class WishList(models.Model): |
|||
name = models.CharField(max_length=255) |
|||
|
|||
|
|||
class Item(models.Model): |
|||
name = models.CharField(max_length=255) |
|||
wishlist = models.ForeignKey(WishList, on_delete=models.CASCADE) |
|||
\end{minted} |
|||
|
|||
Nous pouvons facilement arriver au résultat suivant, en ajoutant |
|||
quelques lignes de configuration dans ce fichier \texttt{admin.py}: |
|||
|
|||
\begin{minted}[tabsize=4]{python} |
|||
from django.contrib import admin |
|||
|
|||
from .models import Item, WishList |
|||
|
|||
|
|||
admin.site.register(Item) |
|||
admin.site.register(WishList) |
|||
\end{minted} |
|||
|
|||
|
|||
\begin{itemize} |
|||
\item |
|||
Nous importons les modèles que nous souhaitons gérer dans l'admin |
|||
\item |
|||
Et nous les déclarons comme gérables. Cette dernière ligne implique aussi qu'un modèle pourrait ne pas être disponible du tout, ce qui n'activera simplement aucune opération de lecture ou modification. |
|||
\end{itemize} |
|||
|
|||
Il nous reste une seule étape à réaliser: créer un nouvel utilisateur. |
|||
Pour cet exemple, notre gestion va se limiter à une gestion manuelle; nous aurons donc besoin d'un \emph{super-utilisateur}, que nous pouvons créer grâce à la commande \texttt{python\ manage.py\ createsuperuser}. |
|||
|
|||
\begin{verbatim} |
|||
$ python manage.py createsuperuser |
|||
Username (leave blank to use 'fred'): fred |
|||
Email address: fred@root.org |
|||
Password: ****** |
|||
Password (again): ****** |
|||
Superuser created successfully. |
|||
\end{verbatim} |
|||
|
|||
|
|||
\begin{figure}[H] |
|||
\centering |
|||
\includegraphics{images/django/django-site-admin.png} |
|||
\caption{Connexion au site d'administration} |
|||
\end{figure} |
|||
|
|||
\begin{figure}[H] |
|||
\centering |
|||
\includegraphics{images/django/django-site-admin-after-connection.png} |
|||
\caption{Administration} |
|||
\end{figure} |
|||
|
|||
Ceci nous permet déjà d'ajouter des éléments (Items), des listes de souhaits, de visualiser les actions récentes, voire de gérer les autorisations attribuées aux utilisateurs, comme les membres du staff ou les administrateurs. |
|||
|
|||
\section{Quelques conseils de base} |
|||
|
|||
\begin{enumerate} |
|||
\item |
|||
Surchargez la méthode \texttt{str(self)} pour chaque classe que vous aurez définie dans le modèle. |
|||
Cela permettra de construire une représentation textuelle pour chaque instance de votre classe. |
|||
Cette information sera utilisée un peu partout dans le code, et donnera une meilleure idée de ce que l'on manipule. |
|||
En plus, cette méthode est également appelée lorsque l'administration historisera une action (et comme cette étape sera inaltérable, autant qu'elle soit fixée dans le début). |
|||
\item |
|||
La méthode \texttt{get\_absolute\_url(self)} retourne l'URL à laquelle on peut accéder pour obtenir les détails d'une instance. Par exemple: |
|||
\end{enumerate} |
|||
|
|||
\begin{minted}[tabsize=4]{python} |
|||
def get_absolute_url(self): |
|||
return reverse('myapp.views.details', args=[self.id]) |
|||
\end{minted} |
|||
|
|||
\begin{enumerate} |
|||
\def\labelenumi{\arabic{enumi}.} |
|||
\item |
|||
Les attributs \texttt{Meta}: |
|||
\end{enumerate} |
|||
|
|||
\begin{minted}[tabsize=4]{python} |
|||
class Meta: |
|||
ordering = ['-field1', 'field2'] |
|||
verbose_name = 'my class in singular' |
|||
verbose_name_plural = 'my class when is in a list!' |
|||
\end{minted} |
|||
|
|||
\begin{enumerate} |
|||
\item |
|||
Le titre: |
|||
|
|||
\begin{itemize} |
|||
\item |
|||
Soit en modifiant le template de l'administration |
|||
\item |
|||
Soit en ajoutant l'assignation suivante dans le fichier \texttt{urls.py} : \texttt{admin.site.site\_header\ =\ "SuperBook\ Secret\ Area}. |
|||
\end{itemize} |
|||
\item |
|||
Prefetch |
|||
\end{enumerate} |
|||
|
|||
\url{https://hackernoon.com/all-you-need-to-know-about-prefetching-in-django-f9068ebe1e60?gi=7da7b9d3ad64} |
|||
|
|||
\url{https://medium.com/@hakibenita/things-you-must-know-about-django-admin-as-your-app-gets-bigger-6be0b0ee9614} |
|||
|
|||
En gros, le problème de l'admin est que si on fait des requêtes imbriquées, on va flinguer l'application et le chargement de la page. |
|||
La solution consiste à utiliser la propriété \texttt{list\_select\_related} de la classe d'Admin, afin d'appliquer une jointure par défaut et et gagner en performances. |
|||
|
|||
\subsection{admin.ModelAdmin} |
|||
|
|||
La classe \texttt{admin.ModelAdmin} que l'on retrouvera principalement dans le fichier \texttt{admin.py} de chaque application contiendra la définition de ce que l'on souhaite faire avec nos données dans l'administration. Cette classe (et sa partie Meta) |
|||
|
|||
\subsection{L'affichage} |
|||
|
|||
Comme l'interface d'administration fonctionne (en trèèèès) gros comme un CRUD auto-généré, on trouve par défaut la possibilité de : |
|||
|
|||
\begin{enumerate} |
|||
\item |
|||
Créer de nouveaux éléments |
|||
\item |
|||
Lister les éléments existants |
|||
\item |
|||
Modifier des éléments existants |
|||
\item |
|||
Supprimer un élément en particulier. |
|||
\end{enumerate} |
|||
|
|||
Les affichages sont donc de deux types: en liste et au détail. |
|||
|
|||
Pour les affichages en liste, le plus simple consiste à jouer sur la propriété \texttt{list\_display}. |
|||
|
|||
Par défaut, la première colonne va accueillir le lien vers le formulaire d'édition. |
|||
On peut donc modifier ceci, voire créer de nouveaux liens vers d'autres éléments en construisant des URLs dynamiquement. |
|||
|
|||
Voir aussi comment personnaliser le fil d'Ariane ? |
|||
|
|||
\section{Filtres} |
|||
|
|||
Chaque liste permet de spécifier des filtres spécifiques; ceux-ci peuvent être: |
|||
|
|||
\begin{enumerate} |
|||
\item \textbf{Appliqués à la liste} (\texttt{list\_filter}) |
|||
\item \textbf{Horizontaux} (\texttt{filter\_horizontal}) |
|||
\item \textbf{Verticaux} (\texttt{filter\_vertical}) |
|||
\item \textbf{Temporels} (\texttt{date\_hierarchy} |
|||
\end{enumerate} |
|||
|
|||
\subsection{Appliqués à la liste} |
|||
|
|||
|
|||
\subsection{Horizontaux} |
|||
|
|||
|
|||
\subsection{Verticaux} |
|||
|
|||
|
|||
\subsection{Temporels} |
|||
|
|||
|
|||
\section{Permissions} |
|||
|
|||
On l'a dit plus haut, il vaut mieux éviter de proposer un accès à l'administration à vos utilisateurs. |
|||
Il est cependant possible de configurer des permissions spécifiques pour certains groupes, en leur autorisant certaines actions de visualisation/ajout/édition ou suppression. |
|||
|
|||
Cela se joue au niveau du \texttt{ModelAdmin}, en implémentant les méthodes suivantes: |
|||
|
|||
\begin{minted}[tabsize=4]{python} |
|||
def has_add_permission(self, request): |
|||
return True |
|||
|
|||
def has_delete_permission(self, request): |
|||
return True |
|||
|
|||
def has_change_permission(self, request): |
|||
return True |
|||
\end{minted} |
|||
|
|||
On peut accéder aux informations de l'utilisateur actuellement connecté au travers de l'objet \texttt{request.user}. |
|||
|
|||
\section{Relations} |
|||
|
|||
\subsection{Relations 1-N} |
|||
|
|||
Les relations 1-n sont implémentées au travers de formsets (que l'on a normalement déjà décrits plus haut). L'administration permet de les définir d'une manière extrêmement simple, grâce à quelques propriétés. |
|||
|
|||
L'implémentation consiste tout d'abord à définir le comportement du type d'objet référencé (la relation -N), puis à inclure cette définition au niveau du type d'objet référençant (la relation 1-). |
|||
|
|||
\begin{minted}[tabsize=4]{python} |
|||
class WishInline(TabularInline): |
|||
model = Wish |
|||
|
|||
class Wishlist(admin.ModelAdmin): |
|||
... |
|||
inlines = [WishInline] |
|||
... |
|||
\end{minted} |
|||
|
|||
Et voilà : l'administration d'une liste de souhaits (\emph{Wishlist}) pourra directement gérer des relations multiples vers des souhaits. |
|||
|
|||
\subsection{Autocomplétion} |
|||
|
|||
Parler de l'intégration de select2. |
|||
|
|||
\section{Forms} |
|||
|
|||
|
|||
|
|||
\section{Présentation} |
|||
|
|||
Parler ici des \texttt{fieldsets} et montrer comment on peut regrouper des champs dans des groupes, ajouter un peu de JavaScript, ... |
|||
|
|||
\section{Actions sur des sélections} |
|||
|
|||
Les actions permettent de partir d'une liste d'éléments, et autorisent |
|||
un utilisateur à appliquer une action sur une sélection d'éléments. Par |
|||
défaut, il existe déjà une action de \textbf{suppression}. |
|||
|
|||
Les paramètres d'entrée sont : |
|||
|
|||
\begin{enumerate} |
|||
\def\labelenumi{\arabic{enumi}.} |
|||
\item |
|||
L'instance de classe |
|||
\item |
|||
La requête entrante |
|||
\item |
|||
Le queryset correspondant à la sélection. |
|||
\end{enumerate} |
|||
|
|||
\begin{minted}[tabsize=4]{python} |
|||
def double_quantity(self, request, queryset): |
|||
for obj in queryset.all(): |
|||
obj.field += 1 |
|||
obj.save() |
|||
double_quantity.short_description = "Doubler la quantité des souhaits." |
|||
\end{minted} |
|||
|
|||
|
|||
Et pour informer l'utilisateur de ce qui a été réalisé, on peut aussi |
|||
lui passer un petit message: |
|||
|
|||
\begin{minted}[tabsize=4]{python} |
|||
if rows_updated = 0: |
|||
self.message_user(request, "Aucun élément n'a été impacté.") |
|||
else: |
|||
self.message_user(request, "{} élément(s) mis à jour".format(rows_updated)) |
|||
\end{minted} |
|||
|
|||
\section{Documentation} |
|||
|
|||
Nous l'avons dit plus haut, l'administration de Django a également la possibilité de rendre accessible la documentation associée à un modèle de données. |
|||
Pour cela, il suffit de suivre les bonnes pratiques, puis \href{https://docs.djangoproject.com/en/stable/ref/contrib/admin/admindocs/}{d'activer la documentation à partir des URLs}: |
|||
|
@ -0,0 +1,396 @@ |
|||
\chapter{Application Programming Interface} |
|||
|
|||
|
|||
\url{https://news.ycombinator.com/item?id=30221016\&utm_term=comment} vs |
|||
Django Rest Framework vs Marshmallow |
|||
|
|||
Expliquer pourquoi une API est intéressante/primordiale/la première chose à réaliser/le cadet de nos soucis: |
|||
|
|||
\begin{itemize} |
|||
\item |
|||
Intéressante: ouverture |
|||
\item |
|||
Primordiale: services |
|||
\item |
|||
La première chose à réaliser: mobile-first |
|||
\item |
|||
Le cadet de nos soucis: monolithique (cf. Rework) |
|||
\end{itemize} |
|||
|
|||
Voir peut-être aussi |
|||
\url{https://christophergs.com/python/2021/12/04/fastapi-ultimate-tutorial/} |
|||
|
|||
Remarque : quatre statuts = le minimum syndical. \cite[p. 297]{restful_web_apis} : |
|||
|
|||
\begin{enumerate} |
|||
\item |
|||
\textbf{200 (OK)}. |
|||
Tout va bien. |
|||
Le document qui se trouve dans le corps de la réponse, s'il y en a un, est la représentation d'une ressource. |
|||
\item |
|||
\textbf{301 (Moved Permanently)}. |
|||
Reçu lorsque la ressource n'est plus disponible à cette URI. |
|||
\item |
|||
\textbf{400 (Bad Request)}. |
|||
Indique qu'il y a eu un problème côté client. |
|||
Le document qui se trouve dans le corps de la réponse, s'il existe, est un message d'erreur. |
|||
Avec un peu de chance, le client a la possibilité d'interpréter ce message d'erreur, afin de corriger le problème. |
|||
\item |
|||
\textbf{500 (Internal Server Error)}. |
|||
Il y a un problème côté serveur. Le document présent dans le corps de la réponse, toujours s'il existe, indique le problème. |
|||
Comme celui-ci se situe au niveau du serveur, le client ne pourra rien faire pour le résoudre. |
|||
\end{enumerate} |
|||
|
|||
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 peut être relativement touffue. |
|||
Pour cette raison, il convient de s'infliger à suivre une structure qui soit similaire pour chaque point de terminaison \cite[Predictability, Rule \#1]{django_for_startup_founders}. |
|||
Il convient de: |
|||
|
|||
\begin{enumerate} |
|||
\item |
|||
\textbf{Spécifier les permissions} |
|||
\item |
|||
\textbf{Copier et assainir les éléments communiqués en entrée vers des variables locales} |
|||
\item |
|||
\textbf{Valider les données d'entrée} |
|||
\item |
|||
\textbf{Enforce business requirements} |
|||
\item |
|||
\textbf{Perform busines logic} |
|||
\item |
|||
\textbf{Retourner une réponse HTTP} |
|||
\end{enumerate} |
|||
|
|||
-> Répartir les responsabilités selon les composants ci-dessous |
|||
|
|||
\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} |
|||
|
|||
\section{Django Rest Framework} |
|||
|
|||
\subsection{Serialiseurs} |
|||
|
|||
Les sérialiseurs agissent litérallement comme des \texttt{forms}, mais au niveau de l'API. |
|||
Ils se basent sur un modèle, définit au niveau de la \texttt{class Meta}, permettent de choisir les champs qui seront sérialisés, définissent différentes méthodes d'accès à des propriétés spécifiques et des méthodes de validation. |
|||
Tout comme les forms. |
|||
Par exemple, avec Django Rest Framework: |
|||
|
|||
\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} |
|||
|
|||
\subsection{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. |
|||
|
|||
\subsection{Conclusions} |
|||
|
|||
Django-Rest-Framework est une librarie complète qui ajoute énormément de possibilités. |
|||
Cependant \cite{django_for_startup_founders}: |
|||
|
|||
\begin{enumerate} |
|||
\item |
|||
La documentation est \textbf{réellement} compliquée. |
|||
Tout nouveau développeur doit appréhender, comprendre et assimiler cette documentation et tous les concepts sous-jacents. |
|||
Ceci inclut notamment le fait que tous les verbes HTTP ont été "traduits" (GET -> retrieve, POST -> create, ...). |
|||
Ceci a du sens par rapport à la définition d'une interface REST-compliant, mais ajoute une complexité mentale relativement lourde. |
|||
\item |
|||
Certains concepts de réutilisation sont tellement compliqués qu'ils prennent plus de temps à mettre en place qu'à écrire une ligne de code Python classique |
|||
\item |
|||
Les sérialiseurs peuvent rapidement devenir difficiles à lire ou relire, spécifiquement lorsque nous utilisons des \textit{nested serializers} ou lorsque les concepts de désérialisation sont abordés. |
|||
\end{enumerate} |
|||
|
|||
\section{Marshmallow} |
|||
|
|||
\textit{Marshmallow} est une alternative plus légère à Django-Rest-Framework, et qui présente une interface plus claire, ainsi qu'une documentation plus concise et facile à comprendre. |
|||
|
|||
Une solution plus facile que les sérializeurs de DRF consistera à |
|||
|
|||
\begin{enumerate} |
|||
\item |
|||
Gérer la validation de données en utilisant Marshmallow |
|||
\item |
|||
Sérialiser les données en utilisant du code Python \cite{django_for_startup_founders}. |
|||
\end{enumerate} |
|||
|
|||
\section{Ninja} |
|||
|
|||
... |
|||
|
|||
\section{OData} |
|||
|
|||
... |
|||
|
|||
\section{Bonnes pratiques} |
|||
|
|||
\subsection{Authentification} |
|||
|
|||
\subsection{Validation des données} |
|||
|
|||
\subsection{Utilisation d'une API Gateway} |
|||
|
|||
\subsection{Rate limiting} |
|||
|
|||
\subsection{Partage des données nécessaires uniquement} |
@ -0,0 +1,957 @@ |
|||
|
|||
|
|||
\chapter{Eléments d'architecture} |
|||
|
|||
\begin{quote} |
|||
Un code mal pensé entraîne nécessairement une perte d'énergie et de temps. |
|||
Il est plus simple de réfléchir, au moment de la conception du programme, à une architecture permettant une meilleure maintenabilité que de devoir corriger un code "sale" \emph{a posteriori}. |
|||
C'est pour aider les développeurs à rester dans le droit chemin que les principes SOLID ont été énumérés. \cite{gnu_linux_mag_hs_104} |
|||
\end{quote} |
|||
|
|||
Les principes SOLID, introduit par Robert C. Martin dans les années 2000 pour orienter le développement de modules, sont les suivants: |
|||
|
|||
\begin{enumerate} |
|||
\item |
|||
\textbf{SRP} - Single responsibility principle - Principe de Responsabilité Unique |
|||
\item |
|||
\textbf{OCP} - Open-closed principle |
|||
\item |
|||
\textbf{LSP} - Liskov Substitution |
|||
\item |
|||
\textbf{ISP} - Interface ségrégation principle |
|||
\item |
|||
\textbf{DIP} - Dependency Inversion Principle |
|||
\end{enumerate} |
|||
|
|||
Des équivalents à ces directives existent au niveau des composants, puis au niveau architectural: |
|||
\begin{enumerate} |
|||
\item |
|||
Reuse/release équivalence principle, |
|||
\item |
|||
\textbf{CCP} - Common Closure Principle, |
|||
\item |
|||
\textbf{CRP} - Common Reuse Principle. |
|||
\end{enumerate} |
|||
|
|||
\begin{figure}[H] |
|||
\centering |
|||
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/arch-comp-modules.png}} |
|||
\end{figure} |
|||
|
|||
\section{Modules} |
|||
|
|||
\subsection{Single Responsility Principle} \label{SRP} |
|||
|
|||
Le principe de responsabilité unique conseille de disposer de concepts ou domaines d'activité qui ne s'occupent chacun que d'une et une seule chose. |
|||
Ceci rejoint (un peu) la \href{https://en.wikipedia.org/wiki/Unix_philosophy}{Philosophie Unix}, documentée par Doug McIlroy et qui demande de "\emph{faire une seule chose, mais de le faire bien}" \cite{unix_philosophy}. |
|||
|
|||
Selon ce principe, une classe ou un élément de programmation ne doit donc pas avoir plus d'une seule raison de changer. |
|||
|
|||
Plutôt que de centraliser le maximum de code à un seul endroit ou dans une seule classe par convenance ou commodité \footnote{Aussi appelé \emph{God-Like object}}, le principe de responsabilité unique suggère que chaque classe soit responsable d'un et un seul concept. |
|||
|
|||
Une manière de voir les choses consiste à différencier les acteurs ou les intervenants: imaginez disposer d'une classe représentant des données de membres du personnel; ces données pourraient être demandées par trois acteurs: |
|||
|
|||
\begin{enumerate} |
|||
\item Le CFO (Chief Financial Officer) |
|||
\item Le CTO (Chief Technical Officer) |
|||
\item Le COO (Chief Operating Officer) |
|||
\end{enumerate} |
|||
|
|||
Chacun d'entre eux aura besoin de données et d'informations relatives à ces membres du personnel, et provenant donc d'une même source de données centralisée. |
|||
Mais chacun d'entre eux également besoin d'une représentation différente ou de traitements distincts. \cite{clean_architecture} |
|||
|
|||
Nous sommes d'accord qu'il s'agit à chaque fois de données liées aux employés; celles-ci vont cependant un cran plus loin et pourraient nécessiter des ajustements spécifiques en fonction de l'acteur concerné et de la manière dont il souhaite disposer des données. |
|||
Dès que possible, identifiez les différents acteurs et demandeurs, en vue de prévoir les modifications qui pourraient être demandées par l'un d'entre eux. |
|||
|
|||
Dans le cas d'un élément de code centralisé, une modification induite par un des acteurs pourrait ainsi avoir un impact sur les données utilisées par les autres. |
|||
|
|||
Vous trouverez ci-dessous une classe \texttt{Document}, dont chaque instance est représentée par trois propriétés: son titre, son contenu et sa date de publication. |
|||
Une méthode \texttt{render} permet également de proposer (très grossièrement) un type de sortie et un format de contenu: \texttt{XML} ou \texttt{Markdown}. |
|||
|
|||
\begin{listing}[H] |
|||
\begin{minted}[tabsize=4]{Python} |
|||
class Document: |
|||
def __init__(self, title, content, published_at): |
|||
self.title = title |
|||
self.content = content |
|||
self.published_at = published_at |
|||
|
|||
def render(self, format_type): |
|||
if format_type == "XML": |
|||
return """<?xml version = "1.0"?> |
|||
<document> |
|||
<title>{}</title> |
|||
<content>{}</content> |
|||
<publication_date>{}</publication_date> |
|||
</document>""".format( |
|||
self.title, |
|||
self.content, |
|||
self.published_at.isoformat() |
|||
) |
|||
|
|||
if format_type == "Markdown": |
|||
import markdown |
|||
return markdown.markdown(self.content) |
|||
|
|||
raise ValueError( |
|||
"Format type '{}' is not known".format(format_type) |
|||
) |
|||
\end{minted} |
|||
\caption{Un convertisseur de document un peu bateau} |
|||
\end{listing} |
|||
|
|||
|
|||
Lorsque nous devrons ajouter un nouveau rendu (Atom, OpenXML, \ldots) il sera nécessaire de modifier la classe \texttt{Document}. |
|||
Ceci n'est: |
|||
|
|||
\begin{enumerate} |
|||
\item Ni intuitif: \emph{ce n'est pas le document qui doit savoir dans quels formats il peut être converti} |
|||
\item Ni conseillé: \emph{lorsque nous aurons quinze formats différents à gérer, il sera nécessaire d'avoir autant de conditions dans cette méthode}. |
|||
\end{enumerate} |
|||
|
|||
En suivant le principe de responsabilité unique, une bonne pratique consiste à créer une nouvelle classe de rendu pour chaque type de format à gérer: |
|||
|
|||
\begin{listing}[H] |
|||
\begin{minted}[tabsize=4]{Python} |
|||
class Document: |
|||
def __init__(self, title, content, published_at): |
|||
self.title = title |
|||
self.content = content |
|||
self.published_at = published_at |
|||
|
|||
class DocumentRenderer: |
|||
def render(self, document): |
|||
if format_type == "XML": |
|||
return """<?xml version = "1.0"?> |
|||
<document> |
|||
<title>{}</title> |
|||
<content>{}</content> |
|||
<publication_date>{}</publication_date> |
|||
</document>""".format( |
|||
self.title, |
|||
self.content, |
|||
self.published_at.isoformat() |
|||
) |
|||
|
|||
if format_type == "Markdown": |
|||
import markdown |
|||
return markdown.markdown(self.content) |
|||
|
|||
raise ValueError( |
|||
|