From ae881a52fea5fbf551b936ca16789dae0d16ee0f Mon Sep 17 00:00:00 2001 From: Fred Pauchet Date: Fri, 17 Feb 2023 10:46:18 +0100 Subject: [PATCH] Update notes --- annexes/gilded-roses.tex | 55 ++ chapters/api.tex | 33 + chapters/continuous-integration.tex | 22 +- chapters/maintenability.tex | 129 +++- chapters/python.tex | 612 ++++++++++++++++-- .../execution-process-memory.drawio.png | Bin 0 -> 17187 bytes images/12factors/separate-run-steps.png | Bin 0 -> 7346 bytes parts/deployment.tex | 43 +- references.bib | 8 + 9 files changed, 802 insertions(+), 100 deletions(-) create mode 100644 annexes/gilded-roses.tex create mode 100644 images/12factors/execution-process-memory.drawio.png create mode 100644 images/12factors/separate-run-steps.png diff --git a/annexes/gilded-roses.tex b/annexes/gilded-roses.tex new file mode 100644 index 0000000..47e9a60 --- /dev/null +++ b/annexes/gilded-roses.tex @@ -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} diff --git a/chapters/api.tex b/chapters/api.tex index f7a327d..8777302 100755 --- a/chapters/api.tex +++ b/chapters/api.tex @@ -20,6 +20,27 @@ Expliquer pourquoi une API est intéressante/primordiale/la première chose à r 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: @@ -361,3 +382,15 @@ Une solution plus facile que les sérializeurs de DRF consistera à \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} diff --git a/chapters/continuous-integration.tex b/chapters/continuous-integration.tex index 52b4bee..be06881 100755 --- a/chapters/continuous-integration.tex +++ b/chapters/continuous-integration.tex @@ -1,10 +1,10 @@ \chapter{Intégration continue / Déploiement Continu} \begin{quote} - New code should never go live without first being audited for correctness and security. \cite{django_for_startup_founders} + \textit{New code should never go live without first being audited for correctness and security}. \cite{django_for_startup_founders} \end{quote} -Les processus d'intégration continue et de déploiement continu (respectivement, CI/CD - Continuous Integration / Continuous Deployment) permettent d'automatiser les étapes de vérifications, autrement réalisées par un être humain, voire manuellement, par des automates réagissant à des évènements particuliers - typiquement, une nouvelle version du code, un nouveau \textit{commit}, la mise à jour d'un des composants, \ldots +Les processus d'intégration continue \footnote{CI - Continuous Integration} et de déploiement continu \footnote{CD - Continuous Deployment} permettent d'automatiser des étapes de vérifications, initialement réalisées par un être humain, par des automates réagissant à des évènements particuliers: typiquement, une nouvelle version du code, un nouveau \textit{commit}, la mise à jour d'un des composants, \ldots \section{Intégration continue} @@ -25,11 +25,10 @@ matrix: - 3.8 pipeline: - static_checks: - image: acidrain/python-poetry:${PYTHON_VERSION} + lint: + image: python:${PYTHON_VERSION} commands: - - python -V - - poetry -V + - pip3 install poetry - poetry install - poetry run flake8 src/ - poetry run pylint src/ @@ -37,18 +36,15 @@ pipeline: - poetry run mypy . unit_tests: - image: acidrain/python-poetry:${PYTHON_VERSION} + image: python:${PYTHON_VERSION} commands: - - python -V - - poetry -V - - poetry install + - pip3 install poetry + - poetry config virtualenvs.create false - poetry run pytest --cov-report term-missing tests/unit/ \end{minted} - - \section{Déploiement continu} Le déploiement continu est une extension de l'intégration continue, qui est jusqu'au-boutiste à s'occuper lui-même de mettre à disposition une nouvelle version, sous réserve qu'elle ait passé les différentes étapes du pipeline. -C'est ainsi qu'Amazon ou certaines grandes \footnote{ie. envahissantes} entreprises publient jusqu'à plusieurs miliers de nouvelles fonctionnalités \textbf{par jour}. +C'est ainsi qu'Amazon ou certaines grandes \footnote{ie. \textit{"envahissantes"}} entreprises publient jusqu'à plusieurs miliers de nouvelles fonctionnalités \textbf{par jour}. diff --git a/chapters/maintenability.tex b/chapters/maintenability.tex index 0a3b420..40ea0ce 100755 --- a/chapters/maintenability.tex +++ b/chapters/maintenability.tex @@ -95,14 +95,23 @@ Ce dépôt n'est pas uniquement destiné à hébergé le code source, mais égal Chaque installation ou configuration doit être toujours réalisée de la même manière, et doit pouvoir être répétée quel que soit l'environnement cible. Ceci permet d'éviter que l'application n'utilise une dépendance qui ne soit installée que sur l'un des sytèmes de développement, et qu'elle soit difficile, voire impossible, à réinstaller sur un autre environnement. +\begin{memorizebox} Chaque dépendance devra être déclarée dans un fichier présent au niveau de la base de code. Lors de la création d'un nouvel environnement vierge, il suffira d'utiliser ce fichier comme paramètre afin d'installer les prérequis au bon fonctionnement de notre application, afin d'assurer une reproductibilité quasi parfaite de l'environnement d'exécution. +\end{memorizebox} -Il est important de bien "épingler" les versions liées aux dépendances de l'application. +Il est important de bien "épingler" la version liée à chacune des dépendances de l'application. Ceci peut éviter des effets de bord comme une nouvelle version d'une librairie dans laquelle un bug aurait pu avoir été introduit.\footnote{Le paquet PyLint dépend par exemple d'Astroid; \href{https://github.com/PyCQA/pylint-django/issues/343}{en janvier 2022}, ce dernier a été mis à jour sans respecter le principe de versions sémantiques et introduisant une régression. PyLint spécifiait que sa dépendance avec Astroid devait respecter une version ~2.9. Lors de sa mise à jour en 2.9.1, Astroid a introduit un changement majeur, qui faisait planter Pylint. L'épinglage explicite aurait pu éviter ceci.}. Dans le cas de Python, la déclaration explicite et l'épinglage pourront notamment être réalisés au travers de \href{https://pypi.org/project/pip/}{PIP} ou \href{https://python-poetry.org/}{Poetry}. -La majorité des langages modernes proposent des mécanismes similaires (\href{https://rubygems.org/}{Gem} pour Ruby, \href{https://www.npmjs.com/}{NPM} pour NodeJS, \ldots) +La majorité des langages modernes proposent des mécanismes similaires : + +\begin{itemize} + \item \href{https://rubygems.org/}{Gem} pour Ruby, + \item \href{https://www.npmjs.com/}{NPM} pour NodeJS, + \item \href{Maven}{https://maven.apache.org/} pour Java, + \item \ldots +\end{itemize} \section{Configuration applicative} @@ -118,15 +127,48 @@ que: En pratique, toute information susceptible d'évoluer ou de changer (un seuil, une ressource externe, un couple utilisateur/mot de passe, \ldots) doit se trouver dans un fichier ou dans une variable d'environnement, et doit être facilement modifiable. -Ceci permet de paramétrer facilement un environnement (par exemple, un container), simplement en modifiant une variable de configuration qui spécifierait la base de données sur laquelle l'application devra se connecter. +En pratique, avec du code Python/Django, nous pourrions utiliser la libraririe \href{django-environ}{https://pypi.org/project/django-environ/}, qui permet de gérer facilement ce fichier de configuration : -Toute clé de configuration (nom du serveur de base de données, adresse d'un service Web externe, clé d'API pour l'interrogation d'une ressource, \ldots) sera définie directement au niveau de l'hôte - à aucun moment, nous ne devons trouver un mot de passe en clair dans le dépôt source ou une valeur susceptible d'évoluer, écrite en dur dans le code. \footnote{Ainsi, nous pourrions faire une \href{https://github.com/search?q=filenamefilename:.env DB_USERNAME&type=Code}{recherche sur Github} pour retrouver certaines variables d'environnement qui auraient été laissées en dur dans le code source de certains projets. Le \href{https://github.com/techgaun/github-dorks}{dépôt suivant} liste quelques idées de variables à rechercher\ldots}. +\begin{listing}[H] + \begin{minted}[tabsize=4]{python} + import environ + import os -Au moment de développer une nouvelle fonctionnalité, réfléchissez si -l'un des paramètres utilisés risquerait de subir une modification ou s'il concerne un principe de sécurité: ce composant peut concerner une nouvelle chaîne de connexion, un point de terminaison nécessaire à télécharger des données officielles ou un chemin vers un répertoire partagé pour y déposer un fichier. + env = environ.Env( + DEBUG=(bool, False) + ) -Le risque est de se retrouver avec une liste colossale de paramètres; pensez à leur assigner une variable par défaut. -Par exemple, Gitea expose la liste suivante de paramètres \footnote{\url{https://docs.gitea.io/en-us/config-cheat-sheet/}}; il serait impossible de tous les configurer un à un avant de pouvoir démarrer une instance. + BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + environ.Env.read_env(os.path.join(BASE_DIR, '.env')) + + DEBUG = env('DEBUG') + SECRET_KEY = env('SECRET_KEY') + + DATABASES = { + # Parse database connection url strings + # like psql://user:pass@127.0.0.1:8458/db + 'default': env.db(), + } + + CACHES = { + 'default': env.cache(), + } + \end{minted} +\end{listing} + +Il suffira ensuite d'appeler ces variables grâce à \texttt{from django.conf import settings} pour récupérer la valeur qui aura été configurée. +Ceci permet de paramétrer facilement n'importe quel environnement, simplement en modifiant une variable de ce fichier de configuration. + +Toute clé de configuration, qu'il s'agisse d'une chaîne de connexion vers une base de données, l'adresse d'un service Web externe, une clé d'API pour l'interrogation d'une ressource, d'un chemin vers un fichier à interprêter ou à transférer, \ldots, ou n'importe quelle valeur susceptible d'évoluer, ne doit se trouver écrite en dur dans le code. + +\begin{dangerbox} +Il est important de ne surtout pas ajouter ce fichier \texttt{.env} dans le dépôt de sources: à aucun moment, nous ne devons y trouver de mot de passe ou d'information confidentielle en clair. \footnote{Ainsi, nous pourrions faire une \href{https://github.com/search?q=filenamefilename:.env DB_USERNAME&type=Code}{recherche sur Github} pour retrouver certaines variables d'environnement qui auraient été laissées en dur dans le code source de certains projets. Le \href{https://github.com/techgaun/github-dorks}{dépôt suivant} liste quelques idées de variables à rechercher\ldots}. +\end{dangerbox} + +Au moment de développer une nouvelle fonctionnalité, réfléchissez si l'un des paramètres utilisés risquerait de subir une modification ou s'il concerne un principe de sécurité. + +Le risque de se retrouver avec une liste colossale de paramètres n'est cependant pas négligeable; pensez à leur assigner une variable par défaut \footnote{Par exemple, Gitea expose \href{la liste suivante de paramètres disponibles}{https://docs.gitea.io/en-us/config-cheat-sheet/}; il serait impossible d'utiliser cette plateforme si chacun d'entre eux devait obligatoirement être configuré avant de pouvoir démarrer une instance.} \section{Ressources externes} @@ -142,7 +184,7 @@ Ces ressources sont donc spécifiés grâce à des variables d'environnement, et \caption{Gestion des ressources attachées} \end{graphic} -Nous serons ainsi ravis de simplement pouvoir modifier la chaîne de connexion \texttt{sqlite:////tmp/my-tmp-sqlite.db} en \texttt{psql://user:pass@127.0.0.1:8458/db} lorsque ce sera nécessaire, sans avoir à recompiler ou redéployer les modifications. +Nous serons ainsi ravis de pouvoir simplement modifier la chaîne de connexion \texttt{sqlite:////tmp/my-tmp-sqlite.db} en \texttt{psql://user:pass@127.0.0.1:8458/db} lorsque ce sera nécessaire, sans avoir à recompiler ou redéployer les modifications. \section{Séparation des phases de construction} @@ -150,30 +192,41 @@ Nous serons ainsi ravis de simplement pouvoir modifier la chaîne de connexion \ \item La \textbf{construction} (\emph{build}) convertit un code source en un ensemble de fichiers exécutables, associé à une version et à une transaction dans le système de gestion de sources. \item - La \textbf{mise à disposition} (\emph{release}) associe cet ensemble à une configuration prête à être exécutée, + La \textbf{mise à disposition} (\emph{release}) associe cet ensemble à une configuration prête à être exécutée sur un environnement désigné, \item La phase d'\textbf{exécution} (\emph{run}) démarre les processus nécessaires au bon fonctionnement de l'application. \end{enumerate} -\begin{graphic}{images/12factors/release.png} +\begin{graphic}{images/12factors/separate-run-steps.png} \caption{Séparation des phases de construction, mise à disposition et exécution} \end{graphic} Parmi les solutions possibles, nous pourrions nous pourrions nous baser sur les \emph{releases} de Gitea, sur un serveur d'artefacts (\href{https://fr.wikipedia.org/wiki/Capistrano_(logiciel)}{Capistrano}), voire directement au niveau de forge logicielle (Gitea, Github, Gitlab, \ldots). +\begin{memorizebox} + Dans le cas de Python, la phase de construction (\textit{Build}) correspondra plutôt à une phase d'\textit{empaquettage} (\textit{packaging}). + Une fois préparée, la librairie ou l'application pourra être publiée sur \url{pypi.org} ou un dépôt géré en interne. + La phase de \emph{release} revient à associer une version spécifiquement empaquêtée pour l'associer à une configuration particulière. +\end{memorizebox} + \section{Mémoire des processus d'exécution} Toute information stockée en mémoire ou sur disque ne doit pas altérer le comportement futur de l'application, par exemple après un redémarrage non souhaité. -Dit autrement, l'exécution de l'application ne doit pas dépendre de la présence d'une information locale à son environnement d'exécution. -Pratiquement, si l'application devait rencontrer un problème - par exemple suite à un problème matériel -, il est nécessaire qu'elle puisse redémarrer rapidement, éventuellement en étant déployée sur un autre serveur. -Toute information stockée physiquement durant son exécution sur le premier hôte, sera perdue lors de son déploiement ultérieur à un autre endroit. +\begin{memorizebox} + L'exécution de l'application ne doit pas dépendre de la présence d'une information locale à son environnement d'exécution. +\end{memorizebox} + +En pratique, si l'application devait rencontrer un problème - par exemple suite à un problème matériel, une coupure réseau, \ldots -, il est nécessaire qu'elle puisse redémarrer rapidement, éventuellement en étant déployée sur un autre serveur. +Toute information stockée physiquement sera alors perdue, puisque le contexte d'exécution aura été déplacée à un autre endroit. Lors de l'initialisation ou de la réinitialisation d'une application, la solution consiste à jouer sur les variables d'environnement (cf. \#3) et sur les informations que l'on pourra trouver au niveau des ressources attachées (cf \#4), afin de faire en sorte que les informations et données primordiales puissent être récupérées ou reconstruites, et donc réutilisées, sans altérer le comportement attendu. -Ceci joue également sur les possibilités de mise à l'échelle: en ajoutant un nouveau serveur d'exécution, si une donnée indispensable à son fonctionnement devait se trouver sur la seule machine où elle est actuellement exécutée, elle en sera inconnue. +\begin{graphic}{images/12factors/execution-process-memory.drawio.png} + \caption{La mémoire des processus d'exécution doit rester accessible, même après leur disparition} +\end{graphic} --> Ajouter un schéma. +Ceci joue énormément sur les possibilités de mise à l'échelle: lors de l'ajout d'un nouveau serveur, il est indispensable qu'il puisse accéder à l'ensemble des informations nécessaires au bon fonctionnement de l'application. \section{Liaison des ports} @@ -189,34 +242,37 @@ Le serveur (= l'hôte) choisit d'appliquer une correspondance entre "son" port 4 \section{Connaissance et confiance des processus systèmes} -Comme décrit plus haut (cf. \#6), l'application doit utiliser des processus \emph{stateless} (sans état). -Nous pouvons créer et utiliser des processus supplémentaires pour tenir plus facilement une lourde charge, ou dédier des particuliers pour certaines tâches: +Comme décrit plus haut (cf. \#6), l'application doit utiliser des processus sans état (\textit{stateless}), c'est-à-dire qu'aucune information ne doit être stockée localement ou ne doit dépendre d'une information précédemment acquise et accessible uniquement à un processus en particulier. +Nous pouvons créer et utiliser des processus supplémentaires pour tenir plus facilement une lourde charge ou dédier des particuliers pour certaines tâches: \begin{itemize} \item Requêtes HTTP \emph{via} des processus Web; - \item \emph{long-running} jobs pour des processus asynchrones, + \item Jobs asynchrones pour des tâches à effectuer en arrière-plan, \item \ldots \end{itemize} -Si l'un des concepts dont vous avez besoin existe sur l'hôte hébergeant l'application, ne vous fatiguez pas: utilisez le et ne réinventez pas la roue. +Si l'un des concepts dont vous avez besoin existe déjà et peut être utilisé, ne vous fatiguez pas: utilisez le et ne réinventez pas surtout la roue. + +\begin{graphic}{images/12factors/process-types.png} + \caption{Charge et types de processus} +\end{graphic} \section{Arrêts élégants et démarrages rapides} -Par "arrêt élégant", nous voulons surtout éviter le fameux \texttt{kill\ -9\ \textless{}pid\textgreater{}} (ou équivalent), ou tout autre arrêt brutal d'un processus qui nécessiterait une intervention urgente du -superviseur. +Par "arrêt élégant", nous voulons surtout éviter le fameux \texttt{kill -9 } (ou équivalent), ou tout autre arrêt brutal d'un processus qui nécessiterait une intervention urgente du superviseur. En prévoyant une manière élégante d'envoyer un signal de terminaison: \begin{enumerate} - \item Les requêtes en cours peuvent se terminer au mieux, - \item Le démarrage rapide de nouveaux processus améliorera la balance d'un processus en cours d'extinction vers des processus tout frais, en autorisant l'exécution parallèle d'anciens et de nouveaux "types" de processus + \item Les requêtes en cours peuvent se terminer sans impact majeur, + \item Le démarrage (rapide) de nouveaux processus améliore la balance de charge d'un processus en cours d'extinction vers des processus tout frais, en autorisant l'exécution parallèle d'anciens et de nouveaux "types" de processus \end{enumerate} -L'intégration de ces mécanismes dès les premières étapes de développement limitera les perturbations et facilitera la prise en compte d'arrêts inopinés (problème matériel, redémarrage du système hôte, etc.). - -\begin{graphic}{images/12factors/process-types.png} - +\begin{graphic}{images/12factors/process-type-chronology.png} + \caption{Evolution des types de processus, en fonction des arrêts et démarrages} \end{graphic} +L'intégration de ces mécanismes dès les premières étapes de développement limitera les perturbations et facilitera la prise en compte d'arrêts inopinés (problème matériel, redémarrage du système hôte, etc.). + \section{Similarité des environnements} \begin{dangerbox} @@ -236,14 +292,19 @@ Faire en sorte que tous les environnements soient les plus similaires possibles Conserver des environements similaires limite ce genre de désagréments. \end{memorizebox} -Ceci permet également de proposer à nos utilisateurs un bac à sable dans lequel ils pourront explorer et réaliser des expérimentations en toute sécurité, sans quel cela n'ait d'impact sur un \textit{réel} environnement de production, où les conséquences pourraient être beaucoup plus graves. \cite[p. 9]{data_design} +Ceci permet également de proposer à nos utilisateurs un bac à sable dans lequel ils pourront explorer et réaliser des expérimentations en toute sécurité, sans quel cela n'ait d'impact sur un \textit{réel} environnement de production, où les conséquences pourraient être beaucoup plus graves. \cite[p. 9]{data_intensive} -\section{Journaux de flux évènementiels} +\section{Flux d'évènements} +Les journaux d'exécution sont la seule manière pour une application de communiquer son état et de décrire ce qu'elle est occupée à faire ou à réaliser. + +Que nous soyons en phase de développement sur le poste d'une personne de l'équipe, avec une sortie console ou sur une machine de production avec un envoi vers une instance \href{https://www.graylog.org/}{Greylog} ou \href{https://sentry.io/welcome/}{Sentry}, le routage des évènements doit être réalisé en fonction de la nécessité du contexte d'exécution et de sa criticité. + +\begin{memorizebox} Une application ne doit jamais se soucier de l'endroit où les évènements qui la concerne seront écrits, mais se doit simplement de les envoyer sur la sortie \texttt{stdout}. -De cette manière, que nous soyons en développement sur le poste d'un développeur avec une sortie console ou sur une machine de production avec un envoi vers une instance \href{https://www.graylog.org/}{Greylog} ou \href{https://sentry.io/welcome/}{Sentry}, le routage des journaux sera réalisé en fonction de sa nécessité et de sa criticité, et non pas parce que le développeur l'a spécifié en dur dans son code. +\end{memorizebox} -Cette phase est critique, dans la mesure où les journaux d'exécution sont la seule manière pour une application de communiquer son état vers l'extérieur: recevoir une erreur interne de serveur est une chose; pouvoir obtenir un minimum d'informations, voire un contexte de plantage complet en est une autre. +Cette phase est essentielle, dans la mesure où recevoir une erreur interne de serveur est une chose; pouvoir obtenir un minimum d'informations, voire un contexte de plantage complet en est une autre. La différence entre ces deux points vous fera, au mieux, gagner plusieurs heures sur l'identification ou la résolution d'un problème. \section{Isolation des tâches administratives} @@ -262,7 +323,7 @@ Au fur et à mesure que le code est délibérément construit pour être mainten \item Conservation précise des dépendances nécessaires \item - Résilience des services et plantage élégant (i.e. \textbf{sans finir un SEGFAULT avec l'OS dans les choux et un écran bleu}) + Résilience des services et plantage élégant (i.e. sans finir un SEGFAULT avec l'OS dans les choux et un écran bleu) \item Compatibilité entre les différentes versions (n+1, \ldots) \item @@ -272,5 +333,5 @@ Au fur et à mesure que le code est délibérément construit pour être mainten \item Traces des requêtes provenant des utilisateurs, indépendamment des services utilisés \item - Centralisation de la configuration (\textbf{via} ZooKeeper, par exemple) + Centralisation de la configuration (\textit{via} ZooKeeper, par exemple) \end{enumerate} diff --git a/chapters/python.tex b/chapters/python.tex index d7b822f..6f2cc6d 100755 --- a/chapters/python.tex +++ b/chapters/python.tex @@ -185,31 +185,31 @@ Ca évite, dans une boucle, de vérifier si la clé existe déjà, et si pas, de def on_command_1(): print("Oune") - + def on_command_2(): print("Dos") - + def on_command_3(): print("Tresse") - + def prrrt(): print("Won't be printed") \end{minted} - + Tu peux retrouver les trois méthodes \texttt{on\_command\_*}` via la fonction `dir()`: - + \begin{minted}{python} >>> import module_chelou >>> dir(module_chelou) ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'on_command_1', 'on_command_2', 'on_command_3', 'prrrt'] - + >>> for function_name in [x for x in dir(module_chelou) if "on_command_" in x]: ... getattr(module_chelou, function_name)() ... Oune Dos Tresse - + ``` \end{minted} @@ -301,17 +301,42 @@ Toute fonction dans la complexité est supérieure à cette valeur sera considé \section{Conventions de documentation} -Python étant un langage interprété fortement typé, il est plus que conseillé, au même titre que les tests unitaires que nous verrons plus bas, de documenter son code. -Ceci impose une certaine rigueur, tout en améliorant énormément la qualité, la compréhension et la reprise du code par une tierce personne. -Ceci implique aussi de \textbf{tout} documenter: les modules, les paquets, les classes, les fonctions, méthodes, \ldots ce qui peut aller à contrecourant d'autres pratiques \cite{clean_code}{53-74} ; il y a donc une juste mesure à prendre entre "tout documenter" et "tout bien documenter": +Il y a une énorme différence entre la documentation du code et les commentaires associés au code: +\begin{enumerate} + \item \textbf{Les commentaires} décrivent comment le code fonctionne + \item \textbf{La documentation du code} décrit comment utiliser le code. +\end{enumerate} + +Pour que le code soit utilisé correctement, il est dès lors important de documenter : + +\begin{enumerate} + \item + Ce que représente une classe ou une API + \item + Les attentes d'une interface : codes d'erreurs, statuts possibles, etc. +\end{enumerate} + +Les commentaires sont cependant utiles lorsque le code n'est pas évident à lire ou lorsque certaines optimisations auraient été appliquées dans un soucis de performances. +Dans la majorité des cas cependant, si des commentaires doivent être rédigés pour que le code devienne lisible, c'est que ce code n'est pas correctement écrit. \cite{clean_code}{53-74} \footnote{\url{https://www.youtube.com/watch?v=Bf7vDBBOBUA}} +Nous pouvons constater un risque de décorrélation entre le code et ses commentaires : + +\begin{listing}[!ht] + \begin{minted}[tabsize=4]{python} + # Check that the maximum length of the array if less than 8 + if len(array) < 10: + ... + \end{minted} +\end{listing} + +Il y a donc une juste mesure à prendre entre "tout documenter" (les modules, les paquets, les classes, les fonctions, méthodes, \ldots) et "bien documenter": \begin{itemize} \item Inutile d'ajouter des watermarks, auteurs, \ldots Git ou tout VCS s'en sortira très bien et sera beaucoup plus efficace que n'importe quelle chaîne de caractères que vous pourriez indiquer et qui sera fausse dans six mois, \item - Inutile de décrire quelque chose qui est évident; documenter la méthode \mintinline{python}{get_age()} d'une personne n'aura pas beaucoup d'intérêt + Inutile de décrire quelque chose qui est évident; documenter la méthode \mintinline{python}{get_age()} d'une personne n'aura pas beaucoup d'intérêt, \item S'il est nécessaire de décrire un comportement au sein-même d'une fonction, avec un commentaire \emph{inline}, c'est que ce comportement pourrait être extrait dans une nouvelle fonction (qui, elle, pourra être documentée proprement). \end{itemize} @@ -352,7 +377,141 @@ A priori, vous pourriez tout à fait utiliser le vôtre, sous réserve que les c \subsection{Numpy} -A remplir +\href{https://numpydoc.readthedocs.io/en/latest/format.html}{Numpy} sépare chaque section avec un pseudo-formatage de titre. +Les sections sont définies selon les types suivants: + +\begin{itemize} + \item + Une courte description (de la classe, du module, de la fonction, \ldots). + Chaque module devrait avoir une docstring au tout début du fichier. + Cette docstring peut s'étendre sur plusieurs lignes (\textit{Short summary}) + \item + Des avertissements liés à la dépréciation: quand la fonctionnalité viendra à disparaitre (dans quelle version), les raisons de sa dépréciation, et les recommandations pour arriver au même résultat. (\textit{Deprecated}) + \item + Une description plus précise des fonctionnalités proposées (et pas de la manière dont l'implémentation est réalisée). (\textit{Extended summary}) + \item + Les paramètres attendus, leur type et une description (\textit{Parameters}) + \item + La ou les valeurs de retour, accompagnées de leur type et d'une description (\textit{Returns}) + \item + Les valeurs générées (\textit{yields}) dans le cas de générateurs (\textit{Yields}) + \item + Un objet attendu par la méthode \texttt{send()} (toujours dans le cas de générateurs) (\textit{Receives}) + \item + D'autres paramètres, notamment dans le cas des paramètres \texttt{*args} et \texttt{**kwargs}. (\textit{Other parameters}) + \item + Les exceptions levées (\textit{Raises}) + \item + Les avertissements destinés à l'utilisateur (\textit{Warnings}) + \item + Une section "Pour continuer\ldots" (\textit{See also}) + \item Des notes complémentaires (\textit{Notes}) + \item Des références (\textit{References}) + \item Des exemples (\textit{Examples}) +\end{itemize} + + +\begin{listing}[!ht] + \begin{minted}[tabsize=4]{python} +"""Docstring for the example.py module. + +Modules names should have short, all-lowercase names. The module name may +have underscores if this improves readability. + +""" + +import os + +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt + + +def foo(var1, var2, *args, long_var_name="hi", only_seldom_used_keyword=0, **kwargs): + r"""Summarize the function in one line. + + Several sentences providing an extended description. Refer to + variables using back-ticks, e.g. `var`. + + Parameters + ---------- + + : + + + Returns + ------- + type + Explanation of anonymous return value of type ``type``. + describe : type + Explanation of return value named `describe`. + out : type + Explanation of `out`. + type_without_description + + Other Parameters + ---------------- + only_seldom_used_keyword : int, optional + Infrequently used parameters can be described under this optional + section to prevent cluttering the Parameters section. + **kwargs : dict + Other infrequently used keyword arguments. Note that all keyword + arguments appearing after the first parameter specified under the + Other Parameters section, should also be described under this + section. + + Raises + ------ + BadException + Because you shouldn't have done that. + + See Also + -------- + numpy.array : Relationship (optional). + numpy.ndarray : Relationship (optional), which could be fairly long, in + which case the line wraps here. + numpy.dot, numpy.linalg.norm, numpy.eye + + Notes + ----- + Notes about the implementation algorithm (if needed). + + This can have multiple paragraphs. + + You may include some math: + + .. math:: X(e^{j\omega } ) = x(n)e^{ - j\omega n} + + And even use a Greek symbol like :math:`\omega` inline. + + References + ---------- + Cite the relevant literature, e.g. [1]_. You may also cite these + references in the notes section above. + + .. [1] O. McNoleg, "The integration of GIS, remote sensing, + expert systems and adaptive co-kriging for environmental habitat + modelling of the Highland Haggis using object-oriented, fuzzy-logic + and neural-network techniques," Computers & Geosciences, vol. 22, + pp. 585-588, 1996. + + Examples + -------- + These are written in doctest format, and should illustrate how to + use the function. + + >>> a = [1, 2, 3] + >>> print([x + 3 for x in a]) + [4, 5, 6] + >>> print("a\nb") + a + b + """ + + pass +\end{minted} + \caption{Un exemple de documentation Numpy} +\end{listing} \subsection{Napoleon} @@ -428,6 +587,309 @@ Un exemple (encore) plus complet peut être trouvé le dépôt sphinxcontrib-napoleon}. Et ici, nous tombons peut-être dans l'excès de zèle: +\begin{listing}[!ht] + \begin{minted}[tabsize=4]{python} +# -*- coding: utf-8 -*- +"""Example Google style docstrings. + +This module demonstrates documentation as specified by the `Google Python +Style Guide`_. Docstrings may extend over multiple lines. Sections are created +with a section header and a colon followed by a block of indented text. + +Example: + Examples can be given using either the ``Example`` or ``Examples`` + sections. Sections support any reStructuredText formatting, including + literal blocks:: + + $ python example_google.py + +Section breaks are created by resuming unindented text. Section breaks +are also implicitly created anytime a new section starts. + +Attributes: + module_level_variable1 (int): Module level variables may be documented in + either the ``Attributes`` section of the module docstring, or in an + inline docstring immediately following the variable. + + Either form is acceptable, but the two should not be mixed. Choose + one convention to document module level variables and be consistent + with it. + +Todo: + * For module TODOs + * You have to also use ``sphinx.ext.todo`` extension + +.. _Google Python Style Guide: + http://google.github.io/styleguide/pyguide.html + +""" + +module_level_variable1 = 12345 + +module_level_variable2 = 98765 +"""int: Module level variable documented inline. + +The docstring may span multiple lines. The type may optionally be specified +on the first line, separated by a colon. +""" + + +def function_with_types_in_docstring(param1, param2): + """Example function with types documented in the docstring. + + `PEP 484`_ type annotations are supported. If attribute, parameter, and + return types are annotated according to `PEP 484`_, they do not need to be + included in the docstring: + + Args: + param1 (int): The first parameter. + param2 (str): The second parameter. + + Returns: + bool: The return value. True for success, False otherwise. + + .. _PEP 484: + https://www.python.org/dev/peps/pep-0484/ + + """ + + +def function_with_pep484_type_annotations(param1: int, param2: str) -> bool: + """Example function with PEP 484 type annotations. + + Args: + param1: The first parameter. + param2: The second parameter. + + Returns: + The return value. True for success, False otherwise. + + """ + + +def module_level_function(param1, param2=None, *args, **kwargs): + """This is an example of a module level function. + + Function parameters should be documented in the ``Args`` section. The name + of each parameter is required. The type and description of each parameter + is optional, but should be included if not obvious. + + If \*args or \*\*kwargs are accepted, + they should be listed as ``*args`` and ``**kwargs``. + + The format for a parameter is:: + + name (type): description + The description may span multiple lines. Following + lines should be indented. The "(type)" is optional. + + Multiple paragraphs are supported in parameter + descriptions. + + Args: + param1 (int): The first parameter. + param2 (:obj:`str`, optional): The second parameter. Defaults to None. + Second line of description should be indented. + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. + + Returns: + bool: True if successful, False otherwise. + + The return type is optional and may be specified at the beginning of + the ``Returns`` section followed by a colon. + + The ``Returns`` section may span multiple lines and paragraphs. + Following lines should be indented to match the first line. + + The ``Returns`` section supports any reStructuredText formatting, + including literal blocks:: + + { + 'param1': param1, + 'param2': param2 + } + + Raises: + AttributeError: The ``Raises`` section is a list of all exceptions + that are relevant to the interface. + ValueError: If `param2` is equal to `param1`. + + """ + if param1 == param2: + raise ValueError('param1 may not be equal to param2') + return True + + +def example_generator(n): + """Generators have a ``Yields`` section instead of a ``Returns`` section. + + Args: + n (int): The upper limit of the range to generate, from 0 to `n` - 1. + + Yields: + int: The next number in the range of 0 to `n` - 1. + + Examples: + Examples should be written in doctest format, and should illustrate how + to use the function. + + >>> print([i for i in example_generator(4)]) + [0, 1, 2, 3] + + """ + for i in range(n): + yield i + + +class ExampleError(Exception): + """Exceptions are documented in the same way as classes. + + The __init__ method may be documented in either the class level + docstring, or as a docstring on the __init__ method itself. + + Either form is acceptable, but the two should not be mixed. Choose one + convention to document the __init__ method and be consistent with it. + + Note: + Do not include the `self` parameter in the ``Args`` section. + + Args: + msg (str): Human readable string describing the exception. + code (:obj:`int`, optional): Error code. + + Attributes: + msg (str): Human readable string describing the exception. + code (int): Exception error code. + + """ + + def __init__(self, msg, code): + self.msg = msg + self.code = code + + +class ExampleClass(object): + """The summary line for a class docstring should fit on one line. + + If the class has public attributes, they may be documented here + in an ``Attributes`` section and follow the same formatting as a + function's ``Args`` section. Alternatively, attributes may be documented + inline with the attribute's declaration (see __init__ method below). + + Properties created with the ``@property`` decorator should be documented + in the property's getter method. + + Attributes: + attr1 (str): Description of `attr1`. + attr2 (:obj:`int`, optional): Description of `attr2`. + + """ + + def __init__(self, param1, param2, param3): + """Example of docstring on the __init__ method. + + The __init__ method may be documented in either the class level + docstring, or as a docstring on the __init__ method itself. + + Either form is acceptable, but the two should not be mixed. Choose one + convention to document the __init__ method and be consistent with it. + + Note: + Do not include the `self` parameter in the ``Args`` section. + + Args: + param1 (str): Description of `param1`. + param2 (:obj:`int`, optional): Description of `param2`. Multiple + lines are supported. + param3 (:obj:`list` of :obj:`str`): Description of `param3`. + + """ + self.attr1 = param1 + self.attr2 = param2 + self.attr3 = param3 #: Doc comment *inline* with attribute + + #: list of str: Doc comment *before* attribute, with type specified + self.attr4 = ['attr4'] + + self.attr5 = None + """str: Docstring *after* attribute, with type specified.""" + + @property + def readonly_property(self): + """str: Properties should be documented in their getter method.""" + return 'readonly_property' + + @property + def readwrite_property(self): + """:obj:`list` of :obj:`str`: Properties with both a getter and setter + should only be documented in their getter method. + + If the setter method contains notable behavior, it should be + mentioned here. + """ + return ['readwrite_property'] + + @readwrite_property.setter + def readwrite_property(self, value): + value + + def example_method(self, param1, param2): + """Class methods are similar to regular functions. + + Note: + Do not include the `self` parameter in the ``Args`` section. + + Args: + param1: The first parameter. + param2: The second parameter. + + Returns: + True if successful, False otherwise. + + """ + return True + + def __special__(self): + """By default special members with docstrings are not included. + + Special members are any methods or attributes that start with and + end with a double underscore. Any special member with a docstring + will be included in the output, if + ``napoleon_include_special_with_doc`` is set to True. + + This behavior can be enabled by changing the following setting in + Sphinx's conf.py:: + + napoleon_include_special_with_doc = True + + """ + pass + + def __special_without_docstring__(self): + pass + + def _private(self): + """By default private members are not included. + + Private members are any methods or attributes that start with an + underscore and are *not* special. By default they are not included + in the output. + + This behavior can be changed such that private members *are* included + by changing the following setting in Sphinx's conf.py:: + + napoleon_include_private_with_doc = True + + """ + pass + + def _private_without_docstring(self): + pass + \end{minted} + \caption{Un exemple de documentation Napoleon} +\end{listing} + \begin{figure}[!ht] \centering \scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/napoleon-module-level-docstring.png}} @@ -442,6 +904,7 @@ générer automatiquement le squelette de documentation d'un bloc de code: Nous le verrons plus loin, Django permet de rendre la documentation immédiatement accessible depuis l'interface d'administration. Toute information pertinente peut donc lier le code à un cas d'utilisation concret, et rien n'est jamais réellement perdu. + \section{Vérification du code \index{lint}} Il existe plusieurs niveaux de \emph{linters}: @@ -449,16 +912,11 @@ Il existe plusieurs niveaux de \emph{linters}: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item - Le premier niveau concerne - \href{https://pypi.org/project/pycodestyle/}{pycodestyle} - (anciennement, \texttt{pep8} justement\ldots\hspace{0pt}), qui analyse - votre code à la recherche d'erreurs de convention. + Le premier niveau est assuré par \href{https://pypi.org/project/pycodestyle/}{pycodestyle}, qui analyse votre code à la recherche d'erreurs de convention de style. \item - Le deuxième niveau concerne - \href{https://pypi.org/project/pyflakes/}{pyflakes}. Pyflakes est un - \emph{simple} \footnote{Ce n'est pas moi qui le dit, c'est la doc du - projet} programme qui recherchera des erreurs parmi vos fichiers - Python. + Le deuxième niveau est \href{https://pypi.org/project/pyflakes/}{pyflakes}. + Il s'agit d'un \emph{simple} \footnote{Ce n'est pas moi qui le dit, c'est la doc du + projet} programme qui recherchera différents types d'erreurs parmi votre code source. \item Le troisième niveau est \href{https://pypi.org/project/flake8/}{Flake8}, qui regroupe les deux @@ -469,18 +927,12 @@ Il existe plusieurs niveaux de \emph{linters}: cercles à l'Enfer} est \href{https://pylint.org/}{PyLint}. \end{enumerate} -PyLint est le meilleur ami de votre \emph{moi} futur, un peu comme quand -vous prenez le temps de faire la vaisselle pour ne pas avoir à la faire -le lendemain: il rendra votre code soyeux et brillant, en posant des -affirmations spécifiques. A vous de les traiter en corrigeant le code ou -en apposant un \emph{tag} indiquant que vous avez pris connaissance de -la remarque, que vous en avez tenu compte, et que vous choisissez malgré -tout de faire autrement. +PyLint est le meilleur ami de votre \emph{moi} futur, un peu comme quand vous prenez le temps de faire la vaisselle pour ne pas avoir à la faire le lendemain: il rendra votre code soyeux et brillant, en posant des affirmations spécifiques. +A vous de les traiter en corrigeant le code ou en apposant un \emph{tag} indiquant que vous avez pris connaissance de la remarque, que vous en avez tenu compte, et que vous choisissez malgré tout de faire autrement. -\begin{listing}[H] - \includegraphics{images/calvin/time-machine.jpg} +\begin{graphic}{images/calvin/time-machine.jpg} \caption{Calvin \& Hobbes se rendent dans le futur pour récupérer les devoirs que son moi-du-futur aura fait (et pour ne pas avoir à les faire lui-même).} -\end{listing} +\end{graphic} Pour vous donner une idée, voici ce que cela pourrait donner avec un code pas très propre et qui ne sert à rien: @@ -506,6 +958,8 @@ code pas très propre et qui ne sert à rien: \caption{Un morceau de code qui ne sert à rien} \end{listing} +\subsection{Flake8} + Avec Flake8, nous obtiendrons ceci: \begin{listing}[!ht] @@ -537,7 +991,10 @@ Nous trouvons des erreurs: votre application se prendra méchamment les pieds dans le tapis). \end{itemize} -L'étape d'après consiste à invoquer pylint. Lui, il est directement moins conciliant: +\subsection{Pylint} + +L'étape d'après consiste à invoquer pylint. +Lui, il est directement moins conciliant: \begin{verbatim} $ pylint test.py @@ -601,16 +1058,97 @@ Cet élément peut être: \begin{enumerate} \item \textbf{Une ligne de code} -\item \textbf{Un bloc de code} - une fonction, une méthode, une classe, un module, \ldots -\item \textbf{Un projet entier}, en spécifiant la non-prise en compte au niveau du fichier \texttt{.pylintrc}, qui contient +\item \textbf{Un bloc de code} : une fonction, une méthode, une classe, un module, \ldots +\item \textbf{Un projet entier} \end{enumerate} -\subsection{Ignorer une ligne de code} +\subsubsection{Ignorer une ligne de code} -\subsection{Ignorer un bloc de code} -\subsection{Ignorer une catégorie globalement} +\subsubsection{Ignorer un bloc de code} + +\subsubsection{Ignorer une catégorie globalement} + +\subsection{Ruff} + +Lui, c'est le petit chouchou à la mode. + +\href{https://beta.ruff.rs/docs/}{ruff} + +\begin{itemize} + \item + 10-100x faster than existing linters + \item + Installable via pip + \item + Python 3.11 compatibility + \item + pyproject.toml support + \item + Built-in caching, to avoid re-analyzing unchanged files + \item + Autofix support, for automatic error correction (e.g., automatically remove unused imports) + \item + Near-parity with the built-in Flake8 rule set + \item + Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear + \item + First-party editor integrations for VS Code and more + \item + Monorepo-friendly, with hierarchical and cascading configuration +\end{itemize} + +\begin{listing}[H] + \begin{verbatim} +[tool.ruff] +# Enable Pyflakes `E` and `F` codes by default. +select = ["E", "F"] +ignore = [] + +# Allow autofix for all enabled rules (when `--fix`) is provided. +fixable = ["A", "B", "C", "D", "E", "F", "..."] +unfixable = [] + +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] +per-file-ignores = {} + +# Same as Black. +line-length = 88 + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +# Assume Python 3.10. +target-version = "py310" + +[tool.ruff.mccabe] +# Unlike Flake8, default to a complexity level of 10. +max-complexity = 10 + \end{verbatim} + \caption{Un morceau de code qui ne sert à rien} +\end{listing} \section{Formatage de code} diff --git a/images/12factors/execution-process-memory.drawio.png b/images/12factors/execution-process-memory.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..940ca3c5357fa13931edbd3b2c1cf90866a3cfe4 GIT binary patch literal 17187 zcmeIacT|&K*CrgmP(?sMM7oG{2%SI>Lhmhfq=epE5=a8l1W}P@4T>NNB7$^9ih>jY z38H|23P>|l>4*ZtocMd*d7oJ`^UeBZt@+mX$E+og-1j->K4j-a(#*)-9&1EE28bzAyBNDz`@o4RYM7|dAO}Ac74>kGnMt&%iLSE+xNGmP zpc+FY+S!CTtAK|a8rtjHBJGte19WW+4INRi7<~s-e_eBXQUrzl5)Zj@t8v_UZ1-&q%96``vT6%s;3`3FYX;;`!aQARONj*3ob>h3T!QN>w0 z2iO^TnW|e-=c}vY;};Tyk3<`S)%jT~>FU|QoScYiX#F4u3qNlp*pPq*stS%cf||LHy-y_4(90pf%sVU)iH!2`F;`U& zwAIsBhzX0q`$ii1;?0dsqP&d!jJ#r;9WB&MY_WmCW-f#|}>q`ID&v$aY% z%$Z<>wIqfpI|S?724it1LEaXoF2RPr5ipp7X*AB(%p4bG=VXesj_?XKBzj{oCdRM; zR9LW)jitA~bx5F-VSuf%lA<0iCcrpA*B)bI6X+L)3J%gWQiu!>K*NJ0Y$J_qob7F$ ztwJ5`mGqs!^38owNGEGw6(y8@2+|_h**3_?+fvaWR9{yWX=xN5Zft7jkJoe1BRbjp z2I71IboJ2@hR%3Ll$Af|jZ_Rlq0lNe=HaTgzL*$O(@F|3#27Y4#R7}0hjjg>*qJwqS15k!mR?avxeRE?tupfp5@Z(|$ zQ}y-p!@w1ga6@dE8o@;us~DsZO9O5@KKzVybVgs}iWLW*=r5X6B@8tn1|+ z7HX~@;B4ZJ2@lpc0;g5i-_F9oD>@>`2#jfHkFxY4VpIseP9`d5E+~vygq;`B1`TfM zsfUK^EBW|1hB^~#_vref!f3)C}F zg$G2~`x0ywRTb>KEbXysdWwo>M5PcD2b_VWLL@u}>E{iDg@V(l8?5Z-s~2Ny6zJ=y zXYHeev9ffuv2r0&Zy9>|ItD5kL>fmJ+hOcNqZNU%GE{X5Ln*2FMFd1*6x3|7K1$dq zL!_;PwS|ejwX<%BuZvBLbD*g&)o7YH!h;D!qPne%HPX(G08XZ^rBkT4W3WP?cMKMz z91VPev!j`_u3|JwmFQw&5NfN8R0)akry8Ir1-~GdZ~`vc3fMpD1!Y48Jkl&o0c&9q z4Xmtgpt56-Gg93O+|siM1^U!=z$hpXozXCixq?y%G6vjmqVB3dypO(vt&uG-1iFz# zXQV1YnW*S%Xa~ndOYCkF>+P|G3Z8Z1HLDP^i|E`SMln-;JKyk#p2kNiQr#|rPn){{8z)Z zHJ7_1(S@{jiOkkC9@5!Rn09;~V&^&cXM&UtRQH+AZ5ihj7~3}2|Of=8sIj)L)1 z+-cenTc|`@YUd-Mv#?W+_XAox;Vou%N?`#l7nnklu!IhY;muWkI_?Lwg^r0Zwm)1C zAfWRRB*~VNQJnVBCommVZ3Zi-rQKx1Tt4T1 zn(ga2!ot`3%As=S%`>^?9!8oz)WvL`s-c%+KM6f8^5f{@Vp*lb6CPO?<bF}JILvK) zAFT1KT05md=-JzID4IK55yGqyGcETuW2EZ75tn2`IfPe7Dm}i;J+9ESkDDkiq_1#k zLqweYp7=y(gIT|QtdvO}k#QR*&DuS23Kd`dG|>@h1sg*virUQ&JE=ki~9PaPGn`(>gXTe;J4Wy!&e=tH92q z)hF6lT`~=RIOnyG*booo4MfuymdF{ZPAVLfH#H?ChI*vfcl?pK zvm7K%Zo{rY4kGf&8RCV;g{A3J)|cf@ll6Q;zUqmQL#6Y7|Gb{mFc^f*L%jM`zp$jJ zz34dv_Dp+bK0g$_Vp5w|gUU^6Kq+`FJYkr+b-p30=dMby*Nebijly9MuD*n!SoSt7RUTSDa%S+-t9X* znM29w)o4vtgc&x{Gi*^qej%_{KBd9px!&*z^osVyCYsI4*Sn^RBbAkskZe<)CLl~Y z3~J=J6;4U(EV*yK+vTJmQwM(p3&qKGq5Ha*5)tl#(;g;8-DNRy_F`FrPD9=L^CQ-r_o{4|NWDhFtdi?A08xKO&*1N%}acQ z^)LVXGA}vPDlm+tz>ny9Rq%Mk**HG;#F@#E{C_s;oIHI6$xbo3{ML0Em6%V6(;zHJ z)uS_u)9}1k6LPMV5jH8}1j+|dr4fRO)x8TM?G$idUy6K!j-T!3=*baHJ4>ev^j&sS*45z>n~)qUpQ^c~ z&BU$85cEn*k0%|)x|6`WST5V;{}E>Ov$5NIdEY;2HrAXp*Da#Gzjl#J+QA$WgQpAq z`r=mKJvFnbw$#C!TEwjX+wbTg#tOW%?oP@)v|vqwKEn!)>D|WJxRSKXnrc%cBa< zSVnG2*-MT0W9Ht=eti)-7;KYQz``Z*fbGHe-aElx@3=+hn&&DSSZe;LyE%{ovnt&; zQ3*Rvv2>ef)XDsQS#2yr&!t6>I4`1A%s6#o|3Yq~+{X7H+ta_c&bA@w$(=w)7m=2^pscY_}#}#V)0b>1f?bg;^Ev_B;d~5l9_4S_RE2w#6%fYVM%mPHM4F;}M zxa>^6xw!ae7B! zoqR4yTm3sh<59h`wpE_UKU#uEs?TL@bv#06zMhEw8p@2tAFPeOvS!4{JBd`EBVB43 zB!ZceN)k2B38{vVh;xitq}OuQANTfx+7Q!F39sqV)e@F|r zvRHY)y~Z2pz)&^v%(Vo~jUNyFQU^yHo{t&F)p&I>Z@;r)r$@>hZ1!u9a5`CwpN#}E z{WK`=T;tPNP{QL!Gqx@ZNx1xkbK=kZ$46pT`3TAT_frU_->sfT)_bmC>9=Q6MDX7w zF5j4O{UP77AJryu;&G*!>Cu95eNq==t-9(FGM`G&8SR5#kK&K?^K-Jft1!2nPJ}nF z$+_%{H#fUB)xMY{ zO30ZhoiU6v(=ldbD-_fs7GX7xQzUO`m%bgp1lNFOw_iav^FtHFf4N?L|J7@^vFOP9 zM~f5!EsWs-jUWEg5*qtGxq!Mg1z9u#dR?p)#&b91s^lpcrsR0_3M8`#NqVpkSv{i}{J?p8W z{B5N5I~==nurMfqP+c>#udNIxZ3(P zA3>Wl+PiDfi-v2f60As^wQUPGQD?dFidltx<4#7*+eZN1 zWSRb~d}9_H7uzx5tj>_L)PkbHNF-)6J9vtOTn;b5Ee&T#URzjNE?ad^Pict#a_O~S z+t@vnCz+(sc>Tz?vuM%s)~E;hQY1Y|f5!mBUSkFsCr*T)8dx8`L;Kq;@H#BUz`=nT3TtrMe+1d!ni@Yk z!N5T|sJ=Id0&Y@u(;{mjH+IBstrcj|tbeTOtm#Yt6hhne^-Fkw-e&CIt>A#_t(e`D z75%~-W7S+)1jsq_44Nv{?VPUL_rF{BTpcUaW9YdfUl5kuTGQ&otV)Y!;2$3KD$^eR zblT>2X|l><%vfaFH6d+xOxvhbN`q`MXK+_~nKLXI?>z>#yt&q@yMPPHTR?i)JN$Ev zSm09J;h&wxo6KyN$Tw{s8TJ(FrD0QPqT58c!zY>vN~rHlJJVCC=TDAC?f`9{EK_@Q zm2vYLmucVXs~7LbY~Cmr%{-G=ae#dbnnY*btginS-?aSYx!F_uvd1R1llMYwj;>pwK6R)36`*NtVqM;DggQ7dk zJUuBN?l5tl4RCwk-~A~0^#iw%{Wnr{T}oX+7c&JkgI;sv|IDimEI*v>FYs7ueJqN| z&VI_%+xw7ZQHNqQR#wYGtR!nz}gqB?fe8&h-(T`HDvU` zgu3p@XD-i82YHu5c9uUsa$Tg+4(UweNq@G-*g5I$qjUsuN7nT*8-2)~uHg&k>E>rJ z{7JXmrN#`>PjsfhCl6Zb*a8~e@RS_%354}pgpR8IYy#3_vUA22>h3un^Xp9s#zIs3MyyEpMur?RlG5NW`uYpB zJ)Sx|dwRFcER{!)36r>X%hBCLGo8Jv-2p$V;rxpD{Otz&Zfi7gR)b+HQftv5sh-V> zS28k(N6bO${H#XS_uK+z3V_>8}=(&g}$KMtlhB?=qeV}Cq96@4WI z-g;r!+27>n^5^YS9hFlbMKffWj}deZxKhqrPil#3vm`Fw{FZR&_WEEqSZiI}L27KP zO{OFn9#tWXn1-BUJ5u!3;JR#GMiu+<_(J`|YyAh8N!66##|24f>WCJcn58Z&p-c$j>8pMhqueTkKFc{JazKax|sZ(k;dFBG>KT&816iRj>)US-@F zs&I2fXOgsE-uo z9Q9{Up;vC#tb8&fHI+R3s=^>hVXXG#W^|{e^*RZ&JiLP7`&^k}%X zeeBZZ5YZRZSi+AU0K!bUxys)M0*w8-C|} zi#u^a?1D|^`LpW1+zdesuh_xBuy&b)=lADg!&oR68Q2_1-k**|chDZp{yOvdY&_W7(430kU=>!FDQtAiiPvU`Ap5<%eCF=R?NY?#7kN zB=ckl)LBM0$IARUQk8w3rq?0o3r&6u7P)%_!JbV^qnQBQaz!jlvO7OXRDlm$=R9q3 z&39Mho;S8uwr$J;r{`WTNuv9ZlPk%g0pDA$RUvP5i)PI$n(e1OKQuO{&#rW9au}KQ zW<9)Nb*^-ezN#N$rW46Fyr>rX#qJe8Wb&%qs2U`|)9svR=Fl-T<8x0B+T(qNT(T~9 zYI!uvs#gPzyIc!0sy_!@1cx=s_$v&$>9pa7pGsxM;J+F3?0>)GvFOWKyS4hw@smA8Y$zGaHtJI! zIC%CJlnte$6}6emhthEW1%kee>Y|61Z3ynCX+?nTwps;P?lYdlr6vAP9pcB}V~fFk zb>oYP&4?UWrDJ8jaBT7BX-<(-%gvpStGkmUSiOF9f8=zY{FEj#G<}U%vtWJt^T!ep z_%P!_@-UA`m$1#-4GOpIhD=B$y4%@x3#w#+etvN+_{4U$h@SG9<)m}1 zU#Xg4?z>hK2MP)5e3pi`Bdgk#2P$uqlP-R=@OJkCQ??#kKoV{CH{6KZUBVFd3eW4u zl}cKfcAtz$C8odG>w@VhmSlcGpU*YE+mq}agE_P`?&{6a`|H3hyNYD)Yp8e76?k(W z9S|4R((^&yrArBfgw09l6rFFKI)i3EyO9*ut(P)wh#3Ft03{NxrVy-U7T!dT zCc<^aAQ}k&Gdg-9d}Xkz69U!i0B{E_*E?3a69v597@xm{C*Ie4Yk1|D6)Cp!IL)$= zA6ihygjVT{_||mC;QbF~e$xw&MFhexAQhO=f?Q_A2>z4?h77MNGsy4S{&Qsu1vZc< zAE8&9?=^1(iBWYbSNfi~cD8Noq9K#%9mdTEP8&lrm$|Ks<~!kbV(#4LdKweE(?1GE zDn-e!!bF62vS2&a>(}RrelrW}Z6jMalcS$*)U#9amCaxp22>W}N+xaZNe+~ia(>uf zP&EImnW%+cvF||# zg(AAXL+1~Ctc@I@@-#1(LaS(2r4qeaXmI`)G;C=-c>>GvjdX$6-p=Au8e->0s|V=D}qXd)dxhhjXfLZ$NXeT1hE(MJOXC zonHL^ZO4UgEiH)k4$kW;!NZ(Gty3c))@5HV&r|d@Z8RMRB;mjl8_~d#igA;XyvVn_ z#Dr?E&TCSD76|zKGIB<8&$sRk#NLqPs(kkEl8Rx6!2Wn8uY+hKipopeQwz6+eS02! zhfB(CzKFqqcuPPtTBahHX>(Rw4xQf|&GgNLBrd}n-km~QlKdOQqW3Aes~|`Pae%zE zLpeKs48(>xAO>MXY+mJ-ewQW`F98O<-7DX69zYV6p4+nb+~@EV^;OO^4exkS-yhvM zqGJB<^9JeyQKfdJ*8$Ae6OwpY>EE9pwKj?T^9N<6C1!C~8IDfLfqknFFTgI3+60Hn6@L65Cx<9qM65&{D0 zr-q{zYU5|tCV6C>giG#6yL|bBmne6ww?t|ZHfFKt2@oK>nL5iUZg~?#vwCcGCsl%O zUW$KeS9%&a$=j5^dn}3j>s>;t%JO^Oylt zJp0i|=+@f|zP&Jd4$27&pk%Il7Lw#CF{3nFh(G=-)rkJ!3vU(yOpyJj$xjr3#NV7R zu@qSN_$VztekMch-S&uwJP74YF2WJtqrSg00sQLLU10VlPip_3<$Tpb++HLyLYW0= zeS2?z+LZ$YG3mtBEpos1VH{L;Q3rYVJbrt;5r-np@Il{EX(K@K4xIY~c!0(2nW+>8 z+x6+r?;z$bNr5l6>d~OZhmz6~g;>yGAA`UFt^RzoEe=prie4R;Ifr!0`95GhGb3o* zm=lRUSK?_-{jQzKZI6v^Q+l}rXan1q5$NChoBiCBwGF_1xIa7LuF>oc(eQX3_k{s+ zu13EN;aGA0i&2ROV{hxDw7HDN`tWb@*Kc`T*tYL$Wo)H;A3Rd=DZ%P{?xW*~u~Vuc z66zcaPs=JczAuZAW$$MEJ;YZv$+S-I@2W`uIRD<_o~p@}<6=h`q>U>4u0S`D7wH3k z9hr#2!aoI(es9hN|B3y%44>)@sPjMAU8*eEW|`1#y~cj*%l@C8Z`GgGOAOV=JcLVlw) zfKs37|5a)yD~7-4(i(JnWX)#@aH|cS97_F+6v!Oa`Igg?Ebl6twDUi6t#?@c+#pou zKe;{)mAb7o%}3X!f$!mEdT&euOqPk(1<1+5f>SC%0e>#{09iQ$^%nn_OqCTp#<)5e zlZXc~gNn?>{)2>dL4t$%jp?xz?YEf%Wg=<#HW`T0aE-SLlP=%oD^~YZEv|A&Mh*N?$I_0p zq2AvR&$@J;z06XcoE$H5ZKm80DO7!m-nab{YvWyL;fcD1a=$n5hlhLL4vTb>Xqltp zFs*bn=$q6)NG*v)?xl{D-IE80Sv*B}% zU-8)_Wdpw4RayP-`8I&+0?IY)oI=p<>uetbAwPnxeTG zC{#ZHn5=i+Sz2ecVt-;waSmo6^EC>5q+87|U_vFKEGKRP8q5q(PdO?NH*@{-7i^cxZ-Y!*6;M}GzT2-q7n+OO#^w0&e(DhS0+O zLFpLh5CR1GP~B48!SA^mbUQtjDDeeE_Xcn!3mffJtA0!nAz_pyc#9t%HLuti_j{uUK9?s7 zIEh==#mr0_qQ1{gr_0}7{8Yv=y(j}Rh}KE^0Qhj)FSji5$h*sIQu=PBvhcinY;mv1 zFyj;;=(#05UN_S=8t0sDyeLZ3BRV&RNfyf z*Y*eH<)?!DuoMI|KDh~rT(`Wh5$FUJ;jX;Zb3@#$D^tK1aM!0${(lq6>=YXer0^Ob zuDYpwY|y8t`S(SBQ6fOHHMqMN(6ISG#5E0}PZ9;daE$j$Ebl-4-Bo?}NH5DP_u)#9 z%HES*Swiw^{lPrsxuYdsp0T=BlPW;rIOY^V3XA{-QC@v1J$w-k zf$-H*YXJ(hF#L&0*d`=iB6YPtTl6t)E=Z9dvQYp=T*Hy$zQ4O=+Y}uN64MEsxY(Vuc{A-#SeZ6rfYgZ0WanQt;p zjT-F7t~AW~Jq?>6$5AvGs_sJBw_Jxiz#d-|();%MLNUn31IV)!&d|(AsdAtu$Q0O`iS!cJlcrKXk`%m^S57)JZn7Uh_d>cE*aU|-o56Yn zwTj|q-ez3aPKYnaXuCHqMjlLR@Rke75mFAwe*2qhW>cBDYr;%s-FV?qMMniR?Dnot z+`iB@))+YInan&H&e(J-GBdP3Y~1JY;19XI;K8K@H%pQ~1jAT=VK15&KyAUbxNjGB z0#`PHnd4JL?tbztX4aJgxOb?^6xjY^{QC651hCFw$5peLptt4Rr0U0`?-6JG@8fRo z16*ng7-FcV`)sX`B`9Y&M$$aKI@W-iSy1wSKZKBTx|eC<1WqT}S!6nl|7MbsakU@6 z;L(XKQ91XzfR9JN3X_#?Ugjw>PNHKtHb3wgq&IEDn%Cdfxlcj^=HB1uUh~h3G`vdW zx#q{TdBjsNa07tq-e$t)5&wuV0EmNzo)q4A4lFnEq_X9~8+xWQE${AY*8BM(qL)d+ zewT~fn_^an>zbV0e}U~a@A9?(+@AU1=yWzExbl@lMf1kH6hB^ce)joh67vyLumBU1 z(fedzUzl`H^Qne>xN`$|43xF^jU%nBi9%XLJ7DAC- zVJ*DCWJnU16$me)eGN{O8e|jiN24l6^CIj7mCkZj<6fh(d}5@pB>I#F)?1(Hxz`@k zrohoFeYrjEz(QqbQ@`bZNSW#ct_F{yX#)%(h3r!aDo%`zso$&u+Sa^)J!l37-I5fG z@vCtJEzcBDzlkfO@e05oOaO-{PJRVf`Ya7v9=D}_ohX@U(~hk9g4YWg@$Lh#W3$C~+%-fldR-P%P{e}At10F< zF>zluqRE^51cKQEsFGK3&)Rb;Gv2b73qrrbyr{jWt)&oF-K%9%5KhZA+ z)+N}Fm{PG59%R-hez)}9daooK>;eVX!@^Xn&q_rwLr=eono}gsQ63)!tGRd2qWlVT z(IZH6RNjMI(vEVhm6IP%boAxCXs|1_CA#pKQ2<&2%tD{=gavfk`T>Xm6cY~Oc(sUi zz*QQu85%9e&3`CV1SKlDO8%LGx4N%4{}u24^3;7hVJ7J7U(+n9@JttdYJCfcsU@!5 z#hVpTo4jvfl=8Gs3Lv?LB3d&zC@F-*616mn0kG9E<+?zCD`c5rQ|N$AA}q!~h@Fzs zpsAGp9N5XRMSd_ROc`fph7Ao7G^1%duTRe=C2Lr?8{1#Za>HAa0m zT^K68G0B$t@685Y#l}bimIeU5Z`5YHquyCPAOY?>^u*?bi#Mm8Fah|<+2@*{?*GeY zxzvZrf@I1wO)1wW&jA>rIyR7|&rBU2J;H5FtxO{PVBA!%1G^)4=$WhID{Kolen!3F zJFwe7T}M8DW2ZtkZ!D2=xX>8?huAQC-OT6HQ+wN2L|n{JbfLp+{S)Wf$K`;em3#fm zi^s+DmR)N(H~`LC_rCy_?FNj36bK@20IX@;b6srQX3P^<>#$n@r<(rDVABWt+1s^h zQ3YQoMt+1908loc|6J_PkW!cDPc@(NJ;C+eRRI+MEDIkBFM@i9L7j~9Wad$-W{mvX z5SJ=YkIg^&#@{$=F~IZ4T^j%K^Xm(-ZkC@b+%zk3S$_&?OTcM~@lR%_EX-fMVbd7> zL&`}k**8I*RqPXpq-7i9j+cc2R3ZwBA)KE!b^I*VX#=2$$?U`kYYKs05uLvO0C};< zqa}Xekx|yoGarpM4st||7E5L3&Dd?(I#e_XHo^b_j4Mb31Xl62KK#y9ds^@)RJ=%d z%geiy#o?)Nkz(YuK5N+);7qUb$O{@}5tz8wDuAFgUx<)7Kj457Ovdk0pQ4 zS9&ruMq5)pDTBg|PY7v1FburJe|%`bIWH++Ogmu@R@9}MLb%>2=iclDcnWbA9=V|# zoxwv-MC_m11Ap8P2r=W z)3~3X{cS7G!?im3)4sZc9FED)%z7KZCQ|j!*EW_|0m?B3+XK}=9#CGqKJ@G32Zj|~ ziF^?4e`;PV-W+QcOYEqsC>q1&P*i1|1OvfVlQ(d60&asrsg9YWzd_3N42YIb zN$+fmoRsVBT+{=VgDaUiOqdv~MH#m{eE71^xi}|K1ydUQY2kj8j7Hf`9aYd25%Ty) z`{0bNclZ6~?>9-Pm;FqoUiUlw|8)ITl#F&qR0o2v!FXGKdJY_N5Ca)uKcDuVle^L% zr1gyYZ~~Y<_R}Y60a`oTt$Pr8X4PxaHXX?_;^}y1lL15^pI-^a!5MucM*9*4|^fQ}KD#6A76oR@kOOSIQS3BLH zq^&Yq;mXkB%DummEot`#+n~53>*Iq<_Rvq_%#E;ZQeaj zIiccUS$3nayZXa$hhYg@{BE|uu$lBkzq@C93=~55DaJXZXxaFrS{+ zk16XJr+}o^rapJ?FJc?QI|FZUo%%Fpp5(7UM~0x_5Om}9o8tE*b^h0W3%O}s!&fqI z<`W)X^^Ma)=T{pxpEdz*>`W@lo6wX7f+#12qT-uzv^L@xjO=yc7&~QHjLque7P;Mf zN#6*t!#16Yuk&I3PNMEPz_Sysx1~<1f++)ouz9z}@DeC;Iq6q@j)4)gbUm#ENu*3w z;~t6@`{zIL;`Oj1`ImE48UweEFqzVg;6sN^bc1JFIE%K+Pe?5SX z{%AIUMa-=rq2RdixwgdNH>lC=O_zTy!M+EIkZ%J+X8Jx4{vBYG;k_&Y&BwT@yMW#( z-XpAxHBd5B_cU0(JO|RSyDn$`ka^Elh?HOxPbbg|fNYhQQaU%W~ z)qvgp1sBdz$yY#3EUY?IoR_R@IcP*|522X%6ApeGQQ-sO$Mca{P$qaQO^|4e{2x~Y zMuD=~ccAX>`~Rvc&cOuWHu#^d!S55XtGE7xn%@M} zeCgaDaLFX+w1E!*qM-7x-uO*2bbANS-m!}q;LN%N-z9zxt(W{T^}F4BM^}XW$`cy? zGYJTc=U*j$+){QQ{+k*A%^;(IlOL>NZeCFh{gUTdOr?7r%AE?` z;mmEqmpg&%=4$-HDtS%-tp6KiI<`ge6vmR)GDD=(5m*|}M6nr9!~DZ^ z<&LD}SO{wW8c7qev&HI>-eC3NLG~48Spm7nZBLK=w#%@k0CH+ujf3O)*Qo zb{zv8d}%uwcM9-v<^u9a~E@8V3Qc$@C3QR-BBynMfZaFb>A0LRdAe3yOscRsYQ0gD|2FfFm zS4|6*wuN|=8^19#PH_{ATSGn{<+MT+7V_f1KsM?E2Z!#_hfFPqHq(X2Zp`*D|8^=6 z-KPtf@s;`#I#6`w(#18!6HBuk6G{GAqwyK1RO(p8>%8N?bU2El1q&?E%~yfyx2@a9 zn)AvPfZ5606rm#DO$;!;m6E~xR^SpKO6%2ty^3{7s}O5LV9#d7wr-JUZcDAOFm1jp zo=&|YmDtm%3mrgB0!Z!iX4ySpc#upSZH=f~W@T~IY!aWcAB=vc5U`6kh z@%MDx4;U5{xPAW;T>0t#L)GgM9F$yD5$OlCk<#ogB#@oD(I~iA(X?{awS1nXnu9Tz z4C+4eu>vnS*iwK}OaNbkV-Q#6$nCDACB@8tK*nS|+y~jCFT)vX6A~a0@3SkIX~>0T z8Hu^;LCF&Acv8aJ8-@)~FgE`-Pjls^p4yj?(JG|cl3lrzO$_SvZ*I@489IqdFMfD) z_EYm_j&8VHXZv?St_SD%egF5S3dV~V7)s-fU$i`TTfzIS@kM%8Wmz5SSwi6F{CzAs z-uHp%H)z-(CZzPg-s1iJhX56i2ajF{|zt z@4iO^80kRTMkO6q>O4SahHj6_GU=Rl?D@4m!@UfV&^;vqDnrwCPkmjhwomA|%uy5a zS&;y;ds`o_YTy>T!3o9jh_`XC%~OlqNP(d_1yEj|^+}zFWq=zDY+Ab<{&MOP3C8g9 zqufSTyHZS>jB!qSKgY2)*F!2PwMUC@*AUVfKAk9bk%AMK{wgM4E@`32! zX+bFo@2*Vl@~;_W%H@6<48lB{lU|bEnnzITq|!M|)4xrFPGoY5w!;!hc>^N&VgjXG z_LzLVb)jzHh;Y#c&|cX{`kg##B@)FM*(P%nRs7uG&l-tlyZgEXS-yEqm$iP{+NhqkSOFSOIOetiSC6eaKxHzjOzJv6*h znRU-fs6!b=a<3sU2&ZfWOEJh#-zBpoTG3OU(78<#jLcZwP=wEStM_^fEOu%2VDA&%|pW;);4Y7mqjVccweE@{92D+a{gU0QGR? zopdu(X+p!{JWp6k7^f!gxDzEMNkSS}<90*i@pRF(v57(x5~rqJYd&q#CAg3Nd0qxR zVZWnUr{s||VfbFlNQc_VDkl6&qJX<mzh}O~nBCpIJFNjY!9C)Bmkedu@-N?g z^Hg+s8OQ9d@BN*90@dM~7kz)J+WDyF#g9D83Z8|u?jW|?>~Oq#!j6{3atA_si^#9QQJB>j8~VZXV#yE;(1lCjgJS zvGXmVOf7!tS^eT}LXpR1=w^36`wQe31_YzilSCd6lf4SKmAKWzMIMZ$Ap)Oo3eqPy zI-b_d{7!_u4rAdTa^T30i?M*o)?_!;f!YZ+S^)Cit<>wdCv)!6?b`Z z2NcKOJld_)uM2IazN@2>e&mUESUbPGJckm)`N&av_aKmbv)^nIUETl{8G&JWO{=e6 zi90LL*uy_QOr<>Rnsb6)Y*v3{m~oW}SD(02dw4xR!ze1w^RQd%BF)w!f58-ZGuj;GawkfZsD9awF6e_4NEYbipz;;sP zi5qxLGRQfzHywmf?scX*yZR)Cj%@x*^1#(!K%Y&Bz7E?d86OQ!kjjoC?V(hEYPdrq z&I?hqMJv+TwxlL)#0&7IBY2H54ICr&WwMPec$qQ+9I-?`Qu`y2JmLo$K9!!6dp0f6T uAMn2+q5)6Awa6$ukfU`{tMnRp=xPD_!X=C_%c2NH3uyHAoeZCW@g*?^TK*y%Qi*l}-pf zQX@z&p%WlL&gOmRn{)m+cjlY9XU@HQlAY{itu?>3)~`IzvqD}xSERYhd=&%&(I_iH zUxGm7mw@j_6c^6#EwqEc2f6E0TTM_^c#okgj3SC=QEW%aj!fm4T=XT4beI*57D;z zEfv96Bfdee#VTDiyiOC6!v5sTP0kHT!GQsgv8t(XZu%j8)G~wVgC`GFqJo`~`#d&w z+&l3J9VJ6t#b3j?p8DE9^}WGCI#W{lOZ*x`=hETsyOV`9%#tU*e<=|{sK;3SG#4TR zfnNL~g!2Q2x0=qggFs&-cdh}y-SwgXfxbmZUj%_(DY62yE##mB+Kxtl0)YZnyW~M2 zIrsnN=KRCU7or;M?Ciq%$pVBt)_<9~QOdm>l>MODbh6X?ED_WSdUufZC0vi!+Kom@ z^Y-m^)_~B%b@?aN|96%b6czRM_scr}Zbkofh4xNYe}8FF(c|)m_iXEwX`Z#qgoE6v zX=(e@BpyG0?Aj52TesB6YdPx^I9f@VxyfZg#mT93aFi@I;(#3#Fu%B{qpdw%_vTMe zJP&xMoe4Ho_a>*XFjVlO!T=~hb5Qo;11o1|=dogaY^gEJ*<&7_wVj3DdG-hI&$}a( zoSg7^>Zy4enf&14d<|T+?R7JX)h}eAoNQl;7ijc-F!QG`%oD&SaRjF#Sl|981D;dV za;RP?sH=Y-B?1#cM z%2BO@kEA^aW8~{W*i>6ssjJiMay$E3wDXC?%N+g=;^X6^lRf-0_;E@sN6h_BffrN- zLaC2&yQUmNeLTmhU{|^{Qhc?$ZA>F##0XQMuYIiC<(=pI-@BFivNy4`w3H5VBB=S5 zI`y$lf2@XV@-HjE@uu3Ly93>^+}E{x)LF!w_TEZU!gvAqGori|`36{S(NxE|Ui#?S zvApABZ+dnHc1OX>$}S@#BZAD_0o%y~Ttx2YY2scx^HcS%_V)JIU!s8ph08v#(g8oq zQlcHJ>Urt9mT#@S!YzVzwbP0B8+vz=1q{`KgS*`Xf$p7tbz)@z(}O@tp>h54PdNdT zVg*KbqYxultVV~n0}wes^5j?TgLAhZY@!^)~SD$=7fUP)Zb}vUkMVra0~fY!YAm@O+u!GYsd0KV0Onz-d;-3 zQj;b-H%h<=U(VlWR+Pe?_$d@-0E;Dz>cY74ZvoMY80nEGG}5JDmROpXcz}-CYTdh5 zfK8Ope?@I<#M(w|(3=cUIE&YVw#!$peC9W!?sqJgjfzeOuGDmyUc6szmKKXu|7&8( zr!W@&(38xvfrYhjzoqWxV{@W6zSalc@j2|e-CId14xbP0I(ikIkcqtJ$@|UqEK>#c zx>-359L$>qxg4Gm1BSgWaHoUyb$t&)VMBx_qB-ruA!w&e-k~u z!%5tk=)RWbL1Rf^o06p<1EnZm)AH`vl|Pt7eV6j8b}8fS4=*VeP>ebcNm&-w!E8{HT;S{xTwmv+yME9I96${AeiiSp- zxS88ciw-QG7!b<)baX6oo)ot_I3j zeWXQ=E|$t(XV5-+*PQ&=CU5Qv1hZf~OY47Q1PXn6rrUFsYW?}As<)07TPR$hZ+>AI zTh-*<{_pukh614orRp+w*NUoEC*=5aQScI0@*wV}$MvnVI^UjV@5Z?Kgh9Qvt+e0lL^J<%GmPAn|yaf zkyWJ}cZ&Da*FR51lSnvjL`i|&Ulp~e|5ChToEV{lrS-yGL6Mitbom5!<&KGt-Vt}c zCnbfdsi`S1cSfg(kj@A#pGzNP^sQ@V9{B0w+LDg{ffuUBT9&Ah^?M5Hwgf$rF7WG>=UZXc2F6p&P zWlZiokZh{@^WIfImQRZ_|J(XfM8}yjpR4qlkD2-yP|sNdJ>Rr%f!^WjtI`4s#~XX>eNp{(1c8TqYe=&@@S9s}QYaBd7mIJH& zY`ng7@q&-OV%`=Kh(u9|U|3euq@P(ca)J9keT9tH?8NvS+fIT2eASTmek`Ic)F`1= z^)$u;jb$;^ICA8Xj)2{DV2F%9q~)yWXv8^0hm6{tXp) ze7A4{^3B~U6j~E;92#onJ&7_@C-|i^j{Qt~%#hJ1kuaFbJfx+efdRJh-BR0|?ij8} z$XOlYWLXh%JRhq8g<{?5CES(=7J3sS*&t3v-QH_e3F|frP_?7d7_@qgs@fw1!cybA zwJr8G%YM<1HBx>Hsf-(rllr>ImcAMlPkvm3XC{^le1EvhDJq#yGf0Zv($aEDVtXMG z#>z+2x$#~Gf0XIHS^(7gu&ymkRzpKWMn(p)5MuU7#zzXH+fPp^l7NQ0K@9PNA&8kW z+dypS1{_EX!VKL=TiiPJY+(7BleSqP{1Af>mnH|xpEWRf*6c$$wW`BT7x%%7L&3wd z6TZ!rd{2@$OMG)`Us2hU^UZXgaG=^=b&W(cnjN=6Xe+gx(%r69- zKdA<5ay@!wIN!$7lA0vfhepf1J1igk`WZZIRNe5srfQIF!T^lJul;~6c?#MrAw+XS z=ED#k^^i-L#A$515y58xJ6*Qi{O)KclXS8)+xghKPcT_cplNT&6(i#bgGay48{#2hxA@| zS0yF9@)S#)sVrOGu*V7~(_N92wx%a}6P%X@a%WAS1c~=U-5CH zr%ZQRsq>m**+OQFLZMRFAtBUqKkcQqjWv`Lec=+KItKekoPL88-%|W(pUJ+vIC)5z#p=Zf-e7~=g z;`Z@c8Q?#bR#qiO^_3_Tu>?6&X;AWvN#s6qoz>##s*=ro7WDXJ7b9TYu=kxxAh$ou zSvb1X7(W&L#P;EHL*Gt&nfpQlMa2o1H8i(}Y*PEErrt~{IIgW0s?#{7R+U$e*Z$;K zJq^BvZVzK3ZGD2AoWD;u{Yik+*-SUMi)E_Rzj^b>c!p^@a34Px^R-#^W5zgGGx}Rs z4(p`PO{c^rXVq`-U^QzInZ$^j@ud+e`DiBrXI;9tYhx1O-~3*6eBR{*?+mF5DC7*r zsXXQA<1!R8Os&OU$}u<5^gOg-YUD>Xwd#AQs4(IxFD<=dopH1`!B;c=qQYJ(?v*|= zf2gm^2WOO8LiNr>(Ma*ke?xkkDp>j1C;xB~C`aM?W*Vx%kYR22YLx@8^-CVa3ovBS zPnixpVr33s9wj9uR80IQK+W)5|5fq+{e>K!1~w#~)#C7~lEv-owx-f==6(X0TXBsj z9qluSNfP>DT{SJ?TNimC;I|zXCj%1|7r#XiPl^`G(*7*;$U_EMTbq!OFe8y(QuZE5 zQZ@G2nWJr+HSSLpYe#>8thpTtq)@k7KNn6*8I8H4nU0PQ<%VY7_^$(zPdSaJ0Q&Pv zTl=>01?uZ!)Mj?G?`{JebULoFnHm7Ufv?&AlYpwv9aVPMHB%>rzQlVIg|)S`EY`UQ zpgjIk0rLBIVMEsGhVNc9o$8Wr&Hdve zDRJu^MQhzZRF^^UYyfZrq~oOM_Cok}Q_Su&V8pwWUGlnRCL61(DTgiq+NKuDk`{)~ zG(ty(xMr9@pxm$j&`_1GE>mr8`Tw2^K}7x`+DUFk@XR}Z8}PbEmBBzgHfX=Na%AfG zZ_Sj`gAgF-^!RscO_FQpxXk&w&{$Fss ze&G$ww_;Zx7Ys)E_~a{nJ8l2^JayXSh2Hk@mf?q!vbdX!ztU-KwkJBiWNeLI0d+Ueyb<^&k_A6Wi?jbRmRx(+VP@=1kC%}Wzu(6%aiU*LpwT#s32MSx zRJ65@ajPAz^>>;_@3J^Xz>xc$<`r8I*oeI-?u{}TF36t4@2ZTPZ1=cFW{Ec|N3Xet~vyqES4|#k5 z($P8|;OO=72WO0~%T^Fo4P=7%aJoTcYq8ujr8}vw0 z)Q_gtRmcoOgVoFBxUyrn%p|_NtO2pXJcu->^)6kNE0l#fL||g!41qwjr!9Sxot~T= zK7SGQ{Wn=MHXG&rG#6O-0-MkJr^gOznmwi+L_Yl>-$6h9&RBIMwX3+_g|>YB=tFL| zxu`8UN6m8Ld*77q6dMmXU3jA{%-_FLhv|>{h1OelB;8(5RNJ0&qIe!%sHLf?DJDY( zuvN-!Ico4LS@*RTi2I0Y&fyLH+!`^W z2>tEV*OED2j3|ZTBd^moR%7VXd_zg6%{z`eydIuNQBT?s>cgVMI1#r{pOE~`R~7){ zU2e-6Wr?_az2!UT-d$~NZ3S>NAO)N)#sPO(hLH38uXi&_IHx+Zj~^~+Q#f6Z>|5Bt zqHB+=;`2}9A&*1q-YFI}vdHvrfRk%u#8{e+z(CEGYIHITs%hOYo{Ns+H{V%zDAt25 z@KdaqSMnX)P4iTRb5`ZMC=_{COf%gS8douqGBSVEjsKBuUo=f?{BvH=P)(v0;v;>N?C7dNYYgm0;`MK zTnae|wDNwtfDD9-ZZjUwV#WVmcK$z6O#k1=w~x##k_Lg&#!y^TwDyAzatCKm+DUBT zHLBa{UvXXR^V;d==6=N(RS2laQNe&V_hx6l`-6Z70EJm2xo5*JQ778EoeO7=EIO!D z(FsusG)U1=EH;cHnquV>ph(;HT33yZjor7@>`fG4^ONIzYLE@o(%)x*B1b&+%P9ge zgaFOz;81YxZ9vU7l}137xB<1gRU8=RT(gtgIWO*iJm-Ff%(9NP#ytbHGPS=2JF~xa z?)#xi9}3P_PYDF3&l3j5xF>YtM*;+z@X`yn=+2+b{?X=I-#WL@f?Au<=~4vz?=ZTi59j^{HUEyV9uBz(>Pha>djhcSrX@XSYn(js zZ&7|j9`xl7^V@*Q0onf)=KlcWKCl53vNMG&g3;Eh4l)6=z|ww6>*#m}fFh^%qethm zJ|GIqNeFL&kXiplgY5+VdqIm$Btx>(kp;K_xT=n3srOvsKgV1P1Fhs!!GQ4m@0|+V z@}~d|jr{|b5~>L*{VZg$f#>_RwmMp5Y;62YmrRWe*aju}-9@z4s&61=52D@t85Y1# zrkuxGKq&p2#|r^_Op>S~^T6Sh)O5!RrUV@^|6Cll7m%dHZS=1q)9m?c;_kMzU@vH0 z_@hNGmonU6yj{G(KaL%#pRyWzzX5mpW;7Qck+*Xq#4oogJ?S)g*MaEXT)H2hq!w$y zXG+nLe>q@}aY}pspsc~!FMT8RFN4m>jmo;l2W4fxy_a#xvc^BxcgC9ZwJpa|btepr zWY?~2v|*0F)9H$e>h_$?Rbo%fs*1M8ab8lowZ+SDuZ|m%TE-RiH4tuGO(B^0qUcZG zQK4e^33njnxnJbg1j_x+`95S-Yj{67%um}SP9O_e)N~i$(T~TQw%R!D;rG_Sd1`1< z#;enUjdzNw`u>hTPz`HH$C@y7#&qEB)b4Y?SQM_ zE3q}u>lKM69<1K#sxHq<=35lvAGUl%v%*26 zXy8wJaiRB8_!HI(0eg#EPGtioMS8B)9KxK(a^>!-$%G_pv{*fpPXlRWsvrz8<~;>B zn12(#-G|Nj7{Tt}3vr`w+I{VMPqJX2yjuuTIGn`l!(JkeDj_T?+PT-;P3jc#d?NM_siAf4`v!MCb2jTN0`lRU!06*~3KB x%2$4r6kfw48kO?TSy`F@;NJhEXK2%;D@0|S=+W#qfarig$_me+rB6-Y{|C^_64L+x literal 0 HcmV?d00001 diff --git a/parts/deployment.tex b/parts/deployment.tex index ea7398a..846e1c5 100755 --- a/parts/deployment.tex +++ b/parts/deployment.tex @@ -3,7 +3,7 @@ \begin{figure}[H] \centering \scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/penguin-overflow.jpg}} - \caption{David Revoy} + \caption{"La Grande Vague" \url{https://www.peppercarrot.com/sr/viewer/framasoft-src__2020-11-06_la-grande-vague_by-David-Revoy.html} par David Revoy et Framasoft, CC-By} \end{figure} @@ -19,23 +19,20 @@ --- Robert C. Martin Clean Architecture \end{quote} -Il y a une raison très simple à aborder le déploiement dès maintenant : à trop attendre et à peaufiner son développement en local, on en oublie que sa finalité sera de se retrouver exposé et accessible depuis un -serveur. -Il est du coup probable d'oublier une partie des désidérata, de zapper une fonctionnalité essentielle ou simplement de passer énormément de temps à adapter les sources pour qu'elles puissent être mises à -disposition sur un environnement en particulier, une fois que leur développement aura été finalisé, testé et validé. -Un bon déploiement ne doit pas dépendre de dizaines de petits scripts éparpillés sur le disque. -L'objectif est qu'il soit rapide et fiable. -Ceci peut être atteint au travers d'un partitionnement correct, incluant le fait que le composant principal s'assure que chaque sous-composant est correctement démarré intégré et supervisé. +Il y a une raison très simple à aborder le déploiement dès maintenant : à trop attendre et à peaufiner son développement pour soi-même, on en oublie que sa finalité sera de se retrouver exposé et accessible depuis un serveur. +Il est probable d'oublier une partie des désidérata, de zapper une fonctionnalité essentielle ou simplement de passer énormément de temps à adapter les sources pour qu'elles puissent être mises à disposition sur un environnement en particulier, une fois que leur développement aura été finalisé, testé et validé. -Aborder le déploiement dès le début permet également de rédiger dès le début les procédures d'installation, de mises à jour et de sauvegardes. +Un bon déploiement ne doit pas dépendre de dizaines de petits scripts éparpillés sur le disque: l'objectif est que celui-ci soit rapide et fiable. +Cet objectif peut être atteint au travers d'un partitionnement correct, incluant le fait que le composant principal s'assure que chaque sous-composant est correctement démarré intégré et supervisé. + +Aborder le déploiement dès le début du développement permet également de rédiger les procédures d'installation, de mises à jour et de sauvegardes adaptées. A la fin de chaque intervalle de développement, les fonctionnalités auront dû avoir été intégrées, testées, fonctionnelles et un code propre, démontrable dans un environnement similaire à un environnement de production, et créées à partir d'un tronc commun au développement \cite{devops_handbook}. Déploier une nouvelle version doit être le plus simple possible, et doit se résumer, dans le pire des cas, à quelques lignes d'un script Bash. \begin{quote} - Because value is created only when our services are running into production, we must ensure that we are not only delivering fast flow, but that our deployments can also be performed without causing chaos and - disruptions such as service outages, service impairments, or security or compliance failures. + Because value is created only when our services are running into production, we must ensure that we are not only delivering fast flow, but that our deployments can also be performed without causing chaos and disruptions such as service outages, service impairments, or security or compliance failures. --- DevOps Handbook Introduction \end{quote} @@ -50,11 +47,23 @@ En production: En restant avec les paramètres par défaut, il est plus que probable que vous rencontriez rapidement des erreurs de verrou parce qu'un autre processus a déjà pris la main pour écrire ses données. En bref, vous avez quelque chose qui fonctionne, qui répond à un besoin, mais qui va attirer la grogne de ses utilisateurs pour des problèmes de latences, pour des erreurs de verrou ou simplement parce que le serveur répondra trop lentement. \end{itemize} -L'objectif de cette partie est de parcourir les différentes possibilités qui s'offrent à nous en termes de déploiement, tout en faisant en sorte que le code soit le moins couplé possible à sa destination de -production. L'objectif est donc de faire en sorte qu'une même application puisse être hébergées par plusieurs hôtes sans avoir à subir de modifications. Nous vous renvoyons vers les 12-facteurs dont nous -avons déjà parlé et qui vous énormément nous aider, puisque ce sont des variables d'environnement qui vont réellement piloter le câblage entre l'application, ses composants et son hébergement. +L'objectif de cette partie est de parcourir les différentes possibilités qui s'offrent à nous en termes de déploiement, tout en faisant en sorte que le code soit le moins couplé possible à sa destination de production. +L'objectif est donc de faire en sorte qu'une même application puisse être hébergées par plusieurs hôtes sans avoir à subir de modifications. Nous vous renvoyons vers les 12-facteurs dont nous avons déjà parlé et qui vous énormément nous aider, puisque ce sont des variables d'environnement qui vont réellement piloter le câblage entre l'application, ses composants et son hébergement. -RedHat proposait récemment un article intitulé \emph{*What Is IaaS*}, qui présentait les principales différences entre types d'hébergement. +RedHat proposait récemment un article intitulé \emph{*What Is IaaS*} \footnote{\url{https://www.redhat.com/fr/topics/cloud-computing/what-is-iaas}}, qui présentait les principales différences entre types d'hébergement. + +\begin{tabular}{r c c c c} + & On-site & IaaS & PaaS & SaaS \\ + Applications & & & & \\ + Données & & & & \\ + Exécution & & & & \\ + Middleware & & & & \\ + Système d'exploitation & & & & \\ + Virtualisation & & & & \\ + Serveurs & & & & \\ + Stockage & & & & \\ + Réseau & & & & \\ +\end{tabular} \begin{figure}[H] \centering @@ -82,7 +91,9 @@ Dans cette partie, nous aborderons les points suivants: \item Définir l'infrastructure et les composants nécessaires à notre application \item - Configurer l'hôte qui hébergera l'application et y déployer notre application: dans une machine physique, virtuelle ou dans un container. Nous aborderons aussi les déploiements via Ansible et Salt. A ce stade, nous aurons déjà une application disponible. + Configurer l'hôte qui hébergera l'application et y déployer notre application: dans une machine physique, virtuelle ou dans un container. + Nous aborderons aussi les déploiements via Ansible et Salt. + A ce stade, nous aurons déjà une application disponible. \item Configurer les outils nécessaires à la bonne exécution de ce code et de ses fonctionnalités: les différentes méthodes de supervision de l'application, comment analyser les fichiers de logs, comment intercepter correctement une erreur si elle se présente et comment remonter correctement l'information. \end{enumerate} diff --git a/references.bib b/references.bib index 7e7fe56..fdd8d7d 100755 --- a/references.bib +++ b/references.bib @@ -60,6 +60,14 @@ isbn = {978-1-491-95452-2}, url = {http://shop.oreilly.com/product/0636920049555.do} } +@book{restful_web_apis, + title = {RESTful Web APIs}, + booktitle = {Services for a changing world}, + author = {Leonard Richardson and Mike Amundsen and Sam Ruby}, + year = {2013}, + isbn = {9781449359737}, + url = {https://www.oreilly.com/library/view/restful-web-apis/9781449359713/}, +} @book{roads_and_bridges, title = {Roads and Bridges}, booktitle = {The Unseen Labor Behind Our Digital Infrastructure},