reorganize files

This commit is contained in:
Fred 2020-04-11 09:23:28 +02:00
parent ccf93ae1bd
commit b8531ae8f1
9 changed files with 324 additions and 213 deletions

View File

@ -41,9 +41,9 @@ Et tout ça à un seul et même endroit. Oui. :-)
Bonne lecture.
include::part-1-workspace/main.adoc[]
include::part-1-workspace/00-main.adoc[]
include::part-2-deployment/main.adoc[]
include::part-2-deployment/00-main.adoc[]
= Modélisation

View File

@ -28,10 +28,10 @@ include::venvs.adoc[]
include::django.adoc[]
include::unit_tests.adoc[]
include::tools.adoc[]
include::external_tools.adoc[]
include::unit_tests.adoc[]
include::summary.adoc[]

View File

@ -41,24 +41,39 @@ Cette application servira à structurer les listes de souhaits, les éléments q
[source,bash]
----
$ python manage.py startapp gwift/wish
$ python manage.py startapp wish
----
Résultat? Django nous a créé un répertoire `wish`, dans lequel on trouve les fichiers et dossiers suivants:
* `wish/admin.py` servira à structurer l'administration de notre application. Chaque information peut en effet être administrée facilement au travers d'une interface générée à la volée par le framework. On y reviendra par la suite.
* `wish/__init__.py` pour que notre répertoire `wish` soit converti en package Python.
* `wish/migrations/`, dossier dans lequel seront stockées toutes les différentes migrations de notre application.
* `wish/models.py` pour représenter et structurer nos données.
* `wish/tests.py` pour les tests unitaires.
Par soucis de clarté, déplacez ce nouveau répertoire `wish` dans votre répertoire `gwift` existant. C'est une forme de convention. La structure de vos répertoires devient celle-ci:
[source,bash]
----
$ tree .
(gwift-env) fred@aerys:~/Sources/gwift$ tree .
.
├── docs
│   └── README.md
├── gwift
│   ├── asgi.py
│   ├── __init__.py
│   ├── __pycache__
│   │   ├── __init__.cpython-37.pyc
│   │   └── settings.cpython-37.pyc
│   ├── settings.py
│   ├── urls.py
│   ├── wish <1>
│   │   ├── admin.py
│   │   ├── apps.py
│   │   ├── __init__.py
│   │   ├── migrations
│   │   │   └── __init__.py
│   │   ├── models.py
│   │   ├── tests.py
│   │   └── views.py
│   └── wsgi.py
├── Makefile
├── manage.py
@ -67,33 +82,14 @@ $ tree .
│   ├── dev.txt
│   └── prod.txt
├── setup.cfg
├── tox.ini
└── wish <1>
├── admin.py
├── apps.py
├── __init__.py
├── migrations
│   └── __init__.py
├── models.py
├── tests.py
└── views.py
└── tox.ini
6 directories, 22 files
----
<1> Notre application a bien été créée!
Chaque nouveau fichier a la fonction suivante:
* `admin.py` servira à structurer l'administration de notre application. Chaque information peut en effet être administrée facilement au travers d'une interface générée à la volée par le framework. On y reviendra par la suite.
* `__init__.py` pour que notre répertoire `wish` soit converti en package Python.
* `migrations/`, dossier dans lequel seront stockées toutes les différentes migrations de notre application.
* `models.py` pour représenter et structurer nos données.
* `tests.py` pour les tests unitaires.
=== Tests unitaires
Plein de trucs à compléter ici ;-) Est-ce qu'on passe par pytest ou par le framework intégré ? Quels sont les avantages de l'un % à l'autre ?
* `views.py` pour définir ce que nous pouvons faire avec nos données.
<1> Notre application a bien été créée, et on l'a déplacée dans le répertoire `gwift` !
NOTE: vérifier s'il s'agit bien d'une forme de convention :-p
NOTE: Vérifier aussi comment les applications sont construites. Type DRF, Django Social Auth, tout ça.

View File

@ -1,4 +1,4 @@
== Git
=== Git
NOTE: insérer ici une description de Gitflow + quelques exemples types création, ajout, suppression, historique, branches, ... et quelques schémas qui-vont-bien.
@ -6,7 +6,10 @@ Il existe plusiseurs outils permettant de gérer les versions du code, dont les
Dans notre cas, nous utilisons git et hebergons le code et le livre directement sur le gitlab de `framasoft <https://git.framasoft.org/>`_
== graphviz
=== SSH
=== graphviz
En utilisant django_extensions (! bien suivre les étapes d'installation !).

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 KiB

View File

@ -1,49 +1,50 @@
== Chaîne d'outils
Le langage Python fonctionne avec un système daméliorations basées sur des propositions: les PEP, ou “Python Enhancement Proposal”.
=== PEP8 - Style Guide for Python Code
Celle qui va nous intéresser pour cette section est la https://www.python.org/dev/peps/pep-0008/[PEP 8 -- Style Guide for Python Code]. Elle spécifie comment du code Python doit être organisé ou formaté, quelles sont les conventions pour lindentation, le nommage des variables et des classes, … En bref, elle décrit comment écrire du code proprement pour que dautres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité.
Le langage Python fonctionne avec un système d'améliorations basées sur des propositions: les PEP, ou "**Python Enhancement Proposal**". Chacune d'entre elles doit être approuvée par le http://fr.wikipedia.org/wiki/Benevolent_Dictator_for_Life[Benevolent Dictator For Life].
=== PEP8
La première qui va nous intéresser est la https://www.python.org/dev/peps/pep-0008/[PEP 8 -- Style Guide for Python Code]. Elle spécifie comment du code Python doit être organisé ou formaté, quelles sont les conventions pour lindentation, le nommage des variables et des classes, … En bref, elle décrit comment écrire du code proprement pour que dautres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité.
Le langage Python fonctionne avec un système d'améliorations basées sur des propositions: les PEP, ou "**Python Enhancement Proposal**". Chacune d'entre elles doit être approuvée par le `Benevolent Dictator For Life <http://fr.wikipedia.org/wiki/Benevolent_Dictator_for_Life>`_.
Sur cette base, un outil existe et listera l'ensemble des conventions qui ne sont pas correctement suivies dans votre projet: pep8. Pour l'installer, passez par pip. Lancez ensuite la commande pep8 suivie du chemin à analyser (`.`, le nom d'un répertoire, le nom d'un fichier `.py`, ...). Si vous souhaitez uniquement avoir le nombre d'erreur de chaque type, saisissez les options `--statistics -qq`.
La PEP qui nous intéresse plus particulièrement pour la suite est la `PEP-8 <https://www.python.org/dev/peps/pep-0008/>`_, ou "Style Guide for Python Code". Elle spécifie des conventions d'organisation et de formatage de code Python, quelles sont les conventions pour l'indentation, le nommage des variables et des classes, etc. En bref, elle décrit comment écrire du code proprement pour que d'autres développeurs puissent le reprendre facilement, ou simplement que votre base de code ne dérive lentement vers un seuil de non-maintenabilité.
[source,bash]
----
Sur cette base, un outil existe et listera l'ensemble des conventions qui ne sont pas correctement suivies dans votre projet: pep8. Pour l'installer, passez par pip. Lancez ensuite la commande pep8 suivie du chemin à analyser (``.``, le nom d'un répertoire, le nom d'un fichier ``.py``, ...). Si vous souhaitez uniquement avoir le nombre d'erreur de chaque type, saisissez les options ``--statistics -qq``.
$ pep8 . --statistics -qq
.. code-block:: shell
7 E101 indentation contains mixed spaces and tabs
6 E122 continuation line missing indentation or outdented
8 E127 continuation line over-indented for visual indent
23 E128 continuation line under-indented for visual indent
3 E131 continuation line unaligned for hanging indent
12 E201 whitespace after '{'
13 E202 whitespace before '}'
86 E203 whitespace before ':'
----
$ pep8 . --statistics -qq
Si vous ne voulez pas être dérangé sur votre manière de coder, et que vous voulez juste avoir un retour sur une analyse de votre code, essayez `pyflakes`: il analysera vos sources à la recherche de sources d'erreurs possibles (imports inutilisés, méthodes inconnues, etc.).
7 E101 indentation contains mixed spaces and tabs
6 E122 continuation line missing indentation or outdented
8 E127 continuation line over-indented for visual indent
23 E128 continuation line under-indented for visual indent
3 E131 continuation line unaligned for hanging indent
12 E201 whitespace after '{'
13 E202 whitespace before '}'
86 E203 whitespace before ':'
Si vous ne voulez pas être dérangé sur votre manière de coder, et que vous voulez juste avoir un retour sur une analyse de votre code, essayez ``pyflakes``: il analysera vos sources à la recherche de sources d'erreurs possibles (imports inutilisés, méthodes inconnues, etc.).
Finalement, la solution qui couvre ces deux domaines existe et s'intitule `flake8 <https://github.com/PyCQA/flake8>`_. Sur base la même interface que ``pep8``, vous aurez en plus tous les avantages liés à ``pyflakes`` concernant votre code source.
Finalement, la solution qui couvre ces deux domaines existe et s'intitule https://github.com/PyCQA/flake8[flake8]. Sur base la même interface que `pep8`, vous aurez en plus tous les avantages liés à `pyflakes` concernant votre code source.
==== PEP257
.. todo:: à remplir avec ``pydocstyle``.
NOTE: à remplir avec `pydocstyle`.
NOTE: parler de Napoleon.
==== Tests
Comme tout bon *framework* qui se respecte, Django embarque tout un environnement facilitant le lancement de tests; chaque application est créée par défaut avec un fichier **tests.py**, qui inclut la classe ``TestCase`` depuis le package ``django.test``:
Comme tout bon *framework* qui se respecte, Django embarque tout un environnement facilitant le lancement de tests; chaque application est créée par défaut avec un fichier **tests.py**, qui inclut la classe `TestCase` depuis le package `django.test`:
.. code-block:: python
[source,python]
----
from django.test import TestCase
from django.test import TestCase
class TestModel(TestCase):
def test_str(self):
raise NotImplementedError('Not implemented yet')
class TestModel(TestCase):
def test_str(self):
raise NotImplementedError('Not implemented yet')
----
Idéalement, chaque fonction ou méthode doit être testée afin de bien en valider le fonctionnement, indépendamment du reste des composants. Cela permet d'isoler chaque bloc de manière unitaire, et permet de ne pas rencontrer de régression lors de l'ajout d'une nouvelle fonctionnalité ou de la modification d'une existante. Il existe plusieurs types de tests (intégration, comportement, ...); on ne parlera ici que des tests unitaires.
@ -51,37 +52,38 @@ Avoir des tests, c'est bien. S'assurer que tout est testé, c'est mieux. C'est l
==== Couverture de code
La couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests. Attention qu'il ne s'agit pas de vérifier que le code est **bien** testé, mais juste de vérifier **quelle partie** du code est testée. En Python, il existe le paquet `coverage <https://pypi.python.org/pypi/coverage/>`_, qui se charge d'évaluer le pourcentage de code couvert par les tests. Ajoutez-le dans le fichier ``requirements/base.txt``, et lancez une couverture de code grâce à la commande ``coverage``. La configuration peut se faire dans un fichier ``.coveragerc`` que vous placerez à la racine de votre projet, et qui sera lu lors de l'exécution.
La couverture de code est une analyse qui donne un pourcentage lié à la quantité de code couvert par les tests. Attention qu'il ne s'agit pas de vérifier que le code est **bien** testé, mais juste de vérifier **quelle partie** du code est testée. En Python, il existe le paquet https://pypi.python.org/pypi/coverage/[coverage], qui se charge d'évaluer le pourcentage de code couvert par les tests. Ajoutez-le dans le fichier `requirements/base.txt`, et lancez une couverture de code grâce à la commande `coverage`. La configuration peut se faire dans un fichier `.coveragerc` que vous placerez à la racine de votre projet, et qui sera lu lors de l'exécution.
.. code-block:: shell
[source,bash]
----
# requirements/base.text
[...]
coverage
django_coverage_plugin
----
# requirements/base.text
[...]
coverage
[source,bash]
----
# .coveragerc to control coverage.py
[run]
branch = True
omit = ../*migrations*
plugins =
django_coverage_plugin
.. code-block:: shell
[report]
ignore_errors = True
# .coveragerc to control coverage.py
[run]
branch = True
omit = ../*migrations*
plugins =
django_coverage_plugin
[html]
directory = coverage_html_report
----
[report]
ignore_errors = True
NOTE: le bloc ci-dessous est à revoir pour isoler la configuration.
[html]
directory = coverage_html_report
.. todo:: le bloc ci-dessous est à revoir pour isoler la configuration.
.. code-block:: shell
$ coverage run --source "." manage.py test
$ coverage report
[source,bash]
----
$ coverage run --source "." manage.py test
$ coverage report
Name Stmts Miss Cover
---------------------------------------------
@ -97,105 +99,109 @@ La couverture de code est une analyse qui donne un pourcentage lié à la quanti
gwift\wish\views.py 6 6 0%
---------------------------------------------
TOTAL 89 32 64%
----
$ coverage html
$ coverage html
----
Ceci vous affichera non seulement la couverture de code estimée, et générera également vos fichiers sources avec les branches non couvertes. Pour gagner un peu de temps, n'hésitez pas à créer un fichier ``Makefile`` que vous placerez à la racine du projet. L'exemple ci-dessous permettra, grâce à la commande ``make coverage``, d'arriver au même résultat que ci-dessus:
Ceci vous affichera non seulement la couverture de code estimée, et générera également vos fichiers sources avec les branches non couvertes. Pour gagner un peu de temps, n'hésitez pas à créer un fichier `Makefile` que vous placerez à la racine du projet. L'exemple ci-dessous permettra, grâce à la commande `make coverage`, d'arriver au même résultat que ci-dessus:
.. code-block:: shell
[source,makefile]
----
# Makefile for gwift
#
# Makefile for gwift
#
# User-friendly check for coverage
ifeq ($(shell which coverage >/dev/null 2>&1; echo $$?), 1)
$(error The 'coverage' command was not found. Make sure you have coverage installed)
endif
# User-friendly check for coverage
ifeq ($(shell which coverage >/dev/null 2>&1; echo $$?), 1)
$(error The 'coverage' command was not found. Make sure you have coverage installed)
endif
.PHONY: help coverage
.PHONY: help coverage
help:
@echo " coverage to run coverage check of the source files."
help:
@echo " coverage to run coverage check of the source files."
coverage:
coverage run --source='.' manage.py test; coverage report; coverage html;
@echo "Testing of coverage in the sources finished."
coverage:
coverage run --source='.' manage.py test; coverage report; coverage html;
@echo "Testing of coverage in the sources finished."
----
==== Complexité de McCabe
La `complexité cyclomatique <https://fr.wikipedia.org/wiki/Nombre_cyclomatique>`_ (ou complexité de McCabe) peut s'apparenter à mesure de difficulté de compréhension du code, en fonction du nombre d'embranchements trouvés dans une même section. Quand le cycle d'exécution du code rencontre une condition, il peut soit rentrer dedans, soit passer directement à la suite. Par exemple:
La https://fr.wikipedia.org/wiki/Nombre_cyclomatique[complexité cyclomatique] (ou complexité de McCabe) peut s'apparenter à mesure de difficulté de compréhension du code, en fonction du nombre d'embranchements trouvés dans une même section. Quand le cycle d'exécution du code rencontre une condition, il peut soit rentrer dedans, soit passer directement à la suite. Par exemple:
.. code-block:: python
[source,python]
----
if True == True:
pass # never happens
if True == True:
pass # never happens
# continue ...
# continue ...
----
La condition existe, mais on ne passera jamais dedans. A l'inverse, le code suivant aura une complexité pourrie à cause du nombre de conditions imbriquées:
.. code-block:: python
def compare(a, b, c, d, e):
if a == b:
if b == c:
if c == d:
if d == e:
print('Yeah!')
return 1
[source,python]
----
def compare(a, b, c, d, e):
if a == b:
if b == c:
if c == d:
if d == e:
print('Yeah!')
return 1
----
Potentiellement, les tests unitaires qui seront nécessaires à couvrir tous les cas de figure seront au nombre de quatre: le cas par défaut (a est différent de b, rien ne se passe), puis les autres cas, jusqu'à arriver à l'impression à l'écran et à la valeur de retour. La complexité cyclomatique d'un bloc est évaluée sur base du nombre d'embranchements possibles; par défaut, sa valeur est de 1. Si on rencontre une condition, elle passera à 2, etc.
Pour l'exemple ci-dessous, on va en fait devoir vérifier au moins chacun des cas pour s'assurer que la couverture est complète. On devrait donc trouver:
1. Un test pour entrer (ou non) dans la condition ``a == b``
2. Un test pour entrer (ou non) dans la condition ``b == c``
3. Un test pour entrer (ou non) dans la condition ``c == d``
4. Un test pour entrer (ou non) dans la condition ``d == e``
5. Et s'assurer que n'importe quel autre cas retournera la valeur ``None``.
. Un test pour entrer dans la condition `a == b`
. Un test pour entrer dans la condition `b == c`
. Un test pour entrer dans la condition `c == d`
. Un test pour entrer dans la condition `d == e`
. Et s'assurer que n'importe quel autre cas retournera la valeur `None`.
On a donc bien besoin de minimum cinq tests pour couvrir l'entièreté des cas présentés.
Le nombre de tests unitaires nécessaires à la couverture d'un bloc est au minimum égal à la complexité cyclomatique de ce bloc. Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil. Certains recommandent de le garder sous une complexité de 10; d'autres de 5.
Le nombre de tests unitaires nécessaires à la couverture d'un bloc fonctionnel est au minimum égal à la complexité cyclomatique de ce bloc. Une possibilité pour améliorer la maintenance du code est de faire baisser ce nombre, et de le conserver sous un certain seuil. Certains recommandent de le garder sous une complexité de 10; d'autres de 5.
.. note::
NOTE: Evidemment, si on refactorise un bloc pour en extraire une méthode, cela n'améliorera pas sa complexité cyclomatique globale
Evidemment, si on refactorise un bloc pour en extraire une méthode, cela n'améliorera pas sa complexité cyclomatique globale
A nouveau, un greffon pour ``flake8`` existe et donnera une estimation de la complexité de McCabe pour les fonctions trop complexes. Installez-le avec `pip install mccabe`, et activez-le avec le paramètre ``--max-complexity``. Toute fonction dans la complexité est supérieure à cette valeur sera considérée comme trop complexe.
A nouveau, un greffon pour `flake8` existe et donnera une estimation de la complexité de McCabe pour les fonctions trop complexes. Installez-le avec `pip install mccabe`, et activez-le avec le paramètre `--max-complexity`. Toute fonction dans la complexité est supérieure à cette valeur sera considérée comme trop complexe.
==== Documentation
Il existe plusieurs manières de générer la documentation d'un projet. Les plus connues sont `Sphinx <http://sphinx-doc.org/>`_ et `MkDocs <http://www.mkdocs.org/>`_. Le premier a l'avantage d'être plus reconnu dans la communauté Python que l'autre, de pouvoir *parser* le code pour en extraire la documentation et de pouvoir lancer des `tests orientés documentation <https://duckduckgo.com/?q=documentation+driven+development&t=ffsb>`_. A contrario, votre syntaxe devra respecter `ReStructuredText <https://en.wikipedia.org/wiki/ReStructuredText>`_. Le second a l'avantage d'avoir une syntaxe plus simple à apprendre et à comprendre, mais est plus limité dans son résultat.
Il existe plusieurs manières de générer la documentation d'un projet. Les plus connues sont http://sphinx-doc.org/[Sphinx] et http://www.mkdocs.org/[MkDocs]. Le premier a l'avantage d'être plus reconnu dans la communauté Python que l'autre, de pouvoir *parser* le code pour en extraire la documentation et de pouvoir lancer des https://duckduckgo.com/?q=documentation+driven+development&t=ffsb[tests orientés documentation]. A contrario, votre syntaxe devra respecter https://en.wikipedia.org/wiki/ReStructuredText[ReStructuredText]. Le second a l'avantage d'avoir une syntaxe plus simple à apprendre et à comprendre, mais est plus limité dans ses résultats.
Dans l'immédiat, nous nous contenterons d'avoir des modules documentés (quelle que soit la méthode Sphinx/MkDocs/...). Dans la continuié de ``Flake8``, il existe un greffon qui vérifie la présence de commentaires au niveau des méthodes et modules développés.
NOTE: parler aussi d'asciidoctor (même si moins bien intégré).
.. note::
Dans l'immédiat, nous nous contenterons d'avoir des modules documentés (quelle que soit la méthode Sphinx/MkDocs/...). Dans la continuié de `Flake8`, il existe un greffon qui vérifie la présence de commentaires au niveau des méthodes et modules développés.
voir si il ne faudrait pas mieux passer par pydocstyle.
NOTE: voir si il ne faudrait pas mieux passer par pydocstyle.
.. code-block:: shell
[source,bash]
----
$ pip install flake8_docstrings
----
$ pip install flake8_docstrings
Lancez ensuite `flake8` avec la commande ``flake8 . --exclude="migrations"``. Sur notre projet (presque) vide, le résultat sera le suivant:
.. code-block:: shell
$ flake8 . --exclude="migrations"
.\src\manage.py:1:1: D100 Missing docstring in public module
.\src\gwift\__init__.py:1:1: D100 Missing docstring in public module
.\src\gwift\urls.py:1:1: D400 First line should end with a period (not 'n')
.\src\wish\__init__.py:1:1: D100 Missing docstring in public module
.\src\wish\admin.py:1:1: D100 Missing docstring in public module
.\src\wish\admin.py:1:1: F401 'admin' imported but unused
.\src\wish\models.py:1:1: D100 Missing docstring in public module
.\src\wish\models.py:1:1: F401 'models' imported but unused
.\src\wish\tests.py:1:1: D100 Missing docstring in public module
.\src\wish\tests.py:1:1: F401 'TestCase' imported but unused
.\src\wish\views.py:1:1: D100 Missing docstring in public module
.\src\wish\views.py:1:1: F401 'render' imported but unused
Lancez ensuite `flake8` avec la commande `flake8 . --exclude="migrations"`. Sur notre projet (presque) vide, le résultat sera le suivant:
[source,bash]
----
$ flake8 . --exclude="migrations"
.\src\manage.py:1:1: D100 Missing docstring in public module
.\src\gwift\__init__.py:1:1: D100 Missing docstring in public module
.\src\gwift\urls.py:1:1: D400 First line should end with a period (not 'n')
.\src\wish\__init__.py:1:1: D100 Missing docstring in public module
.\src\wish\admin.py:1:1: D100 Missing docstring in public module
.\src\wish\admin.py:1:1: F401 'admin' imported but unused
.\src\wish\models.py:1:1: D100 Missing docstring in public module
.\src\wish\models.py:1:1: F401 'models' imported but unused
.\src\wish\tests.py:1:1: D100 Missing docstring in public module
.\src\wish\tests.py:1:1: F401 'TestCase' imported but unused
.\src\wish\views.py:1:1: D100 Missing docstring in public module
.\src\wish\views.py:1:1: F401 'render' imported but unused
----
Bref, on le voit: nous n'avons que très peu de modules, et aucun d'eux n'est commenté.
@ -232,6 +238,8 @@ if __name__ == "__main__":
--
NOTE: l'exemple est sans doute un peu trop tiré par les cheveux...
L'exécution de la commande flake8 . retourne ceci:
[source,bash]
@ -252,7 +260,7 @@ On trouve des erreurs:
L'étape d'après consiste à invoquer pylint. Lui, il est directement moins conciliant:
[source,text]
--
----
$ pylint test.py
************* Module test
test.py:16:6: C0326: Exactly one space required after assignment
@ -273,7 +281,7 @@ test.py:16:10: E0602: Undefined variable 'Get_Today' (undefined-variable)
--------------------------------------------------------------------
Your code has been rated at -5.45/10
--
----
En gros, j'ai programmé comme une grosse bouse anémique (et oui, le score d'évaluation du code permet bien d'aller en négatif). En vrac, on trouve des problèmes liés:
@ -295,6 +303,22 @@ PyLint est la version **++**, pour ceux qui veulent un code propre et sans bavur
=== Black
By using Black, you agree to cede control over minutiae of hand-formatting. In return, Black gives you speed, determinism, and freedom from pycodestyle nagging about formatting. You will save time and mental energy for more important matters.
Black makes code review faster by producing the smallest diffs possible. Blackened code looks the same regardless of the project youre reading. Formatting becomes transparent after a while and you can focus on the content instead.
https://black.readthedocs.io/en/stable/[Black].
Une chose qui fonctionne bien avec le langage Go, c'est que les outils de base sont intégrés au compilateur : le formatage de code et les tests unitaires sont à la portée de tout le monde au travers de deux commandes simples :
. `go fmt`
. `go test`
En Python, c'est plus complexe que cela, puisqu'il n'existe pas une manière unique d'arriver à un résultat (on l'a vu ci-dessus, rien que pour les tests, on a au moins deux librairies...).
Pour revenir à Go : est-ce que ce formatage est idéal et accepté par tout le monde ? Non.
Black fait le même travail: il arrive à un compromis entre la clarté du code, la facilité d'installation et d'intégration et un résultat. Ce résultat ne sera pas parfait, mais il conviendra dans 97,83% des cas (au moins).
=== pytest

View File

@ -0,0 +1,65 @@
== Tests unitaires
Si on schématise l'infrastructure et le chemin parcouru par une éventuelle requête, on devrait arriver à quelque chose de synthéthique:
User -> Firefox -> http GET 1.1 -> Debian GNU/Linux -> Nginx -> gunicorn -> django -> url -> route -> fonction/classe/méthode -> construction -> accès à la base de données -> retour des données -> rendu -> réponse à l'utilisateur.
image::images/scenarii-de-plantages.png
En gros, ça peut planter aux points suivants :
. L'utilisateur n'est pas connecté à l'Internet
. La version du navigateur utilisé est obsolète et les technologies de sécurité ne sont plus supportées
. La version d'HTTP n'est pas prise en charge par notre serveur
. Le verbe HTTP n'est pas pris en charge par la fonction
. Le système n'a plus de place sur son disque
. Nginx est mal configuré
. La communication par socket est mal configuré
. L'URL est inconnue
. La route n'existe pas
. La base de dnnées est inaccessible
. La fonction n'est pas correctement appelée
. Il manque des valeurs au rendu
. Il y a tellement de données qu'Nginx ou Gunicorn ou déjà envoyé une fin de requête à l'utilisateur (_timeout_)
...
En bref, on a potentiellement un ou plusieurs problèmes potentiels à chaque intervenant. Une chose à la fois: dans un premier temps, on va se concentrer sur notre code.
Vous aurez remarqué ci-dessus qu'une nouvelle application créée par Django vient d'office avec un fichier `tests.py`. C'est simplement parce que les tests unitaires et tests d'intégration font partie intégrante du cadre de travail. Ceci dit, chaque batterie de tests demande une certaine préparation et un certain "temps de cuisson"... Comprendre qu'un test ne sera effectif que s'il est correctement structurée (_configuration over convention_ ?), s'il se trouve dans une classe et que chaque test se trouve bien dans une méthode de cette classe... Cela commence à faire beaucoup de boulot pour vérifier qu'une fonction retourne bien une certaine valeur, et en décourager la plupart d'entrée de jeux, surtout quand on sait que chaque fonction, condition ou point d'entrée sera sensée disposer de son test.
Au niveau des améliorations, on va :
. Changer de framework de test et utiliser https://docs.pytest.org/en/latest/[pytest] et son greffon https://pytest-django.readthedocs.io/en/latest/[pytest-django]
. Ajouter une couverture de code.
=== Pytest
Pourquoi pytest, plutôt que Django ? Par pure fainéantise :-) Si, si ! Pytest est relativement plus facile à utiliser, permet un contrôle plus fin de certaines variables d'environnement et est surtout compatible hors-Django (en plus de quelques améliorations sur les performances, comme les tests sur plusieurs processus).
Cela signifie surtout que si vous apprenez Pytest maintenant, et que votre prochain projet est une application en CLI ou avec un autre framework, vous ne serez pas dépaysé. Les tests unitaires de Django sont compatibles uniquement avec Django... D'où une certaine perte de vélocité lorsqu'on devra s'en détacher.
Vous trouverez ci-dessous une comparaison entre des tests avec les deux frameworks:
[source,python]
----
# avec django
----
[source,python]
----
# avec pytest
----
=== Couverture de code
Dans un premier temps, *le pourcentage de code couvert par nos tests*. Une fois ce pourcentage évalué, le but du jeu va consister à *ce que ce pourcentage reste stable ou augmente*. Si vous modifiez une ligne de code et que la couverture passe de 73% à 72%, vous avez perdu et vous devez faire en sorte de corriger.
Plein de trucs à compléter ici ;-) Est-ce qu'on passe par pytest ou par le framework intégré ? Quels sont les avantages de l'un % à l'autre ?
* `views.py` pour définir ce que nous pouvons faire avec nos données.

View File

@ -2,7 +2,7 @@
On va déjà parler de déploiement. Le serveur que django met à notre disposition est prévu uniquement pour le développement: inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, ...). De même, la base de donnée doit supporter plus qu'un seul utilisateur: SQLite fonctionne très bien dès lors qu'on se limite à un seul utilisateur... Sur une application Web, il est plus que probable que vous rencontriez rapidement des erreurs de base de données verrouillée pour écriture par un autre processus. Il est donc plus que bénéfique de passer sur quelque chose de plus solide.
Déploiement[https://docs.djangoproject.com/fr/3.0/howto/deployment/].
https://docs.djangoproject.com/fr/3.0/howto/deployment/[Déploiement].
Si vous avez suivi les étapes jusqu'ici, vous devriez à peine disposer d'un espace de travail proprement configuré, d'un modèle relativement basique et d'une configuration avec une base de données simpliste. En bref, vous avez quelque chose qui fonctionne, mais qui ressemble de très loin à ce que vous souhaitez au final.
@ -17,22 +17,19 @@ Dans cette partie, on abordera les points suivants:
* Les différentes méthodes de supervision de l'application: comment analyser les fichiers de logs et comment intercepter correctement une erreur si elle se présente et comment remonter l'information.
* Une partie sur la sécurité et la sécurisation de l'hôte.
include::infrastructure.adoc[]
include::centos+debian.adoc[]
include::database.adoc[]
=== Définition de l'infrastructure
== Définition de l'infrastructure
Comme on l'a vu dans la première partie, Django est un framework complet, intégrant tous les mécanismes nécessaires à la bonne évolution d'une application. On peut ainsi commencer petit, et suivre l'évolution des besoins en fonction de la charge estimée ou ressentie, ajouter un mécanisme de mise en cache, des logiciels de suivi, ...
Pour une mise ne production, le standard *de facto* est le suivant:
* Nginx comme serveur principal
* Gunicorn comme serveur d'application
* Nginx comme reverse proxy
* Gunicorn ou Uvicorn comme serveur d'application
* Supervisorctl pour le monitoring
* PostgreSQL comme base de données.
* PostgreSQL ou MariaDB comme base de données.
* Redis / Memcache pour la mise à en cache (et pour les sessions ? A vérifier).
En mode _containers_, on passera plutôt par Docker et Traefik.
C'est celle-ci que nous allons décrire ci-dessous.
@ -47,13 +44,39 @@ entity "Gunicorn (sockets/HTTP)" as gunicorn
database PGSQL
--
Aussi : Docker, Heroku, Digital Ocean, Scaleway, OVH, ... Bref, sur Debian et CentOS pour avoir un panel assez large. On oublie Windows.
Aussi : Docker, Heroku, Digital Ocean, Scaleway, OVH, Ansible, Puppet, Chef, ... Bref, sur Debian et CentOS pour avoir un panel assez large. On oublie Windows: rien que Gunicorn et Nginx n'y tournent pas.
=== Mise à jour
Script de mise à jour.
[source,bash]
----
su - <user>
source ~/.venvs/<app>/bin/activate
cd ~/webapps/<app>
git fetch
git checkout vX.Y.Z
pip install -U requirements/prod.txt
python manage.py migrate
python manage.py collectstatic
gunicorn reload -HUP
----
WARNING: le serveur de déploiement ne doit avoir qu'un accès en lecture au dépôt source.
=== Supervision
Qu'est-ce qu'on fait des logs après ? :-)
. Sentry
. Nagios
. LibreNMS
. Zabbix
include::infrastructure.adoc[]
include::centos+debian.adoc[]
include::database.adoc[]

View File

@ -5,9 +5,9 @@
yum update
groupadd --system webapps
groupadd --system gunicorn_sockets
useradd --system --gid webapps --shell /bin/bash --home /home/medplan medplan
mkdir -p /home/medplan
chown medplan:webapps /home/medplan
useradd --system --gid webapps --shell /bin/bash --home /home/gwift gwift
mkdir -p /home/gwift
chown gwift:webapps /home/gwift
----
=== Installation des dépendances systèmes
@ -26,17 +26,17 @@ sudo yum install sqlite-3.8.11-1.fc21.x86_64.rpm sqlite-devel-3.8.11-1.fc21.x86_
[source,bash]
----
su - medplan
su - gwift
cp /etc/skel/.bashrc .
cp /etc/skel/.bash_profile .
ssh-keygen
mkdir bin
mkdir .venvs
mkdir webapps
python3.6 -m venv .venvs/medplan
source .venvs/medplan/bin/activate
cd /home/medplan/webapps
git clone git@vmwmedtools:institutionnel/medplan.git
python3.6 -m venv .venvs/gwift
source .venvs/gwift/bin/activate
cd /home/gwift/webapps
git clone git@vmwmedtools:institutionnel/gwift.git
----
La clé SSH doit ensuite être renseignée au niveau du dépôt, afin de pouvoir y accéder.
@ -45,13 +45,13 @@ A ce stade, on devrait déjà avoir quelque chose de fonctionnel en démarrant l
[source,bash]
----
# en tant qu'utilisateur 'medplan'
# en tant qu'utilisateur 'gwift'
source .venvs/medplan/bin/activate
source .venvs/gwift/bin/activate
pip install -U pip
pip install -r requirements/base.txt
pip install gunicorn
cd webapps/medplan
cd webapps/gwift
gunicorn config.wsgi:application --bind localhost:3000 --settings=config.settings_production
----
@ -61,23 +61,23 @@ gunicorn config.wsgi:application --bind localhost:3000 --settings=config.setting
----
SECRET_KEY=<set your secret key here>
ALLOWED_HOSTS=*
STATIC_ROOT=/var/www/medplan/static
STATIC_ROOT=/var/www/gwift/static
----
=== Création des répertoires de logs
[source,text]
----
mkdir -p /var/www/medplan/static
mkdir -p /var/www/gwift/static
----
=== Création du répertoire pour le socket
Dans le fichier `/etc/tmpfiles.d/medplan.conf`:
Dans le fichier `/etc/tmpfiles.d/gwift.conf`:
[source,text]
----
D /var/run/webapps 0775 medplan gunicorn_sockets -
D /var/run/webapps 0775 gwift gunicorn_sockets -
----
Suivi de la création par systemd :
@ -94,10 +94,10 @@ systemd-tmpfiles --create
#!/bin/bash
# defines settings for gunicorn
NAME="Medplan"
DJANGODIR=/home/medplan/webapps/medplan
SOCKFILE=/var/run/webapps/gunicorn_medplan.sock
USER=medplan
NAME="gwift"
DJANGODIR=/home/gwift/webapps/gwift
SOCKFILE=/var/run/webapps/gunicorn_gwift.sock
USER=gwift
GROUP=gunicorn_sockets
NUM_WORKERS=5
DJANGO_SETTINGS_MODULE=config.settings_production
@ -105,7 +105,7 @@ DJANGO_WSGI_MODULE=config.wsgi
echo "Starting $NAME as `whoami`"
source /home/medplan/.venvs/medplan/bin/activate
source /home/gwift/.venvs/gwift/bin/activate
cd $DJANGODIR
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
@ -126,14 +126,14 @@ exec gunicorn ${DJANGO_WSGI_MODULE}:application \
yum install supervisor -y
----
On crée ensuite le fichier `/etc/supervisord.d/medplan.ini`:
On crée ensuite le fichier `/etc/supervisord.d/gwift.ini`:
[source,bash]
----
[program:medplan]
command=/home/medplan/bin/start_gunicorn.sh
user=medplan
stdout_logfile=/var/log/medplan/medplan.log
[program:gwift]
command=/home/gwift/bin/start_gunicorn.sh
user=gwift
stdout_logfile=/var/log/gwift/gwift.log
autostart=true
autorestart=unexpected
redirect_stdout=true
@ -144,8 +144,8 @@ Et on crée les répertoires de logs, on démarre supervisord et on vérifie qu'
[source,bash]
----
mkdir /var/log/medplan
chown medplan:nagios /var/log/medplan
mkdir /var/log/gwift
chown gwift:nagios /var/log/gwift
systemctl enable supervisord
systemctl start supervisord.service
@ -157,12 +157,12 @@ systemctl status supervisord.service
Main PID: 2310 (supervisord)
CGroup: /system.slice/supervisord.service
├─2310 /usr/bin/python /usr/bin/supervisord -c /etc/supervisord.conf
├─2313 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
├─2317 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
├─2318 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
├─2321 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
├─2322 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
└─2323 /home/medplan/.venvs/medplan/bin/python3 /home/medplan/.venvs/medplan/bin/gunicorn config.wsgi:...
├─2313 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
├─2317 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
├─2318 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
├─2321 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
├─2322 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
└─2323 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...
ls /var/run/webapps/
----
@ -205,19 +205,19 @@ yum install nginx -y
usermod -a -G gunicorn_sockets nginx
----
On configure ensuite le fichier `/etc/nginx/conf.d/medplan.conf`:
On configure ensuite le fichier `/etc/nginx/conf.d/gwift.conf`:
----
upstream medplan_app {
server unix:/var/run/webapps/gunicorn_medplan.sock fail_timeout=0;
upstream gwift_app {
server unix:/var/run/webapps/gunicorn_gwift.sock fail_timeout=0;
}
server {
listen 80;
server_name <server_name>;
root /var/www/medplan;
error_log /var/log/nginx/medplan_error.log;
access_log /var/log/nginx/medplan_access.log;
root /var/www/gwift;
error_log /var/log/nginx/gwift_error.log;
access_log /var/log/nginx/gwift_access.log;
client_max_body_size 4G;
keepalive_timeout 5;
@ -242,7 +242,7 @@ server {
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://medplan_app;
proxy_pass http://gwift_app;
}
}
----
@ -251,17 +251,17 @@ server {
Les sauvegardes ont été configurées avec borg: `yum install borgbackup`.
C'est l'utilisateur medplan qui s'en occupe.
C'est l'utilisateur gwift qui s'en occupe.
----
mkdir -p /home/medplan/borg-backups/
cd /home/medplan/borg-backups/
borg init medplan.borg -e=none
borg create medplan.borg::{now} ~/bin ~/webapps
mkdir -p /home/gwift/borg-backups/
cd /home/gwift/borg-backups/
borg init gwift.borg -e=none
borg create gwift.borg::{now} ~/bin ~/webapps
----
Et dans le fichier crontab :
----
0 23 * * * /home/medplan/bin/backup.sh
0 23 * * * /home/gwift/bin/backup.sh
----