Python Flask 2020
Python Flask 2020
Python Flask 2020
PAR L'EXEMPLE
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
1/755
1 Avant-propos
Ce document propose une liste de scripts Python dans différents domaines :
Ce n'est pas un cours Python exhaustif mais un recueil d'exemples destinés à des développeurs ayant déjà utilisé un langage de script
tel que Perl, PHP, VBScript ou des développeurs habitués aux langages typés tels que Java ou C# et qui seraient intéressés par
découvrir un langage de script orienté objet. Ce document est peu approprié pour des lecteurs n'ayant jamais ou peu programmé.
Ce document n'est pas non plus un recueil de "bonnes pratiques". Le développeur expérimenté pourra ainsi trouver que certains
codes pourraient être mieux écrits. Ce document a pour seul objectif de donner des exemples à une personne désireuse de s'initier au
langage Python 3 et au Framework Flask. Elle approfondira ensuite son apprentissage avec d'autres documents.
Les scripts sont commentés et les résultats de leur exécution reproduits. Parfois des explications supplémentaires sont fournies. Le
document nécessite une lecture active : pour comprendre un script, il faut à la fois lire son code, ses commentaires et ses résultats
d’exécution.
Le document est une mise à jour d'un ancien document paru en juin 2011 |https://tahe.developpez.com/tutoriels-cours/python/|.
Le document de 2011 avait été construit avec l'interpréteur Python 2.7. Depuis, des versions Python 3.x sont apparues. En février
2020, on en est à la version 3.8. Les versions 3.x ont amené une discontinuité dans la portabilité entre versions : des codes s'exécutant
sous Python 2.7 peuvent se révéler inutilisables sous Python 3.x. C'est notamment le cas pour l'écriture console. En 2.7 on peut écrire
[print "toto"] alors qu'en 3.x il faut écrire [print("toto")] : il faut mettre des parenthèses. Cette simple évolution fait que les codes
fournis avec le document de 2011 sont pour la plupart inutilisables directement avec Python 3.x. Il faut les modifier.
Ce nouveau document ne se contente pas de mettre à jour les codes de 2011 pour qu'ils soient exécutables avec Python 3.8 :
• les sections sur la programmation TCP-IP et l’usage des bases de données ont subi d’importantes évolutions ;
• la section sur la programmation web qui n’était qu’une introduction est désormais un cours complet utilisant le framework
FLASK ;
Pour construire ce cours, j’ai suivi une démarche inhabituelle : j’ai fait un portage du cours PHP [Introduction au langage PHP7 par
l’exemple]. Je n’ai donc pas suivi les structures traditionnelles des cours Python ou Flask. Je voulais avant tout savoir comment je
pouvais faire avec Python 3 ce que j’avais fait en PHP 7. Le résultat est que j’ai pu refaire, en Python 3 / Flask, tous les exemples du
cours PHP 7.
Ce document peut comporter des erreurs ou des insuffisances. Le lecteur peut utiliser le flux de discussion |forum| pour les signaler.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
2/755
Le contenu du document est le suivant :
Chapitre 3 [bases] Les bases du langage Python – structures du langage – type de données
– fonctions – affichage console – chaînes de formatage – changement
de types – les listes – les dictionnaires – les expression srégulières
Chapitre 6 [fonctions] Portée des variables – mode de passage des paramètres – utiliser des
modules – le Python Path – paramètres nommés – fonctions récursives
Chapitre 7 [fichiers] Lecture / écriture d’un fichier texte – gestion des fichiers encodés en
UTF-8 – gestion des fichiers jSON
Chapitre 8 [impots/v01] Version 1 de l’exercice d’application, un calcul d’impôt sur les revenus.
L’application est déclinée en 18 versions – La version 1 implémente
une solution procédurale
Chapitre 15 [impots/v04] Version 4 de l’application – cette version implémente une solution avec
une architecture en couches, la programmation par interfaces,
l’utilisation de classes dérivées de [BaseEntity] et [MyException]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
3/755
Chapitre 19 [databases/sqlalchemy] L’ORM (Object Relational Mapper) SqlAlchemy – un ORM permet de
travailler de façon unifiée avec différents SGBD - mapping classes /
tables SQL – opérations sur les classes images des tables SQL
Chapitre 22 [flask] Services web avec le framework web Flask – affichage d’une page
HTML – service web jSON – requêtes GET et POST – gstion d’une
session web
Chapitre 23 [impots/http-servers/01] Version 6 de l’exercice d’application - Création d’un service web jSON
[impots/http-clients/01] de calcul de l’impôt avec une architecture multicouche - Ecriture d’un
client web pour ce serveur avec une architecture multicouche –
programmation client / serveur – utilisation du module [requests]
Chapitre 24 [impots/http-servers/02]
Version 7 de l’exercice de l’application – la version 6 est améliorée : le
client et le serveur sont multithtreadés – utilitaires [Logger] pour loguer
[impots/http-clients/02]
les échanges client / serveur – [SendMail] pour envoyer un mail à
l’administrateur de l’application
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
4/755
Chapitre 35 [impots/http-servers/10] Version 15 de l’exercice d’application – refactorisation du code de la
version 14 pour gérer deux types d’actions : ASV (Action Show View)
qui ne servent qu’à afficher une vue sans modifier l’état du serveur,
ADS (Action Do Something) qui font une action qui modifie l’état du
serveur – ces actions se terminent toutes par une redirection vers une
action ASV – cela permet de gérer correctement les rafraîchissements
de page du navigateur client
Chapitre 36 [impots/http-servers/11] Version 16 de l’application – gestion des URL avec préfixe
La version finale de l’exercice d’application est une application client / serveur de calcul de l’impôt avec l’architecture suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
5/755
Le contenu du document est dense. Le lecteur qui ira jusqu’au bout aura une bonne vision de la programmation web MVC en Python
/ Flask et au-delà une bonne vision de la programmation web MVC dans d’autres langages.
Le lecteur qui préfère voir du code, le tester et le modifier plutôt que de lire un cours pourra procéder ainsi :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
6/755
• le lecteur débutant pourra lire les chapitres 1 à 15 et s’arrêter là. Il pourra ensuite passer du temps à coder ses propres scripts
Python avant de revenir à ce cours ;
• le lecteur ayant les bases de Python et voulant s’initier aux bases de données et à l’ORM (Object Relational Mapper)
[sqlalchemy] pourra se contenter des chapitres 16 à 20 ;
• le lecteur voulant s’initier à la programmation internet (HTTP, SMTP, POP3, IMAP) pourra lire le chapitre 21. Ce chapitre est
assez complexe et montre des scripts avancés. On peut le lire à deux niveaux :
o pour découvrir les protocoles de l’internet ;
o pour obtenir des scripts exploitant ces protocoles ;
• le lecteur ayant les bases de Python et voulant s’initier à la programmation web avec le framework Flask lira le chapitre 22 ;
• le lecteur désirant approfondir la programmation web avec le framework Flask pourra étudier les chapitres 23 à 38. On y
construit des applications client / serveur de plus en plus complexes ainsi qu’une application HTML / Python suivant le
modèle de développement MVC (Modèle – Vue – Contrôleur). Cette application est développée au chapitre 32. On pourra
s’arrêter là. Les chapitres suivants amènent des modifications non fondamentales ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
7/755
2 Installation d'un environnement de travail
2.1 Python 3.8.1
Les exemples de ce document ont été testés avec l'interpréteur Python 3.8.1 disponible à l'URL |https://www.python.org/downloads/| (fév
2020) sur une machine Windows 10 :
L'installation de Python donne naissance à l'arborescence de fichiers [1] et au menu [2] dans la liste des programmes :
Nous n'utiliserons pas l'interpréteur Python interactif. Il faut simplement savoir que les scripts de ce document pourraient être
exécutés avec cet interpréteur. Pratique pour tester le fonctionnement d'une fonctionnalité Python, il l'est peu pour des scripts devant
être réutilisés. Voici un exemple avec l'option [4] ci-dessus :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
8/755
Le prompt >>> permet d'émettre une instruction Python qui est immédiatement exécutée. Le code tapé ci-dessus a la signification
suivante :
1. >>> nom="tintin"
2. >>> print("nom=%s" % nom)
3. nom=tintin
4. >>> print("type=%s" % type(nom))
5. type=<class 'str'>
6. >>>
Lignes :
• 1 : initialisation d'une variable. En Python, on ne déclare pas le type des variables. Celles-ci ont automatiquement le type de
la valeur qu'on leur affecte. Ce type peut changer au cours du temps ;
• 2 : affichage du nom. 'nom=%s' est un format d'affichage où %s est un paramètre formel désignant une chaîne de caractères.
nom est le paramètre effectif qui sera affiché à la place de %s ;
• 3 : le résultat de l'affichage ;
• 4 : l'affichage du type de la variable nom ;
• 5 : la variable nom est ici de type class. Avec Python 2.7 elle aurait la valeur <type 'str'> ;
Le fait qu'on ait pu taper [python] en [1] et que l'exécutable [python.exe] ait été trouvé montre que celui-ci est dans le PATH de la
machine Windows. C'est important car cela signifie que les outils de développement Python sauront trouver l'interpréteur Python.
On peut le vérifier ainsi :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
9/755
• en [2], on quitte l'interpréteur Python ;
• en [3], la commande qui affiche le PATH des exécutables de la machine Windows ;
• en [4], on voit que le dossier de l'interpréteur Python 3.8 fait partie du PATH ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
10/755
• en [2-4], créez un nouveau projet ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
11/755
• en [4], l'interpréteur Python qui sera utilisé pour le projet ;
• en [5], une liste déroulante des interpréteurs disponibles ;
• en [6], on choisit l'interpréteur téléchargé au paragraphe |Python 3.8.1| ;
Commençons par créer un dossier dans lequel nous mettrons notre premier script Python :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
12/755
• cliquer droit sur le dossier [bases], puis [1-3] ;
• en [4-5], indiquez le nom du script ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
13/755
• en [7], la commande exécutée ;
• en [8], le résultat de l'exécution ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
14/755
• en [7-8], les résultats de l'exécution ;
• par défaut, la case [1] est cochée. On la décoche pour que Pycharm n’ouvre pas par défaut le dernier projet ouvert mais nous
laisse choisir celui qu’il faut ouvrir ;
• en [2], on ne confirme pas la sortie de Pycharm lorsqu’on ferme la fenêtre de l’application ;
• en [3], on ouvre les nouveaux projets dans une autre fenêtre ;
• en [4], si on ferme Pycharm et qu’un programme est en cours d’exécution, celui-ci est arrêté ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
15/755
• en [1], on crée un nouveau projet ;
• en [2], indiquer le dossier du projet ;
• en [3], prendre un environnement virtuel. Un environnement virtuel est propre au projet que l’on crée. Il ne se mélange pas
avec les environnements virtuels d’autres projets. Un projet Python / Flask utilise de nombreuses bibliothèques externes
qu’il faut installer. Un projet P1 peut utiliser une bibliothèque B dans sa version v1 et un projet P2 utiliser la même
bibliothèque B mais dans sa version v2. Ces deux versions peuvent être plus ou moins comptatibles. Or lorsqu’on installe
une bibliothèque dans sa version v2 et qu’une version v1 est déjà installée, celle-ci est écrasée par la version v2. Cela peut
être problématique pour le projet qui utilisait la version v1 si la nouvelle version v2 n’est pas totalement compatible avec la
version v1. Pour éviter ces problèmes, on isole chaque projet dans un environnement virtuel ;
• en [4], désigner le dossier où seront rangées les bibliothèques python qui seront téléchargées au cours du projet. Nous avons
choisi ici un dossier [venv] (virtual environment) à l’intérieur du dossier du projet. Rien n’oblige à faire cela ;
• en [5], l’interpréteur Python du projet. C’est celui que nous avons installé dans l’étape précédente ;
• en [6], créer le projet ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
16/755
• en [7-8], le projet créé ;
• en [9], l’environnement d’exécution du projet, appelé environnement virtuel ;
• en [10], le dossier [site-packages] est le dossier dans lequel seront rangées les bibliothèques téléchargées ultérieurement ;
2.2.3 Git
Ensuite, on active un logiciel de contrôle du code source. Ce sera ici Git [1-4] :
Un logiciel de contrôle du code source (VCS : Version Control System) permet de suivre les évolutions d’un projet. On peut prendre
des photos, par une opération appelée commit, du projet à différents moments de sa vie. Si on fait deux commit aux temps T et T+1,
le VCS permet de connaître ce qui a changé entre les deux versions commitées. Normalement le VCS est utilisé par une équipe de
développeurs. Ceux-ci font des commit de leur code lorsque celui-ci a été dûment testé. A partir du VCS, les autres développeurs
peuvent récupérer ce code validé et l’utiliser.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
17/755
Ici, il n’y a qu’un développeur. L’expérience montre qu’on peut avoir une application qui marche au temps T et ne marche plus au
temps T+1. On voudrait bien alors revenir en arrière au temps T pour repartir de zéro. Le VCS permet cela et c’est pour cette raison
que l’on va l’utiliser ici.
Certains dossiers ou fichiers peuvent être ignorés par Git. Ils ne font alors jamais partie de la photo. En [5] ci-dessus, cliquons droit
sur le dossier [venv].
• en [1-3], on indique que le dossier [venv] ne doit pas faire partie des photos de Git. La liste des dossiers et fichiers ignorés
par Git sont mis dans un fichier appelé [.gitignore] [4] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
18/755
• après l’opération précédente, tous les fichiers du dossier [venv] disparaissent de la liste des fichiers non versionnés. Ne reste
plus que le fichier [.gitignore] qui vient d’être créé ;
• en [5], nous le sélectionnons pour qu’il soit sauvegardé ;
• en [6], on crée un message de commit :
• en [7], on valide. La photo du projet est alors prise ;
• en [8-9], le contenu du fichier [.gitignore] : une seule ligne avec le nom du dossier [/venv] qui indique que son contenu
doit être ignoré dans les photos ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
19/755
Montrons maintenant à quoi peut nous servir Git. Tout d’abord, nous créons un dossier [git] (vous pouvez utiliser tout autre nom
– il pourra être détruit à la fin de la démonstration) :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
20/755
Maintenant, committons notre projet. Une photo va être prise avec les deux fichiers [git_01, git_02] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
21/755
• en [1-2], confirmation du commit ;
• en [3], on sélectionne l’onglet [Log] ;
• en [4], une vue des branches du projet. Ici, une seule branche appelée [master] ;
• en [5-6], le commit qui vient d’être effectué ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
22/755
Maintenant nous avons deux commits dans les logs :
Lorsqu’on sélectionne un commit particulier, apparaît sur sa droite l’arbre des fichiers du projet :
Maintenant supposons que le dernier commit nous ait amenés à une impasse et que l’on voudrait revenir à une situation correspondant
à l’un des commit précédents :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
23/755
• en [1-2], on sélectionne le commit dans l’état duquel on veut revenir ;
• en [3-5], existent plusieurs modes de reset. Nous choisissons le mode [hard] qui revient à l’état sélectionné en perdant les
changements qui ont été faits depuis ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
24/755
• en [1-4], on revient à la photo du commit n° 1 ;
• en [5-6], on prend l’option [Keep] au lieu de [Hard]. Ces options ne sont pas simples à comprendre. Aussi faut-il les essayer :
Difficile de dire ce qu’a fait ce [Revert Commit]. Maintenant committons la situation actuelle :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
25/755
• en [1-6], le commit ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
26/755
• en [7-8], on a cette fois bien perdu toutes les modifications faites depuis le 1er commit ;
Dans la suite, nous ne reviendrons plus sur [Git]. Le lecteur pourra travailler de la façon suivante :
• il pourra suivre les exemples qui sont donnés soit en les tapant lui-même, soit en les récupérant sur le site du cours ;
• à chaque fois qu’un exemple fonctionne, il pourra committer son projet ;
• lorsqu’il développe ses propres codes et qu’il est dans une impasse, il sait qu’il peut revenir à une situation stable, en revenant
à un commit précédent ;
• le nom d’un module (module_name) suit une convention appelée parfois [snake_case] : tout en minuscules comportant
éventuellement des mots séparés par le caractère souligné. Cette convention [snake_case] s’applique aux noms de méthodes,
de packages, de variables, de fonctions ;
• le nom d’une classe (ClassName) suit une convention appelée parfois [PascalCase] : une suite de mots collés avec chaque
lettre de début de mot en majuscule ;
• les noms de constantes suivent la convention [SNAKE_CASE] : suite de mots en majuscules séparés par le caractère souligné ;
Ces conventions ont été globalement suivies dans ce document. Néanmoins pour les modules définissant une classe, j’ai donné au
module le même nom que la classe qu’il contient. Il suit donc la convention [PascalCase] au lieu de [snake_case]. Je voulais pouvoir
repérer rapidement les modules de classes.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
27/755
3 Les bases de Python
1. # ----------------------------------
2. def affiche(chaine):
3. # affiche chaine
4. print("chaine=%s" % chaine)
5.
6.
7. # ----------------------------------
8. def affiche_type(variable):
9. # affiche le type de variable
10. print("type[%s]=%s" % (variable, type(variable)))
11.
12.
13. # ----------------------------------
14. def f1(param):
15. # ajoute 10 à param
16. return param + 10
17.
18.
19. # ----------------------------------
20. def f2():
21. # rend un tuple de 3 valeurs
22. return "un", 0, 100
23.
24.
25. # -------------------------------- programme principal ------------------------------------
26. # ceci est un commentaire
27. # variable utilisée sans avoir été déclarée
28. nom = "dupont"
29.
30. # un affichage écran
31. print("nom=%s" % nom)
32.
33. # une liste avec des éléments de type différent
34. liste = ["un", "deux", 3, 4]
35.
36. # son nombre d'éléments
37. n = len(liste)
38.
39. # une boucle
40. for i in range(n):
41. print("liste[%d]=%s" % (i, liste[i]))
42.
43. # initialisation de 2 variables avec un tuple
44. (chaine1, chaine2) = ("chaine1", "chaine2")
45.
46. # concaténation des 2 chaînes
47. chaine3 = chaine1 + chaine2
48.
49. # affichage résultat
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
28/755
50. print("[%s,%s,%s]" % (chaine1, chaine2, chaine3))
51.
52. # utilisation fonction
53. affiche(chaine1)
54.
55. # le type d'une variable peut être connu
56. affiche_type(n)
57. affiche_type(chaine1)
58. affiche_type(liste)
59.
60. # le type d'une variable peut changer en cours d'exécution
61. n = "a changé"
62. affiche_type(n)
63.
64. # une fonction peut rendre un résultat
65. res1 = f1(4)
66. print("res1=%s" % res1)
67.
68. # une fonction peut rendre une liste de valeurs
69. (res1, res2, res3) = f2()
70. print("(res1,res2,res3)=[%s,%s,%s]" % (res1, res2, res3))
71.
72. # on aurait pu récupérer ces valeurs dans une variable
73. liste = f2()
74. for i in range(len(liste)):
75. print("liste[%s]=%s" % (i, liste[i]))
76.
77. # des tests
78. for i in range(len(liste)):
79. # n'affiche que les chaînes
80. if type(liste[i]) == "str":
81. print("liste[%s]=%s" % (i, liste[i]))
82.
83. # d'autres tests
84. for i in range(len(liste)):
85. # n'affiche que les entiers >10
86. if type(liste[i]) == "int" and liste[i] > 10:
87. print("liste[%s]=%s" % (i, liste[i]))
88.
89. # une boucle while
90. liste = (8, 5, 0, -2, 3, 4)
91. i = 0
92. somme = 0
93. while i < len(liste) and liste[i] > 0:
94. print("liste[%s]=%s" % (i, liste[i]))
95. somme += liste[i] # somme=somme+liste[i]
96. i += 1 # i=i+1
97. print("somme=%s" % somme)
98. # fin programme
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
29/755
• ligne 10 : Python gère en interne le type des variables. On peut connaître le type d'une variable avec la fonction type(variable)
qui rend une variable de type 'type'. L'expression '%s' % (type(variable)) est une chaîne de caractères représentant le type de la
variable ;
• ligne 25 : le programme principal. Celui-ci vient habituellement (mais pas nécessairement) après la définition de toutes les
fonctions du script. Son contenu est non indenté ;
• ligne 28 : en Python, on ne déclare pas les variables. Python est sensible à la casse. La variable Nom est différente de la variable
nom. Une chaîne de caractères peut être entourée de guillemets " ou d'apostrophes '. On peut donc écrire 'dupont' ou "dupont" ;
• ligne 34 : il y a une différence entre un tuple (1,2,3) (notez les parenthèses) et une liste [1,2,3] (notez les crochets). Le tuple
est non modifiable alors que la liste l'est. Dans les deux cas, l'élement n° i est noté [i] ;
• ligne 40 : range(n) est le tuple (0,1,2,…,n-1) ;
• ligne 41 : le format %d est utilisé pour les nombres entiers signés ;
• ligne 74 : len(var) est le nombre d'éléments de la collection var (tuple, liste, dictionnaire…) ;
• ligne 84 : la structure [for in …] permet d'itérer une structure itérable. Les listes et les tuples sont des éléments itérables ;
• ligne 86 : les autres opérateurs booléens sont or et not ;
• ligne 93 : fait la somme des nombres >0 de la liste ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/bases/bases_01.py
2. nom=dupont
3. liste[0]=un
4. liste[1]=deux
5. liste[2]=3
6. liste[3]=4
7. [chaine1,chaine2,chaine1chaine2]
8. chaine=chaine1
9. type[4]=<class 'int'>
10. type[chaine1]=<class 'str'>
11. type[['un', 'deux', 3, 4]]=<class 'list'>
12. type[a changé]=<class 'str'>
13. res1=14
14. (res1,res2,res3)=[un,0,100]
15. liste[0]=un
16. liste[1]=0
17. liste[2]=100
18. liste[0]=8
19. liste[1]=5
20. somme=13
21.
22. Process finished with exit code 0
1. # chaînes de formatage
2. # les formats sont ceux du langage C
3. # entier
4. int1 = 10
5. print(f"[int1={int1}]")
6. print(f"[int1={int1:4d}]")
7. print(f"[int1={int1:04d}]")
8. # float
9. float1=8.2
10. print(f"[float1={float1}]")
11. print(f"[float1={float1:8.2f}]")
12. print(f"[float1={float1:.3e}]")
13. # string
14. str1="abcd"
15. print(f"[str1={str1}]")
16. print(f"[str1={str1:8s}]")
17. str2="jean de florette"
18. print(f"[{str2:20.10s}]")
19. # les chaînes formatées peuvent être affectées à des variables
20. str3=f"[{str2:20.10s}]"
21. print(str3)
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
30/755
avec :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/bases/bases_02.py
2. [int1=10]
3. [int1= 10]
4. [int1=0010]
5. [float1=8.2]
6. [float1= 8.20]
7. [float1=8.200e+00]
8. [str1=abcd]
9. [str1=abcd ]
10. [jean de fl ]
11. [jean de fl ]
12.
13. Process finished with exit code 0
1. # changements de type
2. # int --> str, float, bool
3. x = 4
4. print(x, type(x))
5. x = str(4)
6. print(x, type(x))
7. x = float(4)
8. print(x, type(x))
9. x = bool(4)
10. print(x, type(x))
11.
12. # bool --> int, float, str
13. x = True
14. print(x, type(x))
15. x = int(True)
16. print(x, type(x))
17. x = float(True)
18. print(x, type(x))
19. x = str(True)
20. print(x, type(x))
21.
22. # str --> int, float, bool
23. x = "4"
24. print(x, type(x))
25. x = int("4")
26. print(x, type(x))
27. x = float("4")
28. print(x, type(x))
29. x = bool("4")
30. print(x, type(x))
31.
32. # float --> str, int, bool
33. x = 4.32
34. print(x, type(x))
35. x = str(4.32)
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
31/755
36. print(x, type(x))
37. x = int(4.32)
38. print(x, type(x))
39. x = bool(4.32)
40. print(x, type(x))
41.
42. # gestion des erreurs de changement de type
43. try:
44. x = int("abc")
45. print(x, type(x))
46. except ValueError as erreur:
47. print(erreur)
48.
49. # cas divers
50. x = bool("abc")
51. print(x, type(x))
52. x = bool("")
53. print(x, type(x))
54. x = bool(0)
55. print(x, type(x))
56. x = None
57. print(x, type(x))
58. x = bool(None)
59. print(x, type(x))
60. x = bool(0.0)
61. print(x, type(x))
62.
63. # toutes les données sont des instances de classe et à ce titre ont des méthodes
64. # chaîne de caractères
65. str1 = "abc"
66. print(str1.capitalize())
67. # nombre entier
68. int1 = 4
69. print(int1.bit_length())
70. # booléen
71. bool1=True
72. print(bool1.conjugate())
73. # nombre réel
74. float1=8.2
75. print (float1.is_integer())
De nombreux changements de type sont possibles. Certains peuvent échouer, comme celui des lignes 46-47 qui essaient de
transformer la chaîne 'abc' en nombre entier. On a géré l'erreur avec une structure try / except. Une forme générale de cette structure
est la suivante :
1. try:
2. actions
3. except Exception as ex:
4. actions
5. finally:
6. actions
Si l'une des actions du try lance une exception (signale une erreur), il y a branchement immédiat sur la clause except. Si les actions
du try ne lancent pas d'exception, la clause except est ignorée. Les attributs Exception et ex de l'instruction except sont facultatifs.
Lorsqu'ils sont présents, Exception précise le type d'exception interceptée par l'instruction except et ex contient l’exception qui s’est
produite. Il peut y avoir plusieurs instructions except, si on veut gérer différents types d'exceptions dans le même try.
L'instruction finally est facultative. Si elle est présente, les actions du finally sont toujours exécutées qu'il y ait eu exception ou non.
Les lignes 49-61 montrent diverses tentatives pour transformer une donnée de type str, int, float, NoneType en booléen. C'est toujours
possible. Les règles sont les suivantes :
• bool(int i) vaut False si i vaut 0, True dans tous les autres cas ;
• bool(float f) vaut False si f vaut 0.0, True dans tous les autres cas ;
• bool(str chaine) vaut False si chaine a 0 caractère, True dans tous les autres cas ;
• bool(None) vaut False. None est une valeur spéciale qui signifie que la variable existe mais n'a pas de valeur.
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/bases/bases_03.py
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
32/755
2. 4 <class 'int'>
3. 4 <class 'str'>
4. 4.0 <class 'float'>
5. True <class 'bool'>
6. True <class 'bool'>
7. 1 <class 'int'>
8. 1.0 <class 'float'>
9. True <class 'str'>
10. 4 <class 'str'>
11. 4 <class 'int'>
12. 4.0 <class 'float'>
13. True <class 'bool'>
14. 4.32 <class 'float'>
15. 4.32 <class 'str'>
16. 4 <class 'int'>
17. True <class 'bool'>
18. invalid literal for int() with base 10: 'abc'
19. True <class 'bool'>
20. False <class 'bool'>
21. False <class 'bool'>
22. None <class 'NoneType'>
23. False <class 'bool'>
24. False <class 'bool'>
25. Abc
26. 3
27. 1
28. False
29.
30. Process finished with exit code 0
On notera que toutes les données sont des objets, c'est-à-dire des instances de classe. Cela signifie qu'elles peuvent avoir des méthodes.
C'est ce que montrent les lignes 63-75 du code. Nous ne cherchons pas ici à expliquer ce que font les méthodes utilisées mais
simplement à montrer qu'elles existent.
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/bases/bases_04.py
2. i=5, j=7
3.
4. Process finished with exit code 0
Commentaires
• ligne 4 : la variable [i] du bloc [if] est la même que la variable i utilisée ligne 2 ;
• ligne 6 : la variable [j] est celle initialisée dans le bloc [if] ;
Dans certains langages, où on déclare les variables, une variable définie dans un bloc (comme celui des lignes 3-5) n'est pas connue à
l'extérieur de celui-ci. En Python, rien de tel.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
33/755
6. print(f"list1 a {len(list1)} éléments")
7. for i in range(len(list1)):
8. print(f"list1[{i}]={list1[i]}")
9.
10. list1[1] = 10
11. # parcours - 2
12. print(f"list1 a {len(list1)} éléments")
13. for element in list1:
14. print(element)
15.
16. # ajout de deux éléments
17. list1[len(list1):] = [10, 11]
18. # le format %s permet d'afficher la liste sur une ligne
19. print("%s" % list1)
20.
21. # suppression des deux derniers éléments
22. list1[len(list1) - 2:] = []
23. # le format par défaut permet d'afficher la liste sur une ligne
24. print(f"{list1}")
25.
26. # ajout en début de liste d'une liste
27. list1[:0] = [-10, -11, -12]
28. print(f"{list1}")
29.
30. # insertion en milieu de liste de deux éléments
31. list1[3:3] = [100, 101]
32. print(f"{list1}")
33.
34. # suppression de deux éléments en milieu de liste
35. list1[3:4] = []
36. print(f"{list1}")
Notes :
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/bases/bases_05.py
2. list1 a 6 éléments
3. list1[0]=0
4. list1[1]=1
5. list1[2]=2
6. list1[3]=3
7. list1[4]=4
8. list1[5]=5
9. list1 a 6 éléments
10. 0
11. 10
12. 2
13. 3
14. 4
15. 5
16. [0, 10, 2, 3, 4, 5, 10, 11]
17. [0, 10, 2, 3, 4, 5]
18. [-10, -11, -12, 0, 10, 2, 3, 4, 5]
19. [-10, -11, -12, 100, 101, 0, 10, 2, 3, 4, 5]
20. [-10, -11, -12, 101, 0, 10, 2, 3, 4, 5]
21.
22. Process finished with exit code 0
1. # listes à 1 dimension
2.
3. # initialisation
4. list1 = [0, 1, 2, 3, 4, 5]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
34/755
5.
6. # parcours - 1
7. print(f"list1 a {len(list1)} éléments")
8. for i in range(len(list1)):
9. print(f"list1[{i}]={list1[i]}")
10.
11. # modification d'un élément
12. list1[1] = 10
13.
14. # parcours - 2
15. print(f"list1 a {len(list1)} éléments")
16. for element in list1:
17. print(element)
18.
19. # ajout de deux éléments
20. list1.extend([10, 11])
21. print(f"{list1}")
22.
23. # suppression des deux derniers éléments
24. del list1[len(list1) - 2:]
25. print(f"{list1}")
26.
27. # ajout en début de liste d'un tuple
28. for i in (-12, -11, -10):
29. list1.insert(0, i)
30. print(f"{list1}")
31.
32. # insertion en milieu de liste
33. for i in (101, 100):
34. list1.insert(3, i)
35. print(f"{list1}")
36.
37. # suppression en milieu de liste
38. del list1[3:4]
39. print(f"{list1}")
1. # une fonction qui vérifie si la clé mari existe dans le dictionnaire conjoints
2. def existe(conjoints, mari):
3. if mari in conjoints:
4. print(f"La clé [{mari}] existe associée à la valeur [{conjoints[mari]}]")
5. else:
6. print(f"La clé [{mari}] n'existe pas")
7.
8.
9. # ----------------------------- Main
10. # un dictionnaire
11. conjoints = {"Pierre": "Gisèle", "Paul": "Virginie", "Jacques": "Lucette", "Jean": ""}
12.
13. # parcours - 1
14. print(f"Nombre d'éléments du dictionnaire : {len(conjoints)}")
15. for (clé, valeur) in conjoints.items():
16. print(f"conjoints[{clé}]={valeur}")
17.
18. # liste des clés du dictionnaire
19. print("liste des clés-------------")
20. clés = conjoints.keys()
21. print(f"{clés}")
22.
23. # liste des valeurs du dictionnaire
24. print("liste des valeurs------------")
25. valeurs = conjoints.values()
26. print(f"{valeurs}")
27.
28. # recherche d'une clé
29. existe(conjoints, "Jacques")
30. existe(conjoints, "Lucette")
31. existe(conjoints, "Jean")
32.
33. # suppression d'une clé-valeur
34. del (conjoints["Jean"])
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
35/755
35. print(f"Nombre d'éléments du dictionnaire : {len(conjoints)}")
36. print(f"{conjoints}")
37.
38. # les clés et valeurs d'un dictionnaire ne sont pas des listes
39. print(f"type des clés : {type(clés)}")
40. print(f"type des valeurs : {type(valeurs)}")
41.
42. # on peut les transformer en listes
43. lclés = list(clés)
44. print(f"clés : {type(lclés)}, {lclés}")
45. lvaleurs = list(valeurs)
46. print(f"valeurs : {type(lvaleurs)}, {lvaleurs}")
Notes :
• ligne 11 : la définition en dur d'un dictionnaire ;
• ligne 15 : conjoints.items() rend la liste des couples (clé,valeur) du dictionnaire conjoints ;
• ligne 20 : conjoints.keys() rend les clés du dictionnaire conjoints ;
• ligne 25 : conjoints.values() rend les valeurs du dictionnaire conjoints ;
• ligne 3 : mari in conjoints rend True si la clé mari existe dans le dictionnaire conjoints, False sinon ;
• ligne 36 : un dictionnaire peut être affiché en une seule ligne.
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/bases/bases_07.py
2. Nombre d'éléments du dictionnaire : 4
3. conjoints[Pierre]=Gisèle
4. conjoints[Paul]=Virginie
5. conjoints[Jacques]=Lucette
6. conjoints[Jean]=
7. liste des clés-------------
8. dict_keys(['Pierre', 'Paul', 'Jacques', 'Jean'])
9. liste des valeurs------------
10. dict_values(['Gisèle', 'Virginie', 'Lucette', ''])
11. La clé [Jacques] existe associée à la valeur [Lucette]
12. La clé [Lucette] n'existe pas
13. La clé [Jean] existe associée à la valeur []
14. Nombre d'éléments du dictionnaire : 3
15. {'Pierre': 'Gisèle', 'Paul': 'Virginie', 'Jacques': 'Lucette'}
16. type des clés : <class 'dict_keys'>
17. type des valeurs : <class 'dict_values'>
18. clés : <class 'list'>, ['Pierre', 'Paul', 'Jacques']
19. valeurs : <class 'list'>, ['Gisèle', 'Virginie', 'Lucette']
20.
21. Process finished with exit code 0
Notes :
• on notera aux lignes 16-17 des résultats que les clés et valeurs d'un dictionnaire ne forment pas une liste mais un type
'dict_keys' ;
• lignes 18-19 : un simple changement de type permet de les convertir en un type [list] ;
1. # tuples
2. # initialisation
3. tuple1 = (0, 1, 2, 3, 4, 5)
4.
5. # parcours - 1
6. print(f"tuple1 a {len(tuple1)} elements")
7. for i in range(len(tuple1)):
8. print(f"tuple1[{i}]={tuple1[i]}")
9.
10. # parcours - 2
11. print(f"tuple1 a {len(tuple1)} elements")
12. for element in tuple1:
13. print(element)
14.
15. # un tuble peut être affiché en une ligne
16. print(f"tuple1={tuple1}")
17.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
36/755
18. # un tuple ne peut être modifié
19. tuple1[0] = 10
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/bases/bases_08.py
2. tuple1 a 6 elements
3. tuple1[0]=0
4. tuple1[1]=1
5. tuple1[2]=2
6. tuple1[3]=3
7. tuple1[4]=4
8. tuple1[5]=5
9. tuple1 a 6 elements
10. 0
11. 1
12. 2
13. 3
14. 4
15. 5
16. tuple1=(0, 1, 2, 3, 4, 5)
17. Traceback (most recent call last):
18. File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/bases/bases_08.py", line 19, in <module>
19. tuple1[0] = 10
20. TypeError: 'tuple' object does not support item assignment
21.
22. Process finished with exit code 1
Notes :
• lignes 17-20 des résultats : montrent qu'un tuple ne peut pas être modifié.
1. # listes multidimensionnelles
2. # initialisation
3. multi = [[0, 1, 2], [10, 11, 12, 13], [20, 21, 22, 23, 24]]
4.
5. # parcours
6. for i1 in range(len(multi)):
7. for i2 in range(len(multi[i1])):
8. print(f"multi[{i1}][{i2}]={multi[i1][i2]}")
9.
10. # affichage en une ligne
11. print(f"multi={multi}")
12.
13. # dictionnaires multidimensionnels
14. # initialisation
15. multi = {"zéro": [0, 1], "un": [10, 11, 12, 13], "deux": [20, 21, 22, 23, 24]}
16.
17. # parcours
18. for (clé, valeur) in multi.items():
19. for i2 in range(len(valeur)):
20. print(f"multi[{clé}][{i2}]={multi[clé][i2]}")
21.
22. # affichage en une ligne
23. print(f"multi={multi}")
Commentaires
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/bases/bases_09.py
2. multi[0][0]=0
3. multi[0][1]=1
4. multi[0][2]=2
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
37/755
5. multi[1][0]=10
6. multi[1][1]=11
7. multi[1][2]=12
8. multi[1][3]=13
9. multi[2][0]=20
10. multi[2][1]=21
11. multi[2][2]=22
12. multi[2][3]=23
13. multi[2][4]=24
14. multi=[[0, 1, 2], [10, 11, 12, 13], [20, 21, 22, 23, 24]]
15. multi[zéro][0]=0
16. multi[zéro][1]=1
17. multi[un][0]=10
18. multi[un][1]=11
19. multi[un][2]=12
20. multi[un][3]=13
21. multi[deux][0]=20
22. multi[deux][1]=21
23. multi[deux][2]=22
24. multi[deux][3]=23
25. multi[deux][4]=24
26. multi={'zéro': [0, 1], 'un': [10, 11, 12, 13], 'deux': [20, 21, 22, 23, 24]}
27.
28. Process finished with exit code 0
Notes :
• ligne 3 : la méthode chaine.split(séparateur) découpe la chaîne de caractères chaine en éléments séparés par séparateur et les rend
sous forme de liste. Ainsi l'expression '1:2:3:4'.split(":") a pour valeur la liste ('1','2','3','4') ;
• ligne 11 : 'separateur'.join(liste) a pour valeur la chaîne de caractères 'liste [0]+separateur+liste[1]+separateur+…'.
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/bases/bases_10.py
2. <class 'list'>
3. liste a 4 éléments
4. liste=['1', '2', '3', '4']
5. chaine2=1:2:3:4
6. chaine=1:2:3:4:
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
38/755
7. liste a 5 éléments
8. liste=['1', '2', '3', '4', '']
9. chaine=1:2:3:4::
10. liste a 6 éléments
11. liste=['1', '2', '3', '4', '', '']
12.
13. Process finished with exit code 0
Notes :
• noter le module [re] importé en ligne 2. C'est lui qui contient les fonctions de gestion des expressions régulières ;
• ligne 10 : la comparaison d'une chaîne à une expression régulière (modèle) rend le booléen True si la chaîne correspond au
modèle, False sinon ;
• ligne 12 : match.groups() est un tuple dont les éléments sont les parties de la chaîne qui correspondent aux éléments de
l'expression régulière entourés de parenthèses. Dans le modèle :
• ^.*?(\d+).*?, match.groups() sera un tuple d'un élément parce qu'il y a une parenthèse ;
• ^(.*?)(\d+)(.*?)$, match.groups() sera un tuple de 3 éléments parce qu'il y a trois parenthèses ;
• ligne 21 : une expression régulière littérale est notée r"xxx". C'est le symbole r qui fait de la chaîne une expression régulière ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
39/755
Les expressions régulières nous permettent de tester le format d'une chaîne de caractères. Ainsi on peut vérifier qu'une chaîne
représentant une date est au format jj/mm/aa. On utilise pour cela un modèle et on compare la chaîne à ce modèle. Ainsi dans cet
exemple, j m et a doivent être des chiffres. Le modèle d'un format de date valide est alors "\d\d/\d\d/\d\d" où le symbole \d
désigne un chiffre. Les symboles utilisables dans un modèle sont les suivants :
Caractère Description
\ Marque le caractère suivant comme caractère spécial ou littéral. Par exemple, "n" correspond au
caractère "n" alors que "\n" correspond à un caractère de nouvelle ligne. La séquence "\\"
correspond à "\", tandis que "\(" correspond à "(".
^ Correspond au début de la chaîne.
$ Correspond à la fin de la chaîne.
* Correspond au caractère précédent, zéro fois ou plusieurs fois. Ainsi, "zo*" correspond à "z" ou
à "zoo".
+ Correspond au caractère précédent, une ou plusieurs fois. Ainsi, "zo+" correspond à "zoo", mais
pas à "z".
? Correspond au caractère précédent, zéro ou une fois. Par exemple, "a?ve?" correspond à "ve" dans
"lever".
. Correspond à tout caractère unique, sauf le caractère de nouvelle ligne.
(modèle) Recherche le modèle et mémorise la correspondance. La sous-chaîne correspondante peut être
extraite de la collection match.groups(). Pour trouver des correspondances avec des caractères
entre parenthèses ( ), utilisez "\(" ou "\)".
x|y Correspond soit à x soit à y. Par exemple, "z|foot" correspond à "z" ou à "foot". "(z|f)oo"
correspond à "zoo" ou à "foo".
{n} n est un nombre entier non négatif. Correspond exactement à n fois le caractère. Par exemple,
"o{2}" ne correspond pas à "o" dans "Bob," mais aux deux premiers "o" dans "fooooot".
{n,} n est un entier non négatif. Correspond à au moins n fois le caractère. Par exemple, "o{2,}" ne
correspond pas à "o" dans "Bob", mais à tous les "o" dans "fooooot". "o{1,}" équivaut à "o+" et
"o{0,}" équivaut à "o*".
{n,m} m et n sont des entiers non négatifs. Correspond à au moins n et à au plus m fois le caractère. Par
exemple, "o{1,3}" correspond aux trois premiers "o" dans "foooooot" et "o{0,1}" équivaut à
"o?".
[xyz] Jeu de caractères. Correspond à l'un des caractères indiqués. Par exemple, "[abc]" correspond à
"a" dans "plat".
[^xyz] Jeu de caractères négatif. Correspond à tout caractère non indiqué. Par exemple, " [^abc]"
correspond à "p" dans "plat".
[a-z] Plage de caractères. Correspond à tout caractère dans la série spécifiée. Par exemple, "[a-z]"
correspond à tout caractère alphabétique minuscule compris entre "a" et "z".
[^m-z] Plage de caractères négative. Correspond à tout caractère ne se trouvant pas dans la série spécifiée.
Par exemple, "[^m-z]" correspond à tout caractère ne se trouvant pas entre "m" et "z".
\b Correspond à une limite représentant un mot, autrement dit, à la position entre un mot et un
espace. Par exemple, "er\b" correspond à "er" dans "lever", mais pas à "er" dans "verbe".
\B Correspond à une limite ne représentant pas un mot. "en*t\B" correspond à "ent" dans "bien
entendu".
\d Correspond à un caractère représentant un chiffre. Équivaut à [0-9].
\D Correspond à un caractère ne représentant pas un chiffre. Équivaut à [^0-9].
\f Correspond à un caractère de saut de page.
\n Correspond à un caractère de nouvelle ligne.
\r Correspond à un caractère de retour chariot.
\s Correspond à tout espace blanc, y compris l'espace, la tabulation, le saut de page, etc. Équivaut à
"[ \f\n\r\t\v]".
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
40/755
\S Correspond à tout caractère d'espace non blanc. Équivaut à " [^ \f\n\r\t\v]".
\t Correspond à un caractère de tabulation.
\v Correspond à un caractère de tabulation verticale.
\w Correspond à tout caractère représentant un mot et incluant un trait de soulignement. Équivaut à
"[A-Za-z0-9_]".
\W Correspond à tout caractère ne représentant pas un mot. Équivaut à " [^A-Za-z0-9_]".
\num Correspond à num, où num est un entier positif. Fait référence aux correspondances mémorisées.
Par exemple, "(.)\1" correspond à deux caractères identiques consécutifs.
\n Correspond à n, où n est une valeur d'échappement octale. Les valeurs d'échappement octales
doivent comprendre 1, 2 ou 3 chiffres. Par exemple, "\11" et "\011" correspondent tous les deux
à un caractère de tabulation. "\0011" équivaut à "\001" & "1". Les valeurs d'échappement octales
ne doivent pas excéder 256. Si c'était le cas, seuls les deux premiers chiffres seraient pris en compte
dans l'expression. Permet d'utiliser les codes ASCII dans des expressions régulières.
\xn Correspond à n, où n est une valeur d'échappement hexadécimale. Les valeurs d'échappement
hexadécimales doivent comprendre deux chiffres obligatoirement. Par exemple, "\x41"
correspond à "A". "\x041" équivaut à "\x04" & "1". Permet d'utiliser les codes ASCII dans des
expressions régulières.
Un élément dans un modèle peut être présent en 1 ou plusieurs exemplaires. Considérons quelques exemples autour du symbole \d
qui représente 1 chiffre :
modèle signification
\d un chiffre
\d? 0 ou 1 chiffre
\d* 0 ou davantage de chiffres
\d+ 1 ou davantage de chiffres
\d{2} 2 chiffres
\d{3,} au moins 3 chiffres
\d{5,7} entre 5 et 7 chiffres
Imaginons maintenant le modèle capable de décrire le format attendu pour une chaîne de caractères :
modèle signification
^modèle le modèle commence la chaîne
modèle$ le modèle finit la chaîne
^modèle$ le modèle commence et finit la chaîne
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
41/755
modèle le modèle est cherché partout dans la chaîne en commençant par le début de celle-ci.
Les sous-ensembles d'un modèle peuvent être "récupérés". Ainsi non seulement, on peut vérifier qu'une chaîne correspond à un
modèle particulier mais on peut récupérer dans cette chaîne les éléments correspondant aux sous-ensembles du modèle qui ont été
entourés de parenthèses. Ainsi si on analyse une chaîne contenant une date jj/mm/aa et si on veut de plus récupérer les éléments jj,
mm, aa de cette date on utilisera le modèle (\d\d)/(\d\d)/(\d\d).
Résultats du script
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/bases/bases_11.py
2.
3. Résultats(xyz1234abcd,^.*?(\d+).*?$)
4. ('1234',)
5.
6. Résultats(12 34,^.*?(\d+).*?$)
7. ('12',)
8.
9. Résultats(abcd,^.*?(\d+).*?$)
10. La chaîne [abcd] ne correspond pas au modèle [^.*?(\d+).*?$]
11.
12. Résultats(xyz1234abcd,^(.*?)(\d+)(.*?)$)
13. ('xyz', '1234', 'abcd')
14.
15. Résultats(12 34,^(.*?)(\d+)(.*?)$)
16. ('', '12', ' 34')
17.
18. Résultats(abcd,^(.*?)(\d+)(.*?)$)
19. La chaîne [abcd] ne correspond pas au modèle [^(.*?)(\d+)(.*?)$]
20.
21. Résultats(10/05/97,^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$)
22. ('10', '05', '97')
23.
24. Résultats( 04/04/01 ,^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$)
25. ('04', '04', '01')
26.
27. Résultats(5/1/01,^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$)
28. La chaîne [5/1/01] ne correspond pas au modèle [^\s*(\d\d)\/(\d\d)\/(\d\d)\s*$]
29.
30. Résultats(187.8,^\s*([+-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$)
31. ('', '187.8')
32.
33. Résultats(-0.6,^\s*([+-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$)
34. ('-', '0.6')
35.
36. Résultats(4,^\s*([+-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$)
37. ('', '4')
38.
39. Résultats(.6,^\s*([+-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$)
40. ('', '.6')
41.
42. Résultats(4.,^\s*([+-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$)
43. ('', '4.')
44.
45. Résultats( + 4,^\s*([+-]?)\s*(\d+\.\d*|\.\d+|\d+)\s*$)
46. ('+', '4')
47.
48. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
42/755
4 Les chaînes de caractères
1. # chaînes de caractères
2. # trois notations possibles
3. chaine1 = "un"
4. chaine2 = 'deux'
5. chaine3 = """hélène va au
6. marché acheter des légumes"""
7. # affichage
8. print(f"chaine1=[{chaine1}], chaine2=[{chaine2}], chaine3=[{chaine3}]")
Commentaires
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/strings/str_01.py
2. chaine1=[un], chaine2=[deux], chaine3=[hélène va au
3. marché acheter des légumes]
4.
5. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
43/755
26. print(f"'abcd'.replace('a','x')={'abcd'.replace('a', 'x')}")
27. print(f"'abcd'.replace('ab','xy')={'abcd'.replace('ab', 'xy')}")
28. # recherche d'une sous-chaîne : rend la position ou -1 si sous-chaîne pas trouvée
29. print(f"'abcd'.find('bc')={'abcd'.find('bc')}")
30. print(f"'abcd'.find('bc')={'abcd'.find('Bc')}")
31. # début d'une chaîne
32. print(f"'abcd'.startswith('ab')={'abcd'.startswith('ab')}")
33. print(f"'abcd'.startswith('x')={'abcd'.startswith('x')}")
34. # fin d'une chaîne
35. print(f"'abcd'.endswith('cd')={'abcd'.endswith('cd')}")
36. print(f"'abcd'.endswith('x')={'abcd'.endswith('x')}")
37. # passage d'une liste de chaînes à une chaîne
38. print(f"'[X]'.join(['abcd', '123', 'èéà'])={'[X]'.join(['abcd', '123', 'èéà'])}")
39. print(f"''.join(['abcd', '123', 'èéà'])={''.join(['abcd', '123', 'èéà'])}")
40. # passage d'une chaîne à une liste de chaînes
41. print(f"'abcd 123 cdXY'.split('cd')={'abcd 123 cdXY'.split('cd')}")
42. # récupérer les mots d'une chaîne
43. print(f"'abcd 123 cdXY'.split(None)={'abcd 123 cdXY'.split(None)}")
Les commentaires alliés aux résultats obtenus suffisent pour la compréhension du script. Les résultats sont les suivants :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/strings/str_02.py
2. 'ABCD'.lower()=abcd
3. 'abcd'.upper()=ABCD
4. 'cheval[2]=e
5. 'caractères accentués'[5:7]=tè
6. 'caractères accentués'[4:]=ctères accentués
7. 'caractères accentués'[:5]=carac
8. len('123')=3
9. ' abcd '.strip()=[abcd]
10. ' abcd '.rstrip()=[ abcd]
11. ' abcd '.lstrip()=[abcd ]
12. str.strip()=[abcd]
13. 'abcd'.replace('a','x')=xbcd
14. 'abcd'.replace('ab','xy')=xycd
15. 'abcd'.find('bc')=1
16. 'abcd'.find('bc')=-1
17. 'abcd'.startswith('ab')=True
18. 'abcd'.startswith('x')=False
19. 'abcd'.endswith('cd')=True
20. 'abcd'.endswith('x')=False
21. '[X]'.join(['abcd', '123', 'èéà'])=abcd[X]123[X]èéà
22. ''.join(['abcd', '123', 'èéà'])=abcd123èéà
23. 'abcd 123 cdXY'.split('cd')=['ab', ' 123 ', 'XY']
24. 'abcd 123 cdXY'.split(None)=['abcd', '123', 'cdXY']
25.
26. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
44/755
23. print(f"bytes2={bytes2}, type={type(bytes2)}")
L'encodage d'une chaîne de caractère de type <str> produit une chaîne binaire où chaque caractère de la chaîne a été représenté par
un ou plusieurs octets. Il existe différents types d'encodage. Le script ci-dessus présente les deux plus courants en occident "utf-8" et
"iso-8859-1" dit aussi "latin1.
Le principe de l'encodage / décodage est illustré ci-dessous (ref. |https://realpython.com/python-encodings-guide/ |) :
Commentaires
• lignes 4-5 : la chaîne de caractères initiale qui va être encodée. Les instances du type <str> sont des chaînes unicode
|https://docs.python.org/3/howto/unicode.html|, |https://realpython.com/python-encodings-guide/ | ;
• lignes 6-11 : deux façons pour encoder en UTF68 une chaîne de caractères :
• ligne 8 : str.encode('utf-8) ;
• ligne 10 : bytes(str, 'utf-8');
• lignes 12-17 : on refait la même chose avec l'encodage 'iso-8859-1' ;
• lignes 18-23 : 'latin1' est un autre nom de l'encodage 'iso-8859-1' ;
Les résultats sont les suivants :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-
2020/python3-flask-2020/strings/str_03.py
2. str=[hélène va au marché acheter des légumes, type=<class 'str'>
3. --- utf-8
4. bytes1=b'h\xc3\xa9l\xc3\xa8ne va au march\xc3\xa9 acheter des l\xc3\xa9gumes', type=<class 'bytes'>
5. bytes2=b'h\xc3\xa9l\xc3\xa8ne va au march\xc3\xa9 acheter des l\xc3\xa9gumes', type=<class 'bytes'>
6. --- iso-8859-1
7. bytes1=b'h\xe9l\xe8ne va au march\xe9 acheter des l\xe9gumes', type=<class 'bytes'>
8. bytes2=b'h\xe9l\xe8ne va au march\xe9 acheter des l\xe9gumes', type=<class 'bytes'>
9. --- latin1
10. bytes1=b'h\xe9l\xe8ne va au march\xe9 acheter des l\xe9gumes', type=<class 'bytes'>
11. bytes2=b'h\xe9l\xe8ne va au march\xe9 acheter des l\xe9gumes', type=<class 'bytes'>
12.
13. Process finished with exit code 0
Commentaires
• ligne 4 : on voit que les caractères accentués ont été codés sur deux octets :
• é : [\xc3\xa9] qui est la suite binaire 11000011 10101001 ;
• è : [\xc3\xa8] qui est la suite binaire 11000011 10101000 ;
• lignes 7 : avec le codage iso-8859-1, ces deux caractères accentués sont codés différemment :
• é : [\xe9] qui est la suite binaire 11101001 ;
• è : [\xe8] qui est la suite binaire 11101000 ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
45/755
4.4 Script [str_04] : encodage des chaînes de caractères (2)
Le script [str_04] présente deux autres types d'encodage : 'base64' et 'quoted-printable'. Ces deux encodages n'encodent pas des
chaînes de caractères Unicode mais des objets binaires. Par exemple, lorsqu'on attache un document Word à un mail, celui-ci va subir
l'un des deux encodages selon le gestionnaire de courrier utilisé. Ce sera le cas pour la plupart des fichiers attachés.
1. # encodage / décodage
2. import codecs
3.
4. # chaîne
5. print("---- chaîne unicode")
6. str1 = "hélène va au marché acheter des légumes"
7. print(f"str1=[{str1}], type(str1)={type(str1)}")
8.
9. # encodage utf-8
10. print("---- chaîne unicode -> binaire utf-8")
11. bytes1 = bytes(str1, "utf-8")
12. print(f"bytes1=[{bytes1}], type(bytes1)={type(bytes1)}")
13.
14. # décodage utf-8
15. print("---- binaire utf-8 -> chaîne unicode")
16. str2 = bytes1.decode("utf-8")
17. print(f"str2=[{str2}], type(str2)={type(str2)}")
18. print(f"str2==str1={str2 == str1}")
19.
20. # encodage iso-8859-1
21. print("---- chaîne unicode -> binaire iso-8859-1")
22. bytes2 = bytes(str1, "iso-8859-1")
23. print(f"bytes2=[{bytes2}], type(bytes2)={type(bytes2)}")
24.
25. # décodage iso-8859-1
26. print("---- binaire iso-8859-1 -> chaîne unicode")
27. str3 = bytes2.decode("iso-8859-1")
28. print(f"str3=[{str3}], type(str3)={type(str3)}")
29. print(f"str3==str1={str3 == str1}")
30.
31. # erreur de décodage - bytes1 est en utf-8 - on le décode en iso-8859-1
32. print("--- binaire utf-8 (décodage iso-8859-1) --> chaîne unicode")
33. str4 = bytes1.decode("iso-8859-1")
34. print(f"str4=[{str4}], type(str4)={type(str4)}")
35.
36. # encodage utf-8 d’une chaîne Unicode
37. print("---- chaîne unicode -> binaire utf-8")
38. bytes3 = codecs.encode(str1, "utf-8")
39. print(f"bytes3=[{bytes3}], type(bytes3)={type(bytes3)}")
40.
41. # encodage d'une chaîne binaire UTF-8 en base64
42. print("---- binaire utf-8 -> binaire base64")
43. bytes4 = codecs.encode(bytes1, "base64")
44. print(f"bytes4=[{bytes4}], type(bytes4)={type(bytes4)}")
45.
46. # retour à la chaîne unicode d'origine
47. print("---- binaire base64 -> binaire utf-8 -> chaîne unicode")
48. str6 = codecs.decode(bytes4, "base64").decode("utf-8")
49. print(f"str6=[{str6}], type(str6)={type(str6)}")
50.
51. # encodage d'une chaîne binaire en quoted-printable
52. print("---- binaire utf-8 -> binaire quoted-printable")
53. str7 = codecs.encode(bytes1, "quoted-printable")
54. print(f"str7=[{str7}], type(str7)={type(str7)}")
55.
56. # retour à la chaîne unicode d'origine
57. print("---- binaire quoted-printable -> binaire utf-8 -> chaîne unicode")
58. str8 = codecs.decode(str7, "quoted-printable").decode("utf-8")
59. print(f"str8=[{str8}], type(str8)={type(str8)}")
Commentaires
• ligne 2 : le module [codecs] permet de faire les codages 'base64' et 'quoted-printable'. Il peut en faire beuacoup d'autres ;
• lignes 4-7 : la chaîne Unicode qui va subir divers encodages ;
• lignes 9-12 : encodage utf-8. on obtient un binaire ;
• lignes 14-18 : décodage utf-8 pour revenir à la chaîne Unicode d'origine ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
46/755
• lignes 20-29 : on répète le même processus avec l'encodage 'iso-8859-1' ;
• lignes 31-34 : on montre une erreur de décodage :
• ligne 33 : bytes1 est une chaîne binaire encodée en 'utf-8'. On la décode en 'iso-8859-1' ;
• lignes 36-39 : autre façon d'encoder une chaîne de caractères en utf-8 avec le module [codecs] ;
• lignes 41-44 : une chaîne binaire 'utf-8' est encodée en 'base64' ;
• lignes 46-49 : montrent comment passer de la chaîne binaire 'base64' à la chaîne unicode d'origine ;
• lignes 51-59 : on répète ce processus avec un encodage 'quoted-printable' au lieu de 'base64' ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/strings/str_04.py
2. ---- chaîne unicode
3. str1=[hélène va au marché acheter des légumes], type(str1)=<class 'str'>
4. ---- chaîne unicode -> binaire utf-8
5. bytes1=[b'h\xc3\xa9l\xc3\xa8ne va au march\xc3\xa9 acheter des l\xc3\xa9gumes'], type(bytes1)=<class
'bytes'>
6. ---- binaire utf-8 -> chaîne unicode
7. str2=[hélène va au marché acheter des légumes], type(str2)=<class 'str'>
8. str2==str1=True
9. ---- chaîne unicode -> binaire iso-8859-1
10. bytes2=[b'h\xe9l\xe8ne va au march\xe9 acheter des l\xe9gumes'], type(bytes2)=<class 'bytes'>
11. ---- binaire iso-8859-1 -> chaîne unicode
12. str3=[hélène va au marché acheter des légumes], type(str3)=<class 'str'>
13. str3==str1=True
14. --- binaire utf-8 (décodage iso-8859-1) --> chaîne unicode
15. str4=[hélène va au marché acheter des légumes], type(str4)=<class 'str'>
16. ---- chaîne unicode -> binaire utf-8
17. bytes3=[b'h\xc3\xa9l\xc3\xa8ne va au march\xc3\xa9 acheter des l\xc3\xa9gumes'], type(bytes3)=<class
'bytes'>
18. ---- binaire utf-8 -> binaire base64
19. bytes4=[b'aMOpbMOobmUgdmEgYXUgbWFyY2jDqSBhY2hldGVyIGRlcyBsw6lndW1lcw==\n'], type(bytes4)=<class 'bytes'>
20. ---- binaire base64 -> binaire utf-8 -> chaîne unicode
21. str6=[hélène va au marché acheter des légumes], type(str6)=<class 'str'>
22. ---- binaire utf-8 -> binaire quoted-printable
23. str7=[b'h=C3=A9l=C3=A8ne=20va=20au=20march=C3=A9=20acheter=20des=20l=C3=A9gumes'], type(str7)=<class
'bytes'>
24. ---- binaire quoted-printable -> binaire utf-8 -> chaîne unicode
25. str8=[hélène va au marché acheter des légumes], type(str8)=<class 'str'>
26.
27. Process finished with exit code 0
• lignes 14-15 : un binaire utf-8 est décodé en chaîne Unicode avec le mauvais décodeur 'iso-8859-1'. Aussi certains caractères
Unicode générés sont incorrects, ici les caractères accentués ;
• lignes 18-19 : le codage 'base64' consiste à utiliser 64 caractères ASCII (codés sur 7 bits) pour encoder un binaire quleconque.
Cela augmente, comme on le voit, la taille du binaire de la chaîne ;
• lignes 22-23 : le codage 'quoted-printable' consiste lui aussi à utiliser des caractères ASCII (codés sur 7 bits) pour encoder un
binaire quelconque ;
On se souviendra que lorsqu'on reçoit un binaire, du réseau internet par exemple, qui représente un texte, pour retrouver celui-ci il
faut connaître les encodages qu'il a subis.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
47/755
5 Les exceptions
Nous nous intéressons maintenant aux exceptions.
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_01.py
1. Traceback (most recent call last):
2. File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_01.py", line 2, in
<module>
3. x = 4 / 0
4. ZeroDivisionError: division by zero
5.
6. Process finished with exit code 1
Une règle essentielle est qu'on doit tout faire pour éviter les exceptions produites par l'interpréteur Python. Il nous faut gérer les
erreurs nous-mêmes.
1. try:
2. actions susceptibles de lancer une exception
3. except (Ex1, Ex2…) as ex:
4. actions de gestion de l'exception [ex]
5. except (Ex11, Ex12…) as ex:
6. actions de gestion de l'exception [ex]
7. finally:
8. actions toujours exécutées qu'il y ait exception ou non
Dans le try, l'exécution des actions s'arrête dès qu'une exception (erreur) survient. Dans ce cas, l'exécution se poursuit avec les actions
de l'une des clauses except :
• ligne 3 : si l'exception [ex] qui s'est produite est d'un type appartenant au tuple (Ex1, Ex2…) ou dérivé de l'un d'eux, alors les
actions de la ligne 4 sont exécutées ;
• ligne 5 : si l'exception n'a pas été interceptée par la ligne 3, et qu'une autre clause [except] existe, alors le même processus se
déroule. Etc… ;
• il peut y avoir autant de clauses [except] que nécessaires pour gérer les différents types d'exception qui peuvent de produire
dans le [try] ;
• si l'exception n'a été traitée par aucune des clauses [except] alors elle remontera au code appelant. Si celui-ci est lui-même
dans une structure try / except, l'exception est de nouveau gérée sinon elle continue à remonter la chaîne des méthodes
appelées. En dernier ressort, elle arrive à l'interpréteur Python. Celui-ci arrête alors le programme excécuté et affiche un
message d'erreur du type montré dans l'exemple précédent. La règle est donc que le programme principal doit arrêter toutes
les exceptions qui peuvent remonter des méthodes appelées ;
• ligne 7 : la clause [finally] est toujours exécutée qu'il y ait eu exception (suite du except) ou pas (suite du try). Ceci est vrai
même si une exception a eu lieu et qu'elle n'a pas été interceptée. Dans ce cas, la clause [finally] sera exécutée avant que
l'exception ne remonte au code appelant ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
48/755
Une exception transporte avec elle des informations sur l'erreur qui s'est produite. On peut les obtenir avec la syntaxe suivante :
[exception] est l'exception qui s'est produite. [exception.args] représente le tuple des paramètres de l'exception.
où le plus souvent MyException est une classe dérivée de la classe BaseException. Les paramètres passés au constructeur de la classe
seront disponibles à la clause except des structures d'interception des exceptions avec la syntaxe [ex.args] si [ex] est l'exception
interceptée par la clause [except].
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
49/755
53. essai += 1
54. try:
55. x = 4 / 0
56. except:
57. # on ne s'intéresse pas à la nature de l'exception
58. print(f"essai n° {essai} : il y a eu un problème")
59.
60. # un autre type d'exception
61. essai += 1
62. try:
63. # x ne peut être converti en nombre entier
64. x = int("x")
65. except ValueError as erreur:
66. # erreur est l'exception interceptée
67. print(f"essai n° {essai} : {erreur}")
68.
69. # une exception transporte des informations dans un tuple accessible au programme
70. essai += 1
71. try:
72. x = int("x")
73. except ValueError as erreur:
74. # erreur est l'exception interceptée
75. print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")
76.
77. # on peut lancer des exceptions
78. essai += 1
79. try:
80. raise ValueError("param1", "param2", "param3")
81. except ValueError as erreur:
82. # erreur est l'exception interceptée
83. print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")
84.
85.
86. # on peut créer ses propres exceptions
87. # elles doivent dériver de la classe [BaseException]
88. class MyError(BaseException):
89. pass
90.
91.
92. # on lance l'exception MyError
93. essai += 1
94. try:
95. raise MyError("info1", "info2", "info3")
96. except MyError as erreur:
97. # erreur est l'exception interceptée
98. print(f"essai n° {essai} : {erreur}, paramètres={erreur.args}")
99.
100. # on lance l'exception MyError avec un msg d'erreur
101. essai += 1
102. try:
103. raise MyError("mon msg d'erreur")
104. except MyError as erreur:
105. # erreur est l'exception interceptée
106. print(f"essai n° {essai} : {erreur.args[0]}")
107.
108. # la clause finally est toujours exécutée
109. # qu'il y ait exception ou non
110. essai += 1
111. x = None
112. try:
113. x = 1
114. except:
115. # exception
116. print(f"essai n° {essai} : exception")
117. finally:
118. # exécuté dans tous les cas
119. print(f"essai n° {essai} : finally x={x}")
120.
121. essai += 1
122. x = None
123. try:
124. x = 2 / 0
125. except:
126. # exception
127. print(f"essai n° {essai} : exception")
128. finally:
129. # exécuté dans tous les cas
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
50/755
130. print(f"essai n° {essai} : finally x={x}")
131.
132. # on n'est pas obligés de mettre une clause [except]
133. essai += 1
134. try:
135. # on provoque une erreur
136. x = 4 / 0
137. finally:
138. # exécuté dans tous les cas
139. print(f"essai n° {essai} : finally x={x}")
Notes :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_02.py
2. Traceback (most recent call last):
3. File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_02.py", line 136, in
<module>
4. x = 4 / 0
5. ZeroDivisionError: division by zero
6. essai n° 0 : division by zero
7. x=2
8. essai n° 1 : division by zero
9. essai n° 2 : (Exception) division by zero
10. essai n° 3 : (ZeroDivisionError) division by zero
11. essai n° 4 : il y a eu un problème
12. essai n° 5 : invalid literal for int() with base 10: 'x'
13. essai n° 6 : invalid literal for int() with base 10: 'x', paramètres=("invalid literal for int() with base
10: 'x'",)
14. essai n° 7 : ('param1', 'param2', 'param3'), paramètres=('param1', 'param2', 'param3')
15. essai n° 8 : ('info1', 'info2', 'info3'), paramètres=('info1', 'info2', 'info3')
16. essai n° 9 : mon msg d'erreur
17. essai n° 10 : finally x=1
18. essai n° 11 : exception
19. essai n° 11 : finally x=None
20. essai n° 12 : finally x=None
21.
22. Process finished with exit code 1
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
51/755
9. return f2(x)
10.
11.
12. def f2(y: int) -> int:
13. # on ne gère pas les exceptions - elles remontent automatiquement
14. return f3(y)
15.
16.
17. def f3(z: int) -> int:
18. if (z % 2) == 0:
19. # si z est pair, on lance une exception
20. raise MyError("exception dans f3")
21. else:
22. return 2 * z
23.
24.
25. # ---------- main
26.
27. # les exceptions remontent la chaîne des méthodes appelées
28. # jusqu'à ce qu'une méthode l'intercepte. Ici ce sera main
29. try:
30. print(f1(4))
31. except MyError as erreur:
32. print(f"type : {type(erreur)}, arguments : {erreur.args}")
33.
34.
35. # trois autres fonctions qui enrichissent les exceptions qu'elle remonte
36. def f4(x: int) -> int:
37. try:
38. return f5(x)
39. except MyError as erreur:
40. # on enrichit l'exception puis on la relance
41. raise MyError("exception dans f4", erreur)
42.
43.
44. def f5(y: int) -> int:
45. try:
46. return f6(y)
47. except MyError as erreur:
48. # on enrichit l'exception puis on la relance
49. raise MyError("exception dans f5", erreur)
50.
51.
52. def f6(z: int) -> int:
53. if (z % 2) == 0:
54. # on lance une exception si z est pair
55. raise MyError("exception dans f6")
56. else:
57. return 2 * z
58.
59.
60. # ---------- main
61. try:
62. print(f4(4))
63. except MyError as erreur:
64. # affichage de l'exception
65. print(f"type : {type(erreur)}, arguments : {erreur.args}")
66. # on peut remonter la pile des exceptions
67. err = erreur
68. # on affiche le msg d'erreur
69. print(err.args[0])
70. # une exception est-elle encapsulée ?
71. while len(err.args) == 2 and isinstance(err.args[1], BaseException):
72. # changement d'exception
73. err = err.args[1]
74. # le 1er argument est le msg d'erreur
75. print(err.args[0])
Notes :
• lignes 25-32, dans l'appel main --> f1 --> f2 --> f3 (ligne 30), l'exception MyError lancée par f3 va remonter jusqu'à main. Elle
sera alors traitée par la clause except de la ligne 31 ;
• lignes 61-75 : dans l'appel main --> f4 --> f5 --> f6 (ligne 62), l'exception lancée MyError par f6 va remonter jusqu'à main. Elle
sera alors traitée par la clause except de la ligne 63. Cette fois-ci, dans sa remontée de la chaîne des fonctions appelantes,
l'exception MyError qui remonte, est elle-même encapsulée dans une autre exception ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
52/755
• lignes 66-75 : montrent comment remonter la pile des exceptions ;
• ligne 71 : la fonction [isinstance(instance, Classe)] rend True si l'objet [instance] est de type [Classe] ou dérivé. Ici,
nous avons utilisé l'exception de niveau le plus haut [BaseException], ce qui nous assure de récupérer toutes les exceptions ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/exceptions/exceptions_03.py
2. type : <class '__main__.MyError'>, arguments : ('exception dans f3',)
3. type : <class '__main__.MyError'>, arguments : ('exception dans f4', MyError('exception dans f5',
MyError('exception dans f6')))
4. exception dans f4
5. exception dans f5
6. exception dans f6
7.
8. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
53/755
6 Les fonctions
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_01.py
2. f1[i,j]=[1,10]
3. f2[i,j]=[2,20]
4. f3[i,j]=[1,30]
5. [i,j]=[2,0]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
54/755
6.
7. Process finished with exit code 0
Notes :
• le script montre l'utilisation de la variable i, déclarée globale dans les fonctions f1 et f2. Dans ce cas, le programme principal
et les fonctions f1 et f2 partagent la même variable i.
Commentaires :
• lignes 2, 12 : au lieu d'être déclarée globale, la variable [i] est passée en paramètre aux fonctions f1 et f2 ;
• lignes 9, 19 : les fonctions f1 et f2 rendent la variable [i] modifiée au programme principal. Celui-ci la récupère aux lignes 36
et 37 ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_02.py
2. f1[i,j]=[1,10]
3. f2[i,j]=[2,20]
4. f3[i,j]=[1,30]
5. [i,j]=[2,0]
6.
7. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
55/755
6.3 Script [fonc_03] : portée des variables
Le script [fonc_03] montre une particularité des variables utilisées à la fois dans une fonction et dans le code appelant celle-ci, selon
que dans la fonction cette variable est utilisée uniquement en lecture ou non.
1. def f1():
2. # ici la variable globale i est connue
3. print(f"[f1] i={i}")
4. # ici la variable globale j est connue
5. print(f"[f1] j={j}")
6.
7.
8. def f2():
9. # ici la variable globale i n'est pas connue
10. # car la fonction f2 définit une variable locale de même nom
11. # c'est alors elle qui a priorité
12. try:
13. # essai d'affichage de la variable locale i définie plus loin
14. print(f"[f2] i={i}")
15. # l'instruction qui suit fait de i une variable locale de la fonction f2
16. i = 7
17. except BaseException as erreur:
18. print(f"[f2] erreur={erreur}")
19.
20.
21. def f3():
22. # ici la variable globale i n'est pas connue
23. # car la fonction f3 définit une variable locale de même nom
24. # c'est alors elle qui a priorité
25.
26. # l'instruction qui suit fait de i une variable locale
27. i = 7
28. # affichage - ici i est connue
29. print(f"[f3] i={i}")
30.
31.
32. # main -----------
33. # variables globales aux fonctions
34. i = 10
35. j = 20
36. # appel de f1
37. f1()
38. print(f"[main] i={i}, j={j}")
39. # appel de f2
40. f2()
41. print(f"[main] i={i}")
42. # appel de f3
43. f3()
44. print(f"[main] i={i}")
Notes
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_03.py
2. [f1] i=10
3. [f1] j=20
4. [main] i=10, j=20
5. [f2] erreur=local variable 'i' referenced before assignment
6. [main] i=10
7. [f3] i=7
8. [main] i=10
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
56/755
9.
10. Process finished with exit code 0
1. # fonction f1
2. def f1(a):
3. a = 2
4.
5.
6. # fonction f2
7. def f2(a, b):
8. a = 2
9. b = 3
10. return a, b
11.
12.
13. # ------------------------ main
14. x = 1
15. f1(x)
16. print(f"x={x}")
17. (x, y) = (-1, -1)
18. (x, y) = f2(x, y)
19. print(f"x={x}, y={y}")
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_04.py
2. x=1
3. x=2, y=3
4.
5. Process finished with exit code 0
Notes :
• tout est objet en Python. Certains objets sont dits "immutable" (en anglais) : on ne peut pas les modifier. C'est le cas des
nombres, des chaînes de caractères, des tuples. Lorsque des objets Python sont passés en paramètres à des fonctions, ce sont
leur références qui sont passées sauf si ces objets sont "immutable" auquel cas, c'est la valeur de l'objet qui est passée ;
• les fonctions f1 (ligne 2), et f2 (ligne 7) veulent illustrer le passage d'un paramètre de sortie. On veut que le paramètre effectif
d'une fonction soit modifié par la fonction ;
• lignes 2-3 : la fonction f1 modifie son paramètre formel a. On veut savoir si le paramètre effectif va lui aussi être modifié ;
• lignes 14-15 : le paramètre effectif est x=1. La ligne 2 des résultats, montre que le paramètre effectif n'est pas modifié. Ainsi
le paramètre effectif x et le paramètre formel a sont deux objets différents ;
• lignes 8-10 : la fonction f2 modifie ses paramètres formels a et b, et les rend comme résultats ;
• lignes 17-18 : on passe à f2 les paramètres effectifs (x,y) et le résultat de f2 est remis dans (x,y). La ligne 3 des résultats montre
que les paramètres effectifs (x,y) ont été modifiés.
On en conclut que lorsqu'on des objets "immutable" sont des paramètres de sortie, il faut qu'ils fassent partie des résultats renvoyés
par la fonction.
1. # ------------------------ main
2. print(f2(100, 200))
3.
4. # fonction f1
5. def f1(a):
6. return a + 10
7.
8.
9. # fonction f2
10. def f2(a, b):
11. return f1(a + b)
Notes
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
57/755
• la ligne 2 va provoquer une erreur parce qu'elle utilise la fonction f2 qui n'a pas encore été définie dans le script ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_05.py
2. Traceback (most recent call last):
3. File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_05.py", line 2, in <module>
4. print(f2(100, 200))
5. NameError: name 'f2' is not defined
6.
7. Process finished with exit code 1
1. # fonction f2
2. def f2(a, b):
3. return f1(a + b)
4.
5.
6. # fonction f1
7. def f1(a):
8. return a + 10
9.
10.
11. # ------------------------ main
12. print(f2(100, 200))
Notes
• ligne 3 : la fonction [f2] utilise la fonction [f1] définie plus loin dans le script. Cela ne provoque cependant pas d'erreur. On
peut donc en conclure que l'ordre de définition des fonctions dans un script Python n'a pas d'importance ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_06.py
2. 310
3.
4. Process finished with exit code 0
Nous isolons dans un module des fonctions réutilisables. Plutôt que de les transporter d'un script à l'autre :
• on les place dans un fichier à part que l'on déclare d'une façon particulière ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
58/755
• les scripts ayant besoin de ces fonctions 'importe' le module qui les contient ;
1. # fonction f2
1. def f2(a, b):
2. return f1(a + b)
3.
4. # fonction f1
5. def f1(a):
6. return a + 10
Pour que les fonctions du script [fonctions_module_01] puissent être référencées par d'autres scripts, il y a différentes façons de faire.
Elles diffèrent selon qu'on exécute ou non le script à l'intérieur de [PyCharm].
Sous [PyCharm] les modules importés sont cherchés dans des dossiers précis appelés [Sources Root]. Il y a deux façons de faire d'un
Après cette opération, le dossier [fonctions/modules] est reconnu comme un dossier source. On peut alors écrire dans un script :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
59/755
Une autre méthode est de passer par les propriétés du projet :
• ci-dessus, la séquence [1-6] permet de faire du dossier [shared] un dossier où ranger des modules à importer ;
Nous n'allons pour l’instant déclarer aucun dossier comme [Sources Root] à part la racine du projet :
1. # utilisation de modules
2. import sys
3.
4. # ------------------------ main
5. print(f"Python path={sys.path}")
6. from fonctions.shared.fonctions_module_01 import f2
7.
8. print(f2(100, 200))
• ligne 2 : on importe l'objet [sys] pour pouvoir utiliser ligne 5, son attribut [path] qui donne, ce qu'on appelle le [Python
Path] : une liste de dossiers qui seront explorés à la recherche de modules importés ;
• ligne 6 : on importe la fonction f2 du module [fonctions_module_01]. Pour désigner ce module, on utilise le chemin qui mène
de la racine du projet au module. Avec Pycharm, la racine du projet fait toujours partie des dossiers explorés lorsqu'est cherché
un module importé dans un script. Ce dossier fait donc partie du [Python Path] du projet. C'est ce que la ligne 5 va nous
permettre de vérifier ;
• ligne 6 : si on décrivait le chemin qui mène de la racine du projet au dossier [fonctions_module_01] on écrirait
[fonctions/shared/fonctions_module_01]. Dans le chemin d'un module, le signe / est remplacé par le point. On écrit donc
[fonctions.modules.fonctions_module_01] ;
• après la ligne 6, la fonction f2 est connue. On l'utilise ligne 8 ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
60/755
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_07.py
2. Python path=['C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\fonctions', 'C:\\Data\\st-
2020\\dev\\python\\cours-2020\\python3-flask-2020', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-
flask-2020\\fonctions\\shared', 'C:\\myprograms\\Python38\\python38.zip', 'C:\\myprograms\\Python38\\DLLs',
'C:\\myprograms\\Python38\\lib', 'C:\\myprograms\\Python38', 'C:\\Data\\st-2020\\dev\\python\\cours-
2020\\python3-flask-2020\\venv', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-
2020\\venv\\lib\\site-packages']
3. 310
4.
5. Process finished with exit code 0
Ci-dessus :
• surlignée en vert, on voit que la racine du projet fait partie du [Python Path] ;
• surligné en jaune, on voit que le dossier du script exécuté fait également partie du [Python Path] ;
• les autres éléments du [Python Path] proviennent directement du dossier d'installation de Python ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
61/755
Pour chercher le module importé [fonctions.shared.fonctions_module_01], l'interpréteur Python cherche dans les dossiers du
[Python Path] un sous-dossier nommé [fonctions]. Il ne le trouve nulle part. En effet le sous-dossier [fonctions] est sous le dossier
[C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020] qui ne fait pas partie du [Python Path].
Il est possible de modifier le [Python Path] par programmation comme le montre le script [fonc-08] :
1. # utilisation de modules
2. import os
3. import sys
4.
5. # dossier du script
6. script_dir = os.path.dirname(os.path.abspath(__file__))
7. # Python Path avant modification
8. print(f"Python path avant={sys.path}")
9. # on ajoute le dossier [shared] au Python Path
10. sys.path.append(f"{script_dir}/shared")
11. # Python Path après modification
12. print(f"Python path après={sys.path}")
13.
14. # import f2
15. from fonctions_module_01 import f2
16.
17. # ------------------------ main
18. print(f2(100, 200))
Notes
• ligne 4 : la variable spéciale [__file__] est le nom du script qui s’exécute. Selon le contexte d’exécution, ce nom peut être
absolu (Pycharm) ou relatif (console). La fonction [os.path.abspath] donne le nom absolu du fichier dont on lui passe le
nom. La fonction [os.path.dirname] donne le nom absolu du dossier contenant le fichier dont on lui passe le nom ;
• ligne 10 : [sys.path] est un tableau contenant les noms des dossiers à explorer lorsqu'un module est recherché. On ajoute à
ce tableau la racine du projet définie ligne 4 ;
• on affiche le [Python Path] avant (ligne 8) et après (ligne 12) modification ;
• ligne 15 : on importe le module [fonctions_module_01] qui contient la fonction f2 ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_08.py
2. Python path avant=['C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\fonctions',
'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020', 'C:\\Data\\st-2020\\dev\\python\\cours-
2020\\python3-flask-2020\\fonctions\\shared', 'C:\\myprograms\\Python38\\python38.zip',
'C:\\myprograms\\Python38\\DLLs', 'C:\\myprograms\\Python38\\lib', 'C:\\myprograms\\Python38',
'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\venv', 'C:\\Data\\st-
2020\\dev\\python\\cours-2020\\python3-flask-2020\\venv\\lib\\site-packages']
3. Python path après=['C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\fonctions',
'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020', 'C:\\Data\\st-2020\\dev\\python\\cours-
2020\\python3-flask-2020\\fonctions\\shared', 'C:\\myprograms\\Python38\\python38.zip',
'C:\\myprograms\\Python38\\DLLs', 'C:\\myprograms\\Python38\\lib', 'C:\\myprograms\\Python38',
'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\venv', 'C:\\Data\\st-
2020\\dev\\python\\cours-2020\\python3-flask-2020\\venv\\lib\\site-packages', 'C:\\Data\\st-
2020\\dev\\python\\cours-2020\\python3-flask-2020\\fonctions/shared']
4. 310
5.
6. Process finished with exit code 0
• ligne 3 : on voit que le dossier [shared] est présent deux fois dans le [Python Path]. On peut éviter cela mais ici ça ne gêne
pas ;
• ligne 4 : la fonction f2 a bien été exécutée ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
62/755
2020\\python3-flask-2020\\venv', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-
2020\\venv\\lib\\site-packages']
3. Python path après=['C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\fonctions',
'C:\\myprograms\\Python38\\python38.zip', 'C:\\myprograms\\Python38\\DLLs',
'C:\\myprograms\\Python38\\lib', 'C:\\myprograms\\Python38', 'C:\\Data\\st-2020\\dev\\python\\cours-
2020\\python3-flask-2020\\venv', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-
2020\\venv\\lib\\site-packages', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-
2020\\fonctions/shared']
19. 310
• ligne 2 : comme précédemment, le dossier [shared] n'est pas dans le [Python Path] ;
• ligne 3 : maintenant il y est ;
• ligne 4 : la fonction f2 a été trouvée ;
Notes :
• ligne 5 : on déclare que le paramètre formel [param] est de type [int] et que le résultat de la fonction est également de type
[int] ;
• ligne 11 : le paramètre effectif de la fonction [show] a le bon type ;
• ligne 12 : le paramètre effectif de la fonction [show] n'a pas le bon type ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_09.py
2. param=4, type(param)=<class 'int'>
3. 5
4. param=xyz, type(param)=<class 'str'>
5. Traceback (most recent call last):
6. File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_09.py", line 11, in
<module>
7. show("xyz")
8. File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_09.py", line 6, in show
9. return param + 1
10. TypeError: can only concatenate str (not "int") to str
11.
12. Process finished with exit code 1
• ligne 10 : le type du paramètre [param] est de type [str]. Lorsque ce message s'affiche, on est déjà entrés dans le code de la
fonction [show]. L'interpréteur Python a donc accepté que le paramètre effectif da la fonction [show] soit de type [str] ;
• la ligne 7 du code provoque l'exception reflétée par les lignes 4-10 des résultats ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
63/755
En [1], PyCharm a surligné l’appel erroné.
Notes
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_10.py
2. 13
3.
4. Process finished with exit code 0
1. # fonction récursive
2. def fact(i: int) -> int:
3. # factorielle(1) est 1
4. # une fonction récursive doit se terminer à un certain moment
5. if i == 1:
6. return 1
7. else:
8. # factorielle(i)=i*factorielle(i-1)
9. return i * fact(i - 1)
10.
11.
12. # ---------- main
13. print(f"fact(8)={fact(8)}")
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
64/755
• lignes 1-9 : la fonction factorielle ;
• ligne 9 : la fonction [factorielle] s’appelle elle-même ;
• lignes 5-6 : une fonction récursive doit toujours s’arrêter lorsqu’une condition est réalisée sinon on a une récursion infinie ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_11.py
2. fact(8)=40320
3.
4. Process finished with exit code 0
1. # fonction récursive
2. # comportement du paramètre j
3.
4.
5. def fact(i: int, j: int) -> int:
6. # arrêt de la fonction récursive
7. if i == 1:
8. print(f"j={j}")
9. return 1
10. else:
11. # on manipule j
12. j += 1
13. print(f"avant fact j={j}")
14. # récursivité
15. f = fact(i - 1, j)
16. print(f"après fact j={j}")
17. # résultat
18. return i * f
19.
20.
21. # ---------- main
22. print(f"fact(8)={fact(8, 0)}")
Commentaires
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_12.py
2. avant fact j=1
3. avant fact j=2
4. avant fact j=3
5. avant fact j=4
6. avant fact j=5
7. avant fact j=6
8. avant fact j=7
9. j=7
10. après fact j=7
11. après fact j=6
12. après fact j=5
13. après fact j=4
14. après fact j=3
15. après fact j=2
16. après fact j=1
17. fact(8)=40320
18.
19. Process finished with exit code 0
• lignes 2-8 : on voit que la valeur de [j] croît tant que la récursivité continue jusqu’à rencontrer la condition où la récursivité
s’arrête. A partir de ce moment, le retour des appels à la fonction [fact] s’opère en sens inverse des appels ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
65/755
• lignes 10-16 : ces affichages traduisent les retours successifs de l’appel de la factorielle. La variable [j] retrouve ses valeurs
jusqu’à sa valeur initiale 1 ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
66/755
7 Les fichiers texte
1. # imports
2. import sys
3.
4.
5. # création puis exploitation séquentielle d'un fichier texte
6. # celui-ci est un ensemble de lignes de la forme login:pwd:uid:gid:infos:dir:shell
7. # chaque ligne est mis dans un dictionnaire sous la forme login => uid:gid:infos:dir:shell
8.
9. # --------------------------------------------------------------------------
10. def affiche_infos(dico: dict, clé: str):
11. # affiche la valeur associée à clé dans le dictionnaire dico si elle existe
12. if clé in dico.keys():
13. # on affiche la valeur associée à la clé
14. print(f"{clé} : {dico[clé]}")
15. else:
16. # clé n'est pas une clé du dictionnaire dico
17. print(f"la clé [{clé}] n'existe pas")
18.
19.
20. # main -----------------------------------------------
21. # on fixe le nom du fichier
22. FILE_NAME = "./data/infos.txt"
23.
24. # création et remplissage du fichier texte
25. fic = None
26. try:
27. # ouverture du fichier en écriture (w=write)
28. fic = open(FILE_NAME, "w")
29. # on génère un contenu arbitraire
30. for i in range(1, 101):
31. # une ligne
32. ligne = f"login{i}:pwd{i}:uid{i}:gid{i}:infos{i}:dir{i}:shell{i}"
33. # est écrite dans le fichier texte
34. fic.write(f"{ligne}\n")
35. except IOError as erreur:
36. print(f"Erreur d'exploitation du fichier {FILE_NAME} : {erreur}")
37. sys.exit()
38. finally:
39. # on ferme le fichier s'il a été ouvert
40. if fic:
41. fic.close()
42.
43. # on l'ouvre en lecture
44. fic = None
45. try:
46. # ouverture du fichier en lecture
47. fic = open(FILE_NAME, "r")
48. # dictionnaire vide au départ
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
67/755
49. dico = {}
50. # chaque ligne est mis dans le dictionnaire [dico] sous la forme login => uid:gid:infos:dir:shell
51. # lecture 1ère ligne en enlevant espaces de début et fin de ligne
52. ligne = fic.readline().strip()
53. # tant que la ligne n'est pas vide
54. while ligne != '':
55. # on met la ligne dans un tableau
56. infos = ligne.split(":")
57. # on récupère le login
58. login = infos[0]
59. # on néglige le pwd
60. infos[0:2] = []
61. # on crée une entrée dans le dictionnaire
62. dico[login] = infos
63. # lecture ligne suivante
64. ligne = fic.readline().strip()
65. except IOError as erreur:
66. print(f"Erreur d'exploitation du fichier {FILE_NAME} : {erreur}")
67. sys.exit()
68. finally:
69. # on ferme le fichier s'il a été ouvert
70. if fic:
71. fic.close()
72.
73. # exploitation du dictionnaire dico
74. affiche_infos(dico, "login10")
75. affiche_infos(dico, "X")
Notes :
• ligne 28 : ouverture du fichier en écriture (w=write). Si le fichier existe déjà, il sera écrasé ;
• lignes 30-34 : on génère 100 lignes dans le fichier texte ;
• ligne 34 : pour écrire une ligne dans le fichier texte. La méthode [write] ne rajoute pas la marque de fin de ligne. Il faut donc
prévoir celle-ci dans le texte écrit ;
• lignes 35-37 : gestion de l'éventuelle exception ;
• ligne 37 : abandon de l'exécution du script (néanmoins après l'exécution de la clause finally) ;
• lignes 38-41 : dans tous les cas, erreur ou pas, on fermet le fichier s'il est ouvert ;
• ligne 47 : ouverture du fichier en lecture (r=read) ;
• ligne 49 : définition d'un dictionnaire vide ;
• ligne 52 : la méthode [readline] lit une ligne de texte, marque de fin de ligne incluse. La méthode [strip] supprime les
"espaces" de début et fin de chaîne. Par "espace", il faut entendre caractère blanc, marque de fin de ligne, saut de page,
tabulation, et quelques autres. Donc ici, [ligne] n'aura pas les caractères de fin de ligne [\r\n] (windows) ou [\n] (unix) ;
• ligne 54 : on exploite le fichier tant qu'on n'a pas récupéré une ligne vide ;
• lignes 54-64 : le fichier texte est transféré dans le dictionnaire [dico]. La clé est le champ [login], la valeur les champs
[uid:gid:infos:dir:shell] ;
• lignes 65-67 : gestion de l'éventuelle exception ;
• lignes 68-71 : fermeture du fichier dans tous les cas, erreur ou pas ;
• lignes 74-75 : exploitation du dictionnaire [dico] ;
Le fichier [data/infos.txt] :
1. login0:pwd0:uid0:gid0:infos0:dir0:shell0
2. login1:pwd1:uid1:gid1:infos1:dir1:shell1
3. login2:pwd2:uid2:gid2:infos2:dir2:shell2
4. …
5. login98:pwd98:uid98:gid98:infos98:dir98:shell98
6. login99:pwd99:uid99:gid99:infos99:dir99:shell99
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/fichiers/fic_01.py
2. login10 : ['uid10', 'gid10', 'infos10', 'dir10', 'shell10']
3. la clé [X] n'existe pas
4.
5. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
68/755
• en [5-6] : choisir l'encodage UTF-8 pour les fichiers du projet ;
Pour créer un fichier encodé en UTF-8, on pourra procéder comme suit (fic-02) :
1. # imports
2. import codecs
3.
4. # écriture utf8 dans un fichier texte
5. # on ne gère pas les exceptions
6. file=codecs.open("./data/utf8.txt","w","utf8")
7. file.write("Hélène est partie à Bâle pendant l'été chez sa grand-mère")
8. file.close()
Notes
Résultats
Lorsqu'on ouvre le fichier [data/utf8.txt] obtenu (cf. ligne 6), on obtient le résultat suivant :
1. # imports
2. import codecs
3.
4. # écriture iso-8859-1 dans un fichier texte
5. # on ne gère pas les exceptions
6. file=codecs.open("./data/iso-8859-1.txt","w","iso-8859-1")
7. file.write("Hélène est partie à Bâle pendant l'été chez sa grand-mère")
8. file.close()
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
69/755
Lorsqu'on ouvre le fichier [data/iso-8859-1] créé ligne 6, on obtient le résultat suivant :
Parce que nous avons configuré le projet pour fonctionner avec des fichiers UTF-8, PyCharm a essayé d'ouvrir le fichier [iso-8859-
1.txt] en UTF-8. Il est capable de voir [1] que le fichier n'est pas du UTF-8. Il propose alors [2] de recharger le fichier dans un
autre encodage :
• on voit qu'en [6-7], Pycharm a noté le fait que le fichier [iso-8859-1.txt] devait être ouvert avec un encodage ISO-8859-1.
C'est donc une exception à la règle [5] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
70/755
Le fichier jSON géré [data/in.json] sera le suivant :
• en [2], on voit que le contenu texte du fichier [in.json] pourrait représenter un dictionnaire Python. PyCharm a mis en
forme (Ctrl-Alt-L) ce texte mais il serait sur une ligne que ça ne changerait rien. La forme du texte n'a aucune importance tant
qu'il représente syntaxiquement un objet Python ;
1. # imports
2. import codecs
3. import json
4. import sys
5.
6. # lecture / écriture d'un fichier jSON
7. inFile=None
8. outFile=None
9. try:
10. # ouverture du fichier jSON en lecture
11. inFile = codecs.open("./data/in.json", "r", "utf8")
12. # transfert du contenu dans un dictionnaire
13. data = json.load(inFile)
14. # affichage des données lues
15. print(f"data={data}, type(data)={type(data)}")
16. limites = data['limites']
17. print(f"limites={limites}, type(limites)={type(limites)}")
18. print(f"limites[1]={limites[1]}, type(limites[1])={type(limites[1])}")
19. # transfert du dictionnaire [data] dans un fichier json
20. outFile = codecs.open("./data/out.json", "w", "utf8")
21. json.dump(data, outFile)
22. except BaseException as erreur:
23. # on affiche l'erreur et on quitte
24. print(f"L'erreur suivante s'est produite : {erreur}")
25. sys.exit()
26. finally:
27. # fermeture des fichiers s'ils sont ouverts
28. if inFile:
29. inFile.close()
30. if outFile:
31. outFile.close()
Notes
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
71/755
• lignes 26-31 : dans tous les cas, erreur ou pas, on ferme les fichiers qui ont pu être ouverts ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/fichiers/json_01.py
2. data={'limites': [9964, 27519, 73779, 156244, 0], 'coeffR': [0, 0.14, 0.3, 0.41, 0.45], 'coeffN': [0,
1394.96, 5798, 13913.69, 20163.45], 'PLAFOND_QF_DEMI_PART': 1551,
'PLAFOND_REVENUS_CELIBATAIRE_POUR_REDUCTION': 21037, 'PLAFOND_REVENUS_COUPLE_POUR_REDUCTION': 42074,
'VALEUR_REDUC_DEMI_PART': 3797, 'PLAFOND_DECOTE_CELIBATAIRE': 1196, 'PLAFOND_DECOTE_COUPLE': 1970,
'PLAFOND_IMPOT_COUPLE_POUR_DECOTE': 2627, 'PLAFOND_IMPOT_CELIBATAIRE_POUR_DECOTE': 1595,
'ABATTEMENT_DIXPOURCENT_MAX': 12502, 'ABATTEMENT_DIXPOURCENT_MIN': 437}, type(data)=<class 'dict'>
3. limites=[9964, 27519, 73779, 156244, 0], type(limites)=<class 'list'>
4. limites[1]=27519, type(limites[1])=<class 'int'>
5.
6. Process finished with exit code 0
• les lignes 2-4 montrent qu'on a correctement récupéré le dictionnaire présent dans le fichier jSON ;
Le texte du fichier est sur une ligne. Cependant PyCharm reconnaît les fichiers jSON et on peut les formater, comme les fichiers
Python et d'autres par Ctrl-Alt-L. On obtient alors la chose suivante :
1. # imports
2. import codecs
3. import json
4. import sys
5.
6. # dictionnaire
7. data = {'marié': 'oui', 'impôt': 1340}
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
72/755
8.
9. # écriture d'un fichier jSON
10. out_file1 = None
11. out_file2 = None
12. try:
13. # transfert du dictionnaire [data] dans un fichier json
14. out_file1 = codecs.open("./data/out1.json", "w", "utf8")
15. json.dump(data, out_file1, ensure_ascii=True)
16. # transfert du dictionnaire [data] dans un fichier json
17. out_file2 = codecs.open("./data/out2.json", "w", "utf8")
18. json.dump(data, out_file2, ensure_ascii=False)
19. except BaseException as erreur:
20. # on affiche l'erreur et on quitte
21. print(f"L'erreur suivante s'est produite : {erreur}")
22. sys.exit()
23. finally:
24. # fermeture des fichiers s'ils sont ouverts
25. if out_file1:
26. out_file1.close()
27. if out_file2:
28. out_file2.close()
29. …
• dans ce script, on écrit le dictionnaire [data] (ligne 7) dans deux fichiers jSON (lignes 14, 17) ;
• lignes 14, 17 : dans les deux cas, on crée un fichier texte UTF-8 ;
• lignes 15 : lors de l'écriture du dictionnaire, on utilise le paramètre nommé [ensure_ascii=True] ;
• lignes 18 : lors de l'écriture du dictionnaire, on utilise le paramètre nommé [ensure_ascii=False] ;
• dans le fichier [out1.json], les caractères accentués ont été remplacés par une série de caractères représentant leur code
UTF-8. On dit parfois qu'ils ont été 'échappés'. Techniquement, dans le binaire de [out1.json], on trouve pour le caractère é
de [marié] successivement les codes binaires UTF-8 des 6 caractères [\u00e9] ;
• dans le fichier [out2.json], les caractères accentués ont été laissés tels quels. Cela signifie que dans le binaire de [out2.json]
ces caractères sont représentés par leur code binaire UTF-8 (1 seul code UTF-8 donc au lieu de 6 pour out1). Pour le caractère
é de [marié], on trouvera ainsi le code binaire [00e9] sur 4 octets ;
• c'est la valeur du paramètre [ensure_ascii] de la méthode [json.dump] qui décide du format utilisé ;
Certaines applications utilisent de l'UTF-8 'échappé' pour leurs fichiers jSON. C'est la valeur [ensure_ascii=True] qui doit être alors
utilisée. Cette valeur est en fait la valeur par défaut. Si donc on n'utilise pas le paramètre [ensure_ascii] on travaillera avec des fichiers
jSON UTF-8 échappés.
1. # imports
2. import codecs
3. import json
4. import sys
5.
6. # dictionnaire
7. data = {'marié': 'oui', 'impôt': 1340}
8.
9. …
10.
11. # relecture des fichiers jSON
12. in_file1 = None
13. in_file2 = None
14. try:
15. # transfert du fichier jSON 1 dans un dictionnaire
16. in_file1 = codecs.open("./data/out1.json", "r", "utf8")
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
73/755
17. dico1 = json.load(in_file1)
18. # affichage
19. print(f"dico1={dico1}")
20. # transfert du fichier jSON 2 dans un dictionnaire
21. in_file2 = codecs.open("./data/out2.json", "r", "utf8")
22. dico2 = json.load(in_file2)
23. # affichage
24. print(f"dico2={dico2}")
25. except BaseException as erreur:
26. # on affiche l'erreur et on quitte
27. print(f"L'erreur suivante s'est produite : {erreur}")
28. sys.exit()
29. finally:
30. # fermeture des fichiers s'ils sont ouverts
31. if in_file1:
32. in_file1.close()
33. if in_file2:
34. in_file2.close()
Notes
• lignes 11-34 : relecture des deux fichiers [out1.json, out2.json] et affichage du dictionnaire lu dans chacun des cas ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/fichiers/json_02.py
2. dico1={'marié': 'oui', 'impôt': 1340}
3. dico2={'marié': 'oui', 'impôt': 1340}
4.
5. Process finished with exit code 0
De façon surprenante, on constate qu'on n'a pas eu besoin de préciser à la fonction [json.load] (lignes 17, 22) le type d'encodage
(échappé ou non) de la chaîne jSON à lire. On récupère dans les deux cas le bon dictionnaire.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
74/755
8 Exercice d'application – version 1
8.1 Le problème
Le tableau ci-dessus permet de calculer l’impôt dans le cas simplifié d'un contribuable n'ayant que son seul salaire à déclarer. Comme
l’indique la note (1), l’impôt ainsi calculé est l’impôt avant trois mécanismes :
• le plafonnement du quotient familial qui intervient pour les hauts revenus ;
• la décote et la réduction d’impôts qui interviennent pour les faibles revenus ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
75/755
On se propose d'écrire un programme permettant de calculer l'impôt d'un contribuable dans le cas simplifié d'un contribuable n'ayant
que son seul salaire à déclarer :
9964 0 0
27519 0.14 1394.96
73779 0.3 5798
156244 0.4 13913.69
0 0.45 20163.45
Chaque ligne a 3 champs : champ1, champ2, champ3. Pour calculer l'impôt I, on recherche la première ligne où QF<=champ1 et on prend
les valeurs de cette ligne. Par exemple, pour un salarié marié avec deux enfants et un salaire annuel S de 50000 euros :
L'impôt I est alors égal à 0.14*R – 1394,96*nbParts=[0,14*45000-1394,96*3]=2115. L’impôt est arrondi à l’euro inférieur.
Si QF est tel que la relation QF<=champ1 n'est jamais vérifiée, alors ce sont les coefficients de la dernière ligne qui sont utilisés. Ici :
0 0.45 20163.45
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
76/755
8.1.2 Plafonnement du quotient familial
Pour savoir si le plafonnement du quotient familial QF s’applique, on refait le calcul de l’impôt brut sans les enfants. Toujours pour
le salarié marié avec deux enfants et un salaire annuel S de 50000 euros :
Toujours pour le salarié marié avec deux enfants et un salaire annuel S de 50000 euros :
L’impôt brut (2115) issu de l’étape précédente est inférieur à 2627 euros pour un couple (1595 euros pour un célibataire) : la décôte
s’applique donc. Elle est obtenue avec le calcul suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
77/755
décôte= seuil (couple=1970/célibataire=1196)-0,75* Impôt brut
Au-dessous d’un certain seuil, une réduction de 20 % est faite sur l’impôt brut issu des calculs précédents. En 2019, les seuils sont les
suivants :
• célibataire : 21037 euros ;
• couple : 42074 euros ; ( le chiffre 37968 utilisé dans l’exemple ci-dessus semble erroné) ;
Ce seuil est augmenté de la valeur : 3797 * (nombre de demi-parts amenées par les enfants).
Toujours pour le salarié marié avec deux enfants et un salaire annuel S de 50000 euros :
• son revenu imposable (45000 euros) est inférieur au seuil (42074+2*3797)=49668 euros ;
• il a donc droit à une réduction de 20 % de son impôt : 1731 * 0,2= 346,2 euros arrondi à 347 euros ;
• l’impôt brut du contribuable devient : 1731-347= 1384 euros ;
Prenons le cas d’un salarié non marié sans enfants et un salaire annuel de 200000 euros :
• la réduction de 10 % est de 20000 euros > 12502 euros. Elle est donc ramenée à 12502 euros ;
8.1.6.2 Plafonnement du quotient familial
Prenon un cas où le plafonnement familial présenté au paragraphe |Plafonnement du quotient familial|, intervient. Prenons le cas
d’un couple avec trois enfants et des revenus annuels de 100000 euros. Reprenons les étapes du calcul :
• l’abattement de 10 % est de 10000 euros < 12502 euros. Le revenu imposable R est donc 100000-10000=90000 euros ;
• le couple a nbParts=2+0,5*2+1=4 parts ;
• son quotient familial est donc QF= R/nbParts=90000/4=22500 euros ;
• son impôt brut I1 avec enfants est I1=0,14*90000-1394,96*4= 7020 euros ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
78/755
• son impôt brut I2 sans enfants :
• QF=90000/2=45000 euros ;
• I2=0,3*90000-5798*2=15404 euros ;
• la règle du plafonnement du quotient familial dit que le gain amené par les enfants ne peut dépasser (1551*4 demi-parts)=6204
euros. Or ici, il est I2-I1=15404-7020= 8384 euros, donc supérieur à 6204 euros ;
• l’impôt brut est donc recalculé comme I3=I2-6204=15404-6204= 9200 euros ;
Ce couple n’aura ni décote, ni réduction et son impôt final sera de 9200 euros.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
79/755
Couple avec 3 enfants et des revenus annuels de 200000 euros Impôt=42843 euro Impôt=42842 euros
décote=0 euro Surcote=17283 euros
Réduction=0 euro décote=0 euro
Taux d’imposition=41 % Réduction=0 euro
Taux d’imposition=41 %
Ci-dessus, on appelle surcote, ce que paient en plus les hauts revenus à cause de deux phénomènes :
Cet indicateur n’a pu être vérifié car le simulateur de l’administration fiscale ne le donne pas.
On voit que l’algorithme du document donne un impôt juste à chaque fois, avec cependant une marge d’erreur de 1 euro. Cette marge
d’erreur provient des arrondis. Toutes les sommes d’argent sont arrondies parfois à l’euro supérieur, parfois à l’euro inférieur. Comme
je ne connaissais pas les règles officielles, les sommes d’argent de l’algorithme du document ont été arrondies :
• à l’euro supérieur pour les décotes et réductions ;
• à l’euro inférieur pour les surcotes et l’impôt final ;
8.2 Version 1
• les données nécessaires au calcul de l'impôt sont codées en dur dans le code sous forme de listes et de constantes ;
• les données des contribuables (marié, enfants, salaire) sont dans un premier fichier texte [taxpayersdata.txt] ;
• les résultats du calcul de l'impôt (marié, enfants, salaire, impôt) sont mémorisés dans un second fichier texte [résultats.txt] ;
1. # modules
2. import sys
3.
4. from impots.v01.shared.impôts_module_01 import *
5.
6. # main -----------------------
7.
8. # constantes
9. # fichier des contribuables
10. DATA = "./data/taxpayersdata.txt"
11. # fichier des résultats
12. RESULTATS = "./data/résultats.txt"
13.
14. try:
15. # lecture des données contribuables
16. tax_payers = get_taxpayers_data(DATA)
17. # liste des résultats
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
80/755
18. results = []
19. # on calcule l'impôt des contribuables
20. for tax_payer in tax_payers:
21. # le calcul de l'impôt renvoie un dictionnaire de clés
22. # ['marié', 'enfants', 'salaire', 'impôt', 'surcôte', 'décôte', 'réduction', 'taux']
23. result = calcul_impôt(tax_payer['marié'], tax_payer['enfants'], tax_payer['salaire'])
24. # le dictionnaire est ajouté à la liste des résultats
25. results.append(result)
26. # on enregistre les résultats
27. record_results(RESULTATS, results)
28. except BaseException as erreur:
29. # il peut y avoir différentes erreurs : absence de fichier, contenu de fichier incorrect
30. # on affiche l'erreur et on quitte l'application
31. print(f"l'erreur suivante s'est produite : {erreur}]\n")
32. sys.exit()
Notes
• ligne 4 : on utilise le module [impots.v01.modules.impôts_module_01]. On rappelle que ce chemin est mesuré à partir de la
racine du projet PyCharm ;
• ligne 10 : le fichier [data/taxpayersdata.txt] est le suivant :
1. oui,2,55555
2. oui,2,50000
3. oui,3,50000
4. non,2,100000
5. non,3,100000
6. oui,3,100000
7. oui,5,100000
8. non,0,100000
9. oui,2,30000
10. non,0,200000
11. oui,3,200000
Chaque ligne représente un tuple de trois éléments [marié / pacsé ou pas, nombre d'enfants, salaire annuel en euros] .
• ligne 12 : le fichier où on placera les résultats du calcul de l'impôt pour chacun des contribuables du fichier
[taxpayersdata.txt]. Il aura le contenu suivant :
1. {'marié': 'oui', 'enfants': 2, 'salaire': 55555, 'impôt': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0,
'taux': 0.14}
2. {'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'décôte': 384, 'réduction':
347, 'taux': 0.14}
3. {'marié': 'oui', 'enfants': 3, 'salaire': 50000, 'impôt': 0, 'surcôte': 0, 'décôte': 720, 'réduction': 0,
'taux': 0.14}
4. {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'décôte': 0,
'réduction': 0, 'taux': 0.41}
5. {'marié': 'non', 'enfants': 3, 'salaire': 100000, 'impôt': 16782, 'surcôte': 7176, 'décôte': 0,
'réduction': 0, 'taux': 0.41}
6. {'marié': 'oui', 'enfants': 3, 'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'décôte': 0, 'réduction':
0, 'taux': 0.3}
7. {'marié': 'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'décôte': 0, 'réduction': 0,
'taux': 0.14}
8. {'marié': 'non', 'enfants': 0, 'salaire': 100000, 'impôt': 22986, 'surcôte': 0, 'décôte': 0, 'réduction':
0, 'taux': 0.41}
9. {'marié': 'oui', 'enfants': 2, 'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'décôte': 0, 'réduction': 0,
'taux': 0}
10. {'marié': 'non', 'enfants': 0, 'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'décôte': 0,
'réduction': 0, 'taux': 0.45}
11. {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'décôte': 0,
'réduction': 0, 'taux': 0.41}
• ligne 16 : on récupère les données des contribuables contenues dans [taxpayersdata.txt]. On récupère une liste de
dictionnaires de clés [marié, enfants, salaire] chaque dictionnaire représentant un contribuable ;
• lignes 17-25 : on calcule l'impôt des contribuables de la liste [taxPayers]. On récupère une liste [results] dont chaque
élément est de nouveau un dictionnaire de clés [marié, enfants, salaire, impôt, surcôte, décôte, réduction, taux] ;
• ligne 27 : la liste [results] des résultats est enregistrée dans le fichier [résultats.txt] sous la forme montrée ci-dessus ;
• lignes 28-32 : on arrête toutes les exceptions qui peuvent sortir du module [impots.v01.modules.impôts_module_01] ;
Nous allons détailler maintenant les trois fonctions utilisées par le script [main] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
81/755
• [calcul_impôt] : pour calculer l'impôt de ceux-ci ;
• [record_results] : pour enregistrer les résultats dans un fichier texte ;
1. # imports
2. import codecs
3. …
4.
5. # lecture des données des contribuables
6. # ----------------------------------------
7. def get_taxpayers_data(taxpayers_filename: str) -> list:
8. # lecture des données contribuables
9. file = None
10. try:
11. # la liste des contribuables
12. taxpayers = []
13. # ouverture du fichier
14. file = codecs.open(taxpayers_filename, "r", "utf8")
15. # on lit la première ligne du fichier des contribuables
16. ligne = file.readline().strip()
17. # tant qu'il reste une ligne à exploiter
18. while ligne != '':
19. # on récupère les 3 champs marié,enfants,salaire qui forment la ligne
20. (marié, enfants, salaire) = ligne.split(",")
21. # on les ajoute à la liste des contribuables
22. taxpayers.append({'marié': marié.strip().lower(), 'enfants': int(enfants), 'salaire':
int(salaire)})
23. # on lit une nouvelle ligne du fichier des contribuables
24. ligne = file.readline().strip()
25. # on rend le résultat
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
82/755
26. return taxpayers
27. finally:
28. # on ferme le fichier s'il a été ouvert
29. if file:
30. file.close()
Notes
• ligne 7 : [taxpayers_filename] est le nom du fichier à exploiter. La fonction rend une liste ;
• lignes 18-24 : la boucle d'exploitation des lignes [marié, enfants, salaire] du fichier texte ;
• ligne 20 : les trois éléments de la ligne sont récupérés. On suppose ici que la ligne est syntaxiquement correcte, ç-à-d qu'elle a
bien les trois éléments attendus ;
• ligne 22 : on construit un dictionnaire avec les clés [marié, enfants, salaire] et ce dictionnaire est ajouté à la liste
[taxPayers] ;
• ligne 26 : une fois le fichier exploité, on rend la liste [taxPayers] ;
• lignes 10-30 : on remarquera qu'on n'a pas mis de clause [catch] au [try] de la ligne 10. La clause [catch] n'est pas obligatoire.
Ligne 27, on a mis une clause [finally] pour fermer le fichier texte dans tous les cas, erreur ou pas ;
• cette structure try / finally laisse échapper une éventuelle exception (il n'y a pas de catch). Cette exception va remonter au
script principal [main] qui va arrêter et afficher l'exception (cf paragraphe |Le script principal|). Ce mécanisme a été utilisé
pour la plupart des fonctions du module ;
1. # imports
2. import codecs
3. import math
4.
5. # tranches de l'impôt 2019
6. limites = [9964, 27519, 73779, 156244, 0]
7. coeffr = [0, 0.14, 0.3, 0.41, 0.45]
8. coeffn = [0, 1394.96, 5798, 13913.69, 20163.45]
9.
10. # constantes pour le calcul de l'impôt 2019
11. PLAFOND_QF_DEMI_PART = 1551
12. PLAFOND_REVENUS_CELIBATAIRE_POUR_REDUCTION = 21037
13. PLAFOND_REVENUS_COUPLE_POUR_REDUCTION = 42074
14. VALEUR_REDUC_DEMI_PART = 3797
15. PLAFOND_DECOTE_CELIBATAIRE = 1196
16. PLAFOND_DECOTE_COUPLE = 1970
17. PLAFOND_IMPOT_COUPLE_POUR_DECOTE = 2627
18. PLAFOND_IMPOT_CELIBATAIRE_POUR_DECOTE = 1595
19. ABATTEMENT_DIXPOURCENT_MAX = 12502
20. ABATTEMENT_DIXPOURCENT_MIN = 437
21.
22. …
23. # calcul de l'impôt
24. # ----------------------------------------
25. def calcul_impôt(marié: str, enfants: int, salaire: int) -> dict:
26. # marié : oui, non
27. # enfants : nombre d'enfants
28. # salaire : salaire annuel
29. # limites, coeffr, coeffn : les tableaux des données permettant le calcul de l'impôt
30. #
31. # calcul de l'impôt avec enfants
32. result1 = calcul_impôt_2(marié, enfants, salaire)
33. impot1 = result1["impôt"]
34. # calcul de l'impôt sans les enfants
35. if enfants != 0:
36. result2 = calcul_impôt_2(marié, 0, salaire)
37. impot2 = result2["impôt"]
38. # application du plafonnement du quotient familial
39. if enfants < 3:
40. # PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants
41. impot2 = impot2 - enfants * PLAFOND_QF_DEMI_PART
42. else:
43. # PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants, le double pour les suivants
44. impot2 = impot2 - 2 * PLAFOND_QF_DEMI_PART - (enfants - 2) * 2 * PLAFOND_QF_DEMI_PART
45. else:
46. impot2 = impot1
47. result2 = result1
48.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
83/755
49. # on prend l'impôt le plus fort avec le taux et la surcôte qui vont avec
50. if impot1 > impot2:
51. impot = impot1
52. taux = result1["taux"]
53. surcôte = result1["surcôte"]
54. else:
55. surcôte = impot2 - impot1 + result2["surcôte"]
56. impot = impot2
57. taux = result2["taux"]
58.
59. # calcul d'une éventuelle décôte
60. décôte = get_décôte(marié, salaire, impot)
61. impot -= décôte
62. # calcul d'une éventuelle réduction d'impôts
63. réduction = get_réduction(marié, salaire, enfants, impot)
64. impot -= réduction
65. # résultat
66. return {"marié": marié, "enfants": enfants, "salaire": salaire, "impôt": math.floor(impot), "surcôte":
surcôte,
67. "décôte": décôte, "réduction": réduction, "taux": taux}
Notes
• lignes 6-8 : les tranches de l'impôt (cf. paragraphe |Calcul de l’impôt brut|) ;
• lignes 11-20 : les constantes du calcul de l'impôt ;
• on notera que les éléments initialisés aux lignes 5-20 seront globaux aux fonctions que nous allons décrire. Ils sont donc
connus tant que la fonction qui les utilise ne déclare pas de variables de mêmes noms ;
• les chiffres des lignes 5-20 changent chaque année. Ici ce sont les chiffres 2019 ;
• ligne 25 : la fonction [calcul_impôt] reçoit trois paramètres :
o [marié] : oui / non, indique si le contribuable est marié ou pacsé ;
o [enfants] : son nombre d'enfants ;
o [salaire] : son salaire annuel en euros ;
• lignes 31-33 : calcul de l’impôt en prenant en compte les enfants ;
• lignes 34-47 : ces lignes implémentent le plafonnement du quotient familial (cf. paragraphe |Plafonnement du quotient
familial|) ;
• lignes 49-57 : ces lignes calculent le taux d'imposition du contribuable ainsi qu'une éventuelle surcote (cf paragraphe |Cas des
hauts revenus|) ;
• lignes 59-61 : calcul d'une éventuelle décote (cf paragraphe |Calcul de la décôte|) ;
• lignes 62-64 : calcul d'une éventuelle réduction de l'impôt à payer (cf. paragraphe |Calcul de la réduction d’impôts|) ;
L'algorithme est assez complexe et nous ne le détaillerons pas plus que ce que disent les commentaires. L'algorithme implémente le
mode de calcul de l'impôt tel que décrit au paragraphe |Le problème|.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
84/755
26.
27. # quotient familial
28. quotient = revenu_imposable / nb_parts
29. # est mis à la fin du tableau limites pour arrêter la boucle qui suit
30. limites[len(limites) - 1] = quotient
31. # calcul de l'impôt
32. i = 0
33. while quotient > limites[i]:
34. i += 1
35. # du fait qu'on a placé quotient à la fin du tableau limites, la boucle précédente
36. # ne peut déborder du tableau limites
37.
38. # maintenant on peut calculer l'impôt
39. impôt = math.floor(revenu_imposable * coeffr[i] - nb_parts * coeffn[i])
40. # résultat
41. return {"impôt": impôt, "surcôte": surcôte, "taux": coeffr[i]}
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
85/755
1. # revenu_imposable = salaireAnnuel - abattement
2. # l'abattement a un min et un max
3. # ----------------------------------------
4. def get_revenu_imposable(salaire: int) -> int:
5. # abattement de 10% du salaire
6. abattement = 0.1 * salaire
7. # cet abattement ne peut dépasser ABATTEMENT_DIXPOURCENT_MAX
8. if abattement > ABATTEMENT_DIXPOURCENT_MAX:
9. abattement = ABATTEMENT_DIXPOURCENT_MAX
10.
11. # l'abattement ne peut être inférieur à ABATTEMENT_DIXPOURCENT_MIN
12. if abattement < ABATTEMENT_DIXPOURCENT_MIN:
13. abattement = ABATTEMENT_DIXPOURCENT_MIN
14.
15. # revenu imposable
16. revenu_imposable = salaire - abattement
17. # résultat
18. return math.floor(revenu_imposable)
1. oui,2,55555
2. oui,2,50000
3. oui,3,50000
4. non,2,100000
5. non,3,100000
6. oui,3,100000
7. oui,5,100000
8. non,0,100000
9. oui,2,30000
10. non,0,200000
11. oui,3,200000
1. {'marié': 'oui', 'enfants': 2, 'salaire': 55555, 'impôt': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0,
'taux': 0.14}
2. {'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'décôte': 384, 'réduction':
347, 'taux': 0.14}
3. {'marié': 'oui', 'enfants': 3, 'salaire': 50000, 'impôt': 0, 'surcôte': 0, 'décôte': 720, 'réduction': 0,
'taux': 0.14}
4. {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'décôte': 0,
'réduction': 0, 'taux': 0.41}
5. {'marié': 'non', 'enfants': 3, 'salaire': 100000, 'impôt': 16782, 'surcôte': 7176, 'décôte': 0,
'réduction': 0, 'taux': 0.41}
6. {'marié': 'oui', 'enfants': 3, 'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'décôte': 0, 'réduction':
0, 'taux': 0.3}
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
86/755
7. {'marié': 'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'décôte': 0, 'réduction': 0,
'taux': 0.14}
8. {'marié': 'non', 'enfants': 0, 'salaire': 100000, 'impôt': 22986, 'surcôte': 0, 'décôte': 0, 'réduction':
0, 'taux': 0.41}
9. {'marié': 'oui', 'enfants': 2, 'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'décôte': 0, 'réduction': 0,
'taux': 0}
10. {'marié': 'non', 'enfants': 0, 'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'décôte': 0,
'réduction': 0, 'taux': 0.45}
11. {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'décôte': 0,
'réduction': 0, 'taux': 0.41}
Ces résultats sont conformes aux chiffres officiels du paragraphe |Chiffres officiels|.
On retrouve une erreur déjà rencontrée : celle où un module n’est pas trouvé, ici le module [impots]. On rappelle que cela veut
dire que :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
87/755
9 Les imports
L’erreur rencontrée dans la version 1 de l’exercice d’application nous amène à approfondir le rôle de l’instruction [import].
1. # module importé
2. # cette instruction sera exécutée à chaque fois que le module sera importé
3. print("2")
4. # variable appartenant au module importé
5. x=4
Un module est exécuté lorsqu’il est importé. Ainsi lorsque le module [imported] sera importé :
En [1], PyCharm indique qu’il ne connaît pas le module [imported]. En termes techniques, cela signifie que le dossier contenant le
module [imported] n’est pas dans le Python Path de PyCharm. Le Python Path est l’ensemble des dossiers dans lesquels les modules
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
88/755
importés sont cherchés. Pour résoudre ce problème, il suffit de déclarer [Sources root] le dossier dans lequel se trouve le module
[imported], ici le dossier [import/01] :
Après cette opération, le dossier [import/01] est mis dans le Python Path de Pycharm et l’erreur disparaît :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/import/01/main_01.py
2. 2
3. 4
4.
5. Process finished with exit code 0
Commentaires
On retiendra de cet exemple, le concept important qu’un module (ou un script) importé est exécuté.
• ligne 2, on a une autre syntaxe de l’importation [from module import objet1, objet2, …]. Ici on importe la variable
[imported.x]. Avec ectte syntaxe, la variable x devient une variable du script [main_02]. On n’a plus besoin de la préfixer par
son module [imported] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
89/755
• ligne 4 : on affiche la variable x de [main_02] ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/import/01/main_02.py
2. 2
3. 4
4.
5. Process finished with exit code 0
La notation [import *] de la ligne 2 signifie qu’on importe tous les objets visibles du module importé (variables, fonctions).
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/import/01/main_03.py
2. 2
3. 4
4.
5. Process finished with exit code 0
La ligne 3 montre qu’on peut importer un objet du module importé et lui donner un alias. Ici la variable [imported.x] devient la
variable [main_04.y]. Les résultats sont les mêmes qu’auparavant.
1. # une fonction
2. def f1():
3. print("f1")
1. # import
2. import module1
3. # exécution f1
4. module1.f1()
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
90/755
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/import/02/main_01.py
2. f1
3.
4. Process finished with exit code 0
Note : Pour éviter que PyCharm ne signale une erreur sur l’importation de la ligne 2, il mettre le dossier contenant [module1] dans
les [Sources Root] de PyCharm :
En [1], le dossier [02] mis dans les [Sources Root] est passé en bleu. On notera que l’erreur signalée n’empêche pas ici une exécution
correcte des scripts. En effet, lors de l’exécution du script [main_0x], le dossier du script est automatiquement mis dans le Python
Path. Du coup [module1] est trouvé. Dorénavant, lorsque sur une copie d’écran un dossier est en bleu, c’est qu’il a été mis dans les
[Sources Root] de PyCharm.
1. # import
2. from module1 import f1
3. # exécution f1
4. f1()
Les nouveaux scripts vont importer le module [module2] qui n’est pas dans le même dossier qu’eux.
1. # une fonction
2. def f2():
3. print("f2")
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
91/755
4. dir1.module2.f2()
• ligne 2 : on utilise une notation spéciale pour indiquer comment trouver le module [module2]. Il faut lire [dir1.module2]
comme le chemin [dir1/module2] : pour trouver [module2], on part du dossier du script courant [main_01], puis on passe
dans [dir1] et là on trouve [module2]. Il ne faut pas oublier ici que le point de départ du chemin est le dossier du script qui
importe ;
• ligne 4 : pour exécuter la fonction [f2] de [module2] ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/import/03/main_01.py
2. f2
3.
4. Process finished with exit code 0
Cette fois-ci, ligne 2, on n’importe que la fonction [f2] qui devient alors une fonction du script [main_03] (ligne 4).
Tous ces scripts fonctionnent aussi bien dans le contexte PyCharm que dans celui d’une console Python. La raison est que dans les
deux cas, le dossier du script exécuté, ici le dossier [03] fait partie du Python Path. Du coup, le dossier [dir1/module2] est trouvé.
Ici, les dossiers [dir1] et [dir2] ont été mis dans les [Sources Root] du projet PyCharm.
1. # une fonction
2. def f3():
3. print("f3")
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
92/755
3. # une fonction
4. def f4():
5. f3()
6. print("f4")
• ligne 1, on importe la fonction [f3] de [module3]. Ici, [module3] est visible parce qu’on a mis son dossier [dir1] dans les
[Sources Root] ;
• lignes 4-6 : on définit une fonction [f4] qui fait appel à la fonction [f3] de [module3] ;
1. # on importe module4
2. from module4 import f4
3. # exécution f4
4. f4()
• ligne 2, on importe le module [module4]. Celui-ci est visible car on a mis son dossier [dir2] dans les [Sources Root] de
PyCharm ;
• ligne 4 : exécution de la fonction [f4] de [module4] ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/import/04/main_01.py
2. f3
3. f4
4.
5. Process finished with exit code 0
Que s’est-il passé ? Le terminal Python n’a aucune connaissance du Python Path et des [Sources Root] de PyCharm. Il a son propre
Python Path. Dans celui-ci, on a toujours le dossier du script qui s’exécute, ici le script [main_01]. Il connaît donc le dossier
[import/04]. Dans le script exécuté, il trouve la ligne :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
93/755
1. from module4 import f4
L’interpréteur Python cherche [module4] dans les dossiers de son Python Path. Or [module4] ne se trouve pas dans [import/04] qui
se trouve bien dans le Python Path mais dans [import/04/dir2] qui ne s’y trouve pas. D’où l’erreur.
On a donc un problème déjà rencontré : un script s’exécutant correctement dans PyCharm peut planter dans le contexte d’un terminal
Python. C’est un problème récurrent qu’il va nous falloir résoudre.
Note : les dossiers [dir1] et [dir2] sont mis dans le Python Path. Remarquons déjà qu’il y a là un conflit : [module3] et [module4]
seront trouvés à deux endroits du Python Path de PyCharm :
• dans [import/04/dir1] et [import/05/dir1] pour [module3] ;
• dans [import/04/dir2] et [import/05/dir2] pour [module4] ;
On peut alors sortir [import/04/dir1] et [import/04/dir2] des [Sources Root] du projet PyCharm. Il se trouve qu’ici,
[import/05/dir1] est une copie de [import/04/dir1] (idem pour [dir2]) et qu’il n’y a donc pas de problème. Mais notons néanmoins
qu’à l’intérieur même de PyCharm, on doit faire attention à la liste des dossiers des [Sources Root] afin d’éviter les conflits.
1. import sys
2. # on modifie le sys.path pour y inclure les dossiers
3. # contenant les classes à importer
4. sys.path.append(".")
5. sys.path.append("./dir1")
6. sys.path.append("./dir2")
7. # on importe module4
8. from module4 import f4
9. # exécution f4
10. f4()
On cherche à résoudre le problème du Python Path. On en veut un qui fonctionne aussi bien sous PyCharm que dans un terminal
Python. Pour cela, on va le fixer nous-mêmes.
• lignes 4-6 : on ajoute les dossiers [., ./dir1, ./dir2] dans le Python Path. Pour que cela marche, il faut que le dossier
courant au moment de l’exécution soit le dossier [import/05]. Ce sera vrai dans PyCharm mais pas forcément vrai dans un
terminal Python comme nous le verrons ;
• ligne 8 : on importe [module4]. Suite à ce que nous venons de faire, il devrait être trouvé dans [./dir2] ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/import/05/main_01.py
2. f3
3. f4
4.
5. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
94/755
1. (venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\import\05>python main_01.py
2. f3
3. f4
1. (venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\import\05>cd ..
2.
3. (venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\import>python 05/main_01.py
4. Traceback (most recent call last):
5. File "05/main_01.py", line 8, in <module>
6. from module4 import f4
7. ModuleNotFoundError: No module named 'module4'
• ligne 2, lorsque [main_01] est exécuté on n’est plus dans le dossier [import/05] mais dans [import]. Or nous avons écrit :
1. sys.path.append(".")
2. sys.path.append("./dir1")
3. sys.path.append("./dir2")
Cela ajoute au Python Path les dossiers [import, import/dir1, import/dir2], pas du tout ce qu’on veut. Notons qu’ajouter au
Python Path des dossiers qui n’existent pas (import/dir1, import/dir2) ne provoque pas d’erreurs.
On a progressé mais ce n’est pas suffisant. Il faut ajouter au Python Path, non pas des chemins relatifs, mais des chemins absolus.
Le script [main_02] est une variante de [main_01] qui utilise un fichier de configuration [config.json] :
1. {
2. "dependencies": [
3. "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/import/05/dir1",
4. "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/import/05/dir2"
5. ]
6. }
La valeur de la clé [dependencies] est la liste des dossiers à ajouter au Python Path. A noter qu’ici on a mis des noms absolus et non
des noms relatifs.
1. import codecs
2. import json
3. import sys
4.
5. # fichier de configuration
6. config_filename="C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/import/05/config.json"
7. # lecture du fichier de configuration json
8. with codecs.open(config_filename, "r", "utf-8") as file:
9. config = json.load(file)
10.
11. # modification du sys.path
12. for directory in config['dependencies']:
13. sys.path.append(directory)
14.
15. # on importe module4
16. from module4 import f4
17. # exécution f4
18. f4()
19.
L’exécution donne les mêmes résultats que pour [main_02] sauf que le script continue à fonctionner lorsque le dossier d’exécution
n’est plus [import/05] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
95/755
1. (venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\import>python 05/main_02.py
2. f3
3. f4
Néanmoins, mettre des noms absolus dans des scripts n’est pas une solution. Dès que le projet est transporté sur un autre site, il ne
marche plus. Il nous faut trouver autre chose.
Note : les dossiers [06, dir1, dir2] ont été placés dans les [Sources Root] du projet PyCharm. Les dossiers [dir1, dir2] sont
identiques à ceux des exemples précédents.
1. {
2. "rootDir": "C:/Data/st-2020/dev/python/cours-2020/v-02/imports/06",
3. "relativeDependencies": [
4. "dir1",
5. "dir2"
6. ],
7. "absoluteDependencies": [
8. ]
9. }
1. # imports
7. import codecs
8. import json
9. import os
10. import sys
11.
12. # configuration de l'application
13. def config_app(config_filename: str) -> dict:
14. # config_filename : nom du fichier de configuration
15. # on laisse remonter les exceptions
16.
17. # exploitation du fichier de configuration
18. with codecs.open(config_filename, "r", "utf-8") as file:
19. config = json.load(file)
20. # ajout des dépendances au sys.path
21. rootDir = config['rootDir']
22. # on ajoute les dépendances relatives du projet au syspath
23. for directory in config['relativeDependencies']:
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
96/755
24. # on ajoute la dépendance au début du syspath
25. sys.path.insert(0, f"{rootDir}/{directory}")
26. # on lui ajoute les dépendances absolues du projet au syspath
27. for directory in config['absoluteDependencies']:
28. # on ajoute la dépendance au début du syspath
29. sys.path.insert(0, directory)
30. # on rend le dictionnaire de la configuration
31. return config
32.
33. # dossier du sript exécuté
34. def get_scriptdir():
35. return os.path.dirname(os.path.abspath(__file__))
1. # imports
2. import sys
3.
4. from utils import config_app
5.
6.
7. def affiche_path(msg: str):
8. # message
9. print(f"{msg}------------------------------")
10. # sys.path
11. for path in sys.path:
12. print(path)
13.
14.
15. # main -------------
16. try:
17. # le sys.path est configuré
18. affiche_path("avant....")
19. config = config_app(f"{get_scriptdir()}/config.json")
20. affiche_path("après....")
21. # on importe module4
22. from module4 import f4
23. # exécution f4
24. f4()
25. except BaseException as erreur:
26. print(f"L'erreur suivante s'est produite : {erreur}")
27. finally:
28. print("done")
• ligne 4 : la fonction [config_app] est importée. On notera que puisque [utils] et [main] sont dans le même dossier, cet
[import] fonctionne tout le temps. En effet, le dossier du script principal est automatiquement ajouté au python Path ;
• lignes 7-12 : la fonction [affiche_path] affiche la liste des dossiers du Python Path ;
• ligne 19 : on configure l’application. Noter qu’on passe à la fonction [config_app] le nom absolu du fichier de configuration.
Après cette instruction, le Python Path a été reconstruit ;
• ligne 22 : on importe [module4]. Grâce à la reconstruction du Python Path, ce module va être trouvé ;
• ligne 24 : on exécute la fonction [f4] ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/import/06/main.py
2. avant....------------------------------
3. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\import\06
4. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
97/755
5. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\fonctions\shared
6. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\v01\shared
7. …
8. C:\Program Files\Python38\python38.zip
9. C:\Program Files\Python38\DLLs
10. C:\Program Files\Python38\lib
11. C:\Program Files\Python38
12. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv
13. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages
14. après....------------------------------
15. C:/Data/st-2020/dev/python/cours-2020/v-02/imports/06/dir2
16. C:/Data/st-2020/dev/python/cours-2020/v-02/imports/06/dir1
17. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\import\06
18. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020
19. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\fonctions\shared
20. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\v01\shared
21. ….
22. C:\Program Files\Python38\python38.zip
23. C:\Program Files\Python38\DLLs
24. C:\Program Files\Python38\lib
25. C:\Program Files\Python38
26. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv
27. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages
28. f3
29. f4
30. done
31.
32. Process finished with exit code 0
Commentaires
• lignes 2-13 : le Python Path de PyCharm. On y retrouve tous les dossiers mis dans les [Sources Root] du projet ;
• lignes 14-29 : le Python Path construit par la fonction [config_app]. On y trouve aux lignes 15-16, les deux dépendances que
nous avons ajoutées ;
• lignes 22-27 : les dossiers système de l’interpréteur Python qui a exécuté le script ;
• lignes 28-29 : l’exécution se passe normalement ;
Maintenant, plaçons-nous dans le contexte qui jusqu’à maintenant provoquait une erreur d’exécution :
Cette fois-ci c’est bon (lignes 20-21). On notera que le [sys.path] ne contient pas les mêmes dossiers que lorsque l’exécution se passe
sous PyCharm.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
98/755
• nous remplaçons le fichier de configuration [config.json] par un script [config.py]. En effet, le fichier jSON pose un
problème important : il ne peut pas être commenté. Le dictionnaire [config.json] peut être remplacé par un dictionnaire
Python qui a l’avantage de pouvoir être commenté ;
• nous utilisons un module visible de tous les projets Python de la machine ;
Ci-dessus, nous créons un dossier [packages/myutils] dans le projet PyCharm (les noms n’importent pas).
1. # imports
2. import sys
3. import os
4.
5.
6. def set_syspath(absolute_dependencies: list):
7. # absolute_dependencies : une liste de noms absolus de dossiers
8.
9. # on ajoute au syspath les dépendances absolues du projet
10. for directory in absolute_dependencies:
11. # on vérifie l'existence du dossier
12. existe = os.path.exists(directory) and os.path.isdir(directory)
13. if not existe:
14. # on lève une exception
15. raise BaseException(f"[set_syspath] le dossier du Python Path [{directory}] n'existe pas")
16. else:
17. # on joute le dossier au début du syspath
18. sys.path.insert(0, directory)
• lignes 6-18 : la fonction [set_syspath] crée un Python Path avec la liste des dossiers qu’on lui transmet en paramètre ;
• lignes 12-15 : on vérifie que le dossier à mettre dans le Python Path existe ;
Le script [__init.py__] (deux soulignés devant et derrière le nom. Celui-ci est imposé) est le suivant :
On importe la fonction [set_syspath] du script [myutils]. La notation [.myutils] désigne le chemin [./myutils], donc le script
[myutils] se trouvant dans le même dossier que [__init.py]. On aurait pu utiliser la notation [myutils]. Seulement, nous allons
créer un module [myutils] de portée machine. Si bien que la notation [from myutils import set_syspath] deviendrait alors ambigüe.
S’agit-il d’importer le script [myutils] du dossier courant ou le script [myutils] de portée machine ? La notation [.myutils] lève
cette ambiguïté.
Dans ce script, on décrit le module qu’on va créer. Ici, nous allons le créer localement. Mais le même processus est utilisé pour créer
un module distribué officiellement (cf. |pypi|). Les points importants sont ici les suivants :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
99/755
• ligne 4 : la version du module ;
• ligne 5 : sa description ;
• lignes 7-8 : l’auteur du module ;
Pour installer ce module avec une portée machine, on procède de la façon suivante :
A partir de maintenant, tout script de la machine peut importer le module [myutils] sans que celui-ci soit dans les codes du projet.
1. def configure():
2. import os
3.
4. # nom absolu du dossier du script de configuration
5. script_dir = os.path.dirname(os.path.abspath(__file__))
6. # chemins absolus des dossiers à mettre dans le syspath
7. absolute_dependencies = [
8. # dossiers locaux
9. f"{script_dir}/dir1",
10. f"{script_dir}/dir2",
11. ]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
100/755
12. # mise à jour du syspath
13. from myutils import set_syspath
14. set_syspath(absolute_dependencies)
15.
16. # on retourne la config
17. return {}
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. from module4 import f4
8.
9. # main -------------
10. try:
11. f4()
12. except BaseException as erreur:
13. print(f"L'erreur suivante s'est produite : {erreur}")
14. finally:
15. print("done")
• lignes 2-4 : on configure l’application à l’aide du module [config.py]. Celui-ci est accessible car il est dans le même dossier
que le script principal. Or le dossier du script principal fait toujours partie du Python Path ;
• lorsqu’on arrive à la ligne 6, le Python Path a été construit avec dedans le dossier du module [module4]. On peut donc importer
celui-ci ligne 7 ;
• lignes 10-15 : il ne reste plus qu’à exécuter la fonction [f4] ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/import/07/main.py
2. f3
3. f4
4. done
5.
6. Process finished with exit code 0
Dans un terminal Python et ailleurs que dans le dossier du script principal, les résultats sont les suivants :
Désormais nous procèderons toujours de la même façon pour configurer une application :
• présence d’un script [config.py] dans le dossier du script principal. Ce script contient une fonction [configure] qui a deux
objectifs :
o construire le Python Path pour l’application. Pour cela, [config.py] déclare tous les dossiers contenant les modules
utilisés par l’application et construit le Python Path avec leurs noms absolus ;
o construire le dictionnaire [config] de la configuration de l’application ;
Nous appliquons ce schéma à la seconde version de l’exercice d’application. On se souvient en effet que la version 1 fonctionnait
dans l’environnement PyCharm mais pas dans un terminal Python. Le problème venait du Python Path.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
101/755
10 Exercice d’application : version 2
1. def configure():
2. import os
3.
4. # chemin absolu du dossier de ce script
5. script_dir = os.path.dirname(os.path.abspath(__file__))
6.
7. # racine à partir de laquelle vont être mesurés certains chemins relatifs
8. root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots"
9.
10. # dépendances de l'application
11. absolute_dependencies = [
12. f"{root_dir}/v01/shared",
13. ]
14.
15. # configuration de l'application
16. config = {
17. # chemin absolu du fichier des contribuables
18. "taxpayersFilename": f"{script_dir}/../data/taxpayersdata.txt",
19. # chemins absolu du fichier des résultats
20. "resultsFilename": f"{script_dir}/../data/résultats.txt"
21. }
22.
23. # mise à jour du syspath
24. from myutils import set_syspath
25. set_syspath(absolute_dependencies)
26.
27. # on rend la config
28. return config
Commentaires
• ligne 5 : on récupère le nom absolu du dossier qui contient le script qui s’exécute, ici le script [config.py]. On obtient donc
le nom absolu du dossier [main]. C’est également ce dossier qui contient le script principal [main.py] ;
• ligne 8 : lorsqu’un fichier référencé n’appartient pas au dossier de l’application, on n’utilisera pas [script_dir] pour le localiser
mais [root_dir]. Cette ligne devra être changée dès que l’application change de place dans le système de fichiers ;
• lignes 11-13 : on liste les noms absolus de tous les dossiers qui doivent se trouver dans le Python Path pour que l’application
fonctionne. Ligne 12, on référence le dossier [shared] de la version 1 de l’exercice d’application ;
• lignes 16-21 : définissent la configuration de l’application dans un dictionnaire [config]. Ici on inscrit les chemins absolus des
fichiers texte manipulés par l’application. Pour cela, on utilise [script_dir] qui rappelons-le, désigne ici le dossier [main] ;
• lignes 24-25 : on fixe le Python Path nécessaire à l’application ;
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. from impôts_module_01 import calcul_impôt, record_results, get_taxpayers_data
8.
9. # fichier des contribuables
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
102/755
10. taxpayers_filename = config['taxpayersFilename']
11. # fichier des résultats
12. results_filename = config['resultsFilename']
13.
14. # code
15. try:
16.
17. # lecture des données contribuables
18. taxpayers = get_taxpayers_data(taxpayers_filename)
19. # liste des résultats
20. results = []
21. # on calcule l'impôt des contribuables
22. for taxpayer in taxpayers:
23. # le calcul de l'impôt renvoie un dictionnaire de clés
24. # ['marié', 'enfants', 'salaire', 'impôt', 'surcôte', 'décôte', 'réduction', 'taux']
25. result = calcul_impôt(taxpayer['marié'], taxpayer['enfants'], taxpayer['salaire'])
26. # le dictionnaire est ajouté à la liste des résultats
27. results.append(result)
28. # on enregistre les résultats
29. record_results(results_filename, results)
30. except BaseException as erreur:
31. # il peut y avoir différentes erreurs : absence de fichier, contenu de fichier incorrect
32. # on affiche l'erreur et on quitte l'application
33. print(f"L'erreur suivante s'est produite : {erreur}]\n")
34. finally:
35. print("Travail terminé...")
Commentaires
La version 1 ne marchait pas dans une console Python. Dans cette même console, la version 2 donne les résultats suivants :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
103/755
11 Exercice d’application : version 3
• les données nécessaires au calcul de l'impôt et fournies par l'administration fiscale sont placées dans un fichier
jSON [admindata.json]:
1. {
2. "limites": [9964, 27519, 73779, 156244, 0],
3. "coeffR": [0, 0.14, 0.3, 0.41, 0.45],
4. "coeffN": [0, 1394.96, 5798, 13913.69, 20163.45],
5. "PLAFOND_QF_DEMI_PART": 1551,
6. "PLAFOND_REVENUS_CELIBATAIRE_POUR_REDUCTION": 21037,
7. "PLAFOND_REVENUS_COUPLE_POUR_REDUCTION": 42074,
8. "VALEUR_REDUC_DEMI_PART": 3797,
9. "PLAFOND_DECOTE_CELIBATAIRE": 1196,
10. "PLAFOND_DECOTE_COUPLE": 1970,
11. "PLAFOND_IMPOT_COUPLE_POUR_DECOTE": 2627,
12. "PLAFOND_IMPOT_CELIBATAIRE_POUR_DECOTE": 1595,
13. "ABATTEMENT_DIXPOURCENT_MAX": 12502,
14. "ABATTEMENT_DIXPOURCENT_MIN": 437
15. }
• les résultats du calcul de l'impôt seront eux également placés dans un fichier jSON [résultats.json] :
1. [
2. {
3. "marié": "oui",
4. "enfants": 2,
5. "salaire": 55555,
6. "impôt": 2814,
7. "surcôte": 0,
8. "décôte": 0,
9. "réduction": 0,
10. "taux": 0.14
11. },
12. {
13. "marié": "oui",
14. "enfants": 2,
15. "salaire": 50000,
16. "impôt": 1384,
17. "surcôte": 0,
18. "décôte": 384,
19. "réduction": 347,
20. "taux": 0.14
21. },
22. …
23. {
24. "marié": "oui",
25. "enfants": 3,
26. "salaire": 200000,
27. "impôt": 42842,
28. "surcôte": 17283,
29. "décôte": 0,
30. "réduction": 0,
31. "taux": 0.41
32. }
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
104/755
33. ]
1. def configure():
2. import os
3.
4. # chemin absolu du dossier de ce script
5. script_dir = os.path.dirname(os.path.abspath(__file__))
6. # dépendances de l'application
7. absolute_dependencies = [
8. f"{script_dir}/../shared",
9. ]
10. # configuration de l'application
11. config = {
12. # chemin absolu du fichier des contribuables
13. "taxpayersFilename": f"{script_dir}/../data/taxpayersdata.txt",
14. # chemin absolu du fichier des résultats
15. "resultsFilename": f"{script_dir}/../data/résultats.json",
16. # chemin absolu du fichier des données de l'administration fiscale
17. "admindataFilename": f"{script_dir}/../data/admindata.json"
18. }
19. # mise à jour du syspath
20. from myutils import set_syspath
21.
22. set_syspath(absolute_dependencies)
23.
24. # on rend la config
25. return config
• ligne 8 : on met le dossier [shared] dans le Python Path. Ce dossier contient le module [impôts_module_02] utilisé par le
script principal ;
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. from impôts_module_02 import calcul_impôt, get_admindata, get_taxpayers_data, record_results_in_json_file
8.
9. # fichier des contribuables
10. taxpayers_filename = config['taxpayersFilename']
11. # fichier des résultats
12. results_filename = config['resultsFilename']
13. # fichier des données de l'administration fiscale
14. admindata_filename = config['admindataFilename']
15. # code
16. try:
17.
18. # lecture des données de l'administration fiscale
19. admindata = get_admindata(admindata_filename)
20. # lecture des données contribuables
21. taxpayers = get_taxpayers_data(taxpayers_filename)
22. # liste des résultats
23. results = []
24. # on calcule l'impôt des contribuables
25. for taxpayer in taxpayers:
26. # le calcul de l'impôt renvoie un dictionnaire de clés
27. # ['marié', 'enfants', 'salaire', 'impôt', 'surcôte', 'décôte', 'réduction', 'taux']
28. result = calcul_impôt(admindata, taxpayer['marié'], taxpayer['enfants'], taxpayer['salaire'])
29. # le dictionnaire est ajouté à la liste des résultats
30. results.append(result)
31. # on enregistre les résultats
32. record_results_in_json_file(results_filename, results)
33. except BaseException as erreur:
34. # il peut y avoir différentes erreurs : absence de fichier, contenu de fichier incorrect
35. # on affiche l'erreur et on quitte l'application
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
105/755
36. print(f"L'erreur suivante s'est produite : {erreur}]\n")
37. finally:
38. print("Travail terminé...")
Notes
• on retrouve dans le module des fonctions déjà présentes dans le module utilisé par la version 1 avec cependant une différence.
Lorsque le module de la version 2 reprend une fonction présente dans le module de la version 1, elle le fait avec un paramètre
supplémentaire : [adminData] (lignes 29, 51, 77, 127). Ce paramètre représente le dictionnaire des données fiscales issues du
fichier jSON [adminData.json]. Dans le module de la version 1, ces données n'avaient pas besoin d'être passées aux fonctions
car elles étaient définies globalement à celles-ci ce qui faisait que les fonctions les connaissaient ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
106/755
1. # écriture des résultats dans un fichier jSON
2. # ----------------------------------------
3. def record_results_in_json_file(results_filename: str, results: list):
4. file = None
5. try:
6. # ouverture du fichier des résultats
7. file = codecs.open(results_filename, "w", "utf8")
8. # écriture en bloc
9. json.dump(results, file, ensure_ascii=False)
10. finally:
11. # on ferme le fichier s'il a été ouvert
12. if file:
13. file.close()
Notes
• là où [calcul_impôt] appelle d'autres fonctions, elle passe [admin_data] en 1er paramètre (lignes 10, 14, 39, 42) ;
• là où [calcul_impôt] utilise des constantes fiscales, elle passe désormais par le dictionnaire [admin_data] (lignes 19, 22) ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
107/755
Toutes les fonctions recevant [admin_data] comme paramètre, subissent ces mêmes types de modifications.
11.7 Résultats
Les résultats obtenus sont ceux présentés au début du paragraphe 8.3.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
108/755
12 Les classes et objets
La classe est le moule à partir duquel sont fabriqués des objets. On dit de l'objet que c'est l'instance d'une classe.
Note : le dossier [shared] a été placé dans les [Sources Root] du projet.
1. # une classe
2. class Objet(object):
3. """une classe Objet vide"""
4.
5.
6. # toute variable de type [Objet] peut avoir des attributs par construction
7. obj1 = Objet()
8. obj1.attr1 = "un"
9. obj1.attr2 = 100
10. # affiche l'objet
11. print(f"objet1=[{obj1}, {type(obj1)},{id(obj1)},{obj1.attr1},{obj1.attr2}]")
12. # modifie l'objet
13. obj1.attr2 += 100
14. # affiche l'objet
15. print(f"objet1=[{obj1.attr1},{obj1.attr2}]")
16. # affecte la référence obj1 à obj2
17. obj2 = obj1
18. # modifie l'objet pointé par obj2
19. obj2.attr2 = 0
20. # affiche les deux objets - obj1 pointe maintenant sur un objet modifié
21. print(f"objet1=[{obj1.attr1},{obj1.attr2}]")
22. print(f"objet2=[{obj2.attr1},{obj2.attr2}]")
23. # obj1 et obj2 pointent sur le même objet
24. print(f"objet1=[{obj1}, {id(obj1)},{obj1.attr1},{obj1.attr2}]")
25. print(f"objet2=[{obj2}, {id(obj2)},{obj2.attr1},{obj2.attr2}]")
26. print(obj1 == obj2)
27. # type de l'instance obj1
28. print(f"type(obj1)={type(obj1)}")
29. print(f"isinstance(obj1,Objet)={isinstance(obj1, Objet)}, isinstance(obj1,object)={isinstance(obj1,
object)}")
30. # tout type est un objet en Python
31. print(f"type(4)={type(4)}")
32. print(f"isinstance(4, int)={isinstance(4, int)}, isinstance(4, object)={isinstance(4, object)}")
Notes :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
109/755
• lignes 8-9 : initialisation directe de deux attributs de l'objet ;
• ligne 17 : copie de références. Les variables obj1 et obj2 sont deux pointeurs (références) sur un même objet ;
• ligne 19 : on modifie l'objet pointé par [obj2]. Comme [obj1] et [obj2] pointent sur le même objet, les affichages des objets
[obj1, obj2] des lignes 21 et 22 vont montrer que l'objet pointé par [obj1] a changé ;
• lignes 24-26 : ces lignes visent à montrer l'égalité des variables [obj1] et [obj2]. L'affichage de la ligne 26 va le montrer. Dans
cette comparaison, ce sont les adresses [obj1] et [obj2] qui sont égales ;
• chaque objet Python est identifié par un n° unique qu'on obtient avec l'expression [id(objet)]. Les lignes 24 et 25 vont
montrer que les n°s des objets pointés par [obj1] et [obj2] sont identiques, montrant par là que ces deux références pointent
sur le même objet ;
• lignes 27-29 : la fonction [isinstance(expr,Type)] rend le booléen True si l'expression [expr] est de type [Type]. Ici, on va
voir que [obj1] est de type [Objet], ce qui semble naturel, mais également de type [object]. La classe [object] est la classe
parent de toutes les classes Python. Par propriété de l'héritage de classes, une classe fille F a toutes les propriétés de sa classe
parent P et la fonction [isinstance(instance de F, P)] rend True ;
• lignes 30-32 : montrent que type [int] est lui aussi un type [object]. Tous les types de Python dérivent de la classe [object] ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/classes/01/classes_01.py
2. objet1=[<__main__.Objet object at 0x0000025C3F469BB0>, <class '__main__.Objet'>,2595221838768,un,100]
3. objet1=[un,200]
4. objet1=[un,0]
5. objet2=[un,0]
6. objet1=[<__main__.Objet object at 0x0000025C3F469BB0>, 2595221838768,un,0]
7. objet2=[<__main__.Objet object at 0x0000025C3F469BB0>, 2595221838768,un,0]
8. True
9. type(obj1)=<class '__main__.Objet'>
10. isinstance(obj1,Objet)=True, isinstance(obj1,object)=True
11. type(4)=<class 'int'>
12. isinstance(4, int)=True, isinstance(4, object)=True
13.
14. Process finished with exit code 0
1. # classe Personne
2. class Personne:
3. # attributs de la classe
4. # non déclarés - peuvent être créés dynamiquement
5.
6. # méthode
7. def identité(self: object) -> str:
8. # a priori, utilise des attributs inexistants
9. return f"[{self.prénom},{self.nom},{self.âge}]"
10.
11.
12. # ---------------------------------- main
13. # les attributs sont publics et peuvent être créés dynamiquement
14. # instanciation classe
15. p = Personne()
16. # initialisation directe d'attributs de la classe
17. p.prénom = "Paul"
18. p.nom = "de la Hûche"
19. p.âge = 48
20. # appel d'une méthode de la classe
21. print(f"personne={p.identité()}")
22. # type de l'instance p
23. print(f"type(p)={type(p)}")
24. print(f"isinstance(Personne)={isinstance(p, Personne)}, isinstance(object)={isinstance(p, object)}")
Notes :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
110/755
• lignes 16-19 : montrent que les attributs de l'objet peuvent être créés dynamiquement (ils n'existent pas dans la définition de
la classe) ;
• ligne 9 : les attributs de la classe sont désignés par la notation [self.attribut] ;
• les lignes 23-24 vont montrer que l'objet [p] est à la fois une instance de la classe [Personne] et de la classe [object] ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/classes/01/classes_02.py
2. personne=[Paul,de la Hûche,48]
3. type(p)=<class '__main__.Personne'>
4. isinstance(Personne)=True, isinstance(object)=True
5.
6. Process finished with exit code 0
1. # classe Personne
2. class Personne:
3. # constructeur - initialise trois attributs
4. def __init__(self: object, prénom: str, nom: str, âge: int):
5. # prénom : prénom de la personne
6. # nom : nom de la personne
7. # âge : âge de la personne
8.
9. self.prénom = prénom
10. self.nom = nom
11. self.âge = âge
12.
13. # rend les attributs de la classe sous la forme d'une chaîne de caractères
14. def identité(self: object) -> str:
15. return f"[{self.prénom},{self.nom},{self.âge}]"
16.
17.
18. # ---------------------------------- main
19. # un objet Personne
20. p = Personne("Paul", "de la Hûche", 48)
21. # appel d'une méthode
22. print(f"personne={p.identité()}")
Notes :
• ligne 4 : le constructeur d'une classe s'appelle __init__. Comme pour les autres méthodes, son premier paramètre est self ;
• ligne 20 : un objet Personne est construit avec le constructeur de la classe ;
• lignes 13-15 : la méthode [identité] rend une chaîne de caractères représentant le contenu de l'objet ;
• ligne 22 : affichage de l'identité de la personne ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/classes/01/classes_03.py
2. personne=[Paul,de la Hûche,48]
3.
4. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
111/755
1. class Utils:
2. # méthode statique
3. @staticmethod
4. def is_string_ok(string: str) -> bool:
5. # string est-elle une chaîne
6. erreur = not isinstance(string, str)
7. if not erreur:
8. # la chaîne est-elle vide ?
9. erreur = string.strip() == ''
10. # résultat
11. return not erreur
Notes
• ligne 3 : l'annotation [@staticmethod] indique que la méthode ainsi annotée est une méthode de classe et non une méthode
d'instance. Cela se voit au fait que le premier paramètre de la méthode ainsi annotée n'est pas le mot clé [self]. Ainsi la
méthode statique n'a pas accès aux attributs de l'objet. Au lieu d'écrire :
1. u=Utils()
2. print(u.is_string_ok("abcd")
on écrit
1. print(Utils.is_string_ok("abcd")
Parce que ci-dessus on a écrit [Utils.is_string_ok], la méthode [is_string_ok] est dite une méthode de classe (la classe Utils ici).
Pour cela, la méthode [Utils.is_string_ok] doit être annotée avec le mot clé [@staticmethod].
La méthode statique [Utils.is_string_ok] permet ici de vérifier qu'une donnée est une chaîne de caractères non vide.
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. from utilitaires import Utils
8.
9. print(Utils.is_string_ok(" "))
10. print(Utils.is_string_ok(47))
11. print(Utils.is_string_ok(" q "))
1. def configure():
2. import os
3.
4. # dossier du fichier de configuration
5. script_dir = os.path.dirname(os.path.abspath(__file__))
6.
7. # chemins absolus des dossiers à mettre dans le syspath
8. absolute_dependencies = [
9. f"{script_dir}/shared",
10. "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/venv/lib/site-packages",
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
112/755
11. "C:/myprograms/Python38/lib",
12. "C:/myprograms/Python38/DLLs"
13. ]
14.
15. # mise à jour du syspath
16. from myutils import set_syspath
17. set_syspath(absolute_dependencies)
18.
19. # on retourne la config
20. return {}
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/classes/01/classes_04.py
2. False
3. False
4. True
5.
6. Process finished with exit code 0
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. from utilitaires import Utils
8.
9.
10. # une classe d'exception propriétaire dérivant de [BaseException]
11. class MyException(BaseException):
12. # on ne fait rien : classe vide
13. pass
14.
15.
16. # classe Personne
17. class Personne:
18. # constructeur
19. def __init__(self: object, prénom: str = "x", nom: str = "y", âge: int = 0):
20. # prénom : prénom de la personne
21. # nom : nom de la personne
22. # âge : âge de la personne
23.
24. # mémorisation des paramètres
25. # les initialisations seront faites via les setters
26. self.prénom = prénom
27. self.nom = nom
28. self.âge = âge
29.
30. # méthode toString de la classe
31. def __str__(self: object) -> str:
32. return f"[{self.__prénom},{self.__nom},{self.__âge}]"
33.
34. # getters
35. @property
36. def prénom(self) -> str:
37. return self.__prénom
38.
39. @property
40. def nom(self) -> str:
41. return self.__nom
42.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
113/755
43. @property
44. def âge(self) -> int:
45. return self.__âge
46.
47. # setters
48. @prénom.setter
49. def prénom(self, prénom: str):
50. # le prénom doit être non vide
51. if Utils.is_string_ok(prénom):
52. self.__prénom = prénom.strip()
53. else:
54. raise MyException("Le prénom doit être une chaîne de caractères non vide")
55.
56. @nom.setter
57. def nom(self, nom: str):
58. # le prénom doit être non vide
59. if Utils.is_string_ok(nom):
60. self.__nom = nom.strip()
61. else:
62. raise MyException("Le nom doit être une chaîne de caractères non vide")
63.
64. @âge.setter
65. def âge(self, âge: int):
66. # l'âge doit être un entier >=0
67. erreur = False
68. if isinstance(âge, int):
69. if âge >= 0:
70. self.__âge = âge
71. else:
72. erreur = True
73. else:
74. erreur = True
75. # erreur ?
76. if erreur:
77. raise MyException("L'âge doit être un entier >=0")
78.
79.
80. # ---------------------------------- main
81. # un objet Personne
82. try:
83. # instanciation classe Personne
84. p = Personne("Paul", "de la Hûche", 48)
85. # affichage objet p
86. print(f"personne={p}")
87. except MyException as erreur:
88. # affichage erreur
89. print(erreur)
90.
91. # un autre objet Personne
92. try:
93. # instanciation classe Personne
94. p = Personne("xx", "yy", "zz")
95. # affichage objet p
96. print(f"personne={p}")
97. except MyException as erreur:
98. # affichage erreur
99. print(erreur)
100.
101. # une autre personne sans paramètres cette fois
102. try:
103. # instanciation classe Personne
104. p = Personne()
105. # affichage objet p
106. print(f"personne={p}")
107. except MyException as erreur:
108. # affichage msg d'erreur
109. print(erreur)
110.
111. # on ne peut pas accéder aux attributs privés __attr de la classe
112. p.__prénom = "Gaëlle"
113. print(f"p.prénom={p.prénom}")
114. print(f"p.__prénom={p.__prénom}")
115. p.prénom = "Sébastien"
116. print(f"p.prénom={p.prénom}")
117. print(f"p.__prénom={p.__prénom}")
Notes :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
114/755
• lignes 10-13 : une classe MyException dérivée de la classe BaseException (nous verrons ce point un peu plus loin). Elle n'ajoute
aucune fonctionnalité à cette dernière. Elle n'est là que pour avoir une exception propriétaire ;
• ligne 19 : le constructeur a des valeurs par défaut pour ses paramètres. Ainsi l'opération p=Personne() est équivalente à
p=Personne("x","y",0) ;
• lignes 34-45 : les propriétés de la classe. Ce sont des méthodes annotées avec le mot clé [@property]. Elles sont utilisées pour
rendre la valeur des attributs ;
• lignes 47-77 : les setters de la classe. Ce sont des méthodes annotées avec le mot clé [@attributsetter]. Elles sont utilisées
pour fixer la valeur des attributs ;
• lignes 48-54 : le setter de l'attribut [prénom]. Cette méthode sera appelée chaque fois qu'on affectera une valeur à l'attribut
[prénom] :
1. p=Personne(…)
2. p.prénom=valeur
La ligne 2 provoquera l'appel [p.prénom(valeur)]. L'intérêt de passer par un setter pour affecter une valeur à un attribut
est que le setter étant une fonction, on peut vérifier la validité de la valeur affectée à l'attribut ;
• ligne 51 : on vérifie que la valeur affectée à l'attribut [prénom] est une chaîne de caractères non vide. On utilise pour cela la
méthode statique [Utils.isStringOk] vue précédemment ;
• ligne 52 : la valeur affectée à l'attribut [prénom] est débarrassée de ces "blancs" de début / fin de chaîne et affectée à l'attribut
[self.__prénom]. Ce n'est donc pas l'attribut [prénom] qui est utilisé ici. On ne pouvait pas sinon on aurait eu un appel
récursif infini. On pouvait utiliser n'importe quel nom d'attribut. Le fait d'avoir utilisé l'attribut [__prénom] avec deux
underscores en début de l'identificateur a une signification spéciale : les attributs précédés de deux underscores sont privés
à la classe. Cela signifie qu'ils ne sont pas visibles de l'extérieur de celle-ci. On ne peut donc écrire :
1. p=Personne(…)
2. p.__prénom=valeur
En fait on verra bientôt qu'on peut l'écrire mais que ça ne modifie pas le prénom. Ça fait autre chose ;
• lignes 53-54 : si la valeur affectée au prénom n'est pas correct, on lance une exception. Ainsi le code appelant saura que son
appel est incorrect ;
• lignes 35-37 : la propriété [prénom]. Elle sera appelée à chaque fois qu'on écrira [p.prénom] dans une expression. C'est alors
la méthode [p.prénom()] qui sera appelée. Ligne 37, on rend la valeur de l'attribut [__prénom] puisqu'on a vu que le setter
de l'attribut [prénom] affectait sa valeur à l'attribut privé [__prénom] ;
• lignes 56-62 : le setter de l'attribut [nom] est construit de façon analogue à celui de l'attribut [prénom]. Il en est de même
pour celui de l'attribut [âge] aux lignes 64-77 ;
• bien que les propriétés [prénom, nom, valeur] ne soient pas les véritables attributs qui sont en réalité [__prénom, __nom,
__âge], on continuera à les appeler les attributs de la classe, car elles s'utilisent comme tels ;
• lignes 19-28 : le constructeur de la classe utilise de façon implicite les setters des attributs [prénom, nom, âge]. En effet, en
écrivant, ligne 26, [self.prénom = prénom], c'est implicitement la méthode [prénom(self, prénom)] qui va être appelée. La
validité du paramètre [prénom] va alors être vérifiée. Il va en être de même pour les deux autres attributs [nom, âge] ;
• avec ce modèle, on ne peut attribuer de valeurs incorrectes aux attributs [prénom, nom, âge] de la classe ;
• lignes 30-32 : la fonction __str__ remplace la méthode qui s'appelait identité précédemment. Le nom [__str__] (2
underscores devant et derrière) n'est pas anodin. On va le voir par la suite ;
• lignes 83-86 : instanciation d'une personne puis affichage de son identité ;
• ligne 84 : instanciation ;
• ligne 86 : affichage. L'opération demande d'afficher la personne p sous la forme d'une chaîne de caractères. L'interpréteur
Python appelle alors automatiquement la méthode p.__str__() si elle existe. Cette méthode joue le même rôle que la
méthode toString() en Java ou dans les langages .NET ;
• lignes 87-89 : gestion d'une éventuelle exception de type MyException. Affiche alors l'erreur ;
• lignes 91-99 : idem pour une deuxième personne instanciée avec des paramètres erronés ;
• lignes 102-109 : idem pour une troisième personne instanciée avec les paramètres par défaut : on ne passe aucun paramètre.
Ce sont alors les valeurs par défaut de ces paramètres dans le constructeur qui sont ici utilisées ;
• lignes 112-117 : on a dit que l'attribut [__prénom] était privé donc normalement non accessible de l'extérieur de la classe.
On veut le vérifier ;
• lignes 112-114 : on affecte une valeur à l'attribut [__prénom] puis on vérifie la valeur des attributs [__prénom] et [prénom]
qui normalement sont les mêmes ;
• lignes 115-117 : on recommence l'opération en initialisant cette fois l'attribut [prénom] ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/classes/01/classes_05.py
2. personne=[Paul,de la Hûche,48]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
115/755
3. L'âge doit être un entier >=0
4. personne=[x,y,0]
5. p.prénom=x
6. p.__prénom=Gaëlle
7. p.prénom=Sébastien
8. p.__prénom=Gaëlle
9.
10. Process finished with exit code 0
Notes
• lignes 5-6 : on voit que l'affectation [p.__prénom = "Gaëlle"] n'a pas changé la valeur de l'attribut [prénom], ligne 5 ;
• lignes 7-8 : on voit que l'affectation [p.prénom = "Sébastien"] n'a pas changé la valeur de l'attribut [__prénom], ligne 8 ;
Que faut-il en déduire ? Que probablement, l'opération [p.__prénom = "Gaëlle"] a créé un attribut public [__prénom] à la classe
mais que celui-ci est différent de l'attribut privé [__prénom] manipulé au sein de celle-ci ;
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. from utilitaires import Utils
8.
9.
10. # une classe d'exception propriétaire dérivant de [BaseException]
11. class MyException(BaseException):
12. # on ne fait rien : classe vide
13. pass
14.
15.
16. # classe Personne
17. class Personne:
18. # constructeur
19. def __init__(self: object, prénom: str = "x", nom: str = "y", âge: int = 0):
20. # prénom : prénom de la personne
21. # nom : nom de la personne
22. # âge : âge de la personne
23.
24. # mémorisation des paramètres
25. # les initialisations seront faites via les setters
26. self.prénom = prénom
27. self.nom = nom
28. self.âge = âge
29.
30. # autre méthode d'initialisation
31. def init_with_personne(self: object, p: object):
32. # initialise l'objet courant avec une personne p
33. self.__init__(p.prénom, p.nom, p.âge)
34.
35. # méthode toString de la classe
36. def __str__(self: object) -> str:
37. return f"[{self.__prénom},{self.__nom},{self.__âge}]"
38.
39. # getters
40. …
41.
42. # setters
43. …
44.
45.
46. # ---------------------------------- main
47. # un objet Personne
48. try:
49. # instanciation classe Personne
50. p = Personne("Paul", "de la Hûche", 48)
51. # affichage objet p
52. print(f"personne={p}")
53. except MyException as erreur:
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
116/755
54. # affichage msg d'erreur
55. print(erreur)
56.
57. # un autre objet Personne
58. try:
59. # instanciation classe Personne
60. p = Personne("xx", "yy", "zz")
61. # affichage objet p
62. print(f"p={p}")
63. except MyException as erreur:
64. # affichage msg d'erreur
65. print(erreur)
66.
67. # une autre personne sans paramètres cette fois
68. try:
69. # instanciation classe Personne
70. p = Personne()
71. # affichage objet p
72. print(f"p={p}")
73. except MyException as erreur:
74. # affichage msg d'erreur
75. print(erreur)
76.
77. # une autre Personne obtenue par recopie
78. try:
79. # instanciation classe Personne
80. p2 = Personne()
81. p2.init_with_personne(p)
82. # affichage objet p2
83. print(f"p2={p2}")
84. except MyException as erreur:
85. # affichage msg d'erreur
86. print(erreur)
Notes :
• la différence avec le script précédent est en lignes 30-33. On a rajouté la méthode initWithPersonne. Celle-ci fait appel au
constructeur __init__. Il n'y a pas possibilité d'avoir, comme dans les langages typés, des méthodes de même nom différenciées
par la nature de leurs paramètres ou de leur résultat. Il n'y a donc pas possibilité d'avoir plusieurs constructeurs qui
construiraient l'objet à partir de paramètres différents, ici un objet de type Personne ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/classes/01/classes_06.py
2. personne=[Paul,de la Hûche,48]
3. L'âge doit être un entier >=0
4. p=[x,y,0]
5. p2=[x,y,0]
6.
7. Process finished with exit code 0
code :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
117/755
Le script [classes_07] montre qu'on peut avoir une liste d'objets :
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. from myclasses import Personne
8.
9. # ---------------------------------- main
10. # création d'une liste d'objets personne
11. groupe = [Personne("Paul", "Langevin", 48), Personne("Sylvie", "Lefur", 70)]
12. # identité de ces personnes
13. for i in range(len(groupe)):
14. print(f"groupe[{i}]={groupe[i]}")
Notes :
Résultats
1. C:\Users\serge\.virtualenvs\cours-python-v02\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/v-
02/classes/01/classes_07.py
2. groupe[0]=[Paul,Langevin,48]
3. groupe[1]=[Sylvie,Lefur,70]
4.
5. Process finished with exit code 0
1. # classe Enseignant
2. class Enseignant(Personne):
3. # constructeur
4. def __init__(self, prénom: str = "x", nom: str = "x", âge: int = 0, discipline: str = "x"):
5. # prénom : prénom de la personne
6. # nom : nom de la personne
7. # âge : âge de la personne
8. # discipline : discpline enseignée
9.
10. # initialisation du parent
11. Personne.__init__(self, prénom, nom, âge)
12. # autres initialisations
13. self.discipline = discipline
14.
15. # toString
16. def __str__(self) -> str:
17. return f"enseignant[{super().__str__()},{self.discipline}]"
18.
19. # propriétés
20. @property
21. def discipline(self) -> str:
22. return self.__discipline
23.
24. @discipline.setter
25. def discipline(self, discipline: str):
26. # la discipline doit être une chaîne non vide
27. if Utils.is_string_ok(discipline):
28. self.__discipline = discipline
29. else:
30. raise MyException("La discipline doit être une chaîne de caractères non vide")
• ligne 2 : déclare la classe Enseignant comme étant une classe dérivée de la classe Personne. Une classe dérivée a toutes les
propriétés (attributs et méthodes) de sa classe parent plus celles qui lui sont propres ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
118/755
• ligne 13 : la classe [Enseignant] définit un nouvel attribut [discipline] ;
• ligne 11 : le constructeur de la classe dérivée Enseignant doit appeler le constructeur de la classe parent Personne en lui
transmettant les paramètres qu'il attend ;
• ligne 17 : la fonction [super()] rend la classe parent. Ici on appelle la fonction [__str__] de la classe parent ;
• lignes 19-30 : on définit le getter et setter du nouvel attribut [discipline] ;
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. from myclasses import Personne, Enseignant
8.
9. # ---------------------------------- main
10. # création d'un tableau d'objets Personne et dérivés
11. groupe = [Enseignant("Paul", "Langevin", 48, "anglais"), Personne("Sylvie", "Lefur", 70)]
12. # identité de ces personnes
13. for i in range(len(groupe)):
14. print(f"groupe[{i}]={groupe[i]}")
Notes :
• ligne 7 : on importe les classes [Personne] et [Enseignant] définies dans le fichier [myclasses.py] ;
• lignes 11-14 : on définit un groupe de personnes dont on affiche ensuite l'identité ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/classes/01/classes_08.py
2. groupe[0]=enseignant[[Paul,Langevin,48],anglais]
3. groupe[1]=[Sylvie,Lefur,70]
4.
5. Process finished with exit code 0
1. # classe Etudiant
2. class Etudiant(Personne):
3. # constructeur
4. def __init__(self: object, prénom: str = "x", nom: str = "y", âge: int = 0, formation: str = "x"):
5. Personne.__init__(self, prénom, nom, âge)
6. self.formation = formation
7.
8. # toString
9. def __str__(self: object) -> str:
10. return f"étudiant[{super().__str__()},{self.formation}]"
11.
12. # propriétés
13. @property
14. def formation(self) -> str:
15. return self.__formation
16.
17. @formation.setter
18. def formation(self, formation: str):
19. # la formation doit être une chaîne non vide
20. if Utils.is_string_ok(formation):
21. self.__formation = formation
22. else:
23. raise MyException("La formation doit être une chaîne de caractères non vide")
1. # on configure l'application
2. import config
3.
4. config = config.configure()
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
119/755
5.
6. # le syspath est configuré - on peut faire les imports
7. from myclasses import Personne, Enseignant, Etudiant
8.
9. # ---------------------------------- main
10. # création d'un tableau d'objets Personne et dérivés
11. groupe = [Enseignant("Paul", "Langevin", 48, "anglais"), Personne("Sylvie", "Lefur", 70),
12. Etudiant("Steve", "Boer", 22, "iup2 qualité")]
13. # identité de ces personnes
14. for personne in groupe:
15. # affichage personne
16. print(personne)
Notes :
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/classes/01/classes_09.py
2. enseignant[[Paul,Langevin,48],anglais]
3. [Sylvie,Lefur,70]
4. étudiant[[Steve,Boer,22],iup2 qualité]
5.
6. Process finished with exit code 0
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. from myclasses import Etudiant
8.
9. # ---------------------------------- main
10. # création d'un étudiant
11. étudiant=Etudiant("Steve", "Boer", 22, "iup2 qualité")
12. # dictionnaire des propriétés
13. print(étudiant.__dict__)
Commentaires
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/classes/01/classes_10.py
2. {'_Personne__prénom': 'Steve', '_Personne__nom': 'Boer', '_Personne__âge': 22, '_Etudiant__formation':
'iup2 qualité'}
3.
4. Process finished with exit code 0
• ligne 2, on obtient un dictionnaire dont les clés sont les propriétés de l’objet préfixées par le nom de la classe à laquelle elles
appartiennent. Nous utiliserons ce dictionnaire pour faire une passerelle entre objet et dictionnaire ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
120/755
13 Les classes génériques [BaseEntity] et [MyException]
Nous définissons maintenant deux classes que nous utiliserons régulièrement par la suite.
Notes
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
121/755
13.2 La classe [BaseEntity]
La classe [BaseEntity] sera la classe parent de la plupart des classes que nous créerons pour encapsuler des informations sur un objet.
Dans la suite nous utiliserons principalement deux types de classes :
• des classes qui n’ont pour but que d’encapsuler au même endroit des informations sur un même objet. Celles-ci n’auront pas
de comportements (méthodes) autres que des getters / setters et une fonction d’affichage (__str__). S’il y a N objets à gérer,
ces classes sont instanciées N fois. [BaseEntity] sera la classe parent de ce type de classes ;
• des classes dont le rôle principal est d’encapsuler des méthodes et très peu d’informations. Ces classes ne seront instanciées
qu’une fois (singleton). Leur rôle est d’implémenter les algorithmes d’une application ;
1. # imports
1. import json
2. import re
3.
4. from MyException import MyException
5.
6.
7. class BaseEntity(object):
8. # propriétés exclues de l'état de la classe
9. excluded_keys = []
10.
11. # propriétés de la classe
12. @staticmethod
13. def get_allowed_keys() -> list:
14. # id : identifiant de l'objet
15. return ["id"]
16.
17. # toString
18. def __str__(self) -> str:
19. return self.asjson()
20.
21. # getter
22. @property
23. def id(self) -> int:
24. return self.__id
25.
26. # setter
27. @id.setter
28. def id(self, id):
29. # l'id doit être un entier >=0
30. try:
31. id = int(id)
32. erreur = id < 0
33. except:
34. erreur = True
35. # erreur ?
36. if erreur:
37. raise MyException(1, f"L'identifiant d'une entité {self.__class__} doit être un entier >=0")
38. else:
39. self.__id = id
40.
41. def fromdict(self, state: dict, silent=False):
42. …
43.
44. def set_value(self, key: str, value, new_attributes) -> dict:
45. …
46.
47. def asdict(self, included_keys: list = None, excluded_keys: list = []) -> dict:
48. …
49.
50. def asjson(self, excluded_keys: list = []) -> str:
51. …
52.
53. def fromjson(self, json_state: str):
54. …
Commentaires
• l’objectif de la classe [BaseEntity] est de faciliter les conversions Objet / Dictionnaire et Objet / jSON. On offre ainsi les
méthodes suivantes :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
122/755
o [asdict] : rend le dictionnaire des propriétés de l’objet ;
o [fromdict] : construit un objet à partir d’un dictionnaire ;
o [asjson] : rend la chaîne jSON de l’objet comme le fait la fonction [__str__] ;
o [fromjson] : construit un objet à partir de sa chaîne jSON ;
• la classe [BaseEntity] est destinée à être dérivée et non à être utilisée telle quelle ;
• lignes 21-24 : la classe [BaseEntity] n’a qu’une propiété, l’entier [id]. Cette propriété est l’identifiant de l’objet. Dans la
pratique, il est souvent utile de pouvoir différentier les instances d’une même classe. Nous le ferons avec cette propriété,
unique pour une instance. Par ailleurs, les objets proviennent souvent de bases de données où ils sont identifiés par une clé
primaire, un entier généralement. Dans ces cas, [id] sera la clé primaire ;
• lignes 26-39 : le setter de la propriété [id]. On vérifie que c’est un entier >=0. Si ce n’est pas le cas, une exception de type
[MyException] est lancée (ligne 38) ;
• ligne 9 : [excluded_keys] est un attribut de classe et non d’instance. Ainsi on écrira [BaseEntity.excluded_keys]. Cet attribut
de classe est une liste contenant les propriétés de classe qui ne participent pas aux conversions Objet / Dictionnaire et Objet
/ jSON ;
• lignes 11-15 : [get_allowed_keys] rend la liste des propriétés de la classe. Dans une conversion Dictionnaire -> Objet ou
jSON -> Objet, on n’acceptera que les clés présentes dans cette liste. Chaque classe dérivant la classe [BaseEntity] aura à
redéfinir cette liste ;
Il faut comprendre ici que les propriétés et fonctions de la classe [BaseEntity] sont accessibles aux classes dérivées de [BaseEntity].
C’est le point important à comprendre.
Nous allons détailler le code de la classe [BaseEntity]. Il est assez avancé. Le lecteur débutant pourra se contenter de lire le rôle de
chaque fonction sans s’apesantir sur son code.
Commentaires
• ligne 1 : la fonction reçoit en paramètre le dictionnaire [state] à partir duquel l’objet courant va être initialisé ;
• ligne 4 : on fait appel à la fonction statique [get_allowed_keys] de la classe qui a appelé la fonction [fromdict]. Si on a affaire
à une classe dérivée de [BaseEntity] et que cette classe dérivée a redéfini la fonction statique [get_allowed_keys] alors c’est
la fonction [get_allowed_keys] qui est appelée. Chaque classe dérivée redéfinit cette fonction statique pour y déclarer ses
propriétés ;
• ligne 6 : on parcourt les clés et valeurs du dictionnaire [state] ;
• ligne 8 : si la clé [key] ne fait pas partie des propriétés de la classe, alors soit :
o on l’ignore ;
o on lance une exception (ligne 10). Le développeur indique ce qu’il souhaite en passant le bon paramètre [silent] (ligne
1). La valeur par défaut de [silent] fait qu’une exception est lancée si on tente d’initialiser l’objet avec une propriété
qu’il n’a pas ;
• ligne 14 : si la clé fait partie des propriétés de l’objet, alors on l’affecte à l’objet [self] à l’aide de la fonction prédéfinie
[setattr] ;
• ligne 16 : la fonction rend l’objet initialisé ;
13.2.1.2 Exemples
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
123/755
13.2.1.2.1 La classe [Utils]
La classe [Utils] (Utils.py) est la suivante :
1. class Utils:
2. # méthode statique
3. @staticmethod
4. def is_string_ok(string: str) -> bool:
5. # string est-elle une chaîne
6. erreur = not isinstance(string, str)
7. if not erreur:
8. # la chaîne est-elle vide ?
9. erreur = string.strip() == ''
10. # résultat
11. return not erreur
Elle définit aux lignes 3-11, une méthode statique qui rend un booléen vrai si son paramètre [str] est une chaîne de caractères non
vide ;
1. # imports
2. from BaseEntity import BaseEntity
3. from MyException import MyException
4. from Utils import Utils
5.
6.
7. # classe Personne
8. class Personne(BaseEntity):
9. # propriétés exclues de l'état de la classe
10. excluded_keys = []
11.
12. # propriétés de la classe
13. # id : identifiant de la personne
14. # prénom : prénom de la personne
15. # nom : nom de la personne
16. # âge : âge de la personne
17. @staticmethod
18. def get_allowed_keys() -> list:
19. # id : identifiant de l'objet
20. return BaseEntity.get_allowed_keys() + ["nom", "prénom", "âge"]
21.
22. # getters
23. @property
24. def prénom(self) -> str:
25. return self.__prénom
26.
27. @property
28. def nom(self) -> str:
29. return self.__nom
30.
31. @property
32. def âge(self) -> int:
33. return self.__âge
34.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
124/755
35. # setters
36. @prénom.setter
37. def prénom(self, prénom: str):
38. # le prénom doit être non vide
39. if Utils.is_string_ok(prénom):
40. self.__prénom = prénom.strip()
41. else:
42. raise MyException(11, "Le prénom doit être une chaîne de caractères non vide")
43.
44. @nom.setter
45. def nom(self, nom: str):
46. # le prénom doit être non vide
47. if Utils.is_string_ok(nom):
48. self.__nom = nom.strip()
49. else:
50. raise MyException(12, "Le nom doit être une chaîne de caractères non vide")
51.
52. @âge.setter
53. def âge(self, âge: int):
54. # l'âge doit être un entier >=0
55. erreur = False
56. if isinstance(âge, int):
57. if âge >= 0:
58. self.__âge = âge
59. else:
60. erreur = True
61. else:
62. erreur = True
63. # erreur ?
64. if erreur:
65. raise MyException(13, "L'âge doit être un entier >=0")
1. # imports
2. from MyException import MyException
3. from Personne import Personne
4. from Utils import Utils
5.
6.
7. # classe Enseignant
8. class Enseignant(Personne):
9. # propriétés exclues de l'état de la classe
10. excluded_keys = []
11.
12. # propriétés de la classe
13. # id : identifiant de la personne
14. # prénom : prénom de la personne
15. # nom : nom de la personne
16. # âge : âge de la personne
17. # discipline : discpline enseignée
18. @staticmethod
19. def get_allowed_keys() -> list:
20. # id : identifiant de l'objet
21. return Personne.get_allowed_keys() + ["discipline"]
22.
23. # propriétés
24. @property
25. def discipline(self) -> str:
26. return self.__discipline
27.
28. @discipline.setter
29. def discipline(self, discipline: str):
30. # la discipline doit être une chaîne non vide
31. if Utils.is_string_ok(discipline):
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
125/755
32. self.__discipline = discipline
33. else:
34. raise MyException(21, "La discipline doit être une chaîne de caractères non vide")
35.
36. # méthode show
37. def show(self):
38. print(f"Enseignant[{self.id}, {self.prénom}, {self.nom}, {self.âge}]")
1. def configure():
2. import os
3.
4. # dossier du fichier de configuration
5. script_dir = os.path.dirname(os.path.abspath(__file__))
6.
7. # chemins absolus des dossiers à mettre dans le syspath
8. absolute_dependencies = [
9. # la classe BaseEntity
10. f"{script_dir}/entities",
11. ]
12.
13. # mise à jour du syspath
14. from myutils import set_syspath
15. set_syspath(absolute_dependencies)
16.
17. # on rend la configuration
18. return {}
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. from Enseignant import Enseignant
8.
9. # un enseignant
10. enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56})
11. enseignant1.show()
• ligne 10 : on crée un objet [Enseignant] à partir d’un dictionnaire. Pour cela, on utilise le constructeur par défaut de la classe
pour créer un objet [Enseignant] à laquel on applique la méthode [fromdict]. Il faut comprendre qu’ici la méthode [fromdict]
exécutée est celle de la classe parent [BaseEntity]. En effet :
o la méthode [fromdict] est d’abord cherchée dans la classe [Enseignant]. Elle n’existe pas ;
o elle est ensuite cherchée dans la classe parent [Personne]. Elle n’existe pas ;
o elle est ensuite cherchée dans la classe parent [BaseEntity]. Elle existe ;
• ligne 11 : on affiche l’objet [Enseignant] ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_01.py
2. Enseignant[1, paul, lourou, 56]
3.
4. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
126/755
Le script [fromdict_02] est le suivant :
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. from Enseignant import Enseignant
8.
9. # un enseignant
10. enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "", "âge": 56})
11. enseignant1.show()
• ligne 10 : on crée un enseignant avec un prénom vide. Cela doit créer une exception car la classe [Personne] n’accepte pas des
prénoms vides. Cet exemple montre la différence entre un dictionnaire et un objet. Ce dernier peut vérifier la validité de ses
propriétés, pas le dictionnaire ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_02.py
2. Traceback (most recent call last):
3. File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_02.py", line 10, in
<module>
4. enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "", "âge": 56})
5. File "C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\classes\02/entities\BaseEntity.py", line
55, in fromdict
6. setattr(self, key, value)
7. File "C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\classes\02/entities\Personne.py", line 42,
in prénom
8. raise MyException(11, "Le prénom doit être une chaîne de caractères non vide")
9. MyException.MyException: MyException[11, Le prénom doit être une chaîne de caractères non vide]
10.
11. Process finished with exit code 1
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. from Enseignant import Enseignant
8.
9. # un enseignant
10. enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "albert", "âge": 56, "sexe": "M"})
11. enseignant1.show()
• ligne 10 : on crée un enseignant à partir d’un dictionnaire contenant une clé (sexe) qui n’appartient pas à la classe [Enseignant].
Une exception devrait alors être levée ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_03.py
2. Traceback (most recent call last):
3. File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_03.py", line 10, in
<module>
4. enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "albert", "âge": 56, "sexe":
"M"})
5. File "C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\classes\02/entities\BaseEntity.py", line
51, in fromdict
6. raise MyException(2, f"la clé [{key}] n'est pas autorisée")
7. MyException.MyException: MyException[2, la clé [sexe] n'est pas autorisée]
8.
9. Process finished with exit code 1
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
127/755
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. from Enseignant import Enseignant
8.
9. # un enseignant
10. enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "albert", "âge": 56, "sexe": "M"},
silent=True)
11. enseignant1.show()
• ligne 10 : on a utilisé le paramètre [silent=True] pour indiquer que si une clé du dictionnaire n’est pas une propriété de la
classe [Enseignant], elle doit simplement être ignorée. Dans ce cas, aucune exception ne sera lancée ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromdict_04.py
2. Enseignant[1, albert, lourou, 56]
3.
4. Process finished with exit code 0
1. def asdict(self, included_keys: list = None, excluded_keys: list =[]) -> dict:
2. # attributs de l'objet
3. attributes = self.__dict__
4. # les nouveaux attributs
5. new_attributes = {}
6. # on parcourt les attributs
7. for key, value in attributes.items():
8. # si la clé est explicitement demandée
9. if included_keys and key in included_keys:
10. self.set_value(key, value, new_attributes)
11. # sinon, si la clé n'est pas exclue
12. elif not included_keys and key not in self.__class__.excluded_keys and key not in
excluded_keys:
13. self.set_value(key, value, new_attributes)
14. # on rend le dictionnaire des attributs
15. return new_attributes
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
128/755
• ligne 15 : on rend le dictionnaire [new_attributes]
1. @staticmethod
2. def set_value(key: str, value, new_attributes: dict):
3. # les clés peuvent être de la forme __Class__key
4. match = re.match("^.*?__(.*?)$", key)
5. if match:
6. # on note la nouvelle clé
7. newkey = match.groups()[0]
8. else:
9. # la clé reste inchangée
10. newkey = key
11. # on insère la nouvelle clé dans le dictionnaire [new_attributes]
12. # en transformant si besoin est la valeur associée en l'un des types
13. # dict, list, type simple
14. new_attributes[newkey] = BaseEntity.check_value(value)
Commentaires
• ligne 4 : on regarde si la clé est de la forme __Class_key. C’est la forme qu’elle a si elle appartient à un objet inclus dans l’objet
principal. Dans ce cas, on ne veut garder que la chaîne [key] ;
• ligne 7 : on ne garde que la chaîne qui suit les deux derniers caractères soulignés de la chaîne ;
• ligne 8-10 : si la clé n’est pas de la forme __Class_key alors on la garde telle quelle ;
• lignes 11-14 : la valeur associée à la clé [newkey] est calculée par la méthode statique [BaseEntity.check_value] ;
1. @staticmethod
2. def check_value(value):
3. # la valeur peut être de type BaseEntity, list, dict ou un type simple
4. # value est-elle une instance de BaseEntity ?
5. if isinstance(value, BaseEntity):
6. value2 = value.asdict()
7. # value est-elle de type list
8. elif isinstance(value, list):
9. value2 = BaseEntity.list2list(value)
10. # value est-elle de type dict ?
11. elif isinstance(value, dict):
12. value2 = BaseEntity.dict2dict(value)
13. # value est un type simple
14. else:
15. value2 = value
16. # on rend le résultat
17. return value2
• ligne 1 : la méthode [check_value] est statique (méthode de classe et non d’instance). Elle reçoit en paramètre la valeur à
associer à une clé du dictionnaire :
o ligne 17 : si cette valeur est un type simple, elle reste inchangée ;
o lignes 5-6 : si cette valeur est de type BaseEntity, la valeur est remplacée par son dictionnaire. On a alors un appel récursif ;
o lignes 8-9 : si cette valeur est une liste, alors elle est remplacée par la valeur [BaseEntity.list2list] ;
o lignes 11-12 : si cette valeur est un dictionnaire, alors elle est remplacée par la valeur [BaseEntity.dict2dict] ;
1. @staticmethod
2. def list2list(liste: list) -> list:
3. # on inspecte les éléments de la liste
4. newlist = []
5. for value in liste:
6. newlist.append(BaseEntity.check_value(value))
7. # on rend la nouvelle liste
8. return newlist
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
129/755
1. @staticmethod
2. def dict2dict(dictionary: dict) -> dict:
3. # on inspecte les éléments du dictionnaire
4. newdict = {}
5. for key, value in dictionary.items():
6. newdict[key] = BaseEntity.check_value(value)
7. # on rend le nouveau dictionnaire
8. return newdict
1. # on configure l'application
2. import config
3. config = config.configure()
4.
5. # le syspath est configuré - on peut faire les imports
6. from Enseignant import Enseignant
7. from BaseEntity import BaseEntity
8.
9. # un enseignant
10. enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56})
11. dict1 = enseignant1.asdict()
12. print(type(dict1))
13. print(enseignant1.__dict__)
14. print(dict1)
15. print(enseignant1.asdict(excluded_keys=["_Personne__âge"]))
16. Enseignant.excluded_keys = ["_Personne__prénom"]
17. print(enseignant1)
18. # un autre enseignant
19. enseignant2 = Enseignant().fromdict({"id": 2, "nom": "abélard", "prénom": "béatrice", "âge": 57})
20. print(enseignant2.asdict())
21. print(enseignant2.asdict(included_keys=["_Personne__nom"]))
22. # une liste d'entités dans une entité
23. Enseignant.excluded_keys = []
24. entity1 = BaseEntity()
25. enseignants = [enseignant1, enseignant2]
26. setattr(entity1, "enseignants", enseignants)
27. print(entity1.asdict())
28. # un dictionnaire d'entités dans une entité
29. matières = {"maths": enseignant1, "français": enseignant2}
30. setattr(entity1, "matières", matières)
31. print(entity1.asdict())
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/classes/02/asdict_01.py
2. <class 'dict'>
3. {'_BaseEntity__id': 1, '_Personne__nom': 'lourou', '_Personne__prénom': 'paul', '_Personne__âge': 56}
4. {'id': 1, 'nom': 'lourou', 'prénom': 'paul', 'âge': 56}
5. {'id': 1, 'nom': 'lourou', 'prénom': 'paul'}
6. {"id": 1, "nom": "lourou", "âge": 56}
7. {'id': 2, 'nom': 'abélard', 'âge': 57}
8. {'nom': 'abélard'}
9. {'enseignants': [{'id': 1, 'nom': 'lourou', 'prénom': 'paul', 'âge': 56}, {'id': 2, 'nom': 'abélard',
'prénom': 'béatrice', 'âge': 57}]}
10. {'enseignants': [{'id': 1, 'nom': 'lourou', 'prénom': 'paul', 'âge': 56}, {'id': 2, 'nom': 'abélard',
'prénom': 'béatrice', 'âge': 57}], 'matières': {'maths': {'id': 1, 'nom': 'lourou', 'prénom': 'paul',
'âge': 56}, 'français': {'id': 2, 'nom': 'abélard', 'prénom': 'béatrice', 'âge': 57}}}
11.
12. Process finished with exit code 0
• la ligne 4 montre l’intérêt de la méthode [asdict] vis-à-vis de l’utilisation de la propriété [__dict__]. Les propriétés sont
débarrassées du préfixe de leur classe. Cela se prête mieux à un affichage ;
• il y a plusieurs façons d’utiliser la méthode [asdict] :
o on veut toutes les propriétés : on utilise la méthode [asdict] sans paramètres ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
130/755
o on ne veut que certaines propriétés :
▪ il y a plus de propriétés à inclure qu’à exclure : on utilisera le seul paramètre [excluded_keys] ;
▪ il y a moins de propriétés à inclure qu’à exclure : on utilisera le seul paramètre [included_keys] ;
1. def asjson(self, included_keys: list = None, excluded_keys: list = []) -> str:
2. # la chaîne json
3. return json.dumps(self.asdict(included_keys=included_keys, excluded_keys=excluded_keys),
ensure_ascii=False)
1. # on configure l'application
2. import config
3. config = config.configure()
4.
5. # le syspath est configuré - on peut faire les imports
6. from Enseignant import Enseignant
7. from BaseEntity import BaseEntity
8.
9. # un enseignant
10. enseignant1 = Enseignant().fromdict({"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56})
11. print(type(enseignant1.asjson()))
12. print(enseignant1.asjson(excluded_keys=["_Personne__âge"]))
13. Enseignant.excluded_keys = ["_Personne__prénom"]
14. print(enseignant1.asjson())
15. # un autre enseignant
16. enseignant2 = Enseignant().fromdict({"id": 2, "nom": "abélard", "prénom": "béatrice", "âge": 57})
17. print(enseignant2.asjson())
18. print(enseignant2.asjson(included_keys=["_Personne__nom"]))
19. # une liste d'entités dans une entité
20. Enseignant.excluded_keys = []
21. entity1 = BaseEntity()
22. enseignants = [enseignant1, enseignant2]
23. setattr(entity1, "enseignants", enseignants)
24. print(entity1.asjson())
25. # un dictionnaire d'entités dans une entité
26. matières = {"maths": enseignant1, "français": enseignant2}
27. setattr(entity1, "matières", matières)
28. print(entity1.asjson())
La méthode [BaseEntity.__str__] utilise la méthode [asjson] pour afficher l’identité de l’objet [BaseEntity] ou dérivé :
1. # toString
2. def __str__(self) -> str:
3. return self.asjson()
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
131/755
13.2.4 La méthode [BaseEntity.fromjson]
La méthode [BaseEntity.fromjson] permet d’initialiser un objet de type [BaseEntity] ou dérivé à partir d’un dictionnaire jSON.
Son code est le suivant :
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. from Enseignant import Enseignant
8. import json
9.
10. # un enseignant
11. json1 = json.dumps({"id": 1, "nom": "lourou", "prénom": "paul", "âge": 56})
12. enseignant1 = Enseignant().fromjson(json1)
13. enseignant1.show()
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/classes/02/fromjson_01.py
2. Enseignant[1, paul, lourou, 56]
3.
4. Process finished with exit code 0
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. from BaseEntity import BaseEntity
8. from MyException import MyException
9.
10.
11. # une classe
12. class ChildEntity(BaseEntity):
13. # attributs exclus de l'état de la classe
14. excluded_keys = []
15.
16. @staticmethod
17. def get_allowed_keys():
18. return ["att1", "att2", "att3", "att4"]
19.
20. @property
21. def att1(self) -> int:
22. return self.__att1
23.
24. @att1.setter
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
132/755
25. def att1(self, value: int):
26. if 10 >= value >= 1:
27. self.__att1 = value
28. else:
29. raise MyException(1, f"L'attribut [att1] attend une valeur dans l'intervalle [1,10] ({value})")
30.
31.
32. # configuration ChildEntity
33. ChildEntity.excluded_keys = []
34. # instance ChildEntity
35. child = ChildEntity().fromdict({"att1": 1, "att2": 2})
36. # attention aux noms des propriétés
37. # ce sont ces noms qui sont utilisés dans [excluded_keys] et [included_keys]
38. print(child.__dict__)
39. # propriétés non préfixées par leur classe
40. print(child)
41.
42. # instance ChildEntity
43. try:
44. child = ChildEntity().fromdict({"att1": 1, "att5": 5})
45. print(child)
46. except MyException as erreur:
47. print(erreur)
48.
49. # instance ChildEntity
50. child = ChildEntity().fromdict({"att1": 1, "att2": 2, "att3": 3, "att4": 4})
51. print(child)
52.
53. # exclusions de certaines clés de l'état des instances
54. ChildEntity.excluded_keys = ['att3']
55. print(child)
56.
57. # on exclut une clé explicitement de l'affichage
58. # elle se rajoute à celles exclues globalement au niveau de la classe
59. print(child.asdict(excluded_keys=["_ChildEntity__att1"]))
60. print(child.asjson(excluded_keys=["att2"]))
61.
62. # intérêt de la classe vis à vis du dictionnaire
63. # elle peut vérifier la validité de son contenu
64. try:
65. child = ChildEntity().fromdict({"att1": 20})
66. except MyException as erreur:
67. print(erreur)
68.
69. # instance ChildEntity
70. child1 = ChildEntity().fromdict({"att1": 1, "att2": 2, "att3": 3, "att4": 4})
71. # instanche ChildEntity contenant une autre instance ChildEntity
72. child2 = ChildEntity().fromdict({"att1": 10, "att2": 20, "att3": 30, "att4": child1})
73. print(child2)
74.
75. # included_keys a priorité sur excluded_keys qui sont alors ignorées
76. ChildEntity.excluded_keys = ['_ChildEntity__att1', 'att2']
77. print(child.asdict(included_keys=["_ChildEntity__att1", "att3"], excluded_keys=["att3", "att4"]))
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/classes/02/main.py
2. {'_ChildEntity__att1': 1, 'att2': 2}
3. {"att1": 1, "att2": 2}
4. MyException[2, la clé [att5] n'est pas autorisée]
5. {"att1": 1, "att2": 2, "att3": 3, "att4": 4}
6. {"att1": 1, "att2": 2, "att4": 4}
7. {'att2': 2, 'att4': 4}
8. {"att1": 1, "att4": 4}
9. MyException[1, L'attribut [att1] attend une valeur dans l'intervalle [1,10] (20)]
10. {"att1": 10, "att2": 20, "att4": {"att1": 1, "att2": 2, "att4": 4}}
11. {'att1': 1, 'att3': 3}
12.
13. Process finished with exit code 0
On prêtera attention à la ligne 2 des résultats : c’est la propriété [ChildEntity.__dict__] (ligne 38 du code) qui nous permet de
connaître les noms des propriétés à mettre dans les listes [included_keys] et [excluded_keys]. On notera, toujours ligne 2 des
résultats, que selon que la propriété est définie à l’intérieur de la classe par un getter / setter ou qu’elle a été créée comme on créerait
la clé d’un dictionnaire, elle est ou pas préfixée par le nom de la classe [ChildEntity].
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
133/755
14 Architecture en couches et programmation par interfaces
14.1 Introduction
Nous nous proposons d'écrire une application permettant l'affichage des notes des élèves d'un collège. Cette application peut avoir
une architecture multicouche :
• la couche [ui] (User Interface) est la couche en contact avec l'utilisateur de l'application ;
• la couche [métier] implémente les règles de gestion de l'application, tels que le calcul d'un salaire ou d'une facture. Cette
couche utilise des données provenant de l'utilisateur via la couche [présentation] et du SGBD via la couche [dao] ;
• la couche [dao] (Data Access Objects) gère l'accès aux données du SGBD (Système de Gestion de Bases de Données).
C'est l'architecture qui avait été utilisée dans le |cours sur Python 2|. On peut également introduire une variante :
Les différences vis à vis de la structure en couches précédente sont les suivantes :
14.2 Exemple 1
Nous allons illustrer l’architecture en couches avec une application console simple :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
134/755
Note : les dossiers en bleu font partie des [Sources Root] du projet PyCharm.
1. # imports
2. from BaseEntity import BaseEntity
3. from MyException import MyException
4. from Utils import Utils
5.
6.
7. class Classe(BaseEntity):
8. # attributs exclus de l'état de la classe
9. excluded_keys = []
10.
11. # propriétés de la classe
12. @staticmethod
13. def get_allowed_keys() -> list:
14. # id : identifiant de la classe
15. # nom : nom de la classe
16. return BaseEntity.get_allowed_keys() + ["nom"]
17.
18. # getter
19. @property
20. def nom(self: object) -> str:
21. return self.__nom
22.
23. # setters
24. @nom.setter
25. def nom(self: object, nom: str):
26. # nom doit être une chaîne de caractères non vide
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
135/755
27. if Utils.is_string_ok(nom):
28. self.__nom = nom
29. else:
30. raise MyException(11, f"Le nom de la classe {self.id} doit être une chaîne de caractères non
vide")
Notes
• ligne 7 : l'entité [Classe] dérive de l'entité [BaseEntity] étudiée au paragraphe |La classe BaseEntity| ;
• lignes 11-16 : une classe est définie par un n° id et un nom (ligne 16). La propriété [id] est fournie par la classe [BaseEntity]
et le nom par la classe [Classe] ;
• lignes 18-30 : getter / setter de l'attribut [nom] ;
14.2.1.2 L'entité [Matière]
La classe [Matière] (matière.py) est la suivante :
1. # imports
2. from BaseEntity import BaseEntity
3. from MyException import MyException
4. from Utils import Utils
5.
6.
7. class Matière(BaseEntity):
8. # attributs exclus de l'état de la classe
9. excluded_keys = []
10.
11. # propriétés de la classe
12. @staticmethod
13. def get_allowed_keys() -> list:
14. # id : identifiant de la matière
15. # nom : nom de la matière
16. # coefficient : coefficient de la matière
17. return BaseEntity.get_allowed_keys() + ["nom", "coefficient"]
18.
19. # getter
20. @property
21. def nom(self: object) -> str:
22. return self.__nom
23.
24. @property
25. def coefficient(self: object) -> float:
26. return self.__coefficient
27.
28. # setters
29. @nom.setter
30. def nom(self: object, nom: str):
31. # nom doit être une chaîne de caractères non vide
32. if Utils.is_string_ok(nom):
33. self.__nom = nom
34. else:
35. raise MyException(21, f"Le nom de la matière {self.id} doit être une chaîne de caractères non
vide")
36.
37. @coefficient.setter
38. def coefficient(self, coefficient: float):
39. # le coefficient doit être un réel >=0
40. erreur = False
41. if isinstance(coefficient, (int, float)):
42. if coefficient >= 0:
43. self.__coefficient = coefficient
44. else:
45. erreur = True
46. else:
47. erreur = True
48. # erreur ?
49. if erreur:
50. raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")
Notes
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
136/755
14.2.1.3 L'entité [Elève]
La classe [Elève] (élève.py) est la suivante :
1. # imports
2. from BaseEntity import BaseEntity
3. from Classe import Classe
4. from MyException import MyException
5.
6. from Utils import Utils
7.
8.
9. class Elève(BaseEntity):
10. # attributs exclus de l'état de la classe
11. excluded_keys = []
12.
13. # propriétés de la classe
14. @staticmethod
15. def get_allowed_keys() -> list:
16. # id : identifiant de l'élève
17. # nom : nom de l'élève
18. # prénom : prénom de l'élève
19. # classe : classe de l'élève
20. return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]
21.
22. # getters
23. @property
24. def nom(self: object) -> str:
25. return self.__nom
26.
27. @property
28. def prénom(self: object) -> str:
29. return self.__prénom
30.
31. @property
32. def classe(self: object) -> Classe:
33. return self.__classe
34.
35. # setters
36. @nom.setter
37. def nom(self: object, nom: str) -> str:
38. # nom doit être une chaîne de caractères non vide
39. if Utils.is_string_ok(nom):
40. self.__nom = nom
41. else:
42. raise MyException(41, f"Le nom de l'élève {self.id} doit être une chaîne de caractères non
vide")
43.
44. @prénom.setter
45. def prénom(self: object, prénom: str) -> str:
46. # prénom doit être une chaîne de caractères non vide
47. if Utils.is_string_ok(prénom):
48. self.__prénom = prénom
49. else:
50. raise MyException(42, f"Le prénom de l'élève {self.id} doit être une chaîne de caractères non
vide")
51.
52. @classe.setter
53. def classe(self: object, value):
54. try:
55. # on attend un type Classe
56. if isinstance(value, Classe):
57. self.__classe = value
58. # ou un type dict
59. elif isinstance(value,dict):
60. self.__classe=Classe().fromdict(value)
61. # ou un type json
62. elif isinstance(value,str):
63. self.__classe = Classe().fromjson(value)
64. except BaseException as erreur:
65. raise MyException(43, f"L'attribut [{value}] de l'élève {self.id} doit être de type Classe ou
dict ou json. Erreur : {erreur}")
Notes
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
137/755
• lignes 13-20 : un élève est caractérisé par son n° [id], son nom [nom], son prénom [prénom], sa classe[classe]. Ce dernier
paramètre est une référence sur un objet [Classe] ;
• lignes 22-65 : getters / setters des attributs de la classe ;
14.2.1.4 L'entité [Note]
La classe [Note] (note.py) est la suivante :
1. # imports
2. from BaseEntity import BaseEntity
3. from Elève import Elève
4. from Matière import Matière
5. from MyException import MyException
6.
7.
8. class Note(BaseEntity):
9. # attributs exclus de l'état de la classe
10. excluded_keys = []
11.
12. # propriétés de la classe
13. @staticmethod
14. def get_allowed_keys() -> list:
15. # id : identifiant de la note
16. # valeur : la note elle-même
17. # élève : élève (de type Elève) concerné par la note
18. # matière : matière (de type Matière) concernée par la note
19. # l'objet Note est donc la note d'un élève dans une matière
20. return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]
21.
22. # getters
23. @property
24. def valeur(self: object) -> float:
25. return self.__valeur
26.
27. @property
28. def élève(self: object) -> Elève:
29. return self.__élève
30.
31. @property
32. def matière(self: object) -> Matière:
33. return self.__matière
34.
35. # getters
36. @valeur.setter
37. def valeur(self: object, valeur: float):
38. # la note doit être un réel entre 0 et 20
39. if isinstance(valeur, (int, float)) and 0 <= valeur <= 20:
40. self.__valeur = valeur
41. else:
42. raise MyException(31,
43. f"L'attribut {valeur} de la note {self.id} doit être un nombre dans
l'intervalle [0,20]")
44.
45. @élève.setter
46. def élève(self: object, value):
47. try:
48. # on attend un type Elève
49. if isinstance(value, Elève):
50. self.__élève = value
51. # ou un type dict
52. elif isinstance(value, dict):
53. self.__élève = Elève().fromdict(value)
54. # ou un type json
55. elif isinstance(value, str):
56. self.__élève = Elève().fromjson(value)
57. except BaseException as erreur:
58. raise MyException(32,
59. f"L'attribut [{value}] de la note {self.id} doit être de type Elève ou dict
ou json. Erreur : {erreur}")
60.
61. @matière.setter
62. def matière(self: object, value):
63. try:
64. # on attend un type Matière
65. if isinstance(value, Matière):
66. self.__matière = value
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
138/755
67. # ou un type dict
68. elif isinstance(value, dict):
69. self.__matière = Matière().fromdict(value)
70. # ou un type json
71. elif isinstance(value, str):
72. self.__matière = Matière().fromjson(value)
73. except BaseException as erreur:
74. raise MyException(33,
75. f"L'attribut [{value}] de la note {self.id} doit être de type Matière ou dict
ou json. Erreur : {erreur}")
Notes
Le fichier [config.py] configure l’environnement du script principal [main] (1) ainsi que celui des tests (2). Tous ces scripts ont une
instruction [import config] en début de code. On rappelle que le dossier contenant le script objet de la commande [python script]
fait automatiquement partie du Python Path.Si donc [config] est dans le même dossier que les scripts ayant l’instruction [import
config], il sera trouvé. Les fichiers [1] et [2] sont ici identiques. Ce pourrait ne pas être le cas.
1. def configure():
2. import os
3.
4. # chemin absolu du dossier de ce script
5. script_dir = os.path.dirname(os.path.abspath(__file__))
6. # root_dir
7. root_dir="C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"
8. # dépendances absolues
9. absolute_dependencies=[
10. # les dossiers locaux contenant des classes et interfaces
11. f"{root_dir}/02/entities",
12. f"{script_dir}/../entities",
13. f"{script_dir}/../interfaces",
14. f"{script_dir}/../services",
15. ]
16.
17. # mise à jour du syspath
18. from myutils import set_syspath
19. set_syspath(absolute_dependencies)
20.
21. # on rend la config
22. return {}
• lignes 11-14 : les dossiers qui doivent faire partie du Python Path (sys.path) ;
• le dossier [f"{root_dir}/02/entities"] donne accès aux classes [BaseEntity] et [MyException] ;
• le dossier [f"{script_dir}/../entities"] donne accès aux classes [Elève], [Classe], [Matière], [Note] ;
• le dossier [f"{script_dir}/../interfaces",] donne accès aux interfaces de l’application ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
139/755
• le dossier [f"{script_dir}/../services"] donne accès aux classes implémentant les interfaces ;
Nous allons ici écrire des tests exécutés par un outil appelé [unittest]. PyCharm vient avec plusieurs frameworks de test. Le choix
de l’un d’eux se fait dans la configuration de PyCharm :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
140/755
14.2.3.1 La classe de tests [TestBaseEntity]
Le script de test [TestBaseEntity] sera le suivant :
1. import unittest
2.
3. # on configure l'application
4. import config
5.
6. config = config.configure()
7.
8.
9. class TestBaseEntity (unittest.TestCase):
10.
11. def test_note1(self):
12. # imports
13. from Note import Note
14. from Elève import Elève
15. from Classe import Classe
16. from Matière import Matière
17. # construction d'une note à partir d'une chaîne jSON
18. note = Note().fromjson(
19. '{"id": 8, "valeur": 12, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4", "classe":
{"id": 2, "nom": "classe2"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}')
20. # vérifications
21. self.assertIsInstance(note, Note)
22. self.assertIsInstance(note.élève, Elève)
23. self.assertIsInstance(note.élève.classe, Classe)
24. self.assertIsInstance(note.matière, Matière)
25.
26.
27. def test_note2(self):
28. # imports
29. from Note import Note
30. from Elève import Elève
31. from Classe import Classe
32. from Matière import Matière
33. # construction d'une note à partir d'un dictionnaire
34. note = Note().fromdict(
35. {"id": 8, "valeur": 12, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4",
36. "classe": {"id": 2, "nom": "classe2"}},
37. "matière": {"id": 2, "nom": "matière2", "coefficient": 2}})
38. # vérifications
39. self.assertIsInstance(note, Note)
40. self.assertIsInstance(note.élève, Elève)
41. self.assertIsInstance(note.élève.classe, Classe)
42. self.assertIsInstance(note.matière, Matière)
43.
44. if __name__ == '__main__':
45. unittest.main()
Notes
• ligne 1 : on importe le module [unittest] qui va fournir les différentes méthodes de test ;
• lignes 3-6 : on configure l’application pour que les classes nécessaires aux tests soient trouvées ;
• ligne 9 : une classe de test [unittest] doit étendre la classe [unittest.TestCase] ;
• lignes 11, 27 : les fonctions de test doivent avoir un nom commençant par [test] sinon elles ne seront pas reconnues ;
• lignes 13-16 : on importe les classes dont on a besoin ;
• dans cette classe de test, on veut vérifier le comportement des méthodes [BaseEntity.fromdict] (ligne 34) et
[BaseEntity.fromjson] (ligne 18). La classe [Note] a des propriétés qui sont des références à d’autres classes. On veut vérifier
que les deux méthodes précédentes créent des objets [Note] valides ;
• ligne 18 : on crée un objet [Note] à partir d’un objet jSON ;
• ligne 21 : on vérifie que l’objet créé est bien de type [Note]. La méthode [assertIsInstance] est une méthode de la classe
[unittest.TestCase], classe parent de la classe [TestBaseEntity] ;
• ligne 22 : on vérifie que [note.élève] est bien de type [Elève] ;
• ligne 23 : on vérifie que [note.élève.classe] est bien de type [Classe] ;
• ligne 24 : on vérifie que [note.matière] est bien de type [Matière] ;
• lignes 33-42 : on fait de même avec la méthode [BaseEntity.fromdict] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
141/755
Il y a plusieurs façon d’exécuter les tests :
1. import unittest
2.
3. # on configure l'application
4. import config
5.
6. config = config.configure()
7.
8.
9. class TestBaseEntity(unittest.TestCase):
Ce qui gêne le framework [UnitTest], c’est la présence de code exécutable, lignes 3-6, avant la définition de la classe de test, ligne 9.
1. import unittest
2.
3.
4. class TestBaseEntity(unittest.TestCase):
5.
6. def setUp(self):
7. # on configure l'application
8. import config
9.
10. config.configure()
11.
12. def test_note1(self):
13. …
14.
15. def test_note2(self):
16. …
17.
18.
19. if __name__ == '__main__':
20. unittest.main()
• lignes 6-10 : on définit une fonction [setUp]. Cette fonction a un rôle particulier : elle est exécutée avant chaque fonction de
test (test_note1, test_note2) ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
142/755
Ceci fait, l’exécution de la classe [TestBaseEntity] donne les résultats suivants :
Cette fois-ci les deux méthodes de test ont été exécutées et les tests ont été réussis.
Voyons ce qui se passe lorsqu’un test échoue. Modifions le code de [test_note1] de la façon suivante :
1. def test_note1(self):
2. # erreur volontaire - on vérifie que 1==2
3. self.assertEqual(1,2)
4. # imports
5. from Note import Note
6. …
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
143/755
• en [7-8], la cause de l’erreur ;
Une autre façon d’exécuter une classe de tests est de l’exécuter dans un terminal :
La ligne 6 indique que les deux tests ont réussi (on a enlevé l’erreur 1==2) ;
Enfin une troisème façon d’exécuter la classe de tests [TestBaseEntity], toujours dans un terminal, est la suivante. On termine la
classe de tests avec les lignes 6-7 suivantes ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
144/755
1. …
2. self.assertIsInstance(note.élève.classe, Classe)
3. self.assertIsInstance(note.matière, Matière)
4.
5.
6. if __name__ == '__main__':
7. unittest.main()
• ligne 6 : la variable [__name__] est le nom donné au script qui s’exécute. Lorsque le script est le script lancé par la commande
[python script.py], la variable [__name__] vaut [__main__] (2 caractères soulignés avant et après l’identifiant). Ainsi la ligne
7 n’est exécutée que lorsque le script [TestBaseEntity] est lancé par la commande [python TestBaseEntity.py]. L’instruction
[unittest.main()] lance l’exécution du script par le framework [UnitTest]. Voici un exemple :
1. (venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\troiscouches\v01\tests>python
TestBaseEntity.py
2. ..
3. ----------------------------------------------------------------------
4. Ran 2 tests in 0.013s
5.
6. OK
1. import unittest
2.
3.
4. class TestEntités(unittest.TestCase):
5. def setUp(self):
6. # on configure l'application
7. import config
8.
9. config.configure()
10.
11. def test_code1a(self):
12. # imports
13. from Elève import Elève
14. from MyException import MyException
15. # code d'erreur
16. code = None
17. try:
18. # id invalide
19. Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})
20. except MyException as ex:
21. print(f"\ncode erreur={ex.code}, message={ex}")
22. code = ex.code
23. # vérification
24. self.assertEqual(code, 1)
25.
26. def test_code41(self):
27. # imports
28. from Elève import Elève
29. from MyException import MyException
30. # code d'erreur
31. code = None
32.
33. try:
34. # nom invalide
35. Elève().fromdict({"id": 1, "nom": "", "prénom": "z", "classe": "t"})
36. except MyException as ex:
37. print(f"\ncode erreur={ex.code}, message={ex}")
38. code = ex.code
39. # vérification
40. self.assertEqual(code, 41)
41.
42. def test_code42(self):
43. # imports
44. from Elève import Elève
45. from MyException import MyException
46. # code d'erreur
47. code = None
48. try:
49. # prénom invalide
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
145/755
50. Elève().fromdict({"id": 1, "nom": "y", "prénom": "", "classe": "t"})
51. except MyException as ex:
52. print(f"\ncode erreur={ex.code}, message={ex}")
53. code = ex.code
54. # vérification
55. self.assertEqual(code, 42)
56.
57. def test_code43(self):
58. # imports
59. from Elève import Elève
60. from MyException import MyException
61. # code d'erreur
62. code = None
63. try:
64. # classe invalide
65. Elève().fromdict({"id": 1, "nom": "y", "prénom": "z", "classe": "t"})
66. except MyException as ex:
67. print(f"\ncode erreur={ex.code}, message={ex}")
68. code = ex.code
69. # vérification
70. self.assertEqual(code, 43)
71.
72. def test_code1b(self):
73. # imports
74. from Classe import Classe
75. from MyException import MyException
76. # code d'erreur
77. code = None
78. try:
79. # identifiant invalide
80. Classe().fromdict({"id": "x", "nom": "y"})
81. except MyException as ex:
82. print(f"\ncode erreur={ex.code}, message={ex}")
83. code = ex.code
84. # vérification
85. self.assertEqual(code, 1)
86.
87. def test_code11(self):
88. # imports
89. from Classe import Classe
90. from MyException import MyException
91.
92. # code d'erreur
93. code = None
94. try:
95. # nom invalide
96. Classe().fromdict({"id": 1, "nom": ""})
97. except MyException as ex:
98. code = ex.code
99. # vérification
100. self.assertEqual(code, 11)
101.
102. def test_code1c(self):
103. # imports
104. from Matière import Matière
105. from MyException import MyException
106.
107. # code d'erreur
108. code = None
109. try:
110. # identifiant invalide
111. Matière().fromdict({"id": "x", "nom": "y", "coefficient": "t"})
112. except MyException as ex:
113. print(f"\ncode erreur={ex.code}, message={ex}")
114. code = ex.code
115. # vérification
116. self.assertEqual(code, 1)
117.
118. def test_code21(self):
119. # imports
120. from Matière import Matière
121. from MyException import MyException
122. # code d'erreur
123. code = None
124. try:
125. # nom invalide
126. Matière().fromdict({"id": "1", "nom": "", "coefficient": "t"})
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
146/755
127. except MyException as ex:
128. print(f"\ncode erreur={ex.code}, message={ex}")
129. code = ex.code
130. # vérification
131. self.assertEqual(code, 21)
132.
133. def test_code22(self):
134. # imports
135. from Matière import Matière
136. from MyException import MyException
137. # code d'erreur
138. code = None
139. try:
140. # coefficient invalide
141. Matière().fromdict({"id": 1, "nom": "y", "coefficient": "t"})
142. except MyException as ex:
143. print(f"\ncode erreur={ex.code}, message={ex}")
144. code = ex.code
145. # vérification
146. self.assertEqual(code, 22)
147.
148. def test_code1d(self):
149. # imports
150. from Note import Note
151. from MyException import MyException
152. # code d'erreur
153. code = None
154. try:
155. # identifiant invalide
156. Note().fromdict({"id": "x", "valeur": "x", "élève": "y", "matière": "z"})
157. except MyException as ex:
158. print(f"\ncode erreur={ex.code}, message={ex}")
159. code = ex.code
160. # vérification
161. self.assertEqual(code, 1)
162.
163. def test_code31(self):
164. # imports
165. from Note import Note
166. from MyException import MyException
167.
168. # code d'erreur
169. code = None
170. try:
171. # valeur invalide
172. Note().fromdict({"id": 1, "valeur": "x", "élève": "y", "matière": "z"})
173. except MyException as ex:
174. print(f"\ncode erreur={ex.code}, message={ex}")
175. code = ex.code
176. # vérification
177. self.assertEqual(code, 31)
178.
179. def test_code32(self):
180. # imports
181. from Note import Note
182. from MyException import MyException
183.
184. # code d'erreur
185. code = None
186. try:
187. # élève invalide
188. Note().fromdict({"id": 1, "valeur": 10, "élève": "y", "matière": "z"})
189. except MyException as ex:
190. print(f"\ncode erreur={ex.code}, message={ex}")
191. code = ex.code
192. # vérification
193. self.assertEqual(code, 32)
194.
195. def test_code33(self):
196. # imports
197. from Elève import Elève
198. from Note import Note
199. from Classe import Classe
200. from MyException import MyException
201.
202. # code d'erreur
203. code = None
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
147/755
204. try:
205. # matière invalide
206. classe = Classe().fromdict({"id": 1, "nom": "x"})
207. élève = Elève().fromdict({"id": 1, "nom": "a", "prénom": "b", "classe": classe})
208. Note().fromdict({"id": 1, "valeur": 10, "élève": élève, "matière": "z"})
209. except MyException as ex:
210. print(f"\ncode erreur={ex.code}, message={ex}")
211. code = ex.code
212. # vérification
213. self.assertEqual(code, 33)
214.
215. def test_exception(self):
216. # imports
217. from Elève import Elève
218. # le test doit lancer le type [MyException] pour réussir
219. from MyException import MyException
220. with self.assertRaises(MyException):
221. # le test
222. Elève().fromdict({"id": "x", "nom": "y", "prénom": "z", "classe": "t"})
223.
224.
225. if __name__ == '__main__':
226. unittest.main()
• le script de test a pour but de tester les setters des classes : vérifier qu'on ne peut pas attribuer des valeurs incorrectes aux
attributs des différentes entités ;
• lignes 11-24 : on teste qu'on ne peut pas passer un identifiant invalide à un élève. Parce qu'on passe la valeur 'x', ligne 16,
comme identifiant de l'élève, on s'attend à avoir une exception. On devrait donc passer dans les lignes 20-22 ;
• ligne 21 : affichage du message d'erreur ;
• ligne 22 : on récupère le code de l'erreur (cf paragraphe |L'entité MyException|) ;
• ligne 24 : on vérifie (assert) que le code d'erreur est 1. Ici, on vérifie deux choses :
o qu'il y a bien eu erreur ;
o que le code d'erreur est 1 ;
• ce processus est répété avec les fonctions des lignes 24-213 ;
• lignes 215-222 : on teste qu'une action lève une exception d'un certain type ;
• ligne 220 : on indique que le test est réussi s'il lève une exception de type [MyException] ;
Résultats
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
148/755
5.
6. code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Elève.Elève'> doit être un
entier >=0]
7.
8. code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Classe.Classe'> doit être un
entier >=0]
9.
10. code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Matière.Matière'> doit être un
entier >=0]
11.
12. code erreur=1, message=MyException[1, L'identifiant d'une entité <class 'Note.Note'> doit être un
entier >=0]
13.
14. code erreur=21, message=MyException[21, Le nom de la matière 1 doit être une chaîne de caractères non vide]
15.
16. code erreur=22, message=MyException[22, Le coefficient de la matière y doit être un réel >=0]
17.
18. code erreur=31, message=MyException[31, L'attribut x de la note 1 doit être un nombre dans l'intervalle
[0,20]]
19.
20. code erreur=32, message=MyException[32, L'attribut [y] de la note 1 doit être de type Elève ou dict ou
json. Erreur : Expecting value: line 1 column 1 (char 0)]
21.
22. code erreur=33, message=MyException[33, L'attribut [z] de la note 1 doit être de type Matière ou dict ou
json. Erreur : Expecting value: line 1 column 1 (char 0)]
23.
24. code erreur=41, message=MyException[41, Le nom de l'élève 1 doit être une chaîne de caractères non vide]
25.
26. code erreur=42, message=MyException[42, Le prénom de l'élève 1 doit être une chaîne de caractères non vide]
27.
28. code erreur=43, message=MyException[43, L'attribut [t] de l'élève 1 doit être de type Classe ou dict ou
json. Erreur : Expecting value: line 1 column 1 (char 0)]
29.
30.
31. Ran 14 tests in 0.040s
32.
33. OK
34.
35. Process finished with exit code 0
La couche [dao] implémente l’interface [InterfaceDao] [1]. Celle-ci est implémentée par la classe [Dao] (2). Le script [tests_dao]
(3) teste les méthodes de la couche [dao].
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
149/755
14.2.4.1 Interface [InterfaceDao]
Une interface est un contrat passé entre code appelant et code appelé. C’est le code appelé qui offre l’interface :
• le code appelant [1] ne connait pas l’implémentation du code appelé [3]. Il ne connaît que la façon de l’appeler. C’est l’interface
[2] qui le lui indique. Celle-ci définit un certain nombre de méthodes / fonctions à utiliser pour interagir avec le code appelé.
On appelle également API (Application Programming Interface) cette interface ;
Le code appelant n'utilisera que ces méthodes. Il n'a pas à savoir comment elles sont implémentées. Les données peuvent alors
provenir de différentes sources (en dur, d'une base de données, de fichiers texte…) sans que cela impacte le code appelant. On appelle
cela la programmation par interfaces.
Python 3 a une notion qui s'approche de celle d'interface : la classe abstraite. Nous allons l'utiliser. Nous allons regrouper les
interfaces de cet exemple dans le dossier [interfaces].
Nous définissons une classe abstraite [InterfaceDao] (InterfaceDao.py) pour la couche [dao] :
1. # imports
2. from abc import ABC, abstractmethod
3.
4. # interface Dao
5. from Elève import Elève
6.
7.
8. class InterfaceDao(ABC):
9. # liste des classes
10. @abstractmethod
11. def get_classes(self: object) -> list:
12. pass
13.
14. # liste des élèves
15. @abstractmethod
16. def get_élèves(self: object) -> list:
17. pass
18.
19. # liste des matières
20. @abstractmethod
21. def get_matières(self: object) -> list:
22. pass
23.
24. # liste des notes
25. @abstractmethod
26. def get_notes(self: object) -> list:
27. pass
28.
29. # liste des notes d'un élève
30. @abstractmethod
31. def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
32. pass
33.
34. # chercher un élève par son id
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
150/755
35. @abstractmethod
36. def get_élève_by_id(self, élève_id: int) -> Elève:
37. pass
38.
Notes :
• ligne 2 : ABC=Abstract Base Class. On importe du module [abc], la classe ABC ainsi que le décorateur [abstractmethod]
utilisé aux lignes 10, 15, 20, 25, 30 et 35 ;
• ligne 8 : la classe abstraite s'appelle [InterfaceDao] et dérive de la classe [ABC] ;
• les méthodes de la classe abstraite sont décorées avec le décorateur [@abstractmethod] qui fait de la méthode ainsi décorée
une méthode abstraite : son code n'est pas défini. Néanmoins, on y met du code : l’instruction [pass] qui ne fait rien ;
• la classe abstraite [InterfaceDao] ne peut être instanciée. Seules peuvent l'être les classes dérivées de [InterfaceDao] ayant
implémenté toutes les méthodes de [InterfaceDao]. Si donc on crée deux classes [Dao1] et [Dao2] dérivant de la classe
[InterfaceDao], elles implémenteront toutes deux les méthodes abstraites de [InterfaceDao]. On pourrait dire ainsi qu'elles
implémentent l'interface [InterfaceDao] ;
• les langages implémentant à la fois les interfaces et les classes abstraites donnent à l'interface un rôle différent de celui de la
classe abstraite. Une interface n'a pas d'attributs et ne peut être instanciée. Une classe peut implémenter une interface en
définissant toutes les méthodes de celle-ci ;
14.2.4.2 Implémentation [Dao]
La classe [Dao] (dao.py) implémente l'interface [InterfaceDao] de la façon suivante :
Notes :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
151/755
• ligne 11 : la classe [Dao] dérive de la classe abstraite [InterfaceDao]. Nous dirons qu'elle implémente l'interface
[InterfaceDao] ;
• ligne 14 : le constructeur n'a pas de paramètres. Il construit en dur quatre listes :
• lignes 15-18 : la liste des classes ;
• lignes 19-22 : la liste des matières ;
• lignes 23-28 : la liste des élèves ;
• lignes 29-38 : la liste des notes ;
• lignes 40-44 : implémentation des méthodes de l’interface [Interface Dao]. Ici, nous ne les définissons pas pour voir le
message d’erreur émis par Python ;
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # instanciation couche [dao]
7. from Dao import Dao
8.
9. daoImpl = Dao()
10.
11. # liste des classes
12. for classe in daoImpl.get_classes():
13. print(classe)
14.
15. # liste des matières
16. for matière in daoImpl.get_matières():
17. print(matière)
18.
19. # liste des classes
20. for élève in daoImpl.get_élèves():
21. print(élève)
22.
23. # liste des notes
24. for note in daoImpl.get_notes():
25. print(note)
Note : le script [tests-dao.py] n'est pas un test [unittest] car il ne contient pas de méthodes avec un nom commençant par [test_].
Les commentaires se suffisent à eux-mêmes. Les lignes 11-25 utilisent l'interface de la couche [dao]. Il n'y a pas là d'hypothèses sur
l'implémentation réelle de la couche. Ligne 9, on instancie la couche [dao].
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/tests_dao.py
2. Traceback (most recent call last):
3. File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/tests_dao.py", line
9, in <module>
4. daoImpl = Dao()
5. TypeError: Can't instantiate abstract class Dao with abstract methods get_classes, get_matières, get_notes,
get_notes_for_élève_by_id, get_élève_by_id, get_élèves
6.
7. Process finished with exit code 1
On voit qu'une erreur se produit dès l'instanciation de la classe [Dao] (ligne 3 ci-dessus). L'interpréteur Python 3 nous dit qu'il ne peut
instancier la classe, ceci parce que nous n'avons pas défini les méthodes abstraites [get_classes, get_matières, get_notes,
get_notes_for_élève_by_id, get_élève_by_id, get_élèves].
Pycharm a également la notion de classe abstraite et nous propose de définir les méhodes de celle-ci :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
152/755
• en [1], cliquer droit sur le code ;
• en [2-3], choisir [Generate / Implement Methods] pour implémenter les méthodes manquantes de la classe [Dao] ;
• en [4], choisir les méthodes à implémenter, ici toutes ;
Ceci fait, la classe [Dao] est complétée par PyCharm de la façon suivante :
1. # -----------
2. # interface IDao
3. # -----------
4.
5. def get_classes(self: object) -> list:
6. pass
7.
8. def get_élèves(self: object) -> list:
9. pass
10.
11. def get_matières(self: object) -> list:
12. pass
13.
14. def get_notes(self: object) -> list:
15. pass
16.
17. def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
18. pass
19.
20. def get_élève_by_id(self, élève_id: int) -> Elève:
21. pass
1. # -----------
2. # interface IDao
3. # -----------
4.
5. # liste des classes
6. def get_classes(self) -> list:
7. return self.classes
8.
9. # liste des matières
10. def get_matières(self) -> list:
11. return self.matières
12.
13. # liste des élèves
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
153/755
14. def get_élèves(self) -> list:
15. return self.élèves
16.
17. # liste des notes
18. def get_notes(self) -> list:
19. return self.notes
20.
21. def get_notes_for_élève_by_id(self, élève_id: int) -> dict:
22. # on recherche l'élève
23. élève = self.get_élève_by_id(élève_id)
24. # on récupère ses notes
25. notes = list(filter(lambda n: n.élève.id == élève_id, self.get_notes()))
26. # on rend le résultat
27. return {"élève": élève, "notes": notes}
28.
29. def get_élève_by_id(self, élève_id: int) -> Elève:
30. # on filtre les élèves
31. élèves = list(filter(lambda e: e.id == élève_id, self.get_élèves()))
32. # trouvé ?
33. if not élèves:
34. raise MyException(10, f"L'élève d'identifiant {élève_id} n'existe pas")
35. # résultat
36. return élèves[0]
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # instanciation couche [dao]
7. from Dao import Dao
8.
9. daoImpl = Dao()
10.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
154/755
11. # liste des classes
12. for classe in daoImpl.get_classes():
13. print(classe)
14.
15. # liste des matières
16. for matière in daoImpl.get_matières():
17. print(matière)
18.
19. # liste des classes
20. for élève in daoImpl.get_élèves():
21. print(élève)
22.
23. # liste des notes
24. for note in daoImpl.get_notes():
25. print(note)
26.
27. # un élève particulier
28. print(daoImpl.get_élève_by_id(11))
29.
30. # la liste de ses notes
31. dict1 = daoImpl.get_notes_for_élève_by_id(11)
32. print(f"élève n° 11 = {dict1['élève']}")
33. for note in dict1["notes"]:
34. print(f"note de l'élève n° 11 = {note}")
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/tests/tests_dao.py
2. {"id": 1, "nom": "classe1"}
3. {"id": 2, "nom": "classe2"}
4. {"id": 1, "nom": "matière1", "coefficient": 1}
5. {"id": 2, "nom": "matière2", "coefficient": 2}
6. {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
7. {"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom": "classe1"}}
8. {"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom": "classe2"}}
9. {"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom": "classe2"}}
10. {"id": 1, "valeur": 10, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom":
"classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
11. {"id": 2, "valeur": 12, "élève": {"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom":
"classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
12. {"id": 3, "valeur": 14, "élève": {"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom":
"classe2"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
13. {"id": 4, "valeur": 16, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom":
"classe2"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
14. {"id": 5, "valeur": 6, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom":
"classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
15. {"id": 6, "valeur": 8, "élève": {"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": {"id": 1, "nom":
"classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
16. {"id": 7, "valeur": 10, "élève": {"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": {"id": 2, "nom":
"classe2"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
17. {"id": 8, "valeur": 12, "élève": {"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": {"id": 2, "nom":
"classe2"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
18. {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
19. élève n° 11 = {"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}
20. note de l'élève n° 11 = {"id": 1, "valeur": 10, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1",
"classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 1, "nom": "matière1", "coefficient": 1}}
21. note de l'élève n° 11 = {"id": 5, "valeur": 6, "élève": {"id": 11, "nom": "nom1", "prénom": "prénom1",
"classe": {"id": 1, "nom": "classe1"}}, "matière": {"id": 2, "nom": "matière2", "coefficient": 2}}
22.
23. Process finished with exit code 0
On pourra remarquer que lorsqu’on affiche une note (pour les autres objets, c’est similaire), on a également :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
155/755
14.2.5 La couche [métier]
1. # imports
2. from abc import ABC, abstractmethod
3.
4. from StatsForElève import StatsForElève
5.
6.
7. # interface Métier
8. class InterfaceMétier(ABC):
9. # calcul de statistiques pour un élève
10. @abstractmethod
11. def get_stats_for_élève(self, idElève: int) -> StatsForElève:
12. pass
• [get_stats_for_élève] rend les notes de l'élève n° idElève ainsi que des informations sur celles-ci : moyenne pondérée, note la plus
basse, note la plus haute. Ces informations sont encapsulées dans un objet de type [StatsForElève] ;
14.2.5.2 L'entité [StatsForElève]
Le type [StatsForElève] (StatsForElève.py) qui encapsule les statistiques (notes, min, max, moyenne pondérée) d'un élève est le
suivant :
1. # imports
2. from BaseEntity import BaseEntity
3.
4.
5. # statistiques d'un élève particulier
6.
7.
8. class StatsForElève(BaseEntity):
9. # attributs exclus de l'état de la classe
10. excluded_keys = []
11.
12. # propriétés de la classe
13. @staticmethod
14. def get_allowed_keys() -> list:
15. # id : identifiant de la note
16. # élève : l'élève concerné
17. # notes : ses notes
18. # moyennePondérée : sa moyenne pondérée par les coefficients des matières
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
156/755
19. # min : sa note minimale
20. # max : sa note maximale
21.
22. return BaseEntity.get_allowed_keys() + ["élève", "notes", "moyenne_pondérée", "min", "max"]
23.
24. # toString
25. def __str__(self) -> str:
26. # cas de l'élève sans notes
27. if len(self.notes) == 0:
28. return f"Elève={self.élève}, notes=[]"
29. # cas de l'élève avec notes
30. str = ""
31. for note in self.notes:
32. str += f"{note.valeur} "
33. return f"Elève={self.élève}, notes=[{str.strip()}], max={self.max}, min={self.min}, " \
34. f"moyenne pondérée={self.moyenne_pondérée:4.2f}"
Notes :
1. # imports
2. from InterfaceDao import InterfaceDao
3. from InterfaceMétier import InterfaceMétier
4. from StatsForElève import StatsForElève
5.
6.
7. class Métier(InterfaceMétier):
8.
9. # constructeur
10. def __init__(self, dao: InterfaceDao):
11. # on mémorise le paramètre
12. self.__dao = dao
13.
14. # -----------
15. # interface
16. # -----------
17.
18. # les indicateurs sur les notes d'un élève particulier
19. def get_stats_for_élève(self, id_élève: int) -> StatsForElève:
20. # Stats pour l'élève de n° idEleve
21. # id_élève : n° de l'élève
22.
23. # on récupère ses notes avec la couche [dao]
24. notes_élève = self.__dao.get_notes_for_élève_by_id(id_élève)
25. élève = notes_élève["élève"]
26. notes = notes_élève["notes"]
27.
28. # on s'arrête s'il n'y a pas de notes
29. if len(notes) == 0:
30. # on rend le résultat
31. return StatsForElève().fromdict({"élève": élève, "notes": []})
32.
33. # exploitation des notes de l'élève
34. somme_pondérée = 0
35. somme_coeff = 0
36. max = -1
37. min = 21
38. for note in notes:
39. # valeur de la note
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
157/755
40. valeur = note.valeur
41. # coefficient de la matière
42. coeff = note.matière.coefficient
43. # somme des coefficients
44. somme_coeff += coeff
45. # somme pondérée
46. somme_pondérée += valeur * coeff
47. # recherche du min
48. if valeur < min:
49. min = valeur
50. # recherche du max
51. if valeur > max:
52. max = valeur
53. # calcul des indicateurs manquants
54. moyenne_pondérée = float(somme_pondérée) / somme_coeff
55.
56. # on rend le résultat sous la forme d'un type [StatsForElève]
57. return StatsForElève(). \
58. fromdict({"élève": élève, "notes": notes,
59. "moyenne_pondérée": moyenne_pondérée,
60. "min": min, "max": max})
Notes
• ligne 7 : la classe [Métier] dérive de la classe [InterfaceMétier]. On a pris l’habitude de dire qu’elle implémente l’interface
[InterfaceMétier] ;
• lignes 9-12 : le constructeur reçoit pour seul paramètre une référence sur la couche [dao]. Ligne 10, on notera qu’on a donné
le type [InterfaceDao] au paramètre [dao]. On n’attend pas une implémentation précise mais simplement une implémentation
restectant l’interface [InterfaceDao]. Ici, ça n’a pas d’importance puisque Python ne va pas tenir compte de ce type mais c’est
une bonne habitude de travailler avec des interfaces plutôt qu’avec des implémentations précises. Le code est alors plus
facilement modifiable ;
• lignes 19-60 : implémentation de la méthode [get_stats_for_élève] ;
• ligne 19 : la méthode reçoit un unique paramètre, le n° [idElève] de l’élève dont on veut les statistiques ;
• ligne 24 : on demande à la couche [dao], les notes de l’élève. Cette demande débouche sur une exception si l’élève n’existe pas.
Celle-ci n’est pas gérée (absence de try / catch) et remonte donc au code appelant ;
• ligne 25 : on arrive ici s’il n’y a pas eu exception. [notes_élève] est alors un dictionnaire à deux clés [élève, note] :
o ligne 25 : on récupère les informations sur l’élève (son nom, sa classe, …) ;
o ligne 26 : on récupère ses notes ;
• lignes 28-31 : on regarde si l’élève a des notes. S’il n’en a pas, il n’y a pas de statistiques à calculer ;
• ligne 31 : on rend un objet [StatsForElève] construit à partir d’un dictionnaire avec la méthode [BaseEntity.fromdict] ;
• lignes 33-54 : on exploite les notes de l’élève pour calculer les statistiques demandées. Les commentaires du code devraient
suffire à sa compréhension ;
• lignes 56-60 : on rend un objet [StatsForElève] construit à partir d’un dictionnaire avec la méthode [BaseEntity.fromdict] ;
14.2.5.4 Test de la couche [métier]
Un script [UnitTest] de la couche [métier] pourrait être le suivant (TestMétier.py) :
1. # imports
2. import unittest
3.
4.
5. class Testmétier(unittest.TestCase):
6. def setUp(self):
7. # on configure l'application
8. import config
9. config.configure()
10.
11. def test_statsForEleve11(self):
12. # imports
13. from Dao import Dao
14. from Métier import Métier
15. # on teste les indicateurs de l'élève 11
16. dao = Dao()
17. stats_for_élève = Métier(dao).get_stats_for_élève(11)
18. # affichage
19. print(f"\nstats={stats_for_élève}")
20. # vérifications
21. self.assertEqual(stats_for_élève.min, 6)
22. self.assertEqual(stats_for_élève.max, 10)
23. self.assertAlmostEqual(stats_for_élève.moyenne_pondérée, 7.333, delta=1e-3)
24.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
158/755
25.
26. if __name__ == '__main__':
27. unittest.main()
Notes
• lignes 6-9 : la fonction [setUp] est ici utilisée pour configurer le Python Path du test ;
• ligne 16 : on instancie la couche [dao] ;
• ligne 17 : on instancie la couche [métier] et on utilise sa méthode [get_stats_for_élève] pour calculer les statistiques de
l’élève n° 11 ;
• ligne 19 : on affiche le résultat [StatsForElève] obtenu. Comme [StatsForElève] dérive de [BaseEntity], c’est la chaîne
jSON de [StatsForElève] qui est ici affichée ;
• ligne 21 : on vérifie la note minimale de l’élève ;
• ligne 22 : on vérifie sa note maximale ;
• ligne 23 : on teste que la moyenne pondérée vaut 7,333 à 10-3 près. En général, il n'est pas possible de comparer les nombres
réels de façon exacte car de façon interne, ils n'ont le plus souvent qu'une représentation approchée ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
159/755
14.2.6.1 Interface [InterfaceUi]
L'interface de la couche [UI] sera la suivante :
1. # imports
2. from abc import ABC, abstractmethod
3.
4.
5. # interface UI
6. class InterfaceUi(ABC):
7. # exécution de la couche UI
8. @abstractmethod
9. def run(self: object):
10. pass
Notes
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
160/755
• lignes 12-17 : le constructeur de la classe [Console] reçoit en paramètre une référence sur la couche [métier]. On notera
qu’on a donné le type [InterfaceMétier] à ce paramètre pour rappeler qu’on travaille avec des interfaces plutôt qu’avec des
implémentations précises ;
• ligne 24 : implémentation de la méthode [run] de l'interface ;
• ligne 27 : une boucle qui s’arrête lorsque la condition de la ligne 31 est vérifiée ;
• ligne 29 : saisie d’une donnée tapée au clavier. La fonction [input] reçoit un paramètre facultatif : le message à écrire sur
l’écran pour demander la saisie. Celle-ci est toujours récupérée comme une chaîne de caractères. La fonction [strip]
débarrasse celle-ci de ses « blancs » qui la suivent ou la précèdent ;
• lignes 34-39 : on vérifie que la saisie, un n° d’élève, est valide. Il faut que ce soit un entier >=1. On rappelle que la saisie a été
faite en tant que chaîne de caractères ;
• ligne 36 : on essaie de transformer la saisie en nombre entier en base 10. La fonction [int] lance une exception si ce n’est pas
possible ;
• ligne 37 : on arrive là seulement s’il n’y a pas eu d’exception. On vérifie que le nombre entier récupéré est bien >=1 ;
• lignes 38-39 : on gère l’exception. S’il y a eu exception, la variable [ok] de la ligne 34 est restée à [False] ;
• lignes 41-43 : si la saisie a été incorrecte, on affiche un message d’erreur et on reboucle (ligne 43) ;
• lignes 45-48 : on calcule les statistiques de l’élève dont on a saisi le n° ;
• ligne 46 : on utilise la méthode [get_stats_for_élève] de la couche [métier]. Celle-ci lance une exception si l’élève n’existe
pas. Celle-ci est gérée aux lignes 47-48. On sait que les couches [dao] et [métier] lancent l’exception [MyException] ;
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. from Console import Console
8. from Dao import Dao
9. from Métier import Métier
10.
11. # ----------- couche [console]
12. try:
13. # instanciation couche [dao]
14. dao = Dao()
15. # instanciation couche [métier]
16. métier = Métier(dao)
17. # instanciation couche [ui]
18. console = Console(métier)
19. # exécution couche [console]
20. console.run()
21. except BaseException as ex:
22. # on affiche l'erreur
23. print(f"L'erreur suivante s'est produite : {ex}")
24. finally:
25. pass
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
161/755
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v01/main/main.py
2. Numéro de l'élève (>=1 et * pour arrêter) : 11
3. Elève={"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": {"id": 1, "nom": "classe1"}}, notes=[10 6],
max=10, min=6, moyenne pondérée=7.33
4. Numéro de l'élève (>=1 et * pour arrêter) : 1
5. L'erreur suivante s'est produite : MyException[10, L'élève d'identifiant 1 n'existe pas]
6. Numéro de l'élève (>=1 et * pour arrêter) : *
7.
8. Process finished with exit code 0
14.4 Exemple 2
Ce nouvel exemple d’architectures en couches vise à montrer l’intérêt de la programmation par interfaces. Celle-ci facilite la
maintenance et le test des applications. Nous utiliserons de nouveau une architecture à trois couches :
Chaque couche sera implémentée de deux façons différentes. Nous voulons montrer que l’on peut changer facilement
l’implémentation d’une couche avec un impact minimal sur les autres.
1. # imports
2. from abc import ABC, abstractmethod
3.
4.
5. # interface Dao
6. class InterfaceDao(ABC):
7. # une seule méthode
8. @abstractmethod
9. def do_something_in_dao_layer(self, x: int, y: int) -> int:
10. pass
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
162/755
• lignes 8-10: la méthode [do_something_in_dao_layer] est l’unique méthode de l’interface ;
1. # imports
2. from abc import ABC, abstractmethod
3.
4.
5. # interface métier
6. class InterfaceMétier(ABC):
7. # une seule méthode
8. @abstractmethod
9. def do_something_in_métier_layer(self, x: int, y: int) -> int:
10. pass
1. # imports
2. from abc import ABC, abstractmethod
3.
4. from InterfaceDao import InterfaceDao
5. from InterfaceMétier import InterfaceMétier
6.
7.
8. class AbstractBaseMétier(InterfaceMétier, ABC):
9. # propriétés
10. # __dao est une référence sur la couche [dao]
11. @property
12. def dao(self) -> InterfaceDao:
13. return self.__dao
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
163/755
14.
15. @dao.setter
16. def dao(self, dao: InterfaceDao):
17. self.__dao = dao
18.
19. # implémentation de l'interface [InterfaceMétier]
20. @abstractmethod
21. def do_something_in_métier_layer(self, x: int, y: int) -> int:
22. pass
C’est la première fois qu’on utilise l’héritage multiple (hériter de plusieurs classes). La classe [AbstractBaseMétier] hérite, à la fois,
des propriétés des classes [InterfaceMétier] et [ABC].
• lignes 9-17 : on définit la propriété [dao] qui sera une référence sur la couche [dao] ;
Une interface est destinée à être implémentée. Lorsque des implémentations différentes partagent des propriétés il est intéressant de
mettre celles-ci dans une classe parent afin d’éviter de les dupliquer. C’est le cas ici de la propriété [dao]. La classe parent est en
général toujours abstraite parce qu’elle ne sait pas implémenter toutes les méthodes de l’interface.
• ligne 4 : la classe [MétierImpl1] dérive de la classe [AbstractbaseMétier]. Elle hérite donc de la propriété [dao] de cette
classe ;
• lignes 6-9 : implémentation de l’interface [InterfaceMétier] que n’a pas implémentée la classe parent [AbstractbaseMétier] ;
• ligne 9 : on utilise la couche [dao] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
164/755
14.4.3 La couche [ui]
1. # imports
2. from abc import ABC, abstractmethod
3.
4.
5. # interface Ui
6. class InterfaceUi(ABC):
7. # une seule méthode
8. @abstractmethod
9. def do_something_in_ui_layer(self, x: int, y: int) -> int:
10. pass
1. # imports
2. from abc import ABC, abstractmethod
3.
4. from InterfaceMétier import InterfaceMétier
5. from InterfaceUi import InterfaceUi
6.
7.
8. class AbstractBaseUi(InterfaceUi, ABC):
9. # propriétés
10. # métier est une référence sur la couche [métier]
11. @property
12. def métier(self) -> InterfaceMétier:
13. return self.__métier
14.
15. @métier.setter
16. def métier(self, métier: InterfaceMétier):
17. self.__métier = métier
18.
19. # implémentation de l'interface [InterfaceUI]
20. @abstractmethod
21. def do_something_in_ui_layer(self: InterfaceUi, x: int, y: int) -> int:
22. pass
• la classe [AbstractBaseUi] est une classe abstraite (ligne 20). Elle devra être dérivée pour implémenter l’interface
[InterfaceUi] ;
• lignes 9-17 : la classe [AbstractBaseUi] possède une référence sur la couche [métier] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
165/755
8. y += 1
9. return self.métier.do_something_in_métier_layer(x, y)
• ligne 4 : la classe [UiImpl1] dérive de la classe [AbstractBaseUi] et hérite donc de sa propriété [métier]. Celle-ci est utilisée
ligne 9 ;
• ligne 4 : la classe [UiImpl2] dérive de la classe [AbstractBaseUi] et hérite donc de sa propriété [métier]. Celle-ci est utilisée
ligne 9 ;
1. def configure():
2. # étape 1 ------
3. # chemin absolu du dossier de ce script
4. import os
5. script_dir = os.path.dirname(os.path.abspath(__file__))
6. # dépendances
7. absolute_dependencies = [
8. # dossiers locaux du Python Path
9. f"{script_dir}/../dao",
10. f"{script_dir}/../ui",
11. f"{script_dir}/../métier",
12. ]
13.
14. # on configure le syspath
15. from myutils import set_syspath
16. set_syspath(absolute_dependencies)
17.
18. # étape 2 ------
19. # configuration des couches de l'application
20. from DaoImpl1 import DaoImpl1
21. from MétierImpl1 import MétierImpl1
22. from UiImpl1 import UiImpl1
23. # instanciation des couches
24. # dao
25. dao = DaoImpl1()
26. # métier
27. métier = MétierImpl1()
28. métier.dao = dao
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
166/755
29. # ui
30. ui = UiImpl1()
31. ui.métier = métier
32.
33. # on met les instances de couche dans la config
34. # seule la couche ui est ici nécessaire
35. config = {"ui": ui}
36.
37. # on rend la config
38. return config
Le fichier [config2] est analogue et implémente chaque interface avec la 2ième implémentation disponible :
1. def configure():
2. # étape 1 ---
3. # chemin absolu du dossier de ce script
4. import os
5. script_dir = os.path.dirname(os.path.abspath(__file__))
6. # dépendances
7. absolute_dependencies = [
8. # dossiers locaux du Python Path
9. f"{script_dir}/../dao",
10. f"{script_dir}/../ui",
11. f"{script_dir}/../métier",
12. ]
13.
14. # on configure le syspath
15. from myutils import set_syspath
16.
17. set_syspath(absolute_dependencies)
18.
19. # étape 2 ------
20. # configuration des couches de l'application
21. from DaoImpl2 import DaoImpl2
22. from MétierImpl2 import MétierImpl2
23. from UiImpl2 import UiImpl2
24. # instanciation des couches
25. # dao
26. dao = DaoImpl2()
27. # métier
28. métier = MétierImpl2()
29. métier.dao = dao
30. # ui
31. ui = UiImpl2()
32. ui.métier = métier
33.
34. # on met les instances de couche dans la config
35. # seule la couche ui est ici nécessaire
36. config = {"ui": ui}
37.
38. # on rend la config
39. return config
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
167/755
14.4.5 Le script principal [main]
1. # imports
2. import importlib
3. import sys
4.
5. # main ---------
6.
7. # il faut deux arguments
8. nb_args = len(sys.argv)
9. if nb_args != 2 or (sys.argv[1] != "config1" and sys.argv[1] != "config2"):
10. print(f"Syntaxe : {sys.argv[0]} config1 ou config2")
11. sys.exit()
12.
13. # configuration de l'application
14. module = importlib.import_module(sys.argv[1])
15. config = module.configure()
16.
17. # exécution de la couche [ui]
18. print(config["ui"].do_something_in_ui_layer(10, 20))
Une fois connue la configuration souhaitée, il nous faut exécuter cette configuration. Par exemple, si c’est la configuration 1 qui a été
choisie, il nous faut exécuter le code :
1. import config1
2. config1.configure()
Le problème ici est que la configuration à utiliser est dans une variable, la variable [sys.argv[1]. Pour importer un module dont le
nom est dans une variable, il nous faut utiliser le package [importlib] (ligne 2).
1. class UiImpl1(AbstractBaseUi):
2. # implémentation de l'interface [InterfaceUi]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
168/755
3. def do_something_in_ui_layer(self: AbstractBaseUi, x: int, y: int) -> int:
4. x += 1
5. y += 1
6. return self.métier.do_something_in_métier_layer(x, y)
• ligne 6 ci-dessus utilise la propriété [métier] de la classe [UiImpl1], ligne 1. Or dans la configuration [config1] il a été écrit :
1. # métier
2. métier = MétierImpl1()
3. métier.dao = dao
4. # ui
5. ui = UiImpl1()
6. ui.métier = métier
• ligne 6 : la propriété [métier] de [UIImpl1] est une référence à la classe [MétierImpl1] (ligne 2). Ainsi c’est la méthode
[do_something_in_ui_layer] de la classe [MétierImpl1] qui va être exécutée ;
1. class MétierImpl1(AbstractBaseMétier):
2. # implémentation de l'interface [InterfaceMétier]
3. def do_something_in_métier_layer(self: AbstractBaseMétier, x: int, y: int) -> int:
4. x += 1
5. y += 1
6. return self.dao.do_something_in_dao_layer(x, y)
• ligne 6, la méthode appelée par la couche [ui] va à son tour appelée une méthode de la propriété [dao] de la classe
[MétierImpl1] ;
1. # dao
2. dao = DaoImpl1()
3. # métier
4. métier = MétierImpl1()
5. métier.dao = dao
Ce qu’on veut montrer ici, c’est que le script [main] n’a pas à se préoccuper des couches [métier] et [dao]. Il n’a à se préoccuper que
de la couche [ui], les liens entre cette couche et les autres ayant été faits par configuration.
Pour passer le paramètre [config1] ou [config2] au script [main], on procèdera de la façon suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
169/755
• en [1-2], on crée ce qu’on appelle une configuration d’exécution ;
• en [3], on donne un nom à cette configuration pour pouvoir la retrouver ;
• en [4], on sélectionne le script à exécuter. Si on a suivi la procédure [1-2], le bon script a déjà été sélectionné ;
• en [5], on met ici les paramètres à transmettre au script. On passe ici la chaîne [config1] pour demander au script d’utiliser
la configuration n° 1 ;
• en [6], on valide la configuration d’exécution ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
170/755
• en [5], le nom donné à la nouvelle configuration. Ce sera celle qui exécute le script [main] [6] en lui passant le paramètre
[config2] [7] ;
Il suffit de sélectionner [2] ou [3] puis d’appuyer sur [4] pour exécuter le script [main] avec l’un ou l’autre des paramètres [config1]
ou [config2].
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v02/main/main.py config1
2. 34
3.
4. Process finished with exit code 0
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/troiscouches/v02/main/main.py config2
2. -10
3.
4. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
171/755
15 Exercice d'application - version 4
Nous reprenons ici l'exercice décrit au paragraphe |Version 3| et nous le traitons maintenant avec des classes et interfaces. Nous
écrirons deux applications :
Un script principal [main] instanciera une couche [dao] et une couche [métier] :
• la couche [dao] aura pour rôle de gérer des données stockées dans des fichiers texte et ultérieurement dans une base de
données ;
• la couche [métier] aura pour rôle de faire le calcul de l'impôt ;
Dans cette application, il n'y aura pas d'actions de la part d'un utilisateur : les données des contribuables seront trouvées dans un
fichier texte dont on donnera le nom au module [main].
Dans l’application 2, c'est l'utilisateur qui tapera au clavier les données des contribuables. L'architecture évoluera alors de la façon
suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
172/755
• la couche [dao] (Data Access Object) s'occupe de l'accès aux données externes
• la couche [métier] s'occupe des problèmes métier, ici le calcul de l'impôt. Elle ne s'occupe pas des données. Celles-ci
peuvent avoir deux provenances :
• la couche [dao] pour les données persistantes ;
• la couche [ui] pour les données fournies par l'utilisateur.
• la couche [ui] (User Interface) s'occupe des interactions avec l'utilisateur ;
• [main] est le chef d'orchestre ;
Dans la suite, les couches [dao], [métier] et [ui] seront chacune implémentée à l'aide d'une classe. Les couches [métier] et [dao]
seront les mêmes pour les deux applications. C’est la raison pour laquelle on les a réunies dans une même version de l’exercice
d’application.
vérifier la validité de celles-ci. Les entités sont échangées par les couches. Une même entité peut partir de la couche [ui] pour aller
jusqu’à la couche [dao] et vice-versa.
1. # -------------------------------
2. # classe d'exception
3. from MyException import MyException
4.
5.
6. class ImpôtsError(MyException):
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
173/755
7. pass
Dès que les couches [métier] et [dao] rencontreront un problème, elles lanceront cette exception. Elle dérive de la classe
[MyException]. Elle s’utilise donc de la façon suivante : [raise ImpôtsError(code_erreur, msg_erreur)].
• ligne 5 : la classe [AdminData] étend la classe [BaseEntity] décrite au paragraphe |BaseEntity|. On se rappelle que les classes
étendant la classe [BaseEntity] doivent définir :
o un attribut de classe [excluded_keys] (ligne 7) qui liste les propriétés de l’objet exclues lorsque l’objet est transformé en
dictionnaire ;
o une méthode statique [get_allowed_keys] (lignes 10-26) qui rend la liste des propriétés acceptées lorsque l’objet est
initialisé avec un dictionnaire ;
On n’a pas utilisé de setters pour vérifier la validité des données utilisées pour initialiser un objet [AdminData]. En effet, cet objet est
unique et défini par configuration et donc pas susceptible d’être erroné.
15.1.1.3 La classe [TaxPayer]
La classe [TaxPayer] modèlisera un contribuable :
1. # imports
2. from BaseEntity import BaseEntity
3. from ImpôtsError import ImpôtsError
4.
5.
6. # un contribuable
7. class TaxPayer(BaseEntity):
8. # modélise un contribuable
9. # id : identifiant
10. # marié : oui / non
11. # enfants : son nombre d'enfants
12. # salaire : son salaire annuel
13. # impôt : montant de l'impôt à payer
14. # surcôte : surcôte d'impôt à payer
15. # décôte : décôte de l'impôt à payer
16. # réduction : réduction sur l'impôt à payer
17. # taux : taux d'imposition du contribuable
18.
19. # clés exclues de l'état de la classe
20. excluded_keys = []
21.
22. # clés aurorisées
23. @staticmethod
24. def get_allowed_keys() -> list:
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
174/755
25. return ['id', 'marié', 'enfants', 'salaire', 'impôt', 'surcôte', 'décôte', 'réduction', 'taux']
26.
27. # properties
28. @property
29. def marié(self) -> str:
30. return self.__marié
31.
32. @property
33. def enfants(self) -> int:
34. return self.__enfants
35.
36. @property
37. def salaire(self) -> int:
38. return self.__salaire
39.
40. @property
41. def impôt(self) -> int:
42. return self.__impôt
43.
44. @property
45. def surcôte(self) -> int:
46. return self.__surcôte
47.
48. @property
49. def décôte(self) -> int:
50. return self.__décôte
51.
52. @property
53. def réduction(self) -> int:
54. return self.__réduction
55.
56. @property
57. def taux(self) -> float:
58. return self.__taux
59.
60. # setters
61. @marié.setter
62. def marié(self, marié: str):
63. ok = isinstance(marié, str)
64. if ok:
65. marié = marié.strip().lower()
66. ok = marié == "oui" or marié == "non"
67. if ok:
68. self.__marié = marié
69. else:
70. raise ImpôtsError(31, f"l'attribut marié [{marié}] doit avoir l'une des valeurs oui / non")
71.
72. @enfants.setter
73. def enfants(self, enfants):
74. # enfants doit être un entier >=0
75. try:
76. enfants = int(enfants)
77. erreur = enfants < 0
78. except:
79. erreur = True
80. if not erreur:
81. self.__enfants = enfants
82. else:
83. raise ImpôtsError(32, f"L'attribut enfants [{enfants}] doit être un entier >=0")
84.
85. @salaire.setter
86. def salaire(self, salaire):
87. # salaire doit être un entier >=0
88. try:
89. salaire = int(salaire)
90. erreur = salaire < 0
91. except:
92. erreur = True
93. if not erreur:
94. self.__salaire = salaire
95. else:
96. raise ImpôtsError(33, f"L'attribut salaire [{salaire}] doit être un entier >=0")
97.
98. @impôt.setter
99. def impôt(self, impôt):
100. # impôt doit être un entier >=0
101. try:
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
175/755
102. impôt = int(impôt)
103. erreur = impôt < 0
104. except:
105. erreur = True
106. if not erreur:
107. self.__impôt = impôt
108. else:
109. raise ImpôtsError(34, f"L'attribut impôt [{impôt}] doit être un nombre >=0")
110.
111. @décôte.setter
112. def décôte(self, décôte):
113. # décôte doit être un entier >=0
114. try:
115. décôte = int(décôte)
116. erreur = décôte < 0
117. except:
118. erreur = True
119. if not erreur:
120. self.__décôte = décôte
121. else:
122. raise ImpôtsError(35, f"L'attribut décôte [{décôte}] doit être un nombre >=0")
123.
124. @surcôte.setter
125. def surcôte(self, surcôte):
126. # surcôte doit être un entier >=0
127. try:
128. surcôte = int(surcôte)
129. erreur = surcôte < 0
130. except:
131. erreur = True
132. if not erreur:
133. self.__surcôte = surcôte
134. else:
135. raise ImpôtsError(36, f"L'attribut surcôte [{surcôte}] doit être un nombre >=0")
136.
137. @réduction.setter
138. def réduction(self, réduction):
139. # surcôte doit être un entier >=0
140. try:
141. réduction = int(réduction)
142. erreur = réduction < 0
143. except:
144. erreur = True
145. if not erreur:
146. self.__réduction = réduction
147. else:
148. raise ImpôtsError(37, f"L'attribut réduction [{réduction}] doit être un nombre >=0")
149.
150. @taux.setter
151. def taux(self, taux):
152. # taux doit être un réel >=0
153. try:
154. taux = float(taux)
155. erreur = taux < 0
156. except:
157. erreur = True
158. if not erreur:
159. self.__taux = taux
160. else:
161. raise ImpôtsError(38, f"L'attribut taux [{taux}] doit être un nombre >=0")
162.
Notes :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
176/755
15.1.2 La couche [dao]
Nous allons réunir les implémentations des couches dans un dossier [services]. Ces classes implémenteront des interfaces définies
dans le dossier [interfaces].
1. # imports
2. from abc import ABC, abstractmethod
3.
4.
5. # interface IImpôtsDao
6. from AdminData import AdminData
7.
8.
9. class InterfaceImpôtsDao(ABC):
10. # liste des tranches de l'impôt
11. @abstractmethod
12. def get_admindata(self) -> AdminData:
13. pass
14.
15. # liste des données contribuables
16. @abstractmethod
17. def get_taxpayers_data(self) -> dict:
18. pass
19.
20. # écriture des résultats du calcul de l'impôt
21. @abstractmethod
22. def write_taxpayers_results(self, taxpayers_results: list):
23. pass
• [get_admindata] : est la méthode qui obtient le tableau des tranches d'impôt. On notera qu'on ne donne aucun renseignement
sur la façon d'obtenir ces données. Dans la suite, elles seront trouvées d'abord dans un fichier texte puis dans une base de
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
177/755
données. Ce seront aux classes qui implémentent l'interface de s'adapter au mode de stockage des données. On aura donc une
classe pour récupérer les tranches d'impôt dans un fichier texte et une autre pour les récupérer dans une base de données.
Elles implémenteront toutes deux la méthode [get_admindata] ;
• [get_taxpayers_data] : est la méthode qui obtient les données des contribuables. Là encore nous ne disons pas où elles seront
trouvées. Nous ne traiterons que le cas où elles sont dans un fichier texte ;
• [write_taxpayers_results] : est la méthode qui va persister les résultats du calcul de l'impôt. Nous ne disons pas où. Nous
ne traiterons que le cas où les résultats sont persistés dans un fichier texte. Le paramètre [taxpayers_results] sera la liste des
résultats à persister ;
15.1.2.2 La classe [AbstractImpôtsDao]
La couche [dao] va être implémentée par deux classes :
• l'une ira chercher les données (contribuables, résultats, tranches d'impôt) dans des fichiers texte ;
• l'autre ira chercher les données (contribuables, résultats) dans des fichiers texte et les tranches de l'impôt dans une base de
données ;
Les deux classes ne vont différer que par la gestion des tranches de l'impôt. Les données contribuables et les résultats des calculs de
l'impôt seront, elles, gérées de la même façon. Pour cette raison, nous allons les gérer dans une classe parent [AbstractImpôtsDao].
La particularité de la gestion des tranches d'impôt sera, elle, gérée dans deux classes filles :
• la classe [ImpôtsDaoWithAdminDataInJsonFile] ira chercher les tranches de l'impôt dans un fichier texte au format jSON ;
• la classe [ImpôtsDaoWithAdminDataInDatabase] ira chercher les tranches de l'impôt dans une base de données ;
1. # imports
2. import codecs
3. import json
4. from abc import abstractmethod
5.
6. from AdminData import AdminData
7. from ImpôtsError import ImpôtsError
8. from InterfaceImpôtsDao import InterfaceImpôtsDao
9. from TaxPayer import TaxPayer
10.
11.
12. # classe de base pour la couche [dao]
13. class AbstractImpôtsDao(InterfaceImpôtsDao):
14. # les contribuables et leur impôt seront dans des fichiers texte
15. # constructeur
16. def __init__(self, config: dict):
17. # config[taxpayersFilename] : le nom du fichier texte des contribuables
18. # config[resultsFilename] : le nom du fichier jSON des résultats
19. # config[errorsFilename] : le nom du fichier des erreurs
20.
21. # on mémorise les paramètres
22. self.taxpayers_filename = config.get("taxpayersFilename")
23. self.taxpayers_results_filename = config.get("resultsFilename")
24. self.errors_filename = config.get("errorsFilename")
25.
26. # ------------------
27. # interface IImpôtsDao
28. # ------------------
29.
30. # liste des données contribuables
31. def get_taxpayers_data(self) -> dict:
32. …
33.
34. # écriture de l'impôt des contribuables
35. def write_taxpayers_results(self, taxpayers: list):
36. …
37.
38. # lecture des tranches de l'impôt
39. @abstractmethod
40. def get_admindata(self) -> AdminData:
41. pass
• ligne 13 : la classe [AbstractImpôtsDao] implémente l'interface [InterfaceImpôtsDao]. Aussi trouve-t-on les trois méthodes
de cette interface :
o [get_taxpayers_data] : ligne 31 ;
o [write_taxpayers_results] : ligne 35 ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
178/755
o [get_admindata] : ligne 40. Cette méthode ne sera pas implémentée par la classe [AbstractImpôtsDao] aussi est-elle
déclarée abstraite (ligne 39) ;
• ligne 4 : les données des contribuables (marié, enfants, salaire) seront placées dans une liste d'objets de type [TaxPayer] ;
• lignes 8-9 : on ouvre le fichier texte des contribuables en lecture. Son contenu a la forme suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
179/755
20. # des valeurs erronées
21. x,x,x,x
• chaque ligne du fichier [taxpayersFilename] commence par l'identifiant du contribuable, un simple numéro ;
• les commentaires et les lignes vides sont autorisés ;
• on va gérer les erreurs. Ainsi les lignes 17, 19 et 21 doivent être déclarées erronées. Les erreurs sont consignées dans un fichier
à part ;
• ligne 4 : les données du fichier texte sont transférées dans la liste [taxPayersData] ;
• lignes 14-31 : le fichier des contribuables est lu ligne par ligne ;
• ligne 14 : la fin du fichier est atteinte lorsqu’on lit une ligne vide (rien – pas même la marque de fin de ligne \r\n) ;
• ligne 20 : on ignore les lignes vides et les commentaires. Une ligne est un commentaire si, une fois la ligne débarrassée de ses
blancs devant et derrière le texte, le 1er caractère est le caractère # ;
• ligne 24 : une ligne correcte est composée de quatre champs séparés par une virgule. On récupère ceux-ci. L’affectation de
données à un tuple de quatre éléments échoue s’il n’y a pas exactement quatre données affectées ;
• ligne 25 : si l’un des quatre champs récupérés [id, marié, enfants, salaire] est invalide alors la méthode
[BaseEntity.fromdict] lancera une exception de type [MyException] ;
• lignes 25-26 : un objet [TaxPayer] est ajouté à la liste [taxpayers_data] des contribuables ;
• lignes 27-29 : les éventuelles erreurs sont cumulées dans une liste [erreurs]. Cette liste a été créée ligne 6 ;
• lignes 33-36 : la liste des erreurs rencontrées est enregistrée dans le fichier texte [errorsFilename]. Elles sont de deux types :
o une ligne n’avait pas le nombre correct de champs attendus ;
o les informations de la ligne étaient erronées et ont échoué à construire un objet [TaxPayer] ;
• lignes 39-41 : on intercepte toute erreur (BaseException) et on la remonte en l'encapsulant dans un type [ImpôtsError] ;
• lignes 42-45 : dans tous les cas, réussite ou échec, le fichier texte des contrbuables est fermé s'il a été ouvert ;
1. [
2. {
3. "id": 1,
4. "marié": "oui",
5. "enfants": 2,
6. "salaire": 55555,
7. "impôt": 2814,
8. "surcôte": 0,
9. "taux": 0.14,
10. "décôte": 0,
11. "réduction": 0
12. },
13. {
14. "id": 2,
15. "marié": "oui",
16. "enfants": 2,
17. "salaire": 50000,
18. "impôt": 1384,
19. "surcôte": 0,
20. "taux": 0.14,
21. "décôte": 384,
22. "réduction": 347
23. },
24. {
25. "id": 3,
26. "marié": "oui",
27. "enfants": 3,
28. "salaire": 50000,
29. "impôt": 0,
30. "surcôte": 0,
31. "taux": 0.14,
32. "décôte": 720,
33. "réduction": 0
34. },
35. …
36. ]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
180/755
2. def write_taxpayers_results(self, taxpayers: list):
3. # écriture des résultats dans un fichier jSON
4. # taxpayers : liste d'objets de type TaxPayer
5. # (id, marié, enfants, salaire, impôt, surcôte, décôte, réduction, taux)
6. # la liste [taxpayers] est enregistrée dans le fichier texte [self.taxpayers_results_filename]
7. file = None
8. try:
9. # ouverture du fichier des résultats
10. file = codecs.open(self.taxpayers_results_filename, "w", "utf8")
11. # création de la liste à sérialiser en jSON
12. mapping = map(lambda taxpayer: taxpayer.asdict(), taxpayers)
13. # sérialisation jSON
14. json.dump(list(mapping), file, ensure_ascii=False)
15. except BaseException as erreur:
16. # on relance l'erreur sous un autre type
17. raise ImpôtsError(12, f"{erreur}")
18. finally:
19. # on ferme le fichier s'il a été ouvert
20. if file:
21. file.close()
• ligne 2 : la méthode reçoit une liste de contribuables [taxpayers] qu'elle doit enregistrer dans le fichier texte
[self.taxpayers_results_filename] au format jSON ;
• ligne 10 : création du fichier UTF-8 des résultats ;
• ligne 12 : nous introduisons ici la fonction [map] dont la syntaxe ici est [map (fonction, liste1)]. La fonction [fonction]
est appliquée à chaque élément de [liste1] et produit un nouvel élément qui alimente une liste [liste2]. Finalement, pour
chaque i :
liste2[i]=fonction(liste1[i])
Ici, [liste1] est la liste [taxPayers], une liste d'objets de type [TaxPayer]. La fonction [fonction] est exprimée ici
sous la forme d'une fonction dite [lambda] qui exprime la transformation faite sur un élément [taxpayer] de la liste
[taxpayers] : chaque élément [taxpayer] est remplacé par son dictionnaire [taxpayer.asdict()]. Finalement, la liste
[liste2] obtenue est la liste des dictionnaires des éléments de la liste [taxpayers] ;
• ligne 12 : le résultat rendu par la fonction [map] n'est pas la liste [liste2] mais un objet de type [map]. Pour avoir [liste2],
il faut utiliser l'expression [list(mapping)] (ligne 14) ;
• ligne 14 : la liste [liste2] est enregistrée au format jSON dans le fichier [self.taxpayers_results_filename] ;
• lignes 15-17 : tout type d'exception est intercepté et encapsulé dans une erreur de type [ImpôtsError] avant d'être relancé
(ligne 17) ;
• lignes 19-21 : dans tous les cas, réussite ou échec, le fichier des résultats est fermé s'il a été ouvert ;
15.1.2.3 Classe [ImpôtsDaoWithAdminDataInJsonFile]
La classe [ImpôtsDaoWithAdminDataInJsonFile] va dériver de la classe [AbstractImpôtsDao] et implémenter la méthode
[getAdminData] que sa classe parent n'a pas implémentée. Elle ira chercher les données de l'administration fiscale dans un fichier
jSON :
1. {
2. "limites": [9964, 27519, 73779, 156244, 0],
3. "coeffr": [0, 0.14, 0.3, 0.41, 0.45],
4. "coeffn": [0, 1394.96, 5798, 13913.69, 20163.45],
5. "plafond_qf_demi_part": 1551,
6. "plafond_revenus_celibataire_pour_reduction": 21037,
7. "plafond_revenus_couple_pour_reduction": 42074,
8. "valeur_reduc_demi_part": 3797,
9. "plafond_decote_celibataire": 1196,
10. "plafond_decote_couple": 1970,
11. "plafond_impot_couple_pour_decote": 2627,
12. "plafond_impot_celibataire_pour_decote": 1595,
13. "abattement_dixpourcent_max": 12502,
14. "abattement_dixpourcent_min": 437
15. }
1. # imports
2. import codecs
3. import json
4.
5. from AbstractImpôtsDao import AbstractImpôtsDao
6. from AdminData import AdminData
7. from ImpôtsError import ImpôtsError
8.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
181/755
9.
10. # une implémentation de la couche [dao] où les données de l'administration fiscale sont dans un fichier jSON
11. class ImpôtsDaoWithAdminDataInJsonFile(AbstractImpôtsDao):
12. # constructeur
13. def __init__(self, config: dict):
14. # config[admindataFilename] : le nom du fichier jSON contenant les données de l'administration fiscale
15. # config[taxpayersFilename] : le nom du fichier texte des contribuables
16. # config[resultsFilename] : le nom du fichier jSON des résultats
17. # config[errorsFilename] : le nom du fichier des erreurs
18.
19. # initialisation de la classe Parent
20. AbstractImpôtsDao.__init__(self, config)
21. # lecture des données de l'administration fiscale
22. file = None
23. try:
24. # ouverture du fichier jSON des données fiscales en lecture
25. file = codecs.open(config["admindataFilename"], "r", "utf8")
26. # transfert du contenu du fichier jSON dans un objet [AdminData]
27. self.admindata = AdminData().fromdict(json.load(file))
28. except BaseException as erreur:
29. # on relance l'erreur sous la forme d'un type [ImpôtsError]
30. raise ImpôtsError(21, f"{erreur}")
31. finally:
32. # fermeture du fichier s'il a été ouvert
33. if file:
34. file.close()
35.
36. # -------------
37. # interface
38. # -------------
39.
40. # récupération des données de l'administration fiscale
41. # la méthode rend un objet [AdminData]
42. def get_admindata(self) -> AdminData:
43. return self.admindata
1. # imports
2. from abc import ABC, abstractmethod
3.
4. from AdminData import AdminData
5. from TaxPayer import TaxPayer
6.
7.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
182/755
8. # interface IImpôtsMétier
9. class InterfaceImpôtsMétier(ABC):
10. # calcul de l'impôt pour 1 contribuable
11. @abstractmethod
12. def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData):
13. pass
Les méthodes de la classe sont issues du module [impôts_module_02] du paragraphe |Le module
[impots.v02.modules.impôts_module_02]|. On a seulement limité les paramètres des méthodes à deux :
• taxpayer(id, marié, enfants, salaire, impôt, décôte, surcôte, réduction, taux) : l'objet représentant un contribuable et son impôt ;
• admindata : l’objet encapsulant les données de l'administration fiscale ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
183/755
15. # calcul de l'impôt pour le même contribuable sans enfants
16. taxpayer2 = TaxPayer().fromdict(
17. {'id': 0, 'marié': taxpayer.marié, 'enfants': 0, 'salaire': taxpayer.salaire})
18. self.calculate_tax_2(taxpayer2, admindata)
19. # les résultats sont dans taxpayer2
20. taux2 = taxpayer2.taux
21. surcôte2 = taxpayer2.surcôte
22. impot2 = taxpayer2.impôt
23. # application du plafonnement du quotient familial
24. if taxpayer.enfants < 3:
25. # PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants
26. impot2 = impot2 - taxpayer.enfants * admindata.plafond_qf_demi_part
27. else:
28. # PLAFOND_QF_DEMI_PART euros pour les 2 premiers enfants, le double pour les suivants
29. impot2 = impot2 - 2 * admindata.plafond_qf_demi_part - (taxpayer.enfants - 2) \
30. * 2 * admindata.plafond_qf_demi_part
31. else:
32. # si le contribuable n'a pas d'enfants alors impot2=impot1
33. impot2 = impot1
34.
35. # on prend l'impôt le plus fort avec le taux et la surcôte qui vont avec
36. (impot, surcôte, taux) = (impot1, surcôte1, taux1) if impot1 >= impot2 else (
37. impot2, impot2 - impot1 + surcôte2, taux2)
38.
39. # résultats partiels
40. taxpayer.impôt = impot
41. taxpayer.surcôte = surcôte
42. taxpayer.taux = taux
43. # calcul d'une éventuelle décôte
44. self.get_décôte(taxpayer, admindata)
45. taxpayer.impôt -= taxpayer.décôte
46. # calcul d'une éventuelle réduction d'impôts
47. self.get_réduction(taxpayer, admindata)
48. taxpayer.impôt -= taxpayer.réduction
49. # résultat
50. taxpayer.impôt = math.floor(taxpayer.impôt)
• ligne 3: la méthode [calculate_tax] est l'unique méthode de l'interface [InterfaceImpôtsMétier]. Elle admet deux
paramètres :
o [tapPayer] : le contribuable dont on calcule l'impôt ;
o [admindata] : l’objet encapsulant les données de l'administration fiscale ;
o les résultats du calcul sont encapsulés dans le paramètre [taxpayer] (lignes 40-50). Le contenu de cet objet n'est donc
pas le même avant et après l'appel à la méthode ;
1. def configure():
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
184/755
2. import os
3.
4. # étape 1 ------
5. # configuration du python Path
6.
7. # dossier de ce fichier
8. script_dir = os.path.dirname(os.path.abspath(__file__))
9.
10. # root_dir
11. root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"
12.
13. # dépendances absolues de l'application
14. absolute_dependencies = [
15. f"{script_dir}/../entities",
16. f"{script_dir}/../interfaces",
17. f"{script_dir}/../services",
18. f"{root_dir}/02/entities",
19. ]
20.
21. # on fixe le syspath
22. from myutils import set_syspath
23. set_syspath(absolute_dependencies)
24.
25. # étape 2 ------
26. # configuration de l'application
27. config = {
28. # chemins absolus des fichiers de l'application
29. "admindataFilename": f"{script_dir}/../data/input/admindata.json"
30. }
31.
32. # instanciation des couches de l'application
33. from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
34. from ImpôtsMétier import ImpôtsMétier
35.
36. dao = ImpôtsDaoWithAdminDataInJsonFile(config)
37. métier = ImpôtsMétier()
38.
39. # on met les instances de couches dans la config
40. config["dao"] = dao
41. config["métier"] = métier
42.
43. # on rend la config
44. return config
1. import unittest
2.
3.
4. def get_config() -> dict:
5. # configuration de l'application
6. import config
7. # on rend la configuration
8. return config.configure()
9.
10.
11. class TestDaoMétier(unittest.TestCase):
12.
13. # exécutée avant chaque méthode test_
14. def setUp(self) -> None:
15. # on récupère la configuration des tests
16. config = get_config()
17. # on mémorise quelques informations
18. self.métier = config['métier']
19. self.admindata = config['dao'].get_admindata()
20.
21. def test_1(self) -> None:
22. from TaxPayer import TaxPayer
23.
24. # {'marié': 'oui', 'enfants': 2, 'salaire': 55555,
25. # 'impôt': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
26. taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
185/755
27. self.métier.calculate_tax(taxpayer, self.admindata)
28. # vérification
29. self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
30. self.assertEqual(taxpayer.décôte, 0)
31. self.assertEqual(taxpayer.réduction, 0)
32. self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
33. self.assertEqual(taxpayer.surcôte, 0)
34.
35. def test_2(self) -> None:
36. from TaxPayer import TaxPayer
37.
38. # {'marié': 'oui', 'enfants': 2, 'salaire': 50000,
39. # 'impôt': 1384, 'surcôte': 0, 'décôte': 384, 'réduction': 347, 'taux': 0.14}
40. taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 50000})
41. self.métier.calculate_tax(taxpayer, self.admindata)
42. # vérifications
43. self.assertAlmostEqual(taxpayer.impôt, 1384, delta=1)
44. self.assertAlmostEqual(taxpayer.décôte, 384, delta=1)
45. self.assertAlmostEqual(taxpayer.réduction, 347, delta=1)
46. self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
47. self.assertEqual(taxpayer.surcôte, 0)
48.
49. def test_3(self) -> None:
50. from TaxPayer import TaxPayer
51.
52. # {'marié': 'oui', 'enfants': 3, 'salaire': 50000,
53. # 'impôt': 0, 'surcôte': 0, 'décôte': 720, 'réduction': 0, 'taux': 0.14}
54. taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 50000})
55. self.métier.calculate_tax(taxpayer, self.admindata)
56. # vérifications
57. self.assertEqual(taxpayer.impôt, 0)
58. self.assertAlmostEqual(taxpayer.décôte, 720, delta=1)
59. self.assertEqual(taxpayer.réduction, 0)
60. self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
61. self.assertEqual(taxpayer.surcôte, 0)
62.
63. def test_4(self) -> None:
64. from TaxPayer import TaxPayer
65.
66. # {'marié': 'non', 'enfants': 2, 'salaire': 100000,
67. # 'impôt': 19884, 'surcôte': 4480, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
68. taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 2, 'salaire': 100000})
69. self.métier.calculate_tax(taxpayer, self.admindata)
70. # vérifications
71. self.assertAlmostEqual(taxpayer.impôt, 19884, delta=1)
72. self.assertEqual(taxpayer.décôte, 0)
73. self.assertEqual(taxpayer.réduction, 0)
74. self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
75. self.assertAlmostEqual(taxpayer.surcôte, 4480, delta=1)
76.
77. def test_5(self) -> None:
78. from TaxPayer import TaxPayer
79.
80. # {'marié': 'non', 'enfants': 3, 'salaire': 100000,
81. # 'impôt': 16782, 'surcôte': 7176, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
82. taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 3, 'salaire': 100000})
83. self.métier.calculate_tax(taxpayer, self.admindata)
84. # vérifications
85. self.assertAlmostEqual(taxpayer.impôt, 16782, delta=1)
86. self.assertEqual(taxpayer.décôte, 0)
87. self.assertEqual(taxpayer.réduction, 0)
88. self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
89. self.assertAlmostEqual(taxpayer.surcôte, 7176, delta=1)
90.
91. def test_6(self) -> None:
92. from TaxPayer import TaxPayer
93.
94. # {'marié': 'oui', 'enfants': 3, 'salaire': 100000,
95. # 'impôt': 9200, 'surcôte': 2180, 'décôte': 0, 'réduction': 0, 'taux': 0.3}
96. taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 100000})
97. self.métier.calculate_tax(taxpayer, self.admindata)
98. # vérifications
99. self.assertAlmostEqual(taxpayer.impôt, 9200, delta=1)
100. self.assertEqual(taxpayer.décôte, 0)
101. self.assertEqual(taxpayer.réduction, 0)
102. self.assertAlmostEqual(taxpayer.taux, 0.3, delta=0.01)
103. self.assertAlmostEqual(taxpayer.surcôte, 2180, delta=1)
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
186/755
104.
105. def test_7(self) -> None:
106. from TaxPayer import TaxPayer
107.
108. # {'marié': 'oui', 'enfants': 5, 'salaire': 100000,
109. # 'impôt': 4230, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
110. taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 5, 'salaire': 100000})
111. self.métier.calculate_tax(taxpayer, self.admindata)
112. # vérifications
113. self.assertAlmostEqual(taxpayer.impôt, 4230, delta=1)
114. self.assertEqual(taxpayer.décôte, 0)
115. self.assertEqual(taxpayer.réduction, 0)
116. self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
117. self.assertEqual(taxpayer.surcôte, 0)
118.
119. def test_8(self) -> None:
120. from TaxPayer import TaxPayer
121.
122. # {'marié': 'non', 'enfants': 0, 'salaire': 100000,
123. # 'impôt': 22986, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
124. taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 100000})
125. self.métier.calculate_tax(taxpayer, self.admindata)
126. # vérifications
127. self.assertAlmostEqual(taxpayer.impôt, 22986, delta=1)
128. self.assertEqual(taxpayer.décôte, 0)
129. self.assertEqual(taxpayer.réduction, 0)
130. self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
131. self.assertEqual(taxpayer.surcôte, 0)
132.
133. def test_9(self) -> None:
134. from TaxPayer import TaxPayer
135.
136. # {'marié': 'oui', 'enfants': 2, 'salaire': 30000,
137. # 'impôt': 0, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0}
138. taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 2, 'salaire': 30000})
139. self.métier.calculate_tax(taxpayer, self.admindata)
140. # vérifications
141. self.assertEqual(taxpayer.impôt, 0)
142. self.assertEqual(taxpayer.décôte, 0)
143. self.assertEqual(taxpayer.réduction, 0)
144. self.assertAlmostEqual(taxpayer.taux, 0.0, delta=0.01)
145. self.assertEqual(taxpayer.surcôte, 0)
146.
147. def test_10(self) -> None:
148. from TaxPayer import TaxPayer
149.
150. # {'marié': 'non', 'enfants': 0, 'salaire': 200000,
151. # 'impôt': 64210, 'surcôte': 7498, 'décôte': 0, 'réduction': 0, 'taux': 0.45}
152. taxpayer = TaxPayer().fromdict({'marié': 'non', 'enfants': 0, 'salaire': 200000})
153. self.métier.calculate_tax(taxpayer, self.admindata)
154. # vérifications
155. self.assertAlmostEqual(taxpayer.impôt, 64210, 1)
156. self.assertEqual(taxpayer.décôte, 0)
157. self.assertEqual(taxpayer.réduction, 0)
158. self.assertAlmostEqual(taxpayer.taux, 0.45, delta=0.01)
159. self.assertAlmostEqual(taxpayer.surcôte, 7498, delta=1)
160.
161. def test_11(self) -> None:
162. from TaxPayer import TaxPayer
163.
164. # {'marié': 'oui', 'enfants': 3, 'salaire': 200000,
165. # 'impôt': 42842, 'surcôte': 17283, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
166. taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
167. self.métier.calculate_tax(taxpayer, self.admindata)
168. # vérifications
169. self.assertAlmostEqual(taxpayer.impôt, 42842, 1)
170. self.assertEqual(taxpayer.décôte, 0)
171. self.assertEqual(taxpayer.réduction, 0)
172. self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
173. self.assertAlmostEqual(taxpayer.surcôte, 17283, delta=1)
174.
175.
176. if __name__ == '__main__':
177. unittest.main()
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
187/755
• ligne 11 : la classe de test étend la classe [unittest.TestCase] ;
• lignes 13-19 : dans un test UnitTest, la méthode [setUp] est exécutée avant chacune des méthodes [test_] ;
• ligne 16 : on récupère la configuration issue du script [config] étudié précédemment ;
• ligne 18 : on mémorise une référence sur la couche [métier] ;
• ligne 19 : on demande à la couche [dao] l’objet [AdminData] encapsulant les données de l’administration fiscale et on le
mémorise ;
• lignes 21-173 : 11 tests dont les résultats ont été vérifiés sur le site officiel des impôts 2019
|https://www3.impots.gouv.fr/simulateur/calcul_impot/2019/simplifie/index.htm| ;
• lignes 21-33 : tous les tests ont été construits sur le même modèle ;
• ligne 22 : on importe la classe [TaxPayer] ;
• ligne 24 : contribuable testé ;
• ligne 25 : résultats attendus ;
• ligne 26 : construction de l’objet [TaxPayer] du contribuable ;
• ligne 27 : calcul de son impôt. Le résultat est dans [taxpayer] ;
• lignes 29-33 : vérification des résultats obtenus ;
• ligne 29 : on vérifie le montant de l’impôt à l’euro près. Les tests ont en effet montré que les résultats obtenus par l’algorithme
de ce document pouvaient différer des chiffres officiels au plus d’un montant de 1 euro ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
188/755
15.1.5 Script principal
1. def configure():
2. import os
3.
4. # étape 1 ------
5. # configuration du python Path
6.
7. # dossier de ce fichier
8. script_dir = os.path.dirname(os.path.abspath(__file__))
9.
10. # root_dir
11. root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"
12.
13. # dépendances de l'application
14. absolute_dependencies = [
15. # dépendances locales
16. f"{script_dir}/../../entities",
17. f"{script_dir}/../../interfaces",
18. f"{script_dir}/../../services",
19. f"{root_dir}/02/entities",
20. ]
21.
22. # on fixe le syspath
23. from myutils import set_syspath
24. set_syspath(absolute_dependencies)
25.
26. # étape 2 ------
27. # configuration de l'application
28. config = {
29. # chemins absolus des fichiers de l'application
30. "taxpayersFilename": f"{script_dir}/../../data/input/taxpayersdata.txt",
31. "resultsFilename": f"{script_dir}/../../data/output/résultats.json",
32. "admindataFilename": f"{script_dir}/../../data/input/admindata.json",
33. "errorsFilename": f"{script_dir}/../../data/output/errors.txt"
34. }
35.
36. # instanciation des couches de l'application
37. from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
38. from ImpôtsMétier import ImpôtsMétier
39.
40. dao = ImpôtsDaoWithAdminDataInJsonFile(config)
41. métier = ImpôtsMétier()
42.
43. # on met les instances de couches dans la config
44. config["dao"] = dao
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
189/755
45. config["métier"] = métier
46.
47. # on rend la config
48. return config
Il est analogue à celui utilisé pour le test des couches [métier] et [dao].
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # imports
7. from ImpôtsError import ImpôtsError
8.
9. # on récupère les couches de l'application (elles sont déjà instanciées)
10. dao = config["dao"]
11. métier = config["métier"]
12.
13. try:
14. # récupération des tranches de l'impôt
15. admindata = dao.get_admindata()
16. # lecture des données des contribuables
17. taxpayers = dao.get_taxpayers_data()["taxpayers"]
18. # des contribuables ?
19. if not taxpayers:
20. raise ImpôtsError(51, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
21. # calcul de l'impôt des contribuables
22. for taxpayer in taxpayers:
23. # taxpayer est à la fois un paramètre d'entrée et de sortie
24. # taxpayer va être modifié
25. métier.calculate_tax(taxpayer, admindata)
26. # écriture des résultats dans un fichier texte
27. dao.write_taxpayers_results(taxpayers)
28. except ImpôtsError as erreur:
29. # affichage de l'erreur
30. print(f"L'erreur suivante s'est produite : {erreur}")
31. finally:
32. # terminé
33. print("Travail terminé...")
Notes
• lignes 2-4 : on récupère la configuration de l’application. On sait également que le Python Path de l’application a été construit ;
• lignes 9-11 : on récupère des références sur les couches [métier] et [dao] ;
• ligne 15 : on obtient les données de l'administration fiscale ;
• ligne 17 : on obtient la liste des contribuables dont il faut calculer l'impôt ;
• lignes 19-20 : si cette liste est vide, on lève une exception ;
• lignes 22-25 : calcul de l'impôt des différents objets [taxpayer] grâce à la couche [métier] ;
• ligne 27 : [taxpayers] est désormais une liste d'objets [TaxPayer] où les attributs (impôt, décôte, surcôte, réduction, taux) ont
reçu une valeur. Cette liste est écrite dans un fichier jSON ;
• lignes 28-30 : interception d'une éventuelle erreur ;
• lignes 31-33 : exécutées dans tous les cas ;
L’exécution du script donne les mêmes résultats que dans les versions précédentes. Le fichier des erreurs de contribuables était une
nouveauté dans cette version. Après exécution du script [main] son contenu est le suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
190/755
9. 8,non,0,100000
10. 9,oui,2,30000
11. 10,non,0,200000
12. 11,oui,3,200000
13. # on peut avoir des lignes vides
14.
15. # on crée des lignes erronées
16. # pas assez de valeurs
17. 11,12
18. # trop de valeurs
19. 12,oui,3,200000, x, y
20. # des valeurs erronées
21. x,x,x,x
Un nouveau module apparaît : la couche [ui] (User Interface) qui va dialoguer avec l'utilisateur. Cette couche aura une interface et
sera implémentée par une classe.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
191/755
6. class InterfaceImpôtsUi(ABC):
7. # exécution de la classe implémentant l'interface
8. @abstractmethod
9. def run(self):
10. pass
L'interface [InterfaceImpôtsUi] n'aura qu'une méthode, celle des lignes 8-10. L'interface sera ici implémentée avec une application
console mais on pourrait aussi l'implémenter avec une interface graphique. Les paramètres passés à la méthode [run] ne seraient pas
les mêmes dans les deux implémentations. Pour contourner ce problème, la méthode habituelle est de :
Cette méthode permet d'avoir une interface très générale qui est précisée par les paramètres des constructeurs de chaque classe
d'implémentation. Cette méthode a déjà été utilisée pour la version modulaire n° 1.
1. # imports
2. import re
3.
4. from InterfaceImpôtsUi import InterfaceImpôtsUi
5. from TaxPayer import TaxPayer
6.
7.
8. # couche [UI]
9. class ImpôtsConsole(InterfaceImpôtsUi):
10. # constructeur
11. def __init__(self, config: dict):
12. # on mémorise les paramètres
13. self.admindata = config['dao'].get_admindata()
14. self.métier = config['métier']
15.
16. def run(self):
17. # dialogue interactif avec l'utilisateur
18. fini = False
19. while not fini:
20. # le contribuable est-il marié ?
21. marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
22. # vérification de la validité de la saisie
23. while marié != "oui" and marié != "non" and marié != "*":
24. # msg d'erreur
25. print("Tapez oui ou non ou *")
26. # question de nouveau
27. marié = input("Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : ").strip().lower()
28. # fini ?
29. if marié == "*":
30. # dialogue terminé
31. return
32. # nombre d'enfants
33. enfants = input("Nombre d'enfants : ").strip()
34. # vérification de la validité de la saisie
35. if not re.match(r"^\d+$", enfants):
36. # msg d'erreur
37. print("Tapez un nombre entier positif ou nul")
38. # on recommence
39. enfants = input("Nombre d'enfants : ").strip()
40. # salaire annuel
41. salaire = input("Salaire annuel : ").strip()
42. # vérification de la validité de la saisie
43. if not re.match(r"^\d+$", salaire):
44. # msg d'erreur
45. print("Tapez un nombre entier positif ou nul")
46. # on recommence
47. salaire = input("Salaire annuel : ").strip()
48. # calcul de l'impôt
49. taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': int(enfants), 'salaire': int(salaire)})
50. self.métier.calculate_tax(taxpayer, self.admindata)
51. # affichage
52. print(f"Impôt du contribuable = {taxpayer}\n\n")
53. # contribuable suivant
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
192/755
o ligne 14 : on mémorise une référence sur la couche [métier] ;
• ligne 16 : implémentation de la méthode [run] de l'interface ;
• lignes 19-53 : dialogue avec l'utilisateur. Il consiste
o à demander les trois informations (marié, enfants, salaire) du contribuable ;
o à calculer son impôt ;
o à afficher celui-ci ;
o le dialogue se termine lorsque l'utilisateur répond * à la première question ;
• lignes 20-27 : on demande si le contribuable est marié et on vérifie la validité de la réponse ;
• lignes 29-31 : si l’utilisateur a répondu ‘*’ à la question le dialogue est arrêté ;
• lignes 32-39 : on demande le nombre d'enfants du contribuable et on vérifie la validité de la réponse ;
• lignes 40-47 : on demande le salaire annuel du contribuable et on vérifie la validité de la réponse ;
• lignes 48-50 : avec ces informations on fait calculer, par la couche [métier], l'impôt du contribuable ;
• ligne 52 : le montant de l'impôt est affiché ;
1. def configure():
2. import os
3.
4. # étape 1 ------
5. # configuration du python Path
6.
7. # dossier de ce fichier
8. script_dir = os.path.dirname(os.path.abspath(__file__))
9.
10. # root_dir
11. root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/classes"
12.
13. # dépendances de l'application
14. absolute_dependencies = [
15. # dépendances locales
16. f"{script_dir}/../../entities",
17. f"{script_dir}/../../interfaces",
18. f"{script_dir}/../../services",
19. f"{root_dir}/02/entities",
20. ]
21.
22. # on fixe le syspath
23. from myutils import set_syspath
24. set_syspath(absolute_dependencies)
25.
26. # étape 2 ------
27. # configuration de l'application
28. config = {
29. # chemins absolus des fichiers de l'application
30. "admindataFilename": f"{script_dir}/../../data/input/admindata.json",
31. }
32.
33. # instanciation des couches de l'application
34. from ImpôtsDaoWithAdminDataInJsonFile import ImpôtsDaoWithAdminDataInJsonFile
35. from ImpôtsMétier import ImpôtsMétier
36. from ImpôtsConsole import ImpôtsConsole
37.
38. # couche dao
39. dao = ImpôtsDaoWithAdminDataInJsonFile(config)
40. # couche métier
41. métier = ImpôtsMétier()
42. # on met les instances de couches dans la config
43. config["dao"] = dao
44. config["métier"] = métier
45. # couche ui
46. ui = ImpôtsConsole(config)
47. config["ui"] = ui
48.
49. # on rend la config
50. return config
1. # on configure l'application
2. import config
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
193/755
3.
4. config = config.configure()
5.
6. # imports
7. from ImpôtsError import ImpôtsError
8.
9. # on récupère les couches de l'application (elles sont déjà instanciées)
10. ui = config["ui"]
11.
12. # code
13. try:
14. # exécution de la couche [ui]
15. ui.run()
16. except ImpôtsError as erreur:
17. # on affiche le message d'erreur
18. print(f"L'erreur suivante s'est produite : {erreur}")
19. finally:
20. # exécuté dans tous les cas
21. print("Travail terminé...")
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/impots/v04/main/02/main.py
2. Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : oui
3. Nombre d'enfants : 3
4. Salaire annuel : 200000
5. Impôt du contribuable = {"id": 0, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842,
"surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
6.
7.
8. Le contribuable est-il marié / pacsé (oui/non) (* pour arrêter) : *
9. Travail terminé...
10.
11. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
194/755
16 Utilisation du SGBD MySQL
• un serveur web Apache. Nous l'utiliserons pour l'écriture de scripts web en Python ;
• le SGBD MySQL ;
• le langage de script PHP que nous n'utiliserons pas ;
• un serveur Redis implémentant un cache pour des applications web. Nous n'utiliserons ;
https://laragon.org/download/
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
195/755
• l'installation [1-5] donne naissance à l'arborescence suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
196/755
• [1] : le menu principal de Laragon ;
• [2] : le bouton [Start All] lance le serveur web Apache et le SGBD MySQL ;
• [3] : le bouton [WEB] affiche la page web [http://localhost] ;
• [4] : le bouton [Database] permet de gérer le SGBD MySQL avec l’outil [phpMyAdmin]. Il faut auparavant installer celui-ci ;
• [5] : le bouton [Terminal] ouvre un terminal de commandes ;
• [6] : le bouton [Root] ouvre un explorateur Windows positionné sur le dossier [<laragon>/www] qui est la racine du site web
[http://localhost]. C’est là qu’il faut placer les applications web statiques gérées par le serveur Apache de Laragon ;
• une fois lancé, Laragon [1] peut être administré à partir d'un menu [2] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
197/755
• en [3-5], on installe l'outil [phpMyAdmin] d'administration de MySQL s’il n’a pas déjà été installé ;
• en [8-10], on crée une base de données qu’on nomme [dbpersonnes] [11]. On va construire une base de données de
personnes ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
198/755
• l’opération [Bases de données] émet une requête web vers l’URL [http://localhost/phpmyadmin] [12]. C’est le serveur web
Apache de Laragon qui répond. L’URL [http://localhost/phpmyadmin] est l’URL de l’utilitaire [phpMyAdmin] que nous avons
installé précédemment [5]. Cet utilitaire permet de gérer les bases de données MySQL ;
• par défaut, les identifiants de connexion de l’administrateur de la base sont : root [13] sans mot de passe [14] ;
• on a pour l’instant une base [dbpersonnes] [17] qui est vide [18] ;
On crée un utilisateur [admpersonnes] avec le mot de passe [nobody] qui va avoir tous les droits sur la base de données [dbpersonnes] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
199/755
• en [19], on est positionnés sur la base [dbpersonnes] ;
• en [20], on sélectionne l’onglet [Privileges] ;
• en [21-22], on voit que l’utilisateur [root] a tous les droits sur la base [dbpersonnes] ;
• en [23], on crée un nouvel utilisateur ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
200/755
• en [35], phpMyAdmin indique que l’utilisateur a été créé ;
• en [36], l’ordre SQL qui a été émis sur la base ;
• en [37], l’utilisateur [admpersonnes] a tous les droits sur la base de données [dbpersonnes] ;
Un connecteur sert à isoler le code Python du SGBD exploité. Il existe des connecteurs pour différents SGBD et ceux-ci respectent
la même interface. Aussi lorsque ci-dessus, on remplace le SGBD MySQL par le SGBD PostgreSQL, l'architecture devient la suivante :
Parce que tous les connecteurs de SGBD respectent tous la même interface, le script Python n'a normalement pas à être modifié.
Dans la réalité, la plupart des SGBD ont un SQL propriétaire :
• ils respectent la norme SQL (Structured Query Language) ;
• mais l'étendent, car elle n'est pas suffisante, avec des extensions du langage propriétaires ;
Aussi, est-il fréquent que lors d'un changement de SGBD il y ait des modifications de SQL à faire dans les scripts.
Nativement, Python n'offre pas la possibilité de gérer une base MySQL. Il faut pour cela télécharger un package. Il en existe plusieurs.
Nous allons utiliser ici le package [mysql-connector-python] qui est le connecteur Officiel d'Oracle, l'entreprise qui possède MySQL.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
201/755
• le dossier en [2] n'a pas d'importance pour ce qui va suivre ;
• [pip] (Package Installer for Python) est l'outil d'installation des packages Python. L'outil [pip] se connecte au dépôt contenant
les packages Python ;
• [search MySQL] : demande la liste des packages contenant le terme [MySQL] (la casse n'a pas d'importance) dans leur nom ;
Ont été listés tous les modules dont le nom ou la description ont le mot clé MySQL. Celui que nous utiliserons (fév 2020) est [mysql-
connector-python], ligne 17. Pour l'installer, on tape dans le terminal la commande [pip install -U mysql-connector-python] :
• ligne 1 : l'option [install -U] (U=upgrade) demande la version la plus récente des différents packages associés au package
[mysql-connector-python] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
202/755
Pour connaître les packages installés dans l'environnement Python de notre macine, on tape la commande [pip list] :
1. C:\Data\st-2020\dev\python\cours-2020\v-01>pip list
2. Package Version
3. ---------------------- ----------
4. asgiref 3.2.3
5. astroid 2.3.3
6. atomicwrites 1.3.0
7. attrs 19.3.0
8. certifi 2019.11.28
9. …
10. MarkupSafe 1.1.1
11. mccabe 0.6.1
12. more-itertools 8.1.0
13. mysql-connector-python 8.0.19
14. mysqlclient 1.4.6
15. packaging 20.0
16. pip 20.0.1
17. pipenv 2018.11.26
18. …
Pour savoir comment utiliser le package [mysql-connector-python] afin de gérer une base de données MySQL, on ira sur le site du
package |https://dev.mysql.com/doc/connector-python/en/|. La suite présente une série d'exemples.
Notes
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
203/755
o database : la base de données à laquelle on se connecte. Optionnel.
• ligne 20 : si une exception est lancée, elle est de type [DatabaseError] ou [InterfaceError] ;
• lignes 23-26 : dans la clause [finally], on ferme la connexion ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/mysql/mysql_01.py
2. Connexion au SGBD MySQL en cours...
3. Connexion MySQL réussie à la base database=dbpersonnes, host=localhost sous l'identité user=admpersonnes,
passwd=nobody
4.
5. Process finished with exit code 0
Notes :
• lignes 6-19 : une fonction [connexion] qui tente de connecter puis de déconnecter un utilisateur à la base de données
[dbpersonnes]. Affiche le résultat ;
• lignes 29-41 : programme principal – appelle deux fois la méthode connexion et affiche les éventuelles exceptions ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/mysql/mysql_02.py
2. Connexion MySQL réussie à la base database=dbpersonnes, host=localhost sous l'identité user=admpersonnes,
passwd=nobody
3. Déconnexion MySQL réussie
4.
5. 1045 (28000): Access denied for user 'xx'@'localhost' (using password: YES)
6.
7. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
204/755
16.5 script [mysql_03] : création d'une table MySQL
Maintenant qu'on sait créer une connexion avec un SGBD MySQL, on commence à émettre des ordres SQL sur cette connexion.
Pour cela, nous allons nous connecter à la base créée [dbpersonnes] et utiliser la connexion pour créer une table dans la base.
1. # imports
2. import sys
3.
4. from mysql.connector import DatabaseError, InterfaceError, connect
5. from mysql.connector.connection import MySQLConnection
6.
7.
8. # ---------------------------------------------------------------------------------
9. def execute_sql(connexion: MySQLConnection, update: str):
10. # exécute une requête de mise à jour sur la connexion
11. curseur = None
12. try:
13. # on demande un curseur
14. curseur = connexion.cursor()
15. # exécute la requête update sur la connexion
16. curseur.execute(update)
17. finally:
18. # fermeture du curseur s'il a été obtenu
19. if curseur:
20. curseur.close()
21.
22.
23. # ---------------------------------------------- main
24. # identifiants de la connexion
25. # l'identité de l'utilisateur
26. ID = "admpersonnes"
27. PWD = "nobody"
28. # la machine hôte du sgbd
29. HOST = "localhost"
30. # identité de la base
31. DATABASE = "dbpersonnes"
32.
33. # on y va étape par étape
34. try:
35. # connexion
36. connexion = connect(host=HOST, user=ID, password=PWD, database=DATABASE)
37. # mode AUTOCOMMIT
38. connexion.autocommit = True
39. except (InterfaceError, DatabaseError) as erreur:
40. # on affiche l'erreur
41. print(f"L'erreur suivante s'est produite : {erreur}")
42. # on quitte
43. sys.exit()
44.
45. # suppression de la table personnes si elle existe
46. # si elle n'existe pas une erreur se produira - on l'ignore
47. requête = "drop table personnes"
48. try:
49. execute_sql(connexion, requête)
50. except (InterfaceError, DatabaseError):
51. pass
52.
53. # création de la table personnes
54. requête = "create table personnes (id int PRIMARY KEY, prenom varchar(30) NOT NULL, nom varchar(30) NOT
NULL, age integer NOT NULL, " \
55. "unique(nom,prenom)) "
56. try:
57. # exécution requête
58. execute_sql(connexion, requête)
59. # affichage
60. print(f"{requête} : requête réussie")
61. except (InterfaceError, DatabaseError) as erreur:
62. # on affiche l'erreur
63. print(f"L'erreur suivante s'est produite : {erreur}")
64. finally:
65. # on se déconnecte
66. connexion.close()
Notes :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
205/755
• ligne 9 : la fonction execute_sql exécute une requête SQL sur une connexion ouverte ;
• ligne 14 : les opérations SQL sur la connexion se font au travers d'un objet particulier appelé curseur ;
• ligne 14 : obtention d'un curseur ;
• ligne 16 : exécution de la requête SQL ;
• lignes 17-20 : qu'il y ait erreur ou non, le curseur est fermé. Cela libère les ressources qui lui sont associées. Si exception il y a,
alors elle n'est pas gérée ici. Elle va remonter au code appelant ;
• lignes 33-43 : création d'une connexion avec la base de données ;
• ligne 38 : le mode AUTOCOMMIT=True pour une connexion signifie que chaque exécution d'une requête s'exécute dans
une transaction automatique. Le mode par défaut est AUTOCOMMIT=False où c'est le développeur qui a la responsabilité
de gérer les transactions. Une transaction est un mécanisme qui englobe l'exécution de plusieurs requêtes 1 à n. Soit celles-ci
réussissent toutes, soit aucune ne réussit. Ainsi si les requêtes 1 à i réussissent mais que la requête i+1 échoue, alors les requêtes
1 à i vont être 'défaites' pour que la base retrouve l'état qu'elle avait avant l'exécution de la requête 1 ;
• ici, il y a deux requêtes SQL (lignes 49, 58). Elles seront chacune exécutées dans une transaction. Que la seconde échoue n'a
aucun impact sur la première ;
• lignes 45-51 : l'ordre SQL [drop table personnes] est exécuté. Elle supprime la table appelée [personnes]. Si celle-ci n'existe
pas, une erreur peut être signalée. Celle-ci est ignorée (ligne 51) ;
• lignes 53-55 : l'ordre de création de la table [personnes]. Une table peut être vue comme un ensemble de lignes et de colonnes.
L'ordre de création précise le nom des colonnes :
o [id] : un identifiant entier. Il sera unique pour chaque personne. Ce sera la clé primaire (PRIMARY KEY). Cela signifie
que dans la table, cette colonne n’a pas deux fois la même valeur et qu’elle peut être utilisée pour identifier une personne ;
o [nom] : une chaîne d'au plus 30 caractères ;
o [prenom] : une chaîne d'au plus 30 caractères ;
o [age] : un nombre entier ;
o l'attribut [NOT NULL] pour chacune de ces colonnes signifie que dans une ligne de la table, aucune des trois colonnes ne
peut être vide ;
o le paramètre [unique(nom,prenom)] s'appelle une contrainte. Ici la contrainte sur les lignes est que le tuple (nom, prenom)
de la ligne doit être unique dans la table. Cela signifie qu'on peut repérer de façon unique dans la table, un individu dont
on connaît les nom et prénom ;
• lignes 56-60 : exécution de l’ordre SQL ;
• lignes 61-63 : gestion de l’éventuelle exception ;
• lignes 64-66 : on se déconnecte de la base de données ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/mysql/mysql_03.py
2. create table personnes (id int PRIMARY KEY, prenom varchar(30) NOT NULL, nom varchar(30) NOT NULL, age
integer NOT NULL, unique(nom,prenom)) : requête réussie
3.
4. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
206/755
• la base de données [dbpersonnes] [1] a une table [personnes] [2] qui a la structure [3-4], la clé primaire [5] et la contrainte
d’unicité [6] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
207/755
6. insert into personnes(prenom, nom, age) values('Paul','Langevin',48)
7. insert into personnes(prenom, nom, age) values ('Sylvie','Lefur',70)
8. # affichage de la table
9. select prenom, nom, age from personnes
10. # erreur volontaire
11. xx
12. # insertion de trois personnes
13. insert into personnes(prenom, nom, age) values ('Pierre','Nicazou',35)
14. insert into personnes(prenom, nom, age) values ('Geraldine','Colou',26)
15. insert into personnes(prenom, nom, age) values ('Paulette','Girond',56)
16. # affichage de la table
17. select prenom, nom, age from personnes
18. # liste des personnes par ordre alphabétique des noms et à nom égal par ordre alphabétique des prénoms
19. select nom,prenom from personnes order by nom asc, prenom desc
20. # liste des personnes ayant un âge dans l'intervalle [20,40] par ordre décroissant de l'âge
21. # puis à âge égal par ordre alphabétique des noms et à nom égal par ordre alphabétique des prénoms
22. select nom,prenom,age from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc
23. # insertion de mme Bruneau
24. insert into personnes(prenom, nom, age) values('Josette','Bruneau',46)
25. # mise à jour de son âge
26. update personnes set age=47 where nom='Bruneau'
27. # liste des personnes ayant Bruneau pour nom
28. select nom,prenom,age from personnes where nom='Bruneau'
29. # suppression de Mme Bruneau
30. delete from personnes where nom='Bruneau'
31. # liste des personnes ayant Bruneau pour nom
32. select nom,prenom,age from personnes where nom='Bruneau'
Nous définissons tout d'abord des fonctions que nous installons dans un module afin de pouvoir les réutiliser :
1. # imports
2. from mysql.connector import DatabaseError, InterfaceError
3. from mysql.connector.connection import MySQLConnection
4. from mysql.connector.cursor import MySQLCursor
5.
6.
7. # ---------------------------------------------------------------------------------
8. def afficher_infos(curseur: MySQLCursor):
9. # affiche le résultat d'une command sql
10. …
11.
12.
13. # ---------------------------------------------------------------------------------
14. def execute_list_of_commands(connexion: MySQLConnection, sql_commands: list,
15. suivi: bool = False, arrêt: bool = True, with_transaction: bool = True):
16. # utilise la connexion ouverte [connexion]
17. # exécute sur cette connexion les commandes SQL contenues dans la liste [sql_commands]
18. # ce fichier est un fichier de commandes SQL à exécuter à raison d'une par ligne
19. # si suivi=True alors chaque exécution d'un ordre SQL fait l'objet d'un affichage indiquant sa réussite ou son échec
20. # si arrêt=True, la fonction s'arrête sur la 1ère erreur rencontrée sinon elle exécute ttes les commandes sql
21. # si with_transaction=True alors toute erreur annule l'ensemble des ordres SQL exécutés auparavant
22. # si with_transaction=False alors une erreur n'a aucun impact sur les ordres SQL exécutés auparavant
23. # la fonction rend une liste [erreur1, erreur2, ...]
24.
25. ….
26.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
208/755
27.
28. # ---------------------------------------------------------------------------------
29. def execute_file_of_commands(connexion: MySQLConnection, sql_filename: str,
30. suivi: bool = False, arrêt: bool = True, with_transaction: bool = True):
31. # utilise la connexion ouverte [connexion]
32. # exécute sur cette connexion les commandes SQL contenues dans le fichier texte sql_filename
33. # ce fichier est un fichier de commandes SQL à exécuter à raison d'une par ligne
34. # si suivi=True alors chaque exécution d'un ordre SQL fait l'objet d'un affichage indiquant sa réussite ou son échec
35. # si arrêt=True, la fonction s'arrête sur la 1ère erreur rencontrée sinon elle exécute ttes les commandes sql
36. # si with_transaction=True alors toute erreur annule l'ensemble des ordres SQL exécutés auparavant
37. # si with_transaction=False alors une erreur n'a aucun impact sur les ordres SQL exécutés auparavant
38. # la fonction rend une liste [erreur1, erreur2, ...]
39.
40. # exploitation du fichier SQL
41. try:
42. # ouverture du fichier en lecture
43. file = open(sql_filename, "r")
44. # exploitation
45. return execute_list_of_commands(connexion, file.readlines(), suivi, arrêt, with_transaction)
46. except BaseException as erreur:
47. # on rend un tableau d'erreurs
48. return [f"Le fichier {sql_filename} n'a pu être être exploité : {erreur}"]
Notes :
• ligne 29 : la fonction [execute_file_of_commands] exécute les ordres SQL contenu dans le fichier texte nommé
[sql_filename] :
• on lira les commentaires des lignes 31-38 pour connaître la signification des paramètres ;
• lignes 40-48 : on exploite le fichier texte [sql_filename] ;
• ligne 43 : ouverture du fichier ;
• ligne 34 : exécution de la fonction [execute_list_of_commands] qui exécute les commandes SQL qu'on lui passe dans une
liste. Cette liste est ici constituée par la liste de toutes les lignes du fichier texte [file.readlines()] (ligne 45) ;
1. # ---------------------------------------------------------------------------------
2. def execute_list_of_commands(connexion: MySQLConnection, sql_commands: list,
3. suivi: bool = False, arrêt: bool = True, with_transaction: bool = True):
4. # utilise la connexion ouverte [connexion]
5. # exécute sur cette connexion les commandes SQL contenues dans la liste [sql_commands]
6. # ce fichier est un fichier de commandes SQL à exécuter à raison d'une par ligne
7. # si suivi=True alors chaque exécution d'un ordre SQL fait l'objet d'un affichage indiquant sa réussite ou son échec
8. # si arrêt=True, la fonction s'arrête sur la 1ère erreur rencontrée sinon elle exécute ttes les commandes sql
9. # si with_transaction=True alors toute erreur annule l'ensemble des ordres SQL exécutés auparavant
10. # si with_transaction=False alors une erreur n'a aucun impact sur les ordres SQL exécutés auparavant
11. # la fonction rend une liste [erreur1, erreur2, ...]
12.
13. # initialisations
14. curseur = None
15. connexion.autocommit = not with_transaction
16. erreurs = []
17. try:
18. # on demande un curseur
19. curseur = connexion.cursor()
20. # exécution des sql_commands SQL contenues dans sql_commands
21. # on les exécute une à une
22. for command in sql_commands:
23. # on élimine les blancs de début et de fin de la commande courante
24. command = command.strip()
25. # a-t-on une commande vide ou un commentaire ? Si oui, on passe à la commande suivante
26. if command == '' or command[0] == "#":
27. continue
28. # exécution de la commande courante
29. error = None
30. try:
31. curseur.execute(command)
32. except (InterfaceError, DatabaseError) as erreur:
33. error = erreur
34. # y-a-t-il eu une erreur ?
35. if error:
36. # une erreur de plus
37. msg = f"{command} : Erreur ({error})"
38. erreurs.append(msg)
39. # suivi écran ou non ?
40. if suivi:
41. print(msg)
42. # on s'arrête ?
43. if with_transaction or arrêt:
44. # on rend la liste d'erreurs
45. return erreurs
46. else:
47. # pas d'erreur
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
209/755
48. if suivi:
49. print(f"[{command}] : Exécution réussie")
50. # on affiche le résultat de la command
51. afficher_infos(curseur)
52. # on rend le tableau des erreurs
53. return erreurs
54. finally:
55. # fermeture du curseur
56. if curseur:
57. curseur.close()
58. # on valide / annule la transaction si elle existe
59. if with_transaction:
60. if erreurs:
61. # annulation
62. connexion.rollback()
63. else:
64. # validation
65. connexion.commit()
Notes
• ligne 2 : la fonction [execute_list_of_commands] exécute les ordres SQL contenu dans la liste [sql_commands] :
• on lira les commentaires des lignes 4-11 pour connaître la signification des paramètres ;
• ligne 2 : la connexion reçue est une connexion ouverte vers une base de données ;
• ligne 15 : si on veut que l'ensemble des commandes de la liste [sql_commands] s'exécute au sein d'une transaction alors il faut
travailler en mode AUTOCOMMIT=False. Sinon, on travaillera en mode AUTOCOMMIT=True et alors chacune des
commandes de la liste [sqlCommands] s'exécutera au sein d'une transaction automatique et il n'y aura pas de transaction globale ;
• ligne 19 : on demande un curseur pour exécuter les différentes commandes SQL ;
• lignes 22-51 : on exécute les commandes une par une ;
• lignes 26-27 : on accepte les lignes blanches ainsi que les commentaires dans la liste des commandes SQL. Dans ce cas, on
ignore simplement la commande ;
• lignes 30-33 : exécution de la requête courante ;
• lignes 35-45 : on traite le cas d'une éventuelle erreur d'exécution de la requête courante ;
• lignes 37-38 : l'erreur est ajoutée au tableau des erreurs ;
• lignes 40-41 : si un suivi a été demandé, alors le message d'erreur est affiché ;
• lignes 43-45 : si le code appelant a demandé un arrêt après la première erreur ou s'il a demandé l'utilisation d'une transaction,
alors il faut s'arrêter. On rend le tableau des erreurs ;
• lignes 46-51 : cas où il n'y a pas eu d'erreur d'exécution de la requête courante ;
• lignes 48-49 : si un suivi a été demandé, on affiche la requête exécutée avec la mention 'réussie' ;
• lignes 50-51 : on affiche le résultat de la requête exécutée. On va revenir sur la fonction [afficher_infos] un peu plus loin ;
• lignes 54-65 : la clause [finally] est exécutée dans tous les cas, qu'il y ait eu une exception ou pas ;
• lignes 56-57 : fermeture du curseur. Cela libère les ressources allouées à celui-ci ;
• lignes 59-65 : on traite le cas où le code appelant a demandé à ce que les commandes SQL soient exécutées dans une
transaction ;
• ligne 60 : on regarde si la liste [erreurs] est vide, ce qui signifie qu'aucune exception n'a eu lieu. Dans ce cas, la transaction
est validée (ligne 65), sinon elle est annulée (ligne 62) ;
1. # ---------------------------------------------------------------------------------
2. def afficher_infos(curseur: MySQLCursor):
3. print(type(curseur))
4. # affiche le résultat d'une command sql
5. # s'agissait-il d'un select ?
6. if curseur.description:
7. # le curseur a une description - donc il a exécuté un select
8. # description[i] est la description de la colonne n° i du select
9. # description[i][0] est le nom de la colonne n° i du select
10. # on affiche les noms des champs
11. titre = ""
12. for i in range(len(curseur.description)):
13. titre += curseur.description[i][0] + ", "
14. # on affiche la liste des champs sans la virgule de fin
15. print(titre[0:len(titre) - 1])
16. # ligne séparatrice
17. print("*" * (len(titre) - 1))
18. # ligne courante du select
19. ligne = curseur.fetchone()
20. while ligne:
21. # on l'affiche
22. print(ligne)
23. # ligne suivante du select
24. ligne = curseur.fetchone()
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
210/755
25. # ligne séparatrice
26. print("*" * (len(titre) - 1))
27. else:
28. # le curseur n'a pas de champ [description] - il a donc exécuté un ordre SQL
29. # de mise à jour (insert, delete, update)
30. print(f"nombre de lignes modifiées : {curseur.rowcount}")
Notes
• ligne 1 : le paramètre de la fonction est le curseur qui vient d'exécuter un ordre SQL. Selon que cet ordre est un SELECT ou
un ordre de mise à jour INSERT, UPDATE, DELETE, le contenu du curseur n'est pas le même ;
• ligne 6 : si le curseur a le champ [description] alors il a exécuté un SELECT et [description] décrit les champs demandés
dans le SELECT :
o description[i] décrit le champ n° i demandé par le SELECT. C'est une liste ;
o description[i][0] est le nom du champ n° i ;
• lignes 11-17 : on affiche le nom des champs demandés par le SELECT ;
• lignes 18-24 : on exploite le résultat du SELECT ;
• lignes 20, 24 : le résultat d'un SELECT s'exploite séquentiellement. Ce résultat est un ensemble de lignes. La ligne courante
est obtenue par [curseur.fetchone()] (ligne 19). On obtient alors un tuple ;
• lignes 27-30 : si le curseur n'a pas le champ [description] alors il a exécuté un ordre de mise à jour INSERT, UPDATE,
DELETE. On peut alors savoir combien de lignes de la table ont été modifiées par l'exécution de cet ordre ;
• ligne 30 : [curseur.rowcount] est ce nombre ;
Le script principal [mysql-04] utilise le module [mysql_module] que nous venons de décrire :
1. def configure():
2. import os
3.
4. # chemin absolu du dossier du fichier de configuration
5. script_dir = os.path.dirname(os.path.abspath(__file__))
6. # configuration des dossiers du syspath
7. absolute_dependencies = [
8. # dossiers locaux
9. f"{script_dir}/shared",
10. ]
11.
12. # fixation du syspath
13. from myutils import set_syspath
14. set_syspath(absolute_dependencies)
15.
16. # on rend la config
17. return {
18. # fichier des commandes SQL
19. "commands_filename": f"{script_dir}/data/commandes.sql",
20. # identifiants de la connexion à la base de données
21. "host": "localhost",
22. "database": "dbpersonnes",
23. "user": "admpersonnes",
24. "password": "nobody"
25. }
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
211/755
1. # on récupère la configuration de l'application
2. import config_04
3.
4. config = config_04.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. import sys
8. from mysql_module import execute_file_of_commands
9. from mysql.connector import connect, DatabaseError, InterfaceError
10.
11. # ---------------------------------------------- main
12. # vérification de la syntaxe de l'appel
13. # argv[0] true / false
14. args = sys.argv
15. erreur = len(args) != 2
16. if not erreur:
17. with_transaction = args[1].lower()
18. erreur = with_transaction != "true" and with_transaction != "false"
19. # erreur ?
20. if erreur:
21. print(f"syntaxe : {args[0]} true / false")
22. sys.exit()
23.
24. # calcul d'un texte
25. with_transaction = with_transaction == "true"
26. if with_transaction:
27. texte = "avec transaction"
28. else:
29. texte = "sans transaction"
30.
31. # logs écran
32. print("--------------------------------------------------------------------")
33. print(f"Exécution du fichier SQL {config['commands_filename']} {texte}")
34. print("--------------------------------------------------------------------")
35.
36. # exécution des ordres SQL du fichier
37. connexion = None
38. try:
39. # connexion à la bd
40. connexion = connect(host=config['host'], user=config['user'], password=config['password'],
41. database=config['database'])
42. # exécution du fichier des commandes SQL
43. erreurs = execute_file_of_commands(connexion, config["commands_filename"], suivi=True, arrêt=False,
44. with_transaction=with_transaction)
45. except (InterfaceError, DatabaseError) as erreur:
46. # affichage de l'erreur
47. print(f"L'erreur fatale suivante s'est produite : {erreur}")
48. # on s'arrête
49. sys.exit()
50. finally:
51. # fermeture de la connexion si elle a été ouverte
52. if connexion:
53. connexion.close()
54.
55. # affichage nombre d'erreurs
56. print("--------------------------------------------------------------------")
57. print(f"Exécution terminée")
58. print("--------------------------------------------------------------------")
59. print(f"Il y a eu {len(erreurs)} erreur(s)")
60. # affichage des erreurs
61. for erreur in erreurs:
62. print(erreur)
Notes
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
212/755
• lignes 45-49 : s’il se produit une erreur à la connexion (ligne 40) ou non gérée par le script [execute_file_of_commands] on
affiche l’erreur et on arrête tout ;
• lignes 55-62 : en cas d’exécution réussie, on affiche le nombre d’erreurs rencontrées dans l’exécution des commandes SQL ;
Exécution n° 1
On fait d'abord une exécution sans transaction. Pour cela, on va créer une configuration d'exécution comme il a été fait dans le
paragraphe |configuration d’un contexte d’exécution|:
Cette configuration correspond donc à une exécution du fichier SQL avec une transaction. Utiliser le bouton [Apply] pour valider la
configuration.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
213/755
Nous créons de la même façon la configuration d'exécution [mysql mysql-04 without_transaction] :
Cette configuration correspond donc à une exécution du fichier SQL sans transaction. Utiliser le bouton [Apply] pour valider la
configuration.
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/mysql/mysql_04.py false
2. --------------------------------------------------------------------
3. Exécution du fichier SQL C:\Data\st-2020\dev\python\cours-2020\python3-flask-
2020\databases\mysql/data/commandes.sql sans transaction
4. --------------------------------------------------------------------
5. [drop table personnes] : Exécution réussie
6. nombre de lignes modifiées : 0
7. [create table personnes (id int primary key, prenom varchar(30) not null, nom varchar(30) not null, age
integer not null, unique (nom,prenom))] : Exécution réussie
8. nombre de lignes modifiées : 0
9. [insert into personnes(id, prenom, nom, age) values(1, 'Paul','Langevin',48)] : Exécution réussie
10. nombre de lignes modifiées : 1
11. [insert into personnes(id, prenom, nom, age) values (2, 'Sylvie','Lefur',70)] : Exécution réussie
12. nombre de lignes modifiées : 1
13. [select prenom, nom, age from personnes] : Exécution réussie
14. prenom, nom, age,
15. *****************
16. ('Paul', 'Langevin', 48)
17. ('Sylvie', 'Lefur', 70)
18. *****************
19. xx : Erreur (1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your
MySQL server version for the right syntax to use near 'xx' at line 1)
20. [insert into personnes(id, prenom, nom, age) values (3, 'Pierre','Nicazou',35)] : Exécution réussie
21. nombre de lignes modifiées : 1
22. [insert into personnes(id, prenom, nom, age) values (4, 'Geraldine','Colou',26)] : Exécution réussie
23. nombre de lignes modifiées : 1
24. [insert into personnes(id, prenom, nom, age) values (5, 'Paulette','Girond',56)] : Exécution réussie
25. nombre de lignes modifiées : 1
26. [select prenom, nom, age from personnes] : Exécution réussie
27. prenom, nom, age,
28. *****************
29. ('Paul', 'Langevin', 48)
30. ('Sylvie', 'Lefur', 70)
31. ('Pierre', 'Nicazou', 35)
32. ('Geraldine', 'Colou', 26)
33. ('Paulette', 'Girond', 56)
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
214/755
34. *****************
35. [select nom,prenom from personnes order by nom asc, prenom desc] : Exécution réussie
36. nom, prenom,
37. ************
38. ('Colou', 'Geraldine')
39. ('Girond', 'Paulette')
40. ('Langevin', 'Paul')
41. ('Lefur', 'Sylvie')
42. ('Nicazou', 'Pierre')
43. ************
44. [select nom,prenom,age from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc] :
Exécution réussie
45. nom, prenom, age,
46. *****************
47. ('Nicazou', 'Pierre', 35)
48. ('Colou', 'Geraldine', 26)
49. *****************
50. [insert into personnes(id, prenom, nom, age) values(6, 'Josette','Bruneau',46)] : Exécution réussie
51. nombre de lignes modifiées : 1
52. [update personnes set age=47 where nom='Bruneau'] : Exécution réussie
53. nombre de lignes modifiées : 1
54. [select nom,prenom,age from personnes where nom='Bruneau'] : Exécution réussie
55. nom, prenom, age,
56. *****************
57. ('Bruneau', 'Josette', 47)
58. *****************
59. [delete from personnes where nom='Bruneau'] : Exécution réussie
60. nombre de lignes modifiées : 1
61. [select nom,prenom,age from personnes where nom='Bruneau'] : Exécution réussie
62. nom, prenom, age,
63. *****************
64. *****************
65. --------------------------------------------------------------------
66. Exécution terminée
67. --------------------------------------------------------------------
68. Il y a eu 1 erreur(s)
69. xx : Erreur (1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your
MySQL server version for the right syntax to use near 'xx' at line 1)
70.
71. Process finished with exit code 0
Notes :
• ligne 19 : on voit qu'après l'erreur l'exécution des ordres SQL a continué, ceci parce que l'exécution s'est faite sans transaction
et avec le paramètre [arrêt=False]. Tous les ordres SQL ont donc été exécutés. On devrait donc avoir une table [personnes]
reflétant cette exécution ;
Exécution n° 2
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
215/755
Nous exécutons maintenant la configuration [mysql mysql-04 with_transaction]. Les résultats sont les suivants :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/mysql/mysql_04.py true
2. --------------------------------------------------------------------
3. Exécution du fichier SQL C:\Data\st-2020\dev\python\cours-2020\python3-flask-
2020\databases\mysql/data/commandes.sql avec transaction
4. --------------------------------------------------------------------
5. [drop table personnes] : Exécution réussie
6. nombre de lignes modifiées : 0
7. [create table personnes (id int primary key, prenom varchar(30) not null, nom varchar(30) not null, age
integer not null, unique (nom,prenom))] : Exécution réussie
8. nombre de lignes modifiées : 0
9. [insert into personnes(id, prenom, nom, age) values(1, 'Paul','Langevin',48)] : Exécution réussie
10. nombre de lignes modifiées : 1
11. [insert into personnes(id, prenom, nom, age) values (2, 'Sylvie','Lefur',70)] : Exécution réussie
12. nombre de lignes modifiées : 1
13. [select prenom, nom, age from personnes] : Exécution réussie
14. prenom, nom, age,
15. *****************
16. ('Paul', 'Langevin', 48)
17. ('Sylvie', 'Lefur', 70)
18. *****************
19. xx : Erreur (1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your
MySQL server version for the right syntax to use near 'xx' at line 1)
20. --------------------------------------------------------------------
21. Exécution terminée
22. --------------------------------------------------------------------
23. Il y a eu 1 erreur(s)
24. xx : Erreur (1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your
MySQL server version for the right syntax to use near 'xx' at line 1)
25.
26. Process finished with exit code 0
Notes :
• ligne 19 : on voit qu'après l'erreur il n'y a plus d'exécution d'ordres SQL, ceci parce que l'exécution s'est faite dans une
transaction et qu'à la 1re erreur rencontrée nous avons défait la transaction et arrêté l'exécution des ordres SQL. Ceci signifie
que le résultat des ordres des lignes 9, 11, 13 a été défait. On devrait donc avoir une table [personnes] vide ;
1. # imports
2. from mysql.connector import connect, DatabaseError, InterfaceError
3.
4. # l'identité de l'utilisateur
5. ID = "admpersonnes"
6. PWD = "nobody"
7. # la machine hôte du sgbd
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
216/755
8. HOST = "localhost"
9. # identité de la base
10. BASE = "dbpersonnes"
11.
12. # liste de personnes (nom,prenom,age)
13. personnes = []
14. for i in range(5):
15. personnes.append((i, f"n0{i}", f"p0{i}", i + 10))
16. personnes.append((40, "d'Aboot", "Y'éna", 18))
17. # autre liste de personnes
18. autresPersonnes = []
19. for i in range(5):
20. autresPersonnes.append((i + 100, f"n1{i}", f"p1{i}", i + 20))
21. autresPersonnes.append((200, "d'Aboot", "F'ilhem", 34))
22.
23. # accès au SGBD
24. connexion = None
25. try:
26. # connexion
27. connexion = connect(host=HOST, user=ID, password=PWD, database=BASE)
28. # curseur
29. curseur = connexion.cursor()
30. # suppression des enregistrement existants
31. curseur.execute("delete from personnes")
32. # insertions personne par personne avec une requête préparée
33. for personne in personnes:
34. curseur.execute("insert into personnes(id,nom,prenom,age) values(%s,%s,%s,%s)", personne)
35. # insertion en bloc d'une liste de personnes
36. curseur.executemany("insert into personnes(id,nom,prenom,age) values(%s, %s,%s,%s)", autresPersonnes)
37. # validation de la transaction
38. connexion.commit()
39. except (DatabaseError, InterfaceError) as erreur:
40. # affichage erreur
41. print(f"L'erreur suivante s'est produite : {erreur}")
42. # annulation transaction
43. if connexion:
44. connexion.rollback()
45. finally:
46. # fermeture connexion
47. if connexion:
48. connexion.close()
Notes
• lignes 12-21 : on crée deux listes de personnes à inclure dans la base de données [dbpersonnes] ;
• ligne 27 : connexion à la base de données ;
• ligne 31 : suppression du contenu de la table [personnes] ;
• lignes 33-34 : insertion de personnes avec une requête paramétrée. Ligne 34, le 1er paramètre est l'ordre SQL à exécuter. Celui-
ci est incomplet. Il contient des paramètres [%s] qui vont être remplacés un par un et dans l'ordre par les valeurs de la liste du
second paramètre ;
• ligne 36 : insertion de personnes avec cette fois une seule instruction [curseur.executemany]. Le second paramètre de
[executemany] est alors une liste de listes ;
• elles sont exécutées plus rapidement que des requêtes en 'dur' qu'il faut analyser à chaque exécution. La requête paramétrée
[executemany] n'est analysée qu'une fois. Ensuite elle est exécutée n fois sans être analysée de nouveau ;
• les paramètres injectés dans la requête paramétrée sont vérifiés. S'ils contiennent des caractères réservés, comme l'apostrophe
par exemple, ceux-ci sont 'protégés' afin qu'ils n'interfèrent pas dans l'exécution de l'ordre SQL. C’est pour vérifier ce point
qu’on a inclus des noms et prénoms avec apostrophes dans la liste (lignes 16 et 21) ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
217/755
• on notera que les chaînes ayant une apostrophe, caractère réservé en SQL, ont été correctement insérées. La requête
paramétrée les a 'protégées'. Sans requête paramétrée, il aurait fallu faire ce travail nous-mêmes ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
218/755
17 Utilisation du SGBD PostgreSQL
Le SGBD PostgreSQL est librement disponible. Il est une alternative à la version 'community' de MySQL.
Nous l'utilisons ici pour montrer qu'il est assez simple de migrer des scripts Python / MySQL vers des scripts Python / PostgreSQL.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
219/755
• en [6], indiquez un dossier d’installation ;
• en [8], l’option [Stack Builder] est inutile pour ce qu’on veut faire ici ;
• en [10], laissez la valeur qui vous sera présentée ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
220/755
• en [12-13], on a mis ici le mot de passe [root]. Ce sera le mot de passe de l’administrateur du SGBD qui s’appelle [postgres].
PostgreSQL l’appelle également le super-utilisateur ;
• en [15], laissez la valeur par défaut : c’est le port d’écoute du SGBD ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
221/755
Sous windows, le SGBD PostgreSQL est installé comme un service windows lancé automatiquement. La plupart du temps ce n’est
pas souhaitable. Nous allons modifier cette configuration. Tapez [services] dans la barre de recherche de Windows [24-26] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
222/755
• en [29], on voit que le service du SGBD PostgreSQL est en mode automatique. On change cela en accédant aux propriétés
du service [30] :
Lorsque vous voudrez démarrer manuellement le SGBD, revenez à l’application [services], cliquez droit sur le service [postgresql]
(34) et lancez le (35).
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
223/755
Il est possible qu’à un moment donné on vous demande le mot de passe du super-utilisateur. Celui-ci s’appelle [postgres]. Vous avez
défini son mot de passe lors de l’installation du SGBD. Dans ce document, nous avons donné le mot de passe [root] au super-
utilisateur lors de l’installation.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
224/755
• en [17], on a mis [nobody] ;
• en [21], le code SQL que va émettre l’outil [pgAdmin] vers le SGBD PostgreSQL. C’est une façon d’apprendre le langage SQL
propriétaire de PostgreSQL ;
• en [22], après validation de l’assistant [Save], l’utilisateur [admpersonnes] a été créé ;
On clique droit sur [23], puis [24-25] pour créer une nouvelle base de données. Dans l’onglet [26], on définit le nom de la base [27]
et son propriétaire [admpersonnes] [28].
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
225/755
• en [30], le code SQL de création de la base ;
• en [31], après validation de l’assistant [Save], la base [dbpersonnes] est créée ;
Dans le schéma ci-dessus est représenté un connecteur faisant le lien entre les scripts Python et le SGBD PostgreSQL. Il en existe
plusieurs. Nous installons le connecteur [psycopg2]. Cela se fait dans un terminal Python (peu importe le dossier dans lequel est
ouvert ce terminal). Le connecteur est installé par la commande [pip install psycopg2] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
226/755
17.4 Portage des scripts MySQL vers des scripts PostgreSQL
• le dossier [1] des scripts MySQL est dupliqué (Ctrl-C / Ctrl-V), puis les noms des fichiers sont changés mais par leur contenu ;
Au lieu de :
1. # imports
2. from mysql.connector import DatabaseError, InterfaceError
3. from mysql.connector.connection import MySQLConnection
4. from mysql.connector.cursor import MySQLCursor
on écrit :
1. # imports
2. from psycopg2 import DatabaseError, InterfaceError
3. from psycopg2.extensions import connection, cursor
Elle devient :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
227/755
Au lieu de :
on écrit :
Le reste ne change pas. Les résultats sont les mêmes qu'avec MySQL.
Au lieu de :
on écrit :
Les résultats ne sont pas les mêmes que ceux du script [mysql_02] :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/postgresql/pgres_02.py
2. Connexion MySQL réussie à la base database=dbpersonnes, host=localhost sous l'identité user=admpersonnes,
passwd=nobody
3. Déconnexion MySQL réussie
4.
5. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
228/755
39. except (InterfaceError, DatabaseError) as erreur:
40. # on affiche l'erreur
41. print(erreur)
Alors que les lignes 36-41 auraient dû afficher un message d'erreur indiquant que la connexion au SGBD avait échoué, rien n'est
affiché. En fait lorsqu'on creuse la question, on voit qu'on passe bien dans le [except] des lignes 35-37 mais que la variable [erreur]
vaut [None]. Ceci avec la versions 2.8.4 du connecteur [psycopg2].
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/postgresql/pgres_02.py
2. Connexion réussie à la base database=dbpersonnes, host=localhost sous l'identité user=admpersonnes,
passwd=nobody
3. Déconnexion réussie
4.
5. Erreur de connexion à la base [dbpersonnes] par l'utilisateur [xx/yy]
6.
7. Process finished with exit code 0
Au lieu de :
on écrit :
devient :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/postgresql/pgres_03.py
2. create table personnes (id int PRIMARY KEY, prenom varchar(30) NOT NULL, nom varchar(30) NOT NULL, age
integer NOT NULL, unique(nom,prenom)) : requête réussie
3.
4. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
229/755
17.4.5 script [pgres_04]
Le script [pgres_04] est une copie du script [mysql_04] (cf paragraphe |script [mysql-04] : exécution d'un fichier d'ordres SQL|).
Il utilise le module [pgres_module] :
On crée une configuration [pgres pgres-04 without_transaction] comme il a été fait au paragraphe |script [mysql-04] : exécution
d'un fichier d'ordres SQL|. On crée de même une configuration [pgres pgres-04 with_transaction].
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/postgresql/pgres_04.py false
2. --------------------------------------------------------------------
3. Exécution du fichier SQL C:\Data\st-2020\dev\python\cours-2020\python3-flask-
2020\databases\postgresql/data/commandes.sql sans transaction
4. --------------------------------------------------------------------
5. [drop table if exists personnes] : Exécution réussie
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
230/755
6. nombre de lignes modifiées : -1
7. [create table personnes (id int primary key, prenom varchar(30) not null, nom varchar(30) not null, age
integer not null, unique (nom,prenom))] : Exécution réussie
8. nombre de lignes modifiées : -1
9. [insert into personnes(id, prenom, nom, age) values(1, 'Paul','Langevin',48)] : Exécution réussie
10. nombre de lignes modifiées : 1
11. [insert into personnes(id, prenom, nom, age) values (2, 'Sylvie','Lefur',70)] : Exécution réussie
12. nombre de lignes modifiées : 1
13. [select prenom, nom, age from personnes] : Exécution réussie
14. prenom, nom, age,
15. *****************
16. ('Paul', 'Langevin', 48)
17. ('Sylvie', 'Lefur', 70)
18. *****************
19. xx : Erreur (ERREUR: erreur de syntaxe sur ou près de « xx »
20. LINE 1: xx
21. ^
22. )
23. [insert into personnes(id, prenom, nom, age) values (3, 'Pierre','Nicazou',35)] : Exécution réussie
24. nombre de lignes modifiées : 1
25. [insert into personnes(id, prenom, nom, age) values (4, 'Geraldine','Colou',26)] : Exécution réussie
26. nombre de lignes modifiées : 1
27. [insert into personnes(id, prenom, nom, age) values (5, 'Paulette','Girond',56)] : Exécution réussie
28. nombre de lignes modifiées : 1
29. [select prenom, nom, age from personnes] : Exécution réussie
30. prenom, nom, age,
31. *****************
32. ('Paul', 'Langevin', 48)
33. ('Sylvie', 'Lefur', 70)
34. ('Pierre', 'Nicazou', 35)
35. ('Geraldine', 'Colou', 26)
36. ('Paulette', 'Girond', 56)
37. *****************
38. [select nom,prenom from personnes order by nom asc, prenom desc] : Exécution réussie
39. nom, prenom,
40. ************
41. ('Colou', 'Geraldine')
42. ('Girond', 'Paulette')
43. ('Langevin', 'Paul')
44. ('Lefur', 'Sylvie')
45. ('Nicazou', 'Pierre')
46. ************
47. [select nom,prenom,age from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc] :
Exécution réussie
48. nom, prenom, age,
49. *****************
50. ('Nicazou', 'Pierre', 35)
51. ('Colou', 'Geraldine', 26)
52. *****************
53. [insert into personnes(id, prenom, nom, age) values(6, 'Josette','Bruneau',46)] : Exécution réussie
54. nombre de lignes modifiées : 1
55. [update personnes set age=47 where nom='Bruneau'] : Exécution réussie
56. nombre de lignes modifiées : 1
57. [select nom,prenom,age from personnes where nom='Bruneau'] : Exécution réussie
58. nom, prenom, age,
59. *****************
60. ('Bruneau', 'Josette', 47)
61. *****************
62. [delete from personnes where nom='Bruneau'] : Exécution réussie
63. nombre de lignes modifiées : 1
64. [select nom,prenom,age from personnes where nom='Bruneau'] : Exécution réussie
65. nom, prenom, age,
66. *****************
67. *****************
68. --------------------------------------------------------------------
69. Exécution terminée
70. --------------------------------------------------------------------
71. Il y a eu 1 erreur(s)
72. xx : Erreur (ERREUR: erreur de syntaxe sur ou près de « xx »
73. LINE 1: xx
74. ^
75. )
76.
77. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
231/755
[drop table if exists]qui ne lance pas d’exception si la table n’existe pas. Nous l’avons utilisée ici. C’est un exemple où
deux SGBD ne se comportent pas de la même façon dans des situations analogues ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/postgresql/pgres_04.py true
2. --------------------------------------------------------------------
3. Exécution du fichier SQL C:\Data\st-2020\dev\python\cours-2020\python3-flask-
2020\databases\postgresql/data/commandes.sql avec transaction
4. --------------------------------------------------------------------
5. [drop table if exists personnes] : Exécution réussie
6. nombre de lignes modifiées : -1
7. [create table personnes (id int primary key, prenom varchar(30) not null, nom varchar(30) not null, age
integer not null, unique (nom,prenom))] : Exécution réussie
8. nombre de lignes modifiées : -1
9. [insert into personnes(id, prenom, nom, age) values(1, 'Paul','Langevin',48)] : Exécution réussie
10. nombre de lignes modifiées : 1
11. [insert into personnes(id, prenom, nom, age) values (2, 'Sylvie','Lefur',70)] : Exécution réussie
12. nombre de lignes modifiées : 1
13. [select prenom, nom, age from personnes] : Exécution réussie
14. prenom, nom, age,
15. *****************
16. ('Paul', 'Langevin', 48)
17. ('Sylvie', 'Lefur', 70)
18. *****************
19. xx : Erreur (ERREUR: erreur de syntaxe sur ou près de « xx »
20. LINE 1: xx
21. ^
22. )
23. --------------------------------------------------------------------
24. Exécution terminée
25. --------------------------------------------------------------------
26. Il y a eu 1 erreur(s)
27. xx : Erreur (ERREUR: erreur de syntaxe sur ou près de « xx »
28. LINE 1: xx
29. ^
30. )
31.
32. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
232/755
Ici, le résultat est différent de celui obtenu avec MySQL. Si on exécute les scripts dans les mêmes conditions, ç-à-d après exécution
du script sans transaction, on a les résultats suivants :
• avec MySQL, la table [personnes] est vide ;
• avec PostgreSQL, la table [personnes] ne l'est pas ;
La différence repose sur les façons différentes qu'ont ces deux SGBD de défaire la transaction :
• MySQL ne défait pas les ordres [drop table] et [create table]. On se retrouve avec une table [personnes] vide ;
• PostgreSQL défait les ordres [drop table] et [create table]. On retrouve la table dans l'état où elle était avant l'exécution
du script avec transaction ;
Au lieu de :
1. # imports
2. from mysql.connector import connect, DatabaseError, InterfaceError
on écrit :
1. # imports
2. from psycopg2 import connect, DatabaseError, InterfaceError
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
233/755
17.5 Conclusion
Le portage des scripts MySQL vers des scripts PostgreSQL s'est réalisé plutôt facilement. C'est une exception. Les deux SGBD ne
supportent pas les mêmes règles de nommage des objets SQL (bases, tables, colonnes, contraintes, types des données…), ont des
extensions SQL incompatibles… Pour assurer un portage simple, il faut s'en tenir dans les deux cas à la norme SQL sans essayer
d'utiliser les extensions propriétaires des SGBD. Cela se fait alors aux dépens des performances.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
234/755
18 Ecrire du code indépendant du SGBD
Nous avons vu précédemment que dans certains cas il était possible de migrer simplement du code Python écrit pour le SGBD
MySQL vers du code écrit pour le SGBD PostgreSQL. Dans ce chapitre, nous montrons comment systématiser cette approche.
L’architecture proposée devient la suivante :
On souhaite que le choix du connecteur et donc du SGBD se fasse par configuration et ne nécessite pas de réécriture du script. On
rappelle que cela n’est possible que dans les cas où le script n’utilise pas d’extensions propriétaires du SGBD.
Les scripts [any_xx] reprennent les scripts déjà étudiés pour les SGBD MySQL et PostgreSQL. Nous n’allons pas tous les reprendre.
Nous allons nous concentrer sur le script [any_04] qui est le plus complexe. On rappelle que ce script exécute les commandes SQL
du fichier [data/commandes.sql] suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
235/755
27. # liste des personnes ayant Bruneau pour nom
28. select nom,prenom,age from personnes where nom='Bruneau'
29. # suppression de Mme Bruneau
30. delete from personnes where nom='Bruneau'
31. # liste des personnes ayant Bruneau pour nom
32. select nom,prenom,age from personnes where nom='Bruneau'
Nous avons modifié la ligne 2 pour que la commande ait le même comportement pour les SGBD MySQL et PostgreSQL si la table
[personnes] n’existe pas.
1. def configure():
2. import os
3.
4. # chemin absolu du dossier de ce script
5. script_dir = os.path.dirname(os.path.abspath(__file__))
6.
7. # configuration des dossiers du syspath
8. absolute_dependencies = [
9. # dossiers locaux
10. f"{script_dir}/shared",
11. ]
12.
13. # fixation du syspath
14. from myutils import set_syspath
15. set_syspath(absolute_dependencies)
16.
17. # configuration de l'application
18. config = {
19. # sgbd gérés
20. "sgbds": {
21. "mysql": {
22. # connecteur du sgbd
23. "sgbd_connector": "mysql.connector",
24. # nom du module rassemblant des fonctions de gestion du sgbd
25. "sgbd_fonctions": "any_module",
26. # identifiants de la connexion
27. "user": "admpersonnes",
28. "password": "nobody",
29. "host": "localhost",
30. "database": "dbpersonnes"
31. },
32. "postgresql": {
33. # connecteur du sgbd
34. "sgbd_connector": "psycopg2",
35. # nom du module rassemblant des fonctions de gestion du sgbd
36. "sgbd_fonctions": "any_module",
37. # identifiants de la connexion
38. "user": "admpersonnes",
39. "password": "nobody",
40. "host": "localhost",
41. "database": "dbpersonnes"
42. }
43. },
44. # fichier de commandes SQL
45. "commands_filename": f"{script_dir}/data/commandes.sql"
46. }
47. # syspath de l'application
48. from myutils import set_syspath
49. set_syspath(absolute_dependencies)
50.
51. # on rend la config
52. return config
• ligne 20 : [sgbds] est un dictionnaire avec deux clés [mysql] ligne 21 et [postgresql] ligne 32 ;
• la valeur associée à ces clés est un dictionnaire donnant les éléments permettant la connexion à un SGBD :
• lignes 21-32 : les éléments d’une connexion au SGBD MySQL ;
o ligne 23 : le connecteur Python à utiliser ;
o ligne 25 : le module contenant des fonctions partagées ;
o lignes 26-30 : les identifiants de la connexion ;
• lignes 32-41 : les mêmes éléments pour une connexion au SGBD PostgreSQL ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
236/755
Le script [any_04] qui exécute le fichier de commandes SQL [data/commandes.sql] est le suivant :
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on peut faire les imports
7. import importlib
8. import sys
9.
10. # vérification de la syntaxe de l'appel
11. # argv[0] sgbd_name true / false
12. # il faut 3 paramètres
13. args = sys.argv
14. erreur = len(args) != 3
15. if not erreur:
16. # on récupère les deux paramètres qui nous intéressent
17. sgbd_name = args[1].lower()
18. with_transaction = args[2].lower()
19. # vérification des deux paramètres
20. erreur = (with_transaction != "true" and with_transaction != "false") \
21. or sgbd_name not in config["sgbds"].keys()
22. # erreur ?
23. if erreur:
24. print(f"syntaxe : {args[0]} (1) sgbd_name (2) true / false")
25. sys.exit()
26.
27. # configuration du sgbd_name
28. sgbd_config = config["sgbds"][sgbd_name]
29. # connecteur du sgbd_name
30. sgbd_connector = importlib.import_module(sgbd_config["sgbd_connector"])
31. # bibliothèque de fonctions
32. lib = importlib.import_module(sgbd_config["sgbd_fonctions"])
33.
34.
35. # calcul d'un texte à afficher
36. with_transaction = with_transaction == "true"
37. if with_transaction:
38. texte = "avec transaction"
39. else:
40. texte = "sans transaction"
41.
42. # affichage
43. commands_filename=config['commands_filename']
44. print("--------------------------------------------------------------------")
45. print(f"Exécution du fichier SQL {commands_filename} {texte}")
46. print("--------------------------------------------------------------------")
47.
48. # exécution des ordres SQL du fichier
49. connexion = None
50. try:
51. # connexion
52. connexion = sgbd_connector.connect(
53. host=sgbd_config['host'],
54. user=sgbd_config['user'],
55. password=sgbd_config['password'],
56. database=sgbd_config['database'])
57. # exécution du fichier des commandes SQL
58. erreurs = lib.execute_file_of_commands(sgbd_connector, connexion, commands_filename, suivi=True, arrêt=False,
59. with_transaction=with_transaction)
60. except (sgbd_connector.InterfaceError, sgbd_connector.DatabaseError) as erreur:
61. print(f"L'erreur suivante s'est produite : {erreur}")
62. finally:
63. # fermeture de la connexion
64. if connexion:
65. connexion.close()
66.
67. # affichage nombre d'erreurs
68. print("--------------------------------------------------------------------")
69. print(f"Exécution terminée")
70. print("--------------------------------------------------------------------")
71. print(f"Il y a eu {len(erreurs)} erreur(s)")
72. # affichage des erreurs
73. for erreur in erreurs:
74. print(erreur)
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
237/755
o [with_transaction] : True si on veut exécuter le fichier de commandes SQL au sein d’une transaction, False sinon ;
• lignes 10-25 : les paramètres sont récupérés et vérifiés ;
• ligne 28 : la configuration du SGBD choisi ;
• ligne 30 : on importe le connecteur du SGBD choisi. On utilise pour cela la bibliothèque [importlib] (ligne 7) qui permet
d’importer un module dont le nom est dans une variable. Le résultat de l’opération [importlib.import_module] est un module.
Ainsi après la ligne 30, tout se passe comme si l’instruction exécutée avait été :
import sgbd_connector
Ceci va nous permettre d’écrire ligne 52 [sgbd_connector.connect] où on utilise la fonction [connect] du module
[sgbd_connector]. Il faut se rappeler ici que [sgbd_connector] est soit [mysql.connector] ou [psycopg2]. Ces deux
modules ont la fonction [connect]. De même, ligne 60, on peut écrire [sgbd_connector.InterfaceError,
sgbd_connector.DatabaseError].
• ligne 32 : on importe le module des fonctions utilisées par le script ;
• ligne 58 : on exécute la fonction [execute_file_of_commands] du module des fonctions utilisées par le script. Par rapport aux
versions précédentes, la signature de cette fonction a un paramètre de plus, le premier. On passe à la fonction le connecteur
Python [sgbd_connector] qu’elle doit utiliser ;
• en-dehors de ces points, le script [any_04] reste ce qu’il était dans les versions précédentes ;
1. # ---------------------------------------------------------------------------------
2.
3. def afficher_infos(curseur):
4. …
5.
6.
7. # ---------------------------------------------------------------------------------
8. def execute_list_of_commands(sgbd_connector, connexion, sql_commands: list,
9. suivi: bool = False, arrêt: bool = True, with_transaction: bool = True):
10. …
11.
12. # initialisations
13. curseur = None
14. connexion.autocommit = not with_transaction
15. erreurs = []
16. try:
17. # on demande un curseur
18. curseur = connexion.cursor()
19. # exécution des sql_commands SQL contenues dans sql_commands
20. # on les exécute une à une
21. for command in sql_commands:
22. # on élimine les blancs de début et de fin de la commande courante
23. command = command.strip()
24. # a-t-on une commande vide ou un commentaire ? Si oui, on passe à la commande suivante
25. if command == '' or command[0] == "#":
26. continue
27. # exécution de la commande courante
28. error = None
29. try:
30. curseur.execute(command)
31. except (sgbd_connector.InterfaceError, sgbd_connector.DatabaseError) as erreur:
32. error = erreur
33. # y-a-t-il eu une erreur ?
34. …
35.
36.
37. # ---------------------------------------------------------------------------------
38. def execute_file_of_commands(sgbd_connector, connexion, sql_filename: str,
39. suivi: bool = False, arrêt: bool = True, with_transaction: bool = True):
40. …
41.
42. # exploitation du fichier SQL
43. try:
44. # ouverture du fichier en lecture
45. file = open(sql_filename, "r")
46. # exploitation
47. return execute_list_of_commands(sgbd_connector, connexion, file.readlines(), suivi, arrêt, with_transaction)
48. except BaseException as erreur:
49. # on rend un tableau d'erreurs
50. return [f"Le fichier {sql_filename} n'a pu être être exploité : {erreur}"]
51. finally:
52. pass
Le paramètre [sgbd_connector] a été utilisé ligne 31 pour préciser le type des exceptions interceptées.
L’exécution du script [any_04] avec les paramètres [mysql false] donne les résultats suivants :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
238/755
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/anysgbd/any_04.py mysql false
2. --------------------------------------------------------------------
3. Exécution du fichier SQL C:\Data\st-2020\dev\python\cours-2020\python3-flask-
2020\databases\anysgbd/data/commandes.sql sans transaction
4. --------------------------------------------------------------------
5. [drop table if exists personnes] : Exécution réussie
6. nombre de lignes modifiées : 0
7. [create table personnes (id int primary key, prenom varchar(30) not null, nom varchar(30) not null, age
integer not null, unique (nom,prenom))] : Exécution réussie
8. nombre de lignes modifiées : 0
9. [insert into personnes(id, prenom, nom, age) values(1, 'Paul','Langevin',48)] : Exécution réussie
10. nombre de lignes modifiées : 1
11. [insert into personnes(id, prenom, nom, age) values (2, 'Sylvie','Lefur',70)] : Exécution réussie
12. nombre de lignes modifiées : 1
13. [select prenom, nom, age from personnes] : Exécution réussie
14. prenom, nom, age,
15. *****************
16. ('Paul', 'Langevin', 48)
17. ('Sylvie', 'Lefur', 70)
18. *****************
19. xx : Erreur (1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your
MySQL server version for the right syntax to use near 'xx' at line 1)
20. [insert into personnes(id, prenom, nom, age) values (3, 'Pierre','Nicazou',35)] : Exécution réussie
21. nombre de lignes modifiées : 1
22. [insert into personnes(id, prenom, nom, age) values (4, 'Geraldine','Colou',26)] : Exécution réussie
23. nombre de lignes modifiées : 1
24. [insert into personnes(id, prenom, nom, age) values (5, 'Paulette','Girond',56)] : Exécution réussie
25. nombre de lignes modifiées : 1
26. [select prenom, nom, age from personnes] : Exécution réussie
27. prenom, nom, age,
28. *****************
29. ('Paul', 'Langevin', 48)
30. ('Sylvie', 'Lefur', 70)
31. ('Pierre', 'Nicazou', 35)
32. ('Geraldine', 'Colou', 26)
33. ('Paulette', 'Girond', 56)
34. *****************
35. [select nom,prenom from personnes order by nom asc, prenom desc] : Exécution réussie
36. nom, prenom,
37. ************
38. ('Colou', 'Geraldine')
39. ('Girond', 'Paulette')
40. ('Langevin', 'Paul')
41. ('Lefur', 'Sylvie')
42. ('Nicazou', 'Pierre')
43. ************
44. [select nom,prenom,age from personnes where age between 20 and 40 order by age desc, nom asc, prenom asc] :
Exécution réussie
45. nom, prenom, age,
46. *****************
47. ('Nicazou', 'Pierre', 35)
48. ('Colou', 'Geraldine', 26)
49. *****************
50. [insert into personnes(id, prenom, nom, age) values(6, 'Josette','Bruneau',46)] : Exécution réussie
51. nombre de lignes modifiées : 1
52. [update personnes set age=47 where nom='Bruneau'] : Exécution réussie
53. nombre de lignes modifiées : 1
54. [select nom,prenom,age from personnes where nom='Bruneau'] : Exécution réussie
55. nom, prenom, age,
56. *****************
57. ('Bruneau', 'Josette', 47)
58. *****************
59. [delete from personnes where nom='Bruneau'] : Exécution réussie
60. nombre de lignes modifiées : 1
61. [select nom,prenom,age from personnes where nom='Bruneau'] : Exécution réussie
62. nom, prenom, age,
63. *****************
64. *****************
65. --------------------------------------------------------------------
66. Exécution terminée
67. --------------------------------------------------------------------
68. Il y a eu 1 erreur(s)
69. xx : Erreur (1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your
MySQL server version for the right syntax to use near 'xx' at line 1)
70.
71. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
239/755
19 Utilisation de l’ORM SQLALCHEMY
Le chapitre précédent a montré que dans certains cas on pouvait écrire du code indépendant du SGBD utilisé avec l’architecture
suivante :
Dans ce chapitre nous allons utiliser l’ORM (Object Relational Mapper) [sqlalchemy] pour accéder aux SGBD de façon uniforme
quelque soit le SGBD utilisé. Un ORM permet deux choses :
• il permet à un script de dialoguer avec le SGBD sans émettre d’ordres SQL ;
• il masque au script les particularités de chaque SGBD ;
Le script est désormais séparé des connecteurs par l’ORM. Il dialogue avec l’ORM avec des classes et des méthodes. Il n’exécute pas
de code SQL. C’est l’ORM qui le fait avec les connecteurs auxquels il est relié. Il cache au script les particularités de ces connecteurs.
Aussi le code du script est-il insensible à un changement de connecteur (donc du SGBD) ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
240/755
19.2 Scripts 01 : les bases
• en [1], les scripts qui vont être étudiés. Ces scripts vont utiliser les classes de [2] : BaseEntity, MyException, Personne, Utils ;
19.2.1 Configuration
Le fichier [config] configure l’application de la façon suivante :
1. def configure():
2. # root_dir
3. # chemin absolu référence des chemins relatifs de la configuration
4. root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
5. # chemins absolus des dépendances
6. absolute_dependencies = [
7. # BaseEntity, MyException, Personne, Utils
8. f"{root_dir}/classes/02/entities",
9. ]
10.
11. # on fixe le syspath
12. from myutils import set_syspath
13. set_syspath(absolute_dependencies)
14.
15. # configuration des classes
16. from Personne import Personne
17. Personne.excluded_keys = ['_sa_instance_state']
18.
19. # on rend la config
20. return {}
Commentaires
• ligne 8 : on met dans le Python Path le dossier contenant les classes [BaseEntity, MyException, Personne, Utils] ;
• lignes 12-13 : on fixe le Python Path de l’application ;
• lignes 16-17 : on se rappelle peut-être que la classe |BaseEntity| a un attribut de classe nommé [excluded_keys]. Cet attribut
est une liste dans laquelle on met les propriétés de la classe qu’on ne veut pas voir apparaître dans le dictionnaire de celle-ci
(fonction asdict). Ici on exclut la propriété [_sa_instance_state] de l’état de la classe [Personne]. On verra bientôt pourquoi ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
241/755
15. # la table
16. personnes_table = Table("personnes", metadata,
17. Column('id', Integer, primary_key=True),
18. Column('prenom', String(30), nullable=False),
19. Column("nom", String(30), nullable=False),
20. Column("age", Integer, nullable=False),
21. UniqueConstraint('nom', 'prenom', name='uix_1')
22. )
23. # la classe Personne avant le mapping
24. personne1 = Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10})
25. print(f"personne1={personne1.__dict__}")
26.
27. # le mapping
28. mapper(Personne, personnes_table, properties={
29. 'id': personnes_table.c.id,
30. 'prénom': personnes_table.c.prenom,
31. 'nom': personnes_table.c.nom,
32. 'âge': personnes_table.c.age
33. })
34.
35. # personne1 n'a pas été modifiée
36. print(f"personne1={personne1.__dict__}")
37. # la classe Personne a elle été modifiée - elle a été "enrichie"
38. personne2 = Personne().fromdict({"id": 68, "prénom": "x1", "nom": "y1", "âge": 11})
39. print(f"personne2={personne2.__dict__}")
Commentaires
• lignes 27-33 : on fait un mapping, ç-à-d qu’on crée une correspondance entre la classe [Personne] et la table [personnes].
C’est essentiellement une correspondance [propriétés de la classe → colonnes de la table]. La fonction [mapper]
accepte ici trois paramètres :
o ligne 28 : le 1er paramètre est le nom de la classe pour laquelle on fait le mapping ;
o ligne 28 : le second paramètre est la table à laquelle elle va être associée. Celle-ci est l’objet [Table] créé ligne 16 ;
o ligne 28 : le 3ième paramètre est ici un paramètre nommé [properties]. C’est un dictionnaire dans lequel les clés sont les
propriétés de la classe mappée et les valeurs les colonnes de la table mappée. Pour désigner la colonne X de la table
[personnes_table], on écrit [personnes_table.c.X] ;
• lignes 35-36 : on réaffiche la personne [personne1] une fois le mapping fait. On constate qu’elle n’a pas changé :
• lignes 37-39 : on crée une nouvelle personne [personne2] et on l’affiche. On a alors l’affichage suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
242/755
personne2={'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x00000259A6747FA0>,
'id': 68, 'prénom': 'x1', 'nom': 'y1', 'âge': 11}
o une nouvelle propriété [_sa_instance_state] apparaît. On voit que c’est un objet de l’ORM [sqlalchemy] ;
o les autres propriétés ont été débarrassées de leur préfixe qui indiquait à quelle classe elles appartenaient ;
On peut donc conclure que l’opération de mapping des lignes 27-33 a modifié la classe [Personne].
Lorsqu’on voudra afficher l’état d’un objet [Personne], on ne voudra pas en général de la propriété [_sa_instance_state]. Elle n’est
là en effet que pour la cuisine interne de [sqlalchemy] et en général elle ne nous intéresse pas. C’est pourquoi on a écrit dans le script
[config] :
Si [Database1] est la base [dbpersonnes], on voit que la liaison entre le script et cette base passe par deux entités :
Le script [main] va dialoguer avec l’ORM qui va ensuite dialoguer avec le connecteur Python. L’ORM dialogue avec ce connecteur
avec les outils décrits dans les paragraphes |MySQL| et |PostgreSQL| notamment en émettant des ordres SQL. Le script [main] ne
va pas utiliser d’ordres SQL. Il va s’appuyer sur l’API (Application Programming Interface) de l’ORM, faite de classes et d’interfaces.
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # imports
7. from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, UniqueConstraint
8. from sqlalchemy.exc import IntegrityError, InterfaceError
9. from sqlalchemy.orm import mapper, sessionmaker
10.
11. from Personne import Personne
12.
13. # chaîne de connexion à une base de données MySQL
14. engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")
15.
16. # metadata
17. metadata = MetaData()
18.
19. # la table
20. personnes_table = Table("personnes", metadata,
21. Column('id', Integer, primary_key=True),
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
243/755
22. Column('prenom', String(30), nullable=False),
23. Column("nom", String(30), nullable=False),
24. Column("age", Integer, nullable=False),
25. UniqueConstraint('nom', 'prenom', name='uix_1')
26. )
27.
28. # le mapping
29. mapper(Personne, personnes_table, properties={
30. 'id': personnes_table.c.id,
31. 'prénom': personnes_table.c.prenom,
32. 'nom': personnes_table.c.nom,
33. 'âge': personnes_table.c.age
34. })
35.
36. # la session factory
37. Session = sessionmaker()
38. Session.configure(bind=engine)
39.
40. session = None
41. try:
42. # une session
43. session = Session()
44.
45. # suppression de la table [personnes]
46. session.execute("drop table if exists personnes")
47.
48. # recréation de la table à partir du mapping
49. metadata.create_all(engine)
50.
51. # une insertion
52. session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
53. # session.commit()
54.
55. # une requête
56. personnes = session.query(Personne).all()
57.
58. # affichage
59. print("Liste des personnes ---------")
60. for personne in personnes:
61. print(personne)
62.
63. # deux autres insertions dont la seconde échoue à cause de l'unicité (prénom,nom)
64. session.add(Personne().fromdict({"id": 68, "prénom": "x1", "nom": "y1", "âge": 10}))
65. session.add(Personne().fromdict({"id": 69, "prénom": "x1", "nom": "y1", "âge": 10}))
66.
67. # une requête
68. personnes = session.query(Personne).all()
69.
70. # affichage
71. print("Liste des personnes ---------")
72. for personne in personnes:
73. print(personne)
74.
75. # validation de la session
76. session.commit()
77.
78. except (InterfaceError, IntegrityError) as erreur:
79. # affichage
80. print(f"L'erreur suivante s'est produite : {erreur}")
81. # annulation de la dernière session
82. if session:
83. print("rollback...")
84. session.rollback()
85. finally:
86. # on libère les ressources de la session
87. if session:
88. session.close()
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
244/755
o le connecteur Python utilisé (mysql.connector sans le .) ;
o l’utilisateur qui se connecte (admpersonnes) ;
o son mot de passe (nobody) ;
o la machine sur laquelle se trouve le SGBD (localhost=machine sur laquelle se trouve le script qui s’exécute) ;
o le nom de la base de données (dbpersonnes) ;
Avec ces informations, [sqlalchemy] peut se connecter à la base de données. A noter que le connecteur Python utilisé
doit être déjà installé. [sqlalchemy] ne le fait pas.
• lignes 19-26 : description de la table [personnes] ;
• lignes 28-34 : mapping entre la classe [Personne] et la table [personnes] ;
• lignes 36-38 : la plupart des opérations [sqlalchemy] se font dans une session. La notion de session [sqlalchemy] est proche
de celle de transaction SQL. Les sessions sont crées à partir de la classe [Session] rendue par la fonction [sessionmaker] de
la ligne 37 ;
• ligne 38 : la classe [Session] est associée à la base [dbpersonnes] via la chaîne de connexion de la ligne 14 ;
• ligne 43 : on crée une session. Comme il a été dit, on peut rapprocher une session d’une transaction ;
• lignes 45-46 : la méthode [Session.execute] permet d’exécuter un ordre SQL. Ce n’est pas quelque chose de courant
puisqu’on a dit que l’ORM permettait d’éviter le langage SQL ;
• lignes 48-49 : la méthode [metadata.create_all] permet de créer toutes les tables utilisant l’instance [MetaData] de la ligne
17. Nous n’en avons qu’une : la table [personnes] définie lignes 20-26. [sqlalchemy] va utiliser l’information de ces lignes
pour créer la table. On a là un premier intérêt de l’ORM : il cache les spécificités des SGBD. En effet, l’ordre SQL [create]
peut être très différent d’un SGBD à l’autre à cause des types donnés aux colonnes. Il n’y a pas eu d’uniformisation SQL des
types de données. Ainsi l’ordre [create] varie d’un SGBD à l’autre. Ici, grâce à [sqlalchemy] :
o nous décrivons de façon unique la table que nous désirons ;
o [sqlalchemy] se débrouille pour générer le [create] qui va bien pour le SGBD qu’il a en face de lui ;
• ligne 52 : on ajoute un objet [Personne] à la session. Cela ne l’ajoute pas automatiquement en base de données. En effet, un
ORM suit ses propres règles pour se synchroniser avec la base de données. Il va toujours chercher à optimiser le nombre de
requêtes qu’il fait. Prenons un exemple. Le script ajoute (add) deux personnes (personne1, personne2) dans la session puis fait
ensuite une requête : il veut voir toutes les personnes présentes dans la table. [sqlalchemy] peut procéder ainsi :
o l’ajout de [personne1] peut se faire en mémoire. Il n’y a pas besoin pour l’instant de le mettre en base de données ;
o idem pour [personne2] ;
o vient ensuite la requête de type [select]. Il faut alors récupérer toutes les lignes de la table [personnes]. [sqlalchemy]
va alors mettre [personne1, personne2] en base puis faire la requête ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/01/main.py
2. Liste des personnes ---------
3. {"nom": "y", "prénom": "x", "id": 67, "âge": 10}
4. L'erreur suivante s'est produite : (raised as a result of Query-invoked autoflush; consider using a
session.no_autoflush block if this flush is occurring prematurely)
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
245/755
5. (mysql.connector.errors.IntegrityError) 1062 (23000): Duplicate entry 'y1-x1' for key 'uix_1'
6. [SQL: INSERT INTO personnes (id, prenom, nom, age) VALUES (%(id)s, %(prenom)s, %(nom)s, %(age)s)]
7. [parameters: ({'id': 68, 'prenom': 'x1', 'nom': 'y1', 'age': 10}, {'id': 69, 'prenom': 'x1', 'nom': 'y1',
'age': 10})]
8. (Background on this error at: http://sqlalche.me/e/13/gkpj)
9. rollback...
10.
11. Process finished with exit code 0
On voit en [6] que la table est vide. Il n’y a même pas la 1ère personne que le script avait mis dans la session. Ceci parce que celle-ci
se déroulait dans une transaction et que celle-ci a été défaite dans la clause [except] du script [main].
1. # une insertion
2. session.add(Personne().fromdict({"id": 67, "prénom": "x", "nom": "y", "âge": 10}))
3. # session.commit()
Après avoir ajouté une personne ligne 2, nous décommentons la ligne 3. L’opération [session.commit] va valider la transaction sous-
jacente et une nouvelle transaction va démarrer. Après exécution, le contenu de la table [personnes] est le suivant :
On voit en [6] que la 1ère insertion a été conservée. Cela vient du fait qu’elle a été faite au sein d’une transaction 1 et que l’erreur qui
a suivi a été faite au sein d’une transaction 2.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
246/755
19.3 Scripts 02 : les mappings de [sqlalchemy]
Les scripts 02 sont une variante des scripts 01. On essaie de faire le maximum de configurations dans [config.py]. On y configure
maintenant l’environnement [sqlalchemy] de l’application :
1. def configure():
2. # chemin absolu référence des chemins relatifs de la configuration
3. root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
4. # chemins absolus des dépendances
5. absolute_dependencies = [
6. # BaseEntity, MyException, Personne, Utils
7. f"{root_dir}/classes/02/entities",
8. ]
9.
10. # on fixe le syspath
11. from myutils import set_syspath
12. set_syspath(absolute_dependencies)
13.
14. # imports
15. from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData, UniqueConstraint
16. from sqlalchemy.orm import mapper, sessionmaker
17.
18. # lien vers une base de données MySQL
19. engine = create_engine("mysql+mysqlconnector://admpersonnes:nobody@localhost/dbpersonnes")
20.
21. # metadata
22. metadata = MetaData()
23.
24. # la table
25. personnes_table = Table("personnes", metadata,
26. Column('id', Integer, primary_key=True),
27. Column('prenom', String(30), nullable=False),
28. Column("nom", String(30), nullable=False),
29. Column("age", Integer, nullable=False),
30. UniqueConstraint('nom', 'prenom', name='uix_1')
31. )
32.
33. # le mapping
34. from Personne import Personne
35.
36. mapper(Personne, personnes_table, properties={
37. 'id': personnes_table.c.id,
38. 'prénom': personnes_table.c.prenom,
39. 'nom': personnes_table.c.nom,
40. 'âge': personnes_table.c.age
41. })
42.
43. # la session factory
44. Session = sessionmaker()
45. Session.configure(bind=engine)
46.
47. # on met ces informations dans la config
48. config = {}
49. config["Session"] = Session
50. config["metadata"] = metadata
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
247/755
51. config["engine"] = engine
52. config["personnes_table"] = personnes_table
53.
54. # configuration des classes
55. from Personne import Personne
56. Personne.excluded_keys = ['_sa_instance_state']
57.
58. # on rend la config
59. return config
Commentaires
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # le syspath est configuré - on fait les imports
7. from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
8. from sqlalchemy.orm.exc import FlushError
9.
10. from Personne import Personne
11.
12. session = None
13. try:
14. # une session
15. session = config["Session"]()
16.
17. # suppression de la table [personnes]
18. session.execute("drop table if exists personnes")
19.
20. # recréation de la table à partir du mapping
21. config["metadata"].create_all(config["engine"])
22.
23. # deux insertions
24. session.add(Personne().fromdict({"prénom": "x", "nom": "y", "âge": 10}))
25. personne = Personne().fromdict({"prénom": "x1", "nom": "y1", "âge": 7})
26. session.add(personne)
27.
28. # validation des deux insertions
29. session.commit()
30.
31. # une requête
32. personnes = session.query(Personne).all()
33.
34. # affichage
35. print("Liste des personnes-----------")
36. for personne in personnes:
37. print(personne)
38.
39. # deux autres insertions dont la seconde échoue
40. session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))
41. session.add(Personne().fromdict({"prénom": "x2", "nom": "y2", "âge": 10}))
42.
43. # une requête
44. personnes = session.query(Personne).all()
45.
46. # affichage
47. print("Liste des personnes-----------")
48. for personne in personnes:
49. print(personne)
50.
51. # validation de la session
52. session.commit()
53.
54. except (FlushError, DatabaseError, InterfaceError, IntegrityError) as erreur:
55. # affichage
56. print(f"L'erreur suivante s'est produite : {erreur}")
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
248/755
57. # annulation de la dernière session
58. if session:
59. print("rollback...")
60. session.rollback()
61. finally:
62. # affichage
63. print("Travail terminé...")
64. # on libère les ressources de la session
65. if session:
66. session.close()
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/02/main.py
2. Liste des personnes-----------
3. {"âge": 10, "nom": "y", "prénom": "x", "id": 1}
4. {"âge": 7, "nom": "y1", "prénom": "x1", "id": 2}
5. L'erreur suivante s'est produite : (raised as a result of Query-invoked autoflush; consider using a
session.no_autoflush block if this flush is occurring prematurely)
6. (mysql.connector.errors.IntegrityError) 1062 (23000): Duplicate entry 'y2-x2' for key 'uix_1'
7. [SQL: INSERT INTO personnes (prenom, nom, age) VALUES (%(prenom)s, %(nom)s, %(age)s)]
8. [parameters: {'prenom': 'x2', 'nom': 'y2', 'age': 10}]
9. (Background on this error at: http://sqlalche.me/e/13/gkpj)
10. rollback...
11. Travail terminé...
12.
13. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
249/755
Maintenant, regardons la table [personnes] générée par [sqlalchemy] :
Le fichier de configuration [config] est le même que dans l’exemple précédent. Dans le script [main] on fait les opérations classiques
[INSERT, UPDATE, DELETE, SELECT] sur la table [personnes] à l’aide des méthodes de [sqlalchemy] :
1. # on configure l'application
2. import config
3.
4. config = config.configure()
5.
6. # imports
7. from sqlalchemy import func
8. from sqlalchemy.exc import IntegrityError, DatabaseError, InterfaceError
9. from sqlalchemy.orm.session import Session
10. from Personne import Personne
11.
12. # affiche le contenu de la table [personnes]
13. def affiche_table(session: Session):
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
250/755
14. print("----------------")
15. # une requête
16. personnes = session.query(Personne).all()
17. # affichage
18. affiche_personnes(personnes)
19.
20. # affiche une liste de personnes
21. def affiche_personnes(personnes: list):
22. print("----------------")
23. # affichage
24. for personne in personnes:
25. print(personne)
26.
27.
28. # main ---------------------------
29. session = None
30. try:
31. # une session
32. session = config["Session"]()
33.
34. # suppression de la table [personnes]
35. # checkfirst=True : vérifie d'abord que la table existe
36. config["personnes_table"].drop(config["engine"], checkfirst=True)
37.
38. # recréation de la table à partir du mapping
39. config["metadata"].create_all(config["engine"])
40.
41. # des insertions
42. session.add(Personne().fromdict({"prénom": "Pierre", "nom": "Nicazou", "âge": 35}))
43. session.add(Personne().fromdict({"prénom": "Géraldine", "nom": "Colou", "âge": 26}))
44. session.add(Personne().fromdict({"prénom": "Paulette", "nom": "Girondé", "âge": 56}))
45.
46. # on affiche le contenu de la session
47. affiche_table(session)
48.
49. # liste des personnes par ordre alphabétique des noms et à nom égal par ordre alphabétique des prénoms
50. personnes = session.query(Personne).order_by(Personne.nom.desc(), Personne.prénom.desc())
51.
52. # affichage
53. affiche_personnes(personnes)
54.
55. # liste des personnes ayant un âge dans l'intervalle [20,40] par ordre décroissant de l'âge
56. # puis à âge égal par ordre alphabétique des noms et à nom égal par ordre alphabétique des prénoms
57. personnes = session.query(Personne). \
58. filter(Personne.âge >= 20, Personne.âge <= 40). \
59. order_by(Personne.âge.desc(), Personne.nom.asc(), Personne.prénom.asc())
60.
61. # affichage
62. affiche_personnes(personnes)
63.
64. # insertion de mme Bruneau
65. bruneau = Personne().fromdict({"prénom": "Josette", "nom": "Bruneau", "âge": 46})
66. session.add(bruneau)
67. # modification de son âge
68. bruneau.âge = 47
69.
70. # liste des personnes ayant Bruneau pour nom
71. personne = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau").first()
72.
73. # affichage
74. affiche_personnes([personne])
75.
76. # suppression de Mme Bruneau
77. session.delete(personne)
78.
79. # liste des personnes ayant Bruneau pour nom
80. personnes = session.query(Personne).filter(func.lower(Personne.nom) == "bruneau")
81.
82. # affichage
83. affiche_personnes(personnes)
84.
85. # validation de la session
86. session.commit()
87.
88. except (DatabaseError, InterfaceError, IntegrityError) as erreur:
89. # affichage
90. print(f"L'erreur suivante s'est produite : {erreur}")
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
251/755
91. # annulation de la dernière session
92. if session:
93. session.rollback()
94.
95. finally:
96. # affichage
97. print("Travail terminé...")
98. # on libère les ressources de la session
99. if session:
100. session.close()
Commentaires
• lignes 20-25 : la fonction [affiche_personnes] affiche les éléments d’une liste de personnes ;
• lignes 12-18 : la fonction [affiche_table] affiche le contenu de la table [personnes] ;
• lignes 34-36 : on supprime la table [personnes]. Contrairement aux versions précédentes, on n’utilise pas un ordre SQL mais
une méthode de [sqlalchemy] :
o config["personnes_table"] est l’objet [Table] décrivant la table [personnes] ;
o config["engine"] est la chaîne de connexion à la base de données [dbpersonnes] ;
o le paramètre nommé [checkfirst=True] demande à ce que l’opération ne se fasse que si la table [personnes] existe ;
• lignes 38-39 : la table [personnes] est recréée ;
• lignes 41-44 : trois personnes sont mises dans la session. On rappelle qu’elles ne sont pas forcément insérées immédiatement
dans la table [personnes]. Cela dépend de la stratégie de [sqlalchemy] qui vise la performance ;
• lignes 46-47 : le contenu de la table [personnes] es affiché. Si les insertions des trois personnes n’avaient pas encore été faites
elles le sont maintenant à cause de cette demande ;
• lignes 49-50 : un exemple d’utilisation de la méthode [order_by] qui permet de présenter les résultats d’une requête dans un
certain ordre. La syntaxe [order_by(critère1, critère2)] affiche les résultats d’abord selon le critère [critère1] et lorsque
des lignes présentent la même valeur de [critère1], elles sont alors triées selon le critère [critère2]. On peut mettre plusieurs
critères ainsi ;
• lignes 55-59 : introduisent la notion de filtre avec la méthode [filter]. La notation [filter(critère1, critère2)] fait un
ET logique (AND) entre les critère utilisés ;
• lignes 64-67 : une nouvelle personne est mise en session ;
• lignes 70-71 : un autre exemple de requête filtrée. La fonction [func.lower(param)] rend [param] en minuscules. Il existe ainsi
d’autres fonctions disponibles notées [func.xx]. Dans l’expression de la ligne 71 :
o [session.query.filter] rend une liste d’objets [Personne] ;
o [session.query.filter.first] rend le 1er élément de cette liste ;
• ligne 77 : on supprime un élément de la session ;
• ligne 86 : la session est validée ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/03/main.py
2. ----------------
3. ----------------
4. {"âge": 35, "nom": "Nicazou", "prénom": "Pierre", "id": 1}
5. {"âge": 26, "nom": "Colou", "prénom": "Géraldine", "id": 2}
6. {"âge": 56, "nom": "Girondé", "prénom": "Paulette", "id": 3}
7. ----------------
8. {"âge": 35, "nom": "Nicazou", "prénom": "Pierre", "id": 1}
9. {"âge": 56, "nom": "Girondé", "prénom": "Paulette", "id": 3}
10. {"âge": 26, "nom": "Colou", "prénom": "Géraldine", "id": 2}
11. ----------------
12. {"âge": 35, "nom": "Nicazou", "prénom": "Pierre", "id": 1}
13. {"âge": 26, "nom": "Colou", "prénom": "Géraldine", "id": 2}
14. ----------------
15. {"prénom": "Josette", "nom": "Bruneau", "âge": 47, "id": 4}
16. ----------------
17. Travail terminé...
18.
19. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
252/755
Dans phpMyAdmin, le contenu de la table [personnes] à la fin de l’exécution est le suivant :
Le dossier [04] est une copie du dossier [03]. On change une unique chose, la chaîne de connexion dans le fichier [config] :
Désormais cette chaîne de connexion désigne la base [dbpersonnes] d’un SGBD [PostgreSQL]. On notera l’utilisation du connecteur
[psycopg2]. Il faut que celui-ci soit installé.
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/04/main.py
2. ----------------
3. ----------------
4. {"nom": "Nicazou", "prénom": "Pierre", "id": 1, "âge": 35}
5. {"nom": "Colou", "prénom": "Géraldine", "id": 2, "âge": 26}
6. {"nom": "Girondé", "prénom": "Paulette", "id": 3, "âge": 56}
7. ----------------
8. {"nom": "Nicazou", "prénom": "Pierre", "id": 1, "âge": 35}
9. {"nom": "Girondé", "prénom": "Paulette", "id": 3, "âge": 56}
10. {"nom": "Colou", "prénom": "Géraldine", "id": 2, "âge": 26}
11. ----------------
12. {"nom": "Nicazou", "prénom": "Pierre", "id": 1, "âge": 35}
13. {"nom": "Colou", "prénom": "Géraldine", "id": 2, "âge": 26}
14. ----------------
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
253/755
15. {"prénom": "Josette", "nom": "Bruneau", "âge": 47, "id": 4}
16. ----------------
17. Travail terminé...
18.
19. Process finished with exit code 0
Avec l’outil [pgAdmin] (cf. paragraphe |pgAdmin|), la table [personnes] est dans l’état suivant :
• en [4-5], on voit que la colonne [id] est clé primaire. On voit également qu’elle a une valeur par défaut [mot clé DEFAULT]
qui fait que si on insère une ligne sans clé primaire, celle-ci sera générée par le SGBD. C’est un fonctionnement fréquent : on
laisse le SGBD générer les clés primaires ;
Cette version 05 des scripts [sqlalchemy] montre bien la facilité de passer d’un SGBD à l’autre : il a suffi de changer la chaîne de
connexion dans un script de configuration. Rien d’autre n’a changé. Si on compare les types des colonnes [id, nom, prenom, age]
ci-dessus avec ceux de la table MySQL de l’exemple |02|, on voit qu’ils sont différents. [sqlalchemy] les adapte au SGBD utilisé.
Cette facilité de s’adapter à un nouveau SGBD est une raison suffisante pour adopter [sqlalchemy] ou un autre ORM.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
254/755
19.6 Scripts 05 : exemple complet
L’exemple étudié est une reprise de celui étudié au paragraphe |troiscouches-v01|. Cet exemple présentait une architecture à trois
couches [ui, métier, dao] qui manipulait des entités [Classe, Elève, Matière, Note]. Les entités étaient codées en dur dans une
couche [dao]. Nous les mettons maintenant dans une base de données. Nous utiliserons deux SGBD : MySQL et PostgreSQL.
• en [1-3], on trouve les couches [ui, métier, dao] déjà présentes dans l’exemple |troiscouches-v01|. La couche [dao]
communique désormais avec la couche [ORM] ;
• les couches [1-5] sont implémentées par du code Python ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
255/755
• en [1], la base [dbecole] sans tables [3] ;
• en [7], l’utilisateur [admecole] a tous les privilèges sur cette base de données ;
On fait de même avec le SGBD PostgreSQL. Nous construisons une base nommée [dbecole] propriété de l’utilisateur [admecole]
ayant le mot de passe [mdpecole]. Pour cela nous suivons la procédure décrite au paragraphe |création d'une base de données| :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
256/755
• en [1], la base [dbecole] ;
• en [2], l’utilisateur [admecole] ;
• en [3-4], la base [dbecole] est la propriété de l’utilisateur [admecole] ;
La classe [Classe] :
1. # imports
2. from BaseEntity import BaseEntity
3. from MyException import MyException
4. from Utils import Utils
5.
6.
7. class Classe(BaseEntity):
8. # attributs exclus de l'état de la classe
9. excluded_keys = []
10.
11. # propriétés de la classe
12. @staticmethod
13. def get_allowed_keys() -> list:
14. # id : identifiant de la classe
15. # nom : nom de la classe
16. return BaseEntity.get_allowed_keys() + ["nom"]
17.
18. # getter
19. @property
20. def nom(self: object) -> str:
21. return self.__nom
22.
23. # setters
24. @nom.setter
25. def nom(self: object, nom: str):
26. # nom doit être une chaîne de caractères non vide
27. if Utils.is_string_ok(nom):
28. self.__nom = nom
29. else:
30. raise MyException(11, f"Le nom de la classe {self.id} doit être une chaîne de caractères non vide")
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
257/755
La classe [Elève] :
1. # imports
2. from BaseEntity import BaseEntity
3. from Classe import Classe
4. from MyException import MyException
5.
6. from Utils import Utils
7.
8.
9. class Elève(BaseEntity):
10. # attributs exclus de l'état de la classe
11. excluded_keys = []
12.
13. # propriétés de la classe
14. @staticmethod
15. def get_allowed_keys() -> list:
16. # id : identifiant de l'élève
17. # nom : nom de l'élève
18. # prénom : prénom de l'élève
19. # classe : classe de l'élève
20. return BaseEntity.get_allowed_keys() + ["nom", "prénom", "classe"]
21.
22. # getters
23. @property
24. def nom(self: object) -> str:
25. return self.__nom
26.
27. @property
28. def prénom(self: object) -> str:
29. return self.__prénom
30.
31. @property
32. def classe(self: object) -> Classe:
33. return self.__classe
34.
35. # setters
36. @nom.setter
37. def nom(self: object, nom: str) -> str:
38. # nom doit être une chaîne de caractères non vide
39. if Utils.is_string_ok(nom):
40. self.__nom = nom
41. else:
42. raise MyException(41, f"Le nom de l'élève {self.id} doit être une chaîne de caractères non vide")
43.
44. @prénom.setter
45. def prénom(self: object, prénom: str) -> str:
46. # prénom doit être une chaîne de caractères non vide
47. if Utils.is_string_ok(prénom):
48. self.__prénom = prénom
49. else:
50. raise MyException(42, f"Le prénom de l'élève {self.id} doit être une chaîne de caractères non vide")
51.
52. @classe.setter
53. def classe(self: object, value):
54. try:
55. # on attend un type Classe
56. if isinstance(value, Classe):
57. self.__classe = value
58. # ou un type dict
59. elif isinstance(value,dict):
60. self.__classe=Classe().fromdict(value)
61. # ou un type json
62. elif isinstance(value,str):
63. self.__classe = Classe().fromjson(value)
64. except BaseException as erreur:
65. raise MyException(43, f"L'attribut [{value}] de l'élève {self.id} doit être de type Classe ou dict ou json.
Erreur : {erreur}")
La classe [Matière] :
1. # imports
2. from BaseEntity import BaseEntity
3. from MyException import MyException
4. from Utils import Utils
5.
6.
7. class Matière(BaseEntity):
8. # attributs exclus de l'état de la classe
9. excluded_keys = []
10.
11. # propriétés de la classe
12. @staticmethod
13. def get_allowed_keys() -> list:
14. # id : identifiant de la matière
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
258/755
15. # nom : nom de la matière
16. # coefficient : coefficient de la matière
17. return BaseEntity.get_allowed_keys() + ["nom", "coefficient"]
18.
19. # getter
20. @property
21. def nom(self: object) -> str:
22. return self.__nom
23.
24. @property
25. def coefficient(self: object) -> float:
26. return self.__coefficient
27.
28. # setters
29. @nom.setter
30. def nom(self: object, nom: str):
31. # nom doit être une chaîne de caractères non vide
32. if Utils.is_string_ok(nom):
33. self.__nom = nom
34. else:
35. raise MyException(21, f"Le nom de la matière {self.id} doit être une chaîne de caractères non vide")
36.
37. @coefficient.setter
38. def coefficient(self, coefficient: float):
39. # le coefficient doit être un réel >=0
40. erreur = False
41. if isinstance(coefficient, (int, float)):
42. if coefficient >= 0:
43. self.__coefficient = coefficient
44. else:
45. erreur = True
46. else:
47. erreur = True
48. # erreur ?
49. if erreur:
50. raise MyException(22, f"Le coefficient de la matière {self.nom} doit être un réel >=0")
La classe [Note] :
1. # imports
2. from BaseEntity import BaseEntity
3. from Elève import Elève
4. from Matière import Matière
5. from MyException import MyException
6.
7.
8. class Note(BaseEntity):
9. # attributs exclus de l'état de la classe
10. excluded_keys = []
11.
12. # propriétés de la classe
13. @staticmethod
14. def get_allowed_keys() -> list:
15. # id : identifiant de la note
16. # valeur : la note elle-même
17. # élève : élève (de type Elève) concerné par la note
18. # matière : matière (de type Matière) concernée par la note
19. # l'objet Note est donc la note d'un élève dans une matière
20. return BaseEntity.get_allowed_keys() + ["valeur", "élève", "matière"]
21.
22. # getters
23. @property
24. def valeur(self: object) -> float:
25. return self.__valeur
26.
27. @property
28. def élève(self: object) -> Elève:
29. return self.__élève
30.
31. @property
32. def matière(self: object) -> Matière:
33. return self.__matière
34.
35. # getters
36. @valeur.setter
37. def valeur(self: object, valeur: float):
38. # la note doit être un réel entre 0 et 20
39. if isinstance(valeur, (int, float)) and 0 <= valeur <= 20:
40. self.__valeur = valeur
41. else:
42. raise MyException(31,
43. f"L'attribut {valeur} de la note {self.id} doit être un nombre dans l'intervalle [0,20]")
44.
45. @élève.setter
46. def élève(self: object, value):
47. try:
48. # on attend un type Elève
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
259/755
49. if isinstance(value, Elève):
50. self.__élève = value
51. # ou un type dict
52. elif isinstance(value, dict):
53. self.__élève = Elève().fromdict(value)
54. # ou un type json
55. elif isinstance(value, str):
56. self.__élève = Elève().fromjson(value)
57. except BaseException as erreur:
58. raise MyException(32,
59. f"L'attribut [{value}] de la note {self.id} doit être de type Elève ou dict ou json. Erreur : {erreur}")
60.
61. @matière.setter
62. def matière(self: object, value):
63. try:
64. # on attend un type Matière
65. if isinstance(value, Matière):
66. self.__matière = value
67. # ou un type dict
68. elif isinstance(value, dict):
69. self.__matière = Matière().fromdict(value)
70. # ou un type json
71. elif isinstance(value, str):
72. self.__matière = Matière().fromjson(value)
73. except BaseException as erreur:
74. raise MyException(33,
75. f"L'attribut [{value}] de la note {self.id} doit être de type Matière ou dict ou json. Erreur :
{erreur}")
19.6.4 Configuration
• la configuration générale dans [config.py] : elle établit le Python Path de l’application et instancie les couches de l’architecture ;
• la configuration de [sqlalchemy] dans [config_database] : elle fait les mappings Classes / Tables ;
• les couches de l’application sont configurées dans [config_layers] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
260/755
20. # dossiers du présent projet
21. script_dir,
22. f"{script_dir}/../services",
23. ]
24.
25. # mise à jour du syspath
26. from myutils import set_syspath
27. set_syspath(absolute_dependencies)
28.
29. # étape 2 ------
30. # configuration base de données
31. import config_database
32. config = config_database.configure(config)
33.
34. # étape 3 ------
35. # instanciation des couches de l'application
36. import config_layers
37. config = config_layers.configure(config)
38.
39. # on rend la config
40. return config
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
261/755
49. Column('classe_id', Integer, ForeignKey('classes.id')),
50. )
51. # mapping
52. mapper(Elève, tables['élèves'], properties={
53. 'id': élèves_table.c.id,
54. 'nom': élèves_table.c.nom,
55. 'prénom': élèves_table.c.prénom,
56. 'classe': relationship(Classe, backref="élèves", lazy="select")
57. })
58.
59. # la table des matières
60. tables['matières'] = matières_table = \
61. Table("matières", metadata,
62. Column('id', Integer, primary_key=True),
63. Column('nom', String(30), nullable=False),
64. Column('coefficient', Float, nullable=False)
65. )
66. # mapping
67. mapper(Matière, tables['matières'], properties={
68. 'id': matières_table.c.id,
69. 'nom': matières_table.c.nom,
70. "coefficient": matières_table.c.coefficient
71. })
72.
73. # la table des notes
74. tables['notes'] = notes_table = \
75. Table("notes", metadata,
76. Column('id', Integer, primary_key=True),
77. Column('valeur', Float, nullable=False),
78. # une note est celle d'un élève
79. Column('élève_id', Integer, ForeignKey('élèves.id')),
80. # une note est celle d'une matière
81. Column('matière_id', Integer, ForeignKey('matières.id')),
82. )
83.
84. # mapping
85. mapper(Note, tables['notes'], properties={
86. 'id': notes_table.c.id,
87. 'valeur': notes_table.c.valeur,
88. 'élève': relationship(Elève, backref="notes", lazy="select"),
89. 'matière': relationship(Matière, backref="notes", lazy="select")
90. })
91.
92. # configuration des entités [BaseEntity]
93. Elève.excluded_keys = ['_sa_instance_state', 'notes', 'classe']
94. Classe.excluded_keys = ['_sa_instance_state', 'élèves']
95. Matière.excluded_keys = ['_sa_instance_state', 'notes']
96. Note.excluded_keys = ['_sa_instance_state', 'matière', 'élève']
97.
98. # la session factory
99. Session = sessionmaker()
100. Session.configure(bind=engine)
101.
102. # une session
103. session = Session()
104.
105. # on enregistre certaines informations dans le dictionnaire de la configuration
106. config['database'] = {"engine": engine, "metadata": metadata, "tables": tables, "session": session}
107.
108. # on rend la config
109. return config
Commentaires
• lignes 1-4 : la fonction [configure] reçoit un dictionnaire en paramètre. Seule la clé [sgbd] est exploitée. Elle vaut [mysql] si
la base est une base MySQL, [pgres] si la base est une base PostgreSQL ;
• lignes 6-9 : imports d’éléments de [sqlalchemy]. Le script [config_database] fait les mappings entre les tables de la base
[dbecole] et les entités [Classes, Elève, Matière, Note]. Dans la table, les données de l’entité sont encapsulées dans une
ligne. Dans le code Python, elles sont encapsulées dans un objet. D’où le nom ORM (Object Relational Mapper) : l’ORM fait
un mapping (une liaison) entre les lignes d’une base de données relationnelle et des objets. Dans cette application, nous avons
quatre entités [Classe, Elève, Matière, Note] qui seront reliées à quatre tables [classes, élèves, matières, notes].
Notez que les noms des tables peuvent comporter des caractères accentués ;
• lignes 11-17 : la chaîne de connexion à la base de données exploitée. Celle-ci dépend de l’élément config[‘sgbd’] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
262/755
• lignes 24-28 : les entités de l’application qui vont faire l’objet d’un mapping [sqlalchemy]. Lorsque ces lignes seront exécutées,
le Python Path aura déjà été établi par le script [config] ;
• lignes 30-40 : le mapping entre l’entité [Classe] et la table [classes] ;
• lignes 30-35 : la table [classes] est définie avec la classe [Table] de [sqlalchemy]. Nous indiquons que cette table a deux
colonnes :
o la colonne [id] qui est clé primaire et qui est le n° de la classe, ligne 33 ;
o la colonne [nom] qui contient le nom de la classe, ligne 34 ;
• lignes 31-32 : notez que la syntaxe x=y=z est légale en Python : la valeur de z est affectée à y puis la valeur de y à x ;
• lignes 37-40 : on liste les correspondances entre les colonnes de la table [classes] et les propriétés de l’entité [Classe] ;
• lignes 42-57 : le mapping entre l’entité [Elève] et la table [élèves] ;
• lignes 51-57 : la table [élèves] est définie avec la classe [Table] de [sqlalchemy]. Nous indiquons que cette table a quatre
colonnes :
o la colonne [id] qui est clé primaire et qui est le n° de l’élève, ligne 45 ;
o la colonne [nom] qui contient le nom de l’élève, ligne 46 ;
o la colonne [prénom] qui contient le prénom de l’élève, ligne 47. Notez qu’un nom de colonne peut comporter des
caractères accentués ;
o ligne 49, la colonne [classe_id] qui contiendra le n° de la classe à laquelle appartient l’élève. On appelle cela une clé
étrangère. [élèves.classe_id] est une clé étrangère (ForeignKey) sur la colonne [classes.id]. Cela signifie que la valeur
de [élèves.classe_id] doit exister dans la colonne [classes.id] ;
• lignes 51-57 : on liste les correspondances entre les colonnes de la table [élèves] et les propriétés de l’entité [Elève] :
o les lignes 53-55 sont simples à comprendre ;
o la ligne 56 est plus difficile : elle définit la valeur de la propriété [Elève.classe] comme étant calculée par la relation
(relationship) de clé étrangère qui relie les tables [élèves] et [classes]. Les paramètres de la fonction [relationship]
sont les suivants :
▪ [Classe] : c’est le nom de l’entité avec laquelle l’entité [Elève] a une relation de clé étrangère. Celle-ci doit se
matérialiser dans la table [élèves] par la présence d’une clé étrangère sur la table [classes]. Nous savons que celle-
ci existe ;
▪ [backref="élèves"] : le nom d’une propriété qui sera ajoutée à l’entité [Classe]. [Classe.élèves] sera la liste de
tous les élèves de la classe. Cette propriété ne doit pas déjà exister. Si elle existe déjà, il faut simplement choisir
un autre nom ici pour [backref]. Le développeur n’a pas à gérer cette propriété. C’est [sqlalchemy] qui le fera. Il
doit simplement savoir qu’elle existe, ajoutée par [sqlalchemy], et qu’il peut l’utiliser dans son code ;
▪ [lazy=’select’] : cela signifie que l’ORM ne doit pas chercher à donner immédiatement une valeur à la propriété
[Elève.classe]. Il ne doit chercher sa valeur que lorsque le code la demande explicitement. Ainsi :
si le code demande la liste de tous les élèves, ceux-ci seront ramenés mais leur propriété [classe] ne sera pas
calculée ;
un peu plus tard, le code s’intéresse à un élève [e] précis et référence sa classe [e.classe]. Cette référence va
alors forcer [sqlalchemy] à faire une requête en base pour ramener la classe de l’élève, ceci de façon
transparente pour le développeur ;
aussi mettre [lazy=’select’] vise à éviter les requêtes inutiles en base ;
o ligne 56 : lorsque l’ORM récupère une ligne de la table [élèves], il récupère les informations [id, nom, prénom,
classe_id]. A partir de là, il doit construire un objet Elève(id, nom, prénom, classe). Pour les propriétés [id, nom,
prénom] ça ne pose pas de difficultés. Pour la propriété [classe], c’est plus compliqué. Sa valeur est une référence d’objet
de type [Classe]. Or l’ORM ne dispose que d’une information [élèves.classe_id]. Comme [élèves.classe_id] est
une clé étrangère sur la colonne [classes.id], on lui dit ici d’utiliser cette relation pour récupérer dans la table [classes]
la ligne de n° id=[élèves.classe_id] (elle existe forcément) et de créer à partir de cette ligne l’objet [Classe] attendu
par la propriété [Elève.classe] ;
• lignes 59-71 : le mapping entre l’entité [Matière] et la table [matières] ;
• lignes 59-65 : définition de la table [sqlalchemy] nommée [matières] ;
• lignes 66-71 : on liste les correspondances entre les colonnes de la table [matières] et les propriétés de l’entité [Matière]. Il
n’y a pas de difficultés ici ;
• lignes 73-90 : le mapping entre l’entité [Note] et la table [notes] ;
• lignes 73-82 : définition de la table [sqlalchemy] nommée [notes]. Elle a deux clés étrangères :
o ligne 79, la colonne [notes.élève_id] prend ses valeurs dans la colonne [élèves.id] ]. Cette clé étrangère matérialise le
fait qu’une note est la note d’un élève précis ;
o ligne 81, la colonne [notes.matière_id] prend ses valeurs dans la colonne [matières.id]. Cette clé étrangère matérialise
le fait qu’une note est une note dans une matière précise ;
• lignes 84-90 : le mapping entre l’entité [Note] et la table [notes] :
o ligne 88 : la propriété [Note.élève] doit avoir pour valeur une instance de type [Elève]. L’ORM n’a pour information
dans la ligne de la table [notes] que la colonne [notes.élève_id] qui référence la colonne [élèves.id]. On dit ici
d’utiliser cette relation de clé étrangère pour retrouver l’intance [Elève] dont on a la note. Par ailleurs
[relationship(Elève, backref="notes", …)] va créer la nouvelle propriété [Elève.notes] qui sera la liste des notes de
l’élève. Cette propriété ne doit pas déjà exister dans la classe [Elève] ;
o ligne 89 : la propriété [Note.matière] doit avoir pour valeur une instance de type [Matière]. L’ORM n’a pour
information dans la ligne de la table [notes] que la colonne [notes.matière_id] qui référence la colonne [matières.id].
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
263/755
On dit ici d’utiliser cette relation de clé étrangère pour retrouver l’intance [Matière] dont on a la note. Par ailleurs
[relationship(Matière, backref="notes", …)] va créer la nouvelle propriété [Matière.notes] qui sera la liste des notes
dans la matière. Cette propriété ne doit pas déjà exister dans la classe [Matière] ;
• lignes 92-96 : on définit pour chaque entité dérivée de [BaseEntity] la liste des propriétés à exclure du dictionnaire des
propriétés de l’entité (BaseEntity.asdict). Nous avons vu que [sqlalchemy] ajoutait la propriété [_sa_instance_state] à toutes
entités mappées. On ne la veut pas dans le dictionnaire des propriétés. Par ailleurs on a vu que les mappings précédents avaient
ajouté de nouvelles propriétés aux entités :
o [Elève.notes] : toutes les notes de l’élève ;
o [Classe.élèves] : tous les élèves de la classe ;
o [Matière.notes] : toutes les notes de la matière ;
En général, on ve veut pas ces propriétés ajoutées dans l’état de l’entité. En effet, calculer leur valeur a un coût SQL et
cette valeur est souvent inutile. Ainsi si on récupère l’élève de nom ‘X’ :
o l’ORM va ramener une entité [Elève(id, nom, prénom, classe, notes)]. A cause de [lazy=’select’], les propriétés
[classe, notes] liées à des clés étrangères de la base n’auront pas été calculées ;
o maintenant si j’affiche la chaîne jSON de cet élève, on sait que ce sera la chaîne jSON du dictionnaire [asdict] de l’entité.
Si les propriétés [classe] et [notes] sont dedans, [sqlalchemy] va être obligé de faire des requêtes SQL pour calculer
leurs valeurs. C’est coûteux. Si on peut éviter ces requêtes, c’est préférable ;
o ici, nous avons exclu toutes les propriétés liées à une clé étrangère ;
• lignes 98-100 : instanciation et configuration d’une [Session factory] (factory=usine de production). L’objet [Session] sert
à créer des sessions [sqlalchemy] adossées à des transactions ;
• lignes 102-103 : création d’une session sqlalchemy] ;
• ligne 106 : certains éléments de la configuration [sqlalchemy] sont mis dans le dictionnaire global de la configuration de
l’application ;
• ligne 109 : on rend ce dictionnaire ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
264/755
19.6.5 La couche [dao] - 1
Il faut comprendre ici que la couche [dao] [3] communique avec l’ORM [sqlalchemy] [4] configuré comme il a été décrit dans le
paragraphe précédent. Des trois couches [ui, métier, dao] de l’application |troiscouches v01|, seule la couche [dao] doit être
réécrite. Les couches [ui, métier] sont conservées.
• ligne 6 : l’interface [InterfaceDatabaseDao] dérive à la fois de la classe [ABC] pour être une classe abstraite et de l’interface
[InterfaceDao] du projet |troiscouches v01| ;
• lignes 8-11 : on ajoute la méthode [init_database] aux méthodes héritées de [InterfaceDao]. Elle aura pour rôle d’initialiser
la base de données avec les données du dictionnaire [data] qu’on lui passe en paramètre ligne 10 ;
1. # imports
2. from abc import ABC, abstractmethod
3.
4. # interface Dao
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
265/755
5. from Elève import Elève
6.
7.
8. class InterfaceDao(ABC):
9. # liste des classes
10. @abstractmethod
11. def get_classes(self: object) -> list:
12. pass
13.
14. # liste des élèves
15. @abstractmethod
16. def get_élèves(self: object) -> list:
17. pass
18.
19. # liste des matières
20. @abstractmethod
21. def get_matières(self: object) -> list:
22. pass
23.
24. # liste des notes
25. @abstractmethod
26. def get_notes(self: object) -> list:
27. pass
28.
29. # liste des notes d'un élève
30. @abstractmethod
31. def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
32. pass
33.
34. # chercher un élève par son id
35. @abstractmethod
36. def get_élève_by_id(self: object, élève_id: int) -> Elève:
37. pass
1. def configure():
2. from Classe import Classe
3. from Elève import Elève
4. from Matière import Matière
5. from Note import Note
6.
7. # on instancie les classes
8. classe1 = Classe().fromdict({"id": 1, "nom": "classe1"})
9. classe2 = Classe().fromdict({"id": 2, "nom": "classe2"})
10. classes = [classe1, classe2]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
266/755
11. # les matières
12. matière1 = Matière().fromdict({"id": 1, "nom": "matière1", "coefficient": 1})
13. matière2 = Matière().fromdict({"id": 2, "nom": "matière2", "coefficient": 2})
14. matières = [matière1, matière2]
15. # les élèves
16. élève11 = Elève().fromdict({"id": 11, "nom": "nom1", "prénom": "prénom1", "classe": classe1})
17. élève21 = Elève().fromdict({"id": 21, "nom": "nom2", "prénom": "prénom2", "classe": classe1})
18. élève32 = Elève().fromdict({"id": 32, "nom": "nom3", "prénom": "prénom3", "classe": classe2})
19. élève42 = Elève().fromdict({"id": 42, "nom": "nom4", "prénom": "prénom4", "classe": classe2})
20. élèves = [élève11, élève21, élève32, élève42]
21. # les notes des élèves dans les différentes matières
22. note1 = Note().fromdict({"id": 1, "valeur": 10, "élève": élève11, "matière": matière1})
23. note2 = Note().fromdict({"id": 2, "valeur": 12, "élève": élève21, "matière": matière1})
24. note3 = Note().fromdict({"id": 3, "valeur": 14, "élève": élève32, "matière": matière1})
25. note4 = Note().fromdict({"id": 4, "valeur": 16, "élève": élève42, "matière": matière1})
26. note5 = Note().fromdict({"id": 5, "valeur": 6, "élève": élève11, "matière": matière2})
27. note6 = Note().fromdict({"id": 6, "valeur": 8, "élève": élève21, "matière": matière2})
28. note7 = Note().fromdict({"id": 7, "valeur": 10, "élève": élève32, "matière": matière2})
29. note8 = Note().fromdict({"id": 8, "valeur": 12, "élève": élève42, "matière": matière2})
30. notes = [note1, note2, note3, note4, note5, note6, note7, note8]
31. # on regroupe l'ensemble
32. data = {"élèves": élèves, "classes": classes, "matières": matières, "notes": notes}
33. # on rend les données
34. return data
• ligne 34 : le dictionnaire qui sera passé à la méthode [init_database]. Ce dictionnaire est composé des clés suivantes (ligne
32) :
o [élèves] : la liste des élèves ;
o [classes] : la liste des classes ;
o [matières] : la liste des matières ;
o [notes] : la liste des notes de tous les élèves dans toutes les matières ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
267/755
42. # commit
43. session.commit()
44. except (DatabaseError, InterfaceError, IntegrityError) as erreur:
45. # annulation de la session
46. if session:
47. session.rollback()
48. # on remonte l'exception
49. raise MyException(23, f"{erreur}")
Le script [main_init_database] initialise la base de données avec le contenu du script [data.py]. Son code est le suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
268/755
21. import data
22. data = data.configure()
23.
24. # on récupère la couche [dao]
25. dao = config["dao"]
26.
27. # ----------- main
28. try:
29. # création et initialisation des tables de la bd
30. dao.init_database(data)
31. except MyException as ex:
32. # on affiche l'erreur
33. print(f"L'erreur suivante s'est produite : {ex}")
34. finally:
35. # libération des ressources mobilisées par l'application
36. import shutdown
37. shutdown.execute(config)
38. # fin
39. print("Travail terminé...")
• lignes 1-11 : le script attend un paramètre [mysql] ou [pgres] selon qu’on veut initialiser une base MySQL ou PostgreSQL ;
• lignes 13-15 : l’application est configurée pour le SGBD passé en paramètre ;
• lignes 20-22 : on récupère les données à mettre en base ;
• ligne 25 : la couche [dao] a déjà été instanciée et est accessible dans la configuration de l’application ;
• ligne 30 : la base de données est initialisée ;
• lignes 34-37 : erreur ou pas, on libère les ressources de l’application à l’aide du module [shutdown] ;
La fonction [shutdown.execute] ferme la session [sqlalchemy] utilisée pour initialiser la base de données.
Nous créons une 1ère configuration d’exécution (cf. |configuration d’exécution|) pour exécuter [main_init_database] avec le SGBD
MySQL :
Les résultats de l’exécution de cette configuration sont les suivants dans phpMyAdmin :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
269/755
Pour le SGBD [PostgreSQL], nous utilisons la configuration d’exécution suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
270/755
A l’exécution, les résultats dans [pgAdmin] sont les suivants :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
271/755
On notera la facilité avec laquelle on a pu changer de SGBD.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
272/755
23. # requête
24. return self.session.query(Classe).all()
25.
26. # liste de tous les élèves
27. def get_élèves(self: object) -> list:
28. # requête
29. return self.session.query(Elève).all()
30.
31. # liste de toutes les matières
32. def get_matières(self: object) -> list:
33. # requête
34. return self.session.query(Matière).all()
35.
36. # la liste des notes de tous les élèves
37. def get_notes(self: object) -> list:
38. # requête
39. return self.session.query(Note).all()
40.
41. # la liste des notes d'un élève particulier
42. def get_notes_for_élève_by_id(self: object, élève_id: int) -> list:
43. # on cherche l'élève - une exception est lancée s'il n'existe pas
44. # on la laisse remonter
45. élève = self.get_élève_by_id(élève_id)
46. # on récupère ses notes (lazy loading)
47. notes = élève.notes
48. # on rend un dictionnaire
49. return {"élève": élève, "notes": notes}
50.
51. # un élève repéré par son n°
52. def get_élève_by_id(self, élève_id: int) -> Elève:
53. # on cherche l'élève
54. élèves = self.session.query(Elève).filter(Elève.id == élève_id).all()
55. # a-t-on trouvé ?
56. if élèves:
57. return élèves[0]
58. else:
59. raise MyException(11, f"L'élève d'identifiant {élève_id} n'existe pas")
60.
61. # un élève repéré par son nom
62. def get_élève_by_name(self, élève_name: str) -> Elève:
63. # on cherche l'élève
64. élèves = self.session.query(Elève).filter(Elève.nom == élève_name).all()
65. # a-t-on trouvé ?
66. if élèves:
67. return élèves[0]
68. else:
69. raise MyException(12, f"L'élève de nom {élève_name} n'existe pas")
70.
71. # une classe repérée par son n°
72. def get_classe_by_id(self, classe_id: int) -> Classe:
73. # on cherche la classe
74. classes = self.session.query(Classe).filter(Classe.id == classe_id).all()
75. # a-t-on trouvé ?
76. if classes:
77. return classes[0]
78. else:
79. raise MyException(13, f"La classe d'identifiant {classe_id} n'existe pas")
80.
81. # une classe repérée par son nom
82. def get_classe_by_name(self, classe_name: str) -> Classe:
83. # on cherche l'classe
84. classes = self.session.query(Classe).filter(Classe.nom == classe_name).all()
85. # a-t-on trouvé ?
86. if classes:
87. return classes[0]
88. else:
89. raise MyException(14, f"La classe de nom {classe_name} n'existe pas")
90.
91. # une matière repérée par son n°
92. def get_matière_by_id(self, matière_id: int) -> Matière:
93. # on cherche l'matière
94. matières = self.session.query(Matière).filter(Matière.id == matière_id).all()
95. # a-t-on trouvé ?
96. if matières:
97. return matières[0]
98. else:
99. raise MyException(11, f"La matière d'identifiant {matière_id} n'existe pas")
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
273/755
100.
101. # une matière repérée par son nom
102. def get_matière_by_name(self, matière_name: str) -> Matière:
103. # on cherche la matière
104. matières = self.session.query(Matière).filter(Matière.nom == matière_name).all()
105. # a-t-on trouvé ?
106. if matières:
107. return matières[0]
108. else:
109. raise MyException(15, f"La matière de nom {matière_name} n'existe pas")
• lignes 21-24 : la méthode [get_classes] doit rendre la liste des classes de l’école. Ligne 20, nous utilisons une requête déjà
rencontrée ;
• lignes 26-39 : trois autres méthodes similaires pour obtenir les listes des élèves, des matières et des notes ;
• lignes 51-59 : la méthode [get_élève_by_id] doit rendre un élève identifié par son n°. Elle lance une exception si celui-ci
n’existe pas ;
o ligne 54 : on utilise une requêtre filtrée. On obtient une liste vide ou avec un élément ;
o ligne 57 : si la liste récupérée n’est pas vide on rend le 1er élément de la liste ;
o sinon ligne 59, on lance une exception ;
• lignes 41-49 : la méthode [get_notes_for_élève_by_id] doit rendre les notes d’un élève identifié par son n° :
o ligne 45, on utilise la méthode [get_élève_by_id] pour obtenir l’entité Elève de l’élève ;
o ligne 47, on utilise la propriété [Elève.notes] créée par le mapping entre l’entité [Note] et la table [notes] (cf. paragraphe
|configuration sqlalchemy|) et qui représente les notes de l’élève ;
o ligne 49 : on rend un dictionnaire ;
• lignes 61-109 : une série de méthodes analogues permettant de :
o retrouver un élève par son nom, lignes 61-69 ;
o retrouver une classe, lignes 71-89 ;
o retrouver une matière, lignes 91-109 ;
Le script [main_joined_queries] s’appelle ainsi parce qu’il vise à mettre en lumière les requêtes faites implicitement par [sqlalchemy]
pour récupérer des informations appartenant à plusieurs tables. Ces requêtes cachées au programmeur sont faites à chaque fois qu’une
propriété d’une entité a été associée à la fonction [relationship] dans le mapping de l’entité. Par exemple :
1. # mapping
2. mapper(Note, tables['notes'], properties={
3. 'id': notes_table.c.id,
4. 'valeur': notes_table.c.valeur,
5. 'élève': relationship(Elève, backref="notes", lazy="select"),
6. 'matière': relationship(Matière, backref="notes", lazy="select")
7. })
• ligne 5, lorsque la propriété [élève] d’une entité [Note] est demandée pour la 1ère fois, elle sera cherchée dans la table [élèves]
via une requête SQL. Tant que cette propriété n’a pas été demandée, elle reste indéfinie (lazy load). Une fois qu’elle a été
obtenue, sa valeur reste en mémoire de l’ORM. Lorsqu’elle sera référencée une deuxième fois, l’ORM délivrera immédiatement
sa valeur sans passer par une nouvelle requête SQL. Tout cela est transparent pour le développeur ;
• idem pour la propriété inverse [Elève.notes] (backref), ligne 5 ;
• idem pour la propriété [Note.matière] et sa propriété inverse [Matière.notes] (backref), ligne 6 ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
274/755
Le script [main_joined_queries] est le suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
275/755
76. # même chose pour matière2
77. print("matière de nom='matière2' -----------")
78. matière = dao.get_matière_by_name('matière2')
79. print(f"matière={matière}")
80. print("Notes dans la matière : ")
81. for note in matière.notes:
82. print(f"note={note}")
83. except MyException as ex1:
84. # on affiche l'erreur
85. print(f"L'erreur 1 suivante s'est produite : {ex1}")
86. except BaseException as ex2:
87. # on affiche l'erreur
88. print(f"L'erreur 2 suivante s'est produite : {ex2}")
89. finally:
90. # on libère les ressources
91. import shutdown
92. shutdown.execute(config)
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/05/main/main_joined_queries.py mysql
2. élève id=11 -----------
3. élève={"classe_id": 1, "nom": "nom1", "prénom": "prénom1", "id": 11}
4. classe de l'élève : {"nom": "classe1", "id": 1}
5. élèves dans la même classe :
6. élève={"classe_id": 1, "nom": "nom1", "prénom": "prénom1", "id": 11}
7. élève={"classe_id": 1, "nom": "nom2", "prénom": "prénom2", "id": 21}
8. élève nom='nom2' -----------
9. élève={"classe_id": 1, "nom": "nom2", "prénom": "prénom2", "id": 21}
10. classe de l'élève : {"nom": "classe1", "id": 1}
11. notes de l'élève id=11 -----------
12. note={"matière_id": 1, "valeur": 10.0, "élève_id": 11, "id": 1}, matière={"coefficient": 1.0, "nom":
"matière1", "id": 1}
13. note={"matière_id": 2, "valeur": 6.0, "élève_id": 11, "id": 5}, matière={"coefficient": 2.0, "nom":
"matière2", "id": 2}
14. élèves de la classe nom='classe1' -----------
15. {"classe_id": 1, "nom": "nom1", "prénom": "prénom1", "id": 11}
16. {"classe_id": 1, "nom": "nom2", "prénom": "prénom2", "id": 21}
17. élèves de la classe de nom 'classe2' -----------
18. {"classe_id": 2, "nom": "nom3", "prénom": "prénom3", "id": 32}
19. {"classe_id": 2, "nom": "nom4", "prénom": "prénom4", "id": 42}
20. matière de nom='matière1' -----------
21. matière={"coefficient": 1.0, "nom": "matière1", "id": 1}
22. Notes dans la matière :
23. {"matière_id": 1, "valeur": 10.0, "élève_id": 11, "id": 1}
24. {"matière_id": 1, "valeur": 12.0, "élève_id": 21, "id": 2}
25. {"matière_id": 1, "valeur": 14.0, "élève_id": 32, "id": 3}
26. {"matière_id": 1, "valeur": 16.0, "élève_id": 42, "id": 4}
27. matière de nom='matière2' -----------
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
276/755
28. matière={"coefficient": 2.0, "nom": "matière2", "id": 2}
29. Notes dans la matière :
30. note={"matière_id": 2, "valeur": 6.0, "élève_id": 11, "id": 5}
31. note={"matière_id": 2, "valeur": 8.0, "élève_id": 21, "id": 6}
32. note={"matière_id": 2, "valeur": 10.0, "élève_id": 32, "id": 7}
33. note={"matière_id": 2, "valeur": 12.0, "élève_id": 42, "id": 8}
34.
35. Process finished with exit code 0
Pour comprendre ces résultats, il faut se rappeler qu’on a exclu certaines propriétés du dictionnaire des entités (cf |configuration|) :
Ainsi, lorsqu’on écrit [print(f"élève={élève}")] ligne 26 du code, la ligne 1 ci-dessus nous dit que les propriétés
['_sa_instance_state', 'notes', 'classe'] ne seront pas affichées. C’est ce qu’on voit ligne 3 des résultats. Toutes les autres
propriétés sont affichées. Ainsi, toujours ligne 3, on découvre une nouvelle propriété [classe_id] qui n’existait initialement pas dans
l’entité [Elève]. Cette propriété correspond directement à la colonne [classe_id] de la table [élèves]. Ainsi [sqlalchemy] a jouté
les propriétés suivantes à l’entité [Elève] : [classe_id, _sa_instance_state, notes]. Il faut en avoir conscience, notamment parce
qu’elles ne doivent pas déjà exister dans l’entité mappée.
Les propriétés exclues du dictionnaire des entités sont importantes. Si par exemple, on n’exclut pas les propriétés [notes, élève] de
l’entité [Elève] alors l’opération [print(f"élève={élève}")] va les afficher et va donc, comme il vient d’être expliqué, provoquer
des requêtes SQL implicites (lazy loading) pour récupérer les valeurs de ces propriétés. Si, comme ici, c’est une liste d’élèves qui est
affichée, les opérations SQL implicites sont faites pour chaque élève. Cela peut être d’une part inutile et d’autre part sûrement coûteux
en temps d’exécution.
Pour exécuter le script avec une base PostgreSQL, on crée la configuration d’exécution suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
277/755
19.6.9 Le script [main_stats_for_élève]
Le script [main_stats_for_élève] est celui déjà utilisé dans l’application |troiscouches v01]. Il s’appelait alors [main]. C’est une
application console permettant d’obtenir certains indicateurs sur les notes d’un élève : [moyenne pondérée, min, max, liste]. Il
s’insère dans l’architecture suivante :
Dans cette architecture en couches, seule la couche [dao] a été changée entre l’application |troiscouches v01| et celle-ci. Comme la
nouvelle couche [dao] respecte l’interface [InterfaceDao] de l’ancienne couche [dao], les couches [ui, métier] n’ont pas à être
changées. On peut donc continuer à utiliser celles définies dans l’application |troiscouches v01|.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
278/755
19.
20. # la couche [ui]
21. ui = config["ui"]
22. try:
23. # exécution couche [ui]
24. ui.run()
25. except MyException as ex1:
26. # on affiche l'erreur
27. print(f"L'erreur 1 suivante s'est produite : {ex1}")
28. except BaseException as ex2:
29. # on affiche l'erreur
30. print(f"L'erreur 2 suivante s'est produite : {ex2}")
31. finally:
32. # on libère les ressources
33. import shutdown
34. shutdown.execute(config)
• ligne 20 : on récupère une référence sur la couche [ui] dans la configuration de l’application ;
• ligne 24 : on lance le dialogue avec l’utilisateur à l’aide de l’unique méthode de la couche [ui] ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/databases/sqlalchemy/05/main/main_stats_for_élève.py pgres
2. Numéro de l'élève (>=1 et * pour arrêter) : 11
3. Elève={"prénom": "prénom1", "id": 11, "classe_id": 1, "nom": "nom1"}, notes=[10.0 6.0], max=10.0, min=6.0,
moyenne pondérée=7.33
4. Numéro de l'élève (>=1 et * pour arrêter) : 1
5. L'erreur suivante s'est produite : MyException[11, L'élève d'identifiant 1 n'existe pas]
6. Numéro de l'élève (>=1 et * pour arrêter) : *
7.
8. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
279/755
20 Exercice d'application : version 5
• l’application 1 initialisera la base de données qui va venir remplacer le fichier [admindata.json] de la version 4 ;
• l’application 2 fera le calcul des impôts en mode batch ;
• l’application 3 fera le calcul des impôts en mode interactif ;
C'est une évolution de l'architecture de la version 4 (paragraphe |Version 4|) : les données fiscales seront trouvées dans une base de
données au lieu d'être dans un fichier jSON. La couche [dao] va évoluer pour implémenter ce changement.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
280/755
20.1.1 Le fichier [admindata.json]
1. {
2. "limites": [9964, 27519, 73779, 156244, 0],
3. "coeffr": [0, 0.14, 0.3, 0.41, 0.45],
4. "coeffn": [0, 1394.96, 5798, 13913.69, 20163.45],
5. "plafond_qf_demi_part": 1551,
6. "plafond_revenus_celibataire_pour_reduction": 21037,
7. "plafond_revenus_couple_pour_reduction": 42074,
8. "valeur_reduc_demi_part": 3797,
9. "plafond_decote_celibataire": 1196,
10. "plafond_decote_couple": 1970,
11. "plafond_impot_couple_pour_decote": 2627,
12. "plafond_impot_celibataire_pour_decote": 1595,
13. "abattement_dixpourcent_max": 12502,
14. "abattement_dixpourcent_min": 437
15. }
Nous allons utiliser comme colonnes de la base de données les clés de ce dictionnaire.
De même, comme il a été montré au paragraphe |création d’une base de données PostgreSQL|, nous créons une base de données
PostgreSQL nommée [dbimpots-2019] propriété de l’utilisateur [admimpots] de mot de passe [mdpimpots]. Dans [pgAdmin] cela
donne la chose suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
281/755
Les bases sont créées mais pour l’instant elles n’ont aucune table. Celles-ci vont être construites par l’ORM [sqlalchemy].
Définie par [sqlalchemy] la table [tbtranches] rassemblera les données des tableaux [limites, coeffr, coeffn] du dictionnaire
[admindata.json] :
Définie par [sqlalchemy] la table [tbconstantes] rassemblera les constantes du dictionnaire [admindata.json] :
Les entités qui seront mappées avec ces deux tables seront les suivantes :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
282/755
1. from BaseEntity import BaseEntity
2.
3.
4. # classe conteneur des données de l'administration fiscale
5. class Constantes(BaseEntity):
6. # clés exclues de l'état de la classe
7. excluded_keys = ["_sa_instance_state"]
8.
9. # clés autorisées
10. @staticmethod
11. def get_allowed_keys() -> list:
12. return ["id",
13. "plafond_qf_demi_part",
14. "plafond_revenus_celibataire_pour_reduction",
15. "plafond_revenus_couple_pour_reduction",
16. "valeur_reduc_demi_part",
17. "plafond_decote_celibataire",
18. "plafond_decote_couple",
19. "plafond_decote_couple",
20. "plafond_impot_celibataire_pour_decote",
21. "plafond_impot_couple_pour_decote",
22. "abattement_dixpourcent_max",
23. "abattement_dixpourcent_min"]
L’entité [Tranche] encapsule une ligne des trois tableaux [limites, coeffr, coeffn] du dictionnaire [admindata.json] :
Le mapping entre les entités [Constantes, Tranche] et les tables [constantes, tranches] sera le suivant :
1. …
2. # la table des constantes
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
283/755
3. constantes_table = Table("tbconstantes", metadata,
4. Column('id', Integer, primary_key=True),
5. Column('plafond_qf_demi_part', Float, nullable=False),
6. Column('plafond_revenus_celibataire_pour_reduction', Float, nullable=False),
7. Column('plafond_revenus_couple_pour_reduction', Float, nullable=False),
8. Column('valeur_reduc_demi_part', Float, nullable=False),
9. Column('plafond_decote_celibataire', Float, nullable=False),
10. Column('plafond_decote_couple', Float, nullable=False),
11. Column('plafond_impot_celibataire_pour_decote', Float, nullable=False),
12. Column('plafond_impot_couple_pour_decote', Float, nullable=False),
13. Column('abattement_dixpourcent_max', Float, nullable=False),
14. Column('abattement_dixpourcent_min', Float, nullable=False)
15. )
16.
17. # la table des tranches de l'impôt
18. tranches_table = Table("tbtranches", metadata,
19. Column('id', Integer, primary_key=True),
20. Column('limite', Float, nullable=False),
21. Column('coeffr', Float, nullable=False),
22. Column('coeffn', Float, nullable=False)
23. )
24. # les mappings
25. from Tranche import Tranche
26. mapper(Tranche, tranches_table)
27.
28. from Constantes import Constantes
29. mapper(Constantes, constantes_table)
• les mappings sont faits aux lignes 24-29. On y a omis de faire les correspondances entre propriétés des entités mappées et
tables de la base de données. C’est possible lorsque les noms des colonnes des tables sont les mêmes que ceux des propriétés
auxquelles elles doivent être associées. Pour cette raison, nous avons repris dans les tables les noms des propriétés des entités
mappées. Cela facilite l’écriture du code et sa compréhension ;
Nous venons de détailler une partie de la configuration de [sqlalchemy]. Le fichier [config_database] dans sa totalité est le suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
284/755
19. Column('id', Integer, primary_key=True),
20. Column('plafond_qf_demi_part', Float, nullable=False),
21. Column('plafond_revenus_celibataire_pour_reduction', Float, nullable=False),
22. Column('plafond_revenus_couple_pour_reduction', Float, nullable=False),
23. Column('valeur_reduc_demi_part', Float, nullable=False),
24. Column('plafond_decote_celibataire', Float, nullable=False),
25. Column('plafond_decote_couple', Float, nullable=False),
26. Column('plafond_impot_celibataire_pour_decote', Float, nullable=False),
27. Column('plafond_impot_couple_pour_decote', Float, nullable=False),
28. Column('abattement_dixpourcent_max', Float, nullable=False),
29. Column('abattement_dixpourcent_min', Float, nullable=False)
30. )
31.
32. # la table des tranches de l'impôt
33. tranches_table = Table("tbtranches", metadata,
34. Column('id', Integer, primary_key=True),
35. Column('limite', Float, nullable=False),
36. Column('coeffr', Float, nullable=False),
37. Column('coeffn', Float, nullable=False)
38. )
39. # les mappings
40. from Tranche import Tranche
41. mapper(Tranche, tranches_table)
42.
43. from Constantes import Constantes
44. mapper(Constantes, constantes_table)
45.
46. # la session factory
47. session_factory = sessionmaker()
48. session_factory.configure(bind=engine)
49.
50. # une session
51. session = session_factory()
52.
53. # on enregistre certaines informations
54. config['database'] = {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
55. "constantes_table": constantes_table, "session": session}
56.
57. # résultat
58. return config
• ligne 1 : la fonction [configure] reçoit en paramètre un dictionnaire dont la clé [sgbd] lui dit quel SGBD utiliser : MySQL
(mysql) ou PostgreSQL (pgres) ;
• lignes 6-12 : on sélectionne la base de données demandée par la configuration ;
• lignes 14-44 : mappings entités / tables. Ces mappings sont simples car il n’existe aucun lien entre les tables [tranches] et
[constantes]. Elles sont indépendantes. Il n’y a donc pas de clé étrangère de l’une sur l’autre à gérer ;
• lignes 46-51 : on crée la session [session] de travail de l’application ;
• lignes 53-58 : les informations utiles sont mises dans le dictionnaire de la configuration et celui-ci est retourné ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
285/755
La couche [dao] [1] doit lire le fichier [admindata.json] [2] et transférer son contenu dans une des bases [3, 4] ;
La couche [dao] présente l’interface [1] et est implémentée par la classe [2].
1. # imports
2. from abc import ABC, abstractmethod
3.
4.
5. # interface InterfaceImpôtsUI
6. class InterfaceDao4TransferAdminData2Database(ABC):
7. # transfert des données fiscales dans une base de données
8. @abstractmethod
9. def transfer_admindata_in_database(self:object):
10. pass
• lignes 8-10 : l’interface ne présente qu’une méthode [transfer_admindata_in_database] sans paramètres. Comme cette
méthode a besoin de paramètres (quel fichier ?, quelle base de données ?), cela signifie que ceux-ci seront passés au
constructeur des classes implémentant cette interface ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
286/755
3. import json
4.
5. from sqlalchemy.exc import DatabaseError, IntegrityError, InterfaceError
6.
7. from Constantes import Constantes
8. from ImpôtsError import ImpôtsError
9. from InterfaceDao4TransferAdminData2Database import InterfaceDao4TransferAdminData2Database
10. from Tranche import Tranche
11.
12.
13. class DaoTransferAdminDataFromJsonFile2Database(InterfaceDao4TransferAdminData2Database):
14.
15. # constructeur
16. def __init__(self, config: dict):
17. self.config = config
18.
19. # transfert
20. def transfer_admindata_in_database(self) -> None:
21. # initialisations
22. session = None
23. config = self.config
24.
25. try:
26. # on récupère les données de l'administration fiscale
27. with codecs.open(config["admindataFilename"], "r", "utf8") as fd:
28. # transfert du contenu dans un dictionnaire
29. admindata = json.load(fd)
30.
31. # on récupère la configuration de la base de données
32. database = config["database"]
33.
34. # suppression des deux tables de la base de données
35. # checkfirst=True : vérifie d'abord que la table existe
36. database["tranches_table"].drop(database["engine"], checkfirst=True)
37. database["constantes_table"].drop(database["engine"], checkfirst=True)
38.
39. # recréation des tables à partir des mappings
40. database["metadata"].create_all(database["engine"])
41.
42. # la session [sqlalchemy] courante
43. session = database["session"]
44.
45. # on remplit la table des tranches de l'impôt
46. limites = admindata["limites"]
47. coeffr = admindata["coeffr"]
48. coeffn = admindata["coeffn"]
49. for i in range(len(limites)):
50. session.add(Tranche().fromdict(
51. {"limite": limites[i], "coeffr": coeffr[i], "coeffn": coeffn[i]}))
52. # on remplit la table des constantes
53. session.add(Constantes().fromdict({
54. 'plafond_qf_demi_part': admindata["plafond_qf_demi_part"],
55. 'plafond_revenus_celibataire_pour_reduction':
admindata["plafond_revenus_celibataire_pour_reduction"],
56. 'plafond_revenus_couple_pour_reduction': admindata["plafond_revenus_couple_pour_reduction"],
57. 'valeur_reduc_demi_part': admindata["valeur_reduc_demi_part"],
58. 'plafond_decote_celibataire': admindata["plafond_decote_celibataire"],
59. 'plafond_decote_couple': admindata["plafond_decote_couple"],
60. 'plafond_impot_celibataire_pour_decote': admindata["plafond_impot_celibataire_pour_decote"],
61. 'plafond_impot_couple_pour_decote': admindata["plafond_impot_couple_pour_decote"],
62. 'abattement_dixpourcent_max': admindata["abattement_dixpourcent_max"],
63. 'abattement_dixpourcent_min': admindata["abattement_dixpourcent_min"]
64. }))
65.
66. # validation de la session [sqlalchemy]
67. session.commit()
68. except (IntegrityError, DatabaseError, InterfaceError) as erreur:
69. # on relance l'exception sous une autre forme
70. raise ImpôtsError(17, f"{erreur}")
71. finally:
72. # on libère les ressources de la session
73. if session:
74. session.close()
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
287/755
• lignes 39-40 : recréation des deux tables ;
• ligne 43 : on récupère la session [sqlalchemy] présente dans la configuration ;
• lignes 45-51 : les tableaux [limites, coeffr, coeffn] du dictionnaire [admindata] sont mis dans la session. Pour cela on met
dans la session des instances de l’entité [Tranche] ;
• lignes 52-64 : une instance de l’entité [Constantes] est mise en session ;
• lignes 66-67 : la session est validée. Si les données de la session n’étaient pas encore en base, elles y sont mises à ce moment
là ;
• lignes 68-70 : gestion d’une éventuelle erreur ;
• lignes 71-74 : la session est fermée. On peut le faire car la couche [dao] n’est utilisée qu’une fois ;
• [config] est le fichier de configuration générale. C’est lui qui configure l’application [main]. Il se fait aider par les deux autres
fichiers :
o [config_database] que nous avons étudié et qui configure l’ORM [sqlalchemy] ;
o [config_layers] qui configure les couches de l’application ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
288/755
31. f"{script_dir}/../../entities",
32. ]
33.
34. # on fixe le syspath
35. from myutils import set_syspath
36. set_syspath(absolute_dependencies)
37.
38. # étape 2 ------
39. # on complète la configuration de l'application
40. config.update({
41. # chemins absolus des fichiers de données
42. "admindataFilename": f"{script_dir}/../../data/input/admindata.json"
43. })
44.
45. # étape 3 ------
46. # configuration base de données
47. import config_database
48. config = config_database.configure(config)
49.
50. # étape 4 ------
51. # instanciation des couches de l'application
52. import config_layers
53. config = config_layers.configure(config)
54.
55. # on rend la config
56. return config
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
289/755
Le script principal [main] est le suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
290/755
Après exécution avec la base MySQL, celle-ci contient les éléments suivants (phpMyAdmin) :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
291/755
Colonne [3], on voit les valeurs attribuées par MySQL à la clé primaire [id]. La numérotation démarre à 1. La copie d’écran ci-dessus
a été obtenue après plusieurs exécutions du script.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
292/755
Avec la base PostgreSQL les résultats sont les suivants :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
293/755
20.2 Application 2 : calcul de l’impôt en mode batch
20.2.1 Architecture
L’application de calcul de l’impôt de la version 4 utilisait l'architecture suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
294/755
La couche [dao] implémente une interface [InterfaceImpôtsDao]. Nous avons construit une classe implémentant cette interface :
• [ImpôtsDaoWithAdminDataInJsonFile] qui allait chercher les données fiscales dans un fichier jSON. C’était la version 3 ;
Nous allons implémenter l’interface [InterfaceImpôtsDao] par une nouvelle classe [ImpotsDaoWithTaxAdminDataInDatabase] qui ira
chercher les données de l’administration fiscale dans une base de données. La couche [dao], comme précédemment, écrira les résultats
dans un fichier jSON et trouvera les données des contribuables dans un fichier texte. Nous savons que si nous continuons à respecter
l’interface [InterfaceImpôtsDao], la couche [métier] n’aura pas à être modifiée.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
295/755
20.2.2 Configuration de l’application
Le fichier de configuration [config_database] reste ce qu’il était dans l’application 1. La configuration [config] intègre des éléments
nouveaux :
1. # étape 2 ------
2. # on complète la configuration de l'application
3. config.update({
4. # chemins absolus des fichiers de données
5. "admindataFilename": f"{script_dir}/../../data/input/admindata.json",
6. "taxpayersFilename": f"{script_dir}/../../data/input/taxpayersdata.txt",
7. "errorsFilename": f"{script_dir}/../../data/output/errors.txt",
8. "resultsFilename": f"{script_dir}/../../data/output/résultats.json"
9. })
• lignes 6-8 : les chemins absolus des fichiers texte utilisés par l’application 2 ;
• lignes 3-4 : la couche [dao] est désormais implémentée par la classe [ImpotsDaoWithAdminDataInDatabase]. Cette classe est
nouvelle mais implémente la même interface [InterfaceDao] que la version 4 de l’exercice d’application ;
• lignes 7-8 : la couche [métier] est implémentée par la classe [ImpôtsMétier]. C’est la classe utilisée dans la version 4 de
l’exercice d’application ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
296/755
20.2.3 La couche [dao]
1. # imports
2. from sqlalchemy.exc import DatabaseError, IntegrityError, InterfaceError
3.
4. from AbstractImpôtsDao import AbstractImpôtsDao
5. from AdminData import AdminData
6. from Constantes import Constantes
7. from ImpôtsError import ImpôtsError
8. from Tranche import Tranche
9.
10.
11. class ImpotsDaoWithAdminDataInDatabase(AbstractImpôtsDao):
12. # constructeur
13. def __init__(self, config: dict):
14. # config["taxPayersFilename"] : le nom du fichier texte des contribuables
15. # config["taxPayersResultsFilename"] : le nom du fichier jSON des résultats
16. # config["errorsFilename"] : enregistre les erreurs trouvées dans taxPayersFilename
17. # config["database"] : configuration de la base de données
18.
19. # initialisation de la classe Parent
20. AbstractImpôtsDao.__init__(self, config)
21. # mémorisation paramètre
22. self.__config = config
23. # admindata
24. self.__admindata = None
25.
26. # implémentation de l'interface
27. def get_admindata(self):
28. # admindata a-t-il été mémorisé ?
29. if self.__admindata:
30. return self.__admindata
31. # on fait une requête en BD
32. session = None
33. config = self.__config
34. try:
35. # une session
36. database_config = config["database"]
37. session = database_config["session"]
38.
39. # on lit la table des tranches de l'impôt
40. tranches = session.query(Tranche).all()
41.
42. # on lit la table des constantes (1 seule ligne)
43. constantes = session.query(Constantes).first()
44.
45. # on crée l'instance admindata
46. admindata = AdminData()
47. # on y crée les tableaux limtes, coeffR, coeffN
48. limites = admindata.limites = []
49. coeffr = admindata.coeffr = []
50. coeffn = admindata.coeffn = []
51. for tranche in tranches:
52. limites.append(float(tranche.limite))
53. coeffr.append(float(tranche.coeffr))
54. coeffn.append(float(tranche.coeffn))
55. # on y rajoute les constantes
56. admindata.fromdict(constantes.asdict())
57. # on mémorise admindata
58. self.__admindata = admindata
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
297/755
59. # on rend la valeur
60. return self.__admindata
61. except (IntegrityError, DatabaseError, InterfaceError) as erreur:
62. # on relance l'exception sous une autre forme
63. raise ImpôtsError(27, f"{erreur}")
64. finally:
65. # on ferme la session
66. if session:
67. session.close()
Notes
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
298/755
Le test [TestDaoMétier] est le suivant :
1. import unittest
2.
3.
4. class TestDaoMétier(unittest.TestCase):
5.
6. def test_1(self) -> None:
7. from TaxPayer import TaxPayer
8.
9. # {'marié': 'oui', 'enfants': 2, 'salaire': 55555,
10. # 'impôt': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
11. taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
12. métier.calculate_tax(taxpayer, admindata)
13. # vérification
14. self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
15. self.assertEqual(taxpayer.décôte, 0)
16. self.assertEqual(taxpayer.réduction, 0)
17. self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
18. self.assertEqual(taxpayer.surcôte, 0)
19.
20. …
21.
22. def test_11(self) -> None:
23. from TaxPayer import TaxPayer
24.
25. # {'marié': 'oui', 'enfants': 3, 'salaire': 200000,
26. # 'impôt': 42842, 'surcôte': 17283, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
27. taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
28. métier.calculate_tax(taxpayer, admindata)
29. # vérifications
30. self.assertAlmostEqual(taxpayer.impôt, 42842, 1)
31. self.assertEqual(taxpayer.décôte, 0)
32. self.assertEqual(taxpayer.réduction, 0)
33. self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
34. self.assertAlmostEqual(taxpayer.surcôte, 17283, delta=1)
35.
36.
37. if __name__ == '__main__':
38. # on attend un paramètre mysql ou pgres
39. import sys
40. syntaxe = f"{sys.argv[0]} mysql / pgres"
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
299/755
41. erreur = len(sys.argv) != 2
42. if not erreur:
43. sgbd = sys.argv[1].lower()
44. erreur = sgbd != "mysql" and sgbd != "pgres"
45. if erreur:
46. print(f"syntaxe : {syntaxe}")
47. sys.exit()
48.
49. # on configure l'application
50. import config
51. config = config.configure({'sgbd': sgbd})
52. # couche métier
53. métier = config['métier']
54. try:
55. # admindata
56. admindata = config['dao'].get_admindata()
57. except BaseException as ex:
58. # affichage
59. print((f"L'erreur suivante s'est produite : {ex}"))
60. # fin
61. sys.exit()
62. # on enève le paramètre reçu par le script
63. sys.argv.pop()
64. # on exécute les méthodes de test
65. print("tests en cours...")
66. unittest.main()
• nous ne revenons pas sur les 11 tests décrits au paragraphe |test couche [métier] version 4| ;
• lignes 37-66 : nous allons exécuter le script des tests comme une application normale et non pas comme un test UnitTest.
C’est la ligne66qui fera intervenir le framework UnitTest. Dans les tests précédents, nous utilisions la méthode [setUp] pour
configurer l’exécution de chaque test. On refaisait 11 fois la même configuration puisque la fonction [setUp] est exécutée
avant chaque test. Ici, nous faisons la configuration 1 fois. Elle consiste à définir des variables globales [métier] ligne 53,
[admindata] ligne 56 qui seront ensuite utilisées par les méthodes de [TestDaoMétier], ligne 12 par exemple ;
• lignes 39-47 : le script de test attend un paramètre [mysql / pgres] qui indique si on utilise une base MySQL ou PostgreSQL ;
• lignes 50-51 : le test est configuré ;
• ligne 53 : on récupère la couche [métier] dans la configuration ;
• ligne 56 : on fait de même avec la couche [dao]. On récupère alors l’instance [admindata] qui encapsule les données
nécessaires au calcul de l’impôt ;
• les tests ont montré que la méthode [unittest.main()] de la ligne 66 n’ignorait pas le paramètre [mysql / pgres] reçu par
le script mais lui donnait une signification autre. La ligne 63 fait en sorte que cette méthode n’ait plus aucun paramètre ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
300/755
Nous créons deux configurations d’exécution :
Si nous exécutons l’une de ces deux configurations, nous obtenons les résultats suivants :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/impots/v05/tests/TestDaoMétier.py mysql
2. tests en cours...
3. ...........
4. ----------------------------------------------------------------------
5. Ran 11 tests in 0.001s
6.
7. OK
8.
9. Process finished with exit code 0
Rappelons que ces tests ne vérifient que 11 cas du calcul de l’impôt. Leur réussite peut néanmoins suffire pour nous donner confiance
dans la couche [dao].
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
301/755
20.2.5 Le script principal
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
302/755
26. # lecture des données des contribuables
27. taxpayers = dao.get_taxpayers_data()["taxpayers"]
28. # des contribuables ?
29. if not taxpayers:
30. raise ImpôtsError(57, f"Pas de contribuables valides dans le fichier {config ['taxpayersFilename']}")
31. # calcul de l'impôt des contribuables
32. for taxPayer in taxpayers:
33. # taxPayer est à la fois un paramètre d'entrée et de sortei
34. # taxPayer va être modifié
35. métier.calculate_tax(taxPayer, admindata)
36. # écriture des résultats dans un fichier texte
37. dao.write_taxpayers_results(taxpayers)
38. except ImpôtsError as erreur:
39. # affichage de l'erreur
40. print(f"L'erreur suivante s'est produite : {erreur}")
41. finally:
42. # terminé
43. print("Travail terminé...")
Notes
• lignes 1-10 : on récupère le paramètre [mysql / pgres] qui indique le SGBD à utiliser ;
• lignes 12-14 : l’application est configurée ;
• lignes 16-17 : la classe [ImpôtsError] est importée. On en a besoin ligne 38 ;
• lignes 19-21 : on récupère des références sur les couches de l’application ;
• ligne 25 : on demande à la couche [dao] les données de l’administration fiscale. La couche [métier] en a besoin pour le calcul
de l’impôt ;
• ligne 27 : on récupère dans une liste, les données (id, marié, enfants, salaire) des contribuables ;
• lignes 29-30 : si cette liste est vide, on lance une exception ;
• lignes 32-35 : calcul de l'impôt des éléments de la liste [taxpayers] ;
• ligne 37 : écriture des résultats dans le fichier jSON[résultats.json] ;
• lignes 38-40 : gestion de l'éventuelle erreur ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
303/755
20.3 Application 3 : calcul de l’impôt en mode interactif
Nous introduisons maintenant l’application permettant de calculer l’impôt de façon interactive. C’est un portage de l’application 2 de
la version 4.
• le script [main] lance le dialogue avec l’utilisateur avec la méthode [ui.run] de la couche [ui] ;
• la couche [ui] :
o utilise la couche [dao] pour obtenir les données permettant de faire le calcul de l’impôt ;
o demande à l’utilisateur les renseignements concernant le contribuable dont on veut calculer l’impôt ;
o utilise la couche [métier] pour faire ce calcul ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
304/755
11. from ImpôtsConsole import ImpôtsConsole
12. config['ui'] = ImpôtsConsole(config)
13.
14. # on rend la config
15. return config
La classe [ImpôtsConsole], lignes 11-12, est la même que dans la |version 4|.
• lignes 1-10, le script attend un paramètre [mysql / pgres] qui indique le SGBD à utiliser ;
• lignes 12-14 : l’application est configurée ;
• lignes 19-20 : on récupère la couche [ui] dans la configuration ;
• ligne 25 : on l’exécute ;
Les résultats sont identiques à ceux de la |version 4|. Il ne pouvait en être autrement puisque toutes les interfaces de la version 4 ont
été respectées dans la version 5.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
305/755
21 Fonctions internet
Nous abordons maintenant les fonctions internet de Python qui nous permettent de faire de la programmation TCP / IP (Transfer
Control Protocol / Internet Protocol).
Lorsque une application AppA d'une machine A veut communiquer avec une application AppB d'une machine B de l'Internet, elle
doit connaître plusieurs choses :
Aussi les deux applications communicantes doivent-elles être d'accord sur le type de dialogue qu'elles vont adopter. Par exemple,
le dialogue avec un service ftp n'est pas le même qu'avec un service pop : ces deux services n'acceptent pas les mêmes commandes.
Elles ont un protocole de dialogue différent ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
306/755
• le processus qui souhaite émettre établit tout d'abord une connexion avec le processus destinataire des informations qu'il va
émettre. Cette connexion se fait entre un port de la machine émettrice et un port de la machine réceptrice. Il y a entre les deux
ports un chemin virtuel qui est ainsi créé et qui sera réservé aux deux seuls processus ayant réalisé la connexion ;
• tous les paquets émis par le processus source suivent ce chemin virtuel et arrivent dans l'ordre où ils ont été émis ;
• l'information émise a un aspect continu. Le processus émetteur envoie des informations à son rythme. Celles-ci ne sont pas
nécessairement envoyées tout de suite : le protocole TCP attend d'en avoir assez pour les envoyer. Elles sont stockées dans
une structure appelée segment TCP. Ce segment une fois rempli sera transmis à la couche IP où il sera encapsulé dans un paquet
IP ;
• chaque segment envoyé par le protocole TCP est numéroté. Le protocole TCP destinataire vérifie qu'il reçoit bien les segments
en séquence. Pour chaque segment correctement reçu, il envoie un accusé de réception à l'expéditeur ;
• lorsque ce dernier le reçoit, il l'indique au processus émetteur. Celui-ci peut donc savoir qu'un segment est arrivé à bon port ;
• si au bout d'un certain temps, le protocole TCP ayant émis un segment ne reçoit pas d'accusé de réception, il retransmet le
segment en question, garantissant ainsi la qualité du service d'acheminement de l'information ;
• le circuit virtuel établi entre les deux processus qui communiquent est full-duplex : cela signifie que l'information peut transiter
dans les deux sens. Ainsi le processus destination peut envoyer des accusés de réception alors même que le processus source
continue d'envoyer des informations. Cela permet par exemple au protocole TCP source d'envoyer plusieurs segments sans
attendre d'accusé de réception. S'il réalise au bout d'un certain temps qu'il n'a pas reçu l'accusé de réception d'un certain
segment n° n, il reprendra l'émission des segments à ce point ;
Le programme serveur traite différemment la demande de connexion initiale d'un client de ses demandes ultérieures visant à obtenir
un service. Le programme n'assure pas le service lui-même. S'il le faisait, pendant la durée du service il ne serait plus à l'écoute des
demandes de connexion et des clients ne seraient alors pas servis. Il procède autrement : dès qu'une demande de connexion est reçue
sur le port d'écoute puis acceptée, le serveur crée une tâche chargée de rendre le service demandé par le client. Ce service est rendu
sur un autre port de la machine serveur appelé port de service. On peut ainsi servir plusieurs clients en même temps.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
307/755
21.2 Découvrir les protocoles de communication de l'internet
21.2.1 Introduction
Lorsqu'un client s'est connecté à un serveur, s'établit ensuite un dialogue entre-eux. La nature de celui-ci forme ce qu'on appelle le
protocole de communication du serveur. Parmi les protocoles les plus courants de l'internet on trouve les suivants :
• HTTP : HyperText Transfer Protocol - le protocole de dialogue avec un serveur web (serveur HTTP) ;
• SMTP : Simple Mail Transfer Protocol - le protocole de dialogue avec un serveur d'envoi de courriers électroniques (serveur
SMTP) ;
• POP : Post Office Protocol - le protocole de dialogue avec un serveur de stockage du courrier électronique (serveur POP). Il
s'agit là de récupérer les courriers électroniques reçus et non d'en envoyer ;
• IMAP : Internet Message Access Protocol - le protocole de dialogue avec un serveur de stockage du courrier électronique
(serveur IMAP). Ce protocole a remplacé progressivement le protocole POP plus ancien ;
• FTP : File Transfer Protocol - le protocole de dialogue avec un serveur de stockage de fichiers (serveur FTP) ;
Tous ces protocoles ont la particularité d'être des protocoles à lignes de texte : le client et le serveur s'échangent des lignes de texte.
Si on a un client capable de :
alors on est capable de dialoguer avec un serveur TCP ayant un protocole à lignes de texte pour peu qu'on connaisse les règles de ce
protocole.
Dans les codes associés à ce document, on trouve deux utilitaires de communication TCP :
Ce sont deux programmes C# dont les codes sources vous sont donnés. Vous pouvez donc les modifier.
Le serveur TCP [RawTcpServer] s’appelle avec la syntaxe [RawTcpServeur port] pour créer un service TCP sur le port [port] de la
machine locale (l’ordinateur sur lequel vous travaillez) :
• le serveur peut servir plusieurs clients simultanément ;
• le serveur exécute les commandes tapées par l’utilisateur tapées au clavier. Celles-ci sont les suivantes :
o list : liste les clients actuellement connectés au serveur. Ceux-ci sont affichés sous la forme [id=x-nom=y]. Le champ [id]
sert à identifier les clients ;
o send x [texte] : envoie texte au client n° x (id=x). Les crochets [] ne sont pas envoyés. Ils sont nécessaires dans la
commande. Ils servent à délimiter visuellement le texte envoyé au client ;
o close x : ferme la connexion avec le client n° x ;
o quit : ferme toutes les connexions et arrête le service ;
• les lignes envoyées par le client au serveur sont affichées sur la console ;
• l’ensemble des échanges est logué dans un fichier texte portant le nom [machine-port.txt] où
o [machine] est le nom de la machine sur laquelle s’exécute le code ;
o [port] est le port de service qui répond aux demandes du client ;
Le client TCP [RawTcpClient] s’appelle avec la syntaxe [RawTcpClient serveur port] pour se connecter au port [port] du serveur
[serveur] :
• les lignes tapées par l’utilisateur au clavier sont envoyées au serveur ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
308/755
• les lignes envoyées par le serveur sont affichées sur la console ;
• l’ensemble des échanges est logué dans un fichier texte portant le nom [serveur-port.txt] ;
Voyons un exemple. On ouvre deux fenêtres terminal PyCharm et on se positionne dans chacune d’elles sur le dossier des utilitaires :
Dans l’une des fenêtres on lance le serveur [RawTcpServer] sur le port 100 :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
309/755
• ligne 5, un client a été détecté. Le serveur lui a donné le n° 1. Le serveur a correctement identifié le client distant (machine et
port) ;
• ligne 6, le serveur se remet en attente d’un nouveau client ;
• ligne 8, la réponse envoyée au client 1. Seul le texte entre les crochets est envoyé, pas les crochets eux-mêmes ;
• ligne 5, la réponse reçue par le client. Le texte reçu est celui entre crochets ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
310/755
• ligne 12, la confirmation du serveur ;
• ligne 13, nous arrêtons le serveur ;
• ligne 14, la confirmation du serveur ;
Deux fichiers de logs ont été créés, un pour le serveur, un autre pour le client :
• en [1], les logs du serveur : le nom du fichier est le nom du client sous la forme [machine-port]. Cela permet d’avoir des
fichiers de logs différents pour des clients différents ;
• en [2], les logs du client : le nom du fichier est le nom du serveur sous la forme [machine-port] ;
Les machines de l’internet sont identifiées par une adresse IP (IPv4 ou IPv6) et le plus souvent par un nom. Mais finalement seule
l’adresse IP est utilisée par les protocoles de communication de l’internet. Il faut donc connaître l’adresse IP d’une machine identifiée
par son nom.
1. # imports
2. import socket
3.
4.
5. # ------------------------------------------------
6. def get_ip_and_name(nom_machine: str):
7. # nom_machine : nom de la machine dont on veut l'adresse IP
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
311/755
8. try:
9. # nom_machine-->adresse IP
10. ip = socket.gethostbyname(nom_machine)
11. print(f"ip[{nom_machine}]={ip}")
12. except socket.error as erreur:
13. # on affiche l'erreur
14. print(f"ip[{nom_machine}]={erreur}")
15. return
16.
17. try:
18. # adresse IP --> nom_machine
19. names = socket.gethostbyaddr(ip)
20. print(f"names[{ip}]={names}")
21. except socket.error as erreur:
22. # on affiche l'erreur
23. print(f"names[{ip}]={erreur}")
24. return
25.
26.
27. # ---------------------------------------- main
28.
29. # les machines internet
30. hosts = ["istia.univ-angers.fr", "www.univ-angers.fr", "sergetahe.com", "localhost", "xx"]
31.
32. # adresses IP des machines de HOTES
33. for host in hosts:
34. print("-------------------------------------")
35. get_ip_and_name(host)
36. # fin
37. print("Terminé...")
Commentaires
• ligne 2 : le module [socket] fournit les fonctions nécessaires à la gestion des sockets internet. [socket] signifie prise électrique,
prise de réseau ;
• ligne 6 : la fonction [get_ip_and_name] permet à partir du nom internet d'une machine d'obtenir :
o l'adresse IP de la machine ;
o le nom de la machine obtenu à partir de l'adresse IP précédente ;
• ligne 10 : la fonction [socket.gethostbyname] permet d'obtenir l'adresse IP d'une machine à partir d'un de ces noms (une
machine internet peut avoir un nom principal et des alias) ;
• ligne 12 : les fonctions sur les sockets lancent l'exception [socket.error] dès qu'une erreur survient ;
• ligne 19 : la fonction [socket.gethostbyaddr] permet d'obtenir le nom d'une machine à partir de son adresse IP. On va voir
qu'on peut obtenir un nom différent de celui passé ligne 6 ;
• ligne 30 : une liste de noms de machines. Le dernier nom est erroné. Le nom [localhost] désigne la machine sur laquelle vous
travaillez et qui exécute le script ;
• ligne 33-35 : on affiche les IP de ces machines ;
Résultats :
3. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/inet/ip/ip_01.py
4. -------------------------------------
5. ip[istia.univ-angers.fr]=193.49.144.41
6. names[193.49.144.41]=('ametys-fo-2.univ-angers.fr', [], ['193.49.144.41'])
7. -------------------------------------
8. ip[www.univ-angers.fr]=193.49.144.41
9. names[193.49.144.41]=('ametys-fo-2.univ-angers.fr', [], ['193.49.144.41'])
10. -------------------------------------
11. ip[sergetahe.com]=87.98.154.146
12. names[87.98.154.146]=('cluster026.hosting.ovh.net', [], ['87.98.154.146'])
13. -------------------------------------
14. ip[localhost]=127.0.0.1
15. names[127.0.0.1]=('DESKTOP-30FF5FB', [], ['127.0.0.1'])
16. -------------------------------------
17. ip[xx]=[Errno 11001] getaddrinfo failed
18. Terminé...
19.
20. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
312/755
21.4 Le protocole HTTP (HyperText Transfer Protocol)
21.4.1 Exemple 1
Lorsqu’un navigateur affiche une URL, il est le client d’un serveur web ou dit autrement d’un serveur HTTP. C’est lui qui prend
l’initiative et il commence par envoyer un certain nombre de commandes au serveur. Pour ce premier exemple :
Puis avec un navigateur, nous demandons l’URL [http://localhost:100], ç-a-d que nous disons que le serveur HTTP interrogé
travaille sur le port 100 de la machine locale :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
313/755
24. server : Attente d'un client...
1. client 1 : []
2. server : Client 3-DESKTOP-30FF5FB-51441 connecté...
3. server : Attente d'un client...
4. quit
5. server : fin du service
21.4.2 Exemple 2
Maintenant que nous connaissons les commandes envoyées par un navigateur pour réclamer une URL, nous allons réclamer cette
URL avec notre client TCP [RawTcpClient]. Le serveur Apache de Laragon (paragraphe |Installation de Laragon|) sera notre serveur
web.
Maintenant avec un navigateur, demandons l’URL [http://localhost:80]. Ici nous ne précisons que le serveur [localhost:80] et
pas d’URL de document. Dans ce cas c’est l’URL / qui est demandée, ç-à-d la racine du serveur web :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
314/755
• en [1], l’URL demandée. On a tapé initialement [http://localhost:80] et le navigateur (Firefox ici) l’a transformée
simplement en [localhost] car le protocole [http] est implicite lorsqu’aucun protocole n’est mentionné et le port [80] est
implicite lorsque le port n’est pas précisé ;
• en [2], la page racine / du serveur web interrogé ;
• on clique droit sur la page reçue et on choisit l’option [2]. On obtient le code source suivant :
1. <!DOCTYPE html>
2. <html>
3. <head>
4. <title>Laragon</title>
5.
6. <link href="https://fonts.googleapis.com/css?family=Karla:400" rel="stylesheet" type="text/css">
7.
8. <style>
9. html, body {
10. height: 100%;
11. }
12.
13. body {
14. margin: 0;
15. padding: 0;
16. width: 100%;
17. display: table;
18. font-weight: 100;
19. font-family: 'Karla';
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
315/755
20. }
21.
22. .container {
23. text-align: center;
24. display: table-cell;
25. vertical-align: middle;
26. }
27.
28. .content {
29. text-align: center;
30. display: inline-block;
31. }
32.
33. .title {
34. font-size: 96px;
35. }
36.
37. .opt {
38. margin-top: 30px;
39. }
40.
41. .opt a {
42. text-decoration: none;
43. font-size: 150%;
44. }
45.
46. a:hover {
47. color: red;
48. }
49. </style>
50. </head>
51. <body>
52. <div class="container">
53. <div class="content">
54. <div class="title" title="Laragon">Laragon</div>
55.
56. <div class="info">
57. <br />
58. Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19<br />
59. PHP version: 7.2.19 <span><a title="phpinfo()" href="/?q=info">info</a></span><br />
60. Document Root: C:/MyPrograms/laragon/www<br />
61.
62. </div>
63. <div class="opt">
64. <div><a title="Getting Started" href="https://laragon.org/docs">Getting Started</a></div>
65. </div>
66. </div>
67.
68. </div>
69. </body>
70. </html>
• ligne 1, nous nous connectons au port 80 du serveur localhost. C’est là qu’opère le serveur web de Laragon ;
Nous tapons maintenant les commandes que nous avons découvertes dans le paragraphe précédent :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
316/755
14. <-- [<!DOCTYPE html>]
15. <-- [<html>]
16. <-- [ <head>]
17. <-- [ <title>Laragon</title>]
18. <-- []
19. <-- [ <link href="https://fonts.googleapis.com/css?family=Karla:400" rel="stylesheet"
type="text/css">]
20. <-- []
21. <-- [ <style>]
22. <-- [ html, body {]
23. <-- [ height: 100%;]
24. <-- [ }]
25. <-- []
26. <-- [ body {]
27. <-- [ margin: 0;]
28. <-- [ padding: 0;]
29. <-- [ width: 100%;]
30. <-- [ display: table;]
31. <-- [ font-weight: 100;]
32. <-- [ font-family: 'Karla';]
33. <-- [ }]
34. <-- []
35. <-- [ .container {]
36. <-- [ text-align: center;]
37. <-- [ display: table-cell;]
38. <-- [ vertical-align: middle;]
39. <-- [ }]
40. <-- []
41. <-- [ .content {]
42. <-- [ text-align: center;]
43. <-- [ display: inline-block;]
44. <-- [ }]
45. <-- []
46. <-- [ .title {]
47. <-- [ font-size: 96px;]
48. <-- [ }]
49. <-- []
50. <-- [ .opt {]
51. <-- [ margin-top: 30px;]
52. <-- [ }]
53. <-- []
54. <-- [ .opt a {]
55. <-- [ text-decoration: none;]
56. <-- [ font-size: 150%;]
57. <-- [ }]
58. <-- [ ]
59. <-- [ a:hover {]
60. <-- [ color: red;]
61. <-- [ }]
62. <-- [ </style>]
63. <-- [ </head>]
64. <-- [ <body>]
65. <-- [ <div class="container">]
66. <-- [ <div class="content">]
67. <-- [ <div class="title" title="Laragon">Laragon</div>]
68. <-- [ ]
69. <-- [ <div class="info"><br />]
70. <-- [ Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19<br />]
71. <-- [ PHP version: 7.2.19 <span><a title="phpinfo()"
href="/?q=info">info</a></span><br />]
72. <-- [ Document Root: C:/MyPrograms/laragon/www<br />]
73. <-- []
74. <-- [ </div>]
75. <-- [ <div class="opt">]
76. <-- [ <div><a title="Getting Started" href="https://laragon.org/docs">Getting
Started</a></div>]
77. <-- [ </div>]
78. <-- [ </div>]
79. <-- []
80. <-- [ </div>]
81. <-- [ </body>]
82. <-- [</html>]
83. Perte de la connexion avec le serveur...
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
317/755
• ce sont les deux seules commandes indispensables. Pour les autres commandes, le serveur web prendra des valeurs par défaut ;
• ligne 6, la ligne vide qui doit terminer les commandes du client ;
• dessous la ligne 6, vient la réponse du serveur web ;
• lignes 7-12 : les entêtes HTTP de la réponse du serveur ;
• ligne 13 : la ligne vide qui signale la fin des entêtes http ;
• lignes 14-82, le document HTML demandé ligne 4 ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
318/755
47. <-- [ .opt {]
48. <-- [ margin-top: 30px;]
49. <-- [ }]
50. <-- []
51. <-- [ .opt a {]
52. <-- [ text-decoration: none;]
53. <-- [ font-size: 150%;]
54. <-- [ }]
55. <-- [ ]
56. <-- [ a:hover {]
57. <-- [ color: red;]
58. <-- [ }]
59. <-- [ </style>]
60. <-- [ </head>]
61. <-- [ <body>]
62. <-- [ <div class="container">]
63. <-- [ <div class="content">]
64. <-- [ <div class="title" title="Laragon">Laragon</div>]
65. <-- [ ]
66. <-- [ <div class="info"><br />]
67. <-- [ Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19<br />]
68. <-- [ PHP version: 7.2.19 <span><a title="phpinfo()"
href="/?q=info">info</a></span><br />]
69. <-- [ Document Root: C:/MyPrograms/laragon/www<br />]
70. <-- []
71. <-- [ </div>]
72. <-- [ <div class="opt">]
73. <-- [ <div><a title="Getting Started" href="https://laragon.org/docs">Getting
Started</a></div>]
74. <-- [ </div>]
75. <-- [ </div>]
76. <-- []
77. <-- [ </div>]
78. <-- [ </body>]
79. <-- [</html>]
• lignes 11-79 : le document HTML reçu. Dans l’exemple précédent, Firefox avait reçu le même ;
Nous avons désormais les bases pour programmer un client TCP qui demanderait une URL.
21.4.3 Exemple 3
Le script [http/01/main.py] est un client HTTP configuré par le fichier [config.py]. Le contenu de celui-ci est le suivant :
1. def configure():
2. # URLs à interroger
3. urls = [
4. # site : nom du site auquel se connecter
5. # port : port du service web
6. # GET : URL demandée
7. # headers : entêtes HTTP à envoyer dans la requête
8. # endOfLine : marque de fin de ligne dans les entêtes HTTP envoyés
9. # encoding : encodage de la réponse du serveur
10. # timeout : temps d'attente maximum d'une réponse du serveur
11. {
12. "site": "localhost",
13. "port": 80,
14. "GET": "/",
15. "headers": {
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
319/755
16. "Host": "localhost:80",
17. "User-Agent": "client Python",
18. "Accept": "text/HTML",
19. "Accept-Language": "fr"
20. },
21. "endOfLine": "\r\n",
22. "encoding": "utf-8",
23. "timeout": 0.5
24. },
25. {
26. "site": "sergetahe.com",
27. "port": 80,
28. "GET": "/",
29. "headers": {
30. "Host": "sergetahe.com:80",
31. "User-Agent": "client Python",
32. "Accept": "text/HTML",
33. "Accept-Language": "fr"
34. },
35. "endOfLine": "\r\n",
36. "encoding": "utf-8",
37. "timeout": 5
38. },
39. {
40. "site": "tahe.developpez.com",
41. "port": 443,
42. "GET": "/",
43. "headers": {
44. "Host": "tahe.developpez.com:443",
45. "User-Agent": "client Python",
46. "Accept": "text/HTML",
47. "Accept-Language": "fr"
48. },
49. "endOfLine": "\r\n",
50. "encoding": "utf-8",
51. "timeout": 2
52. },
53. {
54. "site": "www.sergetahe.com",
55. "port": 80,
56. "GET": "/cours-tutoriels-de-programmation/",
57. "headers": {
58. "Host": "sergetahe.com:80",
59. "User-Agent": "client Python",
60. "Accept": "text/HTML",
61. "Accept-Language": "fr"
62. },
63. "endOfLine": "\r\n",
64. "encoding": "utf-8",
65. "timeout": 5
66. }
67. ]
68. # on rend la configuration
69. return {
70. "urls": urls
71. }
• le contenu du fichier est une liste d’URL, chaque élément de la liste étant un dictionnaire. Ce dictionnaire indique comment
se connecter au sité désigné par la clé [site] ;
• lignes 4-10 : la signification des clés de chaque dictionnaire ;
1. # imports
2. import codecs
3. import socket
4.
5.
6. # -----------------------------------------------------------------------
7. def get_url(url: dict, suivi: bool = True):
8. # lit l'URL url["GET"] du site url[site] et la stocke dans le fichier url[site].html
9. # le dialogue client /serveur se fait selon le protocole HTTP indiqué dans le dictionnaire [url]
10. # on laisse remonter les exceptions
11.
12. sock = None
13. html = None
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
320/755
14. try:
15. # connexion à [site] sur le port 80 avec un timeout
16. site = url['site']
17. sock = socket.create_connection((site, int(url['port'])), float(url['timeout']))
18.
19. # connexion représente un flux de communication bidirectionnel
20. # entre le client (ce programme) et le serveur web contacté
21. # ce canal est utilisé pour les échanges de commandes et d'informations
22. # le protocole de dialogue est HTTP
23.
24. # création du fichier site.html - on change les caractères gênants pour un nom de fichier
25. site2 = site.replace("/", "_")
26. site2 = site2.replace(".", "_")
27. html_filename = f'{site2}.html'
28. html = codecs.open(f"output/{html_filename}", "w", "utf-8")
29.
30. # le client va commencer le dialogue HTTP avec le serveur
31. if suivi:
32. print(f"Client : début de la communication avec le serveur [{site}]")
33.
34. # selon les serveurs, les lignes du client doivent se terminer par \n ou \r\n
35. end_of_line = url["endOfLine"]
36. # le client envoie la commande GET pour demander l'URL config["GET"]
37. # syntaxe GET URL HTTP/1.1
38. commande = f"GET {url['GET']} HTTP/1.1{end_of_line}"
39. # suivi ?
40. if suivi:
41. print(f"--> {commande}", end='')
42. # on envoie la commande au serveur
43. sock.send(bytearray(commande, 'utf-8'))
44. # émission des entêtes HTTP
45. for verb, value in url['headers'].items():
46. # on construit la commande à envoyer
47. commande = f"{verb}: {value}{end_of_line}"
48. # suivi ?
49. if suivi:
50. print(f"--> {commande}", end='')
51. # on envoie la commande au serveur
52. sock.send(bytearray(commande, 'utf-8'))
53. # on envoie l'entête HTTP [Connection: close] pour demander au serveur web
54. # de fermer la connexion lorsqu'il aura envoyé le document demandé
55. sock.send(bytearray(f"Connection: close{end_of_line}", 'utf-8'))
56. # les entêtes (headers) du protocole HTTP doivent se terminer par une ligne vide
57. sock.send(bytearray(end_of_line, 'utf-8'))
58. #
59. # le serveur va maintenant répondre sur le canal sock. Il va envoyer toutes
60. # ses données puis fermer le canal. Le client lit donc tout ce qui arrive de sock
61. # jusqu'à la fermeture du canal
62. #
63. # on lit tout d'abord les entêtes HTTP envoyés par le serveur
64. # ils se terminent eux-aussi par une ligne vide
65. if suivi:
66. print(f"Réponse du serveur [{site}]")
67.
68. # lecture de la socket comme si elle était un fichier texte
69. encoding = f"{url['encoding']}" if url['encoding'] else None
70. if encoding:
71. file = sock.makefile(encoding=encoding)
72. else:
73. file = sock.makefile()
74. # on exploite ce fichier ligne par ligne
75. fini = False
76. while not fini:
77. # lecture ligne courante
78. ligne = file.readline().strip()
79. # a-t-on une ligne non vide ?
80. if ligne:
81. if suivi:
82. # on affiche l'entête HTTP
83. print(f"<-- {ligne}")
84. else:
85. # c'était la ligne vide - les entêtes HTTP sont terminés
86. fini = True
87. # on lit le document HTML qui va suivre la ligne vide
88. # lecture ligne courante
89. ligne = file.readline()
90. while ligne:
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
321/755
91. # enregistrement dans le fichier de logs
92. html.write(str(ligne))
93. # ligne suivante
94. ligne = file.readline()
95. # la boucle se finit lorsque le serveur ferme la connexion
96. finally:
97. # le client ferme la connexion
98. if sock:
99. sock.close()
100. # fermeture du fichier html
101. if html:
102. html.close()
103.
104.
105. # -------------------main
106.
107. # on configure l'application
108. import config
109. config = config.configure()
110.
111. # obtenir les URL du fichier de configuration
112. for url in config['urls']:
113. print("-------------------------")
114. print(url['site'])
115. print("-------------------------")
116. try:
117. # lecture URL du site [site]
118. get_url(url)
119. except BaseException as erreur:
120. print(f"L'erreur suivante s'est produite : {erreur}")
121. finally:
122. pass
123. # fin
124. print("Terminé...")
Commentaires du code :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
322/755
• lignes 81-83 : si la ligne n'est pas vide et que le suivi a été demandé, la ligne reçue est affichée sur la console ;
• lignes 84-86 : si on a récupéré la ligne vide qui marque la fin des entêtes HTTP envoyés par le serveur alors on arrête la
boucle de la ligne 76 ;
• lignes 90-95 : les lignes de texte de la réponse du serveur peuvent être lues ligne par ligne avec une boucle while et enregistrées
dans le fichier texte [html]. Lorsque le serveur web a envoyé la totalité de la page qu'on lui a demandée, il ferme sa connexion
avec le client. Côté client, cela sera détecté comme une fin de fichier et on sortira de la boucle des lignes 90-95 ;
• lignes 96-102 : erreur ou pas, on libère toutes les ressources utilisées par le code ;
Résultats :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/inet/http/01/main.py
2. -------------------------
3. localhost
4. -------------------------
5. Client : début de la communication avec le serveur [localhost]
6. --> GET / HTTP/1.1
7. --> Host: localhost:80
8. --> User-Agent: client Python
9. --> Accept: text/HTML
10. --> Accept-Language: fr
11. Réponse du serveur [localhost]
12. <-- HTTP/1.1 200 OK
13. <-- Date: Sun, 05 Jul 2020 16:27:46 GMT
14. <-- Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19
15. <-- X-Powered-By: PHP/7.2.19
16. <-- Content-Length: 1776
17. <-- Connection: close
18. <-- Content-Type: text/html; charset=UTF-8
19. -------------------------
20. sergetahe.com
21. -------------------------
22. Client : début de la communication avec le serveur [sergetahe.com]
23. --> GET / HTTP/1.1
24. --> Host: sergetahe.com:80
25. --> User-Agent: client Python
26. --> Accept: text/HTML
27. --> Accept-Language: fr
28. Réponse du serveur [sergetahe.com]
29. <-- HTTP/1.1 302 Found
30. <-- Date: Sun, 05 Jul 2020 16:27:45 GMT
31. <-- Content-Type: text/html; charset=UTF-8
32. <-- Transfer-Encoding: chunked
33. <-- Connection: close
34. <-- Server: Apache
35. <-- X-Powered-By: PHP/7.3
36. <-- Location: http://sergetahe.com:80/cours-tutoriels-de-programmation
37. <-- Set-Cookie: SERVERID68971=2620178|XwH/h|XwH/h; path=/
38. <-- X-IPLB-Instance: 17106
39. -------------------------
40. tahe.developpez.com
41. -------------------------
42. Client : début de la communication avec le serveur [tahe.developpez.com]
43. --> GET / HTTP/1.1
44. --> Host: tahe.developpez.com:443
45. --> User-Agent: client Python
46. --> Accept: text/HTML
47. --> Accept-Language: fr
48. Réponse du serveur [tahe.developpez.com]
49. <-- HTTP/1.1 400 Bad Request
50. <-- Date: Sun, 05 Jul 2020 16:27:45 GMT
51. <-- Server: Apache/2.4.38 (Debian)
52. <-- Content-Length: 453
53. <-- Connection: close
54. <-- Content-Type: text/html; charset=iso-8859-1
55. -------------------------
56. www.sergetahe.com
57. -------------------------
58. Client : début de la communication avec le serveur [www.sergetahe.com]
59. --> GET /cours-tutoriels-de-programmation/ HTTP/1.1
60. --> Host: sergetahe.com:80
61. --> User-Agent: client Python
62. --> Accept: text/HTML
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
323/755
63. --> Accept-Language: fr
64. Réponse du serveur [www.sergetahe.com]
65. <-- HTTP/1.1 301 Moved Permanently
66. <-- Date: Sun, 05 Jul 2020 16:27:45 GMT
67. <-- Content-Type: text/html; charset=iso-8859-1
68. <-- Content-Length: 263
69. <-- Connection: close
70. <-- Server: Apache
71. <-- Location: https://sergetahe.com/cours-tutoriels-de-programmation/
72. <-- Set-Cookie: SERVERID68971=2620178|XwH/h|XwH/h; path=/
73. <-- X-IPLB-Instance: 17095
74. Terminé...
75.
76. Process finished with exit code 0
Commentaires
De façon générale les codes 3xx, 4xx et 5xx d’un serveur HTTP sont des codes d’erreur.
1. <!DOCTYPE html>
2. <html>
3. <head>
4. <title>Laragon</title>
5.
6. <link href="https://fonts.googleapis.com/css?family=Karla:400" rel="stylesheet" type="text/css">
7.
8. <style>
9. html, body {
10. height: 100%;
11. }
12.
13. body {
14. margin: 0;
15. padding: 0;
16. width: 100%;
17. display: table;
18. font-weight: 100;
19. font-family: 'Karla';
20. }
21.
22. .container {
23. text-align: center;
24. display: table-cell;
25. vertical-align: middle;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
324/755
26. }
27.
28. .content {
29. text-align: center;
30. display: inline-block;
31. }
32.
33. .title {
34. font-size: 96px;
35. }
36.
37. .opt {
38. margin-top: 30px;
39. }
40.
41. .opt a {
42. text-decoration: none;
43. font-size: 150%;
44. }
45.
46. a:hover {
47. color: red;
48. }
49. </style>
50. </head>
51. <body>
52. <div class="container">
53. <div class="content">
54. <div class="title" title="Laragon">Laragon</div>
55.
56. <div class="info"><br />
57. Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19<br />
58. PHP version: 7.2.19 <span><a title="phpinfo()" href="/?q=info">info</a></span><br
/>
59. Document Root: C:/MyPrograms/laragon/www<br />
60.
61. </div>
62. <div class="opt">
63. <div><a title="Getting Started" href="https://laragon.org/docs">Getting Started</a></div>
64. </div>
65. </div>
66.
67. </div>
68. </body>
69. </html>
La plupart des serveurs http envoient par morceaux leurs réponses aux requêtes qui leur sont faites. Chaque morceau envoyé est
précédé d'une ligne indiquant le nombre d'octets du morceau qui suit. Cela permet au client de lire ce nombre exact d'octets pour
avoir le morceau. Ici le 0 indique que le morceau qui suit a zéro octet. On rappelle que le serveur avait indiqué le document
[http://sergetahe.com/] avait changé d'URL. Il n'a donc pas envoyé de document.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
325/755
10. <hr>
11. <address>Apache/2.4.38 (Debian) Server at 2eurocents.developpez.com Port 80</address>
12. </body></html>
• lignes 1-12 : le serveur a envoyé un document HTML malgré le fait que la requête était incorrecte (ligne 49 des résultats). Le
document HTML permet au serveur de préciser la cause de l’erreur. Celle-ci est indiquée aux lignes 6 et 7 :
o ligne 7 : notre client a utilisé le protocole HTTP ;
o ligne 8 : le serveur travaille avec le protocole HTTPS (S=sécurisé) et n’accepte pas le protocole HTTP ;
Là également, il s’est produit une erreur (ligne 3). Néanmoins, le serveur prend soin d’envoyer un document HTML détaillant celle-
ci (lignes 1-7).
21.4.4 Exemple 4
Les exemples précédents nous ont montré que notre client HTTP était insuffisant. Nous allons maintenant présenter un outil appelé
[curl] qui permet de récupérer des documents web en gérant les difficultés mentionnées : protocole HTTPS, document envoyé par
morceaux, redirections… L’outil [curl] a été installé avec Laragon :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
326/755
Dans le terminal nous tapons la commande suivante :
Le fait que la commande [curl –help] ait produit des résultats montre que la commande [curl] est dans le PATH du terminal. Sous
Windows, le PATH est l’ensemble des dossiers explorés lorsque l’utilisateur tape une commande exécutable, ici [curl]. La valeur du
PATH peut être connue :
Ligne 2, les dossiers du PATH séparés par des points-virgules. Dans cette liste n’apparaît pas de dossier lié à Laragon. Si on enquête
un peu, on trouve qu’il y a un [curl] dans le dossier [c:\windows\system32]. C’est celui-ci qui a répondu auparavant.
Si on veut utiliser l’outil [curl] livré avec Laragon, on pourra procéder comme suit :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
327/755
• on obtient quelque chose de très différent de ce qui avait été obtenu dans un terminal PyCharm. Ce PATH contient de
nombreux dossiers créés lors de l’installation de Laragon. Le dossier contenant l’outil [curl] en fait partie :
Par la suite, utilisez le terminal de votre choix. Sachez simplement que lorsque vous voulez utiliser un outil amené par Laragon, le
terminal Laragon est à préférer.
La commande [curl --help] fait afficher toutes les options de configuration de [curl]. Il y en a plusieurs dizaines. Nous en
utiliserons très peu. Pour demander une URL il suffit de taper la commande [curl URL]. Cette commande affichera sur la console le
document demandé. Si on veut de plus les échanges HTTP entre le client et le serveur on écrira [curl --verbose URL]. Enfin pour
enregistrer le document HTML demandé dans un fichier on écrira [curl --verbose --output fichier URL].
Pour éviter de polluer le système de fichiers de notre machine, déplaçons-nous à un autre endroit (j’utilise ici un terminal Laragon) :
3. λ cd \Temp\
4.
5. C:\Temp
6. λ mkdir curl
7.
8. C:\Temp
9. λ cd curl\
10.
11. C:\Temp\curl
12. λ dir
13. Le volume dans le lecteur C s’appelle Local Disk
14. Le numéro de série du volume est B84C-D958
15.
16. Répertoire de C:\Temp\curl
17.
18. 05/07/2020 19:31 <DIR> .
19. 05/07/2020 19:31 <DIR> ..
20. 0 fichier(s) 0 octets
21. 2 Rép(s) 892 388 098 048 octets libres
• ligne 3, on se déplace dans le dossier [c:\temp]. Si ce dossier n’existe pas, vous pouvez le créer ou en choisir un autre ;
• ligne 6, on crée un dossier appelé [curl] ;
• ligne 9, on se positionne dessus ;
• ligne 12, on liste son contenu. Il est vide (ligne 20);
Assurez-vous que le serveur Apache de Laragon est lancé et avec [curl] demandez l’URL [http://localhost/] avec la commande
[curl –verbose –output localhost.html http://localhost/]. On obtient les résultats suivants :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
328/755
16. < Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19
17. < X-Powered-By: PHP/7.2.19
18. < Content-Length: 1776
19. < Content-Type: text/html; charset=UTF-8
20. <
21. { [1776 bytes data]
22. 100 1776 100 1776 0 0 1062 0 0:00:01 0:00:01 --:--:-- 1062
23. * Connection #0 to host localhost left intact
• lignes 10-13 : lignes envoyées par [curl] au serveur [localhost]. On reconnaît le protocole HTTP ;
• lignes 14-20 : lignes envoyées en réponse par le serveur ;
• ligne 14 : indique qu’on a bien eu le document demandé ;
Le fichier [localhost.html] contient le document demandé. Vous pouvez le vérifier en chargeant le fichier dans un éditeur de texte.
Maintenant demandons l’URL [https://tahe.developpez.com:443/]. Pour avoir cette URL, le client HTTP doit savoir parler
HTTPS. C’est le cas du client [curl].
1. C:\Temp\curl
2. λ curl --verbose --output tahe.developpez.com.html https://tahe.developpez.com:443/
3. % Total % Received % Xferd Average Speed Time Time Time Current
4. Dload Upload Total Spent Left Speed
5. 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 87.98.130.52...
6. * TCP_NODELAY set
7. 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Connected to
tahe.developpez.com (87.98.130.52) port 443 (#0)
8. * ALPN, offering h2
9. * ALPN, offering http/1.1
10. * successfully set certificate verify locations:
11. * CAfile: C:\MyPrograms\laragon\bin\laragon\utils\curl-ca-bundle.crt
12. CApath: none
13. } [5 bytes data]
14. * TLSv1.3 (OUT), TLS handshake, Client hello (1):
15. } [512 bytes data]
16. * TLSv1.3 (IN), TLS handshake, Server hello (2):
17. { [122 bytes data]
18. * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
19. { [25 bytes data]
20. * TLSv1.3 (IN), TLS handshake, Certificate (11):
21. { [2563 bytes data]
22. * TLSv1.3 (IN), TLS handshake, CERT verify (15):
23. { [264 bytes data]
24. * TLSv1.3 (IN), TLS handshake, Finished (20):
25. { [52 bytes data]
26. * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
27. } [1 bytes data]
28. * TLSv1.3 (OUT), TLS handshake, Finished (20):
29. } [52 bytes data]
30. * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
31. * ALPN, server accepted to use http/1.1
32. * Server certificate:
33. * subject: CN=*.developpez.com
34. * start date: Jul 1 15:38:30 2020 GMT
35. * expire date: Sep 29 15:38:30 2020 GMT
36. * subjectAltName: host "tahe.developpez.com" matched cert's "*.developpez.com"
37. * issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
38. * SSL certificate verify ok.
39. } [5 bytes data]
40. > GET / HTTP/1.1
41. > Host: tahe.developpez.com
42. > User-Agent: curl/7.63.0
43. > Accept: */*
44. >
45. { [5 bytes data]
46. * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
47. { [281 bytes data]
48. * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
49. { [297 bytes data]
50. * old SSL session ID is stale, removing
51. { [5 bytes data]
52. < HTTP/1.1 200 OK
53. < Date: Sun, 05 Jul 2020 17:39:53 GMT
54. < Server: Apache/2.4.38 (Debian)
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
329/755
55. < X-Powered-By: PHP/5.3.29
56. < Vary: Accept-Encoding
57. < Transfer-Encoding: chunked
58. < Content-Type: text/html
59. <
60. { [6 bytes data]
61. 100 99k 0 99k 0 0 79343 0 --:--:-- 0:00:01 --:--:-- 79343
62. * Connection #0 to host tahe.developpez.com left intact
• lignes 10-39 : les échanges client / serveur pour sécuriser la connexion : celle-ci sera chiffrée ;
• lignes 41-44 : les entêtes HTTP envoyés par le client [curl] au serveur ;
• ligne 52 : le document demandé a bien été trouvé ;
• ligne 57 : le document est envoyé par morceaux ;
[curl] gère correctement à la fois le protocole sécurisé HTTPS et le fait que le document soit envoyé par morceaux. Le document
envoyé sera trouvé ici dans le fichier [tahe.developpez.com.html].
Demandons maintenant l’URL [http://sergetahe.com/cours-tutoriels-de-programmation]. Nous avions vu que pour cette URL,
il y avait une redirection vers l’URL [http://sergetahe.com/cours-tutoriels-de-programmation/] (avec un / à la fin).
1. C:\Temp\curl
2. λ curl --verbose --output sergetahe.com.html --location http://sergetahe.com/cours-tutoriels-de-
programmation
3. % Total % Received % Xferd Average Speed Time Time Time Current
4. Dload Upload Total Spent Left Speed
5. 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying 87.98.154.146...
6. * TCP_NODELAY set
7. * Connected to sergetahe.com (87.98.154.146) port 80 (#0)
8. > GET /cours-tutoriels-de-programmation HTTP/1.1
9. > Host: sergetahe.com
10. > User-Agent: curl/7.63.0
11. > Accept: */*
12. >
13. < HTTP/1.1 301 Moved Permanently
14. < Date: Sun, 05 Jul 2020 17:44:17 GMT
15. < Content-Type: text/html; charset=iso-8859-1
16. < Content-Length: 262
17. < Server: Apache
18. < Location: http://sergetahe.com/cours-tutoriels-de-programmation/
19. < Set-Cookie: SERVERID68971=2620178|XwIRd|XwIRd; path=/
20. < X-IPLB-Instance: 17095
21. <
22. * Ignoring the response-body
23. { [262 bytes data]
24. 100 262 100 262 0 0 1858 0 --:--:-- --:--:-- --:--:-- 1858
25. * Connection #0 to host sergetahe.com left intact
26. * Issue another request to this URL: 'http://sergetahe.com/cours-tutoriels-de-programmation/'
27. * Found bundle for host sergetahe.com: 0x14385f8 [can pipeline]
28. * Could pipeline, but not asked to!
29. * Re-using existing connection! (#0) with host sergetahe.com
30. * Connected to sergetahe.com (87.98.154.146) port 80 (#0)
31. > GET /cours-tutoriels-de-programmation/ HTTP/1.1
32. > Host: sergetahe.com
33. > User-Agent: curl/7.63.0
34. > Accept: */*
35. >
36. < HTTP/1.1 301 Moved Permanently
37. < Date: Sun, 05 Jul 2020 17:44:17 GMT
38. < Content-Type: text/html; charset=iso-8859-1
39. < Content-Length: 263
40. < Server: Apache
41. < Location: https://sergetahe.com/cours-tutoriels-de-programmation/
42. < Set-Cookie: SERVERID68971=2620178|XwIRd|XwIRd; path=/
43. < X-IPLB-Instance: 17095
44. <
45. * Ignoring the response-body
46. { [263 bytes data]
47. 100 263 100 263 0 0 764 0 --:--:-- --:--:-- --:--:-- 764
48. * Connection #0 to host sergetahe.com left intact
49. * Issue another request to this URL: 'https://sergetahe.com/cours-tutoriels-de-programmation/'
50. * Trying 87.98.154.146...
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
330/755
51. *TCP_NODELAY set
52. *Connected to sergetahe.com (87.98.154.146) port 443 (#1)
53. *ALPN, offering h2
54. *ALPN, offering http/1.1
55. *successfully set certificate verify locations:
56. * CAfile: C:\MyPrograms\laragon\bin\laragon\utils\curl-ca-bundle.crt
57. CApath: none
58. } [5 bytes data]
59. * TLSv1.3 (OUT), TLS handshake, Client hello (1):
60. } [512 bytes data]
61. * TLSv1.3 (IN), TLS handshake, Server hello (2):
62. { [102 bytes data]
63. * TLSv1.2 (IN), TLS handshake, Certificate (11):
64. { [2572 bytes data]
65. * TLSv1.2 (IN), TLS handshake, Server key exchange (12):
66. { [333 bytes data]
67. * TLSv1.2 (IN), TLS handshake, Server finished (14):
68. { [4 bytes data]
69. * TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
70. } [70 bytes data]
71. * TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
72. } [1 bytes data]
73. * TLSv1.2 (OUT), TLS handshake, Finished (20):
74. } [16 bytes data]
75. 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* TLSv1.2 (IN), TLS
handshake, Finished (20):
76. { [16 bytes data]
77. * SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
78. * ALPN, server accepted to use h2
79. * Server certificate:
80. * subject: CN=sergetahe.com
81. * start date: May 10 01:41:15 2020 GMT
82. * expire date: Aug 8 01:41:15 2020 GMT
83. * subjectAltName: host "sergetahe.com" matched cert's "sergetahe.com"
84. * issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
85. * SSL certificate verify ok.
86. * Using HTTP2, server supports multi-use
87. * Connection state changed (HTTP/2 confirmed)
88. * Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
89. } [5 bytes data]
90. * Using Stream ID: 1 (easy handle 0x2bee870)
91. } [5 bytes data]
92. > GET /cours-tutoriels-de-programmation/ HTTP/2
93. > Host: sergetahe.com
94. > User-Agent: curl/7.63.0
95. > Accept: */*
96. >
97. { [5 bytes data]
98. * Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
99. } [5 bytes data]
100. 0 0 0 0 0 0 0 0 --:--:-- 0:00:01 --:--:-- 0< HTTP/2 200
101. < date: Sun, 05 Jul 2020 17:44:19 GMT
102. < content-type: text/html; charset=UTF-8
103. < server: Apache
104. < x-powered-by: PHP/7.3
105. < link: <https://sergetahe.com/cours-tutoriels-de-programmation/wp-json/>; rel="https://api.w.org/"
106. < link: <https://sergetahe.com/cours-tutoriels-de-programmation/>; rel=shortlink
107. < vary: Accept-Encoding
108. < x-iplb-instance: 17080
109. < set-cookie: SERVERID68971=2620178|XwIRd|XwIRd; path=/
110. <
111. { [5 bytes data]
112. 100 49634 0 49634 0 0 26040 0 --:--:-- 0:00:01 --:--:-- 37830
113. * Connection #1 to host sergetahe.com left intact
• ligne 2 : on utilise l’option [--location] pour indiquer qu’on veut suivre les redirections envoyées par le serveur ;
• ligne 13 : le serveur indique que le document demandé a changé d’URL ;
• ligne 18 : il indique la nouvelle URL du document demandé ;
• ligne 31 : [curl] émet une nouvelle requête vers cette fois la nouvelle URL ;
• ligne 36 : le serveur répond de nouveau que l’URL a changé ;
• ligne 41 : la nouvelle URL est exactement la même que celle qui a été redirigée à un détail près : le procole a changé. Il est
devenu HTTPS (ligne 41) alors qu’il était http auparavant (ligne 31) ;
• ligne 49 : une nouvelle requête st émise vers la nouvelle URL. Celle-ci est chiffrée. Aussi tout un dialogue de mise en place de
la sécurité se met en place, lignes 53-91 ;
• ligne 92 : la nouvelle URL est demandée avec cette fois le protocole HTTP/2 ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
331/755
• ligne 100 : le document a été trouvé ;
1. C:\Temp\curl
2. λ dir
3. Le volume dans le lecteur C s’appelle Local Disk
4. Le numéro de série du volume est B84C-D958
5.
6. Répertoire de C:\Temp\curl
7.
8. 05/07/2020 19:44 <DIR> .
9. 05/07/2020 19:44 <DIR> ..
10. 05/07/2020 19:35 1 776 localhost.html
11. 05/07/2020 19:44 49 634 sergetahe.com.html
12. 05/07/2020 19:39 101 639 tahe.developpez.com.html
13. 3 fichier(s) 153 049 octets
14. 2 Rép(s) 892 385 628 160 octets libres
21.4.5 Exemple 5
Python possède un module appelé [pyccurl] qui permet d’utiliser les capacités de l’outil [curl] dans un programme Python. Nous
installons ce module :
1. def configure():
2. # liste des URL à interroger
3. urls = [
4. # site : serveur auquel se connecter
5. # timeout : délai maximal d'attente d'une réponse du serveur
6. # target : url à demander
7. # encoding : encodage de la réponse du serveur
8. {
9. "site": "sergetahe.com",
10. "timeout": 2000,
11. "target": "http://sergetahe.com",
12. "encoding": "utf-8"
13. },
14. {
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
332/755
15. "site": "tahe.developpez.com",
16. "timeout": 500,
17. "target": "https://tahe.developpez.com",
18. "encoding": "iso-8859-1"
19. },
20. {
21. "site": "www.polytech-angers.fr",
22. "timeout": 500,
23. "target": "http://www.polytech-angers.fr",
24. "encoding": "utf-8"
25. },
26. {
27. "site": "localhost",
28. "timeout": 500,
29. "target": "http://localhost",
30. "encoding": "utf-8"
31. }
32. ]
33. # on rend la configuration
34. return {
35. 'urls': urls
36. }
1. # imports
2. import codecs
3. from io import BytesIO
4.
5. import pycurl
6.
7.
8. # -----------------------------------------------------------------------
9. def get_url(url: dict, suivi=True):
10. # lit l'URL url[url] et la stocke dans le fichier output/url['site'].html
11. # si [suivi=True] alors il y a un suivi console de l'échange client / serveur
12. # url[timeout] est le timeout des appels client;
13. # url [encoding] est l'encodage du document demandé
14.
15. # on récupère les données de configuration
16. server = url['site']
17. timeout = url['timeout']
18. target = url['target']
19. encoding = url['encoding']
20. # suivi
21. print(f"Client : début de la communication avec le serveur [{server}]")
22.
23. # on laisse remonter les exceptions
24. html = None
25. curl = None
26. try:
27. # Initialisation d'une session cURL
28. curl = pycurl.Curl()
29. # flux binaire
30. flux = BytesIO()
31. # options de curl
32. options = {
33. # URL
34. curl.URL: target,
35. # WRITEDATA : là où les données reçues seront stockées
36. curl.WRITEDATA: flux,
37. # mode verbose
38. curl.VERBOSE: suivi,
39. # nouvelle connexion - pas de cache
40. curl.FRESH_CONNECT: True,
41. # timeout de la requête (en secondes)
42. curl.TIMEOUT: timeout,
43. curl.CONNECTTIMEOUT: timeout,
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
333/755
44. # ne pas vérifier la validité des certificats SSL
45. curl.SSL_VERIFYPEER: False,
46. # suivre les redirections
47. curl.FOLLOWLOCATION: True
48. }
49. # paramétrage de curl
50. for option, value in options.items():
51. curl.setopt(option, value)
52. # Execution de la requête CURL ainsi paramétrée
53. curl.perform()
54. # création du fichier server.html - on change les caractères gênants pour un nom de fichier
55. server2 = server.replace("/", "_")
56. server2 = server2.replace(".", "_")
57. html_filename = f'{server2}.html'
58. html = codecs.open(f"output/{html_filename}", "w", encoding)
59. # enregistrement du document reçu dans le fichier HTML
60. html.write(flux.getvalue().decode(encoding))
61. finally:
62. # libération des ressources
63. if curl:
64. curl.close()
65. if html:
66. html.close()
67.
68.
69. # -------------------main
70. # on configure l'application
71. import config
72. config = config.configure()
73.
74. # obtenir les URL du fichier de configuration
75. for url in config['urls']:
76. print("-------------------------")
77. print(url['site'])
78. print("-------------------------")
79. try:
80. # lecture URL du site [site]
81. get_url(url)
82. # except BaseException as erreur:
83. # print(f"L'erreur suivante s'est produite : {erreur}")
84. finally:
85. pass
86. # fin
87. print("Terminé...")
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
334/755
d'encodage du document, il suffit de demander l'URL désirée avec un navigateur et regarder les entêtes HTTP envoyés par
celui-ci en mode débogage du navigateur (F12) ou bien le document lui-même car celui-ci précise également l'encodage :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/inet/http/02/main.py
2. -------------------------
3. sergetahe.com
4. -------------------------
5. Client : début de la communication avec le serveur [sergetahe.com]
6. * Trying 87.98.154.146:80...
7. * TCP_NODELAY set
8. * Connected to sergetahe.com (87.98.154.146) port 80 (#0)
9. > GET / HTTP/1.1
10. Host: sergetahe.com
11. User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0
nghttp2/1.40.0
12. Accept: */*
13.
14. * Mark bundle as not supporting multiuse
15. < HTTP/1.1 302 Found
16. < Date: Mon, 06 Jul 2020 06:45:52 GMT
17. < Content-Type: text/html; charset=UTF-8
18. < Transfer-Encoding: chunked
19. < Server: Apache
20. < X-Powered-By: PHP/7.3
21. < Location: http://sergetahe.com/cours-tutoriels-de-programmation
22. < Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
23. < X-IPLB-Instance: 17102
24. <
25. * Ignoring the response-body
26. * Connection #0 to host sergetahe.com left intact
27. * Issue another request to this URL: 'http://sergetahe.com/cours-tutoriels-de-programmation'
28. * Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
29. * Can not multiplex, even if we wanted to!
30. * Re-using existing connection! (#0) with host sergetahe.com
31. * Connected to sergetahe.com (87.98.154.146) port 80 (#0)
32. > GET /cours-tutoriels-de-programmation HTTP/1.1
33. Host: sergetahe.com
34. User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0
nghttp2/1.40.0
35. Accept: */*
36.
37. * Mark bundle as not supporting multiuse
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
335/755
38. < HTTP/1.1 301 Moved Permanently
39. < Date: Mon, 06 Jul 2020 06:45:52 GMT
40. < Content-Type: text/html; charset=iso-8859-1
41. < Content-Length: 262
42. < Server: Apache
43. < Location: http://sergetahe.com/cours-tutoriels-de-programmation/
44. < Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
45. < X-IPLB-Instance: 17102
46. <
47. * Ignoring the response-body
48. * Connection #0 to host sergetahe.com left intact
49. * Issue another request to this URL: 'http://sergetahe.com/cours-tutoriels-de-programmation/'
50. * Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
51. * Can not multiplex, even if we wanted to!
52. * Re-using existing connection! (#0) with host sergetahe.com
53. * Connected to sergetahe.com (87.98.154.146) port 80 (#0)
54. > GET /cours-tutoriels-de-programmation/ HTTP/1.1
55. Host: sergetahe.com
56. User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0
nghttp2/1.40.0
57. Accept: */*
58.
59. * Mark bundle as not supporting multiuse
60. < HTTP/1.1 301 Moved Permanently
61. < Date: Mon, 06 Jul 2020 06:45:52 GMT
62. < Content-Type: text/html; charset=iso-8859-1
63. < Content-Length: 263
64. < Server: Apache
65. < Location: https://sergetahe.com/cours-tutoriels-de-programmation/
66. < Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
67. < X-IPLB-Instance: 17102
68. <
69. * Ignoring the response-body
70. * Connection #0 to host sergetahe.com left intact
71. * Issue another request to this URL: 'https://sergetahe.com/cours-tutoriels-de-programmation/'
72. * Trying 87.98.154.146:443...
73. * TCP_NODELAY set
74. * ….
75. * Using Stream ID: 1 (easy handle 0x25eaec77010)
76. > GET /cours-tutoriels-de-programmation/ HTTP/2
77. Host: sergetahe.com
78. user-agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0
nghttp2/1.40.0
79. accept: */*
80.
81. * Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
82. < HTTP/2 200
83. < date: Mon, 06 Jul 2020 06:45:53 GMT
84. < content-type: text/html; charset=UTF-8
85. < server: Apache
86. < x-powered-by: PHP/7.3
87. < link: <https://sergetahe.com/cours-tutoriels-de-programmation/wp-json/>; rel="https://api.w.org/"
88. < link: <https://sergetahe.com/cours-tutoriels-de-programmation/>; rel=shortlink
89. < vary: Accept-Encoding
90. < x-iplb-instance: 17080
91. < set-cookie: SERVERID68971=26218|XwLIp|XwLIp; path=/
92. <
93. * Connection #1 to host sergetahe.com left intact
94. -------------------------
95. tahe.developpez.com
96. -------------------------
97. Client : début de la communication avec le serveur [tahe.developpez.com]
98. * Trying 87.98.130.52:443...
99. * TCP_NODELAY set
100. * Connected to tahe.developpez.com (87.98.130.52) port 443 (#0)
101. * ALPN, offering h2
102. * ALPN, offering http/1.1
103. * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
104. * ALPN, server accepted to use http/1.1
105. * Server certificate:
106. * subject: CN=*.developpez.com
107. * start date: Jul 1 15:38:30 2020 GMT
108. * expire date: Sep 29 15:38:30 2020 GMT
109. * subjectAltName: host "tahe.developpez.com" matched cert's "*.developpez.com"
110. * issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
111. * SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
112. > GET / HTTP/1.1
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
336/755
113. Host: tahe.developpez.com
114. User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0
nghttp2/1.40.0
115. Accept: */*
116.
117. * old SSL session ID is stale, removing
118. * Mark bundle as not supporting multiuse
119. < HTTP/1.1 200 OK
120. < Date: Mon, 06 Jul 2020 06:45:53 GMT
121. < Server: Apache/2.4.38 (Debian)
122. < X-Powered-By: PHP/5.3.29
123. < Vary: Accept-Encoding
124. < Transfer-Encoding: chunked
125. < Content-Type: text/html
126. <
127. * Connection #0 to host tahe.developpez.com left intact
128. -------------------------
129. www.polytech-angers.fr
130. -------------------------
131. Client : début de la communication avec le serveur [www.polytech-angers.fr]
132. * Trying 193.49.144.41:80...
133. * TCP_NODELAY set
134. * Connected to www.polytech-angers.fr (193.49.144.41) port 80 (#0)
135. > GET / HTTP/1.1
136. Host: www.polytech-angers.fr
137. User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0
nghttp2/1.40.0
138. Accept: */*
139.
140. * Mark bundle as not supporting multiuse
141. < HTTP/1.1 301 Moved Permanently
142. < Date: Mon, 06 Jul 2020 06:45:54 GMT
143. < Server: Apache/2.4.29 (Ubuntu)
144. < Location: http://www.polytech-angers.fr/fr/index.html
145. < Cache-Control: max-age=1
146. < Expires: Mon, 06 Jul 2020 06:45:55 GMT
147. < Content-Length: 339
148. < Content-Type: text/html; charset=iso-8859-1
149. <
150. * Ignoring the response-body
151. * Connection #0 to host www.polytech-angers.fr left intact
152. * Issue another request to this URL: 'http://www.polytech-angers.fr/fr/index.html'
153. * Found bundle for host www.polytech-angers.fr: 0x25eacafb490 [serially]
154. * Can not multiplex, even if we wanted to!
155. * Re-using existing connection! (#0) with host www.polytech-angers.fr
156. * Connected to www.polytech-angers.fr (193.49.144.41) port 80 (#0)
157. > GET /fr/index.html HTTP/1.1
158. Host: www.polytech-angers.fr
159. User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0
nghttp2/1.40.0
160. Accept: */*
161.
162. * Mark bundle as not supporting multiuse
163. < HTTP/1.1 200 OK
164. < Date: Mon, 06 Jul 2020 06:45:54 GMT
165. < Server: Apache/2.4.29 (Ubuntu)
166. < Last-Modified: Mon, 06 Jul 2020 04:50:09 GMT
167. < ETag: "85be-5a9be9bfcf228"
168. < Accept-Ranges: bytes
169. < Content-Length: 34238
170. < Cache-Control: max-age=1
171. < Expires: Mon, 06 Jul 2020 06:45:55 GMT
172. < Vary: Accept-Encoding
173. < Content-Type: text/html; charset=UTF-8
174. < Content-Language: fr
175. <
176. * Connection #0 to host www.polytech-angers.fr left intact
177. -------------------------
178. localhost
179. -------------------------
180. Client : début de la communication avec le serveur [localhost]
181. * Trying ::1:80...
182. * TCP_NODELAY set
183. * Connected to localhost (::1) port 80 (#0)
184. > GET / HTTP/1.1
185. Host: localhost
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
337/755
186. User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0
nghttp2/1.40.0
187. Accept: */*
188.
189. * Mark bundle as not supporting multiuse
190. < HTTP/1.1 200 OK
191. < Date: Mon, 06 Jul 2020 06:45:54 GMT
192. < Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19
193. < X-Powered-By: PHP/7.2.19
194. < Content-Length: 1776
195. < Content-Type: text/html; charset=UTF-8
196. <
197. * Connection #0 to host localhost left intact
198. Terminé...
199.
200. Process finished with exit code 0
Commentaires
21.4.6 Conclusion
Nous avons, dans cette section, découvert le protocole HTTP et avons écrit un script [http/02/main.py] capable de télécharger une
URL du web.
Dans ce chapitre :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
338/755
Note : Envoyez quelques mails à l'adresse que vous avez créée. Ne passez à la suite que lorsque vous êtes sûr que le compte créé est
capable de recevoir des mails.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
339/755
• en [1-2], sélectionnez à la fois le serveur de mails et les outils pour l’administrer ;
• durant l’installation le mot de l’administrateur vous sera demandé : notez le, car il vous sera nécessaire ;
[hMailServer] s’installe comme un service Windows lancé automatiquement au démarrage de la machine. Il est préférable de choisir
un démarrage manuel :
Une fois démarré, le serveur [hMailServer] doit être configuré. Le serveur a été installé avec un programme d’administration
[hMailServer Administrator] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
340/755
• en [2], dans la zone de saisie de la barre d’état, taper [hmailserver] ;
• en [3], lancer l’administrateur ;
• en [4], connecter l’administrateur au serveur [hMailServer] ;
• en [5], taper le mot de passe saisi lors de l’installation de [hMailServer] ;
• en [100], enlevez le mot de passe de la ligne [AdministratorPassword]. Cela aura pour effet que l'administrateur n'aura plus
de mot de passe. Tapez simplement [Entrée] lorsque celui-ci vous sera demandé ;
1. ValidLanguages=english,swedish
2. [Security]
3. AdministratorPassword=
4. [Database]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
341/755
Continuons la configuration du serveur :
• en [3], on peut mettre à peu près n’importe quoi pour les tests que nous allons opérer. Dans la réalité, il faudrait mettre le
nom d’un domaine existant ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
342/755
• cliquer droit sur [Accounts] (7) puis (8) pour ajouter un nouvel utilisateur ;
• dans l’onglet [General] (9), nous définissons un utilisateur [guest] (10) avec le mot de passe [guest] (11). Il aura l’adresse
mail [guest@localhost] (10) ;
• en [12], l’utilisateur [guest] est activé ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
343/755
On fait de même avec le serveur POP3 :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
344/755
Nous indiquons le domaine par défaut du serveur [hMailServer] (il peut y en avoir plusieurs) :
• en [37], indiquez que le domaine par défaut du serveur SMTP est celui que vous avez créé en [38] ;
Après avoir sauvegardé cette configuration, vous pouvez la tester de la façon suivante. Ouvrez un terminal PyCharm dans le dossier
des utilitaires :
• ligne 1 : on se connecte au port 25 de la machine [localhost]. C’est là qu’officie un serveur SMTP non sécurisé du serveur
[hMailServer] ;
• ligne 4 : on reçoit le message de bienvenue que nous avons configuré à l’étape 30 précédente ;
Le serveur SMTP est donc bien en place. Tapez la commande [quit] pour terminer le dialogue avec le serveur SMTP 25.
Maintenant faisons la même chose avec le port 587 qui est le port par défaut du service SMTP sécurisé de relève du courrier :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
345/755
1. (venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 587
2. Client [DESKTOP-30FF5FB:50217] connecté au serveur [localhost-587]
3. Tapez vos commandes (quit pour arrêter) :
4. <-- [220 Bienvenue sur le serveur SMTP localhost.com]
Maintenant faisons la même chose avec le port 110 qui est le port par défaut du service POP3 de relève du courrier :
Maintenant faisons la même chose avec le port 143 qui est le port par défaut du service IMAP de relève du courrier :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
346/755
• en [7-11] : le serveur POP3 qui va nous permettre de lire le courrier du serveur de mail [hMailServer] est à l'adresse
[localhost] et officie sur le port 110 ;
• en [12-16] : le serveur SMTP qui va nous permettre d'envoyer du courrier de la part des utilisateurs du serveur de mail
[hMailServer] est à l'adresse [localhost] et officie sur le port 25 ;
• [18] : on peut tester la validation de cette configuration ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
347/755
• en [26] : parce qu'on n'a pas de chiffrement SSL, Thunderbird nous avertit que notre configuration comporte des risques ;
• en [28] : le compte a été créé ;
• en [3] : l'expéditeur ;
• en [4] : le destinataire ;
• en [5] : le sujet du mail ;
• en [6] : le contenu du mail ;
• en [7] : pour envoyer le mail ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
348/755
• en [8-9] : on relève le courrier de l'utilisateur [guest@localhost] ;
• en [10-15] : le message reçu ;
Nous allons envoyer également du courrier à l'utilisateur [pymailparlexemple@gmail.com]. Créons-lui un compte dans Thunderbird
pour lire le courrier qu'il recevra :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
349/755
• en [8] : Thunderbird a récupéré les informations suivantes dans sa base de données ;
• en [9] : le protocole de lecture du courrier n'est plus POP3 mais IMAP. La principale différence entre les deux est que [POP3]
ramène le courrier lu sur la machine locale où se trouve le lecteur de courrier et le supprime du serveur distant, alors que [IMAP]
conserve le courrier sur le serveur distant ;
• en [10] : identification du serveur SMTP ;
• en [13] : pour avoir davantage d'informations sur les serveurs IMAP et SMTP, on passe en configuration manuelle ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
350/755
• en [23-24] : le nouveau compte Thunderbird ;
• en [26] : on écrit un nouveau message ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
351/755
• en [32] : on relève le courrier des différents comptes ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
352/755
Nous avons désormais les outils pour explorer les protocoles SMTP, POP3 et IMAP. Nous commençons par le protocole SMTP.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
353/755
21.5.5 Le protocole SMTP
Nous allons découvrir le protocole SMTP en examinant les logs du serveur [hMailServer]. Pour cela, nous les activons avec l’outl
[hmailServerAdministrator] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
354/755
Dans l’exemple qui suit, le client sera [Thunderbird] et le serveur sera [hMailServer]. Avec Thunderbird, faites en sorte que
1. "SMTPD" 5828 22 "2020-07-07 10:02:54.263" "127.0.0.1" "SENT: 220 Bienvenue sur le serveur SMTP
localhost.com"
2. "SMTPD" 21956 22 "2020-07-07 10:02:54.360" "127.0.0.1" "RECEIVED: EHLO [127.0.0.1]"
3. "SMTPD" 21956 22 "2020-07-07 10:02:54.362" "127.0.0.1" "SENT: 250-DESKTOP-30FF5FB[nl]250-SIZE
20480000[nl]250-AUTH LOGIN[nl]250 HELP"
4. "SMTPD" 5828 22 "2020-07-07 10:02:54.381" "127.0.0.1" "RECEIVED: MAIL FROM:<guest@localhost.com>
SIZE=433"
5. "SMTPD" 5828 22 "2020-07-07 10:02:54.386" "127.0.0.1" "SENT: 250 OK"
6. "SMTPD" 21956 22 "2020-07-07 10:02:54.470" "127.0.0.1" "RECEIVED: RCPT TO:<guest@localhost.com>"
7. "SMTPD" 21956 22 "2020-07-07 10:02:54.473" "127.0.0.1" "SENT: 250 OK"
8. "SMTPD" 21956 22 "2020-07-07 10:02:54.478" "127.0.0.1" "RECEIVED: DATA"
9. "SMTPD" 21956 22 "2020-07-07 10:02:54.479" "127.0.0.1" "SENT: 354 OK, send."
10. "SMTPD" 21860 22 "2020-07-07 10:02:54.496" "127.0.0.1" "SENT: 250 Queued (0.016 seconds)"
11. "SMTPD" 21568 22 "2020-07-07 10:02:54.505" "127.0.0.1" "RECEIVED: QUIT"
12. "SMTPD" 21568 22 "2020-07-07 10:02:54.506" "127.0.0.1" "SENT: 221 goodbye"
Les lignes ci-dessus décrivent le dialogue qui a eu lieu entre le client SMTP (le gestionnaire de courrier Thunderbird) et le serveur
SMTP (hMailServer). Les lignes [SENT] indiquent ce que le serveur SMTP a envoyé à son client. Les lignes [RECEIVED] indiquent ce
que le serveur SMTP a reçu de son client.
• ligne 1 : juste après la connexion du client au serveur SMTP, celui-ci envoie le message de bienvenue à son client ;
• ligne 2 : le client envoie la commande [EHLO] pour d’identifier. Ici, il donne son adresse IP [127.0.0.1] qui désigne la machine
[localhost], ç-à-d la machine qui exécute le client SMTP ;
• ligne 3 : le serveur envoie une série de réponses [250]. [nl] signifie [newline] ç-à-d le caractère \n. Les réponses ont la forme
[250-] sauf la dernière qui a la forme [250 ]. C’est ainsi que le client SMTP sait que la réponse du serveur SMTP est terminée
et qu’il peut envoyer une commande. La série de commandes [250] avait pour but d’indiquer au client SMTP une série de
commandes qu’il pouvait utiliser ;
• ligne 4 : le client SMTP envoie la commande [MAIL FROM : adresse_mail_expéditeur] qui indique qui envoie le message ;
• ligne 5 : le serveur SMTP répond par [250 OK] indiquant qu’il a compris la commande ;
• ligne 6 : le client SMTP envoie la commande [RCPT TO : adresse_mail_destinataire] pour indiquer l’adresse du destinataire ;
• ligne 7 : de nouveau le serveur SMTP indique qu’il a compris la commande ;
• ligne 8 : le serveur SMTP envoie la commande [DATA]. Cela veut dire qu’il va envoyer le contenu du message ;
• ligne 9 : le serveur SMTP indique par la réponse [354 OK] qu’il est prêt à recevoir le message. Le texte [send .] indique que
le client SMTP doit terminer son message par une ligne ne contenant qu’un unique point ;
• ce qu’on ne vois pas ensuite, c’est que le client SMTP envoie son message. Les logs ne l’affichent pas ;
• ligne 10 : le client SMTP a envoyé le point qui indique la fin du message. Le serveur SMTP lui répond qu’il a mis le message
en file d’attente (queued) ;
• le client SMTP lui envoie la comande [QUIT] pour indiquer qu’il va fermer la connexion ;
• ligne 12 : le serveur lui répond ;
Maintenant que nous connaissons le dialogue client / serveur du protocole SMTP, essayons de le reproduire avec notre client
[RawTcpClient]. Nous utilisons un terminal PyCharm :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
355/755
Etudions un nouvel exemple :
• ligne [1], on se connecte sur le port 25 de la machine locale, là où opère le service SMTP de [hMailServer]. l’argument [-
-quit bye] indique que l’utilisateur quittera le programme en tapant la commande [bye]. Sans cet argument, la commande
de fin du programme est [quit]. Or [quit] est également une commande du protocole SMTP. Il nous faut donc éviter cette
ambiguïté ;
• ligne [2], le client est bien connecté ;
• ligne [3], le client attend des commandes tapées au clavier ;
• ligne [4], le serveur lui envoie son message de bienvenue ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
356/755
3. Tapez vos commandes (quit pour arrêter) :
4. <-- [220 Bienvenue sur le serveur SMTP localhost.com]
5. EHLO localhost
6. <-- [250-DESKTOP-30FF5FB]
7. <-- [250-SIZE 20480000]
8. <-- [250-AUTH LOGIN]
9. <-- [250 HELP]
10. MAIL FROM: guest@localhost.com
11. <-- [250 OK]
12. RCPT TO: guest@localhost.com
13. <-- [250 OK]
14. DATA
15. <-- [354 OK, send.]
16. from: guest@localhost.com
17. to: guest@localhost.com
18. subject: ceci est un test
19.
20. ligne1
21. ligne2
22. .
23. <-- [250 Queued (37.824 seconds)]
24. QUIT
25. Fin de la connexion avec le serveur
• en [5], le client envoie la commande [EHLO nom-de-la-machine-client]. Le serveur lui répond par une suite de messages de
la forme [250-xx] (6). Le code [250] indique le succès de la commande envoyée par le client ;
• en [10], le client indique l’expéditeur du message, ici [guest@localhost.com] ;
• en [11], la réponse du serveur ;
• en [12], on indique le destinataire du message, ici l’utilisateur [guest@localhost.com] ;
• en [13], la réponse du serveur ;
• en [14], la commande [DATA] indique au serveur que le client va envoyer le contenu du message ;
• en [15], la réponse du serveur ;
• en [16-22], le client doit envoyer une liste de lignes de texte terminée par une ligne ne contenant qu’un unique point. Le
message peut contenir des lignes [Subject:, From:, To:] (16-18) pour définir respectivement le sujet du message, l’expéditeur,
le destinataire ;
• en [19], les entêtes précédents doivent être suivis d’une ligne vide ;
• en [20-21], le texte du message ;
• en [22], la ligne ne contenant qu’un unique point qui indique la fin du message ;
• en [23], une fois que le serveur a reçu la ligne ne contenant qu’un unique point, il met le message en file d’attente ;
• en [24], le client indique au serveur qu’il a fini ;
• en [25], on constate que le serveur a fermé la connexion qui le liait au client ;
Maintenant vérifions avec Thunderbird que l’utilisateur [guest@localhost.com] a bien reçu le message :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
357/755
Finalement, notre client [RawTcpClient] a réussi à envoyer un message via le serveur SMTP [localhost]. Maintenant, utilisons la
même méthode pour envoyer un message à [pymailparlexemple@gmail.com] :
• ligne 1 : on utilise le serveur SMTP de Gmail qui opère sur le port 587 ;
• ligne 15 : on est bloqués parce que le serveur SMTP nous demande de démarrer une connexion sécurisée ce qu’on ne sait pas
faire. Contrairement à l’exemple précédent, le serveur [smtp.gmail.com] (ligne 1) demande une authentification. Il n’accepte
comme clients que les utilisateurs enregistrés dans le domaine [gmail.com]. Cette authentification est sécurisée et a lieu au
sein d’une connexion cryptée.
Le premier exemple nous a donné les bases pour construire un client SMTP basique en Python. Le deuxième nous a montré que
certains serveurs SMTP (la plupart en fait) nécessitent une authentification faite avec une connexion chiffrée.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
358/755
22. },
23. {
24. "description": "mail to gmail via gmail",
25. "smtp-server": "smtp.gmail.com",
26. "smtp-port": "587",
27. "from": "pymailparlexemple@gmail.com",
28. "to": "pymailparlexemple@gmail.com",
29. "subject": "to gmail via gmail",
30. # on envoie de l'UTF-8
31. "Content-type": 'text/plain; charset="utf-8"',
32. # on teste les caractères accentués
33. "message": "aglaë séléné\nva au marché\nacheter des fleurs"
34. }
35. ]
36. }
• lignes 10-35 : une liste de mails à envoyer. Pour chacun d’eux on précise les informations suivantes :
o [description] : un texte décrivant le mail ;
o [smtp-server] : le serveur SMTP à utiliser ;
o [smtp-port] : son port de service ;
o [from] : l’expéditeur du mail ;
o [to] : le destinataire du mail ;
o [subject] : le sujet du mail ;
o [content-type] : l’encodage du mail ;
o [message] : le message du mail ;
1. # imports
2. import socket
3.
4.
5. # -----------------------------------------------------------------------
6. def sendmail(mail: dict, verbose: bool):
7. # envoie message au serveur smtp smtpserver de la part de expéditeur
8. # pour destinataire. Si verbose=True, fait un suivi des échanges client-serveur
9.
10. # on laisse remonter les erreurs système
11. connexion = None
12. try:
13. # nom de la machine locale (nécessaire au protocole SMTP)
14. client = socket.gethostbyaddr(socket.gethostbyname("localhost"))[0]
15. # ouverture d'une connexion sur le port 25 de smtpServer
16. connexion = socket.create_connection((mail["smtp-server"], 25))
17.
18. # connexion représente un flux de communication bidirectionnel
19. # entre le client (ce programme) et le serveur smtp contacté
20. # ce canal est utilisé pour les échanges de commandes et d'informations
21.
22. # après la connexion le serveur envoie un message de bienvenue qu'on lit
23. send_command(connexion, "", verbose, True)
24. # cmde ehlo:
25. send_command(connexion, f"EHLO {client}", verbose, True)
26. # cmde mail from:
27. send_command(connexion, f"MAIL FROM: <{mail['from']}>", verbose, True)
28. # cmde rcpt to:
29. send_command(connexion, f"RCPT TO: <{mail['to']}>", verbose, True)
30. # cmde data
31. send_command(connexion, "DATA", verbose, True)
32. # préparation message à envoyer
33. # il doit contenir les lignes
34. # From: expéditeur
35. # To: destinataire
36. # ligne vide
37. # Message
38. # .
39. data = f"{mail['message']}"
40. # envoi message
41. send_command(connexion, data, verbose, False)
42. # envoi .
43. send_command(connexion, "\r\n.\r\n", verbose, False)
44. # cmde quit
45. send_command(connexion, "QUIT", verbose, True)
46. # fin
47. finally:
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
359/755
48. # fermeture connexion
49. if connexion:
50. connexion.close()
51.
52.
53. # --------------------------------------------------------------------------
54. def send_command(connexion: socket, commande: str, verbose: bool, with_rclf: bool):
55. # envoie commande dans le canal connexion
56. # mode verbeux si verbose=True
57. # si with_rclf=True, ajoute la séquence rclf à commande
58.
59. # données
60. rclf = "\r\n" if with_rclf else ""
61. # envoi cmde si commande non vide
62. if commande:
63. # on laisse remonter les erreurs système
64. #
65. # envoi commande
66. connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
67. # écho éventuel
68. if verbose:
69. affiche(commande, 1)
70. # lecture réponse de moins de 1000 caractères
71. reponse = str(connexion.recv(1000), 'utf-8')
72. # écho éventuel
73. if verbose:
74. affiche(reponse, 2)
75. # récupération code erreur
76. codeErreur = int(reponse[0:3])
77. # erreur renvoyée par le serveur ?
78. if codeErreur >= 500:
79. # on lance une exception avec l'erreur
80. raise BaseException(reponse[4:])
81. # retour sans erreur
82.
83.
84. # --------------------------------------------------------------------------
85. def affiche(echange: str, sens: int):
86. # affiche échange ? l'écran
87. # si sens=1 affiche -->echange
88. # si sens=2 affiche <-- échange sans les 2 derniers caractères rclf
89. if sens == 1:
90. print(f"--> [{echange}]")
91. return
92. elif sens == 2:
93. l = len(echange)
94. print(f"<-- [{echange[0:l - 2]}]")
95. return
96.
97.
98. # main ----------------------------------------------------------------
99.
100. # client SMTP (SendMail Transfer Protocol) permettant d'envoyer un message
101. # les infos sont prises dans un fichier config contenant les informations suivantes pour chaque serveur
102.
103. # description : description du mail envoyé
104. # smtp-server : serveur SMTP
105. # smtp-port : port du serveur SMTP
106. # from : expéditeur
107. # to : destinataire
108. # subject : sujet du mail
109. # message : message du mail
110.
111.
112. # protocole de communication SMTP client-serveur
113. # -> client se connecte sur le port 25 du serveur smtp
114. # <- serveur lui envoie un message de bienvenue
115. # -> client envoie la commande EHLO: nom de sa machine
116. # <- serveur répond OK ou non
117. # -> client envoie la commande mail from: <exp?diteur>
118. # <- serveur répond OK ou non
119. # -> client envoie la commande rcpt to: <destinataire>
120. # <- serveur répond OK ou non
121. # -> client envoie la commande data
122. # <- serveur répond OK ou non
123. # -> client envoie ttes les lignes de son message et termine avec une ligne contenant le seul caractère .
124. # <- serveur répond OK ou non
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
360/755
125. # -> client envoie la commande quit
126. # <- serveur répond OK ou non
127.
128. # les réponses du serveur ont la forme xxx texte où xxx est un nombre à 3 chiffres. Tout nombre xxx >=500
129. # signale une erreur. La réponse peut comporter plusieurs lignes commençant toutes par xxx- sauf la
dernière
130. # de la forme xxx(espace)
131.
132. # les lignes de texte échangées doivent se terminer par les caractéres RC(#13) et LF(#10)
133.
134. # configuration de l'application
135. import config
136. config = config.configure()
137.
138. # on traite les mails un par un
139. for mail in config['mails']:
140. try:
141. # logs
142. print("----------------------------------")
143. print(f"Envoi du message [{mail['description']}]")
144. # préparation du message à envoyer
145. mail[
146. "message"] = f"From: {mail['from']}\nTo: {mail['to']}\n" \
147. f"Subject: {mail['subject']}\n" \
148. f"Content-type: {mail['content-type']}" \
149. f"\n\n{mail['message']}"
150. # envoi du message en mode verbeux
151. sendmail(mail, True)
152. # fin
153. print("Message envoyé...")
154. except BaseException as erreur:
155. # on affiche l'erreur
156. print(f"L'erreur suivante s'est produite : {erreur}")
157. finally:
158. pass
159. # mail suivant
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
361/755
o [with_rclf] : si TRUE, envoie la commande terminée par la séquence \r\n. C’est nécessaire pour toutes les commandes
du protocole SMTP, mais [send_command] sert aussi à envoyer le message. Là on n’ajoute pas la séquence \r\n ;
• ligne 62 : la commande n'est envoyée que si elle est non vide ;
• lignes 65-66 : la commande est envoyée au serveur sous la forme d’une chaîne d’octets UTF-8 ;
• lignes 70-71 : lecture de de l’ensemble des lignes de la réponse. On suppose qu’elle fait moins de 1000 caractères. La réponse
peut comporter plusieurs lignes. Chaque ligne a la forme XXX-YYY où XXX est un code numérique sauf la dernière ligne
de la réponse qui a la forme XXX YYY (absence du caractère -) ;
• lignes 76 : lecture du code d'erreur XXX de la 1re ligne ;
• lignes 78-80 : si le code numérique XXX est supérieur à 500, alors le serveur a renvoyé une erreur. On lance alors une
exception ;
Résultats
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/inet/smtp/01/main.py
2. ----------------------------------
3. Envoi du message [mail to localhost via localhost]
4. --> [EHLO DESKTOP-30FF5FB]
5. <-- [220 Bienvenue sur le serveur SMTP localhost.com]
6. --> [MAIL FROM: <guest@localhost.com>]
7. <-- [250-DESKTOP-30FF5FB
8. 250-SIZE 20480000
9. 250-AUTH LOGIN
10. 250 HELP]
11. --> [RCPT TO: <guest@localhost.com>]
12. <-- [250 OK]
13. --> [DATA]
14. <-- [250 OK]
15. --> [From: guest@localhost.com
16. To: guest@localhost.com
17. Subject: to localhost via localhost
18. Content-type: text/plain; charset="utf-8"
19.
20. aglaë séléné
21. va au marché
22. acheter des fleurs]
23. <-- [354 OK, send.]
24. --> [
25. .
26. ]
27. <-- [250 Queued (0.000 seconds)]
28. --> [QUIT]
29. <-- [221 goodbye]
30. Message envoyé...
31. ----------------------------------
32. Envoi du message [mail to gmail via gmail]
33. --> [EHLO DESKTOP-30FF5FB]
34. <-- [220 smtp.gmail.com ESMTP u1sm1364433wrb.78 - gsmtp]
35. --> [MAIL FROM: <pymailparlexemple@gmail.com>]
36. <-- [250-smtp.gmail.com at your service, [2a01:cb05:80e8:b500:3c4b:2203:91fa:9b00]
37. 250-SIZE 35882577
38. 250-8BITMIME
39. 250-STARTTLS
40. 250-ENHANCEDSTATUSCODES
41. 250-PIPELINING
42. 250-CHUNKING
43. 250 SMTPUTF8]
44. --> [RCPT TO: <pymailparlexemple@gmail.com>]
45. <-- [530 5.7.0 Must issue a STARTTLS command first. u1sm1364433wrb.78 - gsmtp]
46. L'erreur suivante s'est produite : 5.7.0 Must issue a STARTTLS command first. u1sm1364433wrb.78 - gsmtp
47.
48.
49. Process finished with exit code 0
• lignes 3-30 : l’utilisation du serveur SMTP [hMailServer] pour envoyer un mail à [guest@localhost] se passe bien ;
• lignes 32-46 : l’utilisation du serveur SMTP [smtp.gmail.com] pour envoyer un mail à [pymailparlexemple@gmail.com] ne se
passe pas bien : en ligne 45, le serveur SMTP envoie un code d’erreur 530 avec un message d’erreur. Celui-ci indique que le
client SMTP doit au préalable s’authentifier via une connexion sécurisée. Notre client ne l’a pas fait et est donc refusé ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
362/755
21.5.7 scripts [smtp/02] : un lient SMTP écrit avec la bibliothèque [smtplib]
Nous allons traiter la première insuffisance dans le script [smtp/02]. Dans notre nouveau script nous allons utiliser le module Python
[smtplib].
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
363/755
19. "message": "aglaë séléné\nva au marché\nacheter des fleurs",
20. },
21. {
22. "description": "mail to gmail via gmail avec smtplib",
23. "smtp-server": "smtp.gmail.com",
24. "smtp-port": "587",
25. "from": "pymail2parlexemple@gmail.com",
26. "to": "pymail2parlexemple@gmail.com",
27. "subject": "to gmail via gmail avec smtplib",
28. # on teste les caractères accentués
29. "message": "aglaë séléné\nva au marché\nacheter des fleurs",
30. # smtp avec authentification
31. "user": "pymail2parlexemple@gmail.com",
32. "password": "#6prIlh@1QZ3TG",
33. }
34. ]
35. }
On retrouve les mêmes rubriques que dans le fichier [smtp/01/config] avec deux rubriques supplémentaires lorsque el serveur
SMTP demande une authentification :
Ces deux rubriques ne sont présentes que si le serveur SMTP contacté exige une authentification. Celle-ci se fait alors au-travers d'une
connexion sécurisée.
1. # imports
2. import smtplib
3. from email.mime.text import MIMEText
4. from email.utils import formatdate
5.
6.
7. # -----------------------------------------------------------------------
8. def sendmail(mail: dict, verbose: True):
9. # envoie message au serveur smtp smtpserver de la part de expéditeur
10. # pour destinataire. Si verbose=True, fait un suivi des échanges client-serveur
11.
12. # on utilise la bibliothéque smtplib
13. # on laisse remonter les exceptions
14. #
15. # le serveur SMTP
16. server = smtplib.SMTP(mail["smtp-server"])
17. # mode verbose
18. server.set_debuglevel(verbose)
19. # connexion sécurisée ?
20. if "user" in mail:
21. # connexion sécurisée
22. server.starttls()
23. # EHLO commande + authentification
24. server.login(mail["user"], mail["password"])
25.
26. # construction d'un message Multipart - c'est ce message qui Multipart sera envoyé
27. msg = MIMEText(mail["message"])
28. msg['from'] = mail["from"]
29. msg['to'] = mail["to"]
30. msg['date'] = formatdate(localtime=True)
31. msg['subject'] = mail["subject"]
32. # on envoie le message
33. server.send_message(msg)
34. # on quitte
35. server.quit()
36.
37.
38. # main ----------------------------------------------------------------
39.
40. # les infos sont prises dans un fichier config contenant les informations suivantes pour chaque serveur
41.
42. # description : description du mail envoyé
43. # smtp-server : serveur SMTP
44. # smtp-port : port du serveur SMTP
45. # from : expéditeur
46. # to : destinataire
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
364/755
47. # subject : sujet du mail
48. # content-type : encodage du mail
49. # message : message du mail
50.
51.
52. # configurationn de l'application
53. import config
54. config = config.configure()
55.
56. # on traite les mails un par un
57. for mail in config['mails']:
58. try:
59. # logs
60. print("----------------------------------")
61. print(f"Envoi du message [{mail['description']}]")
62. # envoi du message en mode verbeux
63. sendmail(mail, True)
64. # fin
65. print("Message envoyé...")
66. except BaseException as erreur:
67. # on affiche l'erreur
68. print(f"L'erreur suivante s'est produite : {erreur}")
69. finally:
70. pass
71. # mail suivant
Commentaires
• lignes 8-35 : seule la fonction [sendmail] est utilisée. Elle va désormais utiliser le module [smtplib] (ligne 2) ;
• ligne 16 : connexion au serveur SMTP ;
• ligne 18 : si [verbose=True], les échanges client / serveur seront affichés sur la console ;
• lignes 20-24 : on fait l'éventuelle authentification si le serveur SMTP l'exige ;
• ligne 22 : l'authentification se fait au travers d'une connexion sécurisée ;
• ligne 24 : authentification ;
• lignes 26-33 : envoi du message. Le dialogue vu avec le script [smtp/01/main] va alors se dérouler. S'il y a eu authentification,
il se déroulera au sein d'une connexion sécurisée ;
• ligne 35 : on termine le dialogue client / serveur ;
Avant d’exécuter le script [smtp/02/main], vous devez modifier la configuration du compte Gmail [pymailparlexemple@gmail.com] :
Résultats
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
365/755
Lorsqu’on exécute le script [smtp/02/main] on obtient les résultats console suivants :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/inet/smtp/02/main.py
2. ----------------------------------
3. Envoi du message [mail to localhost via localhost avec smtplib]
4. send: 'ehlo [192.168.43.163]\r\n'
5. reply: b'250-DESKTOP-30FF5FB\r\n'
6. reply: b'250-SIZE 20480000\r\n'
7. reply: b'250-AUTH LOGIN\r\n'
8. reply: b'250 HELP\r\n'
9. reply: retcode (250); Msg: b'DESKTOP-30FF5FB\nSIZE 20480000\nAUTH LOGIN\nHELP'
10. send: 'mail FROM:<guest@localhost.com> size=310\r\n'
11. reply: b'250 OK\r\n'
12. reply: retcode (250); Msg: b'OK'
13. send: 'rcpt TO:<guest@localhost.com>\r\n'
14. reply: b'250 OK\r\n'
15. reply: retcode (250); Msg: b'OK'
16. send: 'data\r\n'
17. reply: b'354 OK, send.\r\n'
18. reply: retcode (354); Msg: b'OK, send.'
19. data: (354, b'OK, send.')
20. send: b'Content-Type: text/plain; charset="utf-8"\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding:
base64\r\nfrom: guest@localhost.com\r\nto: guest@localhost.com\r\ndate: Wed, 08 Jul 2020 08:35:39
+0200\r\nsubject: to localhost via localhost avec
smtplib\r\n\r\nYWdsYcOrIHPDqWzDqW7DqQp2YSBhdSBtYXJjaMOpCmFjaGV0ZXIgZGVzIGZsZXVycw==\r\n.\r\n'
21. reply: b'250 Queued (0.000 seconds)\r\n'
22. reply: retcode (250); Msg: b'Queued (0.000 seconds)'
23. data: (250, b'Queued (0.000 seconds)')
24. send: 'quit\r\n'
25. reply: b'221 goodbye\r\n'
26. reply: retcode (221); Msg: b'goodbye'
27. Message envoyé...
28. ----------------------------------
29. Envoi du message [mail to gmail via gmail avec smtplib]
30. send: 'ehlo [192.168.43.163]\r\n'
31. reply: b'250-smtp.gmail.com at your service, [37.172.118.130]\r\n'
32. reply: b'250-SIZE 35882577\r\n'
33. reply: b'250-8BITMIME\r\n'
34. reply: b'250-STARTTLS\r\n'
35. reply: b'250-ENHANCEDSTATUSCODES\r\n'
36. reply: b'250-PIPELINING\r\n'
37. reply: b'250-CHUNKING\r\n'
38. reply: b'250 SMTPUTF8\r\n'
39. reply: retcode (250); Msg: b'smtp.gmail.com at your service, [37.172.118.130]\nSIZE
35882577\n8BITMIME\nSTARTTLS\nENHANCEDSTATUSCODES\nPIPELINING\nCHUNKING\nSMTPUTF8'
40. send: 'STARTTLS\r\n'
41. reply: b'220 2.0.0 Ready to start TLS\r\n'
42. reply: retcode (220); Msg: b'2.0.0 Ready to start TLS'
43. send: 'ehlo [192.168.43.163]\r\n'
44. reply: b'250-smtp.gmail.com at your service, [37.172.118.130]\r\n'
45. reply: b'250-SIZE 35882577\r\n'
46. reply: b'250-8BITMIME\r\n'
47. reply: b'250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH\r\n'
48. reply: b'250-ENHANCEDSTATUSCODES\r\n'
49. reply: b'250-PIPELINING\r\n'
50. reply: b'250-CHUNKING\r\n'
51. reply: b'250 SMTPUTF8\r\n'
52. reply: retcode (250); Msg: b'smtp.gmail.com at your service, [37.172.118.130]\nSIZE
35882577\n8BITMIME\nAUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER
XOAUTH\nENHANCEDSTATUSCODES\nPIPELINING\nCHUNKING\nSMTPUTF8'
53. send: 'AUTH PLAIN AHB5bWFpbDJwYXJsZXhlbXBsZUBnbWFpbC5jb20AIzZwcklsaEQmQDFRWjNURw==\r\n'
54. reply: b'235 2.7.0 Accepted\r\n'
55. reply: retcode (235); Msg: b'2.7.0 Accepted'
56. send: 'mail FROM:<pymail2parlexemple@gmail.com> size=320\r\n'
57. reply: b'250 2.1.0 OK e5sm4132618wrs.33 - gsmtp\r\n'
58. reply: retcode (250); Msg: b'2.1.0 OK e5sm4132618wrs.33 - gsmtp'
59. send: 'rcpt TO:<pymail2parlexemple@gmail.com>\r\n'
60. reply: b'250 2.1.5 OK e5sm4132618wrs.33 - gsmtp\r\n'
61. reply: retcode (250); Msg: b'2.1.5 OK e5sm4132618wrs.33 - gsmtp'
62. send: 'data\r\n'
63. reply: b'354 Go ahead e5sm4132618wrs.33 - gsmtp\r\n'
64. reply: retcode (354); Msg: b'Go ahead e5sm4132618wrs.33 - gsmtp'
65. data: (354, b'Go ahead e5sm4132618wrs.33 - gsmtp')
66. send: b'Content-Type: text/plain; charset="utf-8"\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding:
base64\r\nfrom: pymail2parlexemple@gmail.com\r\nto: pymail2parlexemple@gmail.com\r\ndate: Wed, 08 Jul 2020
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
366/755
08:35:40 +0200\r\nsubject: to gmail via gmail avec
smtplib\r\n\r\nYWdsYcOrIHPDqWzDqW7DqQp2YSBhdSBtYXJjaMOpCmFjaGV0ZXIgZGVzIGZsZXVycw==\r\n.\r\n'
67. reply: b'250 2.0.0 OK 1594190139 e5sm4132618wrs.33 - gsmtp\r\n'
68. reply: retcode (250); Msg: b'2.0.0 OK 1594190139 e5sm4132618wrs.33 - gsmtp'
69. data: (250, b'2.0.0 OK 1594190139 e5sm4132618wrs.33 - gsmtp')
70. send: 'quit\r\n'
71. Message envoyé...
72. reply: b'221 2.0.0 closing connection e5sm4132618wrs.33 - gsmtp\r\n'
73. reply: retcode (221); Msg: b'2.0.0 closing connection e5sm4132618wrs.33 - gsmtp'
74.
75. Process finished with exit code 0
• ligne 40 : le client [smtplib] commence le dialogue pour établir une liaison cryptée avec le serveur SMTP, ce qu’on n’avait pas
su faire dans le script [smtp/main/01] ;
• sinon on retouve les commandes connues du protocole SMTP ;
1. import os
2.
3.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
367/755
4. def configure() -> dict:
5. # configuration de l'application
6. script_dir = os.path.dirname(os.path.abspath(__file__))
7.
8. return {
9. # description : description du mail envoyé
10. # smtp-server : serveur SMTP
11. # smtp-port : port du serveur SMTP
12. # from : expéditeur
13. # to : destinataire
14. # subject : sujet du mail
15. # message : message du mail
16. "mails": [
17. {
18. "description": "mail to gmail via gmail avec smtplib",
19. "smtp-server": "smtp.gmail.com",
20. "smtp-port": "587",
21. "from": "pymail2parlexemple@gmail.com",
22. "to": "pymail2parlexemple@gmail.com",
23. "subject": "to gmail via gmail avec smtplib",
24. # on teste les caractères accentués
25. "message": "aglaë séléné\nva au marché\nacheter des fleurs",
26. # smtp avec authentification
27. "user": "pymail2parlexemple@gmail.com",
28. "password": "#6prIlhD&@1QZ3TG",
29. # ici, il faut mettre des chemins absolus pour les fichiers attachés
30. "attachments": [
31. f"{script_dir}/attachments/fichier attaché.docx",
32. f"{script_dir}/attachments/fichier attaché.pdf",
33. ]
34. }
35. ]
36. }
Le fichier [smtp/03/config] ne diffère du fichier [smtp/02/config] utilisé précédemment que par la présence facultative d'une liste
[attachments] (lignes 30-32) qui désigne la liste des fichiers à attacher au message à envoyer.
1. # imports
2. import email
3. import mimetypes
4. import os
5. import smtplib
6. from email import encoders
7. from email.mime.audio import MIMEAudio
8. from email.mime.base import MIMEBase
9. from email.mime.image import MIMEImage
10. from email.mime.message import MIMEMessage
11. from email.mime.multipart import MIMEMultipart
12. from email.mime.text import MIMEText
13. from email.utils import formatdate
14.
15.
16.
17. # -----------------------------------------------------------------------
18. def sendmail(mail: dict, verbose: True):
19. # envoie mail[message] au serveur smtp mail[smtp-server] de la part de mail[from]
20. # pour mail[to]. Si verbose=True, fait un suivi des échanges client-serveur
21.
22. # on utilise la bibliothéque smtplib
23. # on laisse remonter les exceptions
24. #
25. # le serveur SMTP
26. server = smtplib.SMTP(mail["smtp-server"])
27. # mode verbose
28. server.set_debuglevel(verbose)
29. # connexion sécurisée ?
30. if "user" in mail:
31. server.starttls()
32. server.login(mail["user"], mail["password"])
33.
34. # construction d'un message Multipart - c'est le message qui sera envoyé
35. # credit : https://docs.python.org/3.4/library/email-examples.html
36. msg = MIMEMultipart()
37. msg['From'] = mail["from"]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
368/755
38. msg['To'] = mail["to"]
39. msg['Date'] = formatdate(localtime=True)
40. msg['Subject'] = mail["subject"]
41. # on attache le message texte au format MIMEText
42. msg.attach(MIMEText(mail["message"]))
43. # on parcourt les attachements
44. for path in mail["attachments"]:
45. # path doit être un chemin absolu
46. # on devine le type du fichier attaché
47. ctype, encoding = mimetypes.guess_type(path)
48. # si on n'a pas deviné
49. if ctype is None or encoding is not None:
50. # No guess could be made, or the file is encoded (compressed), so
51. # use a generic bag-of-bits type.
52. ctype = 'application/octet-stream'
53. # on décompose le type en maintype/subtype
54. maintype, subtype = ctype.split('/', 1)
55. # on traite les différents cas
56. if maintype == 'text':
57. with open(path) as fp:
58. # Note: we should handle calculating the charset
59. part = MIMEText(fp.read(), _subtype=subtype)
60. elif maintype == 'image':
61. with open(path, 'rb') as fp:
62. part = MIMEImage(fp.read(), _subtype=subtype)
63. elif maintype == 'audio':
64. with open(path, 'rb') as fp:
65. part = MIMEAudio(fp.read(), _subtype=subtype)
66. # cas du type message / rfc822
67. elif maintype == 'message':
68. with open(path, 'rb') as fp:
69. part = MIMEMessage(email.message_from_bytes(fp.read()))
70. else:
71. # autres cas
72. with open(path, 'rb') as fp:
73. part = MIMEBase(maintype, subtype)
74. part.set_payload(fp.read())
75. # Encode the payload using Base64
76. encoders.encode_base64(part)
77. # Set the filename parameter
78. basename = os.path.basename(path)
79. part.add_header('Content-Disposition', 'attachment', filename=basename)
80. # on attache le fichier au message à envoyer
81. msg.attach(part)
82. # tous les attachements ont été faits - on envoie le message en tant que chaîne de caractères
83. server.send_message(msg)
84.
85.
86. # main ----------------------------------------------------------------
87.
88. ..
Commentaires
• lignes 18-32 : la fonction [sendmail] reste ce qu'elle était lorsqu'il n'y avait pas d'attachements ;
• ligne 35 : le code qui suit est tiré d'une documentation officielle de Python ;
• ligne 36 : le message qui va être envoyé va comprendre plusieurs parties : du texte et des fichiers attachés. On appelle cela
un message [Multipart] ;
• lignes 37-40 : on trouve dans le message [Multipart] les champs habituels de tout mail ;
• ligne 42 : les différentes parties du message [Multipart] [msg] sont attachées au message par la méthode [msg.attach]
(ligne 81). Les parties attachées peuvent être de toute nature. Celles-ci sont caractérisées par un type MIME. Le type MIME
d'un texte ordinaire est le type [MIMEText] ;
• lignes 44-81 : on va attacher au message [msg Multipart] tous les attachements du message à envoyer (ligne 81) ;
• ligne 44 : [path] représente le chemin absolu du fichier à attacher ;
• ligne 47 : pour trouver le type MIME à utiliser pour la partie à attacher, on va utiliser le suffixe (.docx, .php…) du fichier à
attacher. La méthode [mimetypes.guess_type] fait ce travail. Elle rend deux informations :
◦ [ctype] : le type MIME du fichier ;
◦ [encoding] : une information sur son encodage ;
• lignes 49-52 : au cas où on ne peut pas déterminer le type MIME du fichier, on dit que c'est un fichier binaire (ligne 52) ;
• ligne 54 : le type MIME d’un fichier se décompose en type principal / type secondaire, par exemple [application/pdf]. On
sépare ces deux éléments ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
369/755
• lignes 56-76 : on traite différents cas selon la valeur du type MIME principal. Par exemple, dans le cas [application/pdf]
d'un fichier PDF, on va exécuter les lignes 70-76 :
o lignes 56-59 : le cas où le fichier attaché est un fichier texte. Dans ce cas on crée un élément de type [MIMEText] de
contenu [fp.read] ;
o lignes 60-62 : le cas où le fichier contient une image. Dans ce cas on crée un élément de type [MIMEImage] de contenu
[fp.read] ;
o lignes 63-65 : le cas où le fichier est un fichier audio. Dans ce cas on crée un élément de type [MIMEAudio] de contenu
[fp.read] ;
o lignes 66-69 : le cas où le fichier est un mail. Dans ce cas on crée un élément de type [MIMEMessage] (ligne 69) de contenu
[email.message_from_bytes(fp.read())]. Contrairement aux cas précédents où le contenu de l’élément MIME était le
contenu binaire du fichier associé, ici le contenu de l’élément MIMEMessage est de type [email.message.Message] ;
o lignes 70-76 : les autres cas. Cela comprend par exemple les fichiers Word et PDF de notre exemple ;
• ligne 72 : le fichier à attacher est ouvert en mode binaire (rb=read binary) ;
• ligne 74 : [fp.read] lit la totalité du fichier binaire ;
• lignes 72-74 : la structure [with open(…) as file] fait deux choses :
◦ elle ouvre le fichier et lui donne le descripteur [file] ;
◦ elle assure qu'à la sortie du [with], erreur ou pas, le descripteur [file] sera fermé. C'est donc une alternative à la
structure [try file=open(…)/ finally] ;
• ligne 73 : on crée un nouvel élément [part] à incorporer au message Multipart. On utilise ici la classe [MIMEBase] et on
passe au constructeur les éléments [maintype, subtype] déterminés ligne 54 ;
• ligne 74 : l’élement à incorporer dans le message Multipart doit avoir un contenu. Celui-ci peut être initialisé avec la méthode
[set_payload] ;
• lignes 75-76 : les fichiers attachés doivent subir un encodage 7 bits. En effet, historiquement certains serveurs SMTP ne
supportaient que des caractères codés sur 7 bits. Ici c’est le codage appelé ‘Base64’ qui est utilisé ;
• ligne 77 : à partir de cette ligne, le traitement est comment à tous les types MIME que nous avons créés aux lignes 56-76
[MIMEMessage, MIMEImage, MIMEAudio, MIMEBase, MIMEText] ;
• ligne 79 : l’élément à ajouter dans le message Multipart a un entête le décrivant. On indique ici que l’élément ajouté
correspond à un fichier attaché. Le nom de ce fichier est le troisième paramètre passé à la méthode [add_header]. Le nom
de ce fichier est souvent utilisé par les lecteurs de courrier pour enregistrer, sous ce nom, le fichier attaché dans le système
de fichiers du lecteur. On a pour l’instant travaillé avec le nom absolu du fichie attaché. Ici on passe simplement son nom
sans son chemin (ligne 78) ;
• ligne 81 : le binaire du fichier est incorporé dans le message [msg Multipart] ;
• ligne 83 : lorsque tous les parties du message ont été attachées au [msg Multipart], celui-ci est envoyé ;
Résultats
Si on exécute le script [smtp/03/main] avec le fichier [smtp/02/config] déjà présenté, le compte [pymail2parlexemple@gmail.com]
reçoit ceci :
Montrons un exemple maintenant avec un mail attaché. Nous allons sauvegarder le mail reçu en [3] ci-dessus :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
370/755
Nous sauvegardons le mail sous le nom [mail attaché 1.eml] dans le dossier [smtp/03/attachments].
1. import os
2.
3.
4. def configure() -> dict:
5. # configuration de l'application
6. script_dir = os.path.dirname(os.path.abspath(__file__))
7.
8. return {
9. # description : description du mail envoyé
10. # smtp-server : serveur SMTP
11. # smtp-port : port du serveur SMTP
12. # from : expéditeur
13. # to : destinataire
14. # subject : sujet du mail
15. # message : message du mail
16. "mails": [
17. {
18. "description": "mail to gmail via gmail avec smtplib",
19. "smtp-server": "smtp.gmail.com",
20. "smtp-port": "587",
21. "from": "pymail2parlexemple@gmail.com",
22. "to": "pymail2parlexemple@gmail.com",
23. "subject": "to gmail via gmail avec smtplib",
24. # on teste les caractères accentués
25. "message": "aglaë séléné\nva au marché\nacheter des fleurs",
26. # smtp avec authentification
27. "user": "pymail2parlexemple@gmail.com",
28. "password": "#6prIlhD&@1QZ3TG",
29. # ici, il faut mettre des chemins absolus pour les fichiers attachés
30. "attachments": [
31. f"{script_dir}/attachments/fichier attaché.docx",
32. f"{script_dir}/attachments/fichier attaché.pdf",
33. f"{script_dir}/attachments/mail attaché 1.eml",
34. ]
35. }
36. ]
37. }
Maintenant nous exécutons de nouveau le script [smtp/03/main]. Cela donne le résultat suivant dans la boîte à lettres de l’utilisateur
[pymail2parlexemple@gmail.com] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
371/755
• en [1], le mail reçu ;
• en [2] : le texte du message ;
• en [3] : le texte du mail attaché ;
• en [4] : Thunderbird a trouvé 5 pièces jointes :
o [fichier attaché.docx] ;
o [fichier attaché.pdf] ;
o [mail attaché 1.eml]. Cette pièce jointe est elle-même un mail contenant deux pièces jointes :
▪ [fichier attaché.docx] ;
▪ [fichier attaché.pdf] ;
• le protocole POP3 (Post Office Protocol) historiquement le 1 er protocole mais peu utilisé maintenant ;
• le protocole IMAP (Internet Message Access Protocol) protocole plus récent que POP3 et le plus utilisé actuellement ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
372/755
◦ un script Python utilisant des modules Python permettant de gérer les pièces attachées ainsi que l'utilisation d’une
connexion chiffrée et authentifiée lorsque le serveur POP3 l'exige ;
Nous examinons maintenant les logs du serveur [hMailServer]. Pour cela nous utilisons l’outil d’administration [hMailServer
Administrator] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
373/755
Les logs POP3 sont les suivants (les dernières lignes dans le fichier de logs du jour) :
• ligne 1 : le serveur POP3 envoie un message de bienvenue au client (Thunderbird) qui vient de se connecter ;
• ligne 2 : le client envoie la commande [CAPA] (capabilities) pour demander la listes des commandes qu’il peut utiliser ;
• ligne 3 : le serveur lui répond qu’il peut utiliser les commandes [USER, UIDL, TOP]. Le serveur POP commence ses réponses
par [+OK] ou [-ERR] pour indiquer qu’il a réussi ou échoué à exécuter la commande du client ;
• ligne 4 : le client envoie la commande [USER guest] pour indiquer qu’il veut consulter la boîte à lettres de l’utilisateur [guest] ;
• ligne 5 : le serveur lui répond [+OK] et demande le mote de passe de [guest] ;
• ligne 6 : le client envoie la commande [PASS password] pour envoyer le mot de passe de l’utilisateur [guest]. Ici le mot de
passe est en clair car le serveur POP3 n’a pas imposé de connexion sécurisée. Nous verrons que ce sera différent avec le
serveur POP3 de Gmail ;
• ligne 7 : le serveur a validé l’ensemble login / mot de passe. Il indique qu’il bloque la boîte à lettres de l’utilisateur [guest] ;
• ligne 8 : le client lui envoie la commande [STAT] qui demande des informations sur la boîte à lettres ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
374/755
• ligne 9 : le serveur lui répond qu’il y a un message de 612 octets. De façon générale, il répond qu’il y a N messages et donne
la taille totale de ces messages ;
• ligne 10 : le client envoie la commande [LIST]. Cette commande demande la liste des messages ;
• ligne 11 : le serveur lui envoie la liste des messages sous la forme suivante :
o une ligne récapitulative avec le nombre de messages et leur taille totale ;
o une ligne par message indiquant le n° du message et sa taille ;
• ligne 13 : le client envoie la commande [UIDL] qui demande la liste des messages avec leurs identifiants. En effet, chaque
message est repéré par un n° unique au sein du service de mails ;
• ligne 14 : la réponse du serveur. On voit ainsi que le message n° 1 dans la liste a l’identifiant 42 ;
• ligne 15 : le client envoie la commande [RETR 1] qui demande à ce qu’on lui transfère le message n° 1 de la liste ;
• ligne 16 : le serveur POP3 le fait ;
• ligne 17 : le client envoie la commande [QUIT] pour indiquer qu’il va se déconnecter du serveur POP3 ;
• ligne 18 : le serveur va lui également fermer sa connexion avec le client mais auparavant il lui envoie un message d’au-revoir ;
Nous allons reproduire maintenant des éléments du dialogue ci-dessus en utilisant le client [RawTcpClient] exécuté dans une fenêtre
PyCharm :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
375/755
25. <-- [ Thunderbird/68.10.0]
26. <-- [MIME-Version: 1.0]
27. <-- [Content-Type: text/plain; charset=utf-8; format=flowed]
28. <-- [Content-Transfer-Encoding: 8bit]
29. <-- [Content-Language: fr]
30. <-- []
31. <-- [ceci est un test pour découvrir le protocole POP3]
32. <-- []
33. <-- [.]
34. QUIT
35. Fin de la connexion avec le serveur
• ligne 1 : on ouvre une connexion avec le port 110 de la machine [localhost]. C’est là qu’opère le service POP3 de
[hMailServer] ;
• aux lignes 5, 7, 9, 13, 34, nous utilisons les commandes [USER, PASS, LIST, RETR, QUIT] ;
• ligne 4 : le message de bienvenue du serveur POP3 ;
• ligne 5 : on indique qu’on veut accéder à la boîte à lettres de l’utilisateur [guest] ;
• ligne 7 : on envoie le mot de passe de l’utilisateur [guest] en clair ;
• ligne 9 : on demande la liste des messages de la boîte à lettres ;
• ligne 13 : on demande le message n° 1 ;
• lignes 14-33 : le serveur POP3 envoie le message n° 1 ;
• ligne 34 : on termine la session ;
• la commande [USER] sert à définir l’utilisateur dont on veut lire la boîte mail ;
• la commande [PASS] sert à définir son mot de passe ;
• la commande [LIST] demande la liste des messages présents dans la boîte à lettres de l’utilisateur ;
• la commande [RETR] demande à voir le message dont on passe le n° ;
• la commande [DELE] demande la suppression du message dont on passe le n° ;
• la commande [QUIT] indique au serveur qu’on a terminé ;
• une ligne unique commençant par [+OK] pour indiquer que la commande précédente du client a réussi ;
• une ligne unique commençant par [-ERR] pour indiquer que la commande précédente du client a échoué ;
• plusieurs lignes où :
◦ la 1re ligne commence par [+OK] ;
◦ la dernière ligne est constituée d’un unique point ;
Comme le procole POP3 a la même structure que le protocole SMTP, le script [pop3/01/main.py] est un portage du script
[smtp/01/main.py]. Il aura le fichier de configuration [pop3/01/config.py] suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
376/755
7. # password : son mot de passe
8. # maxmails : le nombre maximum de mails à télécharger
9. # timeout : délai d'attente maximal d'une réponse du serveur
10. # encoding : encodage des mails reçus
11. # delete : si True, alors les mails sont supprimés de la boîte à lettres
12. # une fois qu'ils ont été téléchargés localement
13.
14. {
15. "server": "localhost",
16. "port": "110",
17. "user": "guest",
18. "password": "guest",
19. "maxmails": 10,
20. "timeout": 1.0,
21. "encoding": "utf-8",
22. "delete": False
23. }
24. ]
25. # on rend la configuration
26. return {
27. "mailboxes": mailboxes
28. }
• lignes 3-24 : la liste des boîtes à lettres à consulter. Ici il n’y en a qu’une ;
• lignes 4-12 : significations des éléments du dictionnaire définissant chacune des boîtes à lettres ;
• ligne 15 : le serveur POP3 interrogé est le serveur local [hMailServer] ;
• lignes 17-18 : on veut lire la boîte à lettres de l’utilisateur [guest@localhost] ;
• ligne 19 : on lira au plus 10 mails ;
• ligne 20 : le client aura un délai d'attente d'une réponse du serveur d’au plus 1 seconde ;
• ligne 21 : le type d'encodage des messages lus ;
• ligne 22 : on ne supprimera pas les messages téléchargés ;
1. # imports
2. import re
3. import socket
4.
5.
6. # -----------------------------------------------------------------------
7. def readmails(mailbox: dict, verbose: bool):
8. # lit la boîte mail décrite par le dictionnaire [mailbox]
9. # si verbose=True, fait un suivi des échanges client-serveur
10. …
11.
12.
13. # --------------------------------------------------------------------------
14. def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
15. # envoie commande dans le canal connexion
16. # mode verbeux si verbose=True
17. # si with_rclf=True, ajoute la séquence rclf à échange
18. # rend la 1ère ligne de la réponse
19. …
20.
21.
22. # --------------------------------------------------------------------------
23. def affiche(echange: str, sens: int):
24. …
25.
26.
27. # main ----------------------------------------------------------------
28.
29. # client POP3 (Post Office Protocol) permettant de lire des messages d'une boîte à lettres
30. # protocole de communication POP3 client-serveur
31. # -> client se connecte sur le port 110 du serveur smtp
32. # <- serveur lui envoie un message de bienvenue
33. # -> client envoie la commande USER utilisateur
34. # <- serveur répond OK ou non
35. # -> client envoie la commande PASS mot_de_passe
36. # <- serveur répond OK ou non
37. # -> client envoie la commande LIST
38. # <- serveur répond OK ou non
39. # -> client envoie la commande RETR n° pour chacun des mails
40. # <- serveur répond OK ou non. Si OK envoie le contenu du mail demandé
41. # -> serveur envoie ttes les lignes du mail et termine avec une ligne contenant le
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
377/755
42. # seul caractère .
43. # -> client envoie la commande DELE n° pour supprimer un mail
44. # <- serveur répond OK ou non
45. # # -> client envoie la commande QUIT pour terminer le dialogue avec le serveur
46. # <- serveur répond OK ou non
47. # les réponses du serveur ont la forme +OK texte où -ERR texte
48. # La réponse peut comporter plusieurs lignes. Alors la dernière est constituée d'un unique point
49. # les lignes de texte échangées doivent se terminer par les caractères RC(#13) et LF(#10)
50. #
51.
52. # on récupère la configuration de l'application
53. import config
54. config = config.configure()
55.
56. # on traite les boîtes mail une par une
57. for mailbox in config['mailboxes']:
58. try:
59. # affichage console
60. print("----------------------------------")
61. print(
62. f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
63. # lecture de la boîte mail en mode verbeux
64. readmails(mailbox, True)
65. # fin
66. print("Lecture terminée...")
67. except BaseException as erreur:
68. # on affiche l'erreur
69. print(f"L'erreur suivante s'est produite : {erreur}")
70. finally:
71. pass
Commentaires
Comme nous l’avons dit, [pop3/01/main.py] est un portage du script [smtp/01/main.py] que nous avons déjà commenté. Nous ne
commenterons que les principales différences :
• ligne 64 : la fonction [readmails] est chargée de lire les mails d'une boîte aux lettres. Les informations pour se connecter à
cette boîte à lettres sont dans le dictionnaire [mailbox]. Le second paramètre [True] est le paramètre [Verbose] qui demande
ici un suivi des échanges client / serveur ;
1. # -----------------------------------------------------------------------
2. def readmails(mailbox: dict, verbose: bool):
3. # lit les mails de la boîte mail décrite par le dictionnaire [mailbox]
4. # si verbose=True, fait un suivi des échanges client-serveur
5.
6. # on isole les paramètres de la boîte mail
7. # on suppose que le dictionnaire [mailbox] est valide
8. server = mailbox['server']
9. port = int(mailbox['port'])
10. user = mailbox['user']
11. password = mailbox['password']
12. maxmails = mailbox['maxmails']
13. delete = mailbox['delete']
14. timeout = mailbox['timeout']
15.
16. # on laisse remonter les erreurs système
17. connexion = None
18. try:
19. # ouverture d'une connexion sur le port [port] de [server] avec un timeout d'une seconde
20. connexion = socket.create_connection((server, port), timeout=timeout)
21.
22. # connexion représente un flux de communication bidirectionnel
23. # entre le client (ce programme) et le serveur pop3 contacté
24. # ce canal est utilisé pour les échanges de commandes et d'informations
25.
26. # lecture msg de bienvenue
27. send_command(mailbox, connexion, "", verbose, True)
28. # cmde USER
29. send_command(mailbox, connexion, f"USER {user}", verbose, True)
30. # cmde PASS
31. send_command(mailbox, connexion, f"PASS {password}", verbose, True)
32. # cmde LIST
33. première_ligne = send_command(mailbox, connexion, "LIST", verbose, True)
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
378/755
34. # analyse de la 1ère ligne pour connaître le nbre de messages
35. match = re.match(r"^\+OK (\d+)", première_ligne)
36. nbmessages = int(match.groups()[0])
37. # on boucle sur les messages
38. imessage = 0
39. while imessage < nbmessages and imessage < maxmails:
40. # cmde RETR
41. send_command(mailbox, connexion, f"RETR {imessage + 1}", verbose, True)
42. # cmde DELE
43. if delete:
44. send_command(mailbox, connexion, f"DELE {imessage + 1}", verbose, True)
45. # msg suivant
46. imessage += 1
47. # cmde QUIT
48. send_command(mailbox, connexion, "QUIT", verbose, True)
49. # fin
50. finally:
51. # fermeture connexion
52. if connexion:
53. connexion.close()
Commentaires
1. # --------------------------------------------------------------------------
2. def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
3. # envoie commande dans le canal connexion
4. # mode verbeux si verbose=True
5. # si with_rclf=True, ajoute la séquence rclf à échange
6. # rend la 1ère ligne de la réponse
7.
8. # marque de fin de ligne
9. if with_rclf:
10. rclf = "\r\n"
11. else:
12. rclf = ""
13. # envoi commande si non vide
14. if commande:
15. connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
16. # écho éventuel
17. if verbose:
18. affiche(commande, 1)
19. # lecture de la socket comme si elle était un fichier texte
20. encoding = f"{mailbox['encoding']}" if mailbox['encoding'] else None
21. file = connexion.makefile(encoding=encoding)
22. # on exploite ce fichier ligne par ligne
23. # lecture 1ère ligne
24. première_ligne = réponse = file.readline().strip()
25. # mode verbeux ?
26. if verbose:
27. affiche(première_ligne, 2)
28. # récupération code erreur
29. code_erreur = réponse[0]
30. if code_erreur == "-":
31. # il y a eu une erreur
32. raise BaseException(réponse[5:])
33. # cas particulier des réponses à plusieurs lignes LIST, RETR
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
379/755
34. cmd = commande.lower()[0:4]
35. if cmd == "list" or cmd == "retr":
36. # dernière ligne de la réponse ?
37. dernière_ligne = False
38. while not dernière_ligne:
39. # lecture ligne suivante
40. ligne_suivante = file.readline().strip()
41. # mode verbeux ?
42. if verbose:
43. affiche(ligne_suivante, 2)
44. # dernière ligne ?
45. dernière_ligne = ligne_suivante == "."
46. # fini - on rend la 1ère ligne
47. return première_ligne
Commentaires
• lignes 13-18 : la commande [command] n'est envoyée au serveur POP3 que si elle est non vide. Ce cas est nécessaire pour lire
le message de bienvenue du serveur POP3 qu'il envoie alors même que le client n'a pas encore envoyé de commandes ;
• lignes 19-21 : on lit la socket comme si elle était un fichier texte. Cela va nous permettre d'utiliser la méthode [readline] (ligne
24) et de lire ainsi le message ligne par ligne. On utilise la clé [encoding] du dictionnaire [mailbox] pour indiquer le codage
des lignes qui vont être lues ;
• ligne 24 : on lit la 1re ligne de la réponse ;
• lignes 28-32 : on gère le cas d'une éventuelle erreur. Celles-ci sont du type [-ERR invalid password, -ERR mailbox unknown,
-ERR unable to lock mailbox…] ;
• ligne 32 : on lance une exception avec le message de l'erreur ;
• ligne 35 : seules les commandes [list, retr] peuvent avoir des réponses à plusieurs lignes ;
• lignes 36-45 : dans le cas d'une réponse à plusieurs lignes, on affiche toutes les lignes reçues (lignes 42-43) jusqu'à recevoir la
dernière ligne (ligne 45) ;
• ligne 46 : on rend la 1re ligne lue car dans le cas de la commande [LIST], elle comporte le nombre de messages présents dans
la boîte à lettres ;
Résultats
Prenons l'exemple précédent. Avec Thunderbird, nous avions envoyé le message suivant à l'utilisateur [guest@localhost] (il faut que
le serveur hMailServer soit lancé) :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/inet/pop3/01/main.py
2. ----------------------------------
3. Lecture de la boîte mail POP3 guest@localhost:110
4. <-- [+OK Bienvenue sur le serveur POP3 localhost.com]
5. --> [USER guest]
6. <-- [+OK Send your password]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
380/755
7. --> [PASS guest]
8. <-- [+OK Mailbox locked and ready]
9. --> [LIST]
10. <-- [+OK 1 messages (612 octets)]
11. <-- [1 612]
12. <-- [.]
13. --> [RETR 1]
14. <-- [+OK 612 octets]
15. <-- [Return-Path: guest@localhost.com]
16. <-- [Received: from [127.0.0.1] (DESKTOP-30FF5FB [127.0.0.1])]
17. <-- [by DESKTOP-30FF5FB with ESMTP]
18. <-- [; Wed, 8 Jul 2020 14:19:36 +0200]
19. <-- [To: guest@localhost.com]
20. <-- [From: "guest@localhost.com" <guest@localhost.com>]
21. <-- [Subject: protocole POP3]
22. <-- [Message-ID: <ca895136-25c5-411e-373a-a68cbd0eca51@localhost.com>]
23. <-- [Date: Wed, 8 Jul 2020 14:19:33 +0200]
24. <-- [User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101]
25. <-- [Thunderbird/68.10.0]
26. <-- [MIME-Version: 1.0]
27. <-- [Content-Type: text/plain; charset=utf-8; format=flowed]
28. <-- [Content-Transfer-Encoding: 8bit]
29. <-- [Content-Language: fr]
30. <-- []
31. <-- [ceci est un test pour découvrir le protocole POP3]
32. <-- []
33. <-- [.]
34. --> [QUIT]
35. <-- [+OK POP3 server saying goodbye...]
36. Lecture terminée...
37.
38. Process finished with exit code 0
Nous allons implémenter ces deux possibilités avec un nouveau script qui sera cette fois plus complexe.
21.6.4 scripts [pop3/02] : client POP3 avec les modules [poplib] et [email]
Nous allons écrire un client POP3 permettant de gérer les pièces attachées ainsi que la communication avec des serveurs sécurisés.
Par ailleurs, nous sauvegarderons dans des fichiers, les messages et leurs pièces attachées.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
381/755
Le script [inet/pop3/02/main] [1] est configuré par le fichier [inet/pop3/02/config] [2] et utilise le module
[inet/shared/mail_parser] [3].
1. import os
2.
3.
4. def configure() -> dict:
5. # configuration de l'appli
6. config = {
7. # liste des boîtes à lettres à gérer
8. "mailboxes": [
9. # server : serveur POP3
10. # port : port du serveur POP3
11. # user : utilisateur dont on veut lire les messages
12. # password : son mot de passe
13. # maxmails : le nombre maximum de mail à télécharger
14. # timeout : délai d'attente maximal d'une réponse du serveur
15. # delete : à vrai s'il faut supprimer du serveur les messages téléchargés
16. # ssl : à vrai si la lecture des mails se fait au travers d'une liaison sécurisée
17. # output : le dossier de rangement des messages téléchargés
18.
19. {
20. "server": "pop.gmail.com",
21. "port": "995",
22. "user": "pymail2parlexemple@gmail.com",
23. "password": "#6prIlhD&@1QZ3TG",
24. "maxmails": 10,
25. "delete": False,
26. "ssl": True,
27. "timeout": 2.0,
28. "output": "output"
29. }
30. ]
31. }
32. # chemin absolu du dossier du script
33. script_dir = os.path.dirname(os.path.abspath(__file__))
34.
35. # chemins absolus des dossiers à inclure dans le syspath
36. absolute_dependencies = [
37. # dossier local
38. f"{script_dir}/../../shared",
39. ]
40.
41. # configuration du syspath
42. from myutils import set_syspath
43. set_syspath(absolute_dependencies)
44.
45. # on rend la configuration
46. return config
Le fichier définit la liste des boîtes à lettres à consulter et fixe le Python Path de l’application.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
382/755
Il n’y a ici qu’une unique boîte à lettres :
1. # imports
2. import email
3. import os
4. import poplib
5. import shutil
6.
7.
8. # lecture d'une boîte mail
9. def readmails(mailbox: dict, verbose: bool):
10. # lit la boîte mail décrite par le dictionnaire [mailbox]
11. # si verbose=True, fait un suivi des échanges client-serveur
12. …
13.
14. # main ----------------------------------------------------------------
15. # client POP3 (Post Office Protocol) permettant de lire des mails
16.
17. # on récupère la configuration de l'application
18. import config
19. config = config.configure()
20.
21. # on traite les boîtes mail une par une
22. for mailbox in config['mailboxes']:
23. try:
24. # affichage console
25. print("----------------------------------")
26. print(
27. f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
28. # lecture de la boîte mail en mode verbeux
29. readmails(mailbox, True)
30. # fin
31. print("Lecture terminée...")
32. except BaseException as erreur:
33. # on affiche l'erreur
34. print(f"L'erreur suivante s'est produite : {erreur}")
35. finally:
36. pass
• lignes 17-36 : la partie [main] du script est analogue à celle du script [pop3/01] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
383/755
19.
20. # on laisse remonter les erreurs système
21. pop3 = None
22. try:
23. # on crée les dossiers de stockage s'ils n'existent pas
24. if not os.path.isdir(output):
25. os.mkdir(output)
26. # user
27. dir2 = f"{output}/{user}"
28. # on supprime le dossier [dir2] s'il existe puis on le recrée
29. if os.path.isdir(dir2):
30. # suppression
31. shutil.rmtree(dir2)
32. # création
33. os.mkdir(dir2)
34. # ouverture d'une connexion sur le port [port] de [server]
35. if ssl:
36. pop3 = poplib.POP3_SSL(server, port, timeout=timeout)
37. else:
38. pop3 = poplib.POP3(server, port, timeout=timeout)
39.
40. # connexion représente un flux de communication bidirectionnel
41. # entre le client (ce programme) et le serveur pop3 contacté
42. # ce canal est utilisé pour les échanges de commandes et d'informations
43.
44. # mode verbose
45. pop3.set_debuglevel(2 if verbose else 0)
46. # lecture msg de bienvenue
47. pop3.getwelcome( )
48. # cmde USER
49. réponse = pop3.user(user)
50. # cmde PASS
51. réponse = pop3.pass_(password)
52. # cmde LIST
53. liste = pop3.list()
54. # les mails sont dans liste[1]
55. imail = 0
56. nb_mails = len(liste[1])
57. fini = imail == maxmails or imail == nb_mails
58. éléments = liste[1]
59. while not fini:
60. # élément courant
61. élément = éléments[imail]
62. # élément est une liste d'octets qu'on décode en string
63. desc = élément.decode()
64. # on a une chaîne séparée par des blancs
65. # le 1er élément est le n° du message
66. num = desc.split()[0]
67. # on récupère le message
68. message = pop3.retr(int(num))
69. # les lignes du message sont dans message [1]
70. str_message = ""
71. for ligne in message[1]:
72. # ligne est une suite d'octets qu'on décode en string
73. str_message += f"{ligne.decode()}\r\n"
74. # dossier du message
75. dir3 = f"{dir2}/message_{num}"
76. # si le dossier n'existe pas, on le crée
77. if not os.path.isdir(dir3):
78. os.mkdir(dir3)
79. # objet email.message.Message
80. save_message(dir3, email.message_from_string(str_message), 0)
81. # un mail de +
82. imail += 1
83. # a-t-on atteint le max ?
84. fini = imail == maxmails or imail == nb_mails
85.
86. # cmde QUIT
87. pop3.quit()
88. finally:
89. # fermeture connexion
90. if pop3:
91. pop3.close()
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
384/755
• lignes 6-7 : on importe la fonction [mail_parser.save_message] utilisée ligne 80 ;
• le code de la fonction est encapsulée dans un try (ligne 22)/ finally (ligne 88). Ainsi toutes les exceptions remontent au code
principal qui les arrêtent et les affichent ;
• lignes 11-18 : on récupère les informations de configuration de la boîte à lettres ;
• lignes 23-33 : tous les messages seront stockés dans le dossier [output/user] où [output] et [user] sont définis dans la
configuration. On crée donc successivement les dossiers [output] puis [output/user]. Pour créer ce dernier, on le supprime
d'abord ligne 31. [shutil] est un module qu'il faut importer. [shutil.rmtree(dir)] supprime le dossier [dir] et tout ce qu'il
contient ;
• pour toutes les opérations sur les fichiers système on utilise le module [os] qu'il faut également importer ;
• lignes 34-38 : on ouvre une connexion avec le serveur POP3. Si le serveur est sécurisé, on utilise la classe [poplib.POP3_SSL]
sinon la classe [poplib.POP3]. L'attribut [ssl] utilisé ligne 35 provient de la configuration de la boîte à lettres ;
• ligne 45 : on fixe un niveau de logs :
o 0 : pas de logs ;
o 1 : les commandes émises par le client POP3 sont loguées ;
o 2 : logs détaillés. On voit également ce que reçoit le client POP3 ;
• ligne 47 : après la connexion, leserveur POP3 envoie un message de bienvenue. On lit celui-ci ;
• lignes 48-49 : commande USER du protocole POP3 ;
• lignes 50-51 : commande PASS du protocole POP3 ;
• lignes 52-53 : commande LIST du protocole POP3. La réponse est un tuple (response, ['mesg_num octets'…], octets), par
exemple liste=(b'+OK 3 messages (3859 octets)', [b'1 584', b'2 550', b'3 2725'], 22). On voit que les deux premiers
éléments du tuple sont des bytes (préfixe b). liste[1] est un tableau où chaque élément est une suite d'octets contenant deux
informations : le n° du message et sa taille en octets ;
• ligne 56 : de ce qui précède on déduit que le nombre de messages dans la boîte à lettres peut être obtenu par [len[liste1]] ;
• lignes 59-84 : on boucle sur chacun des messages. On s'arrête lorsque tous ont été lus ou qu'on a atteint le nombre maximal
de mails fixé par configuration ;
• ligne 61 : élément courant du tableau liste[1], donc quelque chose comme b'1 584', une suite d'octets ;
• ligne 63 : on passe de la suite d'octets à une chaîne de caractères. On a maintenant la chaîne '1 584' ;
• ligne 66 : on récupère le n° du message, ici la chaîne '1' ;
• ligne 68 : on émet la commande POP3 RETR num. On récupère une réponse du genre :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
385/755
Le module [mail_parser] a été importé aux lignes 6-7 de la fonction [readmails] ;
1. # imports
2. import codecs
3. import email.contentmanager
4. import email.header
5. import email.iterators
6. import email.message
7. import os
8.
9.
10. # sauvegarde d'un message de type email.message.Message
11. # cette fonction peut être appelée de façon récursive
12. def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
13. # output : dossier de sauvegarde des messages
14. # email_message : le message à sauvegarder
15. # irfc822 : n° courant de la numérotation des mails attachés
16. #
17. # partie du message
18. part = email_message
19. # les entêtes [From, To, Subject] sont trouvés dans une des parties multipart
20. # ou bien dans une partie [text/*] lorsqu'il n'y a pas de partie [multipart]
21. keys = part.keys()
22. # From doit faire partie des entêtes, sinon la partie n'a pas les entêtes qu'on cherche
23. if "From" in keys:
24. # on récupère certains entêtes
25. headers = [f"From: {decode_header(part.get('From'))}",
26. f"To: {decode_header(part.get('To'))}",
27. f"Subject: {decode_header(part.get('Subject'))}",
28. f"Return-Path: {decode_header(part.get('Return-Path'))}",
29. f"User-Agent: {decode_header(part.get('User-Agent'))}",
30. f"Date: {decode_header(part.get('Date'))}"]
31. # sauvegarde des entêtes dans un fichier texte
32. with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
33. # écriture dans fichier
34. string = '\r\n'.join(headers)
35. file.write(f"{string}\r\n")
36.
37. # type de la partie [part]
38. main_type = part.get_content_maintype()
39. …
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
386/755
o lorsque [part.ismultipart()] vaut [False], c'est qu'on est arrivé à une feuille de l'arbre du message initial : il peut s'agir :
▪ du corps du message sous la forme d'un texte normal ;
▪ du corps du message sous la forme d'un texte HTML ;
▪ d'une pièce attachée (à l'exception d'un message encapsulé pour lequel [part.ismultipart()] vaut [True]) ;
• de par la nature en arbre du paramètre [email.message.Message], la fonction [save_message] sera appelée de façon récursive.
La récursivité cesse lorsqu'on atteint les feuilles de l'arbre, ç-à-d une partie [part] pour laquelle [part.ismultipart()] vaut
[False] ;
• ligne 21 : nous demandons à voir les clés (ou entêtes) du message couramment analysé (qui de par la récursivité peut être une
sous-partie du message initial) ;
• lignes 23-35 : on veut enregistrer les entêtes :
o [From] : l'expéditeur du message ;
o [To] : le destinataire du message ;
o [Subject] : le sujet du message ;
o [Return-Path] : le destinataire à qui on doit répondre si on veut répondre. En effet, cette information n'est pas toujours
dans le [From] ;
o [User-Agent] : le client POP3 qui dialogue avec le serveur POP3 ;
o [Date] : date d'envoi du mail ;
• ligne 23 : seule l'une des parties d'un message contient ces entêtes. Pour les autres parties, le code des lignes 23-35 sera ignoré ;
• lignes 25-30 : on crée une liste avec les six entêtes ;
• ligne 25 : analysons le 1er entête :
o [part.get(key)] permet d'avoir l'entête associé à la clé [key] ;
o cet entête peut être encodé. Si cet encodage n'est pas utf-8, on décode l'entête pour le réencoder en utf-8 à l’aide de la
fonction [decode_header] ;
o le 1er entête sera de la forme [From: pymail2lexemple@gmail.com] ;
• lignes 31-35 : on sauve les entêtes dans le fichier [output/headers.txt] ;
1. # décodage de headers
2. def decode_header(header: object) -> str:
3. # on décode l'entête
4. header = email.header.decode_header(f"{header}")
5. # le résultat est un tableau - ici il n'aura qu'un élément de type (header, encoding)
6. # si encoding==None, alors header est une chaîne de caractères
7. # sinon c'est une liste d'octets codés par encoding
8. header, encoding = header[0]
9. if not encoding:
10. # si pas d'encodage
11. return header
12. else:
13. # si encodage, on décode
14. return header.decode(encoding)
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
387/755
11. # ou bien dans une partie [text/*] lorsqu'il n'y a pas de partie [multipart]
12. keys = part.keys()
13. # From doit faire partie des entêtes, sinon la partie n'a pas les entêtes qu'on cherche
14. if "From" in keys:
15. # on récupère certains entêtes
16. headers = [f"From: {decode_header(part.get('From'))}",
17. f"To: {decode_header(part.get('To'))}",
18. f"Subject: {decode_header(part.get('Subject'))}",
19. f"Return-Path: {decode_header(part.get('Return-Path'))}",
20. f"User-Agent: {decode_header(part.get('User-Agent'))}",
21. f"Date: {decode_header(part.get('Date'))}"]
22. # sauvegarde des entêtes dans un fichier texte
23. with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
24. # écriture dans fichier
25. string = '\r\n'.join(headers)
26. file.write(f"{string}\r\n")
27.
28. # type de la partie [part]
29. main_type = part.get_content_maintype()
30. sub_type = part.get_content_subtype()
31. type_of_part = f"{main_type}/{sub_type}"
32. # si le message est de type text/plain
33. if type_of_part == "text/plain":
34. # message texte
35. save_textmessage(output, part, 0)
36.
37. # si le message est de type text/html
38. elif type_of_part == "text/html":
39. # message HTML
40. save_textmessage(output, part, 1)
41.
42. # si le message est un conteneur de parties
43. elif part.is_multipart():
44. …
45. else:
46. …
47. # on ignore les autres parties (pas text/plain, pas text/html, pas attachment)
48. # on rend la valeur actuelle de irfc822 (numérotation des mails attachés rangés dans le dossier output)
49. return irfc822
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
388/755
19. # on récupère le message du mail
20. msg = email.contentmanager.raw_data_manager.get_content(part)
21. # selon les types de texte
22. filename = None
23. if type_of_text == 0:
24. # sauvegarde des entêtes
25. with codecs.open(f"{output}/headers.txt", "a", "utf-8") as file:
26. # écriture dans fichier
27. string = '\r\n'.join(headers)
28. file.write(f"{string}\r\n")
29. # fichier texte pour le contenu
30. filename = f"{output}/mail.txt"
31. elif type_of_text == 1:
32. # fichier html pour le contenu
33. filename = f"{output}/mail.html"
34. # sauvegarde du message
35. with codecs.open(filename, "w", "utf-8") as file:
36. # écriture dans fichier
37. file.write(msg)
Commentaires
• comme les entêtes, le texte du message peut être encodé. Il peut y avoir deux encodages :
o l'encodage initial du texte (utf-8, iso-8859-1…). C'est l'encodage utilisé par le gestionnaire de courrier qui a envoyé le
message. Il est connu avec l'entête [Content-Type] du message reçu ;
o un second encodage qu'a pu subir le texte précédent pour être envoyé. Il est connu avec l'entête [Transfer-Content-
Encoding] du message reçu ;
• ligne 6 : l'encodage initial du texte ;
• ligne 11 : le second encodage que le texte a subi pour son transfert vers le destinataire ;
• lignes 9, 13 : ces deux informations sont mises dans la liste [headers]. Elles seront rajoutées aux informations du fichier
[headers.txt] qui enregistre certains entêtes du message ;
• ligne 20 : [email.contentmanager.raw_data_manager.get_content] permet d'avoir le message avec son encodage initial 1. On
s'est débarrassé de l'encodage 2. Seulement l'objet [email.contentmanager.raw_data_manager] ne gère que deux types de
[Transfer-Content-Encoding] :
o [quoted-printable] ;
o [base64] ;
Il ignore les autres. Or Thunderbird par exemple utilise le [Transfer-Content-Encoding] nommé "8bit". Cet encodage est
ignoré et les messages avec des caractères accentués sont dénaturés. Le message peut alors être obtenu par la méthode
[part.get_payload()] (lignes 15-17) ;
• ligne 21 : lorsqu'on est là, on a le message débarrassé de son encodage de transfert, donc le message tel qu'il a été écrit par
l'expéditeur ;
• lignes 22-37 : on est dans le cas où on doit sauvegarder un message texte ;
o lignes 24-28 : on sauvegarde les deux entêtes construits lignes 9, 13 dans le fichier [headers.txt]. Celui-ci existe déjà et
contient des entêtes. Aussi utilise-t-on le mode "a" (ligne 25) pour ouvrir ce fichier. "a" signifie "append" et les nouveaux
entêtes sont ajoutés (en fin de fichier) au contenu existant du fichier [headers.txt] ;
o ligne 30 : le nom du fichier dans lequel sauvegarder le message texte ;
o ligne 33 : le nom du fichier dans lequel sauvegarder le message HTML ;
o lignes 34-37 : on sauvegarde le texte utf-8 dans un fichier ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
389/755
21. f"Date: {decode_header(part.get('Date'))}"]
22. # sauvegarde des entêtes dans un fichier texte
23. with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
24. # écriture dans fichier
25. string = '\r\n'.join(headers)
26. file.write(f"{string}\r\n")
27.
28. # type de la partie [part]
29. main_type = part.get_content_maintype()
30. sub_type = part.get_content_subtype()
31. type_of_part = f"{main_type}/{sub_type}"
32. # si le message est de type text/plain
33. if type_of_part == "text/plain":
34. # message texte
35. save_textmessage(output, part, 0)
36.
37. # si le message est de type text/html
38. elif type_of_part == "text/html":
39. # message HTML
40. save_textmessage(output, part, 1)
41.
42. # si le message est un conteneur de parties
43. elif part.is_multipart():
44. # cas particulier du mail attaché
45. if type_of_part == "message/rfc822":
46. # création d'un nouveau dossier output2 pour le mail attaché
47. irfc822 += 1
48. output2 = f"{output}/rfc822_{irfc822}"
49. os.mkdir(output2)
50. # sauvegarde des sous-parties du message irfc822 dans output2
51. for subpart in part.get_payload():
52. # dans le nouveau dossier irfc822 redémarre à 0
53. save_message(output2, subpart, 0)
54.
55. else:
56. # on n'a pas affaire à un mail attaché
57. # sauvegarde des sous-parties dans le dossier courant output
58. # irfc822 doit alors être incrémenté pour chaque sous-partie message/rfc822
59. for subpart in part.get_payload():
60. # save_message rend la dernière valeur de irfc822
61. # incrémentée de 1 si subpart="message/rfc822", pas incrémentée sinon
62. irfc822 = save_message(output, subpart, irfc822)
63. else:
64. # autres cas (pas text/plain, pas text/html, pas multipart)
65. # attachement ?
66. disposition = part.get('Content-Disposition')
67. if disposition and disposition.startswith('attachment'):
68. save_attachment(output, part)
69. # on ignore les autres parties (pas text/plain, pas text/html, pas attachment)
70. # on rend la valeur actuelle de irfc822 (numérotation des mails attachés rangés dans le dossier output)
71. return irfc822
Commentaires
• lignes 33-40 : nous avons traité deux cas possibles d'un message à une extrémité de l'arbre du message initial (pas de sous-
parties). Il nous reste encore deux cas à traiter :
◦ lignes 43-62 : le cas où la partie analysée contient elle-même des sous-parties (part.ismultipart()==True) ;
◦ lignes 63-68 : pour les cas restants, on ne traite que le cas où la partie analysée est un attachement ;
Nous traitons ce dernier cas. Nous sommes là encore à une extrémité du message initial (pas de sous-parties). Nous avons déjà
rencontré deux cas de cette espèce : les types text/plain et text/html. Nous traitons maintenant le cas du fichier attaché.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
390/755
5.
6. # le nom du fichier peut être encodé
7. # par exemple =?utf-8?Q?Cours-Tutoriels-Serge-Tah=C3=A9-1568x268=2Ep
8. filename = decode_header(filename)
9. # on sauvegarde le fichier attaché
10. with open(f"{output}/{filename}", "wb") as file:
11. file.write(part.get_payload(decode=True))
• ligne 4 : si [part] est un attachement, alors le nom du fichier attaché est obtenu par [part.get_filename]. On ne garde que
le nom du fichier pas son chemin ;
• ligne 8 : les noms des fichiers sont généralement encodés et ce de la même façon que les entêtes du message. Aussi utilise-
t-on la fonction [decode_header] pour le décoder ;
• ligne 11 : le contenu du fichier attaché est pour l'instant une chaîne de caractères produite par l'encodage (souvent base64)
en texte du contenu initial du fichier. Pour obtenir ce contenu initial on utilise la fonction [part.get_payload(decode=True)].
Le paramètre [decode=True] indique que le contenu de la pièce attachée doit être décodé. On obtient alors une suite d'octets ;
• ligne 10 : cette suite d'octets est sauvegardée dans le fichier [output/filename]. Le mode "wb" d'ouverture du fichier signifie
write binary ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
391/755
55. # sauvegarde des sous-parties dans le dossier courant output
56. # irfc822 doit alors être incrémenté pour chaque sous-partie message/rfc822
57. for subpart in part.get_payload():
58. # save_message rend la dernière valeur de irfc822
59. # incrémentée de 1 si subpart="message/rfc822", pas incrémentée sinon
60. irfc822 = save_message(output, subpart, irfc822)
61. else:
62. # autres cas (pas text/plain, pas text/html, pas multipart)
63. # attachement ?
64. disposition = part.get('Content-Disposition')
65. if disposition and disposition.startswith('attachment'):
66. save_attachment(output, part)
67. # on ignore les autres parties (pas text/plain, pas text/html, pas attachment)
68. # on rend la valeur actuelle de irfc822 (numérotation des mails attachés rangés dans le dossier output)
69. return irfc822
Commentaires
• nous avons traité les cas des terminaisons de l'arbre du message initial : les parties [text/plain, text/html et Content-
Disposition=attachment;…] Il nous reste à traiter le cas où la partie analysée est un conteneur de parties, ç-à-d qu'elle
contient des sous-parties [part.is_multipart()==True], ligne 41. Pour arriver aux terminaisons de l'arbre du message, il
faut donc analyser ces sous-parties ;
• ligne 43 : on traite de façon particulière le cas où la partie analysée a un type [message/rfc822]. C'est le type d'un mail. C'est
donc le cas où un mail a comme pièce attachée un autre mail ;
• la différence entre une partie [message/rfc822] et les autres parties multipart est que le dossier de sauvegarde change ;
o lignes 6-8 : pour la partie [message/rfc822], le dossier de sauvegarde devient celui de la ligne 7 [output/rfc822_x] où x
est le n° du mail attaché, 1 pour le premier, 2 pour le deuxième… ;
o ligne 21 : pour les autres parties multipart, le dossier de sauvegarde continue à être le dossier [output] du message initial.
On ne change pas de dossier ;
• lignes 10-12 : chaque sous-partie est sauvegardée par un appel récursif à [save_message]. Le 3e paramètre est l'indice de
numérotation des mails encapsulés dans [subpart]. Au départ cet indice vaut 0 ;
• ligne 21 : même explication que pour la ligne 12, mais la valeur du 3 e paramètre [irfc822] change. Si dans la boucle des
lignes 18-21, il y a plusieurs mails encapsulés, ils doivent être rangés dans des dossiers […/rfc822-1…/rfc822_2…]. Donc le
3e paramètre de la fonction [save_message] doit avoir successivement les valeurs, 1, 2, 3… Pour ce faire, [save_message]
rend la valeur de [irfc822] (ligne 21).
Prenons un exemple et supposons que la liste des sous-parties de la ligne 18 soit [subpart1, subpart2, subpart3,
subpart4, subpart5] et que [subpart1, subpart3, subpart5] soient des mails attachés, [subpart2] une partie text/plain
et [subpart4] un attachement, et qu'on n'a pas encore rencontré de mail attaché dans le message [irfc822=0]. Dans ce
cas :
• [subpart1] est sauvegardé par la ligne 21 : la fonction [saveMessage] est exécutée avec irfc822=0 ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
392/755
• [subpart1] est un mail attaché, donc irfc822 passe à 1 (ligne 6 du code). Un dossier [output/irfc822_1] est créé.
La valeur rendue par [saveMessage(ouput,subpart1,0)] est donc 1 (ligne 23) ;
• [subpart2] est sauvegardé par la ligne 21 : la fonction [saveMessage] est exécutée avec irfc822=1 ;
• [subpart2] n'est pas un mail attaché. Donc irfc822 reste à 1. C'est la valeur récupérée ligne 21 ;
• [subpart3] est sauvegardé par la ligne 21 : la fonction [save_message] est exécutée avec irfc822=1 ;
• [subpart3] est un mail attaché, donc irfc822 passe à 2 (ligne 6 du code). Un dossier [output/irfc822_2] est créé.
La valeur rendue par [save_message(ouput,subpart1,1)] est donc 2 (ligne 21) ;
• [subpart4] est sauvegardé par la ligne 21 : la fonction [save_message] est exécutée avec irfc822=2 ;
• [subpart4] n'est pas un mail attaché. Donc irfc822 reste à 2. C'est la valeur récupérée ligne 21 ;
• [subpart5] est sauvegardé par la ligne 21 : la fonction [save_message] est exécutée avec irfc822=2 ;
• [subpart5] est un mail attaché, donc irfc822 passe à 3 (ligne 6 du code). Un dossier [output/irfc822_3] est créé.
La valeur rendue par [save_message(ouput,subpart1,2)] est donc 3 (ligne 21) ;
Exemples d'exécution
• [Gmail] : [https://mail.google.com/] ;
• [Outlook] : [https://outlook.live.com/owa/] ;
• [em Client] : [https://www.emclient.com/] ;
• [Mozilla Thunderbird] : [https://www.thunderbird.net/fr/] ;
Tous les mails auront le sujet [hélène va au marché] et comme texte [acheter des légumes]. On veut tester comment sont récupérés
les caractères accentués.
Nous les lisons avec le script [pop3/02/main] configuré avec le fichier [pop3/02/config] suivant :
1. import os
2.
3.
4. def configure() -> dict:
5. # configuration de l'appli
6. config = {
7. # liste des boîtes à lettres à gérer
8. "mailboxes": [
9. # server : serveur POP3
10. # port : port du serveur POP3
11. # user : utilisateur dont on veut lire les messages
12. # password : son mot de passe
13. # maxmails : le nombre maximum de mail à télécharger
14. # timeout : délai d'attente maximal d'une réponse du serveur
15. # delete : à vrai s'il faut supprimer du serveur les messages téléchargés
16. # ssl : à vrai si la lecture des mails se fait au travers d'une liaison sécurisée
17. # output : le dossier de rangement des messages téléchargés
18.
19. {
20. "server": "pop.gmail.com",
21. "port": "995",
22. "user": "pymail2parlexemple@gmail.com",
23. "password": "#6prD&@1QZ3TG",
24. "maxmails": 10,
25. "delete": False,
26. "ssl": True,
27. "timeout": 2.0,
28. "output": "output"
29. }
30. ]
31. }
32. # chemin absolu du dossier du script
33. script_dir = os.path.dirname(os.path.abspath(__file__))
34.
35. # chemins absolus des dossiers à inclure dans le syspath
36. absolute_dependencies = [
37. # dossier local
38. f"{script_dir}/../../shared",
39. ]
40.
41. # configuration du syspath
42. from myutils import set_syspath
43. set_syspath(absolute_dependencies)
44.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
393/755
45. # on rend la configuration
46. return config
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
394/755
On remarquera que [em Client] code les textes en utf-8 [4] et qu'il les transfère en [quoted-printable] [5]. Il a également envoyé
une copie du message en HTML [7-8]. Tous les gestionnaires de mail testés ici peuvent faire cela. Il s’agit d’un paramètre de
configuration.
On remarquera que Gmail code les textes en utf-8 [3] et qu'il les transfère en [quoted-printable] [4]. En [6], la version HTML du
message.
On remarquera que Outlook code les textes en iso-8859-1 [3] et qu'il les transfère en [quoted-printable] [4].
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
395/755
• les gestionnaires de courriers ont des façons différentes d'envoyer un mail ;
Voyons maintenant les fichiers attachés. Avec Thunderbird, nous vidons la boîte mail de l'utilisateur [pymail2parlexemple@gmail.com].
Puis nous utilisons le script [smtp/03/main] pour envoyer un mail avec la configuration [smtp/03/config] suivante :
1. import os
2.
3.
4. def configure() -> dict:
5. # configuration de l'application
6. script_dir = os.path.dirname(os.path.abspath(__file__))
7.
8. return {
9. # description : description du mail envoyé
10. # smtp-server : serveur SMTP
11. # smtp-port : port du serveur SMTP
12. # from : expéditeur
13. # to : destinataire
14. # subject : sujet du mail
15. # message : message du mail
16. "mails": [
17. {
18. "description": "mail to gmail via gmail avec smtplib",
19. "smtp-server": "smtp.gmail.com",
20. "smtp-port": "587",
21. "from": "pymail2parlexemple@gmail.com",
22. "to": "pymail2parlexemple@gmail.com",
23. "subject": "to gmail via gmail avec smtplib",
24. # on teste les caractères accentués
25. "message": "aglaë séléné\nva au marché\nacheter des fleurs",
26. # smtp avec authentification
27. "user": "pymail2parlexemple@gmail.com",
28. "password": "#6prIlhD&@1QZ3TG",
29. # ici, il faut mettre des chemins absolus pour les fichiers attachés
30. "attachments": [
31. f"{script_dir}/attachments/fichier attaché.docx",
32. f"{script_dir}/attachments/fichier attaché.pdf",
33. f"{script_dir}/attachments/mail attaché 1.eml",
34. ]
35. }
36. ]
37. }
Une fois le mail envoyé, nous exécutons le script [pop3/02] pour lire la boîte mail de l’utilisateur [pymail2parlexemple@gmail.com].
Les résultats sont les suivants :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
396/755
• en [1] : le message avec ses deux fichiers attachés ;
• en [2] : le mail attaché lui-même avec ses deux fichiers attachés ;
Conclusion
Le module [mail_parser.py] est particulièrement complexe. Cela est dû à la complexité des mails eux-mêmes. Nous allons réutiliser
ce module pour le protocole IMAP.
• le protocole POP3 (Post Office Protocol) historiquement le 1er protocole mais peu utilisé maintenant ;
• le protocole IMAP (Internet Message Access Protocol) protocole plus récent que POP3 et le plus utilisé actuellement ;
• les mails sont conservés sur le serveur IMAP et peuvent être organisés en dossiers ;
• le client IMAP peut envoyer des commandes de création / modification / suppression de ces dossiers ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
397/755
• en [1-6], nous créons le dossier [dossier1] ;
• en [7-8], nous déplaçons (avec la souris) tous les fichiers du dossier [Courrier entrant] dans le dossier [dossier1] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
398/755
Maintenant connectons-nous au site web de Gmail et identifions-nous comme l'utilisateur [pymail2parlexemple@gmail.com] :
• en [4-6] : les mails qui ont été déplacés dans le dossier [dossier1] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
399/755
• Serveur B est le serveur IMAP de Gmail ;
L'arbre des dossiers de l'utilisateur est maintenu par le serveur IMAP. Ensuite tous les clients IMAP se synchronisent sur lui pour
présenter à l'utilisateur les dossiers de son compte. Ici Thunderbird a envoyé plusieurs commandes pour :
1. import os
2.
3.
4. def configure() -> dict:
5. # configuration de l'appli
6. config = {
7. # liste des boîtes à lettres à gérer
8. "mailboxes": [
9. # server : serveur IMAP
10. # port : port du serveur IMAP
11. # user : utilisateur dont on veut lire les messages
12. # password : son mot de passe
13. # maxmails : le nombre maximum de mail à télécharger
14. # timeout : délai d'attente maximal d'une réponse du serveur
15. # delete : à vrai s'il faut supprimer du serveur les messages téléchargés
16. # ssl : à vrai si la lecture des mails se fait au travers d'une liaison sécurisée
17. # output : le dossier de rangement des messages téléchargés
18.
19. {
20. "server": "imap.gmail.com",
21. "port": "993",
22. "user": "pymail2parlexemple@gmail.com",
23. "password": "#6prIlhD&@1QZ3TG",
24. "maxmails": 10,
25. "ssl": True,
26. "timeout": 2.0,
27. "output": "output"
28. }
29. ]
30. }
31. # chemin absolu du dossier du script
32. script_dir = os.path.dirname(os.path.abspath(__file__))
33.
34. # chemins absolus des dossiers à inclure dans le syspath
35. absolute_dependencies = [
36. # dossier local
37. f"{script_dir}/../shared",
38. ]
39.
40. # configuration du syspath
41. from myutils import set_syspath
42. set_syspath(absolute_dependencies)
43.
44. # on rend la configuration
45. return config
Commentaires
• lignes 8-29 : la clé [mailboxes] est associée à la liste des boîtes à lettres à consulter ;
• ligne 20 : le serveur IMAP ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
400/755
• ligne 21 : son port de service ;
• lignes 22-23 : l'utilisateur dont on veut lire les mails ;
• ligne 24 : le nombre maximum de mails qu'on veut lire ;
• ligne 25 : indique s’il faut établir une liaison sécurisée avec le serveur IMAP (True) ou pas (False) ;
• ligne 26 : le délai d'attente maximum d'attente d'une réponse du serveur ;
• ligne 27 : dossier de sauvegarde des mails lus ;
1. # imports
2. import email
3. import imaplib
4. import os
5. import shutil
6.
7.
8. # -----------------------------------------------------------------------
9.
10. def readmails(mailbox: dict):
11. …
12.
13.
14. # main ----------------------------------------------------------------
15. # client IMAP permettant de lire des mails
16.
17. # on récupère la configuration de l'application
18. import config
19. config = config.configure()
20.
21. # on traite les boîtes mail une par une
22. for mailbox in config['mailboxes']:
23. try:
24. # affichage console
25. print("----------------------------------")
26. print(
27. f"Lecture de la boîte mail POP3 {mailbox['user']} / {mailbox['server']}:{mailbox['port']}")
28. # lecture de la boîte mail
29. readmails(mailbox)
30. # fin
31. print("Lecture terminée...")
32. # except BaseException as erreur:
33. # # on affiche l'erreur
34. # print(f"L'erreur suivante s'est produite : {erreur}")
35. finally:
36. pass
Commentaires
• lignes 14-36 : nous retrouvons la démarche déjà rencontrée dans le script |pop3/02/main| ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
401/755
23. # user
24. dir2 = f"{output}/{user}"
25. # on supprime le dossier [dir2] s'il existe puis on le recrée
26. if os.path.isdir(dir2):
27. # suppression
28. shutil.rmtree(dir2)
29. # création
30. os.mkdir(dir2)
31. # connexion au serveur IMAP
32. if ssl:
33. imap_resource = imaplib.IMAP4_SSL(server, port)
34. else:
35. imap_resource = imaplib.IMAP4(server, port)
36. # timeout des communications du client
37. sock = imap_resource.socket()
38. sock.settimeout(timeout)
39. # authentification
40. imap_resource.login(user, password)
41. # on sélectionne le dossier INBOX (courrier entrant)
42. imap_resource.select('INBOX')
43. # on récupère tous les messages de ce dossier : critère ALL
44. # pas d'encoding particulier : None
45. typ1, data1 = imap_resource.search(None, 'ALL')
46. # print(f"typ={typ1}, data={data1}")
47.
48. # data1[0] est un tableau d'octets réunissant les n°s de tous les messages séparés par un espace
49. nums = data1[0].split()
50. imail = 0
51. fini = imail >= maxmails or imail >= len(nums)
52. # on lit les mails un à un
53. while not fini:
54. # num est un n° de message en binaire
55. num = nums[imail]
56. # print(f"message n° {num}")
57.
58. # on récupère le msg n° num
59. typ2, data2 = imap_resource.fetch(num, '(RFC822)')
60. # print(f"type={typ2}, data={data2}")
61.
62. # data est une liste qui contient des tuples, ici un seul
63. # data[0] est le tuple, data[0][1] est le deuxième élément du tuple
64. # data[0][1] contient une suite d'octets représentant toutes les lignes du message
65. # par message il faut entendre texte du message + tous les fichiers attachés
66.
67. # on récupère le message comme type email.message.Message
68. message = email.message_from_bytes(data2[0][1])
69. # dossier du message
70. dir3 = f"{dir2}/message_{int(num)}"
71. # si le dossier n'existe pas, on le crée
72. if not os.path.isdir(dir3):
73. os.mkdir(dir3)
74. # on le sauvegarde
75. save_message(dir3, message)
76. # message suivant
77. imail += 1
78. fini = imail >= maxmails or imail >= len(nums)
79. finally:
80. if imap_resource:
81. # on ferme la connexion avec la mailbox
82. imap_resource.close()
83. # on se déconnecte du serveur IMAP
84. imap_resource.logout()
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
402/755
• lignes 43-45 : on demande la liste de tous les messages trouvés dans [INBOX] :
o le 1er paramètre de [imapResource.search] est un type d'encodage. [None] signifie "pas de filtre sur l'encodage" ;
o le 2e paramètre est un critère. Il y a différentes façons d'exprimer celui-ci. Le critère [ALL] signifie qu'on veut tous les
messages du dossier ;
[data] est une liste qui contient les n°s des messages obtenus. Ceux-ci sont en binaire. Ci-dessus, deux messages ont été
trouvés dans le dossier [INBOX] ;
• ligne 49 : on récupère les n°s des messages. Ci-dessus on aura la liste [b'1' b'2'], une liste de numéros codés en binaire ;
• lignes 53-78 : on va boucler pour lire les messages du dossier [INBOX] ;
• lignes 54-55 : n° du message ;
• lignes 58-59 : le message n° [num] est demandé au serveur IMAP ;
o le 1er paramètre est le n° du message désiré ;
o le second paramètre est une chaîne "(part1)(part2)…" où [parti] est le nom d'une partie du message. Je n'ai pas
approfondi ce point. Le nom (RFC822) désigne la totalité du mail ;
L'élément [data] est ici une liste à 1 élément et cet unique élément est un tuple de trois éléments :
1. data = [
2. (b'1 (RFC822 {614}',
3. b'Return-Path: guest@localhost\r\nReceived: from [127.0.0.1] (localhost [127.0.0.1])\r\n\tby
DESKTOP-528I5CU with ESMTPA\r\n\t; Tue, 17 Mar 2020 09:41:50 +0100\r\nTo: guest@localhost\r\nFrom:
"guest@localhost" <guest@localhost>\r\nSubject: test\r\nMessage-ID: <2572d0f0-5b7c-2c31-5a70-
c628293d5709@localhost>\r\nDate: Tue, 17 Mar 2020 09:41:48 +0100\r\nUser-Agent: Mozilla/5.0
(Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101\r\n Thunderbird/68.6.0\r\nMIME-Version:
1.0\r\nContent-Type: text/plain; charset=utf-8; format=flowed\r\nContent-Transfer-Encoding:
8bit\r\nContent-Language: fr\r\n\r\nh\xc3\xa9l\xc3\xa8ne est all\xc3\xa9e au march\xc3\xa9 acheter
des l\xc3\xa9gumes.\r\n\r\n'),
4. b')'
5. ]
Le second élément de ce tuple est une chaîne binaire représentant la totalité du message demandé. On reconnaît ci-dessus,
des éléments déjà présentés lors de l'étude du module [mail_parser].
data[0] représente un tuple à deux éléments. data[0][1] représente les lignes du message sous une forme binaire.
• ligne 68 : la fonction [email.message_from_bytes(data2[0][1])] construit un objet de type [email.message.Message] à partir
des lignes du message. Le type [email.message.Message] est le type du paramètre du module [mail_parser] que nous avons
écrit précédemment ;
• lignes 69-73 : nous créons le dossier de sauvegarde du message n° [num] ;
• ligne 75 : nous faisons appel à la fonction [save_message] du module [mail_parser] de la ligne 5. Cette fonction a été décrite
au paragraphe |pop3/02/main| ;
• lignes 76-78 : on reboucle pour traiter le message suivant ;
• lignes 79-84 : qu'il y ait eu erreur ou pas :
o ligne 82 : on clôt la connexion avec le dossier interrogé ;
o ligne 84 : on se déconnecte du serveur IMAP ;
Les résultats obtenus sont identiques à ceux obtenus avec le script [pop3/02/main]. C'est normal puisque c'est le même parseur de
mail [mail_parser] qui est utilisé.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
403/755
22 Services web avec le framework Flask
Par service web, on entend ici tout application web délivrant des données brutes consommées par un client, souvent un script console
dans les exemples qui vont suivre. On ne s’intéresse pas à une technologie particulière, REST (REpresentational State Transfer) ou
SOAP (Simple Object Access Protocol) par exemple, qui délivrent des données plus ou moins brutes dans un format bien défini.
REST délivre du jSON alors que pour SOAP c’est du XML. Chacune de ces technologies décrit précisément la façon dont le client
doit interroger le serveur et la forme que doit prendre la réponse de celui-ci. Dans ce cours, on sera beaucoup plus souple quant à la
nature de la requête du client et celle de la réponse du serveur. Cependant, les scripts écrits et les outils utilisés sont proches de ceux
de la technologie REST.
22.1 Introduction
Les scripts Python peuvent être exécutés par un serveur Web. Un tel script devient un programme serveur pouvant servir plusieurs
clients. Du point de vue du client, appeler un service web revient à demander l'URL de ce service. Le client peut être écrit avec
n'importe quel langage, notamment en Python. Dans ce dernier cas, on utilise alors les fonctions internet que nous venons de voir. Il
nous faut par ailleurs savoir "converser" avec un service web, c'est à dire comprendre le protocole HTTP de communication entre
un serveur Web et ses clients. C'était le but du paragraphe |le protocole HTTP|. Les clients web décrits dans cette partie du cours
nous ont permis de découvrir une partie du protocole HTTP.
Dans leur version la plus simple, les échanges client / serveur sont les suivants :
Le document peut être de nature diverse : un texte au format HTML, une image, une vidéo, ... Ce peut être un document existant
(document statique) ou bien un document généré à la volée par un script (document dynamique). Dans ce dernier cas, on parle de
programmation web. Le script de génération dynamique de documents peut être écrit dans divers langages : PHP, Python, Perl, Java,
Ruby, C#, VB.net, ...
Dans la suite, nous allons utiliser des scripts Python pour générer dynamiquement des documents texte.
• en [1], le client ouvre une connexion avec le serveur, demande un script Python, envoie ou non des paramètres à destination
de ce script ;
• en [3], le serveur web fait exécuter le script Python par l'interpréteur Python. Le script génère un document qui est envoyé au
client [2] ;
• le serveur clôt la connexion. Le client en fait autant ;
• le serveur léger Werkzeug [https://werkzeug.palletsprojects.com/en/1.0.x/]. Ce serveur est utilisé par le framework web
Flask [https://flask.palletsprojects.com/en/1.1.x/]. Nous l’appellerons plus fréquemment serveur Flask ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
404/755
• le serveur Apache 2 [https://httpd.apache.org/] ;
Le serveur Flask sera utilisé dans la totalité des exemples. Le serveur Apache sera utilisé pour héberger l’application web que nous
allons développer.
Le framework Flask est développé en Python. C’est un module qu’on installe dans un terminal PyCharm :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
405/755
• en [1], un script Python sera exécuté comme l’est un script console classique ;
• en [2], de façon transparente, un serveur web est instancié et attend des requêtes. En fait il n’acceptera qu’une unique URL ;
• en [3], le navigateur demandera au serveur son unique URL ;
• en [4], le serveur fera exécuter le script Python désigné par la console [1] ;
• en [5], le script rendra ses résultats au serveur web, un document texte ;
• en [6], le serveur web enverra au navigateur ce document texte ;
Pour résumer, il n'est nul besoin de connaître la totalité du langage HTML pour démarrer la programmation web. Cependant cette
connaissance est nécessaire et peut être acquise au travers de l'utilisation de logiciels WYSIWYG de construction de pages WEB tels
que DreamWeaver et des dizaines d'autres. Une autre façon de découvrir les subtilités du langage HTML est de parcourir le web et
d'afficher le code source des pages qui présentent des caractéristiques intéressantes et encore inconnues pour vous.
Considérons l'exemple suivant qui présente quelques éléments qu'on peut trouver dans un document web tels que :
• un tableau ;
• une image ;
• un lien ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
406/755
Un document HTML est encadré par les balises <html>…</html>. Il est formé de deux parties :
• <head>…</head> : c'est la partie non affichable du document. Elle donne des renseignements au navigateur qui va afficher
le document. On y trouve souvent la balise <title>…</title> qui fixe le texte qui sera affiché dans la barre de titre du
navigateur. On peut y trouver d'autres balises notamment des balises définissant les mots clés du document, mot clés utilisés
ensuite par les moteurs de recherche. On peut trouver également dans cette partie des scripts, écrits le plus souvent en
javascript ou vbscript et qui seront exécutés par le navigateur ;
• <body attributs>…</body> : c'est la partie qui sera affichée par le navigateur. Les balises HTML contenues dans cette
partie indiquent au navigateur la forme visuelle "souhaitée" pour le document. Chaque navigateur va interpréter ces balises à
sa façon. Deux navigateurs peuvent alors visualiser différemment un même document web. C'est généralement l'un des casse-
têtes des concepteurs web ;
1. <!DOCTYPE html>
2. <html xmlns="http://www.w3.org/1999/xhtml">
3. <head>
4. <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5. <title>Quelques balises HTML</title>
6. </head>
7.
8. <body style="background-image: url(/static/images/standard.jpg)">
9. <h1 style="text-align: left">Quelques balises HTML</h1>
10. <hr />
11.
12. <table border="1">
13. <thead>
14. <tr>
15. <th>Colonne 1</th>
16. <th>Colonne 2</th>
17. <th>Colonne 3</th>
18. </tr>
19. </thead>
20. <tbody>
21. <tr>
22. <td>cellule(1,1)</td>
23. <td style="text-align: center;">cellule(1,2)</td>
24. <td>cellule(1,3)</td>
25. </tr>
26. <tr>
27. <td>cellule(2,1)</td>
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
407/755
28. <td>cellule(2,2)</td>
29. <td>cellule(2,3</td>
30. </tr>
31. </tbody>
32. </table>
33. <br /><br />
34. <table border="0">
35. <tr>
36. <td>Une image</td>
37. <td>
38. <img border="0" src="/static/images/cerisier.jpg" />
39. </td>
40. </tr>
41. <tr>
42. <td>Le site de Polytech'Angers</td>
43. <td><a href="http://www.polytech-angers.fr/fr/index.html">ici</a></td>
44. </tr>
45. </table>
46. </body>
47. </html>
titre du (ligne 5)
<title>Quelques balises HTML</title>
document le texte [Quelques balises HTML] apparaîtra dans la barre de titre du navigateur qui affichera le
document
exemples :
<table border="1">…</table> : l'attribut border définit l'épaisseur de la bordure du tableau
<td style="text-align: center;">cellule(1,2)</td> (ligne 23) : définit une cellule dont le contenu sera
cellule(1,2). Ce contenu sera centré horizontalement (text-align: center).
image <img border="0" src="/static/images/cerisier.jpg"/> (ligne 38) : définit une image sans bordure
(border=0") dont le fichier source est [/static/images/cerisier.jpg] sur le serveur web
(src="/static/images/cerisier.jpg"). Si ce lien se trouve sur un document web obtenu avec l'URL
[http://server/chemin/balises.html], alors le navigateur demandera l'URL [http://server/
static/images/cerisier.jpg] pour avoir l'image référencée ici.
lien <a href="http://www.polytech-angers.fr/fr/index.html">ici</a> (ligne 43) : fait que le texte ici sert de
lien vers l'URL http://www.polytech-angers.fr/fr/index.html.
On voit dans ce simple exemple que pour construire l'intégralité du document, le navigateur doit faire trois requêtes au serveur :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
408/755
• en [1], le script [exemple_01] qui va être exécuté ;
• en [3], le document HTML qui va être affiché par le script ;
• en [2], les images du document HTML ;
1. import os
2.
3. from flask import Flask, make_response, render_template
4.
5. # application Flask
6. script_dir = os.path.dirname(os.path.abspath(__file__))
7. app = Flask(__name__, template_folder=f"{script_dir}/../templates", static_folder=f"{script_dir}/../static")
8.
9.
10. # Home URL
11. @app.route('/')
12. def index():
13. # affichage de la page
14. return make_response(render_template("balises.html"))
15.
16.
17. # main
18. if __name__ == '__main__':
19. app.config.update(ENV="development", DEBUG=True)
20. app.run()
• ligne 7 : on instancie une application Flask. Une application Flask est une application web ;
o le 1er paramètre est le nom donné à l’application. On peut donner le nom que l’on veut. Ici on a utilisé l’attribut prédéfini
[__name__] qui vaut [__main__] (ligne 18) ;
o le second paramètre est un paramètre nommé, ç-à-d que sa position dans l’ordre des paramètres n’a pas d’importance.
Le paramètre nommé [template_folder] désigne le dossier où trouver les pages statiques de l’application web. Les pages
statiques sont délivrées telles quelles au navigateur. Ici, les pages statiques seront trouvées dans le dossier [templates]
de l’arborescence du projet. Ligne 7, nous avons mis un chemin relatif au dossier [script_dir] contenant le script
[exemple_01] exécuté ;
o le troisième paramètre est également un paramètre nommé. [static_folder] désigne le dossier où on va trouver les
ressources du document HTML (images, vidéos, …). La également, nous avons mis un chemin relatif au dossier
[script_dir] contenant le script [exemple_01] exécuté ;
• lignes 10-14 : on définit les URL acceptées par l’application web. Chaque URL est associée à une fonction qui s’exécute lorsque
l’URL est demandée par un navigateur web ;
• ligne 11 : l’unique URL de l’application est l’URL [/]. Notez que dans [@app.route('/')], [app] est la variable initialisée ligne
7. La définition des routes (les différentes URL gérées par l’application) vient donc forcément après la définition de
l’application [app]. Ce dernier nom est libre ;
• lignes 12-14 : la fonction qui s’exécute lorsqu’on demande l’URL [/] à l’application web [exemple_01] ;
• ligne 12 : la fonction associée à une URL peut porter un nom quelconque. Elle peut parfois avoir des paramètres pour
récupérer des éléments de l’URL qui lui est associée. Ici elle n’en a pas ;
• ligne 14 :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
409/755
o la fonction [render_template] rend une chaîne de caractères qui est le document texte produit par son paramètre. Celui-
ci est ici [balises.html]. A cause du [template_folder] de la ligne 7, ce document sera cherché dans le dossier
[f"{script_dir}/../templates"]. C’est effectivement là qu’il se trouve ;
o la fonction [make_response] génère une réponse HTTP pour le navigateur qui lui a demandé l’URL [/]. On a vu dans
le paragraphe |le protocole HTTP| qu’une réponse HTTP a deux éléments :
Ligne 14, on a donné aucun paramètre à la fonction [make_response] pour générer des entêtes HTTP. Elle va alors en
générer par défaut. On verra ultérieurement comment fixer ces entêtes HTTP.
• finalement, lorsque le navigateur demande l’URL / à l’application Flask, il obtient la page [balises.html] ;
• lignes 17-20 : ces lignes servent à lancer le serveur web qui va exécuter l’application web [exemple_01] ;
o ligne 18 : cette condition n’est vraie que lorsque le script [exemple_01] est lancé au sein d’une console ;
o ligne 19 : l’application [app] de la ligne 7 est configurée :
▪ le paramètre nommé [ENV="development"] met le serveur web en mode développement : dès que le développeur
modifie un élément de l’application celle-ci est régénérée et délivrée au serveur web. Le développeur n’a pas besoin
de demander une nouvelle exécution ;
▪ le paramètre nommé [DEBUG=True] va permettre au développeur de mettre des points d’arrêt dans le code de
l’application ;
o ligne 20 : l’application web est lancée : un serveur web est instancié et l’application web est déployée dessus afin de
répondre aux requêtes de clients web ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/flask/01/main/exemple_01.py
2. * Serving Flask app "exemple_01" (lazy loading)
3. * Environment: development
4. * Debug mode: on
5. * Restarting with stat
6. * Debugger is active!
7. * Debugger PIN: 334-263-283
8. * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
410/755
On obtient bien le document [balises.html] attendu.
1. <!DOCTYPE html>
2. <html lang="fr">
3. <head>
4. <meta charset="UTF-8">
5. <title>{{page.title}}</title>
6. </head>
7. <body>
8. <b>{{page.contents}}</b>
9. </body>
10. </html>
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
411/755
Ce document est dynamique parce que son contenu n’est totalement connu qu’au moment où le serveur web le sert. On y trouve en
effet aux lignes 5 et 8 deux éléments non connus au moment de l’écriture de la page. Ils ne sont connus qu’au moment où la page est
envoyée à un client. Ils sont alors remplacés par leurs valeurs, celles-ci étant des chaînes de caractères.
• nous avons déjà expliqué dans l’exemple précédent, les lignes 4-5 et 18-20. Nous utiliserons toujours ce schéma dans nos
exemples ;
• ligne 9 : la seule URL servie par l’application web est l’URL / ;
• ligne 14 : le document servi à l’URL / est le document [exemple_02.html] que nous venons de commenter. Nous savons qu’il
a un paramètre, un dictionnaire appelé [page] ;
• ligne 12 : nous définissons le dictionnaire qui va être passé en paramètre à la page [exemple_02.html]. Il peut porter n’importe
quel nom. Il doit cependant avoir les attributs [title, contents] utilisés dans le document HTML ;
• ligne 14 : la fonction [render_template] a pour rôle de rendre la chaîne de caractères du document [exemple_02.html].
Comme celui-ci est un document paramétré, on transmet à la fonction [render_template] le ou les paramètres attendus. Nous
le faisons ici en donnant une valeur au paramètre nommé [page]. Dans l’opération [page=page] :
o à gauche du signe =, on a le paramètre [page] utilisé dans le document [exemple_02.html] ;
o à droite du signe =, on a la valeur [page] définie ligne 12 ;
o de façon générale, si un document HTML a les paramètres [param1, param2, …, paramn], on passera leurs valeurs à la
fonction [render_template] sous la forme [render_template(document, param1=valeur1, param2=valeur2, …] ;
Si lors de l’exécution d’un script 1, vous avez l’impression que c’est un script 2 qui s’exécute c’est probablement parce que celui-ci est
toujours en exécution. Pour revenir à un état connu, vous pouvez arrêter tous les processus en cours d’exécution dans PyCharm (en
haut et à droite dans fenêtre PyCharm) :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
412/755
Exécutons le script [exemple_02] :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/flask/01/main/exemple_02.py
2. * Serving Flask app "exemple_02" (lazy loading)
3. * Environment: development
4. * Debug mode: on
5. * Restarting with stat
6. * Debugger is active!
7. * Debugger PIN: 334-263-283
8. * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
La ligne 8 indique le port de déploiement (5000) de l’application [exemple_02] (ligne 1) sur la machine [localhost]. Les lignes
précédentes étant toujours les mêmes, nous ne les remontrerons plus.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
413/755
22.2.3 script [exemple_03] : utiliser des fragments de page
• en [1], le script [exemple_03.py] va générer le document dynamique [exemple_03.html] [2]. Celui-ci sera construit à partir
des fragments de page [fragment_01.html, fragment_02.html] [3] ;
1. <!DOCTYPE html>
2. <html lang="fr">
3. {% include "fragments/fragment_01.html" %}
4. <body>
5. {% include "fragments/fragment_02.html" %}
6. </body>
7. </html>
• lignes 3 et 5, on utilise la directive [include] de Jinja2 pour inclure dans le document des éléments externes à celui-ci ;
• la syntaxe est {% include … %}. Le paramètre de la directive [include] est le chemin du document à incorporer. Ce chemin
est relatif au paramètre [template_folder] de l’application Flask :
Donc ici, les chemins des documents sont mesurés par rapport au dossier [templates].
Le fragment [fragment_01.html] (les noms sont bien sûr libres) est le suivant :
1. <meta charset="UTF-8">
2. <title>{{page.title}}</title>
1. <b>{{page.contents}}</b>
1. <!DOCTYPE html>
2. <html lang="fr">
3. <meta charset="UTF-8">
4. <title>{{page.title}}</title>
5. <body>
6. <b>{{page.contents}}</b>
7. </body>
8. </html>
1. import os
2.
3. from flask import Flask, make_response, render_template
4.
5. # application Flask
6. script_dir = os.path.dirname(os.path.abspath(__file__))
7. app = Flask(__name__, template_folder=f"{script_dir}/../templates", static_folder=f"{script_dir}/../static")
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
414/755
8.
9.
10. # Home URL
11. @app.route('/')
12. def index():
13. # contenu de la page
14. page = {"title": "un autre titre", "contents": "un autre contenu"}
15. # affichage de la page
16. return make_response(render_template("views/exemple_03.html", page=page))
17.
18.
19. # main
20. if __name__ == '__main__':
21. app.config.update(ENV="development", DEBUG=True)
22. app.run()
Le code est analogue à celui de [exemple_02.py]. Ligne 16, on montre comment on peut référencer des documents présents dans
des sous-dossiers de [template_folder] de la ligne 7.
1. <!DOCTYPE html>
2. <html lang="fr">
3. <head>
4. <meta charset="UTF-8">
5. <title>Date et heure du moment</title>
6. </head>
7. <body>
8. <b>Date et heure du moment : {{page.date_heure}}</b>
9. </body>
10. </html>
1. # imports
2. import os
3. import time
4.
5. from flask import Flask, make_response, render_template
6.
7. # application Flask
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
415/755
8. script_dir = os.path.dirname(os.path.abspath(__file__))
9. app = Flask(__name__, template_folder=f"{script_dir}")
10.
11.
12. # Home URL
13. @app.route('/')
14. def index():
15. # envoi heure au client
16. # time.localtime : nb de millisecondes depuis 01/01/1970
17. # time.strftime permet de formater l'heure et la date
18. # format affichage date-heure
19. # d: jour sur 2 chiffres
20. # m: mois sur 2 chiffres
21. # y : année sur 2 chiffres
22. # H : heure 0,23
23. # M : minutes
24. # S: secondes
25.
26. # date / heure du moment
27. time_of_day = time.strftime('%d/%m/%y %H:%M:%S', time.localtime())
28. # on génère le document à envoyer au client
29. page = {"date_heure": time_of_day}
30. document = render_template("date_time_server.html", page=page)
31. print("document", type(document), document)
32. # réponse HTTP au client
33. response = make_response(document)
34. print("response", type(response), response)
35. return response
36.
37.
38. # main seulement
39. if __name__ == '__main__':
40. app.config.update(ENV="development", DEBUG=True)
41. app.run()
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:\Data\st-
2020\dev\python\cours-2020\python3-flask-2020\flask\02\date_time_server.py
2. * Serving Flask app "date_time_server" (lazy loading)
3. * Environment: development
4. * Debug mode: on
5. * Restarting with stat
6. * Debugger is active!
7. * Debugger PIN: 334-263-283
8. * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
9. 127.0.0.1 - - [10/Jul/2020 09:32:09] "GET / HTTP/1.1" 200 -
10. document <class 'str'> <!DOCTYPE html>
11. <html lang="fr">
12. <head>
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
416/755
13. <meta charset="UTF-8">
14. <title>Date et heure du moment</title>
15. </head>
16. <body>
17. <b>Date et heure du moment : 10/07/20 09:42:33</b>
18. </body>
19. </html>
20. response <class 'flask.wrappers.Response'> <Response 195 bytes [200 OK]>
• ligne 10 : on voit que le type de la valeur rendue par [render_template] est de type [str]. Cette chaîne de caractères n’est
autre que le document [date_time_server.html] une fois interprété (lignes 10-19) ;
• ligne 20 : on voit que le type de la valeur rendue par [make_response] est de type [flask.wrappers.Response]. La fonction
[Response.__str__] a été implicitement appelée pour afficher l’objet [Response]. La chaîne rendue par cette fonction donne
deux informations sur la réponse HTTP qui va être faite :
o le document envoyé fait 195 octets ;
o le statut de la réponse HTTP est [200 OK]. On verra ultérieurement qu’on a accès à ce code de statut ;
1. <!DOCTYPE html>
2. <html lang="fr">
3. <head>
4. <meta charset="UTF-8">
5. <title>Date et heure du moment</title>
6. </head>
7. <body>
8. <b>Date et heure du moment : {{page.date_heure}}</b>
9. </body>
10. </html>
Un client web pourrait n’être intéressé que par l’information [page.date_heure] de la ligne 8 et pas par l’habillage HTML qu’il y a
autour. Le service web pourrait délivrer cette information comme une simple chaîne de caractères. Nous allons présenter ici des
exemples de ce type de service web.
1. def configure():
2. # chemin absolu référence des chemins relatifs de la configuration
3. rootDir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
4.
5. # dépendances de l'application
6. absolute_dependencies = [
7. # Personne, Utils, MyException
8. f"{rootDir}/classes/02/entities",
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
417/755
9.
10. ]
11. # on fixe le syspath
12. from myutils import set_syspath
13. set_syspath(absolute_dependencies)
14.
15. # on rend la config
16. return {}
Le rôle premier de cette configuration est de définir le Python Path du service web. Il faut qu’on puisse trouver les entités [2] (ligne
8).
1. # on configure l'application
2. import config
3. config=config.configure()
4.
5. # imports
6. from flask import Flask, make_response
7. from flask_api import status
8.
9. # dépendances
10. from Personne import Personne
11.
12. # application Flask (pas de documents statiques ici)
13. app = Flask(__name__)
14.
15.
16. # Home URL
17. @app.route('/')
18. def index():
19. # une personne
20. personne = Personne().fromdict({"prénom": "Aglaë", "nom": "de la Hûche", "âge": 87})
21. # réponse HTTP
22. response = make_response(str(personne))
23. # headers HTTP
24. response.headers.set("Content-type", "application/json; charser=utf8")
25. # on rend la réponse HTTP
26. return response, status.HTTP_200_OK
27.
28.
29. # main uniquement
30. if __name__ == '__main__':
31. # on lance le serveur
32. app.config.update(ENV="development", DEBUG=True)
33. app.run()
Le module [flask_api] n’est pas disponible nativement. Il faut l’installer. On fait cela dans un terminal PyCharm :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
418/755
7. Requirement already satisfied: Werkzeug>=0.15 in c:\data\st-2020\dev\python\cours-2020\python3-flask-
2020\venv\lib\site-packages (from Flask>=1.1->flask_api) (1.0.1)
8. Requirement already satisfied: click>=5.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-
2020\venv\lib\site-packages (from Flask>=1.1->flask_api) (7.1.2)
9. Requirement already satisfied: itsdangerous>=0.24 in c:\data\st-2020\dev\python\cours-2020\python3-flask-
2020\venv\lib\site-packages (from Flask>=1.1->flask_api) (1.1.0)
10. Requirement already satisfied: MarkupSafe>=0.23 in c:\data\st-2020\dev\python\cours-2020\python3-flask-
2020\venv\lib\site-packages (from Jinja2>=2.10.1->Flask>=1.1->flask_api) (1.1.1
11. )
12. Installing collected packages: flask-api
13. Successfully installed flask-api-2.0
Lorsqu’on exécute le script web [main_01], on obtient les résultats suivants dans un navigateur :
Voyons maintenant le rôle de l’entête [Content-Type] envoyé au client par le service web. On met le navigateur en mode développeur
(F12 en général) et on redemande la même URL. Ci-dessous une copie d’écran d’un navigateur Chrome :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
419/755
• en [8], on sélectionne l’onglet [Response] pour avoir accès au document envoyé par le serveice web, ici une simple chaîne
jSON ;
22.4.2 Postman
[Postman] est l’outil qui va nous permettre d’interroger les différentes URL d’une application web. Il nous permet :
[Postman] est un excellent outil pédagogique pour comprendre la communication client / serveur du protocole HTTP.
[Postman] est disponible à l’URL [https://www.getpostman.com/downloads/]. Procédez à l’installation de votre version de [Postman].
Au cours de l’installation, on vous demandera de créer un compte : celui-ci sera inutile ici. Le compte [Postman] sert à synchroniser
différents appareils afin que la configuration de l’un soit répliqué sur un autre. Rien de tout ceci n’est utile ici.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
420/755
• en [6], la version utilisée dans ce document ;
Nous allons ici utiliser [Postman] pour tester le service web jSON précédent :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
421/755
• en [6], on sélectionne l’onglet [Pretty] qui affiche le document reçu avec une mise en forme appropriée, ici une forme
appropriée à une chaîne jSON ;
• en [7], le document jSON reçu ;
• en [8-9], le document reçu sans mise en forme ;
Il y a une autre façon d’utiliser Postman. Elle consiste à utiliser la console Postman (Ctrl-Alt-C). Celle-ci permet de voir le dialogue
client / serveur. Outre la séquence Ctrl-Alt-C, la console Postman est disponible via une icône en bas à gauche de la fenêtre principale
de Postman :
La console Postman mémorise les dialogues client / serveur qui ont lieu lorsqu’une requête Postman est exécutée :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
422/755
• en [3], la liste des requêtes faites par Postman depuis qu’il a été lancé. Les plus récentes sont en bas de la liste ;
• en [4], la requête HTTP faite par Postman ;
• en [5-6], la réponse HTTP faite par le serveur web ;
• en [7], on peut voir les logs en mode [raw], ç-à-d sans artifice de présentation ;
Afin de faciliter les explications, nous numéroterons les lignes obtenues à partir de la console Postman.
Pour le client :
1. GET / HTTP/1.1
2. User-Agent: PostmanRuntime/7.26.1
3. Accept: */*
4. Cache-Control: no-cache
5. Postman-Token: 70e2acaa-b3e5-46f6-8375-989e6b94e694
6. Host: localhost:5000
7. Accept-Encoding: gzip, deflate, br
8. Connection: keep-alive
Pour le serveur :
1. HTTP/1.0 200 OK
2. Content-type: application/json; charser=utf8
3. Content-Length: 56
4. Server: Werkzeug/1.0.1 Python/3.8.1
5. Date: Mon, 13 Jul 2020 17:19:56 GMT
6. {"prénom": "Aglaë", "nom": "de la Hûche", "âge": 87}
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
423/755
22.4.3 script [main_02]
1. # on configure l'application
2. import config
3. config=config.configure()
4.
5. # imports
6. from flask import Flask, make_response
7. from flask_api import status
8.
9. # dépendances
10. from Personne import Personne
11.
12. # application Flask
13. app = Flask(__name__)
14.
15.
16. # Home URL
17. @app.route('/')
18. def index():
19. # une personne
20. personne = Personne().fromdict({"prénom": "Aglaë", "nom": "de la Hûche", "âge": 87})
21. # contenu
22. response = make_response(f"personne[{personne.prénom}, {personne.nom}, {personne.âge}]")
23. # headers HTTP
24. response.headers.set("Content-Type", "text/plain; charset=utf8")
25. # réponse HTTP
26. return response, status.HTTP_200_OK
27.
28.
29. # main uniquement
30. if __name__ == '__main__':
31. # on lance le serveur
32. app.config.update(ENV="development", DEBUG=True)
33. app.run()
o ligne 22 : le document envoyé au client est une chaîne de caractères brute, pas une chaîne jSON ;
o ligne 24 : ceci est reflété dans l’entête HTTP [Content-Type] qui indique le type [text/plain] pour le document ;
Nous exécutons le script web [main_02] puis utilisons [Postman] pour l’interroger :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
424/755
• en [1-3], on fait la requête au service web ;
• en [5], le statut OK de la réponse ;
• en [4, 6], les entêtes HTTP de la réponse ;
• en [7], l’entête [Content-Type] ;
• en [8-10], le document envoyé par le service web, une chaîne de caractères ;
Requête du client :
1. GET / HTTP/1.1
2. User-Agent: PostmanRuntime/7.26.1
3. Accept: */*
4. Cache-Control: no-cache
5. Postman-Token: 7c7fc9f3-8df8-49ae-9dc8-53c2d87d111a
6. Host: localhost:5000
7. Accept-Encoding: gzip, deflate, br
8. Connection: keep-alive
Réponse du serveur :
1. HTTP/1.0 200 OK
2. Content-Type: text/plain; charset=utf8
3. Content-Length: 34
4. Server: Werkzeug/1.0.1 Python/3.8.1
5. Date: Mon, 13 Jul 2020 17:34:22 GMT
6.
7. personne[Aglaë, de la Hûche, 87]
1. # on configure l'application
2. import config
3. config = config.configure()
4.
5. # imports
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
425/755
6. from flask import Flask, make_response
7. from flask_api import status
8.
9. # dépendances
10. from MyException import MyException
11. from Personne import Personne
12.
13. # application Flask
14. app = Flask(__name__)
15.
16.
17. # Home URL
18. @app.route('/')
19. def index():
20. # une personne incorrecte
21. msg_erreur = None
22. try:
23. personne = Personne().fromdict({"prénom": "", "nom": "", "âge": 87})
24. except MyException as erreur:
25. msg_erreur = f"{erreur}"
26. # erreur ?
27. if msg_erreur:
28. response = make_response(msg_erreur)
29. status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
30. else:
31. response = make_response(f"personne[{personne.prénom}, {personne.nom}, {personne.âge}]")
32. status_code = status.HTTP_200_OK
33. # headers HTTP
34. response.headers.set("Content-Type", "text/plain; charset=utf8")
35. # réponse HTTP
36. return response, status_code
37.
38.
39. # main uniquement
40. if __name__ == '__main__':
41. # on lance le serveur
42. app.config.update(ENV="development", DEBUG=True)
43. app.run()
Nous lançons le service web [main_03] et nous utilisons Postman pour l’interroger :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
426/755
• en [8-10], les entêtes HTTP de la réponse du service web ;
Dans la console Postman, les résultats en mode [raw] sont les suivants :
Requête du client :
1. GET / HTTP/1.1
2. User-Agent: PostmanRuntime/7.26.1
3. Accept: */*
4. Cache-Control: no-cache
5. Postman-Token: 925ff036-a360-47af-adf6-78173c01a247
6. Host: localhost:5000
7. Accept-Encoding: gzip, deflate, br
8. Connection: keep-alive
Réponse du serveur :
Le script [request_parameters.py] se propose de montrer que le service web a accès à diverses informations encapsulées dans la
requête d’un client web. Le code est le suivant :
1. # import
2. from flask import Flask, make_response, request
3. from flask_api import status
4. # application Flask
5. app = Flask(__name__)
6.
7.
8. # Home URL
9. @app.route('/', methods=['GET', 'POST'])
10. def index():
11. # paramètres de la requête
12. request_data = {}
13. request_data["environ"] = f"{request.environ}"
14. request_data["path"] = request.path
15. request_data["full_path"] = request.full_path
16. request_data["script_root"] = request.script_root
17. request_data["url"] = request.url
18. request_data["base_url"] = request.base_url
19. request_data["url_root"] = request.url_root
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
427/755
20. request_data["accept_charsets"] = request.accept_charsets
21. request_data["accept_encodings"] = request.accept_encodings
22. request_data["accept_languages"] = request.accept_languages
23. request_data["accept_mimetypes"] = request.accept_mimetypes
24. request_data["args"] = request.args
25. request_data["content_encoding"] = request.content_encoding
26. request_data["content_length"] = request.content_length
27. request_data["content_type"] = request.content_type
28. request_data["endpoint"] = request.endpoint
29. request_data["files"] = request.files
30. request_data["form"] = request.form
31. request_data["host"] = request.host
32. request_data["method"] = request.method
33. request_data["query_string"] = request.query_string.decode()
34. request_data["referrer"] = request.referrer
35. request_data["remote_addr"] = request.remote_addr
36. request_data["remote_user"] = request.remote_user
37. request_data["scheme"] = request.scheme
38. request_data["script_root"] = request.script_root
39. request_data["user_agent"] = f"{request.user_agent}"
40. request_data["values"] = request.values
41. # réponse HTTP
42. response = make_response(request_data)
43. # headers HTTP
44. response.headers["Content-Type"] = "application/json; charset=utf-8"
45. # envoi réponse HTTP
46. return response, status.HTTP_200_OK
47.
48.
49. # main
50. if __name__ == '__main__':
51. app.config.update(ENV="development", DEBUG=True)
52. app.run()
• ligne 9 : nous introduisons un changement. Nous précisons quelles sont les verbes autorisés dans la requête du client. Postman
en donne la liste :
Les deux premiers [GET, POST] sont les plus utilisés et seront également les seuls à être utilisés dans ce document. Pour en
revenir à la ligne 9 du code, le paramètre [methods] contient la liste des méthodes de la liste ci-dessus autorisées par l’URL.
En l’absence de ce paramètre, seule la méthode [GET] est autorisée. C’est ce qui s’est passé jusqu’à maintenant ;
• ligne 12 : nous allons construire le dictionnaire [request_data] ;
• ligne 13 : la requête du client est disponible dans un objet prédéfini [request], importé ligne 2, de type
[werkzeug.local.LocalProxy]. Les lignes qui suivent récupèrent divers attributs de cet objet ;
• plutôt que de détailler chaque attribut de l’objet [request], nous allons exécuter ce code et regarder les résultats. On
comprendra alors mieux la signification des différents attributs affichés ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
428/755
• ligne 42 : le dictionnaire [request_data] sera le contenu de la réponse HTTP. On se rappelle que celui-ci doit être du texte.
Flask transforme automatiquement les dictionnaires en chaînes jSON ;
• ligne 44 : on dit au client qu’il va recevoir du jSON ;
• ligne 46 : on envoie la réponse au client ;
Avec le client Postman, nous envoyons la requête suivante au service web précédent :
• en [5-7], nous ajoutons des paramètres dans le corps (=body) de le requête. Alors que les paramètres de l’URL sont visibles
par l’utilisateur d’un navigateur web, ceux qui font partie du corps de la requête ne sont pas visibles. Le navigateur (ou Postman
ici) les envoie au serveur après les entêtes HTTP. La requête du client web a alors la même structure que la réponse du serveur
web : des entêtes HTTP suivis par un document. Cela va faire apparaître deux nouveaux entêtes HTTP dans la requête du
client :
o [Content-Type] : le client dit au serveur quel type de document il envoie ;
o [Content-Length] : la taille du document en octets ;
• en [6], le codage à employer pour les paramètres déclarés en [7]. Ceux-ci peuvent être codés de diverses façons. [x-www-form-
urlencoded] est une méthode utilisée fréquemment par les navigateurs ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
429/755
La réponse à cette requête est la suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
430/755
Le reste de la réponse est le suivant :
Requête du client :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
431/755
1. HTTP/1.0 200 OK
2. Content-Type: application/json; charset=utf-8
3. Content-Length: 2433
4. Server: Werkzeug/1.0.1 Python/3.8.1
5. Date: Wed, 15 Jul 2020 06:09:09 GMT
6.
7. {
8. "accept_charsets": [],
9. "accept_encodings": [
10. [
11. "gzip",
12. 1
13. ],
14. [
15. "deflate",
16. 1
17. ],
18. [
19. "br",
20. 1
21. ]
22. ],
23. "accept_languages": [],
24. "accept_mimetypes": [
25. [
26. "*/*",
27. 1
28. ]
29. ],
30. "args": {
31. "param1": "valeur1",
32. "param2": "valeur2"
33. },
34. "base_url": "http://localhost:5000/",
35. "content_encoding": null,
36. "content_length": 60,
37. "content_type": "application/x-www-form-urlencoded",
38. "endpoint": "index",
39. "environ": "{'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.input': <_io.BufferedReader
name=908>, 'wsgi.errors': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>,
'wsgi.multithread': True, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'werkzeug.server.shutdown':
<function WSGIRequestHandler.make_environ.<locals>.shutdown_server at 0x00000173CA6E5160>,
'SERVER_SOFTWARE': 'Werkzeug/1.0.1', 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'PATH_INFO': '/',
'QUERY_STRING': 'param1=valeur1¶m2=valeur2', 'REQUEST_URI': '/?param1=valeur1¶m2=valeur2',
'RAW_URI': '/?param1=valeur1¶m2=valeur2', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': 50592,
'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '5000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'HTTP_USER_AGENT':
'PostmanRuntime/7.26.1', 'HTTP_ACCEPT': '*/*', 'HTTP_CACHE_CONTROL': 'no-cache', 'HTTP_POSTMAN_TOKEN':
'cbfac6aa-71a0-4076-a0c3-91d36d74a4c0', 'HTTP_HOST': 'localhost:5000', 'HTTP_ACCEPT_ENCODING': 'gzip,
deflate, br', 'HTTP_CONNECTION': 'keep-alive', 'CONTENT_TYPE': 'application/x-www-form-urlencoded',
'CONTENT_LENGTH': '60', 'werkzeug.request': <Request 'http://localhost:5000/?param1=valeur1¶m2=valeur2'
[GET]>}",
40. "files": {},
41. "form": {
42. "nom": "s\u00e9l\u00e9n\u00e9",
43. "pr\u00e9nom": "agla\u00eb",
44. "\u00e2ge": "77"
45. },
46. "full_path": "/?param1=valeur1¶m2=valeur2",
47. "host": "localhost:5000",
48. "method": "GET",
49. "path": "/",
50. "query_string": "param1=valeur1¶m2=valeur2",
51. "referrer": null,
52. "remote_addr": "127.0.0.1",
53. "remote_user": null,
54. "scheme": "http",
55. "script_root": "",
56. "url": "http://localhost:5000/?param1=valeur1¶m2=valeur2",
57. "url_root": "http://localhost:5000/",
58. "user_agent": "PostmanRuntime/7.26.1",
59. "values": {
60. "nom": "s\u00e9l\u00e9n\u00e9",
61. "param1": "valeur1",
62. "param2": "valeur2",
63. "pr\u00e9nom": "agla\u00eb",
64. "\u00e2ge": "77"
65. }
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
432/755
66. }
• lignes 1-5 : les entêtes HTTP de la réponse terminés par une ligne vide ;
• lignes 41-45 : les éléments accentués ont subi un encodage UTF-8 ;
Si maintenant on utilise la méthode [POST] pour envoyer la même requête avec les mêmes paramètres, on obtiendra la même réponse
si ce n’est qu’en [12], on aura [‘method’ : ‘POST’].
Aussi quelle est la différence entre les méthodes GET et POST ? La différence est mince et a été instituée par l’usage qu’en ont fait
historiquement les navigateurs :
• les paramètres dans l’URL sont pratiques parce qu’une URL ainsi paramétrée peut servir de lien dans un document HTML.
L’utilisateur peut également changer les paramètres lui-même pour obtenir des réponses différentes du serveur. Dans ce cas,
les navigateurs utilisent couramment la méthode [GET] et il n’y a pas de corps (content_length=0) dans la requête envoyée au
serveur web (pas de paramètres cachés) ;
• parfois on ne veut pas que les paramètres soient affichés dans l’URL. C’est le cas des mots de passe envoyés au serveur. Par
ailleurs, la taille occupée par les paramètres de l’URL est limitée (une URL ne peut dépasser une certaine taille). Les paramètres
du corps de la requête n’ont pas cette limitation. Egalement, beaucoup de paramètres dans l’URL la rendent illisible. Prenons
le cas courant d’un formulaire d’inscription à un site web. Historiquement, lorsque les pages HTML n’embarquaient pas encore
du Javascript, les navigateurs envoyaient les renseignements saisis par un POST. On parlait alors de valeurs postées ;
• les méthodes GET étaient plutôt associées à la demande d’informations délivrées par un serveur web ;
• les méthodes POST étaient plutôt associées à l’envoi d’informations du navigateur vers le serveur. Le serveur était alors ‘enrichi’
par celles-ci ;
Depuis Javascript est passé par là. Alors que dans les exemples précédents, le développeur n’avait pas la main (cliquer sur un lien
déclenchait forcément un GET, valider un formulaire passait forcément par un POST), le Javascript leur a redonné la main. Dans ce
modèle, la page HTML est associée à du code Javascript qui peut court-circuiter le navigateur. Ainsi le clic sur un lien peut-il être
intercepté par le code Javascript qui peut ensuite exécuter un code faisant une requête au serveur. Cette requête sera transparente
pour l’utilisateur. Il ne la verra pas. Ce code est un client web et comme nous l’avons fait avec Postman le développeur peut créer la
requête qu’il veut. Pour revenir sur le clic sur un lien, il peut réaliser un POST alors que par défaut le navigateur aurait réalisé un GET.
Ces évolutions ont rendu les différences entre GET et POST moins pertinentes.
• un GET ne doit pas modifier l’état du serveur. Des GET successifs réalisés avec les mêmes paramètres dans l’URL divent
ramener le même document. De plus le GET n’a le plus souvent pas de corps (pas de document associé), seulement des
paramètres dans l’URL ;
• le POST peut modifier l’état du serveur. Les paramètres sont le plus souvent envoyés dans le corps de la requête. On parle
alors de valeurs postées. L’exemple du formulaire est le plus éloquent : les valeurs saisies par l’utilisateur vont être mises dans
le corps du POST et le serveur va les enregistrer quelque part, souvent une base de données ;
Dans la suite du document, nous ne nous astreignons à respecter aucune règle particulière.
Si le même client fait peu après une nouvelle demande au serveur web, une nouvelle connexion est créée entre le client et le serveur.
Celui-ci ne peut pas savoir si le client qui se connecte est déjà venu ou si c'est une première demande. Entre deux connexions, le
serveur "oublie" son client. Pour cette raison, on dit que le protocole HTTP est un protocole sans état. Il est pourtant utile que le
serveur se souvienne de ses clients. Ainsi si une application est sécurisée, le client va envoyer au serveur un login et un mot de passe
pour s'identifier. Si le serveur "oublie" son client entre deux connexions, celui-ci devra s'identifier à chaque nouvelle connexion, ce
qui n'est pas envisageable.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
433/755
Pour faire le suivi d'un client, le serveur peut procéder de diverses façons :
1. lors d'une première demande d'un client, il inclut dans sa réponse un identifiant que le client doit ensuite lui renvoyer à chaque
nouvelle demande. Grâce à cet identifiant, différent pour chaque client, le serveur peut reconnaître un client. Il peut alors gérer
une mémoire pour ce client sous la forme d'une mémoire associée de façon unique à l'identifiant du client. C’est ainsi que
fonctionnent, par exemple, les services PHP ;
2. lors d'une première demande d'un client, il inclut dans sa réponse non pas un identifiant mais la mémoire de l’utilisateur elle-
même. Il ne garde rien côté serveur. Pour maintenir sa mémoire, le client web doit renvoyer cette mémoire à chaque nouvelle
requête. Celle-ci est modifiée (ou non) à chaque nouvelle requête et renvoyée (ou non) au client. C’est la méthode utilisée par le
framework Flask ;
• la méthode 1 est moins gourmande en bande passante. Seul est échangé entre le client et le serveur un identifiant. Lorsque la
mémoire de l’utilisateur grandit, cela n’a aucune conséquence sur l’identifiant qui reste le même. Ce n’est pas le cas de la
méthode 2 où la mémoire de l’utilisateur est échangée à chaque requête et peut grossir au fil des requêtes ;
• la méthode 1 est plus gourmande en espace mémoire. En effet, le serveur stocke la mémoire de l’utilisateur sur ses systèmes
de fichiers. S’il y a un million d’utilisateurs, cela peut peut-être poser un problème. La méthode 2 ne stocke rien sur le serveur ;
• dans la réponse à un nouveau client, le serveur inclut l'en-tête HTTP [Set-Cookie : MotClé=Identifiant] ou [Set-Cookie :
mémoire]. Avec la méthode 1, il ne fait cela qu'à la première demande. Avec la méthode 2, il le fait à chaque fois que la
mémoire de l’utilisateur change ;
• dans ses demandes, le client renvoie systématiquement ce qu’il a reçu, un identifiant ou une mémoire. Il le fait via l'en-tête
HTTP [Cookie : MotClé=Valeur] ;
On peut se demander comment le serveur fait pour savoir qu'il a affaire à un nouveau client plutôt qu'à un client déjà venu. C'est la
présence de l'en-tête HTTP Cookie dans les en-têtes HTTP du client qui le lui indique. Pour un nouveau client, cet en-tête est absent.
L'ensemble des connexions d'un client donné est appelé une session.
• en [1], la mémoire de la requête est particulière. Elle est utilisée lorsque la demande du client web est traitée non pas par un
service (ou application) mais par plusieurs. Pour passer des informations au service i+1, le service i peut enrichir la requête
traitée (request) avec ces informations. C’est ce qu’on appelle la mémoire de niveau requête. Nous n’utiliserons pas ce type de
mémoire dans ce document ;
• en [2, 4], la mémoire de l’utilisateur que nous venons de décrire. Elle peut être implémentée localement [2] ou maintenue à
l’aide du client [4] ;
• en [3], la mémoire de niveau ‘application’ est très généralement une mémoire en lecture seule. Elle est partagée par tous les
utilisateurs. On y retrouve souvent des éléments de la configuration de l’application web, configuration partagée par tous les
utilisateurs de l’application. On doit être prudents avec ce type de mémoire : l’écriture dedans doit se faire à un moment où
les utilisateurs n’ont pas encore envoyé de requêtes, au démarrage de l’application le plus souvent. Ensuite, lorsque les requêtes
arrivent, il est difficile d’écrire dans cette mémoire. Lorsque le serveur web sert simultanément plusieurs utilisateurs et que
deux d’entre-eux veulent écrire dans la mémoire de niveau ‘application’, il y a un risque que cette mémoire soit corrompue.
En effet, alors que l’utilisateur 1 a commencé à écrire dans la mémoire de niveau ‘application’, il peut être interrompu avant
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
434/755
même d’avoir fini. On a alors une mémoire d’application incomplète. Comme elle est partagée, un utilisateur 2 peut la lire et
obtenir un état incorrect ;
1. # on configure l'application
2. import config
3. config = config.configure()
4.
5. # dépendances
6. import json
7. from flask import Flask, make_response, session
8. from flask_api import status
9.
10. # application Flask
11. app = Flask(__name__)
12.
13. # clé secrète de la session
14. app.secret_key = config["SECRET_KEY"]
15.
16.
17. @app.route('/set-session', methods=['GET'])
18. def set_session():
19. # on met qq chose dans la session
20. session['nom'] = 'séléné'
21. # on envoie une réponse vide
22. response = make_response()
23. response.headers['Content-Length'] = 0
24. return response, status.HTTP_200_OK
25.
26.
27. @app.route('/get-session', methods=['GET'])
28. def get_session():
29. # on récupère la session et on envoie la réponse
30. response = make_response(json.dumps({"nom": session['nom']}, ensure_ascii=False))
31. response.headers['Content-Type'] = 'application/json; charset=utf-8'
32. return response, status.HTTP_200_OK
33.
34.
35. # main seulement
36. if __name__ == '__main__':
37. app.config.update(ENV="development", DEBUG=True)
38. app.run()
1. # on rend la config
2. config = {
3. # configuration Flask
4. "SECRET_KEY": "vibnFfrdWYUp?*LQ"
5. }
• pour la première fois, nous définissons une application web qui sert autre chose que l’URL /
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
435/755
o ligne 17 : l’URL [/set-session] sert à initialiser la session de l’utilisateur ;
o ligne 27 : l’URL [/get-session] sert à retrouver la mémoire de l’utilisateur (ou session de l’utilisateur) ;
• ligne 20 : on met quelque chose dans la mémoire (= la session) de l’utilisateur, ici un nom. La session se gère un peu comme
un dictionnaire. On ne peut pas mettre n’importe quoi dans la session. Il faut que les valeurs qu’on y met puissent être
transformées en jSON. Pour les types prédéfinis de Python, cela se fait sans intervention du développeur. Pour des objets
propriétaires que Python ne connaît pas, il faut faire soi-même la conversion jSON ;
• ligne 22 : on crée une réponse HTTP sans contenu (absence de paramètre à make_response) ;
• ligne 23 : on dit au client qu’il va recevoir un document vide (taille de 0 octet) ;
• ligne 24 : on envoie la réponse HTTP au client. L’URL [/set-session] ne fait donc rien d’autres que d’initialiser une session
utilisateur ;
• ligne 27 : l’URL [/get-session] permet à l’utilisateur de savoir ce qu’il y a dans sa session ;
• ligne 30 : on crée une réponse HTTP contenant la chaîne jSON de la session de l’utilisateur. Ici on a créé la chaîne jSON
nous-mêmes au lieu de laisser Flask la générer. En effet, on ne veut pas que les caractères accentués soient échappés
(ensure_ascii=False) ;
• ligne 31 : on dit au client qu’on lui envoie du jSON ;
• ligne 32 : on envoie la réponse HTTP au client ;
Le but de ce script est de montrer que la session utilisateur permet de faire le lien entre les requêtes successives de celui-ci :
Le script [config] qui configure les scripts du dossier [flask/05] est le suivant :
1. def configure():
2. # chemin absolu référence des chemins relatifs de la configuration
3. root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
4.
5. # dépendances de l'application
6. absolute_dependencies = [
7. # Personne, Utils, MyException
8. f"{root_dir}/classes/02/entities",
9. ]
10. # on fixe le syspath
11. from myutils import set_syspath
12. set_syspath(absolute_dependencies)
13.
14. # on rend la config
15. config = {
16. # configuration Flask
17. "SECRET_KEY": "vibnFfrdWYUp?*LQ"
18. }
19.
20. return config
Nous lançons le script [session_scope_01] puis avec Postman nous allons demander l’URL [/set-session]. Auparavant nous allons
vérifier quelques éléments de la requête qui va être faite :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
436/755
• en [1], accéder aux cookies de Postman ;
• en [2-4], on vérifie les cookies connus de Postman et on les supprime tous [4-5] ;
• en [9] : une partie des entêtes HTTP que Postman va mettre dans la requête à partir de la configuration que nous avons pu
faire pour celle-ci. Cette vérification vous permet de vérifier que vous n’avez pas omis des paramètres ou au contraire laissé
des paramètres inutiles ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
437/755
• en [1-2], la requête faite au service web ;
• en [3-6], les entêtes HTTP de la réponse ;
• en [4], comme dans le code on n’a pas précisé le type de la réponse, Flask a utilisé par défaut le type [text/html] ;
• en [5], le client sait qu’il n’y a pas de document dans la réponse ;
• ligne 6 : l’entête [Set-Cookie] a été envoyé par le serveur Flask. Sa valeur est appelée un cookie de session. Elle est constituée
de trois éléments :
o [session=valeur] : valeur représente la mémoire de l’utilisateur sous une forme codée. Cette mémoire est décodable (cf.
|https://blog.miguelgrinberg.com/post/how-secure-is-the-flask-user-session|). Néanmoins à cause de la clé secrète
utilisée par le serveur, l’utilisateur ne peut pas modifier la mémoire reçue pour la renvoyer ensuite au serveur. Lorsque le
serveur reçoit une session, il est ainsi assuré de recevoir une session non corrompue ;
o [HttpOnly] : la présence de cet élément indique au navigateur qui le reçoit que le cookie ne doit pas être accessible au
Javascript que peut contenir la page qu’il affiche ;
o [Path=/] est le chemin pour lequel il faut renvoyer le cookie de session donc ici tout chemin de l’application web. A
chaque fois que l’utilisateur au clavier demandera explicitement (il tape une URL) ou implicitement (il clique sur un lien)
une URL de ce domaine, le navigateur renverra automatiquement le cookie de session qu’il a reçu ;
L’inconvénient de la fenêtre principale est qu’on n’a pas accès à la requête complète qui a amené à cette réponse. Ce qui est présenté
dans cette fenêtre prête à confusion :
• dans les entêtes HTTP [3-4] est présenté en [5] un cookie de session. On pourrait croire alors que Postman a mis dans la
requête un cookie de session alors que ce n’est pas le cas. Les entêtes [3] représentent en fait les entêtes HTTP qui seront
envoyés lors de la prochaine requête telle que celle-ci est actuellement configurée. Postman vient de recevoir un cookie de
session qu’il renverra lors de la prochaine requête. C’est pourquoi on a [5] ;
On peut avoir accès au dialogue client / serveur dans la console Postman qu’on obtient avec Ctrl-Alt-C :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
438/755
1. GET /get-session HTTP/1.1
2. User-Agent: PostmanRuntime/7.26.1
3. Accept: */*
4. Cache-Control: no-cache
5. Postman-Token: ce991398-2d9a-46d0-9ccd-c7ff3c7f4d6d
6. Host: localhost:5000
7. Accept-Encoding: gzip, deflate, br
8. Connection: keep-alive
9. Cookie: session=eyJub20iOiJzXHUwMGU5bFx1MDBlOW5cdTAwZTkifQ.Xw6jGQ.y5Icu70wTIN-B0o_hwx0xDH247I
10.
11. HTTP/1.0 200 OK
12. Content-Type: application/json; charset=utf-8
13. Content-Length: 20
14. Vary: Cookie
15. Server: Werkzeug/1.0.1 Python/3.8.1
16. Date: Wed, 15 Jul 2020 06:36:52 GMT
17.
18. {"nom": "séléné"}
• ligne 9 : le client Postman a renvoyé au serveur le cookie de session qu’il avait reçu ;
• ligne 18 : la chaîne jSON envoyée par le serveur ;
• le client Postman renvoie le cookie de session qu’il reçoit du serveur Flask. Les navigateurs web procèdent toujours ainsi ;
• nous voyons que la requête 2 [/get-session] a permis de récupérer une information créée lors de la requête 1 [/set-session].
On a donc là une mémoire de l’utilisateur ;
• lignes 11-16 : le serveur Flask n’a pas renvoyé de cookie de session. Ce n’est pas systématique. Le serveur Flask ne renvoie le
cookie de session que si la dernière requête a modifié la mémoire de l’utilisateur ;
1. # dépendances
2. import os
3.
4. from flask import Flask, make_response, session
5. from flask_api import status
6.
7. # application Flask
8. app = Flask(__name__)
9.
10. # clé secrète de la session
11. app.secret_key = os.urandom(12).hex()
12.
13.
14. # Home URL
15. @app.route('/', methods=['GET'])
16. def index():
17. # on gère tois compteurs
18. if session.get('n1') is None:
19. session['n1'] = 0
20. else:
21. session['n1'] = session['n1'] + 1
22. if session.get('n2') is None:
23. session['n2'] = 10
24. else:
25. session['n2'] = session['n2'] + 1
26. if session.get('n3') is None:
27. session['n3'] = 100
28. else:
29. session['n3'] = session['n3'] + 1
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
439/755
30. # dictionnaire des compteurs
31. compteurs = {"n1": session['n1'], "n2": session['n2'], "n3": session['n3']}
32. # on envoie la réponse
33. response = make_response(compteurs)
34. response.headers['Content-Type'] = 'application/json; charset=utf-8'
35. return response, status.HTTP_200_OK
36.
37.
38. # main
39. if __name__ == '__main__':
40. app.config.update(ENV="development", DEBUG=True)
41. app.run()
• ligne 11 : ici la clé secrète est générée à l’aide d’une fonction. L’intérêt de celle-ci est qu’elle génère une chaîne de caractères
complexe de façon aléatoire. On rappelle que la variable [app] est l’instance de classe Flask créée ligne 8 ;
• ligne 15 : cette fois-ci, il n’y aura qu’une route, la route / ;
• lignes 17-29 : on gère une session contenant trois compteurs [n1, n2, n3]. Lors du 1er appel de l’utilisateur [n1, n2, n3]=[0,
10, 100] puis à chaque appel ces compteurs sont incrémentés de 1 ;
• ligne 18 : lors de la 1ère requête, la session de l’application est vide. L’expression [session.get(‘clé’)] rend la valeur [None].
Pour les requêtes suivantes, cette expression rendra la valeur associée à la clé ;
• ligne 31 : ces compteurs sont mis dans un dictionnaire ;
• ligne 33 : ce dictionnaire est le document de la réponse HTTP. On rappelle, que Flask transforme automatiquement les
dictionnaires en chaîne jSON ;
• ligne 34 : on dit au client web qu’il va recevoir du jSON ;
• ligne 35 : on envoie la réponse HTTP au client ;
Exécutons ce script et interrogeons l’application web ainsi créée avec Postman après avoir supprimé tous les cookies du client
Postman [1-3] :
Dans la console Postman, les échanges client / serveur sont les suivants :
1. GET / HTTP/1.1
2. User-Agent: PostmanRuntime/7.26.1
3. Accept: */*
4. Cache-Control: no-cache
5. Postman-Token: c7db536d-9352-4aa6-9877-04560e03d935
6. Host: localhost:5000
7. Accept-Encoding: gzip, deflate, br
8. Connection: keep-alive
9.
10. HTTP/1.0 200 OK
11. Content-Type: application/json; charset=utf-8
12. Content-Length: 41
13. Vary: Cookie
14. Set-Cookie: session=eyJuMSI6MCwibjIiOjEwLCJuMyI6MTAwfQ.Xw6nLg.v49CeDWwqP-6Dp9Qt330GAe-dNA; HttpOnly; Path=/
15. Server: Werkzeug/1.0.1 Python/3.8.1
16. Date: Wed, 15 Jul 2020 06:50:22 GMT
17.
18. {
19. "n1": 0,
20. "n2": 10,
21. "n3": 100
22. }
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
440/755
• en [14], le cookie de session envoyé par le serveur ;
• en [18-22], la réponse du serveur sous la forme d’une chaîne jSON ;
Refaisons une deuxième fois la même requête. Les logs évoluent de la façon suivante :
1. GET / HTTP/1.1
2. User-Agent: PostmanRuntime/7.26.1
3. Accept: */*
4. Cache-Control: no-cache
5. Postman-Token: 8205ad85-37b3-41f2-a171-70dd3b3a1679
6. Host: localhost:5000
7. Accept-Encoding: gzip, deflate, br
8. Connection: keep-alive
9. Cookie: session=eyJuMSI6MCwibjIiOjEwLCJuMyI6MTAwfQ.Xw6nLg.v49CeDWwqP-6Dp9Qt330GAe-dNA
10.
11. HTTP/1.0 200 OK
12. Content-Type: application/json; charset=utf-8
13. Content-Length: 41
14. Vary: Cookie
15. Set-Cookie: session=eyJuMSI6MSwibjIiOjExLCJuMyI6MTAxfQ.Xw6nsw.OuxIQnGhmhSsan5Qu_FL3Iyu-9k; HttpOnly; Path=/
16. Server: Werkzeug/1.0.1 Python/3.8.1
17. Date: Wed, 15 Jul 2020 06:52:35 GMT
18.
19. {
20. "n1": 1,
21. "n2": 11,
22. "n3": 101
23. }
1. # on configure l'application
2. import config
3. config = config.configure()
4.
5. # dépendances
6. import json
7. import os
8.
9. from flask import Flask, make_response, session
10. from flask_api import status
11. from Personne import Personne
12.
13. # application Flask
14. app = Flask(__name__)
15.
16. # clé secrète de la session
17. app.secret_key = os.urandom(12).hex()
18.
19.
20. # Home URL
21. @app.route('/', methods=['GET'])
22. def index():
23. # gestion d'une liste
24. liste = session.get('liste')
25. if liste is None:
26. # 1ère requête
27. liste = [0, 10, 100]
28. else:
29. # requêtes suivantes
30. for i in range(len(liste)):
31. liste[i] += 1
32. # on remet la liste dans la session
33. session['liste'] = liste
34.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
441/755
35. # gestion d'un dictionnaire
36. dico = session.get('dico')
37. if not dico:
38. # 1ère requête
39. dico = {"un": 0, "deux": 10, "trois": 100}
40. else:
41. # requêtes suivantes
42. dico = session['dico']
43. for key in dico.keys():
44. dico[key] += 1
45. # on remet le dictionnaire dans la session
46. session['dico'] = dico
47.
48. # gestion d'une personne
49. personne_json = session.get('personne')
50. if personne_json is None:
51. # 1ère requête
52. personne = Personne().fromdict({"prénom": "aglaë", "nom": "séléné", "âge": 70})
53. else:
54. # requêtes suivantes
55. personne = Personne().fromjson(personne_json)
56. personne.âge += 1
57. # on remet la personne dans la session
58. session['personne'] = personne.asjson()
59.
60. # dictionnaire des résultats
61. résultats = {"liste": liste, "dict": dico, "personne": personne.asdict()}
62.
63. # on envoie une réponse jSON
64. response = make_response(json.dumps(résultats, ensure_ascii=False))
65. response.headers['Content-Type'] = 'application/json; charset=utf-8'
66. return response, status.HTTP_200_OK
67.
68.
69. # main
70. if __name__ == '__main__':
71. app.config.update(ENV="development", DEBUG=True)
72. app.run()
On lance l’application web. On supprime tous les cookies du client Postman. Puis celui-ci demande l’URL [http://localhost:5000].
Le dialogue client / serveur dans la console Postman est le suivant :
1. GET / HTTP/1.1
2. User-Agent: PostmanRuntime/7.26.1
3. Accept: */*
4. Cache-Control: no-cache
5. Postman-Token: 5f8b7c63-aa8a-4429-a2fa-62141423d933
6. Host: localhost:5000
7. Accept-Encoding: gzip, deflate, br
8. Connection: keep-alive
9.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
442/755
10. HTTP/1.0 200 OK
11. Content-Type: application/json; charset=utf-8
12. Content-Length: 135
13. Vary: Cookie
14. Set-Cookie: session=.eJw9isEKwyAQRH-lzHkPm15K91dqD2mzBMFq0AgF8d-
jsRQG9u3MK1jsO0AKFs1fyMSEPQabOjbOHsKV4GzaFfJgmnr4Sdg0puB9a1EMtmgys959-
BjIxWBe3XxWLwNq_39IQ3Q_f5zhnHxdtYs3rqgH4gQvMg.Xw6yGw.Bwpt3q-sH03gFLmg2FIPXV_ZNt8; HttpOnly; Path=/
15. Server: Werkzeug/1.0.1 Python/3.8.1
16. Date: Wed, 15 Jul 2020 07:36:59 GMT
17.
18. {"liste": [0, 10, 100], "dict": {"un": 0, "deux": 10, "trois": 100}, "personne": {"prénom": "aglaë", "nom":
"séléné", "âge": 70}}
1. GET / HTTP/1.1
2. User-Agent: PostmanRuntime/7.26.1
3. Accept: */*
4. Cache-Control: no-cache
5. Postman-Token: 40fd00ea-d45c-46b7-a51e-d4d433a37b5c
6. Host: localhost:5000
7. Accept-Encoding: gzip, deflate, br
8. Connection: keep-alive
9. Cookie: session=.eJw9isEKwyAQRH-lzHkPm15K91dqD2mzBMFq0AgF8d-
jsRQG9u3MK1jsO0AKFs1fyMSEPQabOjbOHsKV4GzaFfJgmnr4Sdg0puB9a1EMtmgys959-
BjIxWBe3XxWLwNq_39IQ3Q_f5zhnHxdtYs3rqgH4gQvMg.Xw6yGw.Bwpt3q-sH03gFLmg2FIPXV_ZNt8
10.
11. HTTP/1.0 200 OK
12. Content-Type: application/json; charset=utf-8
13. Content-Length: 135
14. Vary: Cookie
15. Set-Cookie: session=.eJw9isEKwyAQRH-lzHkP2kupv9LtIW2WIBgNGqEg_nu3seQ0b2Zew-zfCa5hlvqBs5aw5-SLolGuUaETgi-
7wD0sqaHPk7BJLilGXdEYW-ZqjNxjWhnuwpiWMB3Ti0Haz6MMMfz9EcM5-LrIT7zZjv4F5NYvOQ.Xw6ydQ.PMWRCqKx9HNnb_DyK-ha-
9pCF7M; HttpOnly; Path=/
16. Server: Werkzeug/1.0.1 Python/3.8.1
17. Date: Wed, 15 Jul 2020 07:38:29 GMT
18.
19. {"liste": [1, 11, 101], "dict": {"deux": 11, "trois": 101, "un": 1}, "personne": {"prénom": "aglaë", "nom":
"séléné", "âge": 71}}
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
443/755
22.7.2 script [application_scope_01]
Le script [application_scope_01] montre une façon de gérer des données de portée ‘application’ :
1. # on configure l'application
2. import config
3. config = config.configure()
4.
5. # dépendances
6. from flask import Flask, make_response
7. from flask_api import status
8.
9. # application Flask
10. app = Flask(__name__)
11.
12.
13. # Home URL
14. @app.route('/', methods=['GET'])
15. def index():
16. # on vise à montrer que l'application reste en mémoire entre les requêtes des différents clients
17. # chaque client a affaire à la même application
18.
19. # app_infos représente des informations de niveau application et non de niveau session
20. # c-à-d qu'elle concerne tous les utilisateurs et non un en particulier
21. # cette information est ici stockée dans [config] (pas obligatoire)
22.
23. # dictionnaire des résultats
24. résultats = {"config": config}
25.
26. # on envoie la réponse
27. response = make_response(résultats)
28. response.headers['Content-Type'] = 'application/json; charset=utf-8'
29. return response, status.HTTP_200_OK
30.
31.
32. # main
33. if __name__ == '__main__':
34. # on vérifie si ce code est exécuté plusieurs fois
35. print("application app lancée")
36. # on lance l'application web
37. app.config.update(ENV="development", DEBUG=True)
38. app.run()
• lignes 1-3 : on récupère le dictionnaire de la configuration. On va montrer que le code situé en-dehors des fonctions de routage
n’est exécuté qu’une fois. L’application Flask reste en mémoire. Toutes les informations initialisées en-dehors des routes sont
globales à celles-ci et donc connues de celles-ci. Ainsi le dictionnaire [config] de la ligne 3 va-t-il être rendu par la route /
(ligne 24). On va montrer que tous les clients web vont recevoir le même dictionnaire et que celui-ci est donc partagé par tous
les clients. C’est donc une information de portée ‘application’ ;
• ligne 35 : on met un log pour voir si le code des lignes en-dehors de la fonction de routage (lignes 1-10, 32-38) est exécuté
plusieurs fois ;
1. def configure():
2. # on rend la config
3. config = {
4. # configuration Flask
5. "SECRET_KEY": "vibnFfrdWYUp?*LQ"
6. }
7.
8. return config
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
444/755
Nous lançons cette application. Les logs dans la console PyCharm sont les suivants :
1. GET / HTTP/1.1
2. User-Agent: PostmanRuntime/7.26.1
3. Accept: */*
4. Cache-Control: no-cache
5. Postman-Token: 51e75099-8ecb-4f27-ae3b-9386e982ede4
6. Host: localhost:5000
7. Accept-Encoding: gzip, deflate, br
8. Connection: keep-alive
9.
10. HTTP/1.0 200 OK
11. Content-Type: application/json; charset=utf-8
12. Content-Length: 39
13. Server: Werkzeug/1.0.1 Python/3.8.1
14. Date: Wed, 15 Jul 2020 10:34:26 GMT
15.
16. {
17. "SECRET_KEY": "vibnFfrdWYUp?*LQ"
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
445/755
18. }
• les deux logs [1, 2] sont toujours là mais il n’y en a pas d’autres alors que l’on voit les trois requêtes reçues par le serveur
web ;
Pour être totalement sûrs que l’application n’est pas rechargée à chaque nouvelle requête, on peut mettre un compteur dans la
configuration et l’incrémenter à chaque nouvelle requête. On verra alors que chaque client voit le compteur dans l’état où l’a laissé le
précédent client. On rappelle cependant que les clients ne devraient pas modifier des données de portée application parce qu’elles
sont partagées entre tous les clients et que dans un contexte où le serveur sert simultanément plusieurs clients sans garantie que la
requête d’un client soit exécutée entièrement sans être interrompue, un client 1 qui a émis une requête 1 interrompue avant sa fin
peut laisser les données partagées dans un état corrompu pour les clients suivants.
Le script [application_scope_02] va faire ce qu’il ne faut pas faire : permettre aux clients de modifier des informations partagées
avec les autres utilisateurs. On va partager un compteur entre les utilisateurs qui vont l’incrémenter. On va voir que chaque utilisateur
voit les modifications apportées par les autres utilisateurs au compteur.
1. # dépendances
2.
3. from flask import Flask, make_response
4. from flask_api import status
5.
6. # application Flask
7. app = Flask(__name__)
8.
9. # données de portée application
10. config = {
11. "counter": 0
12. }
13.
14.
15. # Home URL
16. @app.route('/', methods=['GET'])
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
446/755
17. def index():
18. # on vise à montrer que le dictionnaire [config] est partagé entre tous les clients
19. # de l'application web
20.
21. # on incrémente le compteur
22. config["counter"] += 1
23. # on envoie la réponse
24. response = make_response(config)
25. response.headers['Content-Type'] = 'application/json; charset=utf-8'
26. return response, status.HTTP_200_OK
27.
28.
29. # main
30. if __name__ == '__main__':
31. app.config.update(ENV="development", DEBUG=True)
32. app.run()
• lignes 10-12 : le dictionnaire [config] partagé par les utilisateurs. Il contient un compteur ;
• ligne 22 : à chaque fois qu’un utilisateur demandera l’URL /, le compteur de la configuration sera incrémenté ;
• lignes 23-26 : la chaîne jSON du dictionnaire est envoyée à chaque client ;
On voit que chaque client récupère le compteur dans l’état où le client précédent l’a laissé. Ils ont donc bien accès à la même
information.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
447/755
22.7.4 script [application_scope_03]
Le script [application_scope_03] montre pourquoi l’information partagée entre utilisateurs doit être en lecture seule.
1. # dépendances
2. import threading
3. from time import sleep
4.
5. from flask import Flask, make_response
6. from flask_api import status
7.
8. # application Flask
9. app = Flask(__name__)
10.
11. # données de portée application
12. config = {
13. "counter": 0
14. }
15.
16.
17. # Home URL
18. @app.route('/', methods=['GET'])
19. def index():
20. # on vise à montrer que le dictionnaire [config] est partagé entre tous les clients
21. # de l'application web et qu'il doit être en lecture seule
22.
23. # nom du thread
24. thread_name = threading.current_thread().name
25. # on lit le compteur
26. counter = config["counter"]
27. print(f"compteur lu : {counter}, par le thread {thread_name}")
28. # on s'arrête 5 secondes - du coup d'autres clients vont être servis
29. sleep(5)
30. # on incrémente le compteur de la configuration
31. config["counter"] = counter + 1
32. # log
33. print(f"compteur écrit : {config['counter']}, par le thread {thread_name}")
34. # on envoie la réponse
35. response = make_response(config)
36. response.headers['Content-Type'] = 'application/json; charset=utf-8'
37. return response, status.HTTP_200_OK
38.
39.
40. # main
41. if __name__ == '__main__':
42. app.config.update(ENV="development", DEBUG=True)
43. app.run(threaded=True)
• ligne 43 : on a changé le mode d’exécution de l’application web. On a écrit [threaded=True] pour indiquer que l’application
devait servir les utilisateurs simultanément. Cela se fait au moyen de threads d’exécution :
o il peut y avoir plusieurs threads d’exécution simultanés, chacun servant un utilisateur ;
o le processeur de la machine est partagé par ces threads ;
o un thread peut être interrompu avant qu’il n’ait terminé son travail. Il sera repris ultérieurement ;
• ligne 19 : la fonction [index] peut être exécutée simultanément par plusieurs threads ;
• ligne 24 : on récupère le nom du thread qui exécute la fonction [index] ;
• ligne 26 : on lit la valeur du compteur. Pour les besoins de notre démonstration on décompose l’incrémentation du compteur
de la façon suivante :
o étape 1 : lecture du compteur (1 par exemple) par le thread 1 ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
448/755
o étape 2 : pause du thread 1 pendant 5 secondes (ligne 29). Parce que le thread 1 a demandé une pause, le processeur est
donné à un autre thread, le thread 2. Le but est que ce nouveau thread lise la même valeur du compteur (=1). Puis lui
aussi fait une pause de 5 secondes et perd le processeur ;
o étape 3 : incrémentation du compteur, ligne 31 à partir de la valeur lue à l’étape 1 (=1). Le thread 1 est le 1er à le faire : il
passe le compteur à 2 puis termine l’exécution de la fonction [index]. Puis c’est au tour du thread 2 de se réveiller et de
passer lui aussi le compteur à 2 à partir de la valeur lue à l’étape 1 (=1). Au final, après le passage des deux threads, le
compteur est à 2 alors qu’il devrait être à 3 ;
• ligne 33 : on affiche la valeur du compteur pour vérification ;
Nous lançons le script puis demandons l’url [http://loaclhost :5000/] avec deux navigateurs puis avec Postman. Les logs dans la
console PyCharm sont alors les suivants :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/flask/06/application_scope_03.py
2. * Serving Flask app "application_scope_03" (lazy loading)
3. * Environment: development
4. * Debug mode: on
5. * Restarting with stat
6. * Debugger is active!
7. * Debugger PIN: 334-263-283
8. * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
9. compteur lu : 0, par le thread Thread-2
10. compteur lu : 0, par le thread Thread-4
11. compteur écrit : 1, par le thread Thread-2
12. 127.0.0.1 - - [16/Jul/2020 08:55:37] "GET / HTTP/1.1" 200 -
13. compteur écrit : 1, par le thread Thread-4
14. 127.0.0.1 - - [16/Jul/2020 08:55:40] "GET / HTTP/1.1" 200 -
15. compteur lu : 1, par le thread Thread-5
16. compteur écrit : 2, par le thread Thread-5
17. 127.0.0.1 - - [16/Jul/2020 08:55:46] "GET / HTTP/1.1" 200 -
• lignes 9-10 : les deux premiers threads 2 et 4 lisent la même valeur 0 du compteur ;
• ligne 11 : le thread 2 passe le compteur à 1 ;
• ligne 13 : le thread 4 passe le compteur à 1. A partir de maintenant la valeur du compteur est incorrecte ;
• lignes 15-16 : le thread 5 n’est pas interrompu et gère correctement la valeur du compteur ;
On retiendra de cet exemple que le code d’une application web ne doit pas modifier la valeur d’informations partagées par les
utilisateurs.
Nous nous intéressons ici à la gestion des routes d’une application, ç-à-d les URL servies par l’application web.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
449/755
11. response = make_response(réponse)
12. response.headers['Content-Type'] = 'text/plain; charset=utf-8'
13. return response, status.HTTP_200_OK
14.
15.
16. # /nom/prenom
17. @app.route('/<string:nom>/<string:prenom>', methods=['GET'])
18. def index(nom, prenom):
19. # réponse
20. return send_plain_response(f"{prenom} {nom}")
21.
22.
23. # init-session
24. @app.route('/init-session/<string:type>', methods=['GET'])
25. def init_session(type: str):
26. # réponse
27. return send_plain_response(f"/init-session/{type}")
28.
29.
30. # authentifier-utilisateur
31. @app.route('/authentifier-utilisateur', methods=['POST'])
32. def authentifier_utilisateur():
33. # réponse
34. return send_plain_response("/authentifier-utilisateur")
35.
36.
37. # calculer-impot
38. @app.route('/calculer-impot', methods=['POST'])
39. def calculer_impot():
40. # réponse
41. return send_plain_response("/calculer-impot")
42.
43.
44. # lister-simulations
45. @app.route('/lister-simulations', methods=['GET'])
46. def lister_simulations():
47. # réponse
48. return send_plain_response("/lister-simulations")
49.
50.
51. # supprimer-simulation
52. @app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
53. def supprimer_simulation(numero: int):
54. # réponse
55. return send_plain_response(f"/supprimer-simulation/{numero}")
56.
57.
58. # fin-session
59. @app.route('/fin-session', methods=['GET'])
60. def fin_session():
61. # réponse
62. return send_plain_response(f"/fin-session")
63.
64.
65. # main
66. if __name__ == '__main__':
67. app.config.update(ENV="development", DEBUG=True)
68. app.run()
• ligne 17 : on précise le type des paramètres de l’URL. Cela permet à Flask de faire des vérifications. Si le paramètre n’est pas
du type attendu, la requête du client sera refusée (erreur 400 Bad Request). Donc Flask fait une partie du travail que nous
aurions du faire ;
• ligne 18 : pour les paramètres, on doit reprendre les noms exacts des paramètres de la ligne 17 mais pas forcément leur ordre ;
• ligne 20 : on utilise la fonction [send_plain_response] pour envoyer la réponse au client web ;
• ligne 9 : la fonction [send_plain_response] reçoit la chaîne de caractères à envoyer au client ;
• ligne 11 : le corps de la réponse HTTP est construit ;
• ligne 12 : on dit au client qu’on lui envoie du texte brut ;
• ligne 13 : on envoie la réponse HTTP ;
• lignes 23-62 : d’autres routes paramétrées qui seront utilisées ultérieurement dans un exercice d’application ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
450/755
22.8.2 script [main_02] : externalisation des routes
Dans le script [main_01] précédent, le code peut devenir important s’il y a de nombreuses routes. Le script [main_02] montre
comment externaliser les routes.
Le script [routes_02] rassemble les fonctions associées aux routes du script précédent :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
451/755
33. return send_response("/calculer-impot")
34.
35.
36. # lister-simulations
37. def lister_simulations():
38. # réponse
39. return send_response("/lister-simulations")
40.
41.
42. # supprimer-simulation
43. def supprimer_simulation(numero: int):
44. # réponse
45. return send_response(f"/supprimer-simulation/{numero}")
46.
47.
48. # fin-session
49. def fin_session():
50. # réponse
51. return send_response(f"/fin-session")
On notera que le script [routes_02] n’est pas un script de routes. C’est une liste de fonctions. C’est le script principal [main_02] qui
fait le lien entre routes et fonctions :
Avec cette méthode, chaque fonction associée à une route peut faire l’objet d’un script séparé si nécessaire.
Les résultats sont les mêmes que ceux obtenus avec le script [main_01] précédent.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
452/755
23 Exercice d’application : version 6
23.1 Introduction
Nous revenons maintenant à notre application de calcul de l’impôt. Nous allons construire autour d’elle différentes applications web.
Dans la version 5 de notre exercice d’application, les données de l’administration fiscale étaient rangées dans une base de données.
Cette version 5 comportait deux applications distinctes mais ayant des couches en commun :
• une application qui calculait l’impôt en mode |batch| pour des contribuables enregistrés dans un fichier texte ;
• une application qui calculait l’impôt en mode |interactif| pour des contribuables dont on saisissait les informations au clavier ;
La version 5 de l’application de calcul de l’impôt par lots (mode batch) avait l’architecture suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
453/755
Ultimement, la version web de cette application aura l’architecture suivante :
• le client web [1] s’adresse au serveur web [2] qui lui communique avec le SGBD [3] ;
• le serveur web [2] conserve les couches [métier] [8] et [dao] [9] de l’application initiale ;
• l’application initiale conserve son script principal [4] et sa couche [métier] [15]. Les couches [métier] [8] et [15] sont
identiques;
• la communication client / serveur nécessite deux couches supplémentaires :
o la couche [web] [7] qui implémente l’application web ;
o la couche [dao] [5] cliente de l’application web [7] ;
Dans la version finale, le calcul de l’impôt par lots pourra se dérouler de deux façons :
• le calcul métier de l’impôt se fait par la couche [métier] du serveur. Le script [main] utilisera cette méthode ;
• le calcul métier de l’impôt se fait par la couche [métier] du client. Le script [main2] utilisera cette méthode ;
A partir de maintenant, nous allons développer plusieurs applications client / serveur du type ci-dessus, chacune illustrant une ou de
nouvelles technologies de développement web.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
454/755
Le script [server_01] est l’application web suivante :
• en [1], on utilise une URL paramétrée dans laquelle on passe trois valeurs :
o [marié] (oui / non) pour indiquer si le contribuable est marié ;
o [enfants] : le nombre d’enfants du contribuable ;
o [salaire] : salaire annuel du contribuable ;
• en [2], le serveur web renvoie une chaîne jSON qui donne le montant de l’impôt à payer avec avec ses différentes composantes ;
• le navigateur [1] interroge le serveur [2]. Le script [server_01] implémente la couche [web] [2] du serveur ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
455/755
• les couches [3-8] sont celles déjà utilisées dans la |version 5| de l’application du calcul de l’impôt. Nous les reprenons telles-
quelles ;
o la couche [métier] [3] est définie |ici| ;
o la couche [dao] [4] est définie |ici| ;
• la fonction [configure] reçoit un dictionnaire [config] en paramètre (ligne 1) et le rend comme résultat (ligne 54) après avoir
enrichi son contenu. On aurait pu dire depuis longtemps qu’il n’était pas nécessaire de rendre le résultat [config]. En effet
[config] est une référence de dictionnaire que le code appelant partage avec le code appelé. Le code appelant possède donc
déjà cette référence (ligne 1) et il est inutile de la lui redonner (ligne 54). Ainsi écrire :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
456/755
config=[module].configure(config) (1)
[module].configure(config) (2)
Néanmoins, j’ai gardé le type (1) d’écriture parce que j’ai pensé qu’il montrait peut-être mieux, que le code appelé modifiait le
dictionnaire [config].
• ligne 1 : le dictionnaire [config] reçu par la fonction [configure] a une clé ‘sgbd’ qui prend sa valeur dans la liste [‘mysql’,
‘pgres’]. [mysql] signifie que la base de données utilisée est gérée par MySQL alors que ‘pgres’ signifie que la base de données
utilisée est gérée par PostgreSQL ;
• lignes 4-27 : on liste tous les dossiers contenant des éléments nécessaires à l’application web. Ils feront partie du Python Path
de l’application (lignes 30-31) ;
• lignes 33-40 : on ne va autoriser que certains utilisateurs à accéder à l’application. Ici on a une liste avec un unique utilisateur ;
• lignes 43-46 : c’est le script [config_database] qui construit la configuration de la base de données utilisée ;
• ligne 46 : la configuration construite par le script [config_database] est un dictionnaire qu’on range dans la configuration
générale associé à la clé ‘database’ ;
• lignes 48-51 : le script [config_layers] instancie les couches de l’application web. Il rend un dictionnaire qu’on range dans la
configuration générale associé à la clé ‘layers’ ;
Le script [config_database] est celui déjà utilisé dans la |version 5|. On le redonne pour mémoire :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
457/755
52.
53. # on enregistre certaines informations et on les rend dans un dictionnaire
54. return {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
55. "constantes_table": constantes_table, "session": session}
Le script [config_layers] configure les couches du serveur web. On reprend un |script| déjà rencontré :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
458/755
46. # on récupère le statut marital dans l’URL
47. marié = request.args.get('marié')
48. if marié is None:
49. erreurs.append("paramètre [marié] manquant")
50. else:
51. marié = marié.strip().lower()
52. erreur = marié != "oui" and marié != "non"
53. if erreur:
54. erreurs.append(f"paramétre marié [{marié}] invalide")
55.
56. # on récupère le nombre d'enfants dans l’URL
57. enfants = request.args.get('enfants')
58. if enfants is None:
59. erreurs.append("paramètre [enfants] manquant")
60. else:
61. enfants = enfants.strip()
62. match = re.match(r"^\d+", enfants)
63. if not match:
64. erreurs.append(f"paramétre enfants [{enfants}] invalide")
65. else:
66. enfants = int(enfants)
67.
68. # on récupère le salaire dans l’URL
69. salaire = request.args.get('salaire')
70. if salaire is None:
71. erreurs.append("paramètre [salaire] manquant")
72. else:
73. salaire = salaire.strip()
74. match = re.match(r"^\d+", salaire)
75. if not match:
76. erreurs.append(f"paramétre salaire [{salaire}] invalide")
77. else:
78. salaire = int(salaire)
79.
80. # des paramètres invalides dans l’URL ?
81. for key in request.args.keys():
82. if key not in ['marié', 'enfants', 'salaire']:
83. erreurs.append(f"paramètre [{key}] invalide")
84.
85. # des erreurs ?
86. if erreurs:
87. # on envoie une réponse d'erreur au client
88. résultats = {"réponse": {"erreurs": erreurs}}
89. return json_response(résultats, status.HTTP_400_BAD_REQUEST)
90.
91. # pas d'erreurs, on peut travailler
92. # calcul de l'impôt
93. taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
94. config["layers"]["métier"].calculate_tax(taxpayer, admindata)
95. # on envoie la réponse au client
96. return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)
97.
98.
99. # main uniquement
100. if __name__ == '__main__':
101. # on lance le serveur Flask
102. app.config.update(ENV="development", DEBUG=True)
103. app.run()
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
459/755
• lignes 43-44 : on vérifie qu’on a trois paramètres exactement (pas moins, pas plus) ;
• lignes 46-49 : on vérifie que le paramètre [marié] est présent dans l’URL ;
• lignes 50-54 : s’il est présent on vérifie que sa valeur minuscule débarrassée des ‘blancs’ de début / fin est oui ou non ;
• lignes 56-59 : on vérifie que le paramètre [enfants] est dans l’URL ;
• lignes 60-66 : s’il est présent on vérifie que sa valeur est un entier positif ;
• ligne 66 : il ne faut pas oublier que les paramètres de l’URL et leurs valeurs sont des chaînes de caractères. La valeur du
paramètre [enfants] est transformée en ‘int’ ;
• lignes 68-78 : pour le paramètre [salaire], on fait les mêmes tests que pour le paramètre [enfants] ;
• lignes 81-83 : on vérifie qu’il n’y a pas de paramètres autres que [‘marié, ‘enfants’, ‘salaire’] dans l’URL ;
• lignes 85-89 : si après toutes ces vérifications, la liste [erreurs] n’est pas vide alors on envoie cette liste d’erreurs au client
sous la forme d’une chaîne jSON et le code de statut [400 Bad Request] ;
Comme par la suite, nous aurons souvent l’occasion d’envoyer une chaîne jSON en réponse au client, les quelques lignes nécessaires
à cet envoi ont été factorisées dans le module [myutils.py] que nous avons déjà utilisé :
1. # imports
2. import json
3. import os
4. import sys
5.
6. from flask import make_response
7.
8.
9. def set_syspath(absolute_dependencies: list):
10. # absolute_dependencies : une liste de noms absolus de dossiers
11.
12. ….
13.
14.
15. # génération d'une réponse HTTP jSON
16. def json_response(réponse: dict, status_code: int) -> tuple:
17. # corps de la réponse HTTP
18. response = make_response(json.dumps(réponse, ensure_ascii=False))
19. # corps de la réponse HTTP est du jSON
20. response.headers['Content-Type'] = 'application/json; charset=utf-8'
21. # on envoie la réponse HTTP
22. return response, status_code
La nouvelle version de [myutils] est installée parmi les modules de portée machine avec la commande [pip install .] dans un
terminal Pycharm :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
460/755
8. Successfully uninstalled myutils-0.1
9. Running setup.py install for myutils ... done
10. Successfully installed myutils-0.1
• ligne 1 : il faut être dans le dossier [packages] pour taper cette instruction ;
1. …
2. # des erreurs ?
3. if erreurs:
4. # on envoie une réponse d'erreur au client
5. résultats = {"réponse": {"erreurs": erreurs}}
6. return json_response(résultats, status.HTTP_400_BAD_REQUEST)
7.
8. # pas d'erreurs, on peut travailler
9. # calcul de l'impôt
10. taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': enfants, 'salaire': salaire})
11. config["layers"]["métier"].calculate_tax(taxpayer, admindata)
12. # on envoie la réponse au client
13. return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)
14.
• ligne 10 : lorsqu’on est là, les paramètres attendus dans l’URL sont là et sont corrects ;
• ligne 10 : on crée l’objet [TaxPayer] qui modélise le contribuable ;
• ligne 11 : on demande à la couche [métier] de calculer l’impôt. On rappelle que les éléments calculés par la couche [métier]
sont insérés dans l’objet [taxpayer] passé en paramètre ;
• ligne 13 : la réponse est envoyée au client web sous la forme d’une chaîne jSON. Celle-ci est la chaîne jSON d’un dictionnaire.
Associé à la clé [result], on y met le dictionnaire de l’objet [taxpayer]. On ne pouvait pas mettre l’objet [taxpayer] lui-
même car celui-ci n’est pas sérialisable en jSON ;
On crée deux configurations d’exécution, une pour MySQL, l’autre pour PostgreSQL :
Voici quelques exemples d’exécution (vous avez lancé l’application [server_01] et le SGBD utilisé puis vous demandez l’URL
http://localhost:5000/ avec un navigateur) :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
461/755
Voici un exemple d’exécution dans la console de Postman :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
462/755
23.2.2 Version 2
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
463/755
56. erreurs.append(f"paramètre [{key}] invalide")
57.
58. # des erreurs ?
59. if erreurs:
60. # on envoie une réponse d'erreur au client
61. résultats = {"réponse": {"erreurs": erreurs}}
62. return résultats, status.HTTP_400_BAD_REQUEST
63.
64. # pas d'erreurs, on peut travailler
65. # calcul de l'impôt
66. taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
67. config["layers"]["métier"].calculate_tax(taxpayer, config["admindata"])
68. # on envoie la réponse au client
69. return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
Nous utiliserons désormais cette technique : chaque route sera traitée par un module qui lui sera propre.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
464/755
Les résultats d’exécution sont les mêmes que pour la version 1.
23.2.3 Version 3
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
465/755
51.
52.
53. # Home URL : /?marié=xx&enfant=yy&salaire=zz
54. @app.route('/', methods=['GET'])
55. @auth.login_required
56. def index():
57. # on exécute la requête
58. résultat, statusCode = index_controller.execute(request, config)
59. # on envoie la réponse
60. return json_response(résultat, statusCode)
61.
62.
63. # main uniquement
64. if __name__ == '__main__':
65. # on lance le serveur
66. app.config.update(ENV="development", DEBUG=True)
67. app.run()
• ligne 21 : on importe un gestionnaire d’authentification. Il existe divers types d’authentification auprès d’un serveur web. Celle
que nous utilisons ici s’appelle [HTTP Basic]. Chaque type d’authentification suit un dialogue client / serveur précis ;
• ligne 33 : on crée une instance du gestionnaire d’authentification ;
• ligne 37 : l’annotation [@auth.verify_password] tague la fonction à exécuter lorsque le gestionnaire d’authentification veut
vérifier les login / password transmis par le client selon le protocole [HTTP Basic] ;
• ligne 55 : l’annotation [@auth.login_required] tague une route pour laquelle le client web doit être authentifié. Si le client
web n’a pas encore envoyé ses identifiants, le serveur web va automatiquement les lui demander selon le protocole HTTP
basic ;
1. GET / HTTP/1.1
2. User-Agent: PostmanRuntime/7.26.1
3. Accept: */*
4. Cache-Control: no-cache
5. Postman-Token: e65e2a28-4fe3-423b-88b3-b3e5a83092b1
6. Host: localhost:5000
7. Accept-Encoding: gzip, deflate, br
8. Connection: keep-alive
9.
10. HTTP/1.0 401 UNAUTHORIZED
11. Content-Type: text/html; charset=utf-8
12. Content-Length: 19
13. WWW-Authenticate: Basic realm="Authentication Required"
14. Server: Werkzeug/1.0.1 Python/3.8.1
15. Date: Fri, 17 Jul 2020 07:05:37 GMT
16.
17. Unauthorized Access
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
466/755
• ligne 10 : le serveur répond que nous ne sommes pas autorisés à accéder à l’URL [/] ;
• ligne 13 : il nous indique le protocole d’authentification à utiliser, ici le protocole dit Authentification Basic ;
Il est possible de configurer Postman pour qu’il envoie les identifiants de l’utilisateur selon le protocole Auth Basic :
1. config['users'] = [
2. {
3. "login": "admin",
4. "password": "admin"
5. }
6. ]
1. GET / HTTP/1.1
2. Authorization: Basic YWRtaW46YWRtaW4=
3. User-Agent: PostmanRuntime/7.26.1
4. Accept: */*
5. Cache-Control: no-cache
6. Postman-Token: 5ce20822-e87c-4eef-a2f4-b9eaec38d881
7. Host: localhost:5000
8. Accept-Encoding: gzip, deflate, br
9. Connection: keep-alive
10.
11. HTTP/1.0 400 BAD REQUEST
12. Content-Type: application/json; charset=utf-8
13. Content-Length: 203
14. Server: Werkzeug/1.0.1 Python/3.8.1
15. Date: Fri, 17 Jul 2020 07:20:01 GMT
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
467/755
16.
17. {"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]",
"paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
• ligne 2 : le client Postman envoie sous forme codée les identifiants de l’utilisateur [admin / admin] ;
• ligne 17 : le serveur répond correctement. Il signale des erreurs car on n’a pas envoyé les paramètres [marié, enfants, salaire]
(ligne 1) mais il ne signale pas d’erreur d’authentification ;
• comme avec Postman, Firefox a reçu la réponse HTTP du serveur avec les entêtes HTTP :
Firefox comme d’autres navigateurs n’arrêtent pas le dialogue lorsqu’il reçoit ces entêtes. Il demandet à l’utilisateur les identifiants
réclamés par le serveur. Il suffit ci-dessus de taper admin / admin pour recevoir la réponse du serveur :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
468/755
• le client web est formé des couches [1-2] ;
• le serveur web est formé des couches [3-9]. Il a été écrit dans le paragraphe précédent ;
La couche [dao] [2] doit savoir communiquer avec le serveur web [3]. Nous connaissons maintenant le protocole HTTP et nous
pourrions écrire, avec le module [pycurl] déjà étudié par exemple, un script communiquant avec le serveur web [3]. Cependant il
existe des modules spécialisés dans les dialogues HTTP client / serveur. Nous allons utiliser l’un d’entre-eux, le module [requests] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
469/755
Le script va implémenter l’application de calcul de l’impôt en mode batch décrite dès la |version 1|. La dernière version de cette
application est la |version 5|. On rappelle son fonctionnement :
• les contribuables dont on va calculer l’impôt sont rassemblés dans le fichier texte [taxpayersdata.txt] :
o le fichier texte [errors.txt] rassemble les erreurs détectées dans le fichier des contribuables :
o le fichier jSON [résultats.json] rassemble les résultats des calculs de l’impôt des différents contribuables :
[
{
"id": 0,
"marié": "oui",
"enfants": 2,
"salaire": 55555,
"impôt": 2814,
"surcôte": 0,
"taux": 0.14,
"décôte": 0,
"réduction": 0
},
{
"id": 1,
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
470/755
"marié": "oui",
"enfants": 2,
"salaire": 50000,
"impôt": 1384,
"surcôte": 0,
"taux": 0.14,
"décôte": 384,
"réduction": 347
},
…
]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
471/755
39. config.update({
40. "taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
41. "resultsFilename": f"{script_dir}/../data/output/résultats.json",
42. "errorsFilename": f"{script_dir}/../data/output/errors.txt",
43. "server": {
44. "urlServer": "http://127.0.0.1:5000/",
45. "authBasic": True,
46. "user": {
47. "login": "admin",
48. "password": "admin"
49. }
50. }
51. }
52. )
53.
54. # étape 3 ------
55. # instanciation des couches
56. import config_layers
57. config['layers'] = config_layers.configure(config)
58.
59. # on rend la configuation
60. return config
• ligne 1 : la fonction [configure] reçoit en paramètre le dictionnaire à remplir avec les informations de configuration. Celui-ci
peut être déjà pré-rempli ou vide. Ici, il sera vide ;
• lignes 40-42 : les noms absolus des trois fichiers texte gérés par la couche [dao] ;
• lignes 43-50 : associées à la clé [server], les informations que doit connaître la couche [dao] sur le serveur web avec lequel
elle doit communiquer :
o ligne 44 : l’URL du service web ;
o ligne 45 : la clé [authBasic] vaut True si l’accès à l’URL nécessite une authentification de type Basic ;
o lignes 46-49 : les identifiants de l’utilisateur qui va s’authentifier si l’authentification est demandée ;
• lignes 56-57 : on instancie les couches, ici l’unique couche [dao] et on met les références des couches dans [config] associées
à la clé [layers] ;
1. # on configure l'application
2. import config
3. config = config.configure({})
4.
5. # dépendances
6. from ImpôtsError import ImpôtsError
7.
8. # code
9. try:
10. # on récupère la couche [dao]
11. dao = config["layers"]["dao"]
12. # lecture des données des contribuables
13. taxpayers = dao.get_taxpayers_data()["taxpayers"]
14. # des contribuables ?
15. if not taxpayers:
16. raise ImpôtsError(f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
472/755
17. # calcul de l'impôt des contribuables
18. for taxpayer in taxpayers:
19. # taxpayer est à la fois un paramètre d'entrée et de sortie
20. # taxpayer va être modifié
21. dao.calculate_tax(taxpayer)
22. # écriture des résultats dans un fichier texte
23. dao.write_taxpayers_results(taxpayers)
24. except ImpôtsError as erreur:
25. # affichage de l'erreur
26. print(f"L'erreur suivante s'est produite : {erreur}")
27. finally:
28. # terminé
29. print("Travail terminé...")
o elle accède au système de fichiers à la fois pour lire les données des contribuables et écrire les résultats des calculs de
l’impôt. Nous avons déjà une classe |AbstractImpôtsDao| qui sait faire cela. Elle a été utilisée dès la |version 4| ;
o elle dialogue avec le serveur web [3] ;
Dans la |version 5|, le script principal [main] [1] dialoguait directement avec la couche [métier] [4]. On voudrait ne pas changer
ce script. Pour cela, on va faire en sorte que la couche [dao] [2] implémente l’interface de la couche [métier] [4]. Ainsi le script
principal [main] aura l’impression de dialoguer directement avec la couche [métier] [4] et pourra ignorer complètement que celle-
ci se situe sur une autre machine.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
473/755
Une définition de la classe implémentant la couche [dao] [2] pourrait être celle-ci :
• la classe [ImpôtsDaoWithHttpClient] :
o hérite de la classe [AbstractImpôtsDao] ce qui va lui permettre de gérer le dialogue avec le système de fichiers [6] ;
o implémente l’interface [InterfaceImpôtsMétier] pour ne pas avoir à changer le script principal [main] de la |version
5| ;
1. # imports
2. import requests
3. from flask_api import status
4.
5. from AbstractImpôtsDao import AbstractImpôtsDao
6. from AdminData import AdminData
7. from ImpôtsError import ImpôtsError
8. from InterfaceImpôtsMétier import InterfaceImpôtsMétier
9. from TaxPayer import TaxPayer
10.
11.
12. class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
13.
14. # constructeur
15. def __init__(self, config: dict):
16. # initialisation parent
17. AbstractImpôtsDao.__init__(self, config)
18. # mémorisation paramètres
19. self.__config_server = config["server"]
20.
21. # méthode inutilisée de [AbstractImpôtsDao]
22. def get_admindata(self) -> AdminData:
23. pass
24.
25. # calcul de l'impôt
26. def calculate_tax(self: object, taxpayer: TaxPayer, admindata: AdminData = None):
27. # on laisse remonter les exceptions
28. # paramètres du get
29. params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
30. # connexion avec authentification Auth Basic ?
31. if self.__config_server['authBasic']:
32. response = requests.get(
33. # URL du serveur interrogé
34. self.__config_server['urlServer'],
35. # paramètres de l'URL
36. params=params,
37. # authentification Basic
38. auth=(
39. self.__config_server["user"]["login"],
40. self.__config_server["user"]["password"]))
41. else:
42. # connexion sans authentification Auth Basic
43. response = requests.get(self.__config_server['urlServer'], params=params)
44. # vérification
45. print(response.text)
46. # code de statut de la réponse HTTP
47. status_code = response.status_code
48. # on met la réponse jSON dans un dictionnaire
49. résultat = response.json()
50. # erreur si code de statut différent de 200 OK
51. if status_code != status.HTTP_200_OK:
52. # on sait que les erreurs ont été associées à la clé [erreurs] de la réponse
53. raise ImpôtsError(87, résultat['réponse']['erreurs'])
54. # on sait que le résultat a été associé à la clé [result] de la réponse
55. # on modifie le paramètre d'entrée avec ce résultat
56. taxpayer.fromdict(résultat["réponse"]["result"])
• lignes 21-23 : la classe [AbstractImpôtsDao] (ligne 12) possède une méthode abstraite [get_admindata]. Nous sommes obligés
de l’implémenter même si on ne s’en sert pas (admindata est géré par le serveur pas par le client) ;
• ligne 26 : la méthode [calculate_tax] appartient à l’interface [InterfaceImpôtsMétier] (ligne 12). il nous faut l’implémenter ;
• ligne 15 : le constructeur reçoit en unique paramètre le dictionnaire de la configuration de l’application ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
474/755
• lignes 16-17 : la classe parent [AbstractImpôtsDao] est initialisée en lui passant, là également, la configuration de l’application.
Elle y trouvera les noms des trois fichiers texte qu’elle a à gérer ;
• lignes 18-19 : on mémorise localement dans la classe les informations concernant le serveur web de calcul de l’impôt ;
• ligne 26 : la méthode [calculate_tax] reçoit en paramètre un objet de type |Taxpayer|. Pour respecter la signature de la
méthode [InterfaceImpôtsMétier.calculate_tax], elle reçoit également un paramètre [admindata] qui est censé encapsuler
les données de l’administration fiscale. Côté client, on n’a pas ces données. Ce paramètre restera toujours à [None]. Cette
contorsion fait dire que la classe [ImpôtsMétier] a été initialement mal écrite :
o la signature de [calculate_tax] aurait dû être simplement :
{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]",
"paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
{"réponse": {"result": {"id": 0, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842,
"surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
• lignes 51-53 : si le code de statut n’est pas 200 alors on lance une exception avec les messages d’erreur encapsulés dans la
réponse ;
• ligne 56 : on récupère le dictionnaire produit par le calcul de l’impôt et on l’utilise pour mettre à jour le paramètre d’entrée
[taxpayer] ;
23.3.5 Exécution
Pour exécuter le client :
Les résultats seront trouvés dans le dossier [data/output]. Ce sont les mêmes que pour la version 5.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
475/755
23.4 Tests de la couche [dao]
Revenons à l’architecture de l’application client / serveur :
• dans le client écrit, nous nous sommes débrouillés pour que la couche [dao] [1] offre la même interface que la couche [métier]
[3]. Nous allons donc utiliser en [4], la classe de tests |TestDaoMétier| déjà étudiée pour tester la couche [métier] [3] ;
• la configuration [2] est identique à la configuration [1] que nous venons d’étudier ;
1. import unittest
2.
3.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
476/755
4. class TestHttpClientDao(unittest.TestCase):
5.
6. def test_1(self) -> None:
7. from TaxPayer import TaxPayer
8.
9. # {'marié': 'oui', 'enfants': 2, 'salaire': 55555,
10. # 'impôt': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
11. taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
12. dao.calculate_tax(taxpayer)
13. # vérification
14. self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
15. self.assertEqual(taxpayer.décôte, 0)
16. self.assertEqual(taxpayer.réduction, 0)
17. self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
18. self.assertEqual(taxpayer.surcôte, 0)
19.
20. …
21.
22. def test_11(self) -> None:
23. from TaxPayer import TaxPayer
24.
25. # {'marié': 'oui', 'enfants': 3, 'salaire': 200000,
26. # 'impôt': 42842, 'surcôte': 17283, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
27. taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
28. dao.calculate_tax(taxpayer)
29. # vérifications
30. self.assertAlmostEqual(taxpayer.impôt, 42842, 1)
31. self.assertEqual(taxpayer.décôte, 0)
32. self.assertEqual(taxpayer.réduction, 0)
33. self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
34. self.assertAlmostEqual(taxpayer.surcôte, 17283, delta=1)
35.
36.
37. if __name__ == '__main__':
38.
39. # on configure l'application
40. import config
41. config = config.configure({})
42.
43. # couche dao
44. dao = config['layers']['dao']
45.
46. # on exécute les méthodes de test
47. print("tests en cours...")
48. unittest.main()
Cette classe est analogue à |celle| déjà étudiée dans la version 4 de l’application.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
477/755
• on crée une configuration d’exécution pour un script console pas pour un test UnitTest ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/impots/http-clients/01/tests/TestHttpClientDao.py
2. tests en cours...
3. {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0,
"taux": 0.14, "décôte": 0, "réduction": 0}}}
4. ....{"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte":
7498, "taux": 0.45, "décôte": 0, "réduction": 0}}}
5. {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283,
"taux": 0.41, "décôte": 0, "réduction": 0}}}
6. {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0,
"taux": 0.14, "décôte": 384, "réduction": 347}}}
7. {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux":
0.14, "décôte": 720, "réduction": 0}}}
8. ...{"réponse": {"result": {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte":
4480, "taux": 0.41, "décôte": 0, "réduction": 0}}}
9. {"réponse": {"result": {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176,
"taux": 0.41, "décôte": 0, "réduction": 0}}}
10. {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180,
"taux": 0.3, "décôte": 0, "réduction": 0}}}
11. {"réponse": {"result": {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0,
"taux": 0.14, "décôte": 0, "réduction": 0}}}
12. {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0,
"taux": 0.41, "décôte": 0, "réduction": 0}}}
13. ....
14. {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux":
0.0, "décôte": 0, "réduction": 0}}}
15. ----------------------------------------------------------------------
16. Ran 11 tests in 0.130s
17.
18. OK
19.
20. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
478/755
24 Exercice d’application : version 7
24.1 Introduction
La version 7 de l’application de calcul de l’impôt est identique à la version 6 aux détails près suivants :
• le client web va lancer simultanément plusieurs requêtes HTTP. Dans la version précédente ces requêtes étaient lancées
séquentiellement. Le serveur ne traitait alors à tout moment qu’une unique requête ;
• le serveur sera multi-threadé : il pourra traiter plusieurs requêtes simultanément ;
• pour suivre l’exécution de ces requêtes, on va doter le serveur web d’un logueur avec lequel on loguera dans un fichier texte
les moments importants du traitement des requêtes ;
• le serveur enverra un mail à l’administrateur de l’application lorsqu’il rencontrera un problème qui l’empêchera de se lancer,
typiquement un problème avec la base de données associée au serveur web ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
479/755
Le dossier [http-servers/02] est d’abord obtenue par recopie du dossier [http-servers/01]. On y fait ensuite des modifications.
1. import codecs
2. import threading
3. from datetime import date, datetime
4. from threading import current_thread
5.
6. from ImpôtsError import ImpôtsError
7.
8.
9. class Logger:
10. # attribut de classe
11. verrou = threading.RLock()
12.
13. # constructeur
14. def __init__(self, logs_filename: str):
15. try:
16. # on ouvre le fichier en mode append (a)
17. self.__resource = codecs.open(logs_filename, "a", "utf-8")
18. except BaseException as erreur:
19. raise ImpôtsError(18, f"{erreur}")
20.
21. # écriture d'un log
22. def write(self, message: str):
23. # date / heure du moment
24. today = date.today()
25. now = datetime.time(datetime.now())
26. # nom du thread
27. thread_name = current_thread().name
28. # on ne veut pas être dérangé pendant qu'on écrira dans le fichier de logs
29. # on demande l'objet de synchronisation (= le verrou) de la classe - un seul thread l'obtiendra
30. Logger.verrou.acquire()
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
480/755
31. try:
32. # écriture du log
33. self.__resource.write(f"{today} {now}, {thread_name} : {message}")
34. # écriture immédiate - sinon le texte ne sera écrit que lors de la fermeture du flux d'écriture
35. # or on veut suivre les logs dans le temps
36. self.__resource.flush()
37. finally:
38. # on libère l'objet de synchronisation (= le verrou) pour qu'un autre thread puisse l'obtenir
39. Logger.verrou.release()
40.
41. # libération des ressources
42. def close(self):
43. # fermeture du fichier
44. if self.__resource:
45. self.__resource.close()
• lignes 10-11 : on définit un attribut de classe. Un attribut de classe est une propriété partagée par toutes les instances de la
classe. On la référence par la notation [Classe.attribut_de_classe] (lignes 30, 39). L’attribut de classe [verrou] sera un
objet de synchronisation pour tous les threads exécutant le code des lignes 31-36 ;
• lignes 14-19 : le constructeur reçoit le nom absolu du fichier de logs. Ce fichier est alors ouvert et le descripteur de fichier
récupéré est mémorisé dans la classe ;
• ligne 17 : le fichier de logs est ouvert en mode ‘append’ (a). Chaque ligne écrite le sera à la fin du fichier ;
• lignes 22-39 : la méthode [write] permet d’écrire dans le fichier de logs un message passé en paramètre. A celui-ci sont
accolées deux informations :
o ligne 24 : la date du jour ;
o ligne 25 : l’heure du moment ;
o ligne 27 : le nom du thread qui écrit le log. Il ne faut pas oublier ici qu’une application web sert plusieurs utilisateurs à la
fois. Toute requête se voit attribuer un thread pour l’exécuter. Si ce thread est mis en pause, typiquement pour une
opération d’ entrée / sortie (réseau, fichiers, base de données), alors le processeur sera donné à un autre thread. A cause
de ces interruptions possibles, on ne peut pas être sûrs qu’un thread va réussir à écrire une ligne dans le fichier de logs
sans être interrompu. On risque alors de voir des logs de deux threads différents se mélanger. Le risque est faible, peut-
être même nul, mais on a néanmoins décidé de montrer comment synchroniser l’accès de deux threads à une ressource
commune, ici le fichier de logs ;
• ligne 30 : avant d’écrire, le thread demande la clé de la porte d’entrée. La clé demandée est celle créée ligne 11. Elle est
effectivement unique : un attribut de classe est unique pour toutes les instances de la classe ;
o au temps T1, un thread Thread1 obtient la clé. Il peut alors exécuter la ligne 33 ;
o au temps T2, le thread Thread1 est mis en pause avant même d’avoir terminé l’écriture du log ;
o au temps T3, le thread Thread2 qui a obtenu le processeur doit lui aussi écrire un log. Il arrive ainsi à la ligne 30 où il
demande la clé de la porte d’entrée. On lui répond qu’un autre thread l’a déjà. Il est alors automatiquement mis en pause.
Il en sera ainsi de tous les threads qui demanderont cette clé ;
o au temps T4, le thread Thread1 qui avait été mis en pause retrouve le processeur. Il termine alors l’écriture du log ;
• lignes 32-36 : l’écriture dans le fichier de logs se fait en deux temps :
o ligne 33 : le descripteur de fichier obtenu ligne 17 travaille avec un buffer. L’opération [write] de la ligne 33 écrit dans
ce buffer mais pas directement dans le fichier. Le buffer est ensuite vidé dans le fichier dans certaines conditions :
▪ le buffer est plein ;
▪ le descripteur de fichier subit une opération [close] ou [flush] ;
o ligne 36 : on force l’écriture de la ligne de log dans le fichier. On fait cela parce qu’on veut voir s’intercaler entre eux les
logs des différents threads. Si on ne fait pas ça, les logs d’un thread seront tous écrits en même temps lors de la fermeture
du descripteur, ligne 45. Il serait alors beaucoup plus difficile de voir que certains threads ont été arrêtés : il faudrait
regarder les heures dans les logs ;
• ligne 39 : le thread Thread1 rend la clé qu’on lui avait donnée. Elle va pouvoir être donnée à un autre thread ;
• ligne 22 : la méthode [write] est donc synchronisée : un seul thread à la fois écrit dans le fichier de logs. La clé du dispositif
est la ligne 30 : quoiqu’il arrive, seul un thread récupère la clé de passage à la ligne suivante. Il la garde tant qu’il ne la rend pas
(ligne 39) ;
• lignes 41-45 : la méthode [close] permet de libérer les ressources allouées au descripteur du fichier de logs ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
481/755
24.2.2 La classe [SendAdminMail]
La classe [SendAminMail] permet d’envoyer un message à l’administrateur de l’application lorsque celle-ci ‘plante’.
La classe [SendAdminMail] est configurée dans le script [config] [2] de la façon suivante :
La classe [SendAminMail] reçoit le dictionnaire des lignes 2-13 ainsi que la configuration de l’envoi du mail. La classe est la suivante :
1. # imports
2. import smtplib
3. from email.mime.text import MIMEText
4. from email.utils import formatdate
5.
6.
7. class SendAdminMail:
8.
9. # -----------------------------------------------------------------------
10. @staticmethod
11. def send(config: dict, message: str, verbose: bool = False):
12. # envoie message au serveur smtp config['smtp-server'] sur le port config[smtp-port]
13. # si config['tls'] est vrai, le support TLS sera utilisé
14. # le mail est envoyé de la part de config['from']
15. # pour le destinataire config['to']
16. # le message a le sujet config['subject']
17. # on trouve la référence d'un logueur dans config['logger']
18.
19. # on récupère le logueur dans la config - peut être égal à None
20. logger = config["logger"]
21. # serveur SMTP
22. server = None
23. # on envoie le message
24. try:
25. # le serveur SMTP
26. server = smtplib.SMTP(config["smtp-server"])
27. # mode verbose
28. server.set_debuglevel(verbose)
29. # connexion sécurisée ?
30. if config['tls']:
31. # début dialogue de sécurisation
32. server.starttls()
33. # authentification
34. server.login(config["user"], config["password"])
35. # construction d'un message Multipart - c'est ce message qui sera envoyé
36. msg = MIMEText(message)
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
482/755
37. msg['From'] = config["from"]
38. msg['To'] = config["to"]
39. msg['Date'] = formatdate(localtime=True)
40. msg['Subject'] = config["subject"]
41. # on envoie le message
42. server.send_message(msg)
43. # log - le logger peut ne pas exister
44. if logger:
45. logger.write(f"[SendAdminMail] Message envoyé à [{config['to']}] : [{message}]\n")
46. except BaseException as erreur:
47. # log- le logger peutne pas exister
48. if logger:
49. logger.write(
50. f"[SendAdminMail] Erreur [{erreur}] lors de l'envoi à [{config['to']}] du message
[{message}] : \n")
51. finally:
52. # on a fini - on libère les ressources mobilisées par la fonction
53. if server:
54. server.quit()
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
483/755
24.3.1 Configuration
La configuration du serveur est très semblable à celle du serveur étudié précédemment. Seul le fichier [config.py] évolue légèrement :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
484/755
55. "smtp-port": "25",
56. # administrateur
57. "from": "guest@localhost.com",
58. "to": "guest@localhost.com",
59. # sujet du mail
60. "subject": "plantage du serveur de calcul d'impôts",
61. # tls à True si le serveur SMTP requiert une autorisation, à False sinon
62. "tls": False
63. },
64. # durée pause thread en secondes
65. "sleep_time": 0
66. })
67.
68. # étape 3 ------
69. # configuration base de données
70. import config_database
71. config["database"] = config_database.configure(config)
72.
73. # étape 4 ------
74. # instanciation des couches de l'application
75. import config_layers
76. config['layers'] = config_layers.configure(config)
77.
78. # on rend la configuration
79. return config
• lignes 40-66 : on ajoute dans le dictionnaire de configuration du serveur, les éléments concernant le logueur (ligne 49) et celles
concernant l’envoi d’un mail d’alerte à l’administrateur de l’application (lignes 51-63) ;
• ligne 65 : pour mieux voir les threads en action on va imposer à certains de s’arrêter. [sleep_time] est la durée de l’arrêt
exprimée en secondes ;
• lignes 27-28 : on notera qu’on utilise le contrôleur [index_controller] de la version 6 précédente ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
485/755
40. if user['login'] == login and user['password'] == password:
41. return True
42. # on n'a pas trouvé
43. return False
44.
45.
46. # envoi d'un mail à l'administrateur
47. def send_adminmail(config: dict, message: str):
48. # on envoie un mail à l'administrateur de l'application
49. config_mail = config["adminMail"]
50. config_mail["logger"] = config['logger']
51. SendAdminMail.send(config_mail, message)
52.
53.
54. # vérification du fichier de logs
55. logger = None
56. erreur = False
57. message_erreur = None
58. try:
59. # logueur
60. logger = Logger(config["logsFilename"])
61. except BaseException as exception:
62. # log console
63. print(f"L'erreur suivante s'est produite : {exception}")
64. # on note l'erreur
65. erreur = True
66. message_erreur = f"{exception}"
67. # on mémorise le logueur dans la config
68. config['logger'] = logger
69. # gestion de l'erreur
70. if erreur:
71. # mail à l'administrateur
72. send_adminmail(config, message_erreur)
73. # fin de l'application
74. sys.exit(1)
75.
76. # log de démarrage
77. log = "[serveur] démarrage du serveur"
78. logger.write(f"{log}\n")
79. print(log)
80.
81. # récupération des données de l'administration fiscale
82. erreur = False
83. try:
84. # admindata sera une donnée de portée application en lecture seule
85. config["admindata"] = config["layers"]["dao"].get_admindata()
86. # log de réussite
87. logger.write("[serveur] connexion à la base de données réussie\n")
88. except ImpôtsError as ex:
89. # on note l'erreur
90. erreur = True
91. # log d'erreur
92. log = f"L'erreur suivante s'est produite : {ex}"
93. # console
94. print(log)
95. # fichier de logs
96. logger.write(f"{log}\n")
97. # mail à l'administrateur
98. send_adminmail(config, log)
99.
100. # le thread principal n'a plus besoin du logger
101. logger.close()
102.
103. # s'il y a eu erreur on s'arrête
104. if erreur:
105. sys.exit(2)
106.
107. # l'application Flask peut démarrer
108. app = Flask(__name__)
109.
110.
111. # Home URL
112. @app.route('/', methods=['GET'])
113. @auth.login_required
114. def index():
115. …
116.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
486/755
117.
118. # main uniquement
119. if __name__ == '__main__':
120. # on lance le serveur
121. app.config.update(ENV="development", DEBUG=True)
122. app.run(threaded=True)
• lignes 1-10 : le script attend un paramètre [mysql / pgres] qui lui indique le SGBD à utiliser ;
• lignes 12-14 : l’application est configurée (Python Path, couches, base de données) ;
• lignes 16-28 : les dépendances nécessaires à l’application ;
• lignes 30-43 : gestion de l’authentification ;
• lignes 46-51 : une fonction qui envoie un mail à l’administrateur de l’application ;
o la fonction attend deux paramètres :
▪ config : un dictionnaire ayant les clés [adminMail] et [logger] ;
▪ le message à envoyer ;
o lignes 49-50 : on prépare la configuration de l’envoi ;
o on envoie le mail ;
• lignes 54-74 : on vérifie la présence du fichier de logs ;
• ligne 70-74 : si on n’a pas réussi à ouvrir le fichier de logs, on envoie un mail à l’administrateur et on s’arrête ;
• lignes 76-79 : on logue le démarrage du serveur ;
• lignes 81-98 : on va chercher les données de l’administration fiscale en base de données ;
• lignes 88-98 : si on n’a pas réussi à obtenir ces données, on logue l’erreur aussi bien sur la console que dans le fichier de logs ;
• lignes 100-101 : le thread principal ne fera plus de logs (les threads créés n’utiliseront pas le même descripteur de fichier) ;
• lignes 103-105 : si on n’a pas pu se connecter à la base de données, on s’arrête ;
• ligne 122 : on lance le serveur en mode multithreadé ;
1. # Home URL
2. @app.route('/', methods=['GET'])
3. @auth.login_required
4. def index():
5. logger = None
6. try:
7. # logger
8. logger = Logger(config["logsFilename"])
9. # on le mémorise dans une config associée au thread
10. thread_config = {"logger": logger}
11. thread_name = threading.current_thread().name
12. config[thread_name] = {"config": thread_config}
13. # on logue la requête
14. logger.write(f"[index] requête : {request}\n")
15. # on interrompt le thread si cela a été demandé
16. sleep_time = config["sleep_time"]
17. if sleep_time != 0:
18. # la pause est aléatoire pour que certains threads soient interrompus et d'autres pas
19. aléa = randint(0, 1)
20. if aléa == 1:
21. # log avant pause
22. logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
23. # pause
24. time.sleep(sleep_time)
25. # on fait exécuter la requête par un contrôleur
26. résultat, status_code = index_controller.execute(request, config)
27. # y-a-t-il eu une erreur fatale ?
28. if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
29. # on envoie un mail à l'administrateur de l'application
30. config_mail = config["adminMail"]
31. config_mail["logger"] = logger
32. SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
33. # on logue la réponse
34. logger.write(f"[index] {résultat}\n")
35. # on envoie la réponse
36. return json_response(résultat, status_code)
37. except BaseException as erreur:
38. # on logue l'erreur si c'est possible
39. if logger:
40. logger.write(f"[index] {erreur}")
41. # on prépare la réponse au client
42. résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
43. # on envoie la réponse
44. return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
45. finally:
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
487/755
46. # on ferme le fichier de logs s'il a été ouvert
47. if logger:
48. logger.close()
• ligne 4 : la fonction exécutée lorsqu’un utilisateur demande l’URL /. Parce que le serveur est multi-threadé (ligne 112), un
thread va être créé pour exécuter la fonction. Ce thread peut à tout moment être interrompu et mis en pause pour reprendre
son exécution un peu plus tard. Il faut toujours se souvenir de ce point lorsque le code accède à une ressource partagée par
tous les threads. Une telle ressource est ici le fichier de logs : tous les threads écrivent dedans ;
• ligne 8 : on crée une instance de logueur. Donc tous les threads auront une instance différence du logueur. Néanmoins tous
ces logueurs pointent sur le même fichier de logs. Il est quand même important de noter que lorsqu’un thread ferme son
logueur, cela n’a pas d’incidence sur les logueurs des autres threads ;
• lignes 9-12 : on mémorise le logueur dans le dictionnaire [config] de l’application associé à une clé portant le nom du thread.
Ainsi s’il y a n threads exécutés simultanément, on aura la création de n entrées dans le dictionnaire [config]. [config] est
une ressource partagée entre tous les threads. Il peut donc y avoir un besoin de synchronisation. J’ai fait ici une hypothèse. J’ai
supposé que si deux threads créaient simultanément leur entrée dans le fichier [config] et que l’un d’eux était interrompu par
l’autre, cela n’avait pas d’incidence. Celui interrompu pouvait ultérieurement terminer la création de l’entrée. Si l’expérience
montrait que cette hypothèse était fausse, il faudrait synchroniser l’accès à la ligne 12 ;
• ligne 10 : on met le logueur dans un dictionnaire ;
• ligne 11 : [threading.current_thread()] est le thread qui exécute cette ligne, donc le thread qui exécute la fonction [index].
On note son nom. Chaque thread a un nom unique ;
• ligne 12 : on mémorise la configuration du thread. A partir de maintenant, nous procèderons toujours ainsi : s’il y a des
informations qui ne peuvent être partagées entre les threads, elles seront mises quand même dans la configuration générale,
mais associées au nom du thread ;
• ligne 14 : on logue la requête qu’on est en train d’exécuter ;
• lignes 15-24 : de façon aléatoire on met certains threads en pause afin qu’ils laissent le processeur à un autre thread ;
o ligne 16 : on récupère la durée de la pause (en secondes) dans la configuration ;
o ligne 17 : il n’y a pause que si la durée de pause est différente de 0 ;
o ligne 19 : un nombre entier aléatoire dans l’intervalle [0, 1]. Donc seules les valeurs 0 et 1 sont possibles ;
o ligne 20 : la pause du thread ne se fait que si le nombre aléatoire est 1 ;
o ligne 22 : on logue le fait que le thread va être interrompu ;
o ligne 24 : on interrompt le thread pendant [sleep_time] secondes ;
• ligne 26 : lorsque le thread se réveille, il fait exécuter la requête par le module [index_controller] ;
• lignes 28-32 : si cette exécution provoque une erreur de type [500 INTERNAL SERVER ERROR], on envoie un mail à
l’administrateur ;
o lignes 30-31 : on configure le dictionnaire [config_mail] qu’on va passer à la classe [SendAdminMail] ;
o ligne 32 : le message envoyé à l’administrateur est la chaîne jSON du résultat qui va être envoyé au client ;
• lignes 33-34 : on logue la réponse qu’on va envoyer au client (ligne 36) ;
• lignes 37-44 : traitement d’une éventuelle exception ;
• lignes 39-40 : si le logueur existe on logue l’erreur qui s’est produite ;
• lignes 47-48 : on ferme le logueur s’il existe. Au final, le thread crée un logueur au début de la requête et le ferme lorsque celle-
ci a été traitée ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
488/755
24.3.4 Exécution
On lance le serveur Flask, le serveur de mails |hMailServer| ainsi que le lecteur de courrier |Thunderbird|. On ne lance pas le SGBD.
Le serveur s’arrête avec les logs console suivants :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/impots/http-servers/02/flask/main.py mysql
2. [serveur] démarrage du serveur
3. L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't
connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur
cible l’a expressément refusée)
4. (Background on this error at: http://sqlalche.me/e/13/rvf5)]
5.
6. Process finished with exit code 2
• lignes 1-4 : on rappelle qu’il y a deux démarrages du serveur parce que le mode [Debug=True] provoque un second démarrage ;
• lignes 5-6 : les logs nous donnent une idée du temps d’exécution d’une requête, ici 2,293 millisecondes ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
489/755
24.4 Le client web
Le dossier [http-clients/02] est obtenu par recopie du dossier [http-clients/01]. On procède ensuite à quelques modifications.
24.4.1 La configuration
La configuration [config] de l’application [http-clients/02] est le même que celle de l’application [http-clients/01] à quelques
détails près :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
490/755
4. # étape 1 ------
5.
6. # dossier de ce fichier
7. script_dir = os.path.dirname(os.path.abspath(__file__))
8.
9. # chemin racine
10. root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
11.
12. # dépendances absolues
13. absolute_dependencies = [
14. # dossiers du projet
15. # BaseEntity, MyException
16. f"{root_dir}/classes/02/entities",
17. # InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
18. f"{root_dir}/impots/v04/interfaces",
19. # AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
20. f"{root_dir}/impots/v04/services",
21. # ImpotsDaoWithAdminDataInDatabase
22. f"{root_dir}/impots/v05/services",
23. # AdminData, ImpôtsError, TaxPayer
24. f"{root_dir}/impots/v04/entities",
25. # Constantes, tranches
26. f"{root_dir}/impots/v05/entities",
27. # ImpôtsDaoWithHttpClient
28. f"{script_dir}/../services",
29. # scripts de configuration
30. script_dir,
31. # Logger
32. f"{root_dir}/impots/http-servers/02/utilities",
33. ]
34.
35. # on fixe le syspath
36. from myutils import set_syspath
37. set_syspath(absolute_dependencies)
38.
39. # étape 2 ------
40. # configuration de l'application avec des constantes
41. config.update({
42. # fichier des contribuables
43. "taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
44. # fichier des résultats
45. "resultsFilename": f"{script_dir}/../data/output/résultats.json",
46. # fichier des erreurs
47. "errorsFilename": f"{script_dir}/../data/output/errors.txt",
48. # fichier de logs
49. "logsFilename": f"{script_dir}/../data/logs/logs.txt",
50. # le serveur de calcul de l'impôt
51. "server": {
52. "urlServer": "http://127.0.0.1:5000/",
53. "authBasic": True,
54. "user": {
55. "login": "admin",
56. "password": "admin"
57. }
58. },
59. # mode debug
60. "debug": True
61. }
62. )
63.
64. # étape 3 ------
65. # instanciation des couches
66. import config_layers
67. config['layers'] = config_layers.configure(config)
68.
69. # on rend la configuation
70. return config
• lignes 31-32 : on va utiliser le même logueur |Logger| que celui utilisé pour le serveur ;
• ligne 49 : le chemin absolu du fichier de logs ;
• ligne 60 : le mode [debug=True] sert à écrire les réponses du serveur web dans le fichier de logs ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
491/755
1. # imports
2.
3. import requests
4. from flask_api import status
5.
6. …
7.
8.
9. class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
10.
11. # constructeur
12. def __init__(self, config: dict):
13. # initialisation parent
14. AbstractImpôtsDao.__init__(self, config)
15. # mémorisation éléments de la configuration
16. # config générale
17. self.__config = config
18. # serveur
19. self.__config_server = config["server"]
20. # mode debug
21. self.__debug = config["debug"]
22. # logger
23. self.__logger = None
24.
25. # méthode inutilisée
26. def get_admindata(self) -> AdminData:
27. pass
28.
29. # calcul de l'impôt
30. def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
31. # on laisse remonter les exceptions
32. …
33. # mode debug ?
34. if self.__debug:
35. # logueur
36. if not self.__logger:
37. self.__logger = self.__config['logger']
38. # on logue
39. self.__logger.write(f"{response.text}\n")
40. # code de statut de la réponse HTTP
41. status_code = response.status_code
42. …
• lignes 17 : on mémorise la configuration générale. On verra ultérieurement que lorsque le constructeur de la classe
[ImpôtsDaoWithHttpClient] s’exécute, le dictionnaire [config] ne contient pas encore la clé [logger] utilisée ligne 37. C’est
pour cette raison qu’on ne peut pas initialiser [self.__logger] (ligne 23) dans le constructeur ;
• ligne 21 : on a ajouté dans la configuration une clé [debug] qui contrôle le log des lignes 33-39 ;
• ligne 34 : si on est en mode [debug] ;
• lignes 36-37 : initialisation éventuelle de la propriété [self.__logger]. Lorsque la méthode [calculate_tax] est utilisée, la clé
[logger] fait partie du dictionnaire [config] ;
• ligne 39 : on logue le document texte associé à la réponse HTTP du serveur ;
La couche [dao] va être exécutée simultanément par plusieurs threads. Or ici on crée un unique exemplaire de cette couche (cf.
config_layers). Il faut donc vérifier que le code n’implique pas l’accès en écriture à des données partagées, typiquement les propriétés
de la classe [ImpôtsDaoWithHttpClient] qui implémente la couche [dao]. Or ci-dessus la ligne 37 modifie une propriété de l’instance
de classe. Ici ça ne porte pas à conséquence car tous les threads partagent le même logueur. Si cela n’avait pas été le cas, l’accès à la
ligne 37 aurait du être synchronisé.
1. # on configure l'application
2.
3. import config
4. config = config.configure({})
5.
6. # dépendances
7. from ImpôtsError import ImpôtsError
8. import random
9. import sys
10. import threading
11. from Logger import Logger
12.
13.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
492/755
14. # exécution de la couche [dao] dans un thread
15. # taxpayers est une liste de contribuables
16. def thread_function(dao, logger, taxpayers: list):
17. …
18.
19.
20. # liste des threads du client
21. threads = []
22. logger = None
23. # code
24. try:
25. # logger
26. logger = Logger(config["logsFilename"])
27. # on le mémorise dans la config
28. config["logger"] = logger
29. # on récupère la couche [dao]
30. dao = config["layers"]["dao"]
31. # lecture des données des contribuables
32. taxpayers = dao.get_taxpayers_data()["taxpayers"]
33. # des contribuables ?
34. if not taxpayers:
35. raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
36. # calcul de l'impôt des contribuables avec plusieurs threads
37. i = 0
38. l_taxpayers = len(taxpayers)
39. while i < len(taxpayers):
40. # chaque thread va traiter de 1 à 4 contribuables
41. nb_taxpayers = min(l_taxpayers - i, random.randint(1, 4))
42. # la liste des contribuables traités par le thread
43. thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
44. # on incrémente i pour le thread suivant
45. i += nb_taxpayers
46. # on crée le thread
47. thread = threading.Thread(target=thread_function, args=(dao, logger, thread_taxpayers))
48. # on l'ajoute à la liste des threads du script principal
49. threads.append(thread)
50. # on lance le thread - cette opération est asynchrone - on n'attend pas le résultat du thread
51. thread.start()
52. # le thread principal attend la fin de tous les threads qu'il a lancés
53. for thread in threads:
54. thread.join()
55. # ici tous les threads ont fini leur travail - chacun a modifié un ou plusieurs objets [taxpayer]
56. # on enregistre les résultats dans le fichier jSON
57. dao.write_taxpayers_results(taxpayers)
58. except BaseException as erreur:
59. # affichage de l'erreur
60. print(f"L'erreur suivante s'est produite : {erreur}")
61. finally:
62. # on ferme le logueur
63. if logger:
64. logger.close()
65. # on a fini
66. print("Travail terminé...")
67. # fin des threads qui pourraient encore exister si on s'est arrêté sur erreur
68. sys.exit()
• le script principal se distingue de celui du client précédent par le fait qu’il va générer plusieurs threads d’exécution pour
effectuer les requêtes au serveur. Le client de la version 6 faisait toutes ses requêtes séquentiellement. La requête n° i n’était
faite qu’une fois la réponse à la requête n° [i-1] reçue. Ici on veut voir comment le serveur va se comporter lorsqu’il reçoit
plusieurs requêtes simultanées. Pour cela nous avons besoin des threads ;
• ligne 21 : les threads générés vont être mis dans une liste. Il faut comprendre que le script [main] est lui aussi exécuté par un
thread qui s’appelle [MainThread]. Ce thread principal va créer d’autres threads qui seront chargés de calculer l’impôt d’un ou
plusieurs contribuables ;
• ligne 26 : on crée un logueur. Celui-ci sera partagé par tous les threads ;
• ligne 32 : on récupère tous les contribuables dont il faut calculer l’impôt ;
• lignes 39-51 : on va répartir ces contribuables sur plusieurs threads ;
• lignes 40-41 : chaque thread va traiter de 1 à 4 contribuables. Ce nombre est fixé de façon aléatoire ;
o [random.randint(1, 4)] donne aléatoirement un nombre dans la liste [1, 2, 3, 4] ;
o le thread ne peut avoir plus de [l-i] contribuables où [l-i] représente le nombre de contribuables à qui on n’a pas
encore attribué de thread ;
o on prend donc le min des deux valeurs ;
• ligne 43 : une fois que [nb_taxpayers], le nombre de contribuables traités par le thread, est connu, on prend ceux-ci dans la
liste des contribuables :
o [slice(10,12)] est l’ensemble des indices [10, 11, 12] ;
o [taxpayers[slice(10,12)]] est la liste [taxpayers[10], taxpayers[11], taxpayers[12] ;
• ligne 45 : on incrémente la valeur de i qui contrôle la boucle de la ligne 39 ;
• ligne 47 : on crée un thread :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
493/755
o [target=thread_function] fixe la fonction qu’exécutera le thread. C’est la fonction des lignes 16-17. Elle attend trois
paramètres ;
o [ags] est la liste des trois paramètres attendus par la fonction [thread_function] ;
Créer un thread ne l’exécute pas. Ca crée un objet et c’est tout ;
• lignes 48-49 : le thread qui vient d’être créé est ajouté à la liste des threads créés par le thread principal ;
• ligne 51 : le thread est lancé. Il va alors être exécuté en parallèle des autres threads actifs. Ici, il va exécuter la fonction
[thread_function] avec les arguments qu’on lui a donnés ;
• lignes 53-54 : le thread principal attend chacun des threads qu’il a lancés. Prenons un exemple :
o le thread principal a lancé trois threads [th1, th2, th3] ;
o le thread principal se met en attente de chacun des threads (lignes 53-54) dans l’ordre de la boucle for : [th1, th2, th3] ;
o supposons que les threads se terminent dans l’ordre [th2, th1, th3] ;
o le thread principal attend la fin de th1. Lorsque th2 se termine, il ne se passe rien ;
o lorsque th1 se termine, le thread principal se met en attente de th2. Or celui-ci est terminé. Le thread principal passe alors
au thread suivant et attend th3 ;
o lorsque th3 se termine, le thread principal a terminé son attente et passe alors à l’exécution de la ligne 57 ;
• la ligne 57 écrit les résultats obtenus dans le fichier des résultats. On a là un bon exemple des références d’objets :
o ligne 43 : la liste [thread_payers] associée à un thread contient des copies des références d’objets contenus dans la liste
[taxpayers] ;
o on sait que le calcul de l’impôt va modifier les objets pointés par les références de la liste [thread_payers]. Ces objets
vont se voir enrichis des résultats du calcul de l’impôt. Néanmoins les références elles ne sont pas modifiées. Donc les
références de la liste initiale [taxpayers] ‘voient’ ou ‘pointent sur’ les objets modifiés ;
• les fonctions exécutées simultanément par plusieurs threads sont souvent délicates à écrire : on doit toujours vérifier que le
code n’essaie pas de modifier une donnée partagée entre threads. Lorsque ce dernier cas se produit, il faut mettre en place un
accès synchronisé à la donnée partagée qui va être modifiée ;
• ligne 3 : la fonction reçoit trois paramètres :
o [dao] : une référence sur la couche [dao]. Cette donnée est partagée ;
o [logger] : une référence sur le logueur. Cette donnée est partagée ;
o [taxpayers] : une liste de contribuables. Cette donnée n’est pas partagée : chaque thread gère une liste différente ;
• examinons les deux références [dao, logger] :
o on a vu que l’objet pointé par la référence [dao] avait une référence [self.__logger] qui était modifiée par les threads
mais pour y mettre une valeur commune à tous les threads ;
o la référence [logger] pointe sur un descripteur de fichier. On a vu qu’il pouvait y avoir un problème lors de l’écriture des
logs dans le fichier. Pour cette raison l’écriture dans le fichier a été synchronisée ;
• lignes 5-6 : on logue le nom du thread et le nombre de contribuables qu’il doit gérer ;
• lignes 8-14 : calcul de l’impôt des contribuables ;
• ligne 16 : on logue la fin du thread ;
24.4.4 Exécution
Lançons le serveur web comme dans le paragraphe précédent (serveur web, SGBD, hMailServer, Thunderbird), puis exécutons le
script [main] du client. Dans les fichiers [data/output/errors.txt, data/output/résultats.json] on a les mêmes résultats que
dans la version précédente. Dans le fichier [data/logs/logs.txt], on a les logs suivants :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
494/755
5. 2020-07-24 10:05:20.946502, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants":
2, "salaire": 50000}
6. 2020-07-24 10:05:20.947003, Thread-3 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants":
3, "salaire": 100000}
7. 2020-07-24 10:05:20.947003, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
8. 2020-07-24 10:05:20.950324, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants":
3, "salaire": 100000}
9. 2020-07-24 10:05:20.948449, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
10. 2020-07-24 10:05:20.953645, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants":
2, "salaire": 30000}
11. 2020-07-24 10:05:20.976143, Thread-1 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire":
55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
12. 2020-07-24 10:05:20.976695, Thread-1 : fin du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2,
"salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
13. 2020-07-24 10:05:20.976695, Thread-1 : fin du thread [Thread-1]
14. 2020-07-24 10:05:21.973914, Thread-2 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire":
50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}}}
15. 2020-07-24 10:05:21.973914, Thread-2 : fin du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2,
"salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}
16. 2020-07-24 10:05:21.973914, Thread-2 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants":
3, "salaire": 50000}
17. 2020-07-24 10:05:21.977130, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire":
100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}}}
18. 2020-07-24 10:05:21.977130, Thread-4 : fin du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3,
"salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}
19. 2020-07-24 10:05:21.977130, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants":
5, "salaire": 100000}
20. 2020-07-24 10:05:21.982634, Thread-3 : {"réponse": {"result": {"marié": "non", "enfants": 3, "salaire":
100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}}}
21. 2020-07-24 10:05:21.982634, Thread-5 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire":
30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}}}
22. 2020-07-24 10:05:21.983134, Thread-3 : fin du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3,
"salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}
23. 2020-07-24 10:05:21.983134, Thread-5 : fin du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2,
"salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}
24. 2020-07-24 10:05:21.983134, Thread-3 : fin du thread [Thread-3]
25. 2020-07-24 10:05:21.983763, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants":
0, "salaire": 200000}
26. 2020-07-24 10:05:22.008562, Thread-5 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire":
200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}}}
27. 2020-07-24 10:05:22.008562, Thread-5 : fin du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0,
"salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}
28. 2020-07-24 10:05:22.009062, Thread-5 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants":
3, "salaire": 200000}
29. 2020-07-24 10:05:22.016848, Thread-5 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire":
200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
30. 2020-07-24 10:05:22.017349, Thread-5 : fin du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3,
"salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
31. 2020-07-24 10:05:22.017349, Thread-5 : fin du thread [Thread-5]
32. 2020-07-24 10:05:23.008486, Thread-2 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire":
50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}}}
33. 2020-07-24 10:05:23.008486, Thread-2 : fin du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3,
"salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}
34. 2020-07-24 10:05:23.009749, Thread-2 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants":
2, "salaire": 100000}
35. 2020-07-24 10:05:23.011722, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 5, "salaire":
100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
36. 2020-07-24 10:05:23.013723, Thread-4 : fin du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5,
"salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
37. 2020-07-24 10:05:23.013723, Thread-4 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants":
0, "salaire": 100000}
38. 2020-07-24 10:05:23.024135, Thread-2 : {"réponse": {"result": {"marié": "non", "enfants": 2, "salaire":
100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}}}
39. 2020-07-24 10:05:23.024135, Thread-2 : fin du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2,
"salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}
40. 2020-07-24 10:05:23.025178, Thread-2 : fin du thread [Thread-2]
41. 2020-07-24 10:05:23.025178, Thread-4 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire":
100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}}}
42. 2020-07-24 10:05:23.026191, Thread-4 : fin du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0,
"salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}
43. 2020-07-24 10:05:23.026191, Thread-4 : fin du thread [Thread-4]
• ces logs montrent que cinq threads ont été lancés pour calculer l’impôt de 11 contribuables. Ces cinq threads ont lancé des
requêtes simultanées au serveur de calcul de l’impôt. Il faut comprendre comment ça marche :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
495/755
o le thread [Thread-1] est lancé le premier. Lorsqu’il a le processeur, il avance dans le code jusqu’à envoyer sa requête
HTTP. Comme il doit attendre le résultat de celle-ci, il est automatiquement mis en attente. Il perd alors le processeur et
un autre thread obtient celui-ci ;
o lignes 1-10 : le même processus se répète pour chacun des 5 threads. Ainsi les 5 threads sont lancés avant même que le
thread [Thread-1] ait reçu sa réponse ligne 11 ;
• les threads ne se terminent pas dans l’ordre où ils ont été lancés. Ainsi c’est le thread [Thread-3] qui termine le premier, ligne
23 ;
Côté serveur, les logs dans le fichier [data/logs/logs.txt] sont les suivants :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
496/755
La classe de test sera exécutée dans l’environnement suivant :
• la configuration [2] est identique à la configuration [1] que nous venons d’étudier ;
1. import unittest
2.
3. from Logger import Logger
4.
5.
6. class TestHttpClientDao(unittest.TestCase):
7.
8. def test_1(self) -> None:
9. from TaxPayer import TaxPayer
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
497/755
10.
11. # {'marié': 'oui', 'enfants': 2, 'salaire': 55555,
12. # 'impôt': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
13. taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
14. dao.calculate_tax(taxpayer)
15. # vérification
16. self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
17. self.assertEqual(taxpayer.décôte, 0)
18. self.assertEqual(taxpayer.réduction, 0)
19. self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
20. self.assertEqual(taxpayer.surcôte, 0)
21.
22. …
23.
24.
25. if __name__ == '__main__':
26. # on configure l'application
27. import config
28. config = config.configure({})
29.
30. # logger
31. logger = Logger(config["logsFilename"])
32. # on le mémorise dans la config
33. config["logger"] = logger
34. # on récupère la couche [dao]
35. dao = config["layers"]["dao"]
36.
37. # on exécute les méthodes de test
38. print("tests en cours...")
39. unittest.main()
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/impots/http-clients/02/tests/TestHttpClientDao.py
2. tests en cours...
3. ...........
4. ----------------------------------------------------------------------
5. Ran 11 tests in 6.128s
6.
7. OK
8.
9. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
498/755
25 Exercice d’application : version 8
25.1 Introduction
Nous allons écrire une nouvelle application client / serveur. La nouveauté du serveur est qu’il va gérer une session. Au lieu de mettre
les données de l’administration fiscale dans un objet de portée [application], on va les mettre dans un objet de portée [session].
Ce faisant, on régresse dans les performances du code. Lorsqu’un objet peut être partagé en lecture seule par tous les utilisateurs, il
est préférable d’en faire un objet de portée [application] plutôt que de portée [session]. On gagne au minimum de la bande
passante puisque qu’on diminue ainsi la taille du cookie de session. Mais nous voulons montrer une application client / serveur où le
client et le serveur s’échangent un cookie de session.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
499/755
25.2 Le serveur web
L’arborescence des scripts du serveur est la suivante :
Le dossier [http-servers/03] est obtenu initialement par recopie du dossier [http-servers/02]. On procède ensuite à des
modifications.
25.2.1 La configuration
Elle est la même que dans la |version précédente| avec quelques modifications dans le script [config] :
1. # dépendances absolues
2. absolute_dependencies = [
3. # dossiers du projet
4. # BaseEntity, MyException
5. f"{root_dir}/classes/02/entities",
6. # InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
7. f"{root_dir}/impots/v04/interfaces",
8. # AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
9. f"{root_dir}/impots/v04/services",
10. # ImpotsDaoWithAdminDataInDatabase
11. f"{root_dir}/impots/v05/services",
12. # AdminData, ImpôtsError, TaxPayer
13. f"{root_dir}/impots/v04/entities",
14. # Constantes, Tranches
15. f"{root_dir}/impots/v05/entities",
16. # index_controller
17. f"{script_dir}/../controllers",
18. # scripts [config_database, config_layers]
19. script_dir,
20. # Logger, SendAdminMail
21. f"{root_dir}/impots/http-servers/02/utilities",
22. ]
• ligne 4 : on crée une clé secrète pour l’application. On sait que celle-ci est nécessaire pour gérer les sessions ;
Ensuite, les données fiscale ne sont plus demandées dans le code de [main]. Les lignes suivantes sont supprimées :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
500/755
3. try:
4. # admindata sera une donnée de portée application en lecture seule
5. config["admindata"] = config["layers"]["dao"].get_admindata()
6. # log de réussite
7. logger.write("[serveur] connexion à la base de données réussie\n")
8. except ImpôtsError as ex:
9. # on note l'erreur
10. erreur = True
11. # log d'erreur
12. log = f"L'erreur suivante s'est produite : {ex}"
13. # console
14. print(log)
15. # fichier de logs
16. logger.write(f"{log}\n")
17. # mail à l'administrateur
18. send_adminmail(config, log)
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
501/755
55. # on rend la réponse au client
56. return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
57. except (BaseException, ImpôtsError) as erreur:
58. # on rend la réponse au client
59. return {"réponse": {"erreurs": [f"{erreur}"]}}, status.HTTP_500_INTERNAL_SERVER_ERROR
1. # imports
2.
3. import requests
4. from flask_api import status
5. from myutils import decode_flask_session
6.
7. from AbstractImpôtsDao import AbstractImpôtsDao
8. from AdminData import AdminData
9. from ImpôtsError import ImpôtsError
10. from InterfaceImpôtsMétier import InterfaceImpôtsMétier
11. from TaxPayer import TaxPayer
12.
13.
14. class ImpôtsDaoWithHttpSession(AbstractImpôtsDao, InterfaceImpôtsMétier):
15.
16. # constructeur
17. def __init__(self, config: dict):
18. # initialisation parent
19. AbstractImpôtsDao.__init__(self, config)
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
502/755
20. # mémorisation éléments de la configuration
21. # config générale
22. self.__config = config
23. # serveur
24. self.__config_server = config["server"]
25. # mode debug
26. self.__debug = config["debug"]
27. # logger
28. self.__logger = None
29. # cookies
30. self.__cookies = None
31.
32. # méthode inutilisée
33. def get_admindata(self) -> AdminData:
34. pass
35.
36. # calcul de l'impôt
37. def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
38. # on laisse remonter les exceptions
39. # paramètres du get
40. params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
41. # connexion avec authentification Auth Basic ?
42. if self.__config_server ['authBasic']:
43. response = requests.get(
44. # URL du serveur interrogé
45. self.__config_server['urlServer'],
46. # paramètres de l'URL
47. params=params,
48. # authentification Basic
49. auth=(
50. self.__config_server["user"]["login"],
51. self.__config_server["user"]["password"]),
52. cookies=self.__cookies)
53.
54. else:
55. # connexion sans authentification Auth Basic
56. response = requests.get(self.__config_server['urlServer'], params=params, cookies=self.__cookies)
57. # on récupère les cookies de la réponse s'il y en a
58. if response.cookies:
59. self.__cookies = response.cookies
60. # on récupère le cookie de session
61. session_cookie = response.cookies.get('session')
62. # on le décode pour le loguer
63. if session_cookie:
64. # logueur
65. if not self.__logger:
66. self.__logger = self.__config['logger']
67. # on logue
68. self.__logger.write(f"cookie de session={decode_flask_session(session_cookie)}\n")
69.
70. # mode debug ?
71. if self.__debug:
72. # logueur
73. if not self.__logger:
74. self.__logger = self.__config['logger']
75. # on logue
76. self.__logger.write(f"{response.text}\n")
77. # code de statut de la réponse HTTP
78. status_code = response.status_code
79. # on met la réponse jSON dans un dictionnaire
80. résultat = response.json()
81. # erreur si code de statut différent de 200 OK
82. if status_code != status.HTTP_200_OK:
83. # on sait que les erreurs ont été associées à la clé [erreurs] de la réponse
84. raise ImpôtsError(87, résultat['réponse']['erreurs'])
85. # on sait que le résultat a été associé à la clé [result] de la réponse
86. # on modifie le paramètre d'entrée avec ce résultat
87. taxpayer.fromdict(résultat["réponse"]["result"])
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
503/755
Il faut se souvenir maintenant que la couche [dao] va être exécutée simultanément par plusieurs threads. Il faut donc regarder toutes
les propriétés de l’instance de classe et voir si l’accès simultané à ces propriétés pose problème. Ici nous avons ajouté la propriété
[self.__cookies], ligne 30. Cette propriété est modifiée à la ligne 59. On a donc un accès en écriture à une donnée partagée par tous
les threads. Or cet accès pose problème : chaque thread représentant un client donné a son propre cookie de session. En effet, dedans
il y a un n° de client (=thread) unique pour chaque client. Si on ne fait rien, le thread T2 peut écraser les cookies du thread T1.
On a déjà vu une méthode pour gérer ce problème : on peut créer dans le fichier [config] passé en paramètre au constructeur (ligne
17), des clés différentes pour chaque thread. On peut par exemple utiliser comme clé le nom du thread :
config[thread_name][‘cookies’]=cookies
cookies=config[thread_name][‘cookies’]
Nous allons ici utiliser une technique différente : chaque thread (=client) aura sa propre couche [dao]. Ainsi la ligne 59 ne pose plus
problème car les cookies utilisés sont alors ceux d’un unique client.
Nous avons déjà étudié le script |myutils|. Ce script est un module de portée machine que les différents scripts de ce cours peuvent
importer avec l’instruction :
import myutils
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
504/755
from .myutils import set_syspath, json_response, decode_flask_session
La nouvelle version de [myutils] est installée parmi les modules de portée machine avec la commande [pip install .] dans un
terminal Pycharm :
• ligne 1 : il faut être dans le dossier [packages] pour taper cette instruction ;
25.3.1.3 La classe [ImpôtsDaoWithHttpSessionFactory]
La classe [ImpôtsDaoWithHttpSessionFactory] est la suivante :
• la classe [ImpôtsDaoWithHttpSessionFactory] permet de créer une nouvelle implémentation de la couche [dao] avec la
méthode [new_instance] des lignes 10-12 ;
25.3.2 La configuration
Le script [config_layers] qui configure les couches du client web est modifié de la façon suivante :
• lignes 5-6 : au lieu d’instancier une couche [dao] unique comme c’était fait précédemment, on instancie une ‘factory’ de cette
couche (factory=usine de production d’objets, ici la couche [dao]) ;
• lignes 9-11 : on rend la configuration des couches ;
1. # on configure l'application
2.
3. import config
4. config = config.configure({})
5.
6. # dépendances
7. from ImpôtsError import ImpôtsError
8. import random
9. import sys
10. import threading
11. from Logger import Logger
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
505/755
12.
13.
14. # exécution de la couche [dao] dans un thread
15. # taxpayers est une liste de contribuables
16. def thread_function(thread_dao, logger, taxpayers: list):
17. …
18.
19.
20. # liste des threads du client
21. threads = []
22. logger = None
23. # code
24. try:
25. …
26. l_taxpayers = len(taxpayers)
27. while i < len(taxpayers):
28. …
29. # chaque thread doit avoir sa propre couche [dao] pour gérer correctement son cookie de session
30. thread_dao = dao_factory.new_instance()
31. # on crée le thread
32. thread = threading.Thread(target=thread_function, args=(thread_dao, logger, thread_taxpayers))
33. # on l'ajoute à la liste des threads du script principal
34. threads.append(thread)
35. # on lance le thread - cette opération est asynchrone - on n'attend pas le résultat du thread
36. thread.start()
37. # le thread principal attend la fin de tous les threads qu'il a lancés
38. …
39. except BaseException as erreur:
40. # affichage de l'erreur
41. print(f"L'erreur suivante s'est produite : {erreur}")
42. finally:
43. # on ferme le logueur
44. if logger:
45. logger.close()
46. # on a fini
47. print("Travail terminé...")
48. # fin des threads qui pourraient encore exister si on s'est arrêté sur erreur
49. sys.exit()
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
506/755
16. 2020-07-25 10:21:46.726960, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants":
3, "salaire": 100000}
17. 2020-07-25 10:21:47.514108, Thread-3 : cookie de
session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0
,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0
,156244.0,24999.5],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celiba
taire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_
revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_d
emi_part":3797.0},"client_id":"700e3f5dc808c7c48f0c9007"}
18. 2020-07-25 10:21:47.514610, Thread-3 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire":
50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}}}
19. 2020-07-25 10:21:47.514939, Thread-3 : fin du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3,
"salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}
20. 2020-07-25 10:21:47.514939, Thread-3 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants":
2, "salaire": 100000}
21. 2020-07-25 10:21:47.527211, Thread-5 : cookie de
session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0
,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0
,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celiba
taire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_
revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_d
emi_part":3797.0},"client_id":"9e14a5d4a3057f69ab95ab2d"}
22. 2020-07-25 10:21:47.527211, Thread-2 : cookie de
session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0
,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0
,156244.0,22500.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celiba
taire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_
revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_d
emi_part":3797.0},"client_id":"a06e8fd70a44c9e311f4dce0"}
23. 2020-07-25 10:21:47.527211, Thread-5 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire":
100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}}}
24. 2020-07-25 10:21:47.527211, Thread-1 : cookie de
session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0
,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0
,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celiba
taire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_
revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_d
emi_part":3797.0},"client_id":"28c38df998f67685b3a482b8"}
25. 2020-07-25 10:21:47.527211, Thread-2 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire":
50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}}}
26. 2020-07-25 10:21:47.528341, Thread-5 : fin du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0,
"salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}
27. 2020-07-25 10:21:47.528341, Thread-1 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire":
55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
28. 2020-07-25 10:21:47.528842, Thread-2 : fin du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2,
"salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}
29. 2020-07-25 10:21:47.529349, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants":
2, "salaire": 30000}
30. 2020-07-25 10:21:47.529699, Thread-1 : fin du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2,
"salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
31. 2020-07-25 10:21:47.529699, Thread-2 : fin du thread [Thread-2]
32. 2020-07-25 10:21:47.531905, Thread-1 : fin du thread [Thread-1]
33. 2020-07-25 10:21:47.536121, Thread-6 : cookie de
session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0
,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0
,156244.0,93749.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celiba
taire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_
revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_d
emi_part":3797.0},"client_id":"38499b63076516c02f2770ec"}
34. 2020-07-25 10:21:47.537161, Thread-3 : {"réponse": {"result": {"marié": "non", "enfants": 2, "salaire":
100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}}}
35. 2020-07-25 10:21:47.537161, Thread-6 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire":
200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
36. 2020-07-25 10:21:47.538156, Thread-3 : fin du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2,
"salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}
37. 2020-07-25 10:21:47.538557, Thread-6 : fin du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3,
"salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
38. 2020-07-25 10:21:47.538828, Thread-3 : fin du thread [Thread-3]
39. 2020-07-25 10:21:47.538828, Thread-6 : fin du thread [Thread-6]
40. 2020-07-25 10:21:47.546198, Thread-5 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire":
30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}}}
41. 2020-07-25 10:21:47.546198, Thread-5 : fin du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2,
"salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}
42. 2020-07-25 10:21:47.546198, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants":
0, "salaire": 200000}
43. 2020-07-25 10:21:47.739643, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire":
100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}}}
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
507/755
44. 2020-07-25 10:21:47.739643, Thread-4 : fin du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3,
"salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}
45. 2020-07-25 10:21:47.740668, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants":
5, "salaire": 100000}
46. 2020-07-25 10:21:48.557469, Thread-5 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire":
200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}}}
47. 2020-07-25 10:21:48.558715, Thread-5 : fin du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0,
"salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}
48. 2020-07-25 10:21:48.558715, Thread-5 : fin du thread [Thread-5]
49. 2020-07-25 10:21:48.753025, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 5, "salaire":
100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
50. 2020-07-25 10:21:48.753318, Thread-4 : fin du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5,
"salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
51. 2020-07-25 10:21:48.753540, Thread-4 : fin du thread [Thread-4]
• on a au total 6 threads donc 6 clients (lignes 1, 3, 4, 6, 8, 9) qui interrogent simultanément le serveur de calcul de l’impôt ;
• nous allons suivre le thread [Thread-4] qui gère 3 contribuables (ligne 6). Il va faire séquentiellement trois requêtes au serveur
de calcul de l’impôt ;
• ligne 10 : la 1ère requête de [Thread-4] ;
• ligne 13 : [Thread-4] a reçu la réponse à sa 1ère requête. Dedans elle trouve un cookie de session dans lequel il y a le n°
[fa3c83b82761c83e13217967] que lui a attribué le serveur ;
• ligne 14 : l’impôt du 1er contribuable ;
• ligne 16 : [Thread-4] fait une requête pour le 2ième contribuable ;
• ligne 43 : [Thread-4] reçoit l’impôt du 2ième contribuable ;
• ligne 45 : [Thread-4] fait une requête pour le 3ième contribuable ;
• ligne 49 : [Thread-4] reçoit l’impôt du 3ième contribuable ;
• ligne 51 : [Thread-4] a terminé son travail ;
Maintenant, regardons comment les 3 requêtes de [Thread-4] ont été traitées côté serveur. On va pouvoir le suivre dans les logs du
serveur grâce à son n° de client [fa3c83b82761c83e13217967].
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
508/755
25. 2020-07-25 10:21:47.514939, Thread-5 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2,
'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'taux': 0.14, 'décôte': 384, 'réduction': 347}}}
26. 2020-07-25 10:21:47.520727, Thread-7 : [index_controller] client [38499b63076516c02f2770ec], données
fiscales prises dans la couche dao
27. 2020-07-25 10:21:47.523162, Thread-7 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3,
'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
28. 2020-07-25 10:21:47.530835, Thread-9 : [index] requête : <Request
'http://127.0.0.1:5000/?marié=non&enfants=2&salaire=100000' [GET]>
29. 2020-07-25 10:21:47.531736, Thread-9 : [index_controller] client [700e3f5dc808c7c48f0c9007], données
fiscales prises en session
30. 2020-07-25 10:21:47.531905, Thread-9 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 2,
'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
31. 2020-07-25 10:21:47.541899, Thread-10 : [index] requête : <Request
'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=30000' [GET]>
32. 2020-07-25 10:21:47.542488, Thread-10 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données
fiscales prises en session
33. 2020-07-25 10:21:47.542488, Thread-10 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2,
'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'taux': 0.0, 'décôte': 0, 'réduction': 0}}}
34. 2020-07-25 10:21:47.553628, Thread-11 : [index] requête : <Request
'http://127.0.0.1:5000/?marié=non&enfants=0&salaire=200000' [GET]>
35. 2020-07-25 10:21:47.553628, Thread-11 : [index] mis en pause du thread pendant 1 seconde(s)
36. 2020-07-25 10:21:47.736910, Thread-8 : [index_controller] client [fa3c83b82761c83e13217967], données
fiscales prises en session
37. 2020-07-25 10:21:47.737191, Thread-8 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3,
'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'taux': 0.3, 'décôte': 0, 'réduction': 0}}}
38. 2020-07-25 10:21:47.748226, Thread-12 : [index] requête : <Request
'http://127.0.0.1:5000/?marié=oui&enfants=5&salaire=100000' [GET]>
39. 2020-07-25 10:21:47.748226, Thread-12 : [index] mis en pause du thread pendant 1 seconde(s)
40. 2020-07-25 10:21:48.554695, Thread-11 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données
fiscales prises en session
41. 2020-07-25 10:21:48.555070, Thread-11 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0,
'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'taux': 0.45, 'décôte': 0, 'réduction': 0}}}
42. 2020-07-25 10:21:48.748753, Thread-12 : [index_controller] client [fa3c83b82761c83e13217967], données
fiscales prises en session
43. 2020-07-25 10:21:48.748753, Thread-12 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 5,
'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
• on trouve le client [fa3c83b82761c83e13217967] pour la 1ère fois en ligne 14 : pour calculer l’impôt, le serveur a dû aller
chercher en base les données de l’administration fiscale ;
• puis on retrouve le client [fa3c83b82761c83e13217967] en ligne 36. Cette fois-ci, le serveur trouve les données de
l’administration fiscale en session, ce qui lui évite un accès, possiblement coûteux, à la couche [dao] ;
• on retrouve le client [fa3c83b82761c83e13217967] une 3ième fois en ligne 42, où là encore le serveur utilise la session du client ;
Cet exemple montre bien l’intérêt de la session pour un client : on y met des données partagées par toutes les requêtes de ce client et
qui sont coûteuses à acquérir.
Côté client, les résultats dans le fichier [data/output/résultats.json] sont les mêmes que pour les versions précédentes.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
509/755
La classe de test sera exécutée dans l’environnement suivant :
• la configuration [2] est identique à la configuration [1] que nous venons d’étudier ;
1. import unittest
2.
3. from Logger import Logger
4.
5.
6. class TestHttpClientDao(unittest.TestCase):
7.
8. def test_1(self) -> None:
9. from TaxPayer import TaxPayer
10.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
510/755
11. # {'marié': 'oui', 'enfants': 2, 'salaire': 55555,
12. # 'impôt': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
13. taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
14. dao.calculate_tax(taxpayer)
15. # vérification
16. self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
17. self.assertEqual(taxpayer.décôte, 0)
18. self.assertEqual(taxpayer.réduction, 0)
19. self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
20. self.assertEqual(taxpayer.surcôte, 0)
21. …
22.
23.
24. if __name__ == '__main__':
25. # on configure l'application
26. import config
27. config = config.configure({})
28.
29. # logger
30. logger = Logger(config["logsFilename"])
31. # on le mémorise dans la config
32. config["logger"] = logger
33. # on récupère la factory de la couche [dao]
34. dao_factory = config["layers"]["dao_factory"]
35. # on crée une instance de la couche [dao]
36. dao = dao_factory.new_instance()
37.
38. # on exécute les méthodes de test
39. print("tests en cours...")
40. unittest.main()
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/impots/http-clients/03/tests/TestHttpClientDao.py
2. tests en cours...
3. ...........
4. ----------------------------------------------------------------------
5. Ran 11 tests in 3.392s
6.
7. OK
8.
9. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
511/755
26 Du dictionnaire à XML et vice-versa
Nous nous proposons ici de découvrir le module [xml2dict] qui permet de transformer :
Avant l’avènement du jSON, la réponse des services web était souvent du XML (eXtended Markup Language). Par ailleurs, le
protocole de ces services web était souvent SOAP (Simple Object Process Protocol). SOAP est un protocole qui s’appuie sur le
protocole HTTP du web. Actuellement (2020), les services web sont plutôt de type REST (Representational State Transfer). Les
services web que nous avons étudiés ne sont d’aucun de ces types mais sont définitivement plus proches de REST que de SOAP.
Néanmoins je préfère dire qu’ils sont de type ‘libre’ ou ‘inconnu’ car ils ne respectent pas toutes les règles du REST.
Nous allons montrer combien il est facile de transformer nos architectures client / serveur jSON en architectures client / serveur
XML. Il suffit d’utiliser le module [xmltodict].
Ceci fait, nous allons étudier sur un exemple ce qu’on peut faire avec ce module :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
512/755
37. 'erreurs': ['Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]', 'paramètre [marié]
manquant',
38. 'paramètre [enfants] manquant', 'paramètre [salaire] manquant']}})
39. # test 5
40. transform("test 5", {'réponse': {
41. 'result': {'id': 0, 'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'décôte': 384, 'surcôte': 0,
42. 'réduction': 347, 'taux': 0.14}}})
43. # test 6
44. transform("test 6", {"root": {'liste': ["un", "deux", "trois"]}})
45. # test 7
46. transform("test 7", {"root": {'liste': [{"un": [10, 11]}, {"deux": [20, 21]}, {"trois": [30, 31]}]}})
• lignes 14-25 : la fonction [transform] reçoit un texte à écrire [message] et un dictionnaire [dictionary] ;
• ligne 16 : affichage du message ;
• ligne 17 : on affiche le dictionnaire reçu ;
• lignes 19-20 : ce dictionnaire est transformé en chaîne XML et celle-ci est affichée. La méthode qui sait faire cela est
[xmltodict.unparse] ;
• lignes 21-23 : la chaîne XML précédente est transformée en dictionnaire et celui-ci est affiché. La méthode qui sait faire cela
est [xmltodict.parse]. Cette méthode ne produit pas un dictionnaire de type [dict] mais de type [OrderedDict] (ligne 1) ;
• lignes 24-25 : on transforme le type [OrderedDict] obtenu en type [dict] à l’aide de la méthode (non encore écrite)
[ordereddict2dict]. Cette méthode travaille de façon récursive. Si certaines valeurs du dictionnaire sont de type [OrderedDict,
list], les valeurs de ces collections sont examinées pour savoir si elles-aussi sont de type [OrderedDict]. Si c’est le cas, elles
sont transformées en type [dict]. On remarquera que la méthode [xmltodict.parse] ne produit aucun dictionnaire de type
[dict] ;
Avant d’examiner les fonctions manquantes, examinons les résultats pour voir ce qui est cherché :
1. test 1-------
2. dictionnaire={'nom': 'séléné'}
3. xml=<?xml version="1.0" encoding="utf-8"?>
4. <nom>séléné</nom>
5. ordereddict_dictionary1=OrderedDict([('nom', 'séléné')])
6. dict_dictionary1={'nom': 'séléné'}
• ligne 2 : le dictionnaire testé. Il faut noter un point important : la méthode [xml2dict.unparse] exisge que le dictionnaire soit
de la forme {‘clé’ : valeur} où [valeur] peut être ensuite un dictionnaire, une liste, un type simple ;
• lignes 3-4 : la chaîne XML issue du dictionnaire. Elle est précédée de l’entête [<?xml version="1.0" encoding="utf-8"?>\n]
qui est normalement la 1ère ligne d’un fichier XML ;
• ligne 5 : le type [OrderedDict] obtenu par la méthode [xml2dict.parse] recevant pour paramètre la chaîne XML précédente ;
• ligne 6 : le dictionnaire de type [dict] obtenu en appliquant la méthode [ordereddict2dict] au type précédent. On retrouve
le dictionnaire d’origine de la ligne 2 ;
Tous les autres tests sont construits sur le même schéma et devraient vous permettre de comprendre comment passer d’un
dictionnaire à une chaîne XML puis de cette chaîne XML au dictionnaire d’origine.
1. test 2-------
2. dictionnaire={'famille': {'père': {'prénom': 'andré'}, 'mère': {'prénom': 'angèle'}, 'nom': 'séléné'}}
3. xml=<?xml version="1.0" encoding="utf-8"?>
4. <famille><père><prénom>andré</prénom></père><mère><prénom>angèle</prénom></mère><nom>séléné</nom></famille>
5. ordereddict_dictionary1=OrderedDict([('famille', OrderedDict([('père', OrderedDict([('prénom', 'andré')])),
('mère', OrderedDict([('prénom', 'angèle')])), ('nom', 'séléné')]))])
6. dict_dictionary1={'famille': {'père': {'prénom': 'andré'}, 'mère': {'prénom': 'angèle'}, 'nom': 'séléné'}}
7.
8. test 3-------
9. dictionnaire={'famille': {'nom': 'séléné', 'père': {'prénom': 'andré'}, 'mère': {'prénom': 'angèle'},
'hobbies': ['chant', 'footing']}}
10. xml=<?xml version="1.0" encoding="utf-8"?>
11. <famille><nom>séléné</nom><père><prénom>andré</prénom></père><mère><prénom>angèle</prénom></mère><hobbies>c
hant</hobbies><hobbies>footing</hobbies></famille>
12. ordereddict_dictionary1=OrderedDict([('famille', OrderedDict([('nom', 'séléné'), ('père',
OrderedDict([('prénom', 'andré')])), ('mère', OrderedDict([('prénom', 'angèle')])), ('hobbies', ['chant',
'footing'])]))])
13. dict_dictionary1={'famille': {'nom': 'séléné', 'père': {'prénom': 'andré'}, 'mère': {'prénom': 'angèle'},
'hobbies': ['chant', 'footing']}}
14.
15. test 4-------
16. dictionnaire={'réponse': {'erreurs': ['Méthode GET requise avec les seuls paramètres [marié, enfants,
salaire]', 'paramètre [marié] manquant', 'paramètre [enfants] manquant', 'paramètre [salaire] manquant']}}
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
513/755
17. xml=<?xml version="1.0" encoding="utf-8"?>
18. <réponse><erreurs>Méthode GET requise avec les seuls paramètres [marié, enfants,
salaire]</erreurs><erreurs>paramètre [marié] manquant</erreurs><erreurs>paramètre [enfants]
manquant</erreurs><erreurs>paramètre [salaire] manquant</erreurs></réponse>
19. ordereddict_dictionary1=OrderedDict([('réponse', OrderedDict([('erreurs', ['Méthode GET requise avec les
seuls paramètres [marié, enfants, salaire]', 'paramètre [marié] manquant', 'paramètre [enfants] manquant',
'paramètre [salaire] manquant'])]))])
20. dict_dictionary1={'réponse': {'erreurs': ['Méthode GET requise avec les seuls paramètres [marié, enfants,
salaire]', 'paramètre [marié] manquant', 'paramètre [enfants] manquant', 'paramètre [salaire] manquant']}}
21.
22. test 5-------
23. dictionnaire={'réponse': {'result': {'id': 0, 'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt':
1384, 'décôte': 384, 'surcôte': 0, 'réduction': 347, 'taux': 0.14}}}
24. xml=<?xml version="1.0" encoding="utf-8"?>
25. <réponse><result><id>0</id><marié>oui</marié><enfants>2</enfants><salaire>50000</salaire><impôt>1384</impôt
><décôte>384</décôte><surcôte>0</surcôte><réduction>347</réduction><taux>0.14</taux></result></réponse>
26. ordereddict_dictionary1=OrderedDict([('réponse', OrderedDict([('result', OrderedDict([('id', '0'),
('marié', 'oui'), ('enfants', '2'), ('salaire', '50000'), ('impôt', '1384'), ('décôte', '384'), ('surcôte',
'0'), ('réduction', '347'), ('taux', '0.14')]))]))])
27. dict_dictionary1={'réponse': {'result': {'id': '0', 'marié': 'oui', 'enfants': '2', 'salaire': '50000',
'impôt': '1384', 'décôte': '384', 'surcôte': '0', 'réduction': '347', 'taux': '0.14'}}}
28.
29. test 6-------
30. dictionnaire={'root': {'liste': ['un', 'deux', 'trois']}}
31. xml=<?xml version="1.0" encoding="utf-8"?>
32. <root><liste>un</liste><liste>deux</liste><liste>trois</liste></root>
33. ordereddict_dictionary1=OrderedDict([('root', OrderedDict([('liste', ['un', 'deux', 'trois'])]))])
34. dict_dictionary1={'root': {'liste': ['un', 'deux', 'trois']}}
35.
36. test 7-------
37. dictionnaire={'root': {'liste': [{'un': [10, 11]}, {'deux': [20, 21]}, {'trois': [30, 31]}]}}
38. xml=<?xml version="1.0" encoding="utf-8"?>
39. <root><liste><un>10</un><un>11</un></liste><liste><deux>20</deux><deux>21</deux></liste><liste><trois>30</t
rois><trois>31</trois></liste></root>
40. ordereddict_dictionary1=OrderedDict([('root', OrderedDict([('liste', [OrderedDict([('un', ['10', '11'])]),
OrderedDict([('deux', ['20', '21'])]), OrderedDict([('trois', ['30', '31'])])])]))])
41. dict_dictionary1={'root': {'liste': [{'un': ['10', '11']}, {'deux': ['20', '21']}, {'trois': ['30',
'31']}]}}
42.
43. Process finished with exit code 0
Attardons-nous maintenant sur la méthode [ordereddict2dict] qui transforme un type [OrderedDict] en type [dict] :
1. # xmltodict.parse pour passer du XML au dictionnaire. Le dictionnaire doit avoir une racine
2. # le dictionnaire produit est de type OrderedDict
3. # xmltodict.unparse pour passer du dictionnaire au XML
4.
5. def check(value):
6. # si la valeur est de type OrderedDict, on la transforme
7. if isinstance(value, OrderedDict):
8. value2 = ordereddict2dict(value)
9. # si la valeur est de type list, on la transforme
10. elif isinstance(value, list):
11. value2 = list2list(value)
12. else:
13. # on est en présence d'un type simple pas d'une collection
14. value2 = value
15. # on rend la nouvelle valeur
16. return value2
17.
18.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
514/755
19. def list2list(liste: list) -> list:
20. # la nouvelle liste
21. newliste = []
22. # on exploite les éléments de la liste paramètre
23. for value in liste:
24. # on ajoute value à la nouvelle liste
25. newliste.append(check(value))
26. # on rend la nouvelle liste
27. return newliste
28.
29.
30. def ordereddict2dict(ordered_dictionary: OrderedDict) -> dict:
31. # OrderedDict -> dict de façon récursive
32. newdict = {}
33. for key, value in ordered_dictionary.items():
34. # on mémorise la valeur dans le nouveau dictionnaire
35. newdict[key] = check(value)
36. # on rend le dictionnaire
37. return newdict
• ligne 5 : on ne connaît pas le type de [value], aussi n’a-t-on pu écrire [value : type] ;
• lignes 7-8 : si [value] est de type [OrderedDict] alors on appelle de façon récursive la fonction [ordereddict2dict] qu’on
vient de commenter ;
• lignes 9-11 : un autre cas possible est que [value] soit une liste. Dans ce cas, ligne 11, on appelle la fonction [list2list] des
19-27 ;
• lignes 12-14 : le dernier cas est que [value] n’est pas une collection mais un type simple. La fonction [check], comme les
fonctions [ordereddict2dict] et [list2list] sont récursives. On sait qu’alors il faut toujours prévoir le cas où la récursion
s’arrête. Les lignes 12-14 sont ce cas ;
• ligne 16 : la fonction [check] appelée récursivement ou pas produit une valeur [valeur2] qui doit remplacer le paramètre
[value] de la ligne 5 ;
La méthode [list2list] définie aux lignes 19-27 exploite une liste passée en paramètre. Elle va l’explorer et remplacer toute valeur
de type [OrderedDict] trouvée dedans en un type [dict].
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
515/755
27 Exercice d’application : version 9
Nous revenons à la version 7 de l’exercice d’application et au lieu que le client et le serveur web s’échangent des chaînes jSON ils
vont ici s’échanger du XML.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
516/755
Le dossier [http-servers/04] est obtenue par recopie du dossier [http-servers/02] à l’exception du sous-dossier [utilities]. Puis
on change les éléments suivants :
1. # dépendances absolues
2. absolute_dependencies = [
3. # dossiers du projet
4. # BaseEntity, MyException
5. f"{root_dir}/classes/02/entities",
6. # InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
7. f"{root_dir}/impots/v04/interfaces",
8. # AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
9. f"{root_dir}/impots/v04/services",
10. # ImpotsDaoWithAdminDataInDatabase
11. f"{root_dir}/impots/v05/services",
12. # AdminData, ImpôtsError, TaxPayer
13. f"{root_dir}/impots/v04/entities",
14. # Constantes, tranches
15. f"{root_dir}/impots/v05/entities",
16. # index_controller
17. f"{root_dir}/impots/http-servers/01/controllers",
18. # scripts [config_database, config_layers]
19. script_dir,
20. # Logger, SendAdminMail
21. f"{root_dir}/impots/http-servers/02/utilities",
22. ]
• ligne 21 : on indique le dossier des utilitaires qui sont restés dans [http-servers/02] ;
1. # Home URL
2. @app.route('/', methods=['GET'])
3. @auth.login_required
4. def index():
5. logger = None
6. try:
7. # logger
8. logger = Logger(config["logsFilename"])
9. # on le mémorise dans une config associée au thread
10. thread_config = {"logger": logger}
11. thread_name = threading.current_thread().name
12. config[thread_name] = {"config": thread_config}
13. # on logue la requête
14. logger.write(f"[index] requête : {request}\n")
15. …
16. # on fait exécuter la requête par un contrôleur
17. résultat, status_code = index_controller.execute(request, config)
18. # y-a-t-il eu une erreur fatale ?
19. …
20. # on logue la réponse
21. logger.write(f"[index] {résultat}\n")
22. # on envoie la réponse
23. return xml_response(résultat, status_code)
24. except BaseException as erreur:
25. # on logue l'erreur si c'est possible
26. if logger:
27. logger.write(f"[index] {erreur}")
28. # on prépare la réponse au client
29. résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
517/755
30. # on envoie la réponse
31. return xml_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
32. finally:
33. # on ferme le fichier de logs s'il a été ouvert
34. if logger:
35. logger.close()
1. import xmltodict
2. …
3. def xml_response(résultat: dict, status_code: int) -> tuple:
4. # résultat : le dictionnaire à transformer en chaîne XML
5. xmlString = xmltodict.unparse(résultat)
6. # on rend la réponse HTTP
7. response = make_response(xmlString)
8. response.headers['Content-Type'] = 'application/xml; charset=utf-8'
9. return response, status_code
Puis le module [myutils] doit être incorporé dans les modules de portée machine. Cela se fait dans un terminal PyCharm avec la
commande [pip install .] (dans le dossier packages).
27.2.1 Le code
Le dossier [http-clients/04] est obtenu par recopie du client [http-clients/02]. Puis nous modifions la classe
[ImpôtsDaoWithHttpClient] de la façon suivante :
1. # imports
2.
3. import requests
4. import xmltodict
5. from flask_api import status
6.
7. from AbstractImpôtsDao import AbstractImpôtsDao
8. from AdminData import AdminData
9. from ImpôtsError import ImpôtsError
10. from InterfaceImpôtsMétier import InterfaceImpôtsMétier
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
518/755
11. from TaxPayer import TaxPayer
12.
13.
14. class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
15.
16. # constructeur
17. def __init__(self, config: dict):
18. …
19.
20. # méthode inutilisée
21. def get_admindata(self) -> AdminData:
22. pass
23.
24. # calcul de l'impôt
25. def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
26. ….
27. # code de statut de la réponse HTTP
28. status_code = response.status_code
29. # on met la réponse XML dans un dictionnaire
30. résultat = xmltodict.parse(response.text[39:])
31. # erreur si code de statut différent de 200 OK
32. ….
• ligne 30 : la réponse HTTP [response] du serveur web est désormais une chaîne XML. Les logs montrent la nature de celle-
ci :
La chaîne [<?xml version="1.0" encoding="utf-8"?>] compte 38 caractères. Par ailleurs, si on regarde le fichier de logs avec
un éditeur hexadécimal, on voit que derrière cette chaîne il y a un caractère de saut de ligne \n. Puis vient la réponse
<réponse>…</réponse>. La chaîne XML que nous devons convertir commence donc après les 39 premiers caractères de la
chaîne XML. Elle commence à partir du caractère n° 39, le 1 er caractère étant numéroté 0. Cette chaîne est obtenue par
l’expression [response.text[39:]].
Si nous exécutons le client (suivez la procédure des exemples précédents), nous obtenons les mêmes résultats dans le fichier
[résultats.json] que dans les versions précédentes. Les logs sont eux les suivants :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
519/755
1. 2020-07-27 16:32:04.983020, Thread-46 : [index] requête : <Request
'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=50000' [GET]>
2. 2020-07-27 16:32:04.983020, Thread-46 : [index] mis en pause du thread pendant 1 seconde(s)
3. 2020-07-27 16:32:04.984021, Thread-47 : [index] requête : <Request
'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=55555' [GET]>
4. 2020-07-27 16:32:04.984021, Thread-47 : [index] mis en pause du thread pendant 1 seconde(s)
5. …
6. 2020-07-27 16:32:07.001271, Thread-56 : [index] mis en pause du thread pendant 1 seconde(s)
7. 2020-07-27 16:32:07.003078, Thread-54 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 5,
'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
8. 2020-07-27 16:32:07.006078, Thread-55 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3,
'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
9. 2020-07-27 16:32:08.002824, Thread-56 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 2,
'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
• le logueur continue à écrire le dictionnaire de la réponse et non la chaîne XML qui est envoyée au client. Ce n’est pas une
erreur et c’est voulu ;
La classe de test [TestHttpClientDao] est la même que dans la |version 7| et donne les mêmes résultats.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
520/755
28 Exercice d’application : version 10
28.1 Introduction
Dans les exemples de clients du serveur de calcul de l’impôt, les threads envoyaient N requêtes séquentiellement si elles devaient
traiter N contribuables. L’idée ici est d’envoyer une seule requête encapsulant les N contribuables. Pour chacun d’entre-eux, il faut
envoyer les informations [marié, enfants, salaire]. On peut les envoyer comme paramètres :
Dans les deux cas, on peut utiliser une requête [GET] ou [POST]. Nous utiliserons une requête POST avec les paramètres encapsulés
dans le corps de la requête HTTP.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
521/755
28.2 Le serveur web
Le dossier [http-servers/05] est obtenu initialement par recopie du dossier [http-servers/02]. On revient aux échanges jSON
entre le client et le serveur. On a vu que passer du jSON au XML est très simple.
28.2.1 Configuration
La configuration [config, config_database, config_layers] reste analogue à celle des versions précédentes. Nous ne revenons pas
dessus.
1. # Home URL
2. @app.route('/', methods=['POST'])
3. @auth.login_required
4. def index():
5. …
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
522/755
29. # a-t-on une liste de dictionnaires ?
30. if not msg_erreur:
31. erreur = False
32. i = 0
33. while not erreur and i < len(list_dict_taxpayers):
34. erreur = not isinstance(list_dict_taxpayers[i], dict)
35. i += 1
36. # erreur ?
37. if erreur:
38. msg_erreur = "le corps du POST doit être une liste de dictionnaires"
39. # erreur ?
40. if msg_erreur:
41. # on envoie une réponse d'erreur au client
42. résultats = {"réponse": {"erreurs": [msg_erreur]}}
43. return résultats, status.HTTP_400_BAD_REQUEST
44.
45. # on vérifie les TaxPayers un par un
46. # au départ pas d'erreurs
47. list_erreurs = []
48. for dict_taxpayer in list_dict_taxpayers:
49. # on crée un TaxPayer à partir de dict_taxpayer
50. msg_erreur = None
51. try:
52. # l'opération suivante va éliminer les cas où les paramètres ne sont pas
53. # des propriétés de la classe TaxPayer ainsi que les cas où leurs valeurs
54. # sont incorrectes
55. TaxPayer().fromdict(dict_taxpayer)
56. except BaseException as erreur:
57. msg_erreur = f"{erreur}"
58. # certaines clés doivent être présentes dans le dictionnaire
59. if not msg_erreur:
60. # les clés [marié, enfants, salaire] doivent être présentes dans le dictionnaire
61. keys = dict_taxpayer.keys()
62. if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
63. msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
64. # des erreurs ?
65. if msg_erreur:
66. # on note l'erreur dans le TaxPayer lui-même
67. dict_taxpayer['erreur'] = msg_erreur
68. # on ajoute le TaxPayer à la liste des erreurs
69. list_erreurs.append(dict_taxpayer)
70.
71. # on a traité tous les taxpayers - y-a-t-il des erreurs ?
72. if list_erreurs:
73. # on envoie une réponse d'erreur au client
74. résultats = {"réponse": {"erreurs": list_erreurs}}
75. return résultats, status.HTTP_400_BAD_REQUEST
76.
77. # pas d'erreurs, on peut travailler
78. # récupération des données de l'administration fiscale
79. admindata = config["admindata"]
80. métier = config["layers"]["métier"]
81. try:
82. # on traite les TaxPayer un à un
83. list_taxpayers = []
84. for dict_taxpayer in list_dict_taxpayers:
85. # calcul de l'impôt
86. taxpayer = TaxPayer().fromdict(
87. {'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
88. 'salaire': dict_taxpayer['salaire']})
89. métier.calculate_tax(taxpayer, admindata)
90. # on mémorise le résultat en tant que dictionnaire
91. list_taxpayers.append(taxpayer.asdict())
92. # on envoie la réponse au client
93. return {"réponse": {"results": list_taxpayers}}, status.HTTP_200_OK
94. except ImpôtsError as erreur:
95. # on envoie une réponse d'erreur au client
96. return {"réponse": {"erreurs": f"[{erreur}]"}}, status.HTTP_500_INTERNAL_SERVER_ERROR
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
523/755
• ligne 18 : [request.data] permet de récupérer le corps (body) de la requête HTTP. On récupère ici du texte et nous savons
que ce texte est du jSON qui représente une liste de dictionnaire [marié, enfants, salaire] ;
• lignes 19-24 : on récupère cette liste de dictionnaires ;
• lignes 22-24 : si la récupération du jSON s’est mal passée, on note l’erreur ;
• lignes 26-28 : si on découvre que l’objet récupéré n’est pas une liste ou que c’est une liste vode, on note l’erreur ;
• lignes 29-38 : si on a bien récupéré une liste, on vérifie que c’est bien une liste de dictionnaires ;
• lignes 40-43 : s’il y a eu erreur, on s’arrête là et on envoie une réponse d’erreur au client ;
• lignes 45-69 : on vérifie maintenant chacun des dictionnaires :
o ils doivent contenir les clés [marié, enfants, salaire] ;
o ils doivent permettre de construire un objet [TaxPayer] valide ;
• lignes 65-69 : si une erreur a été détectée dans un dictionnaire, alors elle est mise dans ce même dictionnaire associée à la clé
‘erreur’ ;
• lignes 72-75 : les dictionnaires erronés ont été cumulés dans la liste [list_erreurs]. Si cette liste n’est pas vide, alors on
l’envoie dans une réponse d’erreur faite au client ;
• ligne 77 : arrivé là, on sait qu’on peut créer une liste d’objets de type [TaxPayer] à partir du corps de la requête envoyée par le
client ;
• lignes 84-91 : on exploite la liste des dictionnaires reçus ;
• ligne 86 : à partir d’un dictionnaire, on crée un objet [TaxPayer] ;
• ligne 89 : on calcule l’impôt de ce [TaxPayer] ;
• ligne 91 : on sait que [taxpayer] a été modifié par le calcul de l’impôt. On le transforme en dictionnaire et on l’ajoute à une
liste de résultats ;
• ligne 93 : on envoie cette liste de résultats au client ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
524/755
• en [7], on regarde les entêtes HTTP que va envoyer le client Postman au serveur ;
• en [8], on voit qu’il va lui envoyer un entête [Content-Type] lui indiquant que la requête contient un corps codé en jSON.
Cela vient du choix [5] fait précédemment ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
525/755
• en [3], on a reçu du jSON ;
• en [4], l’impôt des contribuables ;
Examinons dans la console Postman (Ctrl-Alt-C) le dialogue client / serveur qui a eu lieu :
1. POST / HTTP/1.1
2. Authorization: Basic YWRtaW46YWRtaW4=
3. Content-Type: application/json
4. User-Agent: PostmanRuntime/7.26.2
5. Accept: */*
6. Cache-Control: no-cache
7. Postman-Token: 03c4aa28-5a5d-4bb5-ac51-7ad51968c71d
8. Host: localhost:5000
9. Accept-Encoding: gzip, deflate, br
10. Connection: keep-alive
11. Content-Length: 824
12.
13. [
14. {
15. "marié": "oui",
16. "enfants": 2,
17. "salaire": 55555
18. },
19. {
20. "marié": "oui",
21. "enfants": 2,
22. "salaire": 50000
23. },
24. {
25. "marié": "oui",
26. "enfants": 3,
27. "salaire": 50000
28. },
29. {
30. "marié": "non",
31. "enfants": 2,
32. "salaire": 100000
33. },
34. {
35. "marié": "non",
36. "enfants": 3,
37. "salaire": 100000
38. },
39. {
40. "marié": "oui",
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
526/755
41. "enfants": 3,
42. "salaire": 100000
43. },
44. {
45. "marié": "oui",
46. "enfants": 5,
47. "salaire": 100000
48. },
49. {
50. "marié": "non",
51. "enfants": 0,
52. "salaire": 100000
53. },
54. {
55. "marié": "oui",
56. "enfants": 2,
57. "salaire": 30000
58. },
59. {
60. "marié": "non",
61. "enfants": 0,
62. "salaire": 200000
63. },
64. {
65. "marié": "oui",
66. "enfants": 3,
67. "salaire": 200000
68. }
69. ]
1. HTTP/1.0 200 OK
2. Content-Type: application/json; charset=utf-8
3. Content-Length: 1461
4. Server: Werkzeug/1.0.1 Python/3.8.1
5. Date: Tue, 28 Jul 2020 07:16:34 GMT
6.
7. {"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0,
"taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384,
"surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}, {"marié": "oui", "enfants": 3, "salaire":
50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}, {"marié": "non", "enfants":
2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0},
{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte":
0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180,
"taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230,
"surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 0, "salaire": 100000,
"impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2,
"salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}, {"marié": "non",
"enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction":
0}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41,
"décôte": 0, "réduction": 0}]}}
1. POST / HTTP/1.1
2. Authorization: Basic YWRtaW46YWRtaW4=
3. Content-Type: application/json
4. User-Agent: PostmanRuntime/7.26.2
5. Accept: */*
6. Cache-Control: no-cache
7. Postman-Token: 47652706-9744-46a0-a682-de010e5406c0
8. Host: localhost:5000
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
527/755
9. Accept-Encoding: gzip, deflate, br
10. Connection: keep-alive
11. Content-Length: 3
12.
13. abc
14.
15. HTTP/1.0 400 BAD REQUEST
16. Content-Type: application/json; charset=utf-8
17. Content-Length: 125
18. Server: Werkzeug/1.0.1 Python/3.8.1
19. Date: Tue, 28 Jul 2020 07:43:27 GMT
20.
21. {"réponse": {"erreurs": ["le corps du POST n'est pas une chaîne jSON valide : Expecting value: line 1
column 1 (char 0)"]}}
• ligne 13 : on a envoyé la chaîne [abc] qui n’est pas une chaîne jSON valide (ligne 3) ;
• ligne 15 : le serveur répond par un code d’erreur 400 ;
• ligne 21 : la réponse jSON du serveur ;
Cas 2 : envoyons une chaîne jSON valide qui ne soit pas une liste
1. POST / HTTP/1.1
2. Authorization: Basic YWRtaW46YWRtaW4=
3. Content-Type: application/json
4. User-Agent: PostmanRuntime/7.26.2
5. Accept: */*
6. Cache-Control: no-cache
7. Postman-Token: 03b64735-9239-47b3-b92d-be7c9ebc7559
8. Host: localhost:5000
9. Accept-Encoding: gzip, deflate, br
10. Connection: keep-alive
11. Content-Length: 17
12.
13. {"att1":"value1"}
14.
15. HTTP/1.0 400 BAD REQUEST
16. Content-Type: application/json; charset=utf-8
17. Content-Length: 97
18. Server: Werkzeug/1.0.1 Python/3.8.1
19. Date: Tue, 28 Jul 2020 07:50:11 GMT
20.
21. {"réponse": {"erreurs": ["le corps du POST n'est pas une liste ou alors cette liste est vide"]}}
Cas 3 : envoyons une chaîne jSON qui soit une liste dont les éléments ne sont pas tous des dictionnaires
1. POST / HTTP/1.1
2. Authorization: Basic YWRtaW46YWRtaW4=
3. Content-Type: application/json
4. User-Agent: PostmanRuntime/7.26.2
5. Accept: */*
6. Cache-Control: no-cache
7. Postman-Token: a1528a5f-777c-413f-b3be-7d4e9955b12a
8. Host: localhost:5000
9. Accept-Encoding: gzip, deflate, br
10. Connection: keep-alive
11. Content-Length: 7
12.
13. [0,1,2]
14.
15. HTTP/1.0 400 BAD REQUEST
16. Content-Type: application/json; charset=utf-8
17. Content-Length: 85
18. Server: Werkzeug/1.0.1 Python/3.8.1
19. Date: Tue, 28 Jul 2020 07:52:10 GMT
20.
21. {"réponse": {"erreurs": ["le corps du POST doit être une liste de dictionnaires"]}}
Cas 4 : envoyons une liste de dictionnaires avec un dictionnaire n’ayant pas les bonnes clés
1. POST / HTTP/1.1
2. Authorization: Basic YWRtaW46YWRtaW4=
3. Content-Type: application/json
4. User-Agent: PostmanRuntime/7.26.2
5. Accept: */*
6. Cache-Control: no-cache
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
528/755
7. Postman-Token: ba964d81-c9d9-46ff-a521-b4c4e5639484
8. Host: localhost:5000
9. Accept-Encoding: gzip, deflate, br
10. Connection: keep-alive
11. Content-Length: 19
12.
13. [{"att1":"value1"}]
14.
15. HTTP/1.0 400 BAD REQUEST
16. Content-Type: application/json; charset=utf-8
17. Content-Length: 112
18. Server: Werkzeug/1.0.1 Python/3.8.1
19. Date: Tue, 28 Jul 2020 07:54:33 GMT
20.
21. {"réponse": {"erreurs": [{"att1": "value1", "erreur": "MyException[2, la clé [att1] n'est pas
autorisée]"}]}}
Cas 5 : envoyons une liste de dictionnaires avec un dictionnaire avec des clés manquantes :
1. POST / HTTP/1.1
2. Authorization: Basic YWRtaW46YWRtaW4=
3. Content-Type: application/json
4. User-Agent: PostmanRuntime/7.26.2
5. Accept: */*
6. Cache-Control: no-cache
7. Postman-Token: 98aec51d-f37d-4c14-81cd-c7ffcbbcdc65
8. Host: localhost:5000
9. Accept-Encoding: gzip, deflate, br
10. Connection: keep-alive
11. Content-Length: 18
12.
13. [{"marié":"oui"}]
14.
15. HTTP/1.0 400 BAD REQUEST
16. Content-Type: application/json; charset=utf-8
17. Content-Length: 125
18. Server: Werkzeug/1.0.1 Python/3.8.1
19. Date: Tue, 28 Jul 2020 07:56:40 GMT
20.
21. {"réponse": {"erreurs": [{"marié": "oui", "erreur": "le dictionnaire doit inclure les clés [marié, enfants,
salaire]"}]}}
Cas 6 : envoyons une liste de dictionnaires avec un dictionnaire ayant les bonnes clés mais certaines ayant des valeurs erronées :
1. POST / HTTP/1.1
2. Authorization: Basic YWRtaW46YWRtaW4=
3. Content-Type: application/json
4. User-Agent: PostmanRuntime/7.26.2
5. Accept: */*
6. Cache-Control: no-cache
7. Postman-Token: 3083e601-dee4-4e15-9ea4-fc0328d0fcf0
8. Host: localhost:5000
9. Accept-Encoding: gzip, deflate, br
10. Connection: keep-alive
11. Content-Length: 46
12.
13. [{"marié":"x", "enfants":"x", "salaire":"x"}]
14.
15. HTTP/1.0 400 BAD REQUEST
16. Content-Type: application/json; charset=utf-8
17. Content-Length: 167
18. Server: Werkzeug/1.0.1 Python/3.8.1
19. Date: Tue, 28 Jul 2020 07:59:32 GMT
20.
21. {"réponse": {"erreurs": [{"marié": "x", "enfants": "x", "salaire": "x", "erreur": "MyException[31,
l'attribut marié [x] doit avoir l'une des valeurs oui / non]"}]}}
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
529/755
28.3 Le client web
Le dossier [http-clients/05] (version 10) est obtenu initialement par recopie du dossier [http-clients/02] (version 7). Il est ensuite
modifié.
1. # imports
2.
3. import requests
4. from flask_api import status
5.
6. from AbstractImpôtsDao import AbstractImpôtsDao
7. from AdminData import AdminData
8. from ImpôtsError import ImpôtsError
9. from InterfaceImpôtsMétier import InterfaceImpôtsMétier
10. from TaxPayer import TaxPayer
11.
12.
13. class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
14.
15. # constructeur
16. def __init__(self, config: dict):
17. …
18.
19. # méthode inutilisée
20. def get_admindata(self) -> AdminData:
21. pass
22.
23. # calcul de l'impôt
24. def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
25. …
26.
27. # calcul de l'impôt en mode bulk
28. def calculate_tax_in_bulk_mode(self, taxpayers: list) -> list:
29. # on laisse remonter les exceptions
30.
31. # on transforme les taxpayers en liste de dictionnaires
32. # on ne garde que les propriétés [marié, enfants, salaire]
33. list_dict_taxpayers = list(
34. map(lambda taxpayer:
35. taxpayer.asdict(included_keys=[
36. '_TaxPayer__marié',
37. '_TaxPayer__enfants',
38. '_TaxPayer__salaire']),
39. taxpayers))
40.
41. # connexion au serveur
42. config_server = self.__config_server
43. if config_server['authBasic']:
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
530/755
44. response = requests.post(config_server['urlServer'], json=list_dict_taxpayers,
45. auth=(config_server["user"]["login"],
46. config_server["user"]["password"]))
47. else:
48. response = requests.post(config_server['urlServer'], json=list_dict_taxpayers)
49. # mode debug ?
50. if self.__debug:
51. # logueur
52. if not self.__logger:
53. self.__logger = self.__config['logger']
54. # on logue
55. self.__logger.write(f"{response.text}\n")
56. # code de statut de la réponse HTTP
57. status_code = response.status_code
58. # on met la réponse jSON dans un dictionnaire
59. résultat = response.json()
60. # erreur si code de statut différent de 200 OK
61. if status_code != status.HTTP_200_OK:
62. # on sait que les erreurs ont été associées à la clé [erreurs] de la réponse
63. raise ImpôtsError(93, résultat['réponse']['erreurs'])
64. # on sait que le résultat a été associé à la clé [results] de la réponse
65. list_dict_taxpayers2 = résultat['réponse']['results']
66. # on met à jour la liste initiale des taxpayers avec les résultats reçus
67. for i in range(len(taxpayers)):
68. # mise à jour de taxpayers[i]
69. taxpayers[i].fromdict(list_dict_taxpayers2[i])
70. # ici le paramètre [taxpayers] a été mis à jour avec les résultats du serveur
• lignes 1-26 : le code reste ce qu’il était dans la version 7 et dans d’autres versions ;
• lignes 27-70 : on introduit une nouvelle méthode [calculate_tax_in_bulk_mode] dont le rôle est de calculer l’impôt d’une
liste de contribuables ;
• ligne 28 : [taxpayers] est cette liste de contribuables ;
• lignes 31-39 : on passe d’une liste d’objets de type [TaxPayer] à une liste de dictionnaires grâce à une fonction [map] ;
• lignes 34-38 : la fonction lambda utilisée transforme un objet de type [TaxPayer] en un dictionnaire de type [dict] ayant les
seules clés [marié, enfants, salaire]. On utilise pour cela le paramètre nommé [included_keys] de la méthode
[BaseEntity.asdict]. On rappelle que pour connaître les noms exacts des propriétés à mettre dans les paramètres
[excluded_keys, included_keys], il faut utiliser le dictionnaire prédéfini [taxpayer.__dict__] ;
• lignes 41-48 : connexion au serveur puis obtention de sa réponse HTTP ;
• lignes 44, 48 :
o on utilise la méthode statique [requests.post] pour faire un POST vers le serveur ;
o on utilise le paramètre nommé [json] pour indiquer que le corps du POST est une chaîne jSON. Cela va avoir deux
conséquences :
▪ l’objet affecté au paramètre nommé [json], ici une liste de dictionnaires, va être transformé en chaîne jSON ;
▪ l’entête
Content-Type: application/json
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
531/755
12. logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")
• lignes 9-10 : alors que précédemment on avait une boucle qui passait successivement chacun des contribuables à la méthode
[dao.calculate_tax], ici on ne fait qu’un unique appel à la méthode [dao.calculate_tax_in_bulk_mode] à laquelle on passe
tous les contribuables ;
Tout d’abord la version 6. Pour comparer les deux versions, on met la propriété [sleep_time] du serveur à zéro pour qu’il n’y ait pas
d’attente forcée des threads. Les logs du client sont les suivants :
La durée de l’exécution du client pour calculer l’impôt de 11 contribuables est donc [913065-811347= 101718], ç-à-d environ 102
millisecondes.
Faisons la même chose avec la version 10 (sleep_time du serveur à zéro). Les logs du client sont alors les suivants :
La durée de l’exécution du client pour calculer l’impôt de 11 contribuables est donc [935958-871428= 64530 ns] (ligne 8 – ligne 1),
ç-à-d environ 65 millisecondes. Cette nouvelle version 10 amène ainsi un gain de 57 % environ sur la version 7.
1. import unittest
2.
3. from Logger import Logger
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
532/755
4.
5.
6. class TestHttpClientDao(unittest.TestCase):
7.
8. def test_1(self) -> None:
9. from TaxPayer import TaxPayer
10.
11. # {'marié': 'oui', 'enfants': 2, 'salaire': 55555,
12. # 'impôt': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
13. taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
14. dao.calculate_tax_in_bulk_mode([taxpayer])
15. # vérification
16. self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
17. self.assertEqual(taxpayer.décôte, 0)
18. self.assertEqual(taxpayer.réduction, 0)
19. self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
20. self.assertEqual(taxpayer.surcôte, 0)
21.
22. …
23.
24. if __name__ == '__main__':
25. # on configure l'application
26. import config
27. config = config.configure({})
28.
29. # logger
30. logger = Logger(config["logsFilename"])
31. # on le mémorise dans la config
32. config["logger"] = logger
33. # on récupère la couche [dao]
34. dao = config["layers"]["dao"]
35.
36. # on exécute les méthodes de test
37. print("tests en cours...")
38. unittest.main()
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
533/755
29 Exercice d’application : version 11
29.1 Introduction
Dans les versions précédentes de l’application client / serveur de calcul de l’impôt, la couche [métier] qui implémente les règles
métier de ce calcul était côté serveur. On se propose maintenant de la déplacer côté client. Quel est l’intérêt ? Du travail que faisait le
serveur va être déplacé côté client. Si on pense à la situation d’un serveur interrogé par N clients, N calculs métier de l’impôt seront
faits par les clients. Dans les versions précédentes, le serveur faisait ces N calculs métier. Parce qu’il ne fait plus le calcul métier, le
serveur va répondre plus rapidement à ses clients et va alors pouvoir en servir davantage simultanément.
Le client web aura deux façon de calculer l’imppôt de la liste de contribuables trouvée en [3] :
• utiliser la méthode de la version précédente. Il utilise la couche [métier] [10] du serveur. Le script [main] utilisera cette
méthode ;
• se contenter de demander au serveur les données de l’administration fiscale [2-4] puis utiliser la couche [métier] [12] locale
au client ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
534/755
29.2 Le serveur web
L’arborescence du serveur web sera la suivante :
• le dossier [http-servers/06] est initialement obtenu par recopie du dossier [http-servers/05]. On va en effet conserver
l’acquis de la version 10 précédente. On va simplement lui ajouter une nouvelle fonctionnalité. Celle-ci est matérialisée par la
présence d’un nouveau contrôleur [get_admindata_controller] [1]. L’autre contrôleur [calculate_tax_controller] n’est
autre que l’ancien contrôleur [index_controller] qui a été renommé ;
29.3 Configuration
Le serveur offrira deux URL de service :
• [/calculate-tax] pour calculer l’impôt d’une liste de contribuables passée dans le corps d’un POST. Elle correspond donc à
l’URL [/] de la version 10 précédente ;
• [/get-admindata] délivre la chaîne jSON des données de l’administration fiscale ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
535/755
18.
19. # récupération des données de l'administration fiscale
20. erreur = False
21. try:
22. # admindata sera une donnée de portée application en lecture seule
23. config["admindata"] = config["layers"]["dao"].get_admindata()
24. # log de réussite
25. logger.write("[serveur] connexion à la base de données réussie\n")
26. except ImpôtsError as ex:
27. # on note l'erreur
28. …
29. …
30. # s'il y a eu erreur on s'arrête
31. if erreur:
32. sys.exit(2)
33.
34. # l'application Flask peut démarrer
35. app = Flask(__name__)
36.
37. # contrôleur principal
38. def main_controller() -> tuple:
39. # on récupère l'action demandée
40. dummy, action=request.path.split('/')
41. logger = None
42. try:
43. # logger
44. logger = Logger(config["logsFilename"])
45. # on le mémorise dans une config associée au thread
46. thread_config = {"logger": logger}
47. thread_name = threading.current_thread().name
48. config[thread_name] = {"config": thread_config}
49. # on logue la requête
50. logger.write(f"[index] requête : {request}\n")
51. # on interrompt le thread si cela a été demandé
52. sleep_time = config["sleep_time"]
53. if sleep_time != 0:
54. # la pause est aléatoire pour que certains threads soient interrompus et d'autres pas
55. aléa = randint(0, 1)
56. if aléa == 1:
57. # log avant pause
58. logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
59. # pause
60. time.sleep(sleep_time)
61. # on fait exécuter la requête par le contrôleur de la requête
62. controller = config['controllers'][action]
63. résultat, status_code = controller.execute(request, config)
64. # y-a-t-il eu une erreur fatale ?
65. if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
66. # on envoie un mail à l'administrateur de l'application
67. config_mail = config["adminMail"]
68. config_mail["logger"] = logger
69. SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
70. # on logue la réponse
71. logger.write(f"[index] {résultat}\n")
72. # on envoie la réponse
73. print(résultat)
74. return json_response(résultat, status_code)
75. except BaseException as erreur:
76. # on logue l'erreur si c'est possible
77. if logger:
78. logger.write(f"[index] {erreur}")
79. # on prépare la réponse au client
80. résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
81. # on envoie la réponse
82. return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
83. finally:
84. # on ferme le fichier de logs s'il a été ouvert
85. if logger:
86. logger.close()
87.
88. # calcul de l'impôt
89. @app.route('/calculate-tax', methods=['POST'])
90. @auth.login_required
91. def calculate_tax():
92. # on passe la main au contrôleur principal
93. return main_controller()
94.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
536/755
95. # obtenir les données de l'administration fiscale
96. @app.route('/get-admindata', methods=['GET'])
97. @auth.login_required
98. def get_admindata():
99. # on passe la main au contrôleur principal
100. return main_controller()
101.
102. # main uniquement
103. if __name__ == '__main__':
104. # on lance le serveur
105. app.config.update(ENV="development", DEBUG=True)
106. app.run(threaded=True)
• l’URL [/get-admindata] doit rendre la chaîne jSON des données de l’administration fiscale ;
• ligne 6 : celles-ci ont été récupérées par le script principal [main] et mises dans le dictionnaire [config] sous la forme d’un
objet [AdminData]. On rend le dictionnaire de cet objet ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
537/755
Dans la console Postman, le dialogue client / serveur est le suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
538/755
Le dialogue client / serveur dans la console Postman est le suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
539/755
Le dossier [http-clients/06] est initialement obtenu par recopie du dossier [http-clients/05]. Le travail de modification consiste
essentiellement à :
• modifier la configuration [config_layers] pour qu’elle intégre désormais une couche [métier]. Auparavant elle n’avait qu’une
couche [dao] ;
• ajouter une nouvelle méthode à la couche [dao] ;
• écrire un script [main2] qui s’appuiera sur la couche [métier] du client pour calculer l’impôt des contribuables ;
• dans la configuration [config] qui doit inclure dans les dépendances du client le dossier contenant l’implémentation de la
couche [métier]. Ce dossier était déjà inclus dans les dépendances :
1. absolute_dependencies = [
2. # dossiers du projet
3. # BaseEntity, MyException
4. f"{root_dir}/classes/02/entities",
5. # InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
6. f"{root_dir}/impots/v04/interfaces",
7. # AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
8. f"{root_dir}/impots/v04/services",
9. # ImpotsDaoWithAdminDataInDatabase
10. f"{root_dir}/impots/v05/services",
11. # AdminData, ImpôtsError, TaxPayer
12. f"{root_dir}/impots/v04/entities",
13. # Constantes, tranches
14. f"{root_dir}/impots/v05/entities",
15. # ImpôtsDaoWithHttpClient
16. f"{script_dir}/../services",
17. # scripts de configuration
18. script_dir,
19. # Logger
20. f"{root_dir}/impots/http-servers/02/utilities",
21. ]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
540/755
12. # on rend la configuration des couches
13. return {
14. "dao": dao,
15. "métier": métier
16. }
• ligne 5 : l’interface [InterfaceImpôtsDaoWithHttpClient] hérite de la classe abstraite [AbstractImpôtsDao] qui gère l’accès au
système de fichiers du client. On rappelle qu’elle a une méthode abstraite [get_admindata] ;
• lignes 7-10 : la méthode [calculate_tax_in_bulk_mode] que nous avons définie dans la version précédente permet le calcul
de l’impôt d’une liste de contribuables ;
1. # imports
2.
3. import json
4.
5. import requests
6. from flask_api import status
7.
8. from AbstractImpôtsDao import AbstractImpôtsDao
9. from AdminData import AdminData
10. from ImpôtsError import ImpôtsError
11. from InterfaceImpôtsDaoWithHttpClient import InterfaceImpôtsDaoWithHttpClient
12.
13. class ImpôtsDaoWithHttpClient(InterfaceImpôtsDaoWithHttpClient):
14.
15. # constructeur
16. def __init__(self, config: dict):
17. # initialisation parent
18. AbstractImpôtsDao.__init__(self, config)
19. # mémorisation éléments de la configuration
20. # config générale
21. self.__config = config
22. # serveur
23. self.__config_server = config["server"]
24. # mode debug
25. self.__debug = config["debug"]
26. # logger
27. self.__logger = None
28.
29. def get_admindata(self) -> AdminData:
30. # on laisse remonter les exceptions
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
541/755
31.
32. # url de service
33. config_server = self.__config_server
34. config_services = config_server['url_services']
35. url_service = f"{config_server['urlServer']}{config_services['get-admindata']}"
36.
37. # connexion
38. if config_server['authBasic']:
39. response = requests.get(url_service,
40. auth=(
41. config_server["user"]["login"],
42. config_server["user"]["password"]))
43. else:
44. response = requests.get(url_service)
45.
46. # mode debug ?
47. if self.__debug:
48. # logueur
49. if not self.__logger:
50. self.__logger = self.__config['logger']
51. # on logue
52. self.__logger.write(f"{response.text}\n")
53.
54. # code de statut
55. status_code = response.status_code
56. # résultat sous forme d'un dictionnaire
57. résultat = json.loads(response.text)
58. # si code de statut différent de 200 OK
59. if status_code != status.HTTP_200_OK:
60. raise ImpôtsError(58, résultat['réponse']['erreurs'])
61. # on rend le résultat (un dictionnaire)
62. return AdminData().fromdict(résultat["réponse"]["result"])
63.
64. # calcul de l'impôt en mode bulk
65. def calculate_tax_in_bulk_mode(self, taxpayers: list):
66. # on laisse remonter les exceptions
67.
68. …
Le script [main] est celui de la version précédente. Il utilise la méthode [calculate_tax_in_bulk_mode] de la couche [dao] et utilise
donc la couche [métier] du serveur ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
542/755
Le script [main2] fait la même chose que le script [main] mais en utilisant la couche [métier] du client :
1. # on configure l'application
2.
3. import config
4. config = config.configure({})
5.
6. # dépendances
7. from ImpôtsError import ImpôtsError
8. from Logger import Logger
9.
10. logger = None
11. # code
12. try:
13. # logger
14. logger = Logger(config["logsFilename"])
15. # on le mémorise dans la config
16. config["logger"] = logger
17. # log de début
18. logger.write("début du calcul de l'impôt des contribuables\n")
19. # on récupère la couche [dao]
20. dao = config["layers"]["dao"]
21. # on récupère les contribuables
22. taxpayers = dao.get_taxpayers_data()["taxpayers"]
23. # des contribuables ?
24. if not taxpayers:
25. raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
26. # on récupère les données de l'administration fiscale
27. admindata = dao.get_admindata()
28. # calcul de l'impôt des contribuables par la couche [métier]
29. métier = config['layers']['métier']
30. for taxpayer in taxpayers:
31. métier.calculate_tax(taxpayer, admindata)
32. # on enregistre les résultats dans le fichier jSON
33. dao.write_taxpayers_results(taxpayers)
34. # except BaseException as erreur:
35. # # affichage de l'erreur
36. # print(f"L'erreur suivante s'est produite : {erreur}")
37. finally:
38. # on ferme le logueur
39. if logger:
40. # log de fin
41. logger.write("fin du calcul de l'impôt des contribuables\n")
42. # fermeture du logueur
43. logger.close()
44. # on a fini
45. print("Travail terminé...")
Les calculs métier auront la même durée que ce soit sur le serveur ou le client. La différence va alors se faire sur les requêtes. On peut
s’attendre alors que la durée d’exécution de [main] soit légèrement supérieure à celle de [main2].
On lance le serveur de la version 11, le SGBD et le serveur de mails [hMailServer]. Côté serveur, on met le paramètre [sleep_time]
à zéro pour que les deux tests soient exécutés dans les mêmes conditions.
Exécution 1 [main]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
543/755
L’exécution de [main] donne les logs suivants :
La durée de l’exécution a été de [051214-016079] nanosecondes (ligne 17 – ligne 1), ç-à-d 35 millisecondes et 135 nanosecondes.
On voit qu’entre la 1ère demande faite au serveur et la dernière réponse reçue par le client, il y a la même durée [051214-016079] (ligne
15 – ligne 1), 35 millisecondes et 135 nanosecondes.
Exécution 2 [main2]
La durée de l’exécution a été de [349975-303520] nanosecondes (ligne 3 - ligne 1), ç-à-d 46 millisecondes et 455 nanosecondes. De
façon tout à fait inattendue [main] est plus rapide que [main2].
On voit que l’unique requête de [main2] a duré [345084-303520] (ligne 2 – ligne 1), ç-à-d 41 millisecondes et 564 nanosecondes. Le
calcul de l’impôt a ensuite duré [349975-345084] (ligne 3 – ligne 2) ç-à-d 4 millisecondes et 91 nanosecondes. C’est la requête HTTP
qui fait la durée d’exécution. De façon surprenante, on voit ici que l’unique requête de [main2] a duré plus longtemps [41
millisecondes] que les quatre requêtes simultanées de [main] [35 millisecondes].
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
544/755
8. 2020-07-29 14:35:50.044307, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax'
[POST]>
9. 2020-07-29 14:35:50.045796, Thread-2 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 3,
'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'taux': 0.3, 'décôte': 0, 'réduction': 0}, {'marié':
'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0,
'réduction': 0}]}}
10. 2020-07-29 14:35:50.045796, Thread-3 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 2,
'salaire': 55555, 'impôt': 2814, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}]}}
11. 2020-07-29 14:35:50.046825, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax'
[POST]>
12. 2020-07-29 14:35:50.046825, Thread-6 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 2,
'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'taux': 0.14, 'décôte': 384, 'réduction': 347}, {'marié':
'oui', 'enfants': 3, 'salaire': 50000, 'impôt': 0, 'surcôte': 0, 'taux': 0.14, 'décôte': 720, 'réduction':
0}, {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41,
'décôte': 0, 'réduction': 0}, {'marié': 'non', 'enfants': 3, 'salaire': 100000, 'impôt': 16782, 'surcôte':
7176, 'taux': 0.41, 'décôte': 0, 'réduction': 0}]}}
13. 2020-07-29 14:35:50.046825, Thread-4 : [index] {'réponse': {'results': [{'marié': 'non', 'enfants': 0,
'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'taux': 0.45, 'décôte': 0, 'réduction': 0}, {'marié':
'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0,
'réduction': 0}]}}
14. 2020-07-29 14:35:50.046825, Thread-5 : [index] {'réponse': {'results': [{'marié': 'non', 'enfants': 0,
'salaire': 100000, 'impôt': 22986, 'surcôte': 0, 'taux': 0.41, 'décôte': 0, 'réduction': 0}, {'marié':
'oui', 'enfants': 2, 'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'taux': 0.0, 'décôte': 0, 'réduction':
0}]}}
15. 2020-07-29 14:41:03.341582, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/get-admindata'
[GET]>
16. 2020-07-29 14:41:03.341582, Thread-7 : [index] {'réponse': {'result': {'limites': [9964.0, 27519.0,
73779.0, 156244.0, 13500.0], 'coeffr': [0.0, 0.14, 0.3, 0.41, 0.45], 'coeffn': [0.0, 1394.96, 5798.0,
13913.7, 20163.4], 'plafond_decote_couple': 1970.0, 'valeur_reduc_demi_part': 3797.0,
'plafond_revenus_celibataire_pour_reduction': 21037.0, 'plafond_qf_demi_part': 1551.0,
'abattement_dixpourcent_max': 12502.0, 'plafond_impot_celibataire_pour_decote': 1595.0,
'plafond_decote_celibataire': 1196.0, 'plafond_revenus_couple_pour_reduction': 42074.0, 'id': 1,
'abattement_dixpourcent_min': 437.0, 'plafond_impot_couple_pour_decote': 2627.0}}}
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
545/755
30 Exercice d’application : version 12
Nous allons dans ce chapitre écrire une application web respectant l’architecture MVC (Modèle-Vue-Contrôleur). L’application pourra
délivrer ses réponses dans trois formats : jSON, XML, HTML. Il y a un saut de complexité entre ce que nous allons faire maintenant
et ce qui a été fait précédemment. Nous allons réutiliser la plupart des concepts vus jusqu’à maintenant et allons détailler toutes les
étapes menant à l’application finale.
• 1 - demande
Les URL demandées seront de la forme http://machine:port/action/param1/param2/… Le [Contrôleur principal] utilisera
un fichier de configuration pour " router " la demande vers le bon contrôleur. Pour cela, il utilisera le champ [action] de
l'URL. Le reste de l'URL [param1/param2/…] est formé de paramètres facultatifs qui seront transmis à l'action. Le C de
MVC est ici la chaîne [Contrôleur principal, Contrôleur / Action]. Si aucun contrôleur ne peut traiter l'action
demandée, le serveur web répondra que l'URL demandée n'a pas été trouvée.
• 2 - traitement
o l'action choisie [2a] peut exploiter les paramètres parami que le [Contrôleur principal] lui a transmis. Ceux-ci pourront
provenir de deux sources :
▪ du chemin [/param1/param2/…] de l'URL,
▪ de paramètres postés dans le corps de la requête du client ;
o dans le traitement de la demande de l'utilisateur, l'action peut avoir besoin de la couche [métier] [2b]. Une fois la
demande du client traitée, celle-ci peut appeler diverses réponses. Un exemple classique est :
▪ une réponse d'erreur si la demande n'a pu être traitée correctement ;
▪ une réponse de confirmation sinon ;
o le [Contrôleur / Action] rendra sa réponse [2c] au contrôleur principal ainsi qu’un code d’état. Ces codes d’état
représenteront de façon unique l’état dans lequel se trouve l’application. Ce sera soit un code de réussite, soit un code
d’erreur ;
• 3 - réponse
o selon que le client a demandé une réponse jSON, XML ou HTML, le [Contrôleur principal] instanciera [3a] le type
de réponse appropriée et demandera à celle-ci d’envoyer la réponse au client. Le [Contrôleur principal] lui transmettra
et la réponse et le code d’état fournis par le [Contrôleur / Action] qui a été exécuté ;
o si la réponse souhaitée est de type jSON ou XML, la réponse sélectionnée mettra en forme la réponse du [Contrôleur
/ Action] qu’on lui a donnée et l’enverra [3c]. Le client capable d’exploiter cette réponse peut être un script console
Python ou un script Javascript logé dans une page HTML ;
o si la réponse souhaitée est de type HTML, la réponse sélectionnée sélectionnera [3b] une des vues HTML [Vuei] à l’aide
du code d’état qu’on lui a donné. C’est le V de MVC. A un code d’état correspond une unique vue. Cette vue V va afficher
la réponse du [Contrôleur / Action] qui a été exécuté. Elle habille avec du HTML, CSS, Javascript les données de cette
réponse. On appelle ces données le modèle de la vue. C'est le M de MVC. Le client est alors le plus souvent un
navigateur ;
Maintenant, précisons le lien entre architecture web MVC et architecture en couches. Selon la définition qu'on donne au modèle, ces
deux concepts sont liés ou non. Prenons une application web MVC à une couche :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
546/755
Ci-dessus, les [Contrôleur / Action] intègrent chacun une partie des couches [métier] et [dao]. Dans la couche [web] on a bien
une architecture MVC mais l’ensemble de l’application n’a pas une architecture en couches. Ici il n’y a qu’une couche, la couche web,
qui fait tout.
La couche [web] peut être implémentée sans suivre le modèle MVC. On a bien alors une architecture multicouche mais la couche
web n'implémente pas le modèle MVC.
Par exemple, dans le monde .NET la couche [web] ci-dessus peut être implémentée avec ASP.NET MVC et on a alors une architecture
en couches avec une couche [web] de type MVC. Ceci fait, on peut remplacer cette couche ASP.NET MVC par une couche ASP.NET
classique (WebForms) tout en gardant le reste (métier, DAO, Pilote) à l'identique. On a alors une architecture en couches avec une
couche [web] qui n'est plus de type MVC.
Dans MVC, nous avons dit que le modèle M était celui de la vue V, c.a.d. l'ensemble des données affichées par la vue V. Une autre
définition du modèle M de MVC est donnée :
Beaucoup d'auteurs considèrent que ce qui est à droite de la couche [web] forme le modèle M du MVC. Pour éviter les ambigüités
on peut parler :
• du modèle du domaine lorsqu'on désigne tout ce qui est à droite de la couche [web] ;
• du modèle de la vue lorsqu'on désigne les données affichées par une vue V ;
Dans ce qui suit, lorsque nous parlerons de modèle, il s’agita toujours du modèle de la vue.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
547/755
30.2 Architecture de l’application client / serveur
L’application web aura l’architecture suivante :
• nous allons développer la version jSON du serveur. Nous testerons les URL de service du serveur les unes après les autres
avec un client Postman. Cette méthode nous permet de construire l’ossature du seveur web sans nous préoccuper des vues
(=HTML) de l’application ;
• après avoir testé le serveur jSON avec Postman, nous le testerons avec un client console ;
• puis nous passerons à la version XML du serveur. Nous avons vu que le passage du jSON au XML était trivial ;
• enfin nous passerons à la version HTML du serveur. Nous construirons une architecture MVC et définirons les vues à afficher.
L’application HTML sera testée à la fois avec le client Postman et un navigateur classique ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
548/755
30.3 L’arborescence du code du serveur
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
549/755
• en [6], les éléments statiques de l’application HTML ;
• en [7], les templates de l’application HTML décomposés en vues [9] et en fragments de vue [8] ;
• en [9], les classes implémentant les modèles des vues ;
• à partir des vues de l’application HTML, nous allons définir les actions que doit implémenter l’application web. Nous allons
ici utiliser les vues réelles mais ce pourrait être simplement des vues sur papier ;
• à partir de ces actions, nous allons définir les URL de service de l’application HTML ;
• nous allons implémenter ces URL de service avec un serveur délivrant du jSON. Cela permet de définir l’ossature du serveur
web sans se préoccuper des pages HTML à délivrer. Nous testerons ces URL de service avec Postman ;
• nous testerons ensuite notre serveur jSON avec un client console ;
• une fois que le serveur jSON aura été validé, nous passerons à l’ériture de l’application HTML ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
550/755
• en [1], l’action [authentifier-utilisateur] qui a amené à cette vue ;
• en [2], le clic sur le bouton [Valider] déclenche l’exécution de l’action [calculer-impot] avec trois paramètres postés [2-
5] ;
• le clic sur le lien [6] déclenche l’action [lister-simulations] sans paramètres ;
• le clic sur le lien [7] déclenche l’action [fin-session] sans paramètres ;
La 3ième vue est celle des simulations faites par l’utilisateur authentifié :
Avec ces premières informations, nous pouvons définir les différentes URL de service du serveur :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
551/755
Action Rôle Contexte d’exécution
/init-session Sert à fixer le type (json, xml, html) des réponses Requête GET
souhaitées Peut être émise à tout moment
/authentifier-utilisateur Autorise ou non un utilisateur à se connecter Requête POST.
La requête doit avoir deux paramètres postés [user, password]
Ne peut être émise que si le type de la session (json, xml, html) est connu
/calculer-impot Fait une simulation de calcul d’impôt Requête POST.
La requête doit avoir trois paramètres postés [marié, enfants,
salaire]
Ne peut être émise que si le type de la session (json, xml, html) est connu
et l’utilisateur authentifié
/lister-simulations Demande à voir la liste des simulations opérées Requête GET.
depuis le début de la session Ne peut être émise que si le type de la session (json, xml, html) est connu
et l’utilisateur authentifié
/supprimer-simulation/numéro Supprime une simulation de la liste des simulations Requête GET.
Ne peut être émise que si le type de la session (json, xml, html) est connu
et l’utilisateur authentifié
/afficher-calcul-impot Affiche la page HTML du calul de l’impôt Requête GET.
Ne peut être émise que si le type de la session (json, xml, html) est connu
et l’utilisateur authentifié
/fin-session Termine la session de simulations. Techniquement l’ancienne session web est supprimée et une nouvelle
session est créée
Ne peut être émise que si le type de la session (json, xml, html) est connu
et l’utilisateur authentifié
Ces différentes URL de service seront utilisées aussi bien pour le serveur HTML que pour les serveurs jSON ou XML. Deux URL
ne seront utilisées que pour ces deux derniers serveurs : ce sont les URL de la version précédente du client / serveur web que nous
reprenons ici :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
552/755
Nous allons maintenant passer en revue les différents contrôleurs ou ce qui revient au même les différentes actions que ces contrôleurs
traitent et qui rythment la vie de l’application web.
La configuration de la base de données [config_database] ainsi que celle des couches du serveur [config_layers] sont identiques à
celles des versions précédentes. Le fichier [config] voit apparaître de nouvelles informations :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
553/755
53. from ListerSimulationsController import ListerSimulationsController
54. from MainController import MainController
55. from SupprimerSimulationController import SupprimerSimulationController
56.
57. # les réponses HTTP
58. from HtmlResponse import HtmlResponse
59. from JsonResponse import JsonResponse
60. from XmlResponse import XmlResponse
61.
62. # les modèles des vues
63. from ModelForAuthentificationView import ModelForAuthentificationView
64. from ModelForCalculImpotView import ModelForCalculImpotView
65. from ModelForErreursView import ModelForErreursView
66. from ModelForListeSimulationsView import ModelForListeSimulationsView
67.
68. # étape 2 ------
69. # configuration de l'application
70. config.update({
71. # utilisateurs autorisés à utiliser l'application
72. "users": [
73. {
74. "login": "admin",
75. "password": "admin"
76. }
77. ],
78.
79. # fichier de logs
80. "logsFilename": f"{script_dir}/../data/logs/logs.txt",
81.
82. # config serveur SMTP
83. "adminMail": {
84. # serveur SMTP
85. "smtp-server": "localhost",
86. # port du serveur SMTP
87. "smtp-port": "25",
88. # administrateur
89. "from": "guest@localhost.com",
90. "to": "guest@localhost.com",
91. # sujet du mail
92. "subject": "plantage du serveur de calcul d'impôts",
93. # tls à True si le serveur SMTP requiert une autorisation, à False sinon
94. "tls": False
95. },
96.
97. # durée pause thread en secondes
98. "sleep_time": 0,
99.
100. # actions autorisées et leurs contrôleurs
101. "controllers": {
102. # initialisation d'une session de calcul
103. "init-session": InitSessionController(),
104. # authentification d'un utilisateur
105. "authentifier-utilisateur": AuthentifierUtilisateurController(),
106. # calcul de l'impôt en mode individuel
107. "calculer-impot": CalculerImpotController(),
108. # calcul de l'impôt en mode lots
109. "calculer-impots": CalculerImpotsController(),
110. # liste des simulations
111. "lister-simulations": ListerSimulationsController(),
112. # suppression d'une simulation
113. "supprimer-simulation": SupprimerSimulationController(),
114. # fin de la session de calcul
115. "fin-session": FinSessionController(),
116. # affichage de la vue de calcul de l'impôt
117. "afficher-calcul-impot": AfficherCalculImpotController(),
118. # obtention des données de l'administration fiscale
119. "get-admindata": GetAdminDataController(),
120. # main controller
121. "main-controller": MainController()
122. },
123.
124. # les différents types de réponse (json, xml, html)
125. "responses": {
126. "json": JsonResponse(),
127. "html": HtmlResponse(),
128. "xml": XmlResponse()
129. },
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
554/755
130.
131. # les vues HTML et leurs modèles dépendent de l'état rendu par le contrôleur
132. "views": [
133. {
134. # vue d'authentification
135. "états": [
136. # /init-session réussite
137. 700,
138. # /authentifier-utilisateur échec
139. 201
140. ],
141. "view_name": "views/vue-authentification.html",
142. "model_for_view": ModelForAuthentificationView()
143. },
144. {
145. # vue du calcul de l'impôt
146. "états": [
147. # /authentifier-utilisateur réussite
148. 200,
149. # /calculer-impot réussite
150. 300,
151. # /calculer-impot échec
152. 301,
153. # /afficher-calcul-impot
154. 800
155. ],
156. "view_name": "views/vue-calcul-impot.html",
157. "model_for_view": ModelForCalculImpotView()
158. },
159. {
160. # vue de la liste des simulations
161. "états": [
162. # /lister-simulations
163. 500,
164. # /supprimer-simulation
165. 600
166. ],
167. "view_name": "views/vue-liste-simulations.html",
168. "model_for_view": ModelForListeSimulationsView()
169. }
170. ],
171.
172. # vue des erreurs inattendues
173. "view-erreurs": {
174. "view_name": "views/vue-erreurs.html",
175. "model_for_view": ModelForErreursView()
176. },
177.
178. # redirections
179. "redirections": [
180. {
181. "états": [
182. 400, # /fin-session réussi
183. ],
184. # redirection vers
185. "to": "/init-session/html",
186. }
187. ],
188. }
189. )
190.
191. # étape 3 ------
192. # configuration de la base de données
193. import config_database
194. config["database"] = config_database.configure(config)
195.
196. # étape 4 ------
197. # instanciation des couches de l'application
198. import config_layers
199. config['layers'] = config_layers.configure(config)
200.
201. # on rend la configuration
202. return config
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
555/755
o lignes 45-55 : la liste des contrôleurs ;
o lignes 57-60 : la liste des réponses HTTP ;
o lignes 62-66 : la liste des modèles de vues ;
• lignes 68-189 : la configuration de l’application avec une série de constantes ;
o lignes 71-98 : nous connaissons déà ces lignes rencontrées dans les versions précédentes ;
o lignes 101-122 : le dictionnaire des contrôleurs :
▪ les clés sont les noms des actions ;
▪ les valeurs sont une instance du contrôleur qui doit gérer cette action. Chaque contrôleur n’est instancié qu’en un
unique exemplaire (singleton). La même instance sera exécutée par différents threads du serveur. Il faudra donc
veiller aux données partagées que chaque contrôleur pourrait vouloir modifier ;
o lignes 125-129 : le dictionnaire des trois réponses HTTP possibles :
▪ les clés sont le type de réponse souhaité par le client (jSON, xml, html) ;
▪ les valeurs sont une instance de la réponse HTTP. Chaque générateur de réponse n’est instancié qu’en un unique
exemplaire (singleton). Le même générateur sera exécuté par différents threads du serveur. Il faudra donc veiller aux
données partagées que chaque générateur pourrait vouloir modifier ;
o lignbes 132-186 : configuration des vues HTML. Pour l’instant, on ignore ces lignes ;
• lignes 191-202 : nous avons déjà rencontré ces lignes dans les versions précédentes ;
Nous allons suivre le cheminement d’une requête client arrivant sur le serveur jusqu’à la réponse HTTP envoyée en retour. Elle suit
le cheminement du serveur MVC.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
556/755
15. config = config.configure({'sgbd': sgbd})
16.
17. # dépendances
18. from flask import request, Flask, session, url_for, redirect
19. from flask_api import status
20. from SendAdminMail import SendAdminMail
21. from myutils import json_response
22. from Logger import Logger
23. import threading
24. import time
25. from random import randint
26. from ImpôtsError import ImpôtsError
27. import os
28.
29. # envoi d'un mail à l'administrateur
30. def send_adminmail(config: dict, message: str):
31. # on envoie un mail à l'administrateur de l'application
32. config_mail = config["adminMail"]
33. config_mail["logger"] = config['logger']
34. SendAdminMail.send(config_mail, message)
35.
36. # vérification du fichier de logs
37. logger = None
38. erreur = False
39. message_erreur = None
40. try:
41. # logueur
42. logger = Logger(config["logsFilename"])
43. except BaseException as exception:
44. # log console
45. print(f"L'erreur suivante s'est produite : {exception}")
46. # on note l'erreur
47. erreur = True
48. message_erreur = f"{exception}"
49. # on mémorise le logueur dans la config
50. config['logger'] = logger
51. # gestion de l'erreur
52. if erreur:
53. # mail à l'administrateur
54. send_adminmail(config, message_erreur)
55. # fin de l'application
56. sys.exit(1)
57.
58. # log de démarrage
59. log = "[serveur] démarrage du serveur"
60. logger.write(f"{log}\n")
61. print(log)
62.
63. # récupération des données de l'administration fiscale
64. erreur = False
65. try:
66. # admindata sera une donnée de portée application en lecture seule
67. config["admindata"] = config["layers"]["dao"].get_admindata().asdict()
68. # log de réussite
69. logger.write("[serveur] connexion à la base de données réussie\n")
70. except ImpôtsError as ex:
71. # on note l'erreur
72. erreur = True
73. # log d'erreur
74. log = f"L'erreur suivante s'est produite : {ex}"
75. # console
76. print(log)
77. # fichier de logs
78. logger.write(f"{log}\n")
79. # mail à l'administrateur
80. send_adminmail(config, log)
81.
82. # le thread principal n'a plus besoin du logger
83. logger.close()
84.
85. # s'il y a eu erreur on s'arrête
86. if erreur:
87. sys.exit(2)
88.
89. # application Flask
90. app = Flask(__name__, template_folder="templates", static_folder="static")
91. # clé secrète de la session
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
557/755
92. app.secret_key = os.urandom(12).hex()
93.
94. # le front controller
95. def front_controller() -> tuple:
96. # on traite la requête
97. logger = None
98. …
99.
100. @app.route('/', methods=['GET'])
101. def index() -> tuple:
102. # redirection vers /init-session/html
103. return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)
104.
105. # init-session
106. @app.route('/init-session/<string:type_response>', methods=['GET'])
107. def init_session(type_response: str) -> tuple:
108. # on exécute le contrôleur associé à l'action
109. return front_controller()
110.
111. # authentifier-utilisateur
112. @app.route('/authentifier-utilisateur', methods=['POST'])
113. def authentifier_utilisateur() -> tuple:
114. # on exécute le contrôleur associé à l'action
115. return front_controller()
116.
117. # calculer-impot
118. @app.route('/calculer-impot', methods=['POST'])
119. def calculer_impot() -> tuple:
120. # on exécute le contrôleur associé à l'action
121. return front_controller()
122.
123. # lister-simulations
124. @app.route('/lister-simulations', methods=['GET'])
125. def lister_simulations() -> tuple:
126. # on exécute le contrôleur associé à l'action
127. return front_controller()
128.
129. # supprimer-simulation
130. @app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
131. def supprimer_simulation(numero: int) -> tuple:
132. # on exécute le contrôleur associé à l'action
133. return front_controller()
134.
135. # fin-session
136. @app.route('/fin-session', methods=['GET'])
137. def fin_session() -> tuple:
138. # on exécute le contrôleur associé à l'action
139. return front_controller()
140.
141. # afficher-calcul-impot
142. @app.route('/afficher-calcul-impot', methods=['GET'])
143. def afficher_calcul_impot() -> tuple:
144. # on exécute le contrôleur associé à l'action
145. return front_controller()
146.
147. # get-admindata
148. @app.route('/get-admindata/<int:numero>', methods=['GET'])
149. def get_admindata() -> tuple:
150. # on exécute le contrôleur associé à l'action
151. return front_controller()
152.
153. # main uniquement
154. if __name__ == '__main__':
155. # on lance le serveur
156. app.config.update(ENV="development", DEBUG=True)
157. app.run(threaded=True)
• lignes 1-92 : toutes ces lignes ont été déjà rencontrées et expliquées ;
• ligne 92 : le serveur va gérer une session. Il nous faut donc une clé secrète. Nous mettrons pour chaque utilisateur deux
informations dans la session :
o si l’utilisateur s’est correctement authentifié ;
o à chaque fois qu’il fera un calcul d’impôt, les résultats de ce calcul seront placés dans une liste qu’on appellera la liste des
simulations de l’utilisateur. Cette liste sera placée en session ;
• lignes 100-151 : la liste des URL de service du serveur. Les fonctions associées servent de filtre : toutes les URL non présentes
dans cette liste seront rejetées par le serveur Flask avec l’erreur [404 NOT FOUND]. Une fois passée ce filtrage, la requête est
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
558/755
systématiquement transmise à un ‘Front Controller’ implémenté par la fonction [front_controller] des lignes 94-98 que
nous allons bientôt présenter ;
• lignes 100-103 : gestion de la route [/]. Le point d’entrée de l’application web sera l’URL de la ligne 107. Aussi ligne 103, nous
redirigeons le client vers cette URL :
o la fonction [url_for] est importée ligne 18. Elle a ici deux paramètres :
▪ le 1er paramètre est le nom d’une des fonctions de routage, ici celle de la ligne 107. On voit que cette fonction attend
un paramètre [type_response] qui est le type (json, xml, html) de réponse souhaité par le client ;
▪ le 2ième paramètre reprend le nom du paramètre de la ligne 107, [type_response] et lui donne une valeur. S’il y avait
d’autres paramètres, on répéterait l’opération pour chacun d’eux ;
▪ elle rend l’URL associée à la fonction désignée par les deux paramètres qui lui ont été donnés. Ici cela donnera
l’URL de la ligne 106 où le paramètre est remplacé par sa valeur [/init-session/html] ;
o la fonction [redirect] a été importée ligne 18. Elle a pour rôle d’envoyer un entête HTTP de redirection au client :
▪ le 1er paramètre est l’URL vers laquelle le client doit être redirigé ;
▪ le 2ième paramètre est le code de statut de la réponse HTTP faite au client. Le code [status.HTTP_302_FOUND]
correspond à une redirection HTTP ;
La fonction [front_controller] des lignes 94-98 fait les premiers traitements de la requête du client :
1. # le front controller
2. def front_controller() -> tuple:
3. # on traite la requête
4. logger = None
5. try:
6. # logger
7. logger = Logger(config["logsFilename"])
8. # on le mémorise dans une config associée au thread
9. thread_config = {"logger": logger}
10. thread_name = threading.current_thread().name
11. config[thread_name] = {"config": thread_config}
12. # on logue la requête
13. logger.write(f"[ front_controller] requête : {request}\n")
14. # on interrompt le thread si cela a été demandé
15. sleep_time = config["sleep_time"]
16. if sleep_time != 0:
17. # la pause est aléatoire pour que certains threads soient interrompus et d'autres pas
18. aléa = randint(0, 1)
19. if aléa == 1:
20. # log avant pause
21. logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
22. # pause
23. time.sleep(sleep_time)
24. # on fait suivre la requête au contrôleur principal
25. main_controller = config['controllers']["main-controller"]
26. résultat, status_code = main_controller.execute(request, session, config)
27. # on logue le résultat envoyé au client
28. log = f"[front_controller] {résultat}\n"
29. logger.write(log)
30. # y-a-t-il eu une erreur fatale ?
31. if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
32. # on envoie un mail à l'administrateur de l'application
33. send_adminmail(config, log)
34. # on détermine le type souhaité pour la réponse
35. if session.get('typeResponse') is None:
36. # le type de session n'a pas encore été établi - ce sera du jSON
37. type_response = 'json'
38. else:
39. type_response = session['typeResponse']
40. # on construit la réponse à envoyer
41. response_builder = config["responses"][type_response]
42. response, status_code = response_builder \
43. .build_http_response(request, session, config, status_code, résultat)
44. # on envoie la réponse
45. return response, status_code
46. except BaseException as erreur:
47. # c'est une erreur inattendue - on logue l'erreur si c'est possible
48. if logger:
49. logger.write(f"[ front_controller] {erreur}")
50. # on prépare la réponse au client
51. résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
52. # on envoie lune réponse en jSON
53. return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
54. finally:
55. # on ferme le fichier de logs s'il a été ouvert
56. if logger:
57. logger.close()
• lignes 1-57 : nous connaissons ce code. C’était par exemple le code de la fonction appelée [main] dans le script [main] de la
version précédente. Une seule chose est à noter, le contrôleur utilisé aux lignes 25-26 :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
559/755
• ligne 25 : on récupère dans la configuration l’instance de contrôleur associée au nom [main-controller]. Il s’agit des lignes
suivantes :
Le travail de la fonction [front_controller] puis de la classe [MainController] est de faire le travail commun à toutes les requêtes :
Dans le schéma ci-dessus, on en est toujours à la phase 1 du traitement de la requête. Le contrôleur principal [MainController] va
poursuivre l’étape 1.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
560/755
• l’interface [InterfaceController] ne définit que l’unique méthode [execute] de la ligne 8. Cette méthode reçoit trois
paramètres :
o [request] : la requête du client ;
o [session] : la session du client ;
o [config] : la configuration de l’application ;
• lignes 11-13 : le contrôleur commence par récupérer l’action demandée par le client. On rappelle que les URL de service sont
de la forme [/action/param1/param2/…] et que cette URL est dans [request.path] ;
• lignes 17-23 : l’action [init-session] sert à initialiser le type de réponse (json, xml, html) désiré par le client. Cette information
est mise en session associée à la clé [typeRéponse]. Si donc l’action n’est pas [init-session] alors la session doit contenir la
clé [typeRéponse], sinon la requête est erronée ;
• lignes 21-22 : la structure du résultat rendu par chaque contrôleur, ici un résultat d’erreur :
o [action] : est le nom de l’action en cours. Cela va permettre d’avoir son nom lorsqu’on va loguer le résultat de la requête ;
o [état] : est un code d’état à trois chiffres :
▪ [x00] pour une réussite ;
▪ [x01] pour un échec ;
o [réponse] : est la réponse à la requête. Sa nature est propre à chaque requête ;
• lignes 24-30 : l’action [authentifier-utilisateur] sert à authentifier l’utilisateur. Si elle réussit, une clé [user=True] est mise
dans la session de l’utilisateur. Certaines URL de service ne sont accessibles que par un utilisateur authentifié. C’est ce qu’on
vérifie ici ;
• ligne 26 : seules les actions [init-session] et [authentifier-utilisateur] peuvent être faites par un utilisateur non encore
authentifié ;
• lignes 28-29 : le résultat à envoyer en cas d’erreur ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
561/755
• lignes 32-34 : si l’une des deux erreurs précédentes s’est produite, alors on envoie la réponse d’erreur au client avec le statut
HTTP 400 BAD REQUEST ;
• lignes 35-39 : s’il n’y a pas eu d’erreur alors on passe la main au contrôleur chargé de traiter l’action en cours. Son instance est
trouvée dans la configuration de l’application ;
La classe [MainController] continue le travail de la fonction [front_controller] : à elles deux, elles rassemblent tout ce qui peut
être factorisé dans le traitement des requêtes, attendant le dernier moment pour passer la requête à un contrôleur spécifique. La
répartition du code entre la fonction [front_controller] et la classe [MainController] est tout à fait subjective. Ici j’ai voulu
conserver l’acquis de la version précédente : la fonction [front_controller] existait déjà sous le nom [main]. Dans la pratique, on
pourrait :
Nous en sommes toujours à l’étape 1 ci-dessus. S’il n’y a pas eu d’erreur, l’étape 2 va commencer. La requête a été transmise au
contrôleur spécifique à l’action demandée par la requête. Supposons que cette action soit [/init-session] définie par la route :
1. # init-session
2. @app.route('/init-session/<string:type_response>', methods=['GET'])
3. def init_session(type_response: str) -> tuple:
4. # on exécute le contrôleur associé à l'action
5. return front_controller()
Le contrôleur [InitSessionController] (ligne 4) prend donc la main. Son code est le suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
562/755
17. résultat = {"action": action, "état": 701,
18. "réponse": [f"paramètre [type={type_response}] invalide"]}
19. # si pas d'erreur
20. if not erreur:
21. # on met le type de la session dans la session flask
22. session['typeResponse'] = type_response
23. résultat = {"action": action, "état": 700,
24. "réponse": [f"session démarrée avec le type de réponse {type_response}"]}
25. return résultat, status.HTTP_200_OK
26. else:
27. return résultat, status.HTTP_400_BAD_REQUEST
• ligne 6: comme
les autres contrôleurs, le contrôleur [InitSessionController] implémente l’interface
[InterfaceController] ;
• ligne 10 : l’URL est de type [/init-session/type_response]. On récupère l’action [init-session] et le type de réponse
souhaité ;
• ligne 15 : le type de réponse souhaité ne peut être que l’un de ceux présents dans la configuration des réponses :
• si ce n’est pas le cas on prépare une réponse d’erreur 701 (ligne 17) ;
• lignes 20-25 : cas où le type de réponse souhaité est valide ;
o ligne 22 : le type de réponse souhaité est mis en session. En effet, il va falloir s’en souvenir pour les requêtes qui vont
suivre ;
o lignes 23-24 : on prépare une réponse de réussite 700 ;
o ligne 25 : la réponse de réussite est rendue au code appelant ;
• ligne 27 : s’il y a eu erreur, la réponse d’erreur est rendue au code appelant ;
Nous venons de voir les étapes 1 et 2. Nous avons rencontré trois codes d’état :
Examinons comment la réponse du serveur va être envoyée au client lors de l’étape 3 ci-dessus. Cela se passe dans la fonction
[front_controller] du script [main] :
1. # le front controller
2. def front_controller() -> tuple:
3. # on traite la requête
4. logger = None
5. try:
6. # logger
7. logger = Logger(config["logsFilename"])
8. # on le mémorise dans une config associée au thread
9. thread_config = {"logger": logger}
10. thread_name = threading.current_thread().name
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
563/755
11. config[thread_name] = {"config": thread_config}
12. # on logue la requête
13. logger.write(f"[ front_controller] requête : {request}\n")
14. # on interrompt le thread si cela a été demandé
15. sleep_time = config["sleep_time"]
16. if sleep_time != 0:
17. # la pause est aléatoire pour que certains threads soient interrompus et d'autres pas
18. aléa = randint(0, 1)
19. if aléa == 1:
20. # log avant pause
21. logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
22. # pause
23. time.sleep(sleep_time)
24. # on fait suivre la requête au contrôleur principal
25. main_controller = config['controllers']["main-controller"]
26. résultat, status_code = main_controller.execute(request, session, config)
27. # on logue le résultat envoyé au client
28. log = f"[front_controller] {résultat}\n"
29. logger.write(log)
30. # y-a-t-il eu une erreur fatale ?
31. if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
32. # on envoie un mail à l'administrateur de l'application
33. send_adminmail(config, log)
34. # on détermine le type souhaité pour la réponse
35. if session.get('typeResponse') is None:
36. # le type de session n'a pas encore été établi - ce sera du jSON
37. type_response = 'json'
38. else:
39. type_response = session['typeResponse']
40. # on construit la réponse à envoyer
41. response_builder = config["responses"][type_response]
42. response, status_code = response_builder \
43. .build_http_response(request, session, config, status_code, résultat)
44. # on envoie la réponse
45. return response, status_code
46. except BaseException as erreur:
47. # c'est une erreur inattendue - on logue l'erreur si c'est possible
48. if logger:
49. logger.write(f"[ front_controller] {erreur}")
50. # on prépare la réponse au client
51. résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
52. # on envoie lune réponse en jSON
53. return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
54. finally:
55. # on ferme le fichier de logs s'il a été ouvert
56. if logger:
57. logger.close()
Dans le fichier de configuration, chaque type de réponse (json, xml, html) a été associé à une instance de classe :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
564/755
Chaque classe de réponse implémente l’interface [InterfaceResponse] suivante :
• lignes 8-11 : l’interface [InterfaceResponse] définit une unique méthode [build_http_response] ayant les paramètres
suivants :
o [request, session, config] : ce sont les paramètres reçus par le contrôleur de l’action ;
o [résultat, status_code] : ce sont les résultats produits par le contrôleur de l’action ;
Nous allons présenter la réponse jSON. Elle est produite par la classe [JsonResponse] suivante :
1. import json
2.
3. from flask import make_response
4. from flask.wrappers import Response
5. from werkzeug.local import LocalProxy
6.
7. from InterfaceResponse import InterfaceResponse
8.
9. class JsonResponse(InterfaceResponse):
10.
11. def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
12. résultat: dict) -> (Response, int):
13. # résultats : le dictionnaire des résultats
14. # status_code : le code de statut de la réponse HTTP
15.
16. # on rend la réponse HTTP
17. response = make_response(json.dumps(résultat, ensure_ascii=False))
18. response.headers['Content-Type'] = 'application/json; charset=utf-8'
19. return response, status_code
Nous connaissons ce code que nous avons rencontré de nombreuses fois. C’est le code de la fonction [json_response] du module
[myutils].
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
565/755
• nous lançons un client Postman ;
Test 1
Nous montrons tout d’abord une requête invalide parce que la session n’a pas été initialisé :
# authentifier-utilisateur
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
# on exécute le contrôleur associé à l'action
return front_controller()
mais elle n’est acceptée que si la session a été initialisée auparavant avec l’action [/init-session].
• [1-2] : on a obtenu une réponse jSON. Lorsque le type de réponse n’a pas encore été fixé par le client, le serveur utilise le
jSON pour répondre ;
• [3-5] : le dictionnaire jSON de la réponse ;
o [action] : l’action qui a été exécutée ;
o [état] : le code d’état de la réponse. Un code [x01] indique une erreur ;
o [réponse] : est adaptée à chaque action. Ici elle contient un message d’erreur ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
566/755
• [1-2] est une route correcte :
# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
# on exécute le contrôleur associé à l'action
return front_controller()
Elle va donc entrer dans le tunnel de traitement des requêtes du serveur MVC. Néanmoins elle devrait être refusée au cours
de ce traitement parce que le type de session demandé est incorrect.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
567/755
Maintenant, initialisons une session XML. La réponse jSON va être remplacée par une réponse XML générée par la classe
[XmlResponse] suivante :
1. import xmltodict
2. from flask import make_response
3. from flask.wrappers import Response
4. from werkzeug.local import LocalProxy
5.
6. from InterfaceResponse import InterfaceResponse
7. from Logger import Logger
8.
9. class XmlResponse(InterfaceResponse):
10.
11. def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
12. résultat: dict) -> (Response, int):
13. # résultats : le dictionnaire des résultats
14. # status_code : le code de statut de la réponse HTTP
15.
16. # résultat : le dictionnaire à transformer en chaîne XML
17. xml_string = xmltodict.unparse({"root": résultat})
18. # on rend la réponse HTTP
19. response = make_response(xml_string)
20. response.headers['Content-Type'] = 'application/xml; charset=utf-8'
21. return response, status_code
C’est du code que nous connaissons, celui de la fonction [xml_response] du module partagé [myutils].
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
568/755
Nous obtenons la même réponse qu’en jSON mais cette fois-ci la réponse est habilllée en XML.
1. # authentifier-utilisateur
2. @app.route('/authentifier-utilisateur', methods=['POST'])
3. def authentifier_utilisateur() -> tuple:
4. # on exécute le contrôleur associé à l'action
5. return front_controller()
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
569/755
27. # paramètre [user]
28. user = post_params.get("user")
29. if user is None:
30. erreur = True
31. erreurs.append("paramètre [user] manquant")
32. # paramètre [password]
33. password = post_params.get("password")
34. if password is None:
35. erreur = True
36. erreurs.append("paramètre [password] manquant")
37. # erreur ?
38. if erreur:
39. status_code = status.HTTP_400_BAD_REQUEST
40. # erreur ?
41. if not erreur:
42. # on vérifie la validité du couple (user, password)
43. users = config['users']
44. i = 0
45. nbusers = len(users)
46. trouvé = False
47. while not trouvé and i < nbusers:
48. trouvé = user == users[i]["login"] and password == users[i]["password"]
49. i += 1
50. # trouvé ?
51. if not trouvé:
52. # on note l'erreur
53. erreur = True
54. status_code = status.HTTP_401_UNAUTHORIZED
55. erreurs.append(f"Echec de l'authentification")
56. else:
57. # on note dans la session qu'on a trouvé l'utilisateur
58. session["user"] = True
59. # c'est fini
60. if not erreur:
61. # retour sans erreur
62. résultat = {"action": action, "état": 200, "réponse": f"Authentification réussie"}
63. return résultat, status.HTTP_200_OK
64. else:
65. # retour avec erreur
66. return {"action": action, "état": 201, "réponse": erreurs}, status_code
67.
On notera que si l’utilisateur était authentifié avec les identifiants [identifiants1] et qu’il échoue à s’authentifier avec les identifiants
[identifiants2], il reste néanmoins authentifié avec les identifiants [identifiants1].
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
570/755
• en [3-5], le POST n’a pas de corps ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
571/755
Cas 2 : POST avec identifiants corrects
1. # calculer-impot
2. @app.route('/calculer-impot', methods=['POST'])
3. def calculer_impot() -> tuple:
4. # on exécute le contrôleur associé à l'action
5. return front_controller()
1. import re
2.
3. from flask_api import status
4. from werkzeug.local import LocalProxy
5.
6. from InterfaceController import InterfaceController
7. from TaxPayer import TaxPayer
8.
9. class CalculerImpotController(InterfaceController):
10.
11. def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
12. # on récupère les éléments du path
13. dummy, action = request.path.split('/')
14.
15. # pas d'erreur au départ
16. erreur = False
17. erreurs = []
18. # les paramètres du POST
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
572/755
19. post_params = request.form
20. # il faut un POST avec trois paramètres
21. if len(post_params) != 3:
22. erreur = True
23. erreurs.append(
24. "méthode POST requise avec les paramètres postés [marié, enfants, salaire]")
25. # on analyse les paramètres postés
26. if not erreur:
27. # paramètre marié
28. marié = post_params.get("marié")
29. if marié is None:
30. erreurs.append("paramètre [marié] manquant")
31. else:
32. # le paramètre est-il valide ?
33. marié = marié.lower()
34. if marié != "oui" and marié != "non":
35. erreur = True
36. erreurs.append(f"valeur [{marié}] invalide pour le paramètre [marié (oui/non)]")
37. # paramètre [enfants]
38. enfants = post_params.get("enfants")
39. if enfants is None:
40. erreur = True
41. erreurs.append("paramètre [enfants] manquant")
42. else:
43. # le paramètre est-il valide ?
44. enfants = enfants.strip()
45. match = re.match(r"\d+", enfants)
46. if not match:
47. erreur = True
48. erreurs.append(f"valeur [{enfants}] invalide pour le paramètre [enfants (entier>=0)]")
49. # paramètre salaire
50. salaire = post_params.get("salaire")
51. if salaire is None:
52. erreur = True
53. erreurs.append("paramètre [salaire] manquant")
54. else:
55. # le paramètre est-il valide ?
56. salaire = salaire.strip()
57. match = re.match(r"\d+", salaire)
58. if not match:
59. erreur = True
60. erreurs.append(f"valeur [{salaire}] invalide pour le paramètre [salaire (entier>=0)]")
61. # erreur ?
62. if erreur:
63. status_code = status.HTTP_400_BAD_REQUEST
64. résultat = {"action": action, "état": 301, "réponse": erreurs}
65. # on rend le résultat
66. return résultat, status_code
67.
68. # calcul de l'impôt
69. # on récupère la couche [métier] et le dictionnaire [adminData]
70. métier = config["layers"]["métier"]
71. admin_data = config["admindata"]
72. # calcul de l'impôt
73. taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
74. métier.calculate_tax(taxpayer, admin_data)
75. # n° de la simulation
76. id_simulation = session.get('id_simulation', 0)
77. id_simulation += 1
78. session['id_simulation'] = id_simulation
79. # on met le résultat en session ous la forme du dictionnaire d'un TaxPayer
80. simulation = taxpayer.fromdict({'id': id_simulation}).asdict()
81. # on ajoute le résultat à la liste des simulations déjà faites et on met celle-ci en session
82. simulations = session.get("simulations", [])
83. simulations.append(simulation)
84. session["simulations"] = simulations
85. # résultat
86. résultat = {"action": action, "état": 300, "réponse": simulation}
87. status_code = status.HTTP_200_OK
88.
89. # on rend le résultat
90. return résultat, status_code
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
573/755
• ligne 19 : on récupère les paramètres postés. Ceux-ci sont postés sous la forme [x-www-form-urlencoded] et c’est pourquoi
on les récupère dans [request.form]. S’ils avaient été postés en jSON on les aurait récupérés dans [request.data] ;
• lignes 21-24 : on vérifie qu’il y a bien trois paramètres postés ;
• lignes 27-36 : vérification de la présence et de la validité du paramètre posté [marié] ;
• lignes 37-48 : vérification de la présence et de la validité du paramètre posté [enfants] ;
• lignes 49-60 : vérification de la présence et de la validité du paramètre posté [salaire] ;
• lignes 62-66 : s’il y a eu erreur, on envoie une réponse d’erreur 400 BAD REQUEST avec un code d’état [301] ;
• lignes 69-71 : s’il n’y a pas eu erreur, on se prépare à calculer l’impôt. Pour cela,
o ligne 70 : on récupère une référence sur la couche [métier] ;
o ligne 71 : on récupère les données de l’administration fiscale dans la configuration du serveur ;
• lignes 72-74 : l’impôt du contribuable est calculé ;
• lignes 75-77 : on va compter le nombre de calculs d’impôt faits par l’utilisateur ;
o ligne 76 : on récupère en session, le n° du dernier calcul fait. On appelle ici [simulation] le résultat d’un calcul ;
o ligne 77 : on incrémente le n° de la dernière simulation ;
o ligne 78 : on remet ce n° en session ;
• lignes 79-84 : pour suivre les calculs faits par l’utilisateur, on va mettre dans sa session, la liste des simulations qu’il a faites ;
• ligne 80 : une simulation sera le dictionnaire d’un objet TaxPayer dont la propriété [id] aura pour valeur le n° de la simulation ;
• lignes 82-84 : la simulation courante est ajoutée à la liste des simulations présente en session ;
• lignes 86-87 : on prépare une réponse HTTP de réussite ;
• ligne 90 : on rend le résultat ;
Faisons quelques tests : le serveur web, le SGBD, le serveur de mails, un client Postman sont lancés.
Cas 1 : faire un calcul d’impôt alors que la session n’est pas initialisée
On lance tout d’abord une session jSON avec [/init-session/json]. Puis on fait la même requête que précédemment. La réponse
est alors la suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
574/755
Cas 3 : faire un calcul d’impôt avec des paramètres manquants
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
575/755
Cas 4 : faire un calcul d’impôt avec des paramètres corrects
1. # lister-simulations
2. @app.route('/lister-simulations', methods=['GET'])
3. def lister_simulations() -> tuple:
4. # on exécute le contrôleur associé à l'action
5. return front_controller()
Le serveur n’attend aucun paramètre. L’action [lister-simulations] est traitée par le contrôleur [ListerSimulationsController]
suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
576/755
5.
6. class ListerSimulationsController(InterfaceController):
7.
8. def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
9. # on récupère les éléments du path
10. dummy, action = request.path.split('/')
11.
12. # on récupère la liste des simulations dans la session
13. simulations = session.get("simulations", [])
14. # on rend le résultat
15. return {"action": action, "état": 500,
16. "réponse": simulations}, status.HTTP_200_OK
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
577/755
• en [4], la liste des simulations de l’utilisateur ;
1. # supprimer-simulation
2. @app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
3. def supprimer_simulation(numero: int) -> tuple:
4. # on exécute le contrôleur associé à l'action
5. return front_controller()
Le serveur attend un unique paramètre, le n° de la simulation à supprimer. L’action [supprimer-simulation] est traitée par le
contrôleur [SupprimerSimulationController] suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
578/755
25. session["simulations"] = simulations
26. # on rend le résultat
27. return {"action": action, "état": 600, "réponse": simulations}, status.HTTP_200_OK
• ligne 10 : on récupère les deux éléments du chemin de la requête. On les récupère en temps que chaîne de caractères ;
• ligne 13 : le paramètre [numéro] est transformé en entier. On sait que c’est possible à cause de la signature de sa route,
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
On sait de plus que c’est un entier >=0. On ne peut pas en effet avoir une URL [/supprimer-simulation/-4]. Celle-ci est
refusée par le serveur Flask ;
• ligne 15 : on récupère la liste des simulations dans la session ;
• ligne 16 : avec la fonction [filter], on cherche la simulation ayant id==numéro. On obtient un objet [filter] qu’on convertit
en type [list] ;
• lignes 17-20 : si le filtre n’a rien ramené, alors c’est que la simulation à supprimer n’existe pas. On rend une réponse d’erreur
qui l’indique ;
• lignes 21-23 : on supprime la simulation ramenée par le filtre ;
• ligne 25 : on remet la nouvelle liste de simulations en session ;
• ligne 27 : on rend dans la réponse, la nouvelle liste de simulations ;
Nous faisons un test de réussite et un test d’échec. On fait des simulations puis on demande la liste des simulations :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
579/755
Maintenant, recommençons la même opération (suppression de la simulation d’id=3). La réponse est alors la suivante :
1. # fin-session
2. @app.route('/fin-session', methods=['GET'])
3. def fin_session() -> tuple:
4. # on exécute le contrôleur associé à l'action
5. return front_controller()
Le serveur n’attend aucun paramètre. L’action est traitée par le contrôleur [FinSessionController] suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
580/755
o [user] : l’indicateur que l’utilisateur a été authentifié ;
• on rend la réponse ;
On peut se demander comment va être rendue la réponse HTTP de la ligne 15, maintenant que le type de réponse n’est plus dans la
session. Pour le savoir, il faut revenir à la fonction |front_controller| du script principal [main] et la modifier de la façon suivante :
1. …
2. # on note le type de réponse souhaité si cette information est dans la session
3. type_response1 = session.get('typeResponse', None)
4. # on fait suivre la requête au contrôleur principal
5. main_controller = config['controllers']["main-controller"]
6. résultat, status_code = main_controller.execute(request, session, config)
7. # on logue le résultat envoyé au client
8. log = f"[front_controller] {résultat}\n"
9. logger.write(log)
10. # y-a-t-il eu une erreur fatale ?
11. if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
12. # on envoie un mail à l'administrateur de l'application
13. send_adminmail(config, log)
14. # on détermine le type souhaité pour la réponse
15. type_response2=session.get('typeResponse')
16. if type_response2 is None and type_response1 is None:
17. # le type de session n'a pas encore été établi - ce sera du jSON
18. type_response = 'json'
19. elif type_response2 is not None:
20. # le type de la réponse est connu et dans la session
21. type_response = type_response2
22. else:
23. type_response=type_response1
24. # on construit la réponse à envoyer
25. response_builder = config["responses"][type_response]
26. response, status_code = response_builder \
27. .build_http_response(request, session, config, status_code, résultat)
28. # on envoie la réponse
29. return response, status_code
1. # get-admindata
2. @app.route('/get-admindata', methods=['GET'])
3. def get_admindata() -> tuple:
4. # on exécute le contrôleur associé à l'action
5. return front_controller()
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
581/755
7.
8. class GetAdminDataController(InterfaceController):
9.
10. def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
11. # on récupère les éléments du path
12. dummy, action = request.path.split('/')
13. # seules les sessions json et xml sont acceptées
14. type_response = session.get('typeResponse')
15. if type_response != 'json' and type_response != 'xml':
16. # on rend une réponse d'erreur
17. return {
18. "action": action,
19. "état": 1001,
20. "réponse": ["cette action n'est possible que pour les sessions json ou xml"]
21. }, status.HTTP_400_BAD_REQUEST
22. else:
23. # on rend une réponse de réussite
24. return {"action": action, "état": 1000, "réponse": config["adminData"].asdict()}, status.HTTP_200_OK
• lignes 13-21 : on vérifie qu’on est dans une session json ou xml ;
• ligne 24 : on rend le dictionnaire des données de l’administration fiscale qui dès le démarrage du serveur avaient été placées
dans la configuration :
Prenons un client Postman et demandons l’URL [/get-admindata], après avoir démarré une session jSON et s’être authentifié :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
582/755
30.16 L’action [calculer-impots]
L’action [calculer-impots] calcule l’impôts d’une liste de contribuables trouvée dans le corps de la requête sous la forme d’une
chaîne jSON. Nous connaissons déjà cette action : elle s’appelait [calculate_tax_in_bulk_mode] dans la version précédente.
1. import json
2.
3. from flask_api import status
4. from werkzeug.local import LocalProxy
5.
6. from ImpôtsError import ImpôtsError
7. from InterfaceController import InterfaceController
8. from TaxPayer import TaxPayer
9.
10. class CalculerImpotsController(InterfaceController):
11.
12. def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
13. # on récupère les éléments du path
14. dummy, action = request.path.split('/')
15.
16. # seules les sessions json et xml sont acceptées
17. type_response = session.get('typeResponse')
18. if type_response != 'json' and type_response != 'xml':
19. # on rend une réponse d'erreur
20. return {
21. "action": action,
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
583/755
22. "état": 1501,
23. "réponse": ["cette action n'est possible que pour les sessions json ou xml"]
24. }, status.HTTP_400_BAD_REQUEST
25.
26. # on récupère le corps du post - on attend une liste de dictionnaires
27. msg_erreur = None
28. list_dict_taxpayers = None
29. # le corps jSON du POST
30. request_text = request.data
31. try:
32. # qu'on transforme en une liste de dictionnaires
33. list_dict_taxpayers = json.loads(request_text)
34. except BaseException as erreur:
35. # on note l'erreur
36. msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
37. # a-t-on une liste non vide ?
38. if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
39. # on note l'erreur
40. msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
41. # a-t-on une liste de dictionnaires ?
42. if not msg_erreur:
43. erreur = False
44. i = 0
45. while not erreur and i < len(list_dict_taxpayers):
46. erreur = not isinstance(list_dict_taxpayers[i], dict)
47. i += 1
48. # erreur ?
49. if erreur:
50. msg_erreur = "le corps du POST doit être une liste de dictionnaires"
51. # erreur ?
52. if msg_erreur:
53. # on envoie une réponse d'erreur au client
54. résultats = {"action": action, "état": 1501, "réponse": [msg_erreur]}
55. return résultats, status.HTTP_400_BAD_REQUEST
56.
57. # on vérifie les TaxPayers un par un
58. # au départ pas d'erreurs
59. list_erreurs = []
60. for dict_taxpayer in list_dict_taxpayers:
61. # on crée un TaxPayer à partir de dict_taxpayer
62. msg_erreur = None
63. try:
64. # l'opération suivante va éliminer les cas où les paramètres ne sont pas
65. # des propriétés de la classe TaxPayer ainsi que les cas où leurs valeurs
66. # sont incorrectes
67. TaxPayer().fromdict(dict_taxpayer)
68. except BaseException as erreur:
69. msg_erreur = f"{erreur}"
70. # certaines clés doivent être présentes dans le dictionnaire
71. if not msg_erreur:
72. # les clés [marié, enfants, salaire] doivent être présentes dans le dictionnaire
73. keys = dict_taxpayer.keys()
74. if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
75. msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
76. # des erreurs ?
77. if msg_erreur:
78. # on note l'erreur dans le TaxPayer lui-même
79. dict_taxpayer['erreur'] = msg_erreur
80. # on ajoute le TaxPayer à la liste des erreurs
81. list_erreurs.append(dict_taxpayer)
82.
83. # on a traité tous les taxpayers - y-a-t-il des erreurs ?
84. if list_erreurs:
85. # on envoie une réponse d'erreur au client
86. résultats = {"action": action, "état": 1501, "réponse": list_erreurs}
87. return résultats, status.HTTP_400_BAD_REQUEST
88.
89. # pas d'erreurs, on peut travailler
90. # récupération des données de l'administration fiscale
91. admindata = config["admindata"]
92. métier = config["layers"]["métier"]
93. try:
94. # on traite les TaxPayer un à un
95. list_taxpayers = []
96. for dict_taxpayer in list_dict_taxpayers:
97. # calcul de l'impôt
98. taxpayer = TaxPayer().fromdict(
99. {'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
100. 'salaire': dict_taxpayer['salaire']})
101. métier.calculate_tax(taxpayer, admindata)
102. # on mémorise le résultat en tant que dictionnaire
103. list_taxpayers.append(taxpayer.asdict())
104. # on ajoute list_taxpayers aux simulations actuelles en donnant à chaque simulation un n°
105. simulations = session.get("simulations", [])
106. id_simulation = session.get("id_simulation", 0)
107. for simulation in list_taxpayers:
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
584/755
108. # on donne un n° à chaque simulation
109. id_simulation += 1
110. simulation['id'] = id_simulation
111. # on l'ajoute à la liste actuelle des simulations
112. simulations.append(simulation)
113. # on remet le tout en session
114. session["simulations"] = simulations
115. session["id_simulation"] = id_simulation
116. # on envoie la réponse au client
117. return {"action": action, "état": 1500, "réponse": list_taxpayers}, status.HTTP_200_OK
118. except ImpôtsError as erreur:
119. # on envoie une réponse d'erreur au client
120. return {"action": action, "état": 1501, "réponse": [f"{erreur}"]}, status.HTTP_500_INTERNAL_SERVER_ERROR
• lignes 16-24 : on vérifie qu’on est bien dans une session json ou xml
• lignes 26-120 : ce code nous est globalement connu. C’est celui de la fonction |index_controller| de la version 10 de
l’application qui a été aménagé pour répondre aux spécifications de l’interface [InterfaceController] implémentée ;
• lignes 104-115 : le code ajouté pour tenir compte du nouvel environnement de ce contrôleur. On vient de faire des calculs
d’impôt. Il nous faut mémoriser les résultats dans la liste des simulations maintenues en session ;
• ligne 105 : on récupère la liste des simulations en session ;
• ligne 106 : on récupère le n° de la dernière simulation faite ;
• lignes 107-112 : on parcourt la liste des dictionnaires des résultats du calcul de l’impôt, à chacun d’eux on attribue un n° [id]
de simulation et chaque dictionnaire est ajouté à la liste des simulations ;
• lignes 113-115 : la nouvelle liste des simulations ainsi que le n° de la dernière simulation faite sont remis en session ;
Nous faisons le test Postman suivant, après avoir initialisé une session jSON et s’être authentifié :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
585/755
Si maintenant, on demande la liste des simulations :
On remarquera que dans la liste résultat de [/calcul-impots], les contribuables n’ont pas d’attribut [id] alors que dans la liste des
simulations, chaque simulation a un n° qui l’identifie.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
586/755
31 Clients web pour les services jSON et XML de la version 12
Nous allons écrire trois applications console clientes des services jSON et XML du serveur web que nous venons d’écrire. Nous
reprenons l’architecture client / serveur de la version 11 :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
587/755
• en [1] : les données exploitées ou créées par le client ;
• en [2], la configuration et les scripts console du client ;
• en [3], la couche [dao] du client ;
• en [4], le dossier des tests de la couche [dao] du client ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
588/755
31.2.1 Interface
La couche [dao] implémentera l’interface [InterfaceImpôtsDaoWithHttpSession] suivante :
Chaque méthode de l’interface correspond à une URL de service du serveur de calcul de l’impôt.
• ligne 7 : l’interface étend la classe [AbstractDao] qui gère les accès au système de fichiers ;
La correspondance entre les méthodes et les URL de service est faite dans le fichier de configuration [config] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
589/755
31.2.2 Implémentation
L’interface [InterfaceImpôtsDaoWithHttpSession] est implémentée par la classe [ImpôtsDaoWithHttpSession] suivante :
1. # imports
2. import json
3.
4. import requests
5. import xmltodict
6. from flask_api import status
7.
8. from AbstractImpôtsDao import AbstractImpôtsDao
9. from AdminData import AdminData
10. from ImpôtsError import ImpôtsError
11. from InterfaceImpôtsDaoWithHttpSession import InterfaceImpôtsDaoWithHttpSession
12. from TaxPayer import TaxPayer
13.
14. class ImpôtsDaoWithHttpSession(InterfaceImpôtsDaoWithHttpSession):
15.
16. # constructeur
17. def __init__(self, config: dict):
18. # initialisation parent
19. AbstractImpôtsDao.__init__(self, config)
20. # mémorisation éléments de la configuration
21. # config générale
22. self.__config = config
23. # serveur
24. self.__config_server = config["server"]
25. # services
26. self.__config_services = config["server"]['url_services']
27. # mode debug
28. self.__debug = config["debug"]
29. # logger
30. self.__logger = None
31. # cookies
32. self.__cookies = None
33. # type de session (json, xml)
34. self.__session_type = None
35.
36. # étape request / response
37. def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
38. # [method] : méthode HTTP GET ou POST
39. # [url_service] : URL de service
40. # [data] : paramètres du POST en x-www-form-urlencoded
41. # [json] : paramètres du POST en json
42. # [cookies]: cookies à inclure dans la requête
43.
44. # on doit avoir une session XML ou JSON, sinon on ne pourra pas gérer la réponse
45. if self.__session_type not in ['json', 'xml']:
46. raise ImpôtsError(73, "il n'y a pas de session valide en cours")
47.
48. # connexion
49. if method == "GET":
50. # GET
51. response = requests.get(url_service, cookies=self.__cookies)
52. else:
53. # POST
54. response = requests.post(url_service, data=data_value, json=json_value, cookies=self.__cookies)
55.
56. # mode debug ?
57. if self.__debug:
58. # logueur
59. if not self.__logger:
60. self.__logger = self.__config['logger']
61. # on logue
62. self.__logger.write(f"{response.text}\n")
63.
64. # résultat
65. if self.__session_type == "json":
66. résultat = json.loads(response.text)
67. else: # xml
68. résultat = xmltodict.parse(response.text[39:])['root']
69.
70. # on récupère les cookies de la réponse s'il y en a
71. if response.cookies:
72. self.__cookies = response.cookies
73.
74. # code de statut
75. status_code = response.status_code
76.
77. # si code de statut différent de 200 OK
78. if status_code != status.HTTP_200_OK:
79. raise ImpôtsError(35, résultat['réponse'])
80.
81. # on rend le résultat
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
590/755
82. return résultat['réponse']
83.
84. def init_session(self, session_type: str):
85. # on note le type de la session
86. self.__session_type = session_type
87.
88. # url de service
89. config_server = self.__config_server
90. url_service = f"{config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
91.
92. # exécution requête
93. self.get_response("GET", url_service)
94. …
Le code des lignes 64-68 assure que dans les deux cas, on obtient dans [résultat] un dictionnaire avec les clés [action, état,
réponse] ;
• lignes 70-72 : si la réponse contient des cookies on les récupère. Il faudra les renvoyer lors de la prochaine requête ;
• lignes 74-79 : si le statut HTTP de la réponse est différent de 200, on lève une exception avec le message d’erreur contenu
dans résultat[’réponse’]. Ce peut être une erreur ou une liste d’erreurs ;
• lignes 81-82 : on rend la réponse du serveur au code appelant ;
[init_session]
• ligne 84 : la méthode [init_session] sert à fixer le type de session json ou xml que veut commencer le client avec le serveur ;
• ligne 86 : on note le type de session désiré au sein de la classe. En effet, toutes les méthodes ont besoin de cette information
pour décoder correctement la réponse du serveur ;
• lignes 88-90 : à l’aide de la configuration de l’application, on détermine l’URL de service qu’il faut interroger ;
• ligne 93 : l’URL de service est interrogée. On ne récupère pas le résultat de la méthode [get_response] :
o si elle lance une exception, alors l’opération a échoué. L’exception n’est pas gérée ici et remontera directement au code
appelant qui arrêtera alors le client avec un message d’erreur ;
o si elle ne lance pas d’exception, alors c’est que l’initialisation de la session a réussi ;
[authenticate_user]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
591/755
6. "user": user,
7. "password": password
8. }
9.
10. # exécution requête
11. self.get_response("POST", url_service, post_params)
• la méthode [authenticate_user] sert à s’authentifier auprès du serveur. Pour cela elle reçoit les identifiants de connexion
[user, password] en ligne 1 ;
• lignes 2-4 : on détermine l’URL de service à interroger ;
• lignes 5-8 : les paramètres du POST puisque l’URL [/authentifier-utilisateur] attend un POST avec des paramètres [user,
password] ;
• ligne 11 : la requête est exécutée. Là encore on ne récupère pas la réponse du serveur. C’est l’exception lancée par
[get_response] qui indique si on a réussi ou pas ;
[calculate_tax]
• la méthode [calculate_tax] permet de calculer l’impôt d’un contribuable [taxpayer] passé en paramètre. Ce paramètre est
modifié par la méthode (ligne 15) et constitue donc le résultat de la méthode ;
• lignes 2-4 : on définit l’URL de service à interroger ;
• lignes 6-10 : les paramètres du POST à émettre. En effet, l’URL de service [/calculer-impot] attend un POST avec les
paramètres [marié, enfants, salaire] ;
• lignes 12-13 : la requête est exécutée et la réponse du serveur récupérée. L’URL de service [/calculer-impot] renvoie un
dictionnaire avec les clés [impôt, décôte, surcôte, réduction, taux] de l’impôt ;
• ligne 15 : le dictionnaire [response] obtenu est utilisé pour mettre à jour le contribuable [taxpayer] ;
[calculate_tax_in_bulk_mode]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
592/755
• ligne 2 : la méthode reçoit une liste de contribuables de type TaxPayer ;
• lignes 7-13 : cette liste d’élements de type [TaxPayer] est transformée en liste de dictionnaires [marié, enfants, salaire] ;
• lignes 15-17 : on fixe l’URL de service ;
• lignes 19-20 : on exécute une requête POST dont le corps jSON est formé de la liste des dictionnaires créée ligne 7. On
récupère la réponse du serveur ;
• lignes 23-24 : les tests ont montré un problème lorsque la session est de type XML :
o si la liste initiale des contribuables a N éléments (N>1) on obtient comme résultat une liste de N dictionnaires de type
[OrderedDict] ;
o si la liste initiale n’a qu’un élément, on obtient non pas une liste mais un élément de type [OrderedDict] ;
lignes 23-24 : si on est dans ce dernier cas (1 élément), on transforme le résultat en liste de 1 élément ;
• lignes 25-28 : cette liste de dictionnaires reçus contient l’impôt de chaque contribuable de la liste initiale. On met à jour alors
chacun d’eux avec les résultats reçus ;
[get_simulations]
• ligne 1 : la méthode demande la liste des simulations faites dans la session courante ;
• ligne 2 : la méthode rend la réponse du serveur ;
[delete_simulation]
[get-admindata]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
593/755
• ligne 1 : la méthode demande au serveur les constantes fiscales permettant le calcul de l’impôt ;
• ligne 29 : elle rend un type [AdminData] ;
• ligne 9 : on récupère la réponse du serveur sous la forme d’un dictionnaire. Les tests montrent qu’il y a un problème lorsque
la session est une session XML : au lieu d’être des valeurs numériques, les valeurs du dictionnaire sont des chaînes de caractères.
Nous avions signalé ce problème lors de l’étude du module [xmltodict] et constaté que c’était un fonctionnement normal.
[xmltodict] n’a pas d’informations de types dans le flux XML qu’on lui donne. Ceci dit, dans ce cas précis, il faut convertir
en numérique toutes les valeurs du dictionnaire reçu. Celui-ci contient trois listes [limites, coeffr, coeffn] et une suite de
propriétés numériques ;
• lignes 13-25 : création d’un dictionnaire [résultat2] à valeurs numériques à partir du dictionnaire [résultat] à valeurs de
type chaîne de caractères ;
• ligne 29 : le dictionnaire [resultat2] est utilisé pour initialiser un type [AdminData] ;
Les clients sont configurés par les fichiers [config] et [config_layers]. Le fichier [config] est le suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
594/755
22. f"{root_dir}/impots/v05/services",
23. # AdminData, ImpôtsError, TaxPayer
24. f"{root_dir}/impots/v04/entities",
25. # Constantes, tranches
26. f"{root_dir}/impots/v05/entities",
27. # ImpôtsDaoWithHttpSession, ImpôtsDaoWithHttpSessionFactory, InterfaceImpôtsDaoWithHttpSession
28. f"{script_dir}/../services",
29. # scripts de configuration
30. script_dir,
31. # Logger
32. f"{root_dir}/impots/http-servers/02/utilities",
33. ]
34.
35. # on fixe le syspath
36. from myutils import set_syspath
37. set_syspath(absolute_dependencies)
38.
39. # étape 2 ------
40. # configuration de l'application avec des constantes
41. config.update({
42. # fichier des contribuables
43. "taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
44. # fichier des résultats
45. "resultsFilename": f"{script_dir}/../data/output/résultats.json",
46. # fichier des erreurs
47. "errorsFilename": f"{script_dir}/../data/output/errors.txt",
48. # fichier de logs
49. "logsFilename": f"{script_dir}/../data/logs/logs.txt",
50. # le serveur de calcul de l'impôt
51. "server": {
52. "urlServer": "http://127.0.0.1:5000",
53. "user": {
54. "login": "admin",
55. "password": "admin"
56. },
57. "url_services": {
58. "calculate-tax": "/calculer-impot",
59. "get-admindata": "/get-admindata",
60. "calculate-tax-in-bulk-mode": "/calculer-impots",
61. "init-session": "/init-session",
62. "end-session": "/fin-session",
63. "authenticate-user": "/authentifier-utilisateur",
64. "get-simulations": "/lister-simulations",
65. "delete-simulation": "/supprimer-simulation"
66. }
67. },
68. # mode debug
69. "debug": True
70. }
71. )
72.
73. # étape 3 ------
74. # instanciation des couches
75. import config_layers
76. config['layers'] = config_layers.configure(config)
77.
78. # on rend la configuation
79. return config
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
595/755
• les clients n’auront pas un accès direct à la couche [dao]. Pour en avoir une, ils devront passer par la factory de la couche
[dao] ;
Le client [main] permet de tester les URL [/init-session, /authentifier-utilisateur, /calculer-impots, /fin-session] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
596/755
59. # on récupère la factory de la couche [dao]
60. dao_factory = config["layers"]["dao_factory"]
61. # on crée une instance de la couche [dao]
62. dao = dao_factory.new_instance()
63. # lecture des données des contribuables
64. taxpayers = dao.get_taxpayers_data()["taxpayers"]
65. # des contribuables ?
66. if not taxpayers:
67. raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
68. # calcul de l'impôt des contribuables avec plusieurs threads
69. i = 0
70. l_taxpayers = len(taxpayers)
71. while i < len(taxpayers):
72. # chaque thread va traiter de 1 à 4 contribuables
73. nb_taxpayers = min(l_taxpayers - i, random.randint(1, 4))
74. # la liste des contribuables traités par le thread
75. thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
76. # on incrémente i pour le thread suivant
77. i += nb_taxpayers
78. # on crée le thread
79. thread = threading.Thread(target=thread_function, args=(config, thread_taxpayers))
80. # on l'ajoute à la liste des threads du script principal
81. threads.append(thread)
82. # on lance le thread - cette opération est asynchrone - on n'attend pas le résultat du thread
83. thread.start()
84. # le thread principal attend la fin de tous les threads qu'il a lancés
85. for thread in threads:
86. thread.join()
87. # ici tous les threads ont fini leur travail - chacun a modifié un ou plusieurs objets [taxpayer]
88. # on enregistre les résultats dans le fichier jSON
89. dao.write_taxpayers_results(taxpayers)
90. # fin
91. except BaseException as erreur:
92. # affichage de l'erreur
93. print(f"L'erreur suivante s'est produite : {erreur}")
94. finally:
95. # on ferme le logueur
96. if logger:
97. # log de fin
98. logger.write("fin du calcul de l'impôt des contribuables\n")
99. # fermeture du logueur
100. logger.close()
101. # on a fini
102. print("Travail terminé...")
103. # fin des threads qui pourraient encore exister si on s'est arrêté sur erreur
104. sys.exit()
• lignes 4-11 : le client attend un paramètre lui indiquant le type de session, json ou xml, à utiliser avec le serveur ;
• lignes 13-15 : le client est configuré ;
• lignes 48-104 : ce code est connu. Il a été utilisé de nombreuses fois. Il répartit les contribuables pour lesquels on veut calculer
l’impôt sur plusieurs threads ;
• ligne 26 : la méthode [thread_function] est la méthode exécutée par chaque thread pour calculer l’impôt des contribuables
qui lui ont été attribués ;
• lignes 27-30 : chaque thread a sa propre couche [dao] ;
• le calcul de l’impôt se fait en quatre étapes :
o lignes 37-38 : initialisation d’une session, json ou xml, avec le serveur ;
o lignes 39-40 : authentification auprès du serveur ;
o lignes 41-42 : calcul de l’impôt ;
o lignes 43-44 : fermeture de la session avec le serveur ;
1. 2020-08-03
14:28:34.320751, MainThread : début du calcul de l'impôt des contribuables
2. 2020-08-03
14:28:34.328749, Thread-1 : début du calcul de l'impôt des 4 contribuables
3. 2020-08-03
14:28:34.328749, Thread-2 : début du calcul de l'impôt des 4 contribuables
4. 2020-08-03
14:28:34.333592, Thread-3 : début du calcul de l'impôt des 3 contribuables
5. 2020-08-03
14:28:34.368651, Thread-2 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le
type de réponse json"]}
6. 2020-08-03 14:28:34.375699, Thread-1 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le
type de réponse json"]}
7. 2020-08-03 14:28:34.377432, Thread-3 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le
type de réponse json"]}
8. 2020-08-03 14:28:34.385653, Thread-2 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification
réussie"}
9. 2020-08-03 14:28:34.392656, Thread-1 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification
réussie"}
10. 2020-08-03 14:28:34.396377, Thread-3 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification
réussie"}
11. 2020-08-03 14:28:34.406528,Thread-2 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "non",
"enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction":
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
597/755
0, "id": 1}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3,
"décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230,
"surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 0,
"salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}] }
12. 2020-08-03 14:28:34.413837, Thread-1 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui",
"enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0,
"id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14,
"décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0,
"surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2,
"salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}] }
13. 2020-08-03 14:28:34.416695, Thread-3 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui",
"enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id":
1}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45,
"décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842,
"surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 3}]}
14. 2020-08-03 14:28:34.425747, Thread-2 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
15. 2020-08-03 14:28:34.425747, Thread-2 : fin du calcul de l'impôt des 4 contribuables
16. 2020-08-03 14:28:34.428956, Thread-1 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
17. 2020-08-03 14:28:34.428956, Thread-1 : fin du calcul de l'impôt des 4 contribuables
18. 2020-08-03 14:28:34.428956, Thread-3 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
19. 2020-08-03 14:28:34.428956, Thread-3 : fin du calcul de l'impôt des 3 contribuables
20. 2020-08-03 14:28:34.428956, MainThread : fin du calcul de l'impôt des contribuables
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
598/755
38. 2020-08-03 14:32:48.595198, Thread-3 : fin du calcul de l'impôt des 4 contribuables
39. 2020-08-03 14:32:48.603351, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
40. <root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
41. 2020-08-03 14:32:48.603351, Thread-4 : fin du calcul de l'impôt des 3 contribuables
42. 2020-08-03 14:32:48.603351, MainThread : fin du calcul de l'impôt des contribuables
Le client [main2] permet de tester les URL [/init-session, /authentifier-utilisateur, /get-admindata, /fin-session] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
599/755
55. except BaseException as erreur:
56. # affichage de l'erreur
57. print(f"L'erreur suivante s'est produite : {erreur}")
58. finally:
59. # on ferme le logueur
60. if logger:
61. # log de fin
62. logger.write("fin du calcul de l'impôt des contribuables\n")
63. # fermeture du logueur
64. logger.close()
65. # on a fini
66. print("Travail terminé...")
• lignes 1-11 : on récupère le paramètre [json, xml] qui fixe le type de la session à établir avec le serveur ;
• lignes 13-15 : on configure le client ;
• lignes 30-33 : on crée une couche [dao] ;
• lignes 34-35 : avec elle, on récupère la liste des contribuables pour lesquels il faut calculer l’impôt ;
• on retrouve ensuite les quatre étapes du dialogue avec le serveur ;
o lignes 41-42 : une session est démarrée avec le serveur ;
o lignes 43-44 : on s’authentifie auprès du serveur ;
o lignes 45-46 : on demande au serveur les constantes fiscales permettant le calcul de l’impôt ;
o lignes 47-48 : on ferme la session avec le serveur ;
• lignes 49-52 : avec ces constantes, on est capables de calculer l’impôt des contribuables à l’aide de la couche [métier] locale
au client ;
• lignes 53-54 : on enregistre les résultats obtenus ;
Le client [main3] permet de tester les URL [/init-session, /calculer-impots, /get-simulations, /delete-simulation, /fin-
session] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
600/755
4. syntaxe = f"{sys.argv[0]} json / xml"
5. erreur = len(sys.argv) != 2
6. if not erreur:
7. session_type = sys.argv[1].lower()
8. erreur = session_type != "json" and session_type != "xml"
9. if erreur:
10. print(f"syntaxe : {syntaxe}")
11. sys.exit()
12.
13. # on configure l'application
14. import config
15. config = config.configure({"session_type": session_type})
16.
17. # dépendances
18. from ImpôtsError import ImpôtsError
19. import sys
20. from Logger import Logger
21.
22. logger = None
23. # code
24. try:
25. # logger
26. logger = Logger(config["logsFilename"])
27. # on le mémorise dans la config
28. config["logger"] = logger
29. # log de début
30. logger.write("début du calcul de l'impôt des contribuables\n")
31. # on récupère la factory de la couche [dao]
32. dao_factory = config["layers"]["dao_factory"]
33. # on crée une instance de la couche [dao]
34. dao = dao_factory.new_instance()
35. # lecture des données des contribuables
36. taxpayers = dao.get_taxpayers_data()["taxpayers"]
37. # des contribuables ?
38. if not taxpayers:
39. raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
40. # calcul de l'impôt des contribuables
41. # nombre de contribuables
42. nb_taxpayers = len(taxpayers)
43. # log
44. logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
45. # on initialise la session
46. dao.init_session(session_type)
47. # on s'authentifie
48. dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
49. # on calcule l'impôt des contribuables
50. dao.calculate_tax_in_bulk_mode(taxpayers)
51. # on demande la liste des simulations
52. simulations = dao.get_simulations()
53. # on en supprime une sur deux
54. for i in range(len(simulations)):
55. if i % 2 == 0:
56. # on supprime la simulation
57. dao.delete_simulation(simulations[i]['id'])
58. # fin de session
59. dao.end_session()
60. # consultez les logs pour voir les différents résultats (mode debug=True)
61. except BaseException as erreur:
62. # affichage de l'erreur
63. print(f"L'erreur suivante s'est produite : {erreur}")
64. finally:
65. # on ferme le logueur
66. if logger:
67. # log de fin
68. logger.write("fin du calcul de l'impôt des contribuables\n")
69. # fermeture du logueur
70. logger.close()
71. # on a fini
72. print("Travail terminé...")
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
601/755
3. 2020-08-03 15:01:52.734806, MainThread : {"action": "init-session", "état": 700, "réponse": ["session
démarrée avec le type de réponse json"]}
4. 2020-08-03 15:01:52.747961, MainThread : {"action": "authentifier-utilisateur", "état": 200, "réponse":
"Authentification réussie"}
5. 2020-08-03 15:01:52.765721, MainThread : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié":
"oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction":
0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14,
"décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0,
"surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2,
"salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4},
{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte":
0, "réduction": 0, "id": 5}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte":
2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 6}, {"marié": "oui", "enfants": 5, "salaire": 100000,
"impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 7}, {"marié": "non",
"enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0,
"id": 8}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte":
0, "réduction": 0, "id": 9}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte":
7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 10}, {"marié": "oui", "enfants": 3, "salaire":
200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 11}]}
6. 2020-08-03 15:01:52.785505, MainThread : {"action": "lister-simulations", "état": 500, "réponse":
[{"décôte": 0, "enfants": 2, "id": 1, "impôt": 2814, "marié": "oui", "réduction": 0, "salaire": 55555,
"surcôte": 0, "taux": 0.14}, {"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui",
"réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 720, "enfants": 3, "id": 3,
"impôt": 0, "marié": "oui", "réduction": 0, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0,
"enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480,
"taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0,
"salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200,
"marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants":
5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14},
{"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000,
"surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction":
0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210,
"marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants":
3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux":
0.41}]}
7. 2020-08-03 15:01:52.801475, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse":
[{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000,
"surcôte": 0, "taux": 0.14}, {"décôte": 720, "enfants": 3, "id": 3, "impôt": 0, "marié": "oui",
"réduction": 0, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4,
"impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41},
{"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000,
"surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui",
"réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7,
"impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte":
0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0,
"taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire":
30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non",
"réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11,
"impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
8. 2020-08-03 15:01:52.810129, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse":
[{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000,
"surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non",
"réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5,
"impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41},
{"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000,
"surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui",
"réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8,
"impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte":
0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0,
"taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0,
"salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842,
"marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
9. 2020-08-03 15:01:52.818803, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse":
[{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000,
"surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non",
"réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6,
"impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte":
0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0,
"taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0,
"salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié":
"oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10,
"impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45},
{"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000,
"surcôte": 17283, "taux": 0.41}]}
10. 2020-08-03 15:01:52.834604, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse":
[{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000,
"surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non",
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
602/755
"réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6,
"impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte":
0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0,
"taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire":
30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non",
"réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11,
"impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
11. 2020-08-03 15:01:52.843803, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse":
[{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000,
"surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non",
"réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6,
"impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte":
0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0,
"taux": 0.41}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0,
"salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842,
"marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
12. 2020-08-03 15:01:52.851855, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse":
[{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000,
"surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non",
"réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6,
"impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte":
0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0,
"taux": 0.41}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0,
"salaire": 200000, "surcôte": 7498, "taux": 0.45}]}
13. 2020-08-03 15:01:52.863165, MainThread : {"action": "fin-session", "état": 400, "réponse": "session
réinitialisée"}
14. 2020-08-03 15:01:52.863165, MainThread : fin du calcul de l'impôt des contribuables
1. import unittest
2.
3. from ImpôtsError import ImpôtsError
4. from Logger import Logger
5. from TaxPayer import TaxPayer
6.
7. class Test2HttpClientDaoWithSession(unittest.TestCase):
8.
9. def test_init_session_json(self) -> None:
10. print('test_init_session_json')
11. erreur = False
12. try:
13. dao.init_session('json')
14. except ImpôtsError as ex:
15. print(ex)
16. erreur = True
17. # il ne doit oas y avoir d'erreur
18. self.assertFalse(erreur)
19.
20. def test_init_session_xml(self) -> None:
21. print('test_init_session_xml')
22. erreur = False
23. try:
24. dao.init_session('xml')
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
603/755
25. except ImpôtsError as ex:
26. print(ex)
27. erreur = True
28. # in ne doit pas y avoir d'erreur
29. self.assertFalse(erreur)
30.
31. def test_init_session_xxx(self) -> None:
32. print('test_init_session_xxx')
33. erreur = False
34. try:
35. dao.init_session('xxx')
36. except ImpôtsError as ex:
37. print(ex)
38. erreur = True
39. # il doit y avoir une erreur
40. self.assertTrue(erreur)
41.
42. def test_authenticate_user_success(self) -> None:
43. print('test_authenticate_user_success')
44. # init session
45. dao.init_session('json')
46. # test
47. erreur = False
48. try:
49. dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
50. except ImpôtsError as ex:
51. print(ex)
52. erreur = True
53. # il ne doit pas y avoir d'erreur
54. self.assertFalse(erreur)
55.
56. def test_authenticate_user_failed(self) -> None:
57. print('test_authenticate_user_failed')
58. # init session
59. dao.init_session('json')
60. # test
61. erreur = False
62. try:
63. dao.authenticate_user('x', 'y')
64. except ImpôtsError as ex:
65. print(ex)
66. erreur = True
67. # il doit y avoir une erreur
68. self.assertTrue(erreur)
69.
70. def test_get_simulations(self) -> None:
71. print('test_get_simulations')
72. # init session
73. dao.init_session('json')
74. # authentification
75. dao.authenticate_user('admin', 'admin')
76. # calcul impôt
77. # {'marié': 'oui', 'enfants': 2, 'salaire': 55555,
78. # 'impôt': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
79. taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
80. dao.calculate_tax(taxpayer)
81. # get_simulations
82. simulations = dao.get_simulations()
83. # vérifications
84. # il doit y avoir 1 simulation
85. self.assertEqual(1, len(simulations))
86. simulation = simulations[0]
87. # vérification de l'impôt calculé
88. self.assertAlmostEqual(simulation['impôt'], 2815, delta=1)
89. self.assertEqual(simulation['décôte'], 0)
90. self.assertEqual(simulation['réduction'], 0)
91. self.assertAlmostEqual(simulation['taux'], 0.14, delta=0.01)
92. self.assertEqual(simulation['surcôte'], 0)
93.
94. def test_delete_simulation(self) -> None:
95. print('test_delete_simulation')
96. # init session
97. dao.init_session('json')
98. # authentification
99. dao.authenticate_user('admin', 'admin')
100. # calcul impôt
101. taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
604/755
102. dao.calculate_tax(taxpayer)
103. # get_simulations
104. simulations = dao.get_simulations()
105. # delete_simulation
106. dao.delete_simulation(simulations[0]['id'])
107. # get_simulations
108. simulations = dao.get_simulations()
109. # vérification - il ne doit plus y avoir de simulations
110. self.assertEqual(0, len(simulations))
111. # on supprime une simulation qui n'existe pas
112. erreur = False
113. try:
114. dao.delete_simulation(100)
115. except ImpôtsError as ex:
116. print(ex)
117. erreur = True
118. # il doit y avoir une erreur
119. self.assertTrue(erreur)
120.
121. if __name__ == '__main__':
122. # on configure l'application
123. import config
124. config = config.configure({})
125.
126. # logger
127. logger = Logger(config["logsFilename"])
128. # on le mémorise dans la config
129. config["logger"] = logger
130.
131. # couche [dao]
132. dao_factory = config['layers']['dao_factory']
133. dao = dao_factory.new_instance()
134.
135. # on exécute les méthodes de test
136. print("tests en cours...")
137. unittest.main()
• la couche [dao] envoie une requête au serveur, réceptionne sa réponse et la met en forme pour la rendre au code appelant.
Lorsque le serveur envoie un réponse avec un code de statut différent de 200, la couche [dao] lance une exception. Aussi un
certain nombre de tests consiste à savoir s’il y a eu exception ou pas ;
• lignes 9-18 : on initialise une session jSON. On ne doit pas avoir d’erreur ;
• lignes 20-29 : on initialise une session XML. On ne doit pas avoir d’erreur ;
• lignes 31-40 : on initialise une session avec un type incorrect. on doit avoir une erreur ;
• lignes 42-54 : on s’authentifie avec les bons identifiants. On ne doit pas avoir d’erreur ;
• lignes 56-68 : on s’authentifie avec des identifiants incorrects. On doit avoir une erreur ;
• lignes 70-92 : on fait un calcul d’impôt puis on demande la liste des simulations. On doit en avoir une. Par ailleurs on verifie
que cette simulation contient bien l’impôt demandé ;
• lignes 94-119 : on fait une simulation qu’on supprime ensuite. Puis on essaie ensuite de supprimer une simulation alors qu’il
n’y en a plus. On doit avoir une erreur ;
• lignes 121-137 : le test est lancé comme un script console classique ;
• lignes 122-124 : on configure l’application ;
• lignes 126-129 : on configure le logger. Cela nous permettra de suivre les logs ;
• lignes 131-133 : on instancie la couche [dao] qui va être testée ;
• lignes 135-137 : on lance les tests ;
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/impots/http-clients/07/tests/Test2HttpClientDaoWithSession.py
2. tests en cours...
3. test_authenticate_user_failed
4. ..MyException[35, ["Echec de l'authentification"]]
5. test_authenticate_user_success
6. test_delete_simulation
7. MyException[35, ["la simulation n° [100] n'existe pas"]]
8. test_get_simulations
9. test_init_session_json
10. test_init_session_xml
11. test_init_session_xxx
12. MyException[73, il n'y a pas de session valide en cours]
13. ----------------------------------------------------------------------
14. Ran 7 tests in 0.171s
15.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
605/755
16. OK
17.
18. Process finished with exit code 0
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
606/755
32 Le mode HTML de la version 12
Nous avions indiqué au début de la version 12 que nous allions développer l’application en plusieurs temps. Nous avions écrit :
• à partir des vues de l’application HTML, nous allons définir les actions que doit implémenter l’application web. Nous allons
ici utiliser les vues réelles mais ce pourrait être simplement des vues sur papier ;
• à partir de ces actions, nous allons définir les URL de service de l’application HTML ;
• nous allons implémenter ces URL de service avec un serveur délivrant du jSON. Cela permet de définir l’ossature du serveur
web sans se préoccuper des pages HTML à délivrer. Nous testerons ces URL de service avec Postman ;
• nous testerons ensuite notre serveur jSON avec un client console ;
• une fois que le serveur jSON aura été validé, nous passerons à l’ériture de l’application HTML ;
Nous avons un serveur jSON et XML opérationnel. On peut maintenant passer au serveur HTML. Nous allons voir que celui-ci
reprend toute l’architecture développée pour le serveur jSON / XML et leur ajoute une gestion de vues HTML.
• 1 - demande
Les URL demandées seront de la forme http://machine:port/action/param1/param2/… Le [Contrôleur principal] utilisera
un fichier de configuration pour " router " la demande vers le bon contrôleur. Pour cela, il utilisera le champ [action] de
l'URL. Le reste de l'URL [param1/param2/…] est formé de paramètres facultatifs qui seront transmis à l'action. Le C de
MVC est ici la chaîne [Contrôleur principal, Contrôleur / Action]. Si aucun contrôleur ne peut traiter l'action
demandée, le serveur web répondra que l'URL demandée n'a pas été trouvée.
• 2 - traitement
o l'action choisie [2a] peut exploiter les paramètres parami que le [Contrôleur principal] lui a transmis. Ceux-ci pourront
provenir de deux sources :
▪ du chemin [/param1/param2/…] de l'URL,
▪ de paramètres postés dans le corps de la requête du client ;
o dans le traitement de la demande de l'utilisateur, l'action peut avoir besoin de la couche [métier] [2b]. Une fois la
demande du client traitée, celle-ci peut appeler diverses réponses. Un exemple classique est :
▪ une réponse d'erreur si la demande n'a pu être traitée correctement ;
▪ une réponse de confirmation sinon ;
o le [Contrôleur / Action] rendra sa réponse [2c] au contrôleur principal ainsi qu’un code d’état. Ces codes d’état
représenteront de façon unique l’état dans lequel se trouve l’application. Ce sera soit un code de réussite, soit un code
d’erreur ;
• 3 - réponse
o selon que le client a demandé une réponse jSON, XML ou HTML, le [Contrôleur principal] instanciera [3a] le type
de réponse appropriée et demandera à celle-ci d’envoyer la réponse au client. Le [Contrôleur principal] lui transmettra
et la réponse et le code d’état fournis par le [Contrôleur / Action] qui a été exécuté ;
o si la réponse souhaitée est de type jSON ou XML, la réponse sélectionnée mettra en forme la réponse du [Contrôleur
/ Action] qu’on lui a donnée et l’enverra [3c]. Le client capable d’exploiter cette réponse peut être un script console
Python ou un script Javascript logé dans une page HTML ;
o si la réponse souhaitée est de type HTML, la réponse sélectionnée sélectionnera [3b] une des vues HTML [Vuei] à l’aide
du code d’état qu’on lui a donné. C’est le V de MVC. A un code d’état correspond une unique vue. Cette vue V va afficher
la réponse du [Contrôleur / Action] qui a été exécuté. Elle habille avec du HTML, CSS, Javascript les données de cette
réponse. On appelle ces données le modèle de la vue. C'est le M de MVC. Le client est alors le plus souvent un
navigateur ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
607/755
32.2 L’arborescence des scripts du serveur HTML
• l’action qui mène à cette 1ère vue est l’action [/init-session] [1] ;
• le clic sur le bouton [Valider] déclenche l’action [/authentifier-utilisateur] avec deux paramètres postés [2-3] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
608/755
La vue du calcul de l’impôt :
La 3ième vue est celle des simulations faites par l’utilisateur authentifié :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
609/755
• en [1] : l’utilisateur a tapé lui-même l’URL. Or dans cet exemple il n’y avait pas de simulations. On reçoit donc le message
d’erreur [2]. On connaît ce message. On l’avait en jSON / XML. On appellera ce type d’erreur, erreur inattendue, car elle ne
peut pas se produire en utilisation normale de l’application. C’est lorsque l’utilisateur tape lui-même les URL qu’elles peuvent
de produire ;
• en cas d’erreur inattendue, les liens [3-5] permettent de revenir à l’une des trois autres vues ;
Ces différentes URL de service seront également utilisées pour le serveur HTML.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
610/755
Dans une session HTML, la page affichée suite à une action, dépend du code d’état rendu par le cntrôleur. Cette dépendance est
matérialisée dans la configuration [config] de la façon suivante :
1. # les vues HTML et leurs modèles dépendent de l'état rendu par le contrôleur
2. "views": [
3. {
4. # vue d'authentification
5. "états": [
6. # /init-session réussite
7. 700,
8. # /authentifier-utilisateur échec
9. 201
10. ],
11. "view_name": "views/vue-authentification.html",
12. "model_for_view": ModelForAuthentificationView()
13. },
14. {
15. # vue du calcul de l'impôt
16. "états": [
17. # /authentifier-utilisateur réussite
18. 200,
19. # /calculer-impot réussite
20. 300,
21. # /calculer-impot échec
22. 301,
23. # /afficher-calcul-impot
24. 800
25. ],
26. "view_name": "views/vue-calcul-impot.html",
27. "model_for_view": ModelForCalculImpotView()
28. },
29. {
30. # vue de la liste des simulations
31. "états": [
32. # /lister-simulations
33. 500,
34. # /supprimer-simulation
35. 600
36. ],
37. "view_name": "views/vue-liste-simulations.html",
38. "model_for_view": ModelForListeSimulationsView()
39. }
40. ],
41.
42. # vue des erreurs inattendues
43. "view-erreurs": {
44. "view_name": "views/vue-erreurs.html",
45. "model_for_view": ModelForErreursView()
46. },
47.
48. # redirections
49. "redirections": [
50. {
51. "états": [
52. 400, # /fin-session réussite
53. ],
54. # redirection vers
55. "to": "/init-session/html",
56. }
57. ],
58. }
• ligne 2-40 : [views] est une liste de vues. Raisonnons sur la vue des lignes 3-13 :
o ligne 11 : la vue V affichée ;
o ligne 12 : l’instance de classe chargée de générer le modèle M de cette vue ;
o lignes 5-10 : les états qui mènent à cette vue ;
• lignes 3-13 : la vue d’authentification ;
• lignes 14-28 : la vue du calcul de l’impôt ;
• lignes 29-39 : la vue de la liste des simulations ;
• lignes 42-46 : la vue des erreurs inattendues ;
• lignes 49-57 : certains états mènent à une vue via une redirection. C’est le cas de l’état 400 qui est celui de l’action [/fin-
session] réussie. Il faut alors rediriger le client vers l’action [http://machine:port/chemin/init-session/html] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
611/755
32.5 La vue d’authentification
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
612/755
7. <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
8. <!-- Bootstrap CSS -->
9. <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
10. <title>Application impôts</title>
11. </head>
12.
13. <body>
14. <div class="container">
15. <!-- bandeau -->
16. {% include "fragments/v-bandeau.html" %}
17. <!-- ligne à deux colonnes -->
18. <div class="row">
19. <div class="col-md-9">
20. {% include "fragments/v-authentification.html" %}
21. </div>
22. </div>
23. <!-- si erreur - on affiche une alerte d'erreur -->
24. {% if modèle.error %}
25. <div class="row">
26. <div class="col-md-9">
27. <div class="alert alert-danger" role="alert">
28. Les erreurs suivantes se sont produites :
29. <ul>{{modèle.erreurs|safe}}</ul>
30. </div>
31. </div>
32. </div>
33. {% endif %}
34. </div>
35. </body>
36. </html>
Commentaires
• lignes 13-35 : le corps de la page web est encapsulé dans les balises <body></body> ;
• lignes 14-34 : la balise <div> délimite une section de la page affichée. Les attributs [class] utilisés dans la vue se réfèrent tous
au framework CSS Bootstrap. La balise <div class=’container’> (ligne 14) délimite un conteneur Bootstrap ;
• ligne 26 : on inclut le fragment [v-bandeau.html]. Ce fragment génère le bandeau [1] de la page. Nous le décrirons bientôt ;
• lignes 18-22 : la balise <div class=’row’> délimite une ligne Bootstrap. Ces lignes sont constituées de 12 colonnes ;
• ligne 19 : la balise <div class=’col-md-9’> délimite une section de 9 colonnes ;
• ligne 20 : on inclut le fragment [v-authentification.html] qui affiche le formulaire d’authentification [2] de la page. Nous
le décrirons bientôt ;
• lignes 24-33 : le code HTML de ces lignes n’est utilisé que si [modèle.error] vaut True. Nous procèderons toujours ainsi : le
modèle d’une vue HTML sera encapsulé dans un dictionnaire [modèle] ;
• lignes 24-33 : l’authentification échoue si l’utilisateur entre des identifiants incorrects. Dans ce cas, la vue d’authentification est
réaffichée avec un message d’erreur. L’attribut [modèle.error] indique s’il faut afficher ce message d’erreur ;
• lignes 27-30 : délimitent une zone à fond rose (class="alert alert-danger") (ligne 27) ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
613/755
• ligne 28 : un texte ;
• ligne 29 : la balise HTML <ul> (unordered list) affiche une liste à puces. Chaque élément de la liste doit avoir la syntaxe
<li>élément</li>. On affiche ici la valeur de [modèle.erreurs]. Cette valeur est filtrée (présence de |) par le filtre [safe].
Par défaut, lorsqu’une chaîne de caractères doit être envoyée au navigateur, Flask ‘neutralise’ toutes les balises HTML qui
pourraient s’y trouver afin que le navigateur ne les interprète pas. Mais parfois on veut les interpréter. Ce sera ici le cas où la
chaîne [modèle.erreurs] contiendra des balises HTML <li> et </li> qui servent à délimiter un élément de la liste. Dans ce
cas, on utilise le filtre [safe] qui dit à Flask que la chaîne à afficher est sûre (safe) et que donc il ne doit pas neutraliser les
balises HTML qu’il y trouvera ;
Commentaires
• lignes 2-13 : le bandeau est encapsulé dans une section Bootstrap de type Jumbotron [<div class="jumbotron">]. Cette classe
Bootstrap stylise d’une façon particulière le contenu affiché pour le faire ressortir ;
• lignes 3-12 : une ligne Bootstrap ;
• lignes 4-6 : une image [img] est placée dans les quatre premières colonnes de la ligne ;
• ligne 5 : la syntaxe :
{{ url_for('static', filename='images/logo.jpg') }}
utilise la fonction [url_for] de Flask. Ici, sa valeur sera l’URL du fichier [images/logo.pg] du dossier [static] ;
• lignes 7-11 : les 8 autres colonnes de la ligne (on rappelle qu’il y en a 12 au total) serviront à placer un texte (ligne 9) en gros
caractères (<h1>, lignes 8-10) ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
614/755
32.5.3 Le fragment [v-authentification.html]
Le fragment [v-authentification.html] affiche le formulaire d’authentification de l’application web :
1. <!-- formulaire HTML - on poste ses valeurs avec l'action [authentifier-utilisateur] -->
2. <form method="post" action="/authentifier-utilisateur">
3.
4. <!-- titre -->
5. <div class="alert alert-primary" role="alert">
6. <h4>Veuillez vous authentifier</h4>
7. </div>
8.
9. <!-- formulaire Bootstrap -->
10. <fieldset class="form-group">
11. <!-- 1ère ligne -->
12. <div class="form-group row">
13. <!-- libellé -->
14. <label for="user" class="col-md-3 col-form-label">Nom d'utilisateur</label>
15. <div class="col-md-4">
16. <!-- zone de saisie texte -->
17. <input type="text" class="form-control" id="user" name="user"
18. placeholder="Nom d'utilisateur" value="{{ modèle.login }}" required>
19. </div>
20. </div>
21. <!-- 2ième ligne -->
22. <div class="form-group row">
23. <!-- libellé -->
24. <label for="password" class="col-md-3 col-form-label">Mot de passe</label>
25. <!-- zone de saisie texte -->
26. <div class="col-md-4">
27. <input type="password" class="form-control" id="password" name="password"
28. placeholder="Mot de passe" required>
29. </div>
30. </div>
31. <!-- bouton de type [submit] sur une 3ième ligne -->
32. <div class="form-group row">
33. <div class="col-md-2">
34. <button type="submit" class="btn btn-primary">Valider</button>
35. </div>
36. </div>
37. </fieldset>
38.
39. </form>
Commentaires
• lignes 2-39 : la balise <form> délimite un formulaire HTML. Celui-ci a en général les caractéristiques suivantes :
o il définit des zones de saisies (balises <input> des lignes 17 et 27 ;
o il a un bouton de type [submit] (ligne 34) qui envoie les valeurs saisies à l’URL indiquée dans l’attribut [action] de la
balise [form] (ligne 2). La méthode HTTP utilisée pour requêter cette URL est précisée dans l’attribut [method] de la
balise [form] (ligne 2) ;
o ici, lorsque l’utilisateur va cliquer sur le bouton [Valider] (ligne 34), le navigateur va poster (ligne 2) les valeurs saisies
dans le formulaire à l’URL [/authentifier-utilisateur] (ligne 2) ;
o les valeurs postées sont les valeurs saisies par l’utilisateur dans les zones de saisie des lignes 17 et 27. Elles seront postées
dans le corps de la requête HTTP que fera le navigateur sous la forme [x-www-forl-urlencoded]. Les noms des
paramètres [user, password] sont ceux des attributs [name] des zones de saisie des lignes 17 et 27 ;
• ligne 5-7 : une section Bootstrap pour afficher un titre dans un fond bleu :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
615/755
• lignes 10-37 : un formulaire Bootstrap. Tous les éléments du formulaire vont alors être stylisés d’une certaine façon ;
• lignes 12-20 : définissent la 1re ligne Bootstrap du formulaire :
• la ligne 14 définit le libellé [1] sur trois colonnes. L’attribut [for] de la balise [label] relie le libellé à l’attribut [id] de la zone
de saisie de la ligne 17 ;
• lignes 15-19 : met la zone de saisie dans un ensemble de quatre colonnes ;
• lignes 17-18 : la balise HTML [input] décrit une zone de saisie. Elle a plusieurs paramètres :
o [type=’text’] : c’est une zone de saisie texte. On peut y taper n’importe quoi ;
o [class=’form-control’] : style Bootstrap pour la zone de saisie ;
o [id=’user’] : identifiant de la zone de saisie. Cet identifiant est en général utilisé par le CSS et le code Javascript ;
o [name=’user’] : nom de la zone de saisie. C’est sous ce nom que la valeur saisie par l’utilisateur sera postée par le
navigateur [user=xx] ;
o [placeholder=’invite’] : le texte affiché dans la zone de saisie lorsque l’utilisateur n’a encore rien tapé ;
o [value=’valeur’] : le texte ‘valeur’ sera affiché dans la zone de saisie dès que celle-ci sera affichée, avant donc que
l’utilisateur ne saisisse autre chose. Ce mécanisme est utilisé en cas d’erreur pour afficher la saisie qui a provoqué l’erreur.
Ici cette valeur sera la valeur de la variable [modèle.login] ;
o [required] : exige que l’utilisateur mette une valeur pour que le formulaire puisse être envoyé au serveur :
• lignes 21-30 : un code analogue pour la saisie du mot de passe ;
• ligne 27 : [type=’password’] fait qu’on a une zone de saisie texte (on peut taper n’importe quoi) mais les caractètres tapés
sont cachés :
• ligne 34 : parce qu’il a l’attribut [type=submit], un clic sur ce bouton déclenche l’envoi au serveur par le navigateur des valeurs
saisies comme il a été expliqué précédemment. L’attribut CSS [class="btn btn-primary"] affiche un bouton bleu :
Il nous reste à expliquer une dernière chose. Ligne 2, l’attribut [action="/authentifier-utilisateur"] définit une URL
incomplète (elle ne commence pas par http://machine:port/chemin). Dans notre exemple, toutes les URL de l’application sont de
la forme [http://machine:port/chemin/action/param1/param2/..] où [http://machine:port/chemin] est la racine des URL de
service. Dans [action="/authentifier-utilisateur"] nous avons une URL absolue ç-à-d mesurée à partir de la racine des URL.
L’URL complète du POST est donc [http://machine:port/chemin/authentifier-utilisateur] et c’est ce qu’utilisera le
navigateur.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
616/755
32.5.4 Tests visuels
On peut procéder aux tests des vues bien avant leur intégration dans l’application. Il s’agit ici de tester leur aspect visuel. Nous
Pour tester la vue V [vue-authentification.html], il nous faut créer le modèle M de données qu’elle va afficher. On le fait avec le
script [test_vue_authentification.py] :
Commentaires
• lignes 1-3 : on crée une application Flask dont le seul but est d’afficher la vue [vue-authentification.html] (ligne 22) ;
• ligne 7 : l’application n’a qu’une unique URL de service ;
• lignes 9-20 : la vue d’authentification a des parties dynamiques contrôlées par l’objet [modèle]. On appelle cet objet le modèle
de la vue. Selon l’une des deux définitions données pour le sigle MVC, on a là le M du MVC. Lors de la définition de la vue
[vue-authentification.html], nous avions identifié trois valeurs dynamiques :
o [modèle.error] : booléen indiquant s’il faut afficher un message d’erreur ;
o [modèle.erreurs] : une liste HTML de messages d’erreur ;
o [modèle.login] : le login d’un utilisateur ;
Il nous faut donc définir ces trois valeurs dynamiques.
• lignes 9-20 : on définit les trois éléments dynamiques de la vue d’authentification ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
617/755
On poursuit ces tests visuels jusqu’à être satisfait du résultat.
• lignes 8-10 : la méthode [get_model_for_view] est chargée de produire un modèle de vue encapsulé dans un dictionnaire. Elle
reçoit pour cela les informations suivantes :
o [request, session, config] sont les mêmes paramètres utilisés par le contrôleur de l’action. Ils sont donc également
transmis au modèle ;
o le contrôleur a produit un résultat [résultat] qui est également transmis au modèle. Ce résultat contient un élément
important [état] qui indique comme s’est passée l’exécution de l’action en cours. Le modèle va utiliser cette information ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
618/755
Nous avons vu que dans la configuration [config] de l’application, les codes d’état rendus par les contrôleurs sont utilisés pour
désigner la vue HTML à afficher :
1. # les vues HTML et leurs modèles dépendent de l'état rendu par le contrôleur
2. "views": [
3. {
4. # vue d'authentification
5. "états": [
6. # /init-session réussite
7. 700,
8. # /authentifier-utilisateur échec
9. 201
10. ],
11. "view_name": "views/vue-authentification.html",
12. "model_for_view": ModelForAuthentificationView()
13. },
14. {
15. # vue du calcul de l'impôt
16. "états": [
17. # /authentifier-utilisateur réussite
18. 200,
19. # /calculer-impot réussite
20. 300,
21. # /calculer-impot échec
22. 301,
23. # /afficher-calcul-impot
24. 800
25. ],
26. "view_name": "views/vue-calcul-impot.html",
27. "model_for_view": ModelForCalculImpotView()
28. },
29. {
30. # vue de la liste des simulations
31. "états": [
32. # /lister-simulations
33. 500,
34. # /supprimer-simulation
35. 600
36. ],
37. "view_name": "views/vue-liste-simulations.html",
38. "model_for_view": ModelForListeSimulationsView()
39. }
40. ],
41. # vue des erreurs inattendues
42. "view-erreurs": {
43. "view_name": "views/vue-erreurs.html",
44. "model_for_view": ModelForErreursView()
45. },
46. # redirections
47. "redirections": [
48. {
49. "états": [
50. 400, # /fin-session réussite
51. ],
52. # redirection vers
53. "to": "/init-session/html",
54. }
55. ],
56. }
Ce sont donc les codes d’état [700, 201] (lignes 7 et 9) qui font afficher la vue d’authentification. Pour retrouver la signification de
ces codes, on peut s’aider des tests [Postman] réalisés sur l’application jSON :
• [init-session-json-700] : 700 est le code d’état à l’issue d’une action [init-session] réussie : on présente alors le
formulaire d’authentification vide ;
• [authentifier-utilisateur-201] : 201 est le code d’état à l’issue d’une action [authentifier-utilisateur] ayant échoué
(identifiants non reconnus) : on présente alors le formulaire d’authentification pour qu’il soit corrigé ;
Maintenant que nous savons à quels moments doit être affiché le formulaire d’authentification, on peut calculer son modèle dans
[ModelForAuthentificationView] (ligne 12) :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
619/755
2. from werkzeug.local import LocalProxy
3.
4. from InterfaceModelForView import InterfaceModelForView
5.
6. class ModelForAuthentificationView(InterfaceModelForView):
7.
8. def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
9. # on encapsule les données de la pagé dans modèle
10. modèle = {}
11. # état de l'application
12. état = résultat["état"]
13. # le modèle dépend de l'état
14. if état == 700:
15. # cas de l'affichage du formulaire vide
16. modèle["login"] = ""
17. # il n'y pas d'erreur à afficher
18. modèle["error"] = False
19. elif état == 201:
20. # authentification erronée
21. # on réaffiche l'utilisateur initialement saisi
22. modèle["login"] = request.form.get("user")
23. # il y a une erreur à afficher
24. modèle["error"] = True
25. # liste HTML de msg d'erreur
26. erreurs = ""
27. for erreur in résultat["réponse"]:
28. erreurs += f"<li>{erreur}</li>"
29. modèle["erreurs"] = erreurs
30.
31. # on rend le modèle
32. return modèle
Commentaires
• ligne 8 : la méthode [get_model_for_view] de la vue d’authentification doit fournir un dictionnaire avec trois clés [error,
erreurs, login]. Ce calcul se fait à partir du code d’état rendu par le contrôleur de l’action ;
• ligne 12 : on récupère le code d’état rendu par le contrôleur qui a traité l’action en cours ;
• lignes 14-29 : le modèle dépend de ce code d’état ;
• lignes 15-18 : cas où on doit afficher un formulaire d’authentification vierge ;
• lignes 20-29 : cas de l’authentification erronée : on raffiche l’identifiant saisi pour l’utilisateur et on affiche un message d’erreur.
L’utilisateur au clavier peut alors réessayer une autre authentification ;
• ligne 22 : l’identifiant initialement saisi par l’utilisateur peut être retrouvé dans la requête du client ;
• ligne 24 : on signale qu’il y a des erreurs à afficher ;
• lignes 26-29 : en cas d’erreur, résultat[‘réponse’] contient une liste d’erreurs ;
En [3a], un type de réponse (jSON, XML, HTML) est choisi. Nous avons vu comment les réponses jSON et XML étaient générées
mais pas encore les réponses HTML. Celles-ci sont générées par la classe [HtmlResponse] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
620/755
Rappelons comment dans le script principal [main] le type de réponse à faire à l’utilisateur est déterminé :
1. ….
2. # on construit la réponse à envoyer
3. response_builder = config["responses"][type_response]
4. response, status_code = response_builder \
5. .build_http_response(request, session, config, status_code, résultat)
6. # on envoie la réponse
7. return response, status_code
C’est donc la classe [HtmlResponse] qui génère la réponse HTML. Son code est le suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
621/755
45. # on rend la vue d'erreurs
46. view_config = config["view-erreurs"]
47.
48. # on calcule le modèle de la vue à afficher
49. model_for_view = view_config["model_for_view"]
50. modèle = model_for_view.get_model_for_view(request, session, config, résultat)
51. # on génère le code HTML de la réponse
52. html = render_template(view_config["view_name"], modèle=modèle)
53. # on construit la réponse HTTP
54. response = make_response(html)
55. response.headers['Content-Type'] = 'text/html; charset=utf-8'
56. # on rend le résultat
57. return response, status_code
• line 11 : la méthode [build_http_response] chargée de générer la réponse HTML reçoit les paramètres suivants :
o [request, session, dict] : ce sont les paramètres utilisés par le contrôleur pour traiter l’action en cours ;
o [status_code, résultat] sont les deux résultats produits par ce même contrôleur ;
• ligne 14 : comme nous l’avons dit, la réponse HTML du serveur dépend du code d’état contenu dans le dictionnaire
[résultat] ;
• lignes 16-22 : on gère d’abord les redirections. Pour l’instant nous allons ignorer ce cas jusqu’au moment où on rencontrera
un exemple de redirection. On notera que les redirections sont typiquement un cas d’usage du serveur HTML. On ne rencontre
pas ce cas avec les serveurs jSON ouXML ;
• lignes 24-41 : on cherche parmi les vues celle dont la liste [états] contient l’état recherché ;
• lignes 42-46 : si aucune vue n’a été trouvée, il s’agit d’une erreur inattendue. Prenons un exemple. Dans le fonctionnement
normal de l’application, l’action [/supprimer-simulation] ne doit jamais boguer. En effet, nous allons voir que cette
suppression de simulations se fait à partir de liens générés par le code. Ces liens sont corrects et ne peuvent mener à une erreur.
Cependant, nous l’avons vu, l’utilisateur peut taper directement l’URL [/supprimer-simulation/id] et provoquer ainsi une
erreur. Dans ce cas, le contrôleur [SupprimerSimulationController] rend un code d’état 601. Or ce code d’état ne se trouve
pas dans la liste des codes d’état menant à l’affichage d’une page HTML. Ce sera donc la vue d’erreur qui sera affichée. Elle
est définie ainsi dans la configuration :
• ligne 49 : une fois qu’on connaît la vue à afficher, on récupère la classe générant son modèle. Elle aussi se trouve dans la
configuration [config] ;
• ligne 50 : une fois cette classe trouvée, on génère le modèle de la vue ;
• ligne 52 : une fois le modèle M de la vue V calculé, on peut générer le code HTML de la vue ;
• lignes 54-55 : on construit la réponse HTTP de la réponse avec un corps HTML ;
• lignes 56-57 : on rend la réponse HTTP avec son code de statut ;
• [init-session-html-700] : 700 est le code d’état à l’issue d’une action [init-session] réussie : on présente alors le
formulaire d’authentification vide ;
• [authentifier-utilisateur-201] : 201 est le code d’état à l’issue d’une action [authentifier-utilisateur] ayant échoué
(identifiants non reconnus) : on présente alors le formulaire d’authentification pour qu’il soit corrigé ;
Il suffit de les réutiliser et de voir s’ils affichent bien la vue d’authentification. On montre ici deux cas :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
622/755
La réponse est la suivante :
• en [3], le lien que Postman n’a pas chargé. Il a affiché la valeur de l’attribut [alt=alternative] qui est affichée lorsque l’image
ne peut être chargée. Ici c’est plutôt que Postman n’a pas voulu la charger. On peut le vérifier en demandant l’URL
[http://localhost :5000/static/images.logo.jpg] avec Postman :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
623/755
Cas 2 : [authentifier-utilisateur-201], authentification erronée
Maintenant, faisons une authentification erronée, après avoit fait une initialisation de session HTML réussie :
Ci-dessus :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
624/755
• en [4], un message d’erreur est affiché ;
• en [3], l’utilisateur erroné a été réaffiché ;
32.5.8 Conclusion
Nous avons pu tester la vue [vue-authentification.html] sans avoir écrit les autres vues. Cela a été possible parce que :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
625/755
La vue a trois parties :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
626/755
38. {{modèle.impôt}}</br>
39. {{modèle.décôte}}</br>
40. {{modèle.réduction}}</br>
41. {{modèle.surcôte}}</br>
42. {{modèle.taux}}</br>
43. </div>
44. </div>
45. </div>
46. {% endif %}
47.
48. {% if modèle.error %}
49. <!-- liste des erreurs sur 9 colonnes -->
50. <div class="row">
51. <div class="col-md-3">
52.
53. </div>
54. <div class="col-md-9">
55. <div class="alert alert-danger" role="alert">
56. Les erreurs suivantes se sont produites :
57. <ul>{{modèle.erreurs | safe}}</ul>
58. </div>
59. </div>
60. </div>
61. {% endif %}
62. </div>
63. </body>
64. </html>
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
627/755
10. <fieldset class="form-group">
11. <!-- première ligne sur 9 colonnes -->
12. <div class="row">
13. <!-- libellé sur 4 colonnes -->
14. <legend class="col-form-label col-md-4 pt-0">Etes-vous marié(e) ou pacsé(e)?</legend>
15. <!-- boutons radio sur 5 colonnes-->
16. <div class="col-md-5">
17. <div class="form-check">
18. <input class="form-check-
input" type="radio" name="marié" id="gridRadios1" value="oui" {{modèle.checkedOui}}>
19. <label class="form-check-label" for="gridRadios1">
20. Oui
21. </label>
22. </div>
23. <div class="form-check">
24. <input class="form-check-
input" type="radio" name="marié" id="gridRadios2" value="non" {{modèle.checkedNon}}>
25. <label class="form-check-label" for="gridRadios2">
26. Non
27. </label>
28. </div>
29. </div>
30. </div>
31. <!-- deuxième ligne sur 9 colonnes -->
32. <div class="form-group row">
33. <!-- libellé sur 4 colonnes -->
34. <label for="enfants" class="col-md-4 col-form-label">Nombre d'enfants à charge</label>
35. <!-- zone de saisie numérique du nombre d'enfants sur 5 colonnes -->
36. <div class="col-md-5">
37. <input type="number" min="0" step="1" class="form-
control" id="enfants" name="enfants" placeholder="Nombre d'enfants à charge" value="{{modèle.enfants}}"
required>
38. </div>
39. </div>
40. <!-- troisème ligne sur 9 colonnes -->
41. <div class="form-group row">
42. <!-- libellé sur 4 colonnes -->
43. <label for="salaire" class="col-md-4 col-form-label">Salaire annuel net imposable</label>
44. <!-- zone de saisie numérique pour le salaire sur 5 colonnes -->
45. <div class="col-md-5">
46. <input type="number" min="0" step="1" class="form-
control" id="salaire" name="salaire" placeholder="Salaire annuel net imposable" aria-
describedby="salaireHelp" value="{{modèle.salaire}}" required>
47. <small id="salaireHelp" class="form-text text-muted">Arrondissez à l'euro inférieur</small>
48. </div>
49. </div>
50. <!-- quatrième ligne, bouton [submit] sur 5 colonnes -->
51. <div class="form-group row">
52. <div class="col-md-5">
53. <button type="submit" class="btn btn-primary">Valider</button>
54. </div>
55. </div>
56. </fieldset>
57.
58. </form>
Commentaires
• ligne 2 : le formulaire HTML sera posté (attribut [method]) à l’URL [/calculer-impot] (attribut [action]). Les valeurs postées
seront les valeurs des zones de saisie :
o la valeur du bouton radio coché sous la forme :
▪ [marié=oui] si le bouton radio [Oui] est coché (lignes 17-22). [marié] est la valeur de l’attribut [name] de la ligne
18, [oui] la valeur de l’attribut [value] de la ligne 18 ;
▪ [marié=non] si le bouton radio [Non] est coché (lignes 23-28). [marié] est la valeur de l’attribut [name] de la ligne
24, [non] la valeur de l’attribut [value] de la ligne 24 ;
o la valeur de la zone de saisie numérique de la ligne 37 sous la forme [enfants=xx] où [enfants] est la valeur de l’attribut
[name] de la ligne 37, et [xx] la valeur saisie par l’utilisateur au clavier ;
o la valeur de la zone de saisie numérique de la ligne 46 sous la forme [salaire=xx] où [salaire] est la valeur de l’attribut
[name] de la ligne 46, et [xx] la valeur saisie par l’utilisateur au clavier ;
o les valeurs saisies seront postées lorsque l’utilisateur cliquera sur le bouton de type [submit] de la ligne 53 ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
628/755
• lignes 16-30 : les deux boutons radio :
Les deux boutons radio font partie du même groupe de boutons radio car ils ont le même attribut [name] (lignes 18, 24).
Le navigateur s’assure que dans un groupe de boutons radio, un seul est coché à un moment donné. Donc cliquer sur l’un
désactive celui qui était coché auparavant ;
• ce sont des boutons radio à cause de l’attribut [type="radio"] (lignes 18, 24) ;
• à l’affichage du formulaire (avant saisie), l’un des boutons radio devra être coché : il suffit pour cela d’ajouter l’attribut
[checked=’checked’] à la balise <input type="radio"> concernée. Cela est réalisé avec des variables dynamiques :
o [modèle.checkedOui] à la ligne 18 ;
o [modèle->checkedNon] à la ligne 24 ;
Ces variables feront partie du modèle de la vue.
• ligne 37 : une zone de saisie numérique [type="number"] avec une valeur minimale de 0 [min="0"]. Dans les navigateurs
récents, cela signifie que l’utilisateur ne pourra saisir qu’un nombre >=0. Sur ces mêmes navigateurs récents, la saisie peut être
faite avec un variateur qu’on peut cliquer à la hausse ou à la baisse. L’attribut [step="1"] de la ligne 37 indique que le variateur
opèrera avec des sauts de 1 unité. Cela a pour conséquence que le variateur ne prendra pour valeur que des entiers progressant
de 0 à n avec un pas de 1. Pour la saisie manuelle, cela signifie que les nombres avec virgule ne seront pas acceptés ;
• ligne 37 : lors de certains affichages, la zone de saisie des enfants devra être préremplie avec la dernière saisie faite dans cette
zone. On utilise pour cela l’attribut [value] qui fixe la valeur à afficher dans la zone de saisie. Cette valeur sera dynamique et
générée par la variable [modèle.enfants] ;
• ligne 37 : l’attribut [required] force l’utilisateur à saisir une donnée pour que le formulaire soit validé ;
• ligne 46 : mêmes explications pour la saisie du salaire que pour celle des enfants ;
• ligne 53 : le bouton de type [submit] qui déclenche le POST des valeurs saisies à l’URL [/calculer-impot] (ligne 2) ;
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
629/755
• lignes 2-7 : la balise HTML [nav] encadre une portion de document HTML présentant des liens de navigation vers d’autres
documents ;
• ligne 5 : la balise HTML [a] introduit un lien de navigation :
o [optionMenu.url] : est l’URL vers laquelle on navigue lorsqu’on clique sur le lien [optionMenu.text]. C’est alors une
opération [GET optionMenu.url] qui est faite par le navigateur. [optionMenu.url] sera une URL absolue mesurée à partir
de la racine [http://machine :port/chemin] de l’application. Ainsi en [1], on créera le lien :
• lignes 2, 7 : les classes CSS [nav, flex-column, nav-link] sont des classes Bootstrap qui donnent son apparence au menu ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
630/755
Commentaires
• lignes 9-34 : on initialise toutes les parties dynamiques de la vue [vue-calcul-impot.html] et des fragments [v-calcul-
impot.html] et [v-menu.html] ;
• ligne 36 : on affiche la vue [vue-calcul-impot.html] ;
On travaille sur cette vue jusqu’à ce que le résultat obtenu visuellement nous convienne. On peut ensuite passer à l’intégration de la
vue dans l’application web en cours d’écriture.
1. {
2. # vue du calcul de l'impôt
3. "états": [
4. # /authentifier-utilisateur réussite
5. 200,
6. # /calculer-impot réussite
7. 300,
8. # /calculer-impot échec
9. 301,
10. # /afficher-calcul-impot
11. 800
12. ],
13. "view_name": "views/vue-calcul-impot.html",
14. "model_for_view": ModelForCalculImpotView()
15. },
Ce sont donc les codes d’état [200, 300, 301, 800] qui font afficher la vue de calcul de l’impôt. Pour retrouver la signification de
ces codes, on peut s’aider des tests [Postman] réalisés sur l’application jSON :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
631/755
• [authentifier-utilisateur-200] : 200 est le code d’état à l’issue d’une action [authentifier-utilisateur] réussie : on
présente alors le formulaire de calcul d’impôt vide ;
• [calculer-impot-300] : 300 est le code d’état à l’issue d’une action [calculer-impot] réussie. On affiche alors le formulaire
de calcul avec les données qui y ont été saisies et le montant de l’impôt. L’utilisateur peut alors refaire un autre calcul ;
• le code d’état [301] est celui obtenu pour un calcul d’impôt erroné ;
• le code d’état [800] sera présenté ultérieurement. Nous ne l’avons pas encore rencontré ;
Maintenant que nous savons à quels moments doit être affiché le formulaire de calcul de l’impôt, on peut calculer son modèle dans
la classe [ModelForCalculImpotView] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
632/755
53. # on rend le modèle
54. return modèle
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
633/755
La réponse du serveur est la suivante :
Maintenant essayons un cas inattendu, celui où il manque des paramètres au POST. Ce cas n’est pas possible dans le fonctionneent
normal de l’application. Mais n’importe qui peut ‘bricoler’ une requête HTTP comme nous le faisons maintenant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
634/755
• en [3], le message d’erreur du serveur ;
Dans cette application, nous avions le choix. Nous pouvions donner à ce cas d’erreur un code d’état qui envoie la page des erreurs
inattendues. Nous avons dans cette application choisi pour chaque contrôleur deux codes d’état :
o [xx0] : pour une réussite ;
o [xx1] : pour un échec ;
Pour les cas d’échec on peut diversifier les codes d’état pour avoir une gestion plus fine des erreurs. Nous aurions pu avoir par
exemple :
o [xx1] : pour des erreurs à afficher sur la page qui a provoqué l’erreur ;
o [xx2] : pour des erreurs inattendues dans le cadre d’une utilisation normale de l’application ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
635/755
La vue générée par le code [vue-liste-simulations.html] a trois parties :
Commentaires
Nous avons déjà commenté deux des trois fragments de cette vue :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
636/755
Le fragment [v-liste-simulations.html] est le suivant :
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
637/755
• lignes 49-57 : chaque balise <td> (table data) définit une colonne de la ligne ;
• ligne 34 : la liste des simulations sera trouvée dans le modèle [modèle.simulations] qui est une liste de dictionnaires ;
• ligne 57 : un lien pour supprimer la simulation. L’URL utilise le n° de la simulation affichée dans la ligne ;
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
638/755
Affichons cette vue en exécutant ce script. On obtient le résultat suivant :
On travaille sur cette vue jusqu’à ce que le résultat obtenu visuellement nous convienne. On peut ensuite passer à l’intégration de la
vue dans l’application web en cours d’écriture.
Une fois l’aspect visuel de la vue déterminé, on peut procéder au calcul du modèle de la vue en conditions réelles. Rappelons les codes
d’état qui mènent à cette vue. On les trouve dans le fichier de configuration :
1. {
2. # vue de la liste des simulations
3. "états": [
4. # /lister-simulations
5. 500,
6. # /supprimer-simulation
7. 600
8. ],
9. "view_name": "views/vue-liste-simulations.html",
10. "model_for_view": ModelForListeSimulationsView()
11. }
Ce sont donc les codes d’état [500, 600] qui font afficher la vue des simulations. Pour retrouver la signification de ces codes, on peut
s’aider des tests [Postman] réalisés sur l’application jSON :
• [lister-simulations-500] : 500 est le code d’état à l’issue d’une action [lister-simulations] réussie : on présente alors la
liste des simulations réalisées par l’utilisateur ;
• [supprimer-simulation-600] : 600 est le code d’état à l’issue d’une action [supprimer-simulation] réussie. On présente
alors la nouvelle liste des simulations obtenue après cette suppression ;
Maintenant que nous savons à quels moments doit être affichée la liste des simulations, on peut calculer son modèle dans la classe
[ModelForListeSimulationsView] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
639/755
8. def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
9. # on encapsule les données de la pagé dans modèle
10. modèle = {}
11. # les simulations sont trouvées dans la réponse du contrôleur qui a exécuté l'action
12. # sous la forme d'un tableau de dictionnaires TaxPayer
13. modèle["simulations"] = résultat["réponse"]
14. # menu
15. modèle["optionsMenu"] = [
16. {"text": "Calcul de l'impôt", "url": '/afficher-calcul-impot'},
17. {"text": 'Fin de session', "url": '/fin-session'}]
18. # on rend le modèle
19. return modèle
Commentaires
Le test [lister-simulations-500] nous permet d’avoir le code d’état 500. Il correspond à une demande pour voir les simulations :
Le test [supprimer-simulation-600] nous permet d’avoir le code d’état 600. On va ici supprimer la simulation n° 2.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
640/755
Le résultat renvoyé est une liste de simulations avec une simulation en moins :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
641/755
32.8.1 Présentation de la vue
La vue des erreurs inattendues est générée par le script [vue-erreurs.html] suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
642/755
23. <!-- liste des erreurs sur 9 colonnes -->
24. <div class="col-md-9">
25. <div class="alert alert-danger" role="alert">
26. Les erreurs inattendues suivantes se sont produites :
27. <ul>{{modèle.erreurs|safe}}</ul>
28. </div>
29. </div>
30. </div>
31. </div>
32. </body>
33. </html>
Commentaires
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
643/755
• lignes 11-15 : construction de la liste HTML des erreurs ;
• lignes 17-20 : le tableau des options de menu ;
On travaille sur cette vue jusqu’à ce que le résultat obtenu visuellement nous convienne. On peut ensuite passer à l’intégration de la
vue dans l’application web en cours d’écriture.
Une fois l’aspect visuel de la vue déterminé, on peut procéder au calcul du modèle de la vue en conditions réelles. Rappelons les codes
d’état qui mènent à cette vue. On les trouve dans le fichier de configuration :
1. # les vues HTML et leurs modèles dépendent de l'état rendu par le contrôleur
2. "views": [
3. {
4. # vue d'authentification
5. "états": [
6. # /init-session réussite
7. 700,
8. # /fin-session
9. 400,
10. # /authentifier-utilisateur échec
11. 201
12. ],
13. "view_name": "views/vue-authentification.html",
14. "model_for_view": ModelForAuthentificationView()
15. },
16. {
17. # vue du calcul de l'impôt
18. "états": [
19. # /authentifier-utilisateur réussite
20. 200,
21. # /calculer-impot réussite
22. 300,
23. # /calculer-impot échec
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
644/755
24. 301,
25. # /afficher-calcul-impot
26. 800
27. ],
28. "view_name": "views/vue-calcul-impot.html",
29. "model_for_view": ModelForCalculImpotView()
30. },
31. {
32. # vue de la liste des simulations
33. "états": [
34. # /lister-simulations
35. 500,
36. # /supprimer-simulation
37. 600
38. ],
39. "view_name": "views/vue-liste-simulations.html",
40. "model_for_view": ModelForListeSimulationsView()
41. }
42. ],
43. # vue des erreurs inattendues
44. "view-erreurs": {
45. "view_name": "views/vue-erreurs.html",
46. "model_for_view": ModelForErreursView()
47. },
Ce sont les codes d’état qui ne mènent pas à une vue HTML des lignes 3-41 qui font afficher la vue des erreurs inattendues.
Le calcul du modèle de la vue [vue-erreurs.html] est fait par la classe [ModelForErreursView] suivante :
Commentaires
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
645/755
32.9 Implémentation des actions du menu de l’application
Nous allons ici traiter de l’implémentation des actions du menu. Rappelons la signification des liens que nous avons rencontrés
Il faut rappeler qu’un clic sur un lien provoque un GET vers la cible du lien. Les actions [/lister-simulations, /fin-session] ont
été implémentées avec une opération GET, ce qui nous permet de les mettre comme cibles de liens. Lorsque l’action se fait par un
POST, l’utilisation d’un lien n’est plus possible sauf à l’associer avec du Javascript.
Il nous faut donc implémenter l’action [/afficher-calcul-impot]. Cela va nous permettre de réviser le mode opératoire de
l’implémentation d’une action au sein du serveur.
Tout d’abord, il nous faut ajouter un nouveau contrôleur secondaire. Nous l’appellerons [AfficherCalculImpotController] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
646/755
Ce contrôleur doit être ajouté dans le fichier de configuration [config] :
1. # les contrôleurs
2. from AfficherCalculImpotController import AfficherCalculImpotController
3. from AuthentifierUtilisateurController import AuthentifierUtilisateurController
4. from CalculerImpotController import CalculerImpotController
5. from CalculerImpotsController import CalculerImpotsController
6. from FinSessionController import FinSessionController
7. from GetAdminDataController import GetAdminDataController
8. …
9.
10.
11. # actions autorisées et leurs contrôleurs
12. "controllers": {
13. # initialisation d'une session de calcul
14. "init-session": InitSessionController(),
15. # authentification d'un utilisateur
16. "authentifier-utilisateur": AuthentifierUtilisateurController(),
17. # calcul de l'impôt en mode individuel
18. "calculer-impot": CalculerImpotController(),
19. # calcul de l'impôt en mode lots
20. "calculer-impots": CalculerImpotsController(),
21. # liste des simulations
22. "lister-simulations": ListerSimulationsController(),
23. # suppression d'une simulation
24. "supprimer-simulation": SupprimerSimulationController(),
25. # fin de la session de calcul
26. "fin-session": FinSessionController(),
27. # affichage de la vue de calcul de l'impôt
28. "afficher-calcul-impot": AfficherCalculImpotController(),
29. # obtention des données de l'administration fiscale
30. "get-admindata": GetAdminDataController(),
31. # main controller
32. "main-controller": MainController()
33. },
34. …
35. # les vues HTML et leurs modèles dépendent de l'état rendu par le contrôleur
36. "views": [
37. {
38. # vue d'authentification
39. …
40. },
41. {
42. # vue du calcul de l'impôt
43. "états": [
44. # /authentifier-utilisateur réussite
45. 200,
46. # /calculer-impot réussite
47. 300,
48. # /calculer-impot échec
49. 301,
50. # /afficher-calcul-impot
51. 800
52. ],
53. "view_name": "views/vue-calcul-impot.html",
54. "model_for_view": ModelForCalculImpotView()
55. },
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
647/755
56. {…
57. }
58. ],
Commentaires
• ligne 6 : comme les autres contrôleurs secondaires, le nouveau contrôleur implémente l’interface [InterfaceController] ;
• ligne 13 : les changements de vue sont simples à implémenter : il suffit de rendre un code d’état associé à la vue cible, ici le
code 800 comme il a été vu plus haut ;
1. # redirections
2. "redirections": [
3. {
4. "états": [
5. 400, # /fin-session réussi
6. ],
7. # redirection vers
8. "to": "/init-session/html",
9. }
10. ],
• lorsque le contrôleur rend le code d’état [400] (ligne 5), il faut rediriger le client vers l’URL
[http://machine:port/chemin/init-session/html] (ligne 8) ;
Le code d’état [400] est le code rendu suite à une action [/fin-session] réussie. Pourquoi faut-il alors rediriger le client l’URL
[/init-session/html] ? Parce que le code de l’action [/fin-session] supprime le type de la session présent dans la session web. On
ne sait plus alors qu’on est dans une session html. Il faut le redire. On le fait à l’aide de l’action [/init-session/html].
1. def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
2. résultat: dict) -> (Response, int):
3. # la réponse HTML dépend du code d'état rendu par le contrôleur
4. état = résultat["état"]
5.
6. # faut-il faire une redirection ?
7. for redirection in config["redirections"]:
8. # états nécessitant une redirection
9. états = redirection["états"]
10. if état in états:
11. # il faut faire une redirection
12. return redirect(f"{redirection['to']}"), status.HTTP_302_FOUND
13.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
648/755
14. # à un état, correspond une vue
15. # on cherche celle-ci dans la liste des vues
16. ..
On a obtenu la vue d’authentification. C’est bien elle qu’on attendait. Maintenant, voyons comment elle a été obtenue. Passons sur la
console [Postman] (Ctrl-Alt-C) :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
649/755
• en [1], l’action [/fin-session] ;
• en [2-3], le code 302 de statut HTTP rendu par le serveur indique au client qu’il dit se rediriger ;
• en [4], le client [Postman] suit la redirection ;
• lignes 4-7 : gestion de la route [/]. Le point d’entrée de l’application web sera l’URL[/init-session/html] (ligne 10). Aussi
ligne 7, nous redirigeons le client vers cette URL :
o la fonction [url_for] est importée ligne 1. Elle a ici deux paramètres (ligne 7) :
▪ le 1er paramètre est le nom d’une des fonctions de routage, ici celle de la ligne 11. On voit que cette fonction attend
un paramètre [type_response] qui est le type (json, xml, html) de réponse souhaité par le client ;
▪ le 2ième paramètre reprend le nom du paramètre de la ligne 11, [type_response], et lui donne une valeur. S’il y avait
d’autres paramètres, on répéterait l’opération pour chacun d’eux ;
▪ elle rend l’URL associée à la fonction désignée par les deux paramètres qui lui ont été donnés. Ici cela donnera
l’URL de la ligne 10 où le paramètre est remplacé par sa valeur [/init-session/html] ;
o la fonction [redirect] a été importée ligne 1. Elle a pour rôle d’envoyer un entête HTTP de redirection au client :
▪ le 1er paramètre est l’URL vers laquelle le client doit être redirigé ;
▪ le 2ième paramètre est le code de statut de la réponse HTTP faite au client. Le code [status.HTTP_302_FOUND]
correspond à une redirection HTTP ;
Dans notre navigateur, nous activons le suivi des requêtes (F12 sur Chrome, Firefox, Edge) et nous demandons l’URL de
démarrage [http://localhost:5000/]. La réponse du serveur est la suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
650/755
Si on regarde les échanges réseau qui ont eu lieu entre le client et le serveur :
• on voit qu’en [4, 5], le navigateur a reçu une demande de redirection vers l’URL [/init-session/html] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
651/755
Puis faisons quelques simulations :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
652/755
Demandons la liste des simulations :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
653/755
Supprimons la 1re simulation :
Terminons la session :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
654/755
33 Exercice d’application : version 13
La version 13 modifie la version 12 sur les points suivants :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
655/755
Trois nouveaux fichiers de configuration apparaissent :
Le script [mvc] configure l’architecture MVC de l’application web jSON / XML / HTML :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
656/755
14. from SupprimerSimulationController import SupprimerSimulationController
15. # les réponses HTTP
16. from HtmlResponse import HtmlResponse
17. from JsonResponse import JsonResponse
18. from XmlResponse import XmlResponse
19. # les modèles des vues
20. from ModelForAuthentificationView import ModelForAuthentificationView
21. from ModelForCalculImpotView import ModelForCalculImpotView
22. from ModelForErreursView import ModelForErreursView
23. from ModelForListeSimulationsView import ModelForListeSimulationsView
24.
25. # actions autorisées et leurs contrôleurs
26. controllers = {
27. # initialisation d'une session de calcul
28. "init-session": InitSessionController(),
29. # authentification d'un utilisateur
30. "authentifier-utilisateur": AuthentifierUtilisateurController(),
31. # calcul de l'impôt en mode individuel
32. "calculer-impot": CalculerImpotController(),
33. # calcul de l'impôt en mode lots
34. "calculer-impots": CalculerImpotsController(),
35. # liste des simulations
36. "lister-simulations": ListerSimulationsController(),
37. # suppression d'une simulation
38. "supprimer-simulation": SupprimerSimulationController(),
39. # fin de la session de calcul
40. "fin-session": FinSessionController(),
41. # affichage de la vue de calcul de l'impôt
42. "afficher-calcul-impot": AfficherCalculImpotController(),
43. # obtention des données de l'administration fiscale
44. "get-admindata": GetAdminDataController(),
45. # main controller
46. "main-controller": MainController()
47. }
48. # les différents types de réponse (json, xml, html)
49. responses = {
50. "json": JsonResponse(),
51. "html": HtmlResponse(),
52. "xml": XmlResponse()
53. }
54. # les vues HTML et leurs modèles dépendent de l'état rendu par le contrôleur
55. views = [
56. {
57. # vue d'authentification
58. "états": [
59. 700, # /init-session - succès
60. 201, # /authentifier-utilisateur échec
61. ],
62. "view_name": "views/vue-authentification.html",
63. "model_for_view": ModelForAuthentificationView()
64. },
65. {
66. # vue du calcul de l'impôt
67. "états": [
68. 200, # /authentifier-utilisateur réussite
69. 300, # /calculer-impot réussite
70. 301, # /calculer-impot échec
71. 800, # /afficher-calcul-impot réussite
72. ],
73. "view_name": "views/vue-calcul-impot.html",
74. "model_for_view": ModelForCalculImpotView()
75. },
76. {
77. # vue de la liste des simulations
78. "états": [
79. 500, # /lister-simulations réussite
80. 600, # /supprimer-simulation réussite
81. ],
82. "view_name": "views/vue-liste-simulations.html",
83. "model_for_view": ModelForListeSimulationsView()
84. },
85.
86. ]
87. # vue des erreurs inattendues
88. view_erreurs = {
89. "view_name": "views/vue-erreurs.html",
90. "model_for_view": ModelForErreursView()
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
657/755
91. }
92. # redirections
93. redirections = [
94. {
95. "états": [
96. 400, # /fin-session réussite
97. ],
98. # redirection vers l’URL
99. "to": "/init-session/html",
100. }
101. ]
102.
103.
104. # on rend la configuration MVC
105. return {
106. # contrôleurs
107. "controllers": controllers,
108. # réponses HTTP
109. "responses": responses,
110. # vues et modèles
111. "views": views,
112. # liste des redirections
113. "redirections": redirections,
114. # vue des erreurs inattendues
115. "view_erreurs": view_erreurs
116. }
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
658/755
Avec ces nouveaux fichiers de configuration, le script [config] devient le suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
659/755
25. # on envoie un mail à l'administrateur de l'application
26. config_mail = config['parameters']['adminMail']
27. config_mail["logger"] = config['logger']
28. SendAdminMail.send(config_mail, message)
29.
30. # vérification du fichier de logs
31. logger = None
32. erreur = False
33. message_erreur = None
34. try:
35. # logueur
36. logger = Logger(config['parameters']['logsFilename'])
37. except BaseException as exception:
38. # log console
39. print(f"L'erreur suivante s'est produite : {exception}")
40. # on note l'erreur
41. erreur = True
42. message_erreur = f"{exception}"
43. # on mémorise le logueur dans la config
44. config['logger'] = logger
45. # gestion de l'erreur
46. if erreur:
47. # mail à l'administrateur
48. send_adminmail(config, message_erreur)
49. # fin de l'application
50. sys.exit(1)
51.
52. # log de démarrage
53. log = "[serveur] démarrage du serveur"
54. logger.write(f"{log}\n")
55.
56. # on vérifie la disponibilité du serveur Redis
57. redis_client = redis.Redis(host=config["parameters"]["redis"]["host"],
58. port=config["parameters"]["redis"]["port"])
59. # on ping le serveur Redis
60. try:
61. redis_client.ping()
62. except BaseException as exception:
63. # Redis pas disponible
64. log = f"[serveur] Le serveur Redis n'est pas disponible : {exception}"
65. # console
66. print(log)
67. # log
68. logger.write(f"{log}\n")
69. # fin
70. sys.exit(1)
71.
72. # on mémorise le client [redis] dans la config
73. config['redis_client'] = redis_client
74.
75. # récupération des données de l'administration fiscale
76. erreur = False
77. try:
78. # admindata sera une donnée de portée application en lecture seule
79. config["admindata"] = config["layers"]["dao"].get_admindata()
80. # log de réussite
81. logger.write("[serveur] connexion à la base de données réussie\n")
82. except ImpôtsError as ex:
83. # on note l'erreur
84. erreur = True
85. # log d'erreur
86. log = f"L'erreur suivante s'est produite : {ex}"
87. # console
88. print(log)
89. # fichier de logs
90. logger.write(f"{log}\n")
91. # mail à l'administrateur
92. send_adminmail(config, log)
93.
94. # le thread principal n'a plus besoin du logger
95. logger.close()
96.
97. # s'il y a eu erreur on s'arrête
98. if erreur:
99. sys.exit(2)
100.
101. # import des routes de l'application web
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
660/755
102. import routes
103. routes.config=config
104. routes.execute(__name__)
Pour dialoguer avec le serveur Redis, il nous faut un client Redis. Celui nous sera fourni par le module [redis] que nous installons
également :
• chaque utilisateur a un identifiant de session et c’est ça qui est envoyé au client et uniquement ça. Le client ne reçoit de cookie
de session qu’une seule fois, à l’issue de sa première requête. Ce cookie contient l’identifiant de session de l’utilisateur qui ne
changera plus au fil des requêtes du client. C’est pourquoi le serveur n’a pas à renvoyer de nouveau cookie de session ;
• précédemment le contenu de la session était envoyé au client. Ce ne sera donc plus le cas. Le contenu de la session de
l’utilisateur sera stocké sur le serveur Redis ;
Laragon vient avec un serveur Redis non activé par défaut. Il faut donc commencer par l’activer :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
661/755
Le serveur Redis peut être interrogé en mode commande. On ouvre un terminal Laragon [6] :
En juillet 2019, le client Redis peut utiliser 172 commandes pour dialoguer avec le serveur [https://redis.io/commands#list]. L’une
d’elles [command count] [2], affiche ce nombre [3].
L’écriture dans [Redis] se fait avec la commande Redis [set attribut valeur] [4]. La valeur peut ensuite être récupérée avec la
commande [get attribut] [5].
Il peut être nécessaire de vider la mémoire de Redis. Cela se fait avec la commande [flushdb] [6]. Ensuite si on demande la valeur
de l’attribut [titre] [7], on obtient une référence [nil] [8] indiquant que l’attribut n’a pas été trouvé. On peut également utiliser la
commande [exists] [9-10] pour vérifier l’existence d’un attribut.
On peut également utiliser une interface web pour gérer les clés présentes dans le serveur Redis. Pour cela, il faut que le serveur
Apache de Laragon soit lancé :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
662/755
On obtient l’interface suivante :
1. import redis
2. …
3. # on vérifie la disponibilité du serveur Redis
4. redis_client = redis.Redis(host=config["parameters"]["redis"]["host"],
5. port=config["parameters"]["redis"]["port"])
6. # on ping le serveur Redis
7. try:
8. redis_client.ping()
9. except BaseException as exception:
10. # Redis pas disponible
11. log = f"[serveur] Le serveur Redis n'est pas disponible : {exception}"
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
663/755
12. # console
13. print(log)
14. # log
15. logger.write(f"{log}\n")
16. # fin
17. sys.exit(1)
18.
19. # on mémorise le client [redis] dans la config
20. config['redis_client'] = redis_client
• ligne 4 : le constructeur de la classe [redis.Redis] construit un client du serveur Redis. Les caractéristiques de celui-ci (adresse,
port) sont trouvées dans le script [parameters] ;
• ligne 8 : la méthode [ping] permet de vérifier la présence du serveur Redis ;
• lignes 9-17 : si le ping n’aboutit pas, alors on logue l’erreur et on arrête le serveur ;
• ligne 20 : la référence du client Redis est mise dans la configuration ;
1. # dépendances
2. import os
3.
4. from flask import Flask, redirect, request, session, url_for
5. from flask_api import status
6. from flask_session import Session
7.
8. # application Flask
9. app = Flask(__name__, template_folder="../flask/templates", static_folder="../flask/static")
10.
11. # configuration application
12. config = {}
13.
14. # le front controller
15. def front_controller() -> tuple:
16. # on fait suivre la requête au contrôleur principal
17. main_controller = config['mvc']['controllers']['main-controller']
18. return main_controller.execute(request, session, config)
19.
20. @app.route('/', methods=['GET'])
21. def index() -> tuple:
22. # redirection vers /init-session/html
23. return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)
24.
25. # init-session
26. @app.route('/init-session/<string:type_response>', methods=['GET'])
27. def init_session(type_response: str) -> tuple:
28. # on exécute le contrôleur associé à l'action
29. return front_controller()
30.
31. # authentifier-utilisateur
32. @app.route('/authentifier-utilisateur', methods=['POST'])
33. def authentifier_utilisateur() -> tuple:
34. # on exécute le contrôleur associé à l'action
35. return front_controller()
36.
37. # calculer-impot
38. @app.route('/calculer-impot', methods=['POST'])
39. def calculer_impot() -> tuple:
40. # on exécute le contrôleur associé à l'action
41. return front_controller()
42.
43. # calcul de l'impôt par lots
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
664/755
44. @app.route('/calculer-impots', methods=['POST'])
45. def calculer_impots():
46. # on exécute le contrôleur associé à l'action
47. return front_controller()
48.
49. # lister-simulations
50. @app.route('/lister-simulations', methods=['GET'])
51. def lister_simulations() -> tuple:
52. # on exécute le contrôleur associé à l'action
53. return front_controller()
54.
55. # supprimer-simulation
56. @app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
57. def supprimer_simulation(numero: int) -> tuple:
58. # on exécute le contrôleur associé à l'action
59. return front_controller()
60.
61. # fin-session
62. @app.route('/fin-session', methods=['GET'])
63. def fin_session() -> tuple:
64. # on exécute le contrôleur associé à l'action
65. return front_controller()
66.
67. # afficher-calcul-impot
68. @app.route('/afficher-calcul-impot', methods=['GET'])
69. def afficher_calcul_impot() -> tuple:
70. # on exécute le contrôleur associé à l'action
71. return front_controller()
72.
73. # get-admindata
74. @app.route('/get-admindata', methods=['GET'])
75. def get_admindata() -> tuple:
76. # on exécute le contrôleur associé à l'action
77. return front_controller()
78.
79. def execute(name: str):
80. # clé secrète de la session
81. app.secret_key = os.urandom(12).hex()
82. # Flask-Session
83. app.config.update(SESSION_TYPE='redis',
84. SESSION_REDIS=config['redis_client'])
85. Session(app)
86. # cas où on lance l'application Flask via un script console
87. if name == '__main__':
88. app.config.update(ENV="development", DEBUG=True)
89. app.run(threaded=True)
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
665/755
33.3 Refactorisation du contrôleur principal
Le code autrefois dans la fonction [front_controller] du script [main] a été déplacé dans le contrôleur principal :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
666/755
63. # pause
64. time.sleep(sleep_time)
65.
66. # pour certaines actions on doit être authentifié
67. user = session.get('user')
68. if not erreur and user is None and action not in ["init-session", "authentifier-utilisateur"]:
69. # on note l'erreur
70. résultat = {"action": action, "état": 101,
71. "réponse": [f"action [{action}] demandée par utilisateur non authentifié"]}
72. erreur = True
73.
74. # y-a-t-il des erreurs ?
75. if erreur:
76. # la requête est invalide
77. status_code = status.HTTP_400_BAD_REQUEST
78. else:
79. # on exécute le contrôleur associé à l'action
80. controller = config['mvc']['controllers'][action]
81. résultat, status_code = controller.execute(request, session, config)
82.
83. except BaseException as exception:
84. # autres exceptions (inattendues)
85. résultat = {"action": action, "état": 131, "réponse": [f"{exception}"]}
86. status_code = status.HTTP_400_BAD_REQUEST
87.
88. finally:
89. pass
90.
91. # on logue le résultat envoyé au client
92. log = f"[MainController] {résultat}\n"
93. logger.write(log)
94.
95. # y-a-t-il eu une erreur fatale ?
96. if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
97. # on envoie un mail à l'administrateur de l'application
98. send_adminmail(config, log)
99.
100. # on détermine le type souhaité pour la réponse
101. type_response2 = session.get('typeResponse')
102. if type_response2 is None and type_response1 is None:
103. # le type de session n'a pas encore été établi - ce sera du jSON
104. type_response = 'json'
105. elif type_response2 is not None:
106. # le type de la réponse est connu et dans la session
107. type_response = type_response2
108. else:
109. # sinon on continue à utiliser type_response1
110. type_response = type_response1
111.
112. # on construit la réponse à envoyer
113. response_builder = config['mvc']['responses'][type_response]
114. response, status_code = response_builder \
115. .build_http_response(request, session, config, status_code, résultat)
116.
117. # on ferme le fichier de logs s'il a été ouvert
118. if logger:
119. logger.close()
120.
121. # on envoie la réponse HTTP
122. return response, status_code
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
667/755
Voici un exemple de script cryptant le mot de passe qu’on lui passe en paramètre :
1. import sys
2.
3. # fonction de cryptage
4. from passlib.hash import pbkdf2_sha256
5.
6. # on attend le mot de passe à cryter
7. syntaxe = f"{sys.argv[0]} password"
8. erreur = len(sys.argv) != 2
9. if erreur:
10. print(f"syntaxe : {syntaxe}")
11. sys.exit()
12. else:
13. password = sys.argv[1]
14.
15. # on crypte le mot de passe
16. hash = pbkdf2_sha256.hash(password)
17. print(f"version cryptée de [{password}] = {hash}")
18.
19. # vérification
20. correct = pbkdf2_sha256.verify(password, hash)
21. print(correct)
Le script ci-dessus nous permet d’avoir la version cryptée du mot de passe [admin] :
1. C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-
2020/dev/python/cours-2020/python3-flask-2020/impots/http-servers/08/passlib/create_hashed_password.py
admin
2. version cryptée de [admin] = $pbkdf2-
sha256$29000$fU9pTendO6c0ZoyR8r5Xqg$5ZXywIUnbMfN2hPnBaefiuqWjEbmAY.Lu06i4dwcnek
3. True
1. "users": [
2. {
3. "login": "admin",
4. "password": "$pbkdf2-
sha256$29000$fU9pTendO6c0ZoyR8r5Xqg$5ZXywIUnbMfN2hPnBaefiuqWjEbmAY.Lu06i4dwcnek"
5. }
6. ],
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
668/755
8. while not trouvé and i < nbusers:
9. trouvé = user == users[i]["login"] and pbkdf2_sha256.verify(password, users[i]["password"])
10. i += 1
11. # trouvé ?
12. if not trouvé:
13. …
33.5 Tests
Outre les tests avec un navigateur, on peut également utiliser les clients du dossier [http-servers/07] écrits pour la version 12. Ils
doivent ‘marcher’ également pour la version 13 :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
669/755
34 Exercice d’application : version 14
Le dossier [http-servers/09] de la version 14 est obtenu par recopie du dossier [http-servers/08] de la version 13.
34.1 Introduction
CSRF (Cross Site Request Forgery) est une technique de vol de session. Elle est expliquée ainsi dans Wikipedia
(https://fr.wikipedia.org/wiki/Cross-site_request_forgery):
Supposons qu'Alice soit l'administratrice d'un forum et qu'elle soit connectée à celui-ci par un système de sessions. Malorie est un
membre de ce même forum, elle veut supprimer un des messages du forum. Comme elle n'a pas les droits nécessaires avec son
compte, elle utilise celui d'Alice grâce à une attaque de type CSRF.
Même expliqué comme ça, la technique du CSRF est difficile à comprendre. Faisons un schéma :
• en [1-2], Alice communique avec le forum (Site A). Ce forum maintient une session pour chaque utilisateur. Le navigateur
d’Alice stocke localement ce cookie de session et le renvoie à chaque fois qu’il fait une nouvelle requête au site A ;
• en [3], Malorie envoie un message à Alice. Celle-ci le lit avec son navigateur. Le message lu est au format HTML et contient
un lien vers une image du site B. En fait ce lien est un lien vers un script Javascript qui s’exécute une fois arrivé sur le navigateur
d’Alice ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
670/755
• ce script Javascript réalise alors une requête vers le site A. Le navigateur d’Alice envoie alors automatiquement la requête avec
le cookie de session stocké localement. C’est ici que se produit l’attaque : Malorie a réussi à interroger le site A avec les droits
(session) de Alice. Ensuite, peu importe ce qui se passe, l’attaque a eu lieu ;
• à chaque échange [1-2] avec Alice, le site A envoie une clé, appelée par la suite token (jeton) CSRF, qu’Alice doit lui renvoyer
lors de la requête suivante. Ainsi Alice doit envoyer à chaque requête deux informations :
o le cookie de session ;
o le token CSRF reçu lors de la réponse à sa dernière requête au site A ;
La protection est là : si le navigateur renvoie automatiquement au site A le cookie de session, il ne le fait pas pour le token
CSRF. Pour cette raison, l’échange 6-7 fait par le script d’attaque sera refusé car la requête 6 n’aura pas envoyé le jeton CSRF ;
Le site A peut envoyer à Alice le jeton CSRF de diverses façons pour une application HTML :
• il peut à chaque requête envoyer une page HTML où tous les liens auront le jeton CSRF, par exemple
[http://siteA/chemin/csrf_token]. Lors de la requête suivante, Alice cliquant sur l’un de ces liens, le site A n’aura qu’à
récupérer le jeton CSRF dans l’URL de la requête et vérifier qu’il est correct. C’est ce qui sera fait ici ;
• il peut, pour les pages HTML contenant un formulaire, envoyer celui-ci avec un champ caché [input type=’hidden’]
contenant le jeton CSRF. Celui-ci sera alors posté automatiquement avec le formulaire lorsqu’Alice validera la page. Le site A
récupèrera le jeton CSRF dans le corps (body) de la requête ;
• d’autres techniques sont envisageables ;
34.2 Configuration
• [with_redissession] : à True, l’application utilise une session Redis. A False, l’application utilise une session Flask normale ;
• [with_csrftoken] : à True, les URL de l’application contiennent un jeton CSRF ;
config['parameters']['with_csrftoken']
vaut [True], l’application envoie au navigateur client des pages web dont les liens contiendront un jeton CSRF.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
671/755
3. …
• ligne 9 : la classe [AbstractBaseModelForView] implémente l’interface [InterfaceModelForView] implémentée par les classes
des modèles ;
• lignes 11-13 : la méthode [get_model_for_view] n’est pas implémentée ;
• lignes 15-20 : la méthode [get_csrftoken] génère le jeton CSRF si l’application a été configurée pour les utiliser. Selon les cas,
la fonction rend un jeton précédé du signe / sinon une chaîne vide. La fonction [generate_csrf] a la particularité de toujours
générer la même valeur pour une requête client donnée. Le traitement d’une requête implique l’exécution de différentes
fonctions. Utiliser [generate_csrf] dans celles-ci génère toujours la même valeur. Lors de la requête suivante, en revanche,
un nouveau jeton CSRF est généré ;
1. class ModelForAuthentificationView(AbstractBaseModelForView):
2.
3. def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
4. # on encapsule les données de la pagé dans modèle
5. modèle = {}
6. …
7.
8. # jeton csrf
9. modèle['csrf_token'] = super().get_csrftoken(config)
10.
11. # on rend le modèle
12. return modèle
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
672/755
34.3.3 Les vues
De ce qu’on vient de voir, toutes les vues V auront dans leur modèle M le jeton CSRF. Elles pourront donc l’utiliser dans les liens
qu’elles contiennent. Prenons quelques exemples :
1. <!-- formulaire HTML - on poste ses valeurs avec l'action [authentifier-utilisateur] -->
2. <form method="post" action="/authentifier-utilisateur{{modèle.csrf_token}}">
3.
4. <!-- titre -->
5. <div class="alert alert-primary" role="alert">
6. <h4>Veuillez vous authentifier</h4>
7. </div>
8. …
9.
10. </form>
• ligne 2 : d’après ce qui vient d’être vu, l’URL de l’attribut [action] sera :
[/authentifier-utilisateur/Ijk4NjQ2ZDdjZjI0ZDJiYTVjZTZjYmFhZGNjMjE3Y2U5M2I3ODI0NzYi.Xy5Okg.n-
kSR_nslkndfT7AFVy2UDtdb8c]
ou
[/authentifier-utilisateur]
selon que l’application a été configurée pour utiliser ou non des jetons CSRF ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
673/755
30. <!-- colonne 8 : valeur paramètre [réduction] - balise <td> -->
31. <!-- colonne 9 : valeur paramètre [taux] (de l'impôt) - balise <td> -->
32. <!-- colonne 10 : lien de suppression de la simulation - balise <td> -->
33. <tr>
34. <th scope="row">{{simulation.id}}</th>
35. <td>{{simulation.marié}}</td>
36. <td>{{simulation.enfants}}</td>
37. <td>{{simulation.salaire}}</td>
38. <td>{{simulation.impôt}}</td>
39. <td>{{simulation.surcôte}}</td>
40. <td>{{simulation.décôte}}</td>
41. <td>{{simulation.réduction}}</td>
42. <td>{{simulation.taux}}</td>
43. <td><a href="/supprimer-simulation/{{simulation.id}}{{modèle.csrf_token}}">Supprimer</a></td>
44. </tr>
45. {% endfor %}
46. </tr>
47. </tbody>
48. </table>
49. {% endif %}
• [routes_without_csrftoken] sont les routes sans jeton CSRF. Ce sont les routes de la version précédente ;
• [routes_with_csrftoken] sont les routes avec jeton CSRF.
Dans [routes_with_csrftoken], les routes ont désormais un paramètre supplémentaire, le jeton CSRF :
1. # le front controller
2. def front_controller() -> tuple:
3. # on fait suivre la requête au contrôleur principal
4. main_controller = config['mvc']['controllers']['main-controller']
5. return main_controller.execute(request, session, config)
6.
7. @app.route('/', methods=['GET'])
8. def index() -> tuple:
9. # redirection vers /init-session/html
10. return redirect(url_for("init_session", type_response="html", csrf_token=generate_csrf()), status.HTTP_302_FOUND)
11.
12. # init-session
13. @app.route('/init-session/<string:type_response>/<string:csrf_token>', methods=['GET'])
14. def init_session(type_response: str, csrf_token: str) -> tuple:
15. # on exécute le contrôleur associé à l'action
16. return front_controller()
17.
18. # authentifier-utilisateur
19. @app.route('/authentifier-utilisateur/<string:csrf_token>', methods=['POST'])
20. def authentifier_utilisateur(csrf_token: str) -> tuple:
21. # on exécute le contrôleur associé à l'action
22. return front_controller()
23.
24. # calculer-impot
25. @app.route('/calculer-impot/<string:csrf_token>', methods=['POST'])
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
674/755
26. def calculer_impot(csrf_token: str) -> tuple:
27. # on exécute le contrôleur associé à l'action
28. return front_controller()
29.
30. # calcul de l'impôt par lots
31. @app.route('/calculer-impots/<string:csrf_token>', methods=['POST'])
32. def calculer_impots(csrf_token: str):
33. # on exécute le contrôleur associé à l'action
34. return front_controller()
35.
36. # lister-simulations
37. @app.route('/lister-simulations/<string:csrf_token>', methods=['GET'])
38. def lister_simulations(csrf_token: str) -> tuple:
39. # on exécute le contrôleur associé à l'action
40. return front_controller()
41.
42. # supprimer-simulation
43. @app.route('/supprimer-simulation/<int:numero>/<string:csrf_token>', methods=['GET'])
44. def supprimer_simulation(numero: int, csrf_token: str) -> tuple:
45. # on exécute le contrôleur associé à l'action
46. return front_controller()
47.
48. # fin-session
49. @app.route('/fin-session/<string:csrf_token>', methods=['GET'])
50. def fin_session(csrf_token: str) -> tuple:
51. # on exécute le contrôleur associé à l'action
52. return front_controller()
53.
54. # afficher-calcul-impot
55. @app.route('/afficher-calcul-impot/<string:csrf_token>', methods=['GET'])
56. def afficher_calcul_impot(csrf_token: str) -> tuple:
57. # on exécute le contrôleur associé à l'action
58. return front_controller()
59.
60. # get-admindata
61. @app.route('/get-admindata/<string:csrf_token>', methods=['GET'])
62. def get_admindata(csrf_token: str) -> tuple:
63. # on exécute le contrôleur associé à l'action
64. return front_controller()
Toutes les routes ont désormais le jeton CSRF dans leurs paramètres, même la route [/init-session]. Cela signifie que le client ne
peut pas démarrer l’application en tapant directement l’URL [/init-session/html] car il y manquera le jeton CSRF. Il doit désormais
obligatoirement passer par l’URL [/] des lignes 7-10.
1. …
2. # le thread principal n'a plus besoin du logger
3. logger.close()
4.
5. # s'il y a eu erreur on s'arrête
6. if erreur:
7. sys.exit(2)
8.
9. # import des routes de l'application web
10. if config['parameters']['with_csrftoken']:
11. import routes_with_csrftoken as routes
12. else:
13. import routes_without_csrftoken as routes
14.
15. # configuration des routes
16. routes.config = config
17.
18. # démarrage application Flask
19. routes.execute(__name__)
• lignes 9-13 : choix des routes selon que l’application utilise ou non des jetons CSRF ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
675/755
6. logger = Logger(config['parameters']['logsFilename'])
7.
8. …
9.
10. # on récupère les éléments du path
11. params = request.path.split('/')
12.
13. # l'action est le 1er élément
14. action = params[1]
15.
16. …
17.
18. if config['parameters']['with_csrftoken']:
19. # le csrf_token est le dernier élément du path
20. csrf_token = params.pop()
21. # on vérifie la validité du token
22. # une exception sera lancée si le csrf_token n'est pas celui attendu
23. validate_csrf(csrf_token)
24.
25. …
26.
27. except ValidationError as exception:
28. # csrf token invalide
29. résultat = {"action": action, "état": 121, "réponse": [f"{exception}"]}
30. status_code = status.HTTP_400_BAD_REQUEST
31.
32. except BaseException as exception:
33. # autres exceptions (inattendues)
34. résultat = {"action": action, "état": 131, "réponse": [f"{exception}"]}
35. status_code = status.HTTP_400_BAD_REQUEST
36.
37. finally:
38. pass
39.
40. # on ajoute le csrf_token au résultat
41. résultat['csrf_token'] = generate_csrf()
42.
43. # on logue le résultat envoyé au client
44. log = f"[MainController] {résultat}\n"
45. logger.write(log)
Note : la fonction [validate_csrf] de la ligne 23 ne vérifie pas une concordance stricte. Le jeton CSRF est mémorisé dans la session
avec la clé [csrf_token]. Les test semblent montrer qu’un jeton CSRF est valide s’il a été généré au cours de la session. Ainsi si
manuellement, dans l’URL affichée dans le navigateur, par exemple (/lister-simulations/xyz), vous remplacez le jeton [xyz] CSRF
par un autre [abc] déjà reçu lors d’une précédente action, l’action [/lister-simulations] va réussir ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
676/755
• en [1], le jeton CSRF ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
677/755
Note 1 : il n’est pas sûr que la méthode utilisée ici soit toujours suffisante pour contrer les attaques CSRF. Revenons au schéma de
l’attaque :
Si le script Javascript téléchargé en [5] est capable de lire l’historique du navigateur utilisé par Alice, il sera capable de récupérer les
URL exécutées par le navigateur, des URL telles que [/cible/csrf_token]. Il pourra alors récupérer le jeton de session [csrf_token]
et faire son attaque en [6-7]. Néanmoins, le navigateur autorise uniquement l’exploitation de l’historique de la fenêtre du navigateur
dans laquelle s’exécute le script. Si donc Alice n’utilise pas la même fenêtre pour travailler avec le site A [1-2] et lire le message de
Malorie [3], l’attaque CSRF ne sera pas possible.
Le dossier [impots/http-clients/09] est obtenu initialement par recopie du dossier [impots/http-clients/07]. Il est ensuite
modifié.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
678/755
Revenons aux routes qui initialisent une session :
1. # racine de l'application
2. @app.route('/', methods=['GET'])
3. def index() -> tuple:
4. # redirection vers /init-session/html
5. return redirect(url_for("init_session", type_response="html", csrf_token=generate_csrf()), status.HTTP_302_FOUND)
6.
7. # init-session-with-csrf-token
8. @app.route('/init-session/<string:type_response>/<string:csrf_token>', methods=['GET'])
9. def init_session(type_response: str, csrf_token: str) -> tuple:
10. # on exécute le contrôleur associé à l'action
11. return front_controller()
Aucune de ces routes ne convient pour initialiser une session jSON ou XML :
1. # init-session-without-csrftoken
2. @app.route('/init-session-without-csrftoken/<string:type_response>', methods=['GET'])
3. def init_session_without_csrftoken(type_response: str) -> tuple:
4. # redirection vers /init-session/type_response
5. return redirect(url_for("init_session", type_response=type_response, csrf_token=generate_csrf()), status.HTTP_302_FOUND
)
• ligne 2 : la nouvelle route. Elle n’attend pas de jeton CSRF. On est ainsi revenu à la route [/init-session] de la version
précédente ;
• lignes 4-5 : on redirige le client (jSON, XML, HTML) vers la route [/init-session] ayant le jeton CSRF dans ses paramètres ;
• en [1], le serveur a été redirigé vers la route [/init-session] avec jeton CSRF dans l’URL ;
• en [2], le jeton CSRF est dans le dictionnaire jSON envoyé par le serveur associé à la clé [csrf_token] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
679/755
Nous modifions la configuration [config] de la façon suivante :
1. config.update({
2. # fichier des contribuables
3. "taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
4. # fichier des résultats
5. "resultsFilename": f"{script_dir}/../data/output/résultats.json",
6. # fichier des erreurs
7. "errorsFilename": f"{script_dir}/../data/output/errors.txt",
8. # fichier de logs
9. "logsFilename": f"{script_dir}/../data/logs/logs.txt",
10. # le serveur de calcul de l'impôt
11. "server": {
12. "urlServer": "http://127.0.0.1:5000",
13. "user": {
14. "login": "admin",
15. "password": "admin"
16. },
17. "url_services": {
18. "calculate-tax": "/calculer-impot",
19. "get-admindata": "/get-admindata",
20. "calculate-tax-in-bulk-mode": "/calculer-impots",
21. "init-session": "/init-session-without-csrftoken",
22. "end-session": "/fin-session",
23. "authenticate-user": "/authentifier-utilisateur",
24. "get-simulations": "/lister-simulations",
25. "delete-simulation": "/supprimer-simulation",
26. }
27. },
28. # mode debug
29. "debug": True,
30. # csrf_token
31. "with_csrftoken": True,
32. }
33. )
34. …
35. # route init-session
36. url_services = config['server']['url_services']
37. if config['with_csrftoken']:
38. url_services['init-session'] = '/init-session-without-csrftoken'
39. else:
40. url_services['init-session'] = '/init-session'
• ligne 31 : un booléen indiquera au client si le serveur auquel il s’adresse travaille ou non avec des jetons CSRF ;
• lignes 37-40 : on fixe l’URL de service de l’action [init-session] :
o si le serveur utilise des jetons CSRF alors l’URL de service est [/init-session-without-csrftoken] ;
o sinon l’URL de service est [/init-session] ;
La route [/init-session-without-csrftoken] a été présentée. Elle permet à un client jSON / XML de démarrer une session avec le
serveur sans posséder de jeton CSRF. Il trouvera ce jeton dans la réponse du serveur.
1. # imports
2. import json
3.
4. import requests
5. import xmltodict
6. from flask_api import status
7.
8. from AbstractImpôtsDao import AbstractImpôtsDao
9. from AdminData import AdminData
10. from ImpôtsError import ImpôtsError
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
680/755
11. from InterfaceImpôtsDaoWithHttpSession import InterfaceImpôtsDaoWithHttpSession
12. from TaxPayer import TaxPayer
13.
14. class ImpôtsDaoWithHttpSession(InterfaceImpôtsDaoWithHttpSession):
15.
16. # constructeur
17. def __init__(self, config: dict):
18. # initialisation parent
19. AbstractImpôtsDao.__init__(self, config)
20. # mémorisation éléments de la configuration
21. # config générale
22. self.__config = config
23. # serveur
24. self.__config_server = config["server"]
25. # services
26. self.__config_services = config["server"]['url_services']
27. # mode debug
28. self.__debug = config["debug"]
29. # logger
30. self.__logger = None
31. # cookies
32. self.__cookies = None
33. # type de session (json, xml)
34. self.__session_type = None
35. # jeton CSRF
36. self.__csrf_token = None
37.
38. # étape request / response
39. def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
40. # [method] : méthode HTTP GET ou POST
41. # [url_service] : URL de service
42. # [data] : paramètres du POST en x-www-form-urlencoded
43. # [json] : paramètres du POST en json
44. # [cookies]: cookies à inclure dans la requête
45.
46. # on doit avoir une session XML ou JSON, sinon on ne pourra pas gérer la réponse
47. if self.__session_type not in ['json', 'xml']:
48. raise ImpôtsError(73, "il n'y a pas de session valide en cours")
49.
50. # on ajoute le jeton CSRF à l'URL de service
51. if self.__csrf_token:
52. url_service = f"{url_service}/{self.__csrf_token}"
53.
54. # exécution de la requête
55. response = requests.request(method,
56. url_service,
57. data=data_value,
58. json=json_value,
59. cookies=self.__cookies,
60. allow_redirects=True)
61.
62. # mode debug ?
63. if self.__debug:
64. # logueur
65. if not self.__logger:
66. self.__logger = self.__config['logger']
67. # on logue
68. self.__logger.write(f"{response.text}\n")
69.
70. # résultat
71. if self.__session_type == "json":
72. résultat = json.loads(response.text)
73. else: # xml
74. résultat = xmltodict.parse(response.text[39:])['root']
75.
76. # on récupère les cookies de la réponse s'il y en a
77. if response.cookies:
78. self.__cookies = response.cookies
79.
80. # on récupère le jeton CSRF
81. if self.__config['with_csrftoken']:
82. self.__csrf_token = résultat.get('csrf_token', None)
83.
84. # code de statut
85. status_code = response.status_code
86.
87. # si code de statut différent de 200 OK
88. if status_code != status.HTTP_200_OK:
89. raise ImpôtsError(35, résultat['réponse'])
90.
91. # on rend le résultat
92. return résultat['réponse']
93.
94.
95. def init_session(self, session_type: str):
96. # on note le type de la session
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
681/755
97. self.__session_type = session_type
98.
99. # on supprime le jeton CSRF des précédents appels
100. self.__csrf_token = None
101.
102. # on demande l'URL de l'action init-session
103. url_service = f"{self.__config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
104.
105. # exécution requête
106. self.get_response("GET", url_service)
107. …
• lignes 38-92 : la gestion du jeton CSRF se passe principalement dans la méthode [get_response] ;
• ligne 60 : le point important est le paramètre [allow_redirects=True]. C’est sa valeur par défaut mais on a tenu à le mettre
en relief ;
o les clients commencent leur dialogue avec le serveur par l’appel à la route [/init-session /type_response] ;
o le serveur répond à cette requête par une redirection vers la route [/init-session/type_response] ;
o à cause du paramètre [allow_redirects=True], cette redirection va être suivie par le client [requests] ;
o il n’y a pas de jeton CSRF à récupérer aux lignes 81-82. La propriété [self.__csrf_token] reste alors toujours à None
(ligne 36) ;
• lignes 51-52 : pour toutes les requêtes suivantes, le jeton CSRF, s’il existe, est ajouté à la route initiale ;
• lignes 81-82 : le nouveau jeton que génère le serveur à chaque nouvelle requête du client est mémorisé localement pour être
renvoyé ligne 52 à la requête suivante ;
Il faut se rappeler ici qu’on a créé une route [/init-session-without-csrftoken/<type-response>] pour initialiser le dialogue client
/ serveur sans jeton CSRF. Or nous avons vu que la méthode [get_response] appelée ligne 12 du code ajoute systématiquement à la
fin de l’URL de service le jeton CSRF mémorisé dans [self.__csrf_token]. C’est pourquoi ligne 6 du code on supprime ce jeton
CSRF s’il existait.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
682/755
Voici comme exemple les logs obtenus à l’exécution du client [main json] avec [with_csrftoken=True] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
683/755
0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14,
"décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0,
"surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2,
"salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}],
"csrf_token":
"IjIxNmU4MDQyZDFmZmIyZDlmZjE4MzNlNDUzYzFjMGYxMWYxYzEwNGYi.Xy63sw.fgs6Cm2owsJf4NjTm7gKrVESabI"}
20. 2020-08-08 16:33:23.457912, Thread-4 : {"action": "fin-session", "état": 400, "réponse": "session
réinitialisée", "csrf_token": "IjQ0ZDQxODgzN2M5NjRiYWI0NjA2MTk5YWFkNGFhMzY1M2IxNWMyNDIi.Xy63sw.mOa5MKXvJ-
EXf_qEok-OqC5j_mg"}
21. 2020-08-08 16:33:23.458442, Thread-4 : fin du calcul de l'impôt des 1 contribuables
22. 2020-08-08 16:33:23.459045, Thread-3 : {"action": "fin-session", "état": 400, "réponse": "session
réinitialisée", "csrf_token":
"ImQ0NDZlYmViYjY1ZDUxYzJhMTNmM2JiZTRkMjBjZGJkYzE0OGVkYzMi.Xy63sw.fviTJz4zFDqVLlVlkrosT_JRPww"}
23. 2020-08-08 16:33:23.459700, Thread-3 : fin du calcul de l'impôt des 4 contribuables
24. 2020-08-08 16:33:23.460492, Thread-1 : {"action": "fin-session", "état": 400, "réponse": "session
réinitialisée", "csrf_token":
"Ijg3MjQ1NGUyYTUyOGEyNTdmZmNmYWZkMmU2OTgyMzUwNjI1YTlhZjIi.Xy63sw.I0xBl9Q8DzsuXPSgOdeARc_VKBA"}
25. 2020-08-08 16:33:23.460492, Thread-1 : fin du calcul de l'impôt des 4 contribuables
26. 2020-08-08 16:33:23.460492, MainThread : fin du calcul de l'impôt des contribuables
Si on regarde les jetons CSRF reçus successivement on voit qu’ils sont tous différents.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
684/755
35 Exercice d’application : version 15
35.1 Introduction
Cette version vise à résoudre des problèmes liés au rafraîchissement des pages de l’application dans le navigateur (F5). Prenons un
exemple. L’utilisateur vient de suprimer la simulation d’id=3 :
Après la suppression l’URL dans le navigateur est [/supprimer-simulation/3/…]. Si l’utilisateur rafraîchit la page (F5), l’URL [1] est
rejouée. On demande donc de nouveau la suppression de la simulation d’id=3. Le résultat est alors le suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
685/755
• en [1], l’URL [/calculer-impot] qui vient d’être interrogée avec un POST ;
L’opération de rafraîchissement de page rejoue la dernière requête exécutée par le navigateur, ici un [POST /calculer-impot].
Lorsqu’on demande à rejouer un POST, les navigateurs émettent un avertissement analogue à celui ci-dessus. Celui-ci avertit qu’il est
en train de rejouer une action déjà faite. Supposons que ce POST ait effectué un achat, il serait malheureux de refaire celui-ci.
Par ailleurs, nous allons limiter la possibilité de l’utilisateur de taper des URL dans son navigateur. Prenons par exemple l’une des vues
précédentes :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
686/755
Les liens offerts par cette page sont :
Lorsque la vue du calcul de l’impôt sera affichée, nous n’accepterons que les actions [/lister-simulations, /fin-session,
/calculer-impot]. Si l’utilisateur tape une autre action dans son navigateur, une erreur sera déclarée. On fera ce type de vérification
pour les quatre vues de l’application.
Nous nous proposons de résoudre le problème du rafraîchissement des pages de la façon suivante :
L’intérêt de cette méthode est que le navigateur affichera l’URL ASV dans son champ d’adresse. Le rafraîchissement de la page
rejouera alors l’action ASV. Celle-ci ne modifie pas l’état de l’application et utilise un modèle en session. Donc la même page sera
réaffichée sans effets de bord. Finalement, à cause des redirections, l’utilisateur ne verra que des URL d’actions ASV dans son
navigateur et aura l’impression de naviguer de page en page ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
687/755
35.2 Implémentation
Le dossier [impots/http-servers/10] est obtenu initialement par recopie du dossier [impots/http-servers/09]. Il est ensuite
modifié.
Dans les deux fichiers [routes_with_csrftoken] et [routes_without_csrftoken], il nous faut créer les quatre routes des quatre
actions ASV qui affichent les quatre vues. Les autres routes restent à l’identique.
Dans [routes_with_csrftoken] :
1. # afficher-vue-calcul-impot
2. @app.route('/afficher-vue-calcul-impot/<string:csrf_token>', methods=['GET'])
3. def afficher_vue_calcul_impot(csrf_token: str) -> tuple:
4. # on exécute le contrôleur associé à l'action
5. return front_controller()
6.
7. # afficher-vue-authentification
8. @app.route('/afficher-vue-authentification/<string:csrf_token>', methods=['GET'])
9. def afficher_vue_authentification(csrf_token: str) -> tuple:
10. # on exécute le contrôleur associé à l'action
11. return front_controller()
12.
13. # afficher-vue-liste-simulations
14. @app.route('/afficher-vue-liste-simulations/<string:csrf_token>', methods=['GET'])
15. def afficher_vue_liste_simulations(csrf_token: str) -> tuple:
16. # on exécute le contrôleur associé à l'action
17. return front_controller()
18.
19. # afficher-vue-liste_erreurs
20. @app.route('/afficher-vue-liste-erreurs/<string:csrf_token>', methods=['GET'])
21. def afficher_vue_liste_erreurs(csrf_token: str) -> tuple:
22. # on exécute le contrôleur associé à l'action
23. return front_controller()
Lignes 1-23, nous avons créé quatre routes pour quatre actions ASV :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
688/755
• [/afficher-vue-liste-simulations], ligne 14, affiche la vue des simulations ;
• [/afficher-vue-liste-erreurs], ligne 20, affiche la vue des erreurs inattendues ;
On a dit que les actions ASV n’avaient aucun paramètre et ne modifiaient pas l’état de l’application. On se contente d’afficher la vue
souhaitée en positionnant, ligne 14, un code d’état.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
689/755
5.
6. class AfficherVueCalculImpotController(InterfaceController):
7.
8. def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
9. # on récupère les éléments du path
10. params = request.path.split('/')
11. action = params[1]
12.
13. # changement de vue - juste un code d'état à positionner
14. return {"action": action, "état": 1400, "réponse": ""}, status.HTTP_200_OK
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
690/755
35.2.3 La nouvelle configuration MVC
Parce qu’elle devenait trop importante, la configuration MVC a été éclatée sur quatre fichiers :
Pas de surprise.
Le fichier [controllers] des contrôleurs est également sans surprise. On y a simplement ajouté les nouveaux contrôleurs des actions
ASV.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
691/755
10. from CalculerImpotsController import CalculerImpotsController
11. from FinSessionController import FinSessionController
12. from GetAdminDataController import GetAdminDataController
13. from InitSessionController import InitSessionController
14. from ListerSimulationsController import ListerSimulationsController
15. from SupprimerSimulationController import SupprimerSimulationController
16. from AfficherCalculImpotController import AfficherCalculImpotController
17.
18. # les contrôleurs d'actions ASV
19. from AfficherVueCalculImpotController import AfficherVueCalculImpotController
20. from AfficherVueAuthentificationController import AfficherVueAuthentificationController
21. from AfficherVueListeErreursController import AfficherVueListeErreursController
22. from AfficherVueListeSimulationsController import AfficherVueListeSimulationsController
23.
24. # actions autorisées et leurs contrôleurs
25. controllers = {
26. # initialisation d'une session de calcul
27. "init-session": InitSessionController(),
28. # authentification d'un utilisateur
29. "authentifier-utilisateur": AuthentifierUtilisateurController(),
30. # lien vers vue calcul impot
31. "afficher-calcul-impot": AfficherCalculImpotController(),
32. # calcul de l'impôt en mode individuel
33. "calculer-impot": CalculerImpotController(),
34. # calcul de l'impôt en mode lots
35. "calculer-impots": CalculerImpotsController(),
36. # liste des simulations
37. "lister-simulations": ListerSimulationsController(),
38. # suppression d'une simulation
39. "supprimer-simulation": SupprimerSimulationController(),
40. # fin de la session de calcul
41. "fin-session": FinSessionController(),
42. # obtention des données de l'administration fiscale
43. "get-admindata": GetAdminDataController(),
44. # main controller
45. "main-controller": MainController(),
46. # affichage de la vue d'authentification
47. "afficher-vue-authentification": AfficherVueAuthentificationController(),
48. # affichage de la vue de calcul de l'impôt
49. "afficher-vue-calcul-impot": AfficherVueCalculImpotController(),
50. # affichage de la vue des simulations
51. "afficher-vue-liste-simulations": AfficherVueListeSimulationsController(),
52. # affichage de la vue des erreurs
53. "afficher-vue-liste-erreurs": AfficherVueListeErreursController()
54. }
55.
56. # on rend la configuration des contrôeleurs
57. return {
58. # contrôleurs
59. "controllers": controllers,
60. }
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
692/755
24. 1200, # /afficher-vue-liste-simulations
25. ],
26. "view_name": "views/vue-liste-simulations.html",
27. },
28. {
29. # vue de la liste des erreurs
30. "états": [
31. 1300, # /afficher-vue-liste-erreurs
32. ],
33. "view_name": "views/vue-erreurs.html",
34. },
35. ]
36.
37. # on rend la configuration ASV
38. return {
39. # vues et modèles
40. "asv": asv,
41. }
• le fichier [asv_actions] rassemble les quatre nouvelles actions dont on rappelle le fonctionnement :
o elles n’ont aucun paramètre ;
o elles affichent une vue précise dont le modèle est en session ;
• la liste [asv] des lignes 6-35, associent une vue à chaque action ASV ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
693/755
51. ]
52.
53. # vue des erreurs inattendues
54. view_erreurs = {
55. # redirection vers action ASV
56. "to": "/afficher-vue-liste-erreurs",
57. # modèle de la vue suivante
58. "model_for_view": ModelForErreursView()
59. }
60.
61. # on rend la configuration MVC
62. return {
63. # actions ADS
64. "ads": ads,
65. # la vue des erreurs inattendues
66. "view_erreurs": view_erreurs,
67. }
• lignes 11-51 : la liste des actions ADS (Action Do Something). On retrouve toutes les actions des versions précédentes. Leur
fonctionnement a cependant changé :
o elles n’affichent pas une vue V. Elles préparent seulement le modèle M de cette vue V ;
o elles demandent l’affichage de la vue V via une redirection vers l’action ASV associée à la vue V ;
• les actions ADS ne mènent pas toutes à une redirection vers une action ASV : lignes 12-18, l’action ADS [/fin-session]
mène à une redirection vers l’action ADS [/init-session/html]. Pour distinguer les redirections ADS -> ADS et ADS ->
ASV, on peut s’aider du modèle [model_for_view]. Celui-ci n’existe pas pour les redirections ADS -> ADS ;
Le fichier [main/config] qui rassemble toutes les configurations évolue comme suit :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
694/755
35.2.4 Les nouveaux modèles
Les modèles vont générer une nouvelle information. Prenons par exemple, le modèle de la vue d’authentification :
• la nouveauté est ligne 17 : chaque modèle va générer une liste d’actions possibles lorsque la vue V dont il est le modèle M va
s’afficher. Pour connaître ces actions, il faut revenir à la vue V. Dans le cas de la vue d’authentification :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
695/755
• on voit que la vue d’authentification n’offre qu’une action, celle du bouton [Valider]. Cette action est [/authentifier-
utilisateur] ;
On a écrit :
Sur la vue ci-dessus, on voit que si l’utilisateur rafraîchit la vue, l’action [1] [/afficher-vue-authentification] va être rejouée. Il
faut donc qu’elle soit autorisée. On n’aurait pu ne pas l’autoriser auquel cas l’utilisateur aurait une erreur à chaque fois qu’il rechargerait
la page. On a estimé que ce n’était pas souhaitable.
Les actions possibles sont mis dans le modèle de la vue. On sait que ce modèle va être mis en session.
On fait cela pour chacune des quatre vues. Les actions possibles sont alors les suivantes :
Vue d’authentification
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
696/755
35.2.5 Le nouveau contrôleur principal
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
697/755
51.
52. except BaseException as exception:
53. # exceptions (inattendues)
54. résultat = {"action": action, "état": 131, "réponse": [f"{exception}"]}
55. erreur = True
56.
57. finally:
58. pass
59.
60. # erreur d'exécution
61. if erreur:
62. # requête erronée
63. status_code = status.HTTP_400_BAD_REQUEST
64.
65. if config['parameters']['with_csrftoken']:
66. # on ajoute le csrf_token au résultat
67. résultat['csrf_token'] = generate_csrf()
68.
69. ….
70.
71. # on envoie la réponse HTTP
72. return response, status_code
73.
74. @staticmethod
75. def check_action(params: list, session: LocalProxy, config: dict) -> (bool, dict, str):
76. …
77.
78. # résultat de la méthode
79. return erreur, résultat, type_response1
• lignes 39-44 : les premières vérifications sont faites sur l’action. Nous y reviendrons. La méthode statique
[MainController.check_action] rend un tuple de trois éléments :
o [erreur] : True si une erreur a été détectée, False sinon ;
o [résultat] : un résultat d’erreur si (erreur==True), None sinon ;
o [type_response1] : le type (json, xml, html, None) de la session, type trouvé en session ;
• ligne 39 : aucune vérification n’est faite si l’action est l’action ASV [afficher-vue-liste-erreurs] qui va afficher la liste des
erreurs. En effet, si une erreur était trouvée lors de cette action, on serait dirigé de nouveau vers l’action [/afficher-vue-
liste-erreurs] et on entrerait dans une boucle infinie de redirections ;
• la méthode statique [check_action] est la suivante :
1. @staticmethod
2. def check_action(params: list, session: LocalProxy, config: dict) -> (bool, dict, str):
3. # on récupère l'action en cours
4. action = params[1]
5.
6. # pas d'erreur et de résultat au départ
7. erreur = False
8. résultat = None
9.
10. # le type de session doit être connu avant certaines actions ADS
11. type_response1 = session.get('typeResponse')
12. if type_response1 is None and action != "init-session":
13. # on note l'erreur
14. résultat = {"action": action, "état": 101,
15. "réponse": ["pas de session en cours. Commencer par action [init-session]"]}
16. erreur = True
17.
18. # pour certaines actions ADS on doit être authentifié
19. user = session.get('user')
20. if user is None and action not in ["init-session",
21. "authentifier-utilisateur",
22. "afficher-vue-authentification"]:
23. # on note l'erreur
24. résultat = {"action": action, "état": 101,
25. "réponse": [f"action [{action}] demandée par utilisateur non authentifié"]}
26. erreur = True
27.
28. # à partir d'une vue seules certaines actions sont possibles
29. if not erreur and action != "init-session":
30. # pour l'instant pas d'actions possibles
31. actions_possibles = None
32. # on récupère le modèle de la future vue en session s'il existe
33. modèle = session.get('modèle')
34. # si on a trouvé un modèle, on récupère ses actions possibles
35. if modèle:
36. actions_possibles = modèle.get('actions_possibles')
37. # si on a une liste d'actions possibles, on vérifie que l'action en cours en fait partie
38. if actions_possibles and action not in actions_possibles:
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
698/755
39. # on note l'erreur
40. résultat = {"action": action, "état": 151,
41. "réponse": [f"action [{action}] incorrecte dans l'environnement actuel"]}
42. erreur = True
43.
44.
45. # erreur ?
46. if not erreur and config['parameters']['with_csrftoken']:
47. # on vérifie la validité du token csrf
48. # le csrf_token est le dernier élément du path
49. csrf_token = params.pop()
50. try:
51. # une exception sera lancée si le csrf_token n'est pas valide
52. validate_csrf(csrf_token)
53. except ValidationError as exception:
54. # csrf token invalide
55. résultat = {"action": action, "état": 121, "réponse": [f"{exception}"]}
56. # on note l'erreur
57. erreur = True
58.
59. # résultat de la méthode
60. return erreur, résultat, type_response1
• ligne 2 : la méthode [check_action] fait plusieurs vérifications sur la validité de l’action en cours ;
• lignes 6-26, 45-57 : les versions précédentes faisaient déjà ces vérifications ;
• lignes 28-42 : on ajoute une nouvelle vérification. On vérifie si l’action en cours est possible dans l’état actuel de l’application.
Si l’action en cours n’est pas possible, on génère un état 151 (ligne 40) qui nous assure que l’action actuelle va être redirigée
vers la vue des erreurs inattendues ;
Les modifications en cours ne concernent que les sessions HTML. Les sessions jSON ou XML ne sont pas impactées. La classe
[HtmlResponse] évolue de la façon suivante :
1. # dépendances
2.
3. from flask import make_response, redirect, render_template
4. from flask.wrappers import Response
5. from flask_api import status
6. from flask_wtf.csrf import generate_csrf
7. from werkzeug.local import LocalProxy
8.
9. from InterfaceResponse import InterfaceResponse
10.
11. class HtmlResponse(InterfaceResponse):
12.
13. def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
14. résultat: dict) -> (Response, int):
15. # la réponse HTML dépend du code d'état rendu par le contrôleur
16. état = résultat["état"]
17.
18. # on cherche si l'état a été produit par une action ASV
19. # auquel cas, il faut afficher une vue
20. asv_configs = config['mvc']["asv"]
21. trouvé = False
22. i = 0
23. # on parcourt la liste des vues
24. nb_views = len(asv_configs)
25. while not trouvé and i < nb_views:
26. # vue n° i
27. asv_config = asv_configs[i]
28. # états associés à la vue n° i
29. états = asv_config["états"]
30. # est-ce que l'état cherché se trouve dans les états associés à la vue n° i
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
699/755
31. if état in états:
32. trouvé = True
33. else:
34. # vue suivante
35. i += 1
36.
37. # trouvé ?
38. if trouvé:
39. # il s'agit d'une action ASV - il faut afficher une vue dont le modèle est déjà en session
40. # on génère le code HTML de la réponse
41. html = render_template(asv_config["view_name"], modèle=session['modèle'])
42. # on construit la réponse HTTP
43. response = make_response(html)
44. response.headers['Content-Type'] = 'text/html; charset=utf-8'
45. # on rend le résultat
46. return response, status_code
47.
48. # pas trouvé - il s'agit d'un code d'état d'une action ADS
49. # celle-ci va être suivie d'une redirection
50. redirected = False
51. for ads in config['mvc']['ads']:
52. # états nécessitant une redirection
53. états = ads["états"]
54. if état in états:
55. # il y a redirection
56. redirected = True
57. break
58. # dictionnaire de redirection pour le cas des erreurs inattendues
59. if not redirected:
60. ads = config['mvc']['view_erreurs']
61.
62. # est-ce une redirection vers une action ASD ou ASV ?
63. # s'il y a un modèle, alors il s'agit d'une redirection vers une action ASV
64. # il faut alors calculer le modèle de la vue V qui sera affichée par l'action ASV
65. model_for_view = ads.get("model_for_view")
66. if model_for_view:
67. # calcul du modèle de la vue suivante
68. modèle = model_for_view.get_model_for_view(request, session, config, résultat)
69. # le modèle est mis en session pour la vue suivante
70. session['modèle'] = modèle
71.
72. # maintenant il faut générer l'URL de redirection sans oublier le jeton CSRF s'il est demandé
73. if config['parameters']['with_csrftoken']:
74. csrf_token = f"/{generate_csrf()}"
75. else:
76. csrf_token = ""
77.
78. # réponse de redirection
79. return redirect(f"{ads['to']}{csrf_token}"), status.HTTP_302_FOUND
• lignes 18-35 : on cherche si l’état produit par la dernière action exécutée est celui d’une action ASV ;
• lignes 36-46 : si oui, la vue V associée à l’action ASV est affichée avec pour modèle le modèle trouvé en session associé à la
clé [‘modèle’] ;
• lignes 48-60 : lorsqu’on arrive là, on sait que l’état produit par la dernière action exécutée est celui d’une action ADS. Il va
alors provoquer une redirection. On cherche dans le fichier de configuration, la définition de celle-ci ;
• ligne 62 : lorsqu’on est là, on a la configuration de la redirection à faire. Il y a deux cas :
o il s’agit d’une redirection vers une autre action ADS. Il n’y a alors pas de modèle de vue à calculer ;
o il s’agit d’une redirection vers une action ASV. Il y a alors un modèle de vue à calculer (lignes 67-68). Ce modèle est
ensuite mis en session (ligne 70) ;
• lignes 72-76 : on calcule l’URL de redirection ;
• lignes 78-79 : on envoie la réponse de redirection au client ;
35.3 Tests
Faites les tests suivants avec un navigateur :
• utilisez normalement l’application. Vérifiez que les seules URL affichées par le navigateur sont des URL ASV [/afficher-
vue-nom_de_la_vue] ;
• rafraîchissez les pages (F5) et constatez que la même page se réaffiche alors. Il n’y a aucun effet de bord ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
700/755
Par ailleurs, utilisez le client [impots/http-clients/09]. Comme les modifications faites concernent uniquement les sessions HTML,
les clients [main, main2, main3, Test1HttpClientDaoWithSession, Test2HttpClientDaoWithSession] doivent continuer à
fonctionner.
A la place de [1], on tape l’URL [/supprimer-simulation/1]. L’action [/supprimer-simulation] ne fait pas partie des actions
proposées par la vue qui sont les actions 1-4. Elle va donc être refusée. La réponse du serveur est la suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
701/755
36 Exercice d’application : version 16
36.1 Introduction
Les URL de notre application sont pour l’instant de la forme [/action/param1/param2/…]. Nous voudrions pouvoir préfixer ces URL.
Par exemple avec le préfixe [/do], nous aurions des URL de la forme [/do/action/param1/param2/…].
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
702/755
29. # actions ADS (Action Do Something)
30. import ads_actions
31. config['mvc'].update(ads_actions.configure(config))
32.
33. # configuration des réponses HTTP
34. import responses
35. config['mvc'].update(responses.configure(config))
36.
37. # configuration des routes
38. import routes
39. routes.configure(config)
40.
41. # on rend la configuration
42. return config
• lignes 37-39 : le module [routes] (ligne 38) s’occupe de configurer les routes (ligne 39) ;
Le fichier des routes sans jeton CSRF [configs/routes_without_csrftoken] évolue de la façon suivante :
1. # dépendances
2.
3. from flask import redirect, request, session, url_for
4. from flask_api import status
5.
6. # configuration application
7. config = {}
8.
9. # le front controller
10. def front_controller() -> tuple:
11. # on fait suivre la requête au contrôleur principal
12. main_controller = config['mvc']['controllers']['main-controller']
13. return main_controller.execute(request, session, config)
14.
15. # racine de l'application
16. def index() -> tuple:
17. # redirection vers /init-session/html
18. return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)
19.
20. # init-session
21. def init_session(type_response: str) -> tuple:
22. # on exécute le contrôleur associé à l'action
23. return front_controller()
24.
25. # authentifier-utilisateur
26. def authentifier_utilisateur() -> tuple:
27. # on exécute le contrôleur associé à l'action
28. return front_controller()
29.
30. # calculer-impot
31. def calculer_impot() -> tuple:
32. # on exécute le contrôleur associé à l'action
33. return front_controller()
34.
35. …
Le fichier a été débarrassé de ses routes. Il ne reste que les fonctions associées à celles-ci.
Les fichier des routes avec jeton CSRF [configs/routes_with_csrftoken] subit le même sort :
1. # dépendances
2.
3. from flask import redirect, request, session, url_for
4. from flask_api import status
5. from flask_wtf.csrf import generate_csrf
6.
7. # configuration
8. config = {}
9.
10. # le front controller
11. def front_controller() -> tuple:
12. # on fait suivre la requête au contrôleur principal
13. main_controller = config['mvc']['controllers']['main-controller']
14. return main_controller.execute(request, session, config)
15.
16. # racine de l'application
17. def index() -> tuple:
18. # redirection vers /init-session/html
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
703/755
19. return redirect(url_for("init_session", type_response="html", csrf_token=generate_csrf()), status.HTTP_302_FOUND)
20.
21. # init-session
22. def init_session(type_response: str, csrf_token: str) -> tuple:
23. # on exécute le contrôleur associé à l'action
24. return front_controller()
25.
26. # authentifier-utilisateur
27. def authentifier_utilisateur(csrf_token: str) -> tuple:
28. # on exécute le contrôleur associé à l'action
29. return front_controller()
30.
31. …
32.
33. # init-session-without-csrftoken pour les clients json et xml
34. def init_session_without_csrftoken(type_response: str) -> tuple:
35. # redirection vers /init-session/type_response
36. return redirect(url_for("init_session", type_response=type_response, csrf_token=generate_csrf()),
37. status.HTTP_302_FOUND)
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
704/755
58. view_func=routes.lister_simulations)
59.
60. # supprimer-simulation
61. app.add_url_rule(f'{prefix_url}/supprimer-simulation/<int:numero>{csrftoken_param}', methods=['GET'],
62. view_func=routes.supprimer_simulation)
63.
64. # fin-session
65. app.add_url_rule(f'{prefix_url}/fin-session{csrftoken_param}', methods=['GET'],
66. view_func=routes.fin_session)
67.
68. # afficher-calcul-impot
69. app.add_url_rule(f'{prefix_url}/afficher-calcul-impot{csrftoken_param}', methods=['GET'],
70. view_func=routes.afficher_calcul_impot)
71.
72. # get-admindata
73. app.add_url_rule(f'{prefix_url}/get-admindata{csrftoken_param}', methods=['GET'],
74. view_func=routes.get_admindata)
75.
76. # afficher-vue-calcul-impot
77. app.add_url_rule(f'{prefix_url}/afficher-vue-calcul-impot{csrftoken_param}', methods=['GET'],
78. view_func=routes.afficher_vue_calcul_impot)
79.
80. # afficher-vue-authentification
81. app.add_url_rule(f'{prefix_url}/afficher-vue-authentification{csrftoken_param}', methods=['GET'],
82. view_func=routes.afficher_vue_authentification)
83.
84. # afficher-vue-liste-simulations
85. app.add_url_rule(f'{prefix_url}/afficher-vue-liste-simulations{csrftoken_param}', methods=['GET'],
86. view_func=routes.afficher_vue_liste_simulations)
87.
88. # afficher-vue-liste_erreurs
89. app.add_url_rule(f'{prefix_url}/afficher-vue-liste-erreurs{csrftoken_param}', methods=['GET'],
90. view_func=routes.afficher_vue_liste_erreurs)
91.
92. # cas particulier
93. if with_csrftoken:
94. # init-session-without-csrftoken pour les clients json et xml
95. app.add_url_rule(f'{prefix_url}/init-session-without-csrftoken', methods=['GET'],
96. view_func=routes.init_session_without_csrftoken)
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
705/755
36.3 Les nouveaux contrôleurs
Dans la version précédente, tous les contrôleurs obtenaient l’action en cours de traitement de la façon suivante :
1. def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
2. # on récupère les éléments du path
3. params = request.path.split('/')
4. action = params[1]
Ce code ne fonctionne plus s’il y a un préfixe, par exemple [/do/lister-simulations]. Dans ce cas, l’action, ligne 3 serait [do] et
donc serait incorrecte.
1. def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
2. # on récupère les éléments du path
3. prefix_url = config["parameters"]["prefix_url"]
4. params = request.path[len(prefix_url):].split('/')
5. action = params[1]
Dans la version précédente, chaque classe de modèle générait un modèle de la façon suivante :
1. def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
2. # on encapsule les données de la pagé dans modèle
3. modèle = {}
4. # état de l'application
5. état = résultat["état"]
6. # le modèle dépend de l'état
7. …
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
706/755
8.
9. # jeton csrf
10. modèle['csrf_token'] = super().get_csrftoken(config)
11.
12. # actions possibles à partir de la vue
13. modèle['actions_possibles'] = […]
14.
15. # on rend le modèle
16. return modèle
1. def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
2. # on encapsule les données de la pagé dans modèle
3. modèle = {}
4. # état de l'application
5. état = résultat["état"]
6. # le modèle dépend de l'état
7. …
8.
9. # jeton csrf
10. modèle['csrf_token'] = super().get_csrftoken(config)
11.
12. # actions possibles à partir de la vue
13. modèle['actions_possibles'] = […]
14.
15. # préfixe des URL
16. modèle["prefix_url"] = config["parameters"]["prefix_url"]
17.
18. # on rend le modèle
19. return modèle
Le fragment [v-authentification]
1. <!-- formulaire HTML - on poste ses valeurs avec l'action [authentifier-utilisateur] -->
2. <form method="post" action="{{modèle.prefix_url}}/authentifier-utilisateur{{modèle.csrf_token}}">
3.
4. <!-- titre -->
5. <div class="alert alert-primary" role="alert">
6. <h4>Veuillez vous authentifier</h4>
7. </div>
8. …
9.
10. </form>
Le fragment [v-calcul-impot]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
707/755
Le fragment [v-liste-simulations]
1. …
2.
3. {% if modèle.simulations is defined and modèle.simulations|length!=0 %}
4. …
5.
6. <!-- tableau des simulations -->
7. <table class="table table-sm table-hover table-striped">
8. …
9. <tr>
10. <th scope="row">{{simulation.id}}</th>
11. <td>{{simulation.marié}}</td>
12. <td>{{simulation.enfants}}</td>
13. <td>{{simulation.salaire}}</td>
14. <td>{{simulation.impôt}}</td>
15. <td>{{simulation.surcôte}}</td>
16. <td>{{simulation.décôte}}</td>
17. <td>{{simulation.réduction}}</td>
18. <td>{{simulation.taux}}</td>
19. <td><a href="{{modèle.prefix_url}}/supprimer-simulation/{{simulation.id}}{{modèle.csrf_token}}">Supprimer</a></td>
20. </tr>
21. {% endfor %}
22. </tr>
23. </tbody>
24. </table>
25. {% endif %}
Le fragment [v-menu]
Dans la précédente version le code de la réponse HTML se terminait par une redirection :
1. class HtmlResponse(InterfaceResponse):
2.
3. def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
4. résultat: dict) -> (Response, int):
5. # la réponse HTML dépend du code d'état rendu par le contrôleur
6. état = résultat["état"]
7.
8. …
9.
10. # maintenant il faut générer l'URL de redirection sans oublier le jeton CSRF s'il est demandé
11. if config['parameters']['with_csrftoken']:
12. csrf_token = f"/{generate_csrf()}"
13. else:
14. csrf_token = ""
15.
16. # réponse de redirection
17. return redirect(f"{ads['to']}{csrf_token}"), status.HTTP_302_FOUND
1. def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
2. résultat: dict) -> (Response, int):
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
708/755
3. …
4.
5. # maintenant il faut générer l'URL de redirection sans oublier le jeton CSRF s'il est demandé
6. if config['parameters']['with_csrftoken']:
7. csrf_token = f"/{generate_csrf()}"
8. else:
9. csrf_token = ""
10.
11. # réponse de redirection
12. return redirect(f"{config['parameters']['prefix_url']}{ads['to']}{csrf_token}"), status.HTTP_302_FOUND
36.7 Tests
1. …
2. # token csrf
3. "with_csrftoken": True,
4. # bases gérées MySQL (mysql), PostgreSQL (pgres)
5. "databases": ["mysql", "pgres"],
6. # préfixe des URL de l'application
7. # mettre la chaîne vide si on ne veut pas de préfixe ou /préfixe sinon
8. "prefix_url": "/do",
9. …
L’application peut être également testée avec les tests console de [http-clients/09] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
709/755
Dans les fichiers de configuration [1] et [2], le préfixe des URL doit être inclus dans l’URL du serveur :
1. "server": {
2. # "urlServer": "http://127.0.0.1:5000",
3. "urlServer": "http://127.0.0.1:5000/do",
4. "user": {
5. "login": "admin",
6. "password": "admin"
7. },
8. "url_services": {
9. …
10. }
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
710/755
37 Exercice d’application : version 17
On rappelle que la gestion des dépendances de l’application est faite dans le script [syspath]. Dans la version précédente, ce script
était le suivant :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
711/755
31. # contrôleurs
32. f"{script_dir}/../controllers",
33. # réponses HTTP
34. f"{script_dir}/../responses",
35. # modèles des vues
36. f"{script_dir}/../models_for_views",
37. ]
38.
39. # on fixe le syspath
40. from myutils import set_syspath
41. set_syspath(absolute_dependencies)
42.
43. # on rend la configuration
44. return {
45. "root_dir": root_dir,
46. "script_dir": script_dir
47. }
Il faut relocaliser toutes les dépendances dont le nom absolu dépend de la variable [root_dir] de la ligne 8, ç-à-d les lignes 13-26.
• lignes 8-27 : toutes les dépendances sont désormais relatives à la variable [script_dir] de la ligne 5 ;
• lignes 42-45 : la variable [root_dir] a disparu de la configuration du syspath ;
• ligne 10 : les entités de l’application sont dans le dossier [entities] [1] ;
• ligne 12 : la couche [dao] est dans le dossier [layers/dao] [2] ;
• ligne 14 : la couche [métier] est dans le dossier [layers/métier] [2] ;
• ligne 16 : les utilitaires [Logger, SendMail] sont dans le dossier [utilities] [3] ;
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
712/755
• lignes 29-40 : on calcule le Python Path de l’application sans importer le module [myutils] ;
37.2 Tests
A ce stade, la version 17 doit fonctionner. Vérifiez-le.
37.3 Portage d’une application Python / Flask sur un serveur Apache / Windows
37.3.1 Sources
Pour réaliser le portage d’une application Flask sur Apache / Windows, j’ai dû chercher sur Internet. Voici le lien qui m’a aidé à
démarrer : [https://medium.com/@madumalt/flask-app-deployment-in-windows-apache-server-mod-wsgi-82e1cfeeb2ed] ;
J’ai utilisé les informations de ce lien sauf pour la configuration du serveur Apache. Pour celle-ci j’ai utilisé un exemple de
configuration du serveur Apache de Laragon.
Pour que le serveur Apache puisse héberger une application Python, il nous faut installer le module Python [mod_wsgi]. L’installation
de ce module est délicate parce qu’au cours de cette installation une compilation C++ a lieu. Pour réussir l’installation, il faut un
compilateur Microsoft C++. Une solution simple est d’installer la version courante de Visual Studio Community
[https://visualstudio.microsoft.com/fr/vs/community/].
Si on n’a pas l’utilité de Visual Studio autrement que pour [mod_wsgi], on peut limiter l’installation à l’environnement C++ :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
713/755
Une fois le compilateur C++ installé, l’installation du module [mod_wsgi] se fait dans un terminal PyCharm :
1. (venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\11>SET
MOD_WSGI_APACHE_ROOTDIR=C:\MyPrograms\laragon\bin\apache\httpd-2.4.35-win64-VC15
2.
3. (venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\11>pip install mod_wsgi
4. Collecting mod_wsgi
5. Using cached mod_wsgi-4.7.1.tar.gz (498 kB)
6. Using legacy setup.py install for mod-wsgi, since package 'wheel' is not installed.
7. Installing collected packages: mod-wsgi
8. Running setup.py install for mod-wsgi ... done
9. Successfully installed mod-wsgi-4.7.1
• ligne 1 : on fixe la valeur de la variable d’environnement [MOD_WSGI_APACHE_ROOTDIR]. Cette valeur est l’emplacement du
serveur Apache dans le système de fichiers. Ici cet emplacement est [<laragon>\bin\apache\httpd-2.4.35-win64-VC15] où
<laragon> est le dossier d’installation de Laragon. Vous pouvez obtenir cet emplacement de diverses façons. En voici
une obtenue à partir d’une des options de Laragon :
En [1-3], le fichier [httpd.conf] est le fichier de configuration principal du serveur Apache. Le fichier en question est alors
ouvert dans un éditeur de texte (Notepad ++ ci-dessous) :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
714/755
En [2], le dossier d’installation d’Apache est ce qui précède la chaîne [conf\httpd.conf].
1. …
2. IncludeOptional "C:/MyPrograms/laragon/etc/apache2/alias/*.conf"
3. IncludeOptional "C:/MyPrograms/laragon/etc/apache2/sites-enabled/*.conf"
4. Include "C:/MyPrograms/laragon/etc/apache2/httpd-ssl.conf"
5. Include "C:/MyPrograms/laragon/etc/apache2/mod_php.conf"
6.
7. # python mod_wsgi
8. LoadModule wsgi_module "c:/data/st-2020/dev/python/cours-2020/python3-flask-2020/venv/lib/site-
packages/mod_wsgi/server/mod_wsgi.cp38-win_amd64.pyd"
• la ligne 8 a été ajoutée à l’existant du fichier [httpd.conf] en fin de fichier. Elle indique au serveur Apache où se trouve un
élément du module [mod_wsgi] que nous venons d’installer ;
Une façon simple d’avoir le chemin de la ligne 8 est d’exécuter la commande suivante dans un terminal PyCharm :
1. (venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\11>mod_wsgi-express
module-config
2. LoadModule wsgi_module "c:/data/st-2020/dev/python/cours-2020/python3-flask-2020/venv/lib/site-
packages/mod_wsgi/server/mod_wsgi.cp38-win_amd64.pyd"
3. WSGIPythonHome "c:/data/st-2020/dev/python/cours-2020/python3-flask-2020/venv"
Certaines documentations disent qu’il faut ajouter les lignes 2 et 3 à la fin du fichier [httpd.conf]. Dans mon cas, la ligne 3 ci-dessus
provoquait une erreur (module [encodings] manquant). Aussi n’a-t-elle pas été mise dans le fichier [httpd.conf]. Seule la ligne 2 y a
été mise. La signification des différents paramètres du module [mod_wsgi] utilisables dans les fichiers de configuration d’Apache sont
décrits |ici|.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
715/755
Ensuite, nous allons activer le protocole HTTPS du serveur Apache :
Pour configurer Apache de façon à servir une application Flask, le lien référencé |plus haut|utilise des serveurs virtuels. Laragon
propose également de gérer des serveurs virtuels :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
716/755
• en [1-3], on crée un projet PHP vide ;
• en [4-8], Laragon a créé un site virtuel appelé [auto.projet-test.test] configuré par le fichier [auto.projet-
test.test.conf] [8] du dossier [sites-enabled] [7]. Ce dossier se trouve à l’adresse [<laragon>\etc\apache2\sites-
enabled] où [laragon] est le dossier d’installation de Laragon ;
Bien que ça n’entre pas dans ce qui est fait en ce moment, vous pouvez avoir la curiosité d’aller voir ce qu’est ce site web [projet-
test] que nous venons de créer :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
717/755
• en [1-5], un projet vide a été créé. C’est un projet PHP situé dans le dossier [<laragon>/www] où [laragon] est le dossier
d’installation de Laragon ;
Maintenant examinons le fichier [auto.projet-test.test.conf] généré par Laragon dans le dossier [<laragon>\etc\apache2\sites-
enabled] :
Voyons comment fonctionne un serveur virtuel. Lançons tout d’abord le serveur Apache et PHP :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
718/755
• en [1], l’URL demandée ;
• en [2], le protocole HTTP a été utilisé ;
• en [3], parce que le projet [projet-test] est vide, on obtient l’index de son dossier (liste de son contenu), index vide ;
• en [1-2], on obtient la même réponse que précédemment mais avec le protocole HTTPS [1] ;
La création du serveur virtuel [projet-test.test] a créé une nouvelle entrée dans le fichier
[<windows>/system32/drivers/etc/hosts] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
719/755
17. # 38.25.63.10 x.acme.com # x client host
18.
19. # localhost name resolution is handled within DNS itself.
20. # 127.0.0.1 localhost
21. # ::1 localhost
22.
23. 127.0.0.1 projet-test.test #laragon magic!
• ligne 23 : l’adresse IP du nom [projet-test.test] est 127.0.0.1, ç-à-d l’adresse de [localhost] (ligne 20), la machine locale.
Ainsi lorsque dans un navigateur on tape l’URL [http://projet-test.test/chemin], la requête est envoyée à l’adresse
127.0.0.1 sur le port 80. C’est alors le serveur Apache de la machine locale (localhost) qui répond.
On peut se demander pourquoi lorsqu’on tape la requête [http://projet-test.test/], le serveur Apache utilise la configuration du fichier
[<laragon>\etc\apache2\sites-enabled\auto.projet-test.test.conf] :
Pour le comprendre, il faut voir ce qu’envoie le navigateur au serveur Apache lorsqu’on fait cette requête. Faisons-la avec Postman :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
720/755
Nous cliquons sur le lien [5] pour désactiver la vérification SSL (Secure Sockets Layer). SSL / TSL (Transport Layer Security) est un
protocole de sécurité qui crée un canal de communication sécurisé entre deux machines de l’internet. C’est le protocole utilisé ici par
Apache. La réponse est la suivante :
Nous recevons la même page qu’avec un navigateur traditionnel. Maintenant voyons le dialogue client / serveur dans la console
Postman (Ctrl-Alt-C) :
1. GET / HTTP/1.1
2. User-Agent: PostmanRuntime/7.26.2
3. Accept: */*
4. Cache-Control: no-cache
5. Postman-Token: d153f711-ad99-4e1d-93c3-61c25374d1be
6. Host: projet-test.test
7. Accept-Encoding: gzip, deflate, br
8. Connection: keep-alive
9.
10. HTTP/1.1 200 OK
11. Date: Fri, 14 Aug 2020 09:19:24 GMT
12. Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19 mod_wsgi/4.7.1 Python/3.8
13. Content-Length: 161
14. Keep-Alive: timeout=5, max=100
15. Connection: Keep-Alive
16. Content-Type: text/html;charset=UTF-8
• ligne 6 : l’entête HTTP [Host] précise le nom du serveur ciblé par le client web. C’est le principe des serveurs virtuels. A une
même adresse IP (ici 127.0.0.1), un serveur web peut héberger plusieurs sites web avec des noms différents. L’entête HTTP
[Host] permet au client de dire à quel serveur (ici de l’adresse 127.0.0.1) il s’adresse ;
Lorsqu’il démarre, Apache lit tous les fichiers de configuration trouvés dans le dossier [[<laragon>\etc\apache2\sites-enabled] :
Chaque fichier de configuration définit un serveur virtuel. Par exemple dans le fichier [auto.projet-test.test.conf], on trouve la
ligne suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
721/755
La ligne 2 définit le serveur virtuel [projet-test.test]. Le fichier [auto.projet-test.test.conf] est la configuration de ce serveur
virtuel. Parce qu’il lit au démarrage tous les fichiers de configuration du dossier [<laragon>\etc\apache2\sites-enabled], le serveur
Apache sait qu’il existe un serveur virtuel appelé [projet-test.test]. Ainsi lorsqu’il reçoit du client Postman, la requête HTTPS :
1. GET / HTTP/1.1
2. User-Agent: PostmanRuntime/7.26.2
3. Accept: */*
4. Cache-Control: no-cache
5. Postman-Token: d153f711-ad99-4e1d-93c3-61c25374d1be
6. Host: projet-test.test
7. Accept-Encoding: gzip, deflate, br
8. Connection: keep-alive
il reconnaît que la requête est adressée au serveur virtuel [projet-test.test] (ligne 6) et que celui-ci existe. Il utilise alors la
configuration du serveur virtuel [projet-test.test] pour répondre au client Postman.
Nous avons mis dans le dossier [http-servers/12/apache/exemple] l’application développée au paragraphe |lien|, un service web
de date /heure :
1. # imports
2. import os
3. import sys
4. import time
5.
6. # il nous faut mettre dans le Python Path le dossier des modules
7. # pour portage vers apache windows
8. sys.path.insert(0, "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/venv/lib/site-packages")
9.
10. from flask import Flask, make_response, render_template
11.
12. # application Flask
13. script_dir = os.path.dirname(os.path.abspath(__file__))
14. application = Flask(__name__, template_folder=f"{script_dir}")
15.
16. # Home URL
17. @application.route('/')
18. def index():
19. # envoi heure au client
20. # time.localtime : nb de millisecondes depuis 01/01/1970
21. # time.strftime permet de formater l'heure et la date
22. # format affichage date-heure
23. # d: jour sur 2 chiffres
24. # m: mois sur 2 chiffres
25. # y : année sur 2 chiffres
26. # H : heure 0,23
27. # M : minutes
28. # S: secondes
29.
30. # date / heure du moment
31. time_of_day = time.strftime('%d/%m/%y %H:%M:%S', time.localtime())
32. # on génère le document à envoyer au client
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
722/755
33. page = {"date_heure": time_of_day}
34. document = render_template("date_time_server.html", page=page)
35. print("document", type(document), document)
36. # réponse HTTP au client
37. response = make_response(document)
38. print("response", type(response), response)
39. return response
40.
41. # main seulement
42. if __name__ == '__main__':
43. application.config.update(ENV="development", DEBUG=True)
44. application.run()
L’application Flask est référencée par l’identifiant [application] (lignes 14, 43, 44). Ce nom est obligatoire. Si on référence
l’application Flask avec un autre identifiant l’application ne va pas marcher avec un message d’erreur indiquant qu’elle ne trouve pas
l’URL demandée. Ce message d’erreur ne donne aucune indication sur la source de l’erreur. Il faut donc être vigilant sur ce point.
1. <!DOCTYPE html>
2. <html lang="fr">
3. <head>
4. <meta charset="UTF-8">
5. <title>Date et heure du moment</title>
6. </head>
7. <body>
8. <b>Date et heure du moment : {{page.date_heure}}</b>
9. </body>
10. </html>
• [date-time-server] sera le serveur virtuel qui hébergera cette application. Il sera configuré par le fichier
[<laragon>\etc\apache2\sites-enabled\date-time-server.conf] (on rappelle que ce nom est libre – Apache lit tous les
fichiers présents dans [sites-enabled] pour découvrir les sites virtuels hébergés) ;
Nous obtenons ce fichier tout d’abord par recopie du fichier [auto.projet-test.test.conf] puis nous le modifions.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
723/755
23.
24. # URL sécurisées avec HTTPS
25. <VirtualHost *:443>
26. # avec l'alias / les URL auront la forme http(s)://date-time-server/chemin/...
27. WSGIScriptAlias / "${ROOT}/date_time_server.py"
28. DocumentRoot "${ROOT}"
29. ServerName ${SITE}
30. ServerAlias *.${SITE}
31. <Directory "${ROOT}">
32. AllowOverride All
33. Require all granted
34. </Directory>
35.
36. SSLEngine on
37. SSLCertificateFile C:/MyPrograms/laragon/etc/ssl/laragon.crt
38. SSLCertificateKeyFile C:/myprograms/laragon/etc/ssl/laragon.key
39.
40. </VirtualHost>
On ajoute la ligne 25, pour donner l’adresse IP [127.0.0.1] au serveur virtuel [date-time-server].
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
724/755
Nous demandons ensuite l’URL [https://date-time-server] avec un navigateur :
La modification n’est pas prise en compte immédiatement par le serveur Apache. Il faut le recharger :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
725/755
37.5 Portage de l’application de calcul de l’impôt sur Apache / Windows
Le dossier [apache] [2] est obtenu initialement par recopie du dossier [main]. Il est important qu’ils soient au même niveau pour
que les chemins du script [syspath.py] copié de [1] vers [2] restent valides. Pour ne pas polluer l’application [impots / http-
servers/ 12] qui fonctionne, nous mettons dans [apache] la configuration qui sera exécutée par le serveur Apache ;
Le script principal [main] recevait un paramètre [mysql / pgres] qui lui disait quel SGBD utilisé. Le script
[main_withmysql] utilise le SGBD MySQL :
• le fichier [main_withpgres] de [2] est le fichier [main] de [1] avec les modifications suivantes : il utilise le SGBD
PostgreSQL :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
726/755
Ligne 7, on fixe le SGBD à PostgreSQL.
Ceci fait, on crée le script [main_withmysql.wsgi] (le suffixe utilisé n’a pas d’importance) suivant :
1. # dossier de ce fichier
2. import os
3. script_dir = os.path.dirname(os.path.abspath(__file__))
4.
5. # on l'ajoute au syspath pour que l'import qui suit soit possible
6. import sys
7. sys.path.insert(0, script_dir)
8.
9. # on importe l'application Flask [app] en lui donnant le nom [application]
10. from main_withmysql import app as application
Le script [main_withmysql.wsgi] sera la cible exécutée par le serveur Apache en mode WSGI :
• la cible du serveur Apache aurait pu être le script [main_withmysql.py] comme il a été fait précédemment avec le script
[date_time_server.py]. Mais il aurait fallu le modifier un peu :
o contrairement au mode d’exécution avec un script console, avec Apache, le dossier contenant la cible
[main_withmysql.py] ne fait pas partie du Python Path. Aussi, la ligne 6 du script [main_withmysql.py] provoque-t-elle
une erreur ;
o la seconde modification qu’il aurait fallu faire est que dans [main_withmysql] l’application Flask est référencée par
l’identifiant [app]. On sait que pour Apache / WSGI elle doit être également référencée par un identifiant
[application] ;
• plutôt que de modifier [main_withmysql.py], on change la cible d’Apache. Ce sera désormais le script [main_withmysql.wsgi]
ci-dessus :
o lignes 1-7 : on met le dossier du script dans le Python Path. Du coup, la ligne 6 de [main_withmysql.py] ne provoque
plus d’erreur ;
o lignes 9-10 : l’importation de [main_withmysql.py] provoque son exécution. Par ailleurs on référence l’application Flask
[app] trouvée dans [main_withmysql.py] avec l’identificateur [application] dont a besoin Apache en mode WSGI ;
1. # dossier de ce fichier
2. import os
3. script_dir = os.path.dirname(os.path.abspath(__file__))
4.
5. # on l'ajoute au syspath pour que l'import qui suit soit possible
6. import sys
7. sys.path.insert(0, script_dir)
8.
9. # on importe l'application Flask [app] en lui donnant le nom [application]
10. from main_withpgres import app as application
Nous avons désormais les cibles exécutables pour le serveur Apache. Il nous faut maintenant créer deux serveurs virtuels, un pour
chaque cible.
Dans [<laragon>\etc\apache2\sites-enabled], nous créons le fichier [flask-impots-withmysql.conf] (le nom donné n’a pas
d’importance) :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
727/755
4. # nom du site web configuré par ce fichier
5. # ici il s'appellera flask-impots-withmysql
6. # les URL seront du type http(s)://flask-impots-withmysql/chemin
7. define SITE "flask-impots-withmysql"
8.
9. # mettre l'adresse IP 127.0.0.1 pour site SITE dans c:/windows/system32/drivers/etc/hosts
10.
11. # mettre ici les chemins des bibliothèques Python à utiliser - les séparer par des virgules
12. # ici les bibliothèques d'un environnement virtuel Python
13. WSGIPythonPath "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/venv/lib/site-packages"
14.
15. # Python Home - nécessaire uniquement s'il y a plusieurs versions de Python installées
16. # WSGIPythonHome "C:/Program Files/Python38"
17.
18. # URL HTTP
19. <VirtualHost *:80>
20. # avec l'alias / les URL auront la forme /{prefixe_url}/action/...
21. # avec l'alias /impots les URL auront la forme /impots/{prefixe_url}/action/...
22. # où [prefixe_url] est défini dans parameters.py
23. WSGIScriptAlias / "${ROOT}/main_withmysql.wsgi"
24. DocumentRoot "${ROOT}"
25. ServerName ${SITE}
26. ServerAlias *.${SITE}
27. <Directory "${ROOT}">
28. AllowOverride All
29. Require all granted
30. </Directory>
31. </VirtualHost>
32.
33. # URL sécurisées avec HTTPS
34. <VirtualHost *:443>
35. # avec l'alias / les URL auront la forme /{prefixe_url}/action/...
36. # avec l'alias /impots les URL auront la forme /impots/{prefixe_url}/action/...
37. # où [prefixe_url] est défini dans parameters.py
38. WSGIScriptAlias / "${ROOT}/main_withmysql.wsgi"
39. DocumentRoot "${ROOT}"
40. ServerName ${SITE}
41. ServerAlias *.${SITE}
42. <Directory "${ROOT}">
43. AllowOverride All
44. Require all granted
45. </Directory>
46.
47. SSLEngine on
48. SSLCertificateFile C:/MyPrograms/laragon/etc/ssl/laragon.crt
49. SSLCertificateKeyFile C:/myprograms/laragon/etc/ssl/laragon.key
50.
51. </VirtualHost>
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
728/755
11. # mettre ici les chemins des bibliothèques Python à utiliser - les séparer par des virgules
12. # ici les bibliothèques d'un environnement virtuel Python
13. WSGIPythonPath "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/venv/lib/site-packages"
14.
15. # Python Home - nécessaire uniquement s'il y a plusieurs versions de Python installées
16. # WSGIPythonHome "C:/Program Files/Python38"
17.
18. # URL HTTP
19. <VirtualHost *:80>
20. # avec l'alias / les URL auront la forme /{prefixe_url}/action/...
21. # avec l'alias /impots les URL auront la forme /impots/{prefixe_url}/action/...
22. # où [prefixe_url] est défini dans parameters.py
23. WSGIScriptAlias / "${ROOT}/main_withpgres.wsgi"
24. DocumentRoot "${ROOT}"
25. ServerName ${SITE}
26. ServerAlias *.${SITE}
27. <Directory "${ROOT}">
28. AllowOverride All
29. Require all granted
30. </Directory>
31. </VirtualHost>
32.
33. # URL sécurisées avec HTTPS
34. <VirtualHost *:443>
35. # avec l'alias / les URL auront la forme /{prefixe_url}/action/...
36. # avec l'alias /impots les URL auront la forme /impots/{prefixe_url}/action/...
37. # où [prefixe_url] est défini dans parameters.py
38. WSGIScriptAlias / "${ROOT}/main_withpgres.wsgi"
39. DocumentRoot "${ROOT}"
40. ServerName ${SITE}
41. ServerAlias *.${SITE}
42. <Directory "${ROOT}">
43. AllowOverride All
44. Require all granted
45. </Directory>
46.
47. SSLEngine on
48. SSLCertificateFile C:/MyPrograms/laragon/etc/ssl/laragon.crt
49. SSLCertificateKeyFile C:/myprograms/laragon/etc/ssl/laragon.key
50.
51. </VirtualHost>
Nous sauvegardons tous ces fichiers, lançons le serveur Apache et les SGBD MySQL et PostgreSQL. L’application est configurée
avec le préfixe d’URL [/do] et [with_csrftoken=False] (pas de jeton CSRF) dans [configs/parameters.py]. Nous demandons
l’URL [https://flask-impots-withmysql/do]. La réponse du serveur est la suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
729/755
Nous demandons maintenant l’URL [https://flask-impots-pgres/do]. La réponse est la suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
730/755
2. define ROOT "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-servers/12/apache"
3.
4. …
5.
6. # URL HTTP
7. <VirtualHost *:80>
8. # avec l'alias / les URL auront la forme /{prefixe_url}/action/...
9. # avec l'alias /impots les URL auront la forme /impots/{prefixe_url}/action/...
10. # où [prefixe_url] est défini dans parameters.py
11. WSGIScriptAlias /impots "${ROOT}/main_withmysql.wsgi"
12. …
13. </VirtualHost>
14.
15. # URL sécurisées avec HTTPS
16. <VirtualHost *:443>
17. # avec l'alias / les URL auront la forme /{prefixe_url}/action/...
18. # avec l'alias /impots les URL auront la forme /impots/{prefixe_url}/action/...
19. # où [prefixe_url] est défini dans parameters.py
20. WSGIScriptAlias /impots "${ROOT}/main_withmysql.wsgi"
21. …
22.
23. </VirtualHost>
Nous arrêtons / relançons le serveur Apache puis nous demandons l’URL [https://flask-impots-withmysql/impots/do]. La
réponse du serveur est la suivante :
Il y a un plantage. L’URL [1] nous en donne la cause. Elle aurait du être [https://flask-impots-withmysql/impots/do/afficher-
vue-authentification]. L’alias WSGI manque. C’est une erreur de notre application. Elle sait gérer un préfixe d’URL (/do est bien
présent). On pourrait penser qu’en mettant le préfixe [/impots/do] à notre application cela résoudrait le problème précédent. Mais
non. On rencontre alors d’autres types de problèmes. L’alias WSGI ne se comporte pas comme un préfixe d’URL.
Essayons de comprendre ce qui s’est passé. Nous avons demandé l’URL [https://flask-impots-withmysql/impots/do]. Nous nous
attendions à avoir la vue d’authentification. En [1], ci-dessus on voit que l’application a demandé son affichage mais pas avec la
bonne URL. Examinons le cheminement de la requête [https://flask-impots-withmysql/impots/do].
La route de la ligne 3 est dans notre exemple [https://flask-impots-withmysql/impots/do]. On voit que la route a été débarrassée
de la partie [https://flask-impots-withmysql/impots] pour devenir simplement [/do]. Pour la partie [https://flask-impots-
withmysql] c’est normal, le nom du serveur n’est pas repris dans la route. Mais on voit qu’il ne reprend pas non plus l’alias WSGI
[/impots]. C’est un point important. Même avec un alais WSGI nos routes initiales restent valides.
1. # racine de l'application
2. def index() -> tuple:
3. # redirection vers /init-session/html
4. return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)
Ligne 4, on est redirigé vers l’URL de la fonction [init_session]. Dans [configs/routes.py], cette fonction a été associée à la route
[/do/init-session/html] :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
731/755
1. # init-session
2. app.add_url_rule(f'{prefix_url}/init-session/<string:type_response>{csrftoken_param}', methods=['GET'],
3. view_func=routes.init_session)
Ligne 2, dans notre test [csrftoken_param] est la chaîne vide. L’application ne gère pas ici de jeton CSRF.
1. # init-session
2. def init_session(type_response: str) -> tuple:
3. # on exécute le contrôleur associé à l'action
4. return front_controller()
Ligne 4, on commence la chaîne de traitement de l’action [init-session]. Cette chaîne se termine de la façon suivante dans
[responses/HtmlResponse] :
1. …
2. # maintenant il faut générer l'URL de redirection sans oublier le jeton CSRF s'il est demandé
3. if config['parameters']['with_csrftoken']:
4. csrf_token = f"/{generate_csrf()}"
5. else:
6. csrf_token = ""
7.
8. # réponse de redirection
9. return redirect(f"{config['parameters']['prefix_url']}{ads['to']}{csrf_token}"), status.HTTP_302_FOUND
L’action [init-session] est une action ADS (Action Do Something) qui se termine pas une redirection vers une vue, ligne 9. C’est
là que réside le problème. La fonction [redirect] de la ligne 9, n’ajoute pas automatiquement l’alias WSGI à l’URL de redirection.
C’est ce que nous montre la copie d’écran ci-dessus. Il manque l’alias /impots dans l’URL objet de la redirection.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
732/755
38 Exercice d’application : version 18
38.1 Implémentation
Le dossier [impots/http-servers/13] est obtenu initialement par recopie du dossier [impots/http-servers/12] puis modifié
partiellement.
1. …
2. # token csrf
3. "with_csrftoken": False,
4. # bases gérées MySQL (mysql), PostgreSQL (pgres)
5. "databases": ["mysql", "pgres"],
6. # préfixe des URL de l'application
7. # mettre la chaîne vide si on ne veut pas de préfixe ou /préfixe sinon
8. "prefix_url": "/do",
9. # url racine du serveur Apache - mettre la chaîne vide pour une exécution en-dehors d'Apache
10. "application_root": "/impots"
11. …
Ligne 10, le paramètre [application_root] représentera l’alias WSGI du serveur virtuel Apache.
Avec ce paramètre, nous pouvons corriger l’instruction de [responses/HtmlResponse] qui avait provoqué l’erreur :
1. …
2. # maintenant il faut générer l'URL de redirection sans oublier le jeton CSRF s'il est demandé
3. if config['parameters']['with_csrftoken']:
4. csrf_token = f"/{generate_csrf()}"
5. else:
6. csrf_token = ""
7.
8. # réponse de redirection
9. return redirect(f"{config['parameters']['application_root']}{config['parameters']['prefix_url']}{ads['to'
]}{csrf_token}")
10. , status.HTTP_302_FOUND
• ligne 9 : nous avons ajouté la racine de l’application au début de l’URL cible de la redirection ;
Il nous faut également corriger tous les fragments pour que les URL qu’ils contiennent commencent par la racine de l’application (ou
alias WSGI) :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
733/755
Le fragment [v-authentification]
1. <!-- formulaire HTML - on poste ses valeurs avec l'action [authentifier-utilisateur] -->
2. <form method="post" action="{{modèle.application_root}}{{modèle.prefix_url}}/authentifier-utilisateur{{modèle.csrf_token}}"
3. >
4.
5. <!-- titre -->
6. <div class="alert alert-primary" role="alert">
7. <h4>Veuillez vous authentifier</h4>
8. </div>
9. …
10.
11. </form>
Le fragment [v-calcul-impot]
Le fragment [v-liste-simulations]
1. …
2.
3. {% if modèle.simulations is defined and modèle.simulations|length!=0 %}
4. …
5.
6. <!-- tableau des simulations -->
7. <table class="table table-sm table-hover table-striped">
8. …
9. <tr>
10. <th scope="row">{{simulation.id}}</th>
11. <td>{{simulation.marié}}</td>
12. <td>{{simulation.enfants}}</td>
13. <td>{{simulation.salaire}}</td>
14. <td>{{simulation.impôt}}</td>
15. <td>{{simulation.surcôte}}</td>
16. <td>{{simulation.décôte}}</td>
17. <td>{{simulation.réduction}}</td>
18. <td>{{simulation.taux}}</td>
19. <td><a href="{{modèle.application_root}}{{modèle.prefix_url}}/supprimer-
simulation/{{simulation.id}}{{modèle.csrf_token}}">Supprimer</a></td>
20. </tr>
21. {% endfor %}
22. </tr>
23. </tbody>
24. </table>
25. {% endif %}
Le fragment [v-menu]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
734/755
Les fragments ci-dessus utilisent tous le modèle [modèle.application_root]. Pour l’instant la clé [application_root] n’existe pas
dans les modèles générés par les classes de modèles.
La classe [AbstractBaseModelForView] qui est la classe parent de toutes les classes générant un modèle devient la suivante :
• ligne 15 : la méthode [update_model] a pour rôle de mettre dans le modèle des vues :
o ligne 24 : le jeton CSRF ;
o ligne 26 : le préfixe des URL ;
o ligne 28 : la racine de l’application ou alias WSGI ;
Les quatre classes filles font appel à la classe parent avec le code suivant :
1. …
2. # actions possibles à partir de la vue
3. modèle['actions_possibles'] = ["afficher-vue-authentification", "authentifier-utilisateur"]
4.
5. # finition du modèle par la classe parent
6. super().update_model(modèle, config)
7.
8. # on rend le modèle
9. return modèle
• ligne 6 : chaque classe fille appelle sa classe parent pour mettre à jour le modèle qu’elle a créé ;
La version 18 est prête. Nous reprenons les deux serveurs virtuels d’Apache de la version 17 et nous les modifions :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
735/755
Les deux fichiers [flask-impots-withXX.conf] ne sont modifiés qu’en un seul point :
Nous sommes prêts pour les tests de la version 18 portée sur Apache. La configuration est la suivante :
• l’alias WSGI est /impots dans les deux fichiers de configuration des serveurs virtuels ;
• dans le fichier de paramétrage [configs/parameters], les paramètres sont les suivants :
1. # token csrf
2. "with_csrftoken": False,
3. # préfixe des URL de l'application
4. # mettre la chaîne vide si on ne veut pas de préfixe ou /préfixe sinon
5. "prefix_url": "/do",
6. # url racine du serveur Apache - mettre la chaîne vide pour une exécution en-dehors d'Apache
7. "application_root": "/impots"
On lance le serveur Apache ainsi que les deux SGBD. On demande l’URL [https://flask-impots-withmysql/impots/do]. La
réponse du serveur est la suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
736/755
On obtient bien la vue d’authentification qu’on n’avait pu obtenir dans la version précédente. Le reste de l’application fonctionne
normalement.
Les notions d’alias WSGI et de préfixe d’URL jouent le même rôle. L’une de ces deux notions est redondante. Ainsi pour préfixer les
URL du serveur Apache avec la chaîne [/impots/do], on peut s’y prendre de trois manières :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
737/755
2 - [WGSIAlias /] et [prefix_url=’/impots/do’] ;
3 - [WGSIAlias /impots/do] et [prefix_url=’’] ;
• l’URL du serveur doit être modifiée dans les configurations [1] et [3] ;
• il faut apporter une modification à la couche [dao] pour qu’elle supporte le protocole HTTPS du serveur Apache ;
1. "server": {
2. # "urlServer": "http://127.0.0.1:5000",
3. # "urlServer": "http://127.0.0.1:5000/do",
4. "urlServer": "https://flask-impots-withmysql/impots/do",
5. "user": {
6. "login": "admin",
7. "password": "admin"
8. },
9. "url_services": {
10. …
11. }
12. },
13. # mode debug
14. "debug": True,
15. # csrf_token
16. "with_csrftoken": False,
• ligne 4 : la nouvelle URL du serveur. Pour la 1ère fois dans ce document, le client utilise le protocole HTTPS ;
1. …
2. # étape request / response
3. def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
4. # [method] : méthode HTTP GET ou POST
5. # [url_service] : URL de service
6. # [data] : paramètres du POST en x-www-form-urlencoded
7. # [json] : paramètres du POST en json
8. # [cookies]: cookies à inclure dans la requête
9.
10. # on doit avoir une session XML ou JSON, sinon on ne pourra pas gérer la réponse
11. if self.__session_type not in ['json', 'xml']:
12. raise ImpôtsError(73, "il n'y a pas de session valide en cours")
13.
14. # on ajoute le jeton CSRF à l'URL de service
15. if self.__csrf_token:
16. url_service = f"{url_service}/{self.__csrf_token}"
17.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
738/755
18. # exécution de la requête
19. response = requests.request(method,
20. url_service,
21. data=data_value,
22. json=json_value,
23. cookies=self.__cookies,
24. allow_redirects=True,
25. # pour le protocole https
26. verify=False)
27.
28. # mode debug ?
29. if self.__debug:
30. # logueur
31. if not self.__logger:
32. self.__logger = self.__config['logger']
33. # on logue
34. self.__logger.write(f"{response.text}\n")
35.
36. …
37.
38. # on rend le résultat
39. return résultat['réponse']
• ligne 26, on ajoute le paramètre [verify=False] à cause du protocole HTTPS utilisé par le serveur Apache. Le module
[requests] (ligne 19) sait gérer nativement le protocole HTTPS. Par défaut, il vérifie la validité du certificat de sécurité que
lui envoie le serveur HTTPS et lance une exception si le certificat reçu n’est pas valide. C’est le cas ici où le serveur Apache de
Laragon envoie un certificat auto-signé. Pour éviter l’exception, on utilise le paramètre [verify=False] pour dire au module
[requests] de ne pas lancer d’exception. [requests] se contente alors d’afficher un avertissement (warning) sur la console.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
739/755
39 Conclusion
Rappelons le travail fait dans ce document :
Chapitre 3 [bases] Les bases du langage Python – structures du langage – type de données
– fonctions – affichage console – chaînes de formatage – changement
de types – les listes – les dictionnaires – les expression srégulières
Chapitre 6 [fonctions] Portée des variables – mode de passage des paramètres – utiliser des
modules – le Python Path – paramètres nommés – fonctions récursives
Chapitre 7 [fichiers] Lecture / écriture d’un fichier texte – gestion des fichiers encodés en
UTF-8 – gestion des fichiers jSON
Chapitre 8 [impots/v01] Version 1 de l’exercice d’application, un calcul d’impôt sur les revenus.
L’application est déclinée en 18 versions – La version 1 implémente
une solution procédurale
Chapitre 15 [impots/v04] Version 4 de l’application – cette version implémente une solution avec
une architecture en couches, la programmation par interfaces,
l’utilisation de classes dérivées de [BaseEntity] et [MyException]
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
740/755
Chapitre 18 [databases/anysgbd] Ecrire du code indépendant du SGBD
Chapitre 22 [flask] Services web avec le framework web Flask – affichage d’une page
HTML – service web jSON – requêtes GET et POST – gstion d’une
session web
Chapitre 23 [impots/http-servers/01] Version 6 de l’exercice d’application - Création d’un service web jSON
[impots/http-clients/01] de calcul de l’impôt avec une architecture multicouche - Ecriture d’un
client web pour ce serveur avec une architecture multicouche –
programmation client / serveur – utilisation du module [requests]
Chapitre 24 [impots/http-servers/02]
Version 7 de l’exercice de l’application – la version 6 est améliorée : le
client et le serveur sont multithtreadés – utilitaires [Logger] pour loguer
[impots/http-clients/02]
les échanges client / serveur – [SendMail] pour envoyer un mail à
l’administrateur de l’application
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
741/755
Chapitre 35 [impots/http-servers/10] Version 15 de l’exercice d’application – refactorisation du code de la
version 14 pour gérer deux types d’actions : ASV (Action Show View)
qui ne servent qu’à afficher une vue sans modifier l’état du serveur,
ADS (Action Do Something) qui font une action qui modifie l’état du
serveur – ces actions se terminent toutes par une redirection vers une
action ASV – cela permet de gérer correctement les rafraîchissements
de page du navigateur client
Chapitre 36 [impots/http-servers/11] Version 16 de l’application – gestion des URL avec préfixe
Les applications client / serveur du calcul de l’impôt ont implémenté l’architecture suivante :
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
742/755
Le contenu du document est dense. Le lecteur qui ira jusqu’au bout aura une bonne vision de la programmation web MVC en Python
/ Flask et au-delà une bonne vision de la programmation web MVC dans d’autres langages.
On pourra trouver des informations complémentaires sur le framework Flask dans le document
[https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world] de l’auteur Miguel Grinberg. J’ai lu quelques
éléments de son cours et ceux-ci m’ont semblé très pédagogiques. L’auteur présente beaucoup de notions non abordées dans ce
document.
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
743/755
1 Avant-propos .............................................................................................................................................................2
2 Installation d'un environnement de travail ............................................................................................................8
2.1 Python 3.8.1 .......................................................................................................................................................8
2.2 L'IDE PyCharm Community ....................................................................................................................... 10
2.2.1 Introduction ............................................................................................................................................ 10
2.2.2 Environnement virtuel d’exécution ..................................................................................................... 15
2.2.3 Git............................................................................................................................................................. 17
2.3 Conventions d’écriture du code Python...................................................................................................... 27
3 Les bases de Python .............................................................................................................................................. 28
3.1 Script [bases_01] : opérations élémentaires .................................................................................................. 28
3.2 Script [bases_02] : chaînes de formatage ..................................................................................................... 30
3.3 Script [bases_03] : changements de types ..................................................................................................... 31
3.4 Script [bases_04]: portée des variables .......................................................................................................... 33
3.5 Script [bases_05] : listes - 1 ............................................................................................................................. 33
3.6 Script [bases_06] : listes - 2 ............................................................................................................................. 34
3.7 script [bases_07] : le dictionnaire ................................................................................................................... 35
3.8 script [bases_08] : les tuples ............................................................................................................................ 36
3.9 Script [bases_09] : les listes et dictionnaires à plusieurs dimensions ........................................................ 37
3.10 Script [bases_10] : liens entre chaînes et listes ............................................................................................. 38
3.11 Script [bases_11] : les expressions régulières ............................................................................................... 39
4 Les chaînes de caractères ...................................................................................................................................... 43
4.1 Script [str_01] : notation des chaînes de caractères ................................................................................... 43
4.2 Script [str_02] : les méthodes de la classe <str> ........................................................................................ 43
4.3 Script [str_03] : codage des chaînes de caractères (1) ................................................................................ 44
4.4 Script [str_04] : encodage des chaînes de caractères (2)............................................................................ 46
5 Les exceptions ........................................................................................................................................................ 48
5.1 script [exceptions_01] ....................................................................................................................................... 48
5.2 script [exceptions_02] ....................................................................................................................................... 49
5.3 script [exceptions_03] ....................................................................................................................................... 51
6 Les fonctions .......................................................................................................................................................... 54
6.1 Script [fonc_01] : portée des variables .......................................................................................................... 54
6.2 Script [fonc_02] : portée des variables .......................................................................................................... 55
6.3 Script [fonc_03] : portée des variables .......................................................................................................... 56
6.4 Script [fonc_04] : mode de passage des paramètres .................................................................................... 57
6.5 Script [fonc_05] : ordre d'écriture des fonctions dans un script ............................................................... 57
6.6 Script [fonc_06] : ordre d'écriture des fonctions dans un script ............................................................... 58
6.7 Script [fonc_07] : utilisation de modules ...................................................................................................... 58
6.8 Script [fonc_08] : ajouter des dossiers au [Python Path] ............................................................................... 62
6.9 Script [fonc_09] : déclaration du type des paramètres ................................................................................ 63
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
744/755
6.10 Script [fonc_10] : paramètres nommés ......................................................................................................... 64
6.11 Script [fonc_11] : fonction récursive ............................................................................................................. 64
6.12 Script [fonc_12] : fonction récursive ............................................................................................................. 65
7 Les fichiers texte..................................................................................................................................................... 67
7.1 Script [fic_01] : lecture / écriture d'un fichier texte .................................................................................. 67
7.2 Script [fic_02] : gérer des fichiers texte encodés en UTF-8 ..................................................................... 68
7.3 Script [fic_03] : gérer des fichiers texte encodés en ISO-8859-1............................................................. 69
7.4 Script [json_01] : gestion d'un fichier jSON ................................................................................................ 70
7.5 Script [json_02] : gestion des fichiers jSON codés en UTF-8 .................................................................. 72
8 Exercice d'application – version 1 ....................................................................................................................... 75
8.1 Le problème .................................................................................................................................................... 75
8.1.1 Calcul de l’impôt brut ............................................................................................................................ 76
8.1.2 Plafonnement du quotient familial ...................................................................................................... 77
8.1.3 Calcul de la décote ................................................................................................................................. 77
8.1.4 Calcul de la réduction d’impôts ............................................................................................................ 78
8.1.5 Calcul de l’impôt net .............................................................................................................................. 78
8.1.6 Cas des hauts revenus ............................................................................................................................ 78
8.1.7 Chiffres officiels ..................................................................................................................................... 79
8.2 Version 1 .......................................................................................................................................................... 80
8.2.1 Le script principal................................................................................................................................... 80
8.2.2 Le module [impots.v01.shared.impôts_module_01] .................................................................................. 82
8.2.3 La fonction [get_taxpayers_data] ........................................................................................................... 82
8.2.4 La fonction [calcul_impôt] ..................................................................................................................... 83
8.2.5 La fonction [calcul_impôt_2] .................................................................................................................. 84
8.2.6 La fonction [get_décôte]......................................................................................................................... 85
8.2.7 La fonction [get_réduction] .................................................................................................................... 85
8.2.8 La fonction [get_revenu_imposable] ........................................................................................................ 85
8.2.9 La fonction [record_results] .................................................................................................................. 86
8.2.10 Les résultats............................................................................................................................................. 86
9 Les imports ............................................................................................................................................................. 88
9.1 Scripts [import_01] ........................................................................................................................................... 88
9.2 Script [import_02] ............................................................................................................................................. 90
9.3 Scripts [import_03] ........................................................................................................................................... 91
9.4 Scripts [import_04] ........................................................................................................................................... 92
9.5 Scripts [import_05] ........................................................................................................................................... 94
9.6 Scripts [import_06] ........................................................................................................................................... 96
9.7 Scripts [import_07] ........................................................................................................................................... 98
9.7.1 Installation d’un module de portée machine ...................................................................................... 99
9.7.2 Le script [config.py].............................................................................................................................. 100
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
745/755
9.7.3 Le script [main.py] ................................................................................................................................. 101
10 Exercice d’application : version 2 ...................................................................................................................... 102
11 Exercice d’application : version 3 ...................................................................................................................... 104
11.1 Le script de configuration [config.py] ........................................................................................................ 105
11.2 Script principal [main.py] .............................................................................................................................. 105
11.3 Le module [impots.v02.modules.impôts_module_02] ....................................................................................... 106
11.4 Lecture des données de l'administration fiscale ....................................................................................... 106
11.5 Enregistrement des résultats ....................................................................................................................... 106
11.6 Modification des fonctions ......................................................................................................................... 107
11.7 Résultats ......................................................................................................................................................... 108
12 Les classes et objets ............................................................................................................................................. 109
12.1 script [classes_01] : une classe Objet .......................................................................................................... 109
12.2 Script [classes_02] : une classe Personne ................................................................................................... 110
12.3 Script [classes_03] : la classe Personne avec un constructeur ................................................................. 111
12.4 Script [classes_04] : méthode statiques ...................................................................................................... 111
12.5 Script [classes_05] : contrôles de validité des attributs ............................................................................ 113
12.6 Script [classes_06] : ajout d'une méthode d'initialisation de l'objet ....................................................... 116
12.7 Script [classes_07] : une liste d'objets Personne ....................................................................................... 117
12.8 Script [classes_08] : création d'une classe dérivée de la classe Personne .............................................. 118
12.9 Script [classes_09] : seconde classe dérivée de la classe Personne ......................................................... 119
12.10 Script [classes_10] : la propriété [__dict__] ............................................................................................ 120
13 Les classes génériques [BaseEntity] et [MyException] ......................................................................................... 121
13.1 La classe MyException ................................................................................................................................ 121
13.2 La classe [BaseEntity] .................................................................................................................................... 122
13.2.1 La méthode [BaseEntity.fromdict] ....................................................................................................... 123
13.2.2 La méthode [BaseEntity.asdict] .......................................................................................................... 128
13.2.3 La méthode [BaseEntity.asjson] .......................................................................................................... 131
13.2.4 La méthode [BaseEntity.fromjson] ....................................................................................................... 132
13.2.5 Le script [main] ...................................................................................................................................... 132
14 Architecture en couches et programmation par interfaces ............................................................................ 134
14.1 Introduction .................................................................................................................................................. 134
14.2 Exemple 1 ...................................................................................................................................................... 134
14.2.1 Les entités de l'application .................................................................................................................. 135
14.2.2 Configuration de l’application ............................................................................................................ 139
14.2.3 Tests des entités .................................................................................................................................... 140
14.2.4 La couche [dao] ..................................................................................................................................... 149
14.2.5 La couche [métier] ................................................................................................................................ 156
14.2.6 La couche [ui]....................................................................................................................................... 159
14.3 Le script principal [main] .............................................................................................................................. 161
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
746/755
14.4 Exemple 2 ...................................................................................................................................................... 162
14.4.1 La couche [dao] ..................................................................................................................................... 162
14.4.2 La couche [métier] ................................................................................................................................ 163
14.4.3 La couche [ui]....................................................................................................................................... 165
14.4.4 Les fichiers de configuration .............................................................................................................. 166
14.4.5 Le script principal [main] ..................................................................................................................... 168
15 Exercice d'application - version 4 ...................................................................................................................... 172
15.1 Version 4 – application 1 ............................................................................................................................. 173
15.1.1 Les entités.............................................................................................................................................. 173
15.1.2 La couche [dao] ..................................................................................................................................... 177
15.1.3 La couche [métier] ................................................................................................................................ 182
15.1.4 Tests des couches [dao] et [métier] ..................................................................................................... 184
15.1.5 Script principal...................................................................................................................................... 189
15.2 Version 4 – application 2 ............................................................................................................................. 191
15.2.1 L'interface [InterfaceImpôtsUi] ............................................................................................................. 191
15.2.2 La classe [ImpôtsConsole]....................................................................................................................... 192
15.2.3 Le script principal................................................................................................................................. 193
16 Utilisation du SGBD MySQL ............................................................................................................................ 195
16.1 Intallation du SGBD MySQL ..................................................................................................................... 195
16.1.1 Installation de Laragon........................................................................................................................ 195
16.1.2 Création d’une base de données ........................................................................................................ 197
16.2 Intallation du package [mysql-connector-python] ......................................................................................... 201
16.3 script [mysql_01] : connexion à une base MySQL - 1 ............................................................................... 203
16.4 script [mysql_02] : connexion à une base MySQL - 2 ............................................................................... 204
16.5 script [mysql_03] : création d'une table MySQL ........................................................................................ 205
16.6 script [mysql_04] : exécution d'un fichier d'ordres SQL ........................................................................... 207
16.7 script [mysql_05] : utilisation de requêtes paramétrées ............................................................................. 216
17 Utilisation du SGBD PostgreSQL ..................................................................................................................... 219
17.1 Installation du SGBD PostgreSQL .......................................................................................................... 219
17.2 Administrer PostgreSQL avec l’outil [pgAdmin] ......................................................................................... 223
17.3 Installation du connecteur Python du SGBD PostgreSQL ................................................................... 226
17.4 Portage des scripts MySQL vers des scripts PostgreSQL ...................................................................... 227
17.4.1 module [pgres_module] ........................................................................................................................... 227
17.4.2 script [pgres_01] ..................................................................................................................................... 227
17.4.3 script [pgres_02] ..................................................................................................................................... 228
17.4.4 script [pgres_03] ..................................................................................................................................... 229
17.4.5 script [pgres_04] ..................................................................................................................................... 230
17.4.6 script [pgres_05] ..................................................................................................................................... 233
17.5 Conclusion..................................................................................................................................................... 234
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
747/755
18 Ecrire du code indépendant du SGBD............................................................................................................. 235
19 Utilisation de l’ORM SQLALCHEMY ............................................................................................................ 240
19.1 Installation de l’ORM [sqlalchemy] ............................................................................................................. 240
19.2 Scripts 01 : les bases ..................................................................................................................................... 241
19.2.1 Configuration ........................................................................................................................................ 241
19.2.2 Script [démo] ........................................................................................................................................... 241
19.2.3 Le script [main] ...................................................................................................................................... 243
19.3 Scripts 02 : les mappings de [sqlalchemy] ................................................................................................... 247
19.4 Scripts 03 : manipulation des entités de la session [sqlalchemy] ............................................................. 250
19.5 Scripts 04 : utilisation d’une base [PostgreSQL] .......................................................................................... 253
19.6 Scripts 05 : exemple complet ...................................................................................................................... 255
19.6.1 L’architecture de l’application ............................................................................................................ 255
19.6.2 Les bases de données........................................................................................................................... 255
19.6.3 Les entités manipulées par l’application ........................................................................................... 257
19.6.4 Configuration ........................................................................................................................................ 260
19.6.5 La couche [dao] - 1 ............................................................................................................................... 265
19.6.6 Initialisation de la base de données ................................................................................................... 268
19.6.7 La couche [dao] – 2 .............................................................................................................................. 272
19.6.8 Le script [main_joined_queries] ............................................................................................................. 274
19.6.9 Le script [main_stats_for_élève] ........................................................................................................... 278
20 Exercice d'application : version 5 ...................................................................................................................... 280
20.1 Application 1 : initialisation de la base de données ................................................................................. 280
20.1.1 Le fichier [admindata.json] .................................................................................................................... 281
20.1.2 Création des bases de données ........................................................................................................... 281
20.1.3 Les entitées mappées par [sqlalchemy] ............................................................................................... 282
20.1.4 Le fichier de configuration de [sqlalchemy] ....................................................................................... 284
20.1.5 La couche [dao] ..................................................................................................................................... 285
20.1.6 Configuration de l’application ............................................................................................................ 288
20.1.7 Le script [main] de l’application .......................................................................................................... 289
20.2 Application 2 : calcul de l’impôt en mode batch ..................................................................................... 294
20.2.1 Architecture........................................................................................................................................... 294
20.2.2 Configuration de l’application ............................................................................................................ 296
20.2.3 La couche [dao] ..................................................................................................................................... 297
20.2.4 Test de la couche [dao]......................................................................................................................... 298
20.2.5 Le script principal................................................................................................................................. 302
20.3 Application 3 : calcul de l’impôt en mode interactif ............................................................................... 304
21 Fonctions réseau................................................................................................................................................... 306
21.1 Les bases de la programmation internet ................................................................................................... 306
21.1.1 Généralités ............................................................................................................................................ 306
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
748/755
21.1.2 Les caractéristiques du protocole TCP ............................................................................................. 306
21.1.3 La relation client-serveur..................................................................................................................... 307
21.1.4 Architecture d'un client ....................................................................................................................... 307
21.1.5 Architecture d'un serveur .................................................................................................................... 307
21.2 Découvrir les protocoles de communication de l'internet ..................................................................... 308
21.2.1 Introduction .......................................................................................................................................... 308
21.2.2 Utilitaires TCP ...................................................................................................................................... 308
21.3 Obtenir le nom ou l'adresse IP d'une machine de l'Internet.................................................................. 311
21.4 Le protocole HTTP (HyperText Transfer Protocol) .............................................................................. 313
21.4.1 Exemple 1 ............................................................................................................................................. 313
21.4.2 Exemple 2 ............................................................................................................................................. 314
21.4.3 Exemple 3 ............................................................................................................................................. 319
21.4.4 Exemple 4 ............................................................................................................................................. 326
21.4.5 Exemple 5 ............................................................................................................................................. 332
21.4.6 Conclusion ............................................................................................................................................ 338
21.5 Le protocole SMTP (Simple Mail Transfer Protocol) ............................................................................. 338
21.5.1 Introduction .......................................................................................................................................... 338
21.5.2 Création d’une adresse [gmail] ............................................................................................................ 338
21.5.3 Installation d’un serveur SMTP ......................................................................................................... 339
21.5.4 Installation d'un lecteur de courrier................................................................................................... 346
21.5.5 Le protocole SMTP ............................................................................................................................. 354
21.5.6 scripts [smtp/01] : un client SMTP basique........................................................................................ 358
21.5.7 scripts [smtp/02] : un lient SMTP écrit avec la bibliothèque [smtplib] ........................................... 363
21.5.8 scripts [smtp/03] : gestion des fichiers attachés ................................................................................. 367
21.6 Le protocole POP3 ...................................................................................................................................... 372
21.6.1 Introduction .......................................................................................................................................... 372
21.6.2 Découverte du protocole POP3 ........................................................................................................ 373
21.6.3 scripts [pop3/01] : un client POP3 basique ........................................................................................ 376
21.6.4 scripts [pop3/02] : client POP3 avec les modules [poplib] et [email] .............................................. 381
21.7 Le protocole IMAP ...................................................................................................................................... 397
21.7.1 Introduction .......................................................................................................................................... 397
21.7.2 script [imap/main] : client IMAP avec le module [imaplib] ........................................................... 400
22 Services web avec le framework Flask .............................................................................................................. 404
22.1 Introduction .................................................................................................................................................. 404
22.2 scripts [flask/01] : premiers éléments de programmation web .............................................................. 405
22.2.1 script [exemple_01] : rudiments du langage HTML........................................................................... 406
22.2.2 script [exemple_02] : générer un document HTML dynamiquement ............................................. 411
22.2.3 script [exemple_03] : utiliser des fragments de page .......................................................................... 414
22.3 scripts [flask/02] : service web de date et heure....................................................................................... 415
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
749/755
22.4 scripts [flask/03] : services web générant du texte .................................................................................. 417
22.4.1 script [main_01]....................................................................................................................................... 417
22.4.2 Postman ................................................................................................................................................. 420
22.4.3 script [main_02]....................................................................................................................................... 424
22.4.4 script [main_03]....................................................................................................................................... 425
22.5 scripts [flask/04] : informations encapsulées dans la requête ................................................................ 427
22.6 scripts [flask-05] : gestion de la mémoire de l’utilisateur ........................................................................ 433
22.6.1 Introduction .......................................................................................................................................... 433
22.6.2 script [session_scope_01]........................................................................................................................ 435
22.6.3 script [session_scope_02]........................................................................................................................ 439
22.6.4 script [session_scope_03]........................................................................................................................ 441
22.7 scripts [flask/06] : informations partagées par les utilisateurs ............................................................... 443
22.7.1 Introduction .......................................................................................................................................... 443
22.7.2 script [application_scope_01] ................................................................................................................. 444
22.7.3 script [application_scope_02] ................................................................................................................. 446
22.7.4 script [application_scope_03] ................................................................................................................. 448
22.8 scripts [flask/07] : gestion des routes......................................................................................................... 449
22.8.1 script [main_01] : routes paramétrées .................................................................................................. 449
22.8.2 script [main_02] : externalisation des routes....................................................................................... 451
23 Exercice d’application : version 6 ...................................................................................................................... 453
23.1 Introduction .................................................................................................................................................. 453
23.2 Le serveur web de calcul de l’impôt .......................................................................................................... 454
23.2.1 Version 1................................................................................................................................................ 454
23.2.2 Version 2................................................................................................................................................ 463
23.2.3 Version 3................................................................................................................................................ 465
23.3 Le client web du serveur de calcul de l’impôt .......................................................................................... 468
23.3.1 Introduction .......................................................................................................................................... 468
23.3.2 Configuration du client web ............................................................................................................... 471
23.3.3 Le script principal [main] ..................................................................................................................... 472
23.3.4 Implémentation de la couche [dao] .................................................................................................... 473
23.3.5 Exécution .............................................................................................................................................. 475
23.4 Tests de la couche [dao] ............................................................................................................................... 476
24 Exercice d’application : version 7 ...................................................................................................................... 479
24.1 Introduction .................................................................................................................................................. 479
24.2 Les utilitaires ................................................................................................................................................. 480
24.2.1 La classe [Logger] .................................................................................................................................. 480
24.2.2 La classe [SendAdminMail]....................................................................................................................... 482
24.3 Le serveur web .............................................................................................................................................. 483
24.3.1 Configuration ........................................................................................................................................ 484
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
750/755
24.3.2 Le script principal [main] ..................................................................................................................... 485
24.3.3 Le contrôleur [index_controller] ......................................................................................................... 488
24.3.4 Exécution .............................................................................................................................................. 489
24.4 Le client web ................................................................................................................................................. 490
24.4.1 La configuration ................................................................................................................................... 490
24.4.2 La couche [dao] ..................................................................................................................................... 491
24.4.3 Le script principal................................................................................................................................. 492
24.4.4 Exécution .............................................................................................................................................. 494
24.5 Tests de la couche [dao] ............................................................................................................................... 496
25 Exercice d’application : version 8 ...................................................................................................................... 499
25.1 Introduction .................................................................................................................................................. 499
25.2 Le serveur web .............................................................................................................................................. 500
25.2.1 La configuration ................................................................................................................................... 500
25.2.2 Le script principal [main] ..................................................................................................................... 500
25.2.3 Le contrôleur [index_controller] ......................................................................................................... 501
25.3 Le client web ................................................................................................................................................. 502
25.3.1 La couche [dao] ..................................................................................................................................... 502
25.3.2 La configuration ................................................................................................................................... 505
25.3.3 Le script principal du client ................................................................................................................ 505
25.3.4 Exécution du client .............................................................................................................................. 506
25.4 Tests de la couche [dao] ............................................................................................................................... 509
26 Du dictionnaire à XML et vice-versa ................................................................................................................ 512
27 Exercice d’application : version 9 ...................................................................................................................... 516
27.1 Le serveur web .............................................................................................................................................. 516
27.2 Le client web ................................................................................................................................................. 518
27.2.1 Le code .................................................................................................................................................. 518
27.2.2 Test de la couche [dao] du client ........................................................................................................ 520
28 Exercice d’application : version 10 .................................................................................................................... 521
28.1 Introduction .................................................................................................................................................. 521
28.2 Le serveur web .............................................................................................................................................. 522
28.2.1 Configuration ........................................................................................................................................ 522
28.2.2 Le script principal [main] ..................................................................................................................... 522
28.2.3 Le contrôleur [index_controller] ......................................................................................................... 522
28.2.4 Tests du serveur.................................................................................................................................... 524
28.3 Le client web ................................................................................................................................................. 530
28.3.1 La couche [dao] ..................................................................................................................................... 530
28.3.2 Le script principal [main] ..................................................................................................................... 531
28.3.3 Exécution du client .............................................................................................................................. 532
28.3.4 Tests de la couche [dao] du client....................................................................................................... 532
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
751/755
29 Exercice d’application : version 11 .................................................................................................................... 534
29.1 Introduction .................................................................................................................................................. 534
29.2 Le serveur web .............................................................................................................................................. 535
29.3 Configuration ................................................................................................................................................ 535
29.4 Le script principal [main] .............................................................................................................................. 535
29.5 Les contrôleurs ............................................................................................................................................. 537
29.6 Tests Postman ............................................................................................................................................... 537
29.7 Le client web ................................................................................................................................................. 539
29.7.1 Configuration des couches du client ................................................................................................. 540
29.7.2 Implémentation de la couche [dao] .................................................................................................... 541
29.8 Les scripts [main, main2] ............................................................................................................................... 542
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
752/755
31.5 Le client [main2] ............................................................................................................................................. 599
31.6 Le client [main3] ............................................................................................................................................. 600
31.7 La classe de test [Test2HttpClientDaoWithSession] ........................................................................................ 603
32 Le mode HTML de la version 12 ...................................................................................................................... 607
32.1 Architecture MVC ........................................................................................................................................ 607
32.2 L’arborescence des scripts du serveur HTML ......................................................................................... 608
32.3 Présentation des vues ................................................................................................................................... 608
32.4 Configuration des vues ................................................................................................................................ 610
32.5 La vue d’authentification ............................................................................................................................. 612
32.5.1 Présentation de la vue .......................................................................................................................... 612
32.5.2 Le fragment [v-bandeau.html] ............................................................................................................... 614
32.5.3 Le fragment [v-authentification.html] ................................................................................................ 615
32.5.4 Tests visuels........................................................................................................................................... 617
32.5.5 Calcul du modèle de la vue ................................................................................................................. 618
32.5.6 Génération des réponses HTML ....................................................................................................... 620
32.5.7 Tests [Postman] ....................................................................................................................................... 622
32.5.8 Conclusion ............................................................................................................................................ 625
32.6 La vue de calcul de l’impôt.......................................................................................................................... 625
32.6.1 Présentation de la vue .......................................................................................................................... 625
32.6.2 Le fragment [v-calcul-impot.html] ....................................................................................................... 627
32.6.3 Le fragment [v-menu.html] .................................................................................................................... 629
32.6.4 Test visuel .............................................................................................................................................. 630
32.6.5 Calcul du modèle de la vue ................................................................................................................. 631
32.6.6 Tests [Postman] ....................................................................................................................................... 633
32.7 La vue de la liste des simulations ............................................................................................................... 635
32.7.1 Présentation de la vue .......................................................................................................................... 635
32.7.2 Test visuel .............................................................................................................................................. 638
32.7.3 Calcul du modèle de la vue ................................................................................................................. 639
32.7.4 Tests [Postman] ....................................................................................................................................... 640
32.8 La vue des erreurs inattendues ................................................................................................................... 641
32.8.1 Présentation de la vue .......................................................................................................................... 642
32.8.2 Test visuel .............................................................................................................................................. 643
32.8.3 Calcul du modèle de la vue ................................................................................................................. 644
32.8.4 Tests [Postman] ....................................................................................................................................... 645
32.9 Implémentation des actions du menu de l’application............................................................................ 646
32.9.1 L’action [/afficher-calcul-impot] ......................................................................................................... 646
32.9.2 L’action [/fin-session] .......................................................................................................................... 648
32.10 Tests de l’application HTML en conditions réelles ............................................................................. 650
33 Exercice d’application : version 13 .................................................................................................................... 655
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
753/755
33.1 Refactorisation de la configuration de l’application ................................................................................ 655
33.2 Refactorisation du script principal [main] .................................................................................................. 659
33.2.1 Les modules [flask_session], et [redis] .............................................................................................. 661
33.2.2 Le serveur Redis ................................................................................................................................... 661
33.2.3 Gestion du serveur Redis dans le script principal [main] ................................................................ 663
33.2.4 Gestion des routes dans le script principal [main] ........................................................................... 664
33.3 Refactorisation du contrôleur principal..................................................................................................... 666
33.4 Gestion des mots de passe cryptés ............................................................................................................ 667
33.5 Tests................................................................................................................................................................ 669
34 Exercice d’application : version 14 .................................................................................................................... 670
34.1 Introduction .................................................................................................................................................. 670
34.2 Configuration ................................................................................................................................................ 671
34.3 Implémentation CSRF ................................................................................................................................. 671
34.3.1 Le module [flask_wtf] .......................................................................................................................... 671
34.3.2 Les modèles des vues........................................................................................................................... 672
34.3.3 Les vues ................................................................................................................................................. 673
34.3.4 Les routes .............................................................................................................................................. 674
34.3.5 Le contrôleur [MainController]............................................................................................................. 675
34.4 Tests avec un navigateur .............................................................................................................................. 676
34.5 Clients console .............................................................................................................................................. 678
35 Exercice d’application : version 15 .................................................................................................................... 685
35.1 Introduction .................................................................................................................................................. 685
35.2 Implémentation ............................................................................................................................................ 688
35.2.1 Les nouvelles routes............................................................................................................................. 688
35.2.2 Les nouveaux contrôleurs ................................................................................................................... 689
35.2.3 La nouvelle configuration MVC ........................................................................................................ 691
35.2.4 Les nouveaux modèles ........................................................................................................................ 695
35.2.5 Le nouveau contrôleur principal ........................................................................................................ 697
35.2.6 La nouvelle réponse HTML ............................................................................................................... 699
35.3 Tests................................................................................................................................................................ 700
36 Exercice d’application : version 16 .................................................................................................................... 702
36.1 Introduction .................................................................................................................................................. 702
36.2 La nouvelle configuration des routes ........................................................................................................ 702
36.3 Les nouveaux contrôleurs ........................................................................................................................... 706
36.4 Les nouveaux modèles ................................................................................................................................. 706
36.5 Les nouveaux fragments .............................................................................................................................. 707
36.6 La nouvelle réponse HTML ....................................................................................................................... 708
36.7 Tests................................................................................................................................................................ 709
37 Exercice d’application : version 17 .................................................................................................................... 711
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
754/755
37.1 Relocalisation des dépendances de l’application ...................................................................................... 711
37.2 Tests................................................................................................................................................................ 713
37.3 Portage d’une application Python / Flask sur un serveur Apache / Windows .................................. 713
37.3.1 Sources ................................................................................................................................................... 713
37.3.2 Installation du module Python mod_wsgi........................................................................................ 713
37.3.3 Configuration du serveur Apache de Laragon................................................................................. 715
37.4 Création d’un premier serveur virtuel Apache ......................................................................................... 722
37.5 Portage de l’application de calcul de l’impôt sur Apache / Windows .................................................. 726
38 Exercice d’application : version 18 .................................................................................................................... 733
38.1 Implémentation ............................................................................................................................................ 733
38.2 Tests console ................................................................................................................................................. 738
39 Conclusion ............................................................................................................................................................ 740
https://tahe.developpez.com/ - Ce cours tutoriel écrit par Serge Tahé est mis à disposition du public selon les termes de la Licence Creative Commons Attribution – Pas d’Utilisation Commerciale –
Partage dans les Mêmes Conditions 3.0 non transposé.
755/755