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.
L’architecture de l’application ne change pas :
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] :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
# dépendances absolues
absolute_dependencies =
[
# dossiers du projet
# BaseEntity, MyException
f"
{root_dir}
/classes/02/entities"
,
# InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
f"
{root_dir}
/impots/v04/interfaces"
,
# AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
f"
{root_dir}
/impots/v04/services"
,
# ImpotsDaoWithAdminDataInDatabase
f"
{root_dir}
/impots/v05/services"
,
# AdminData, ImpôtsError, TaxPayer
f"
{root_dir}
/impots/v04/entities"
,
# Constantes, Tranches
f"
{root_dir}
/impots/v05/entities"
,
# index_controller
f"
{script_dir}
/../controllers"
,
# scripts [config_database, config_layers]
script_dir,
# Logger, SendAdminMail
f"
{root_dir}
/impots/http-servers/02/utilities"
,
]
-
ligne 17 : on va réécrire un contrôleur pour la fonction [index] qui traite l’URL / ;
-
ligne 21 : on utilise les utilitaires de la version précédente ;
25-2-2. Le script principal [main]▲
Le nouveau script [main] amène quelques modifications au script principal [main] de la version précédente :
2.
3.
4.
# l'application Flask peut démarrer
app =
Flask
(
__name__
)
# clé secrète de la session
app.secret_key =
os.urandom
(
12
).hex(
)
-
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 fiscales ne sont plus demandées dans le code de [main]. Les lignes suivantes sont supprimées :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
# récupération des données de l'administration fiscale
erreur =
False
try
:
# admindata sera une donnée de portée application en lecture seule
config["admindata"
] =
config["layers"
]["dao"
].get_admindata
(
)
# log de réussite
logger.write
(
"[serveur] connexion à la base de données réussie
\n
"
)
except
ImpôtsError as
ex:
# on note l'erreur
erreur =
True
# log d'erreur
log =
f"L'erreur suivante s'est produite :
{ex}
"
# console
print
(
log)
# fichier de logs
logger.write
(
f"
{log}
\n"
)
# mail à l'administrateur
send_adminmail
(
config, log)
Par ailleurs, le contrôleur [index_controller] admet un paramètre supplémentaire, la session Flask :
2.
3.
4.
from
flask import
request, Flask, session
….
# on fait exécuter la requête par un contrôleur
résultat, status_code =
index_controller.execute
(
request, session, config)
25-2-3. Le contrôleur [index_controller]▲
Le contrôleur [index_controller] gère désormais une session :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
# import des dépendances
import
os
import
re
import
threading
from
flask_api import
status
from
werkzeug.local import
LocalProxy
# URL paramétrée : /?marié=xx&enfants=yy&salaire=zz
from
AdminData import
AdminData
from
ImpôtsError import
ImpôtsError
def
execute
(
request: LocalProxy, session: LocalProxy, config: dict) ->
tuple:
# dépendances
from
TaxPayer import
TaxPayer
# au départ pas d'erreurs
erreurs =
[]
…
# des erreurs ?
if
erreurs:
# on rend une réponse d'erreur au client
return
{"réponse"
: {"erreurs"
: erreurs}}, status.HTTP_400_BAD_REQUEST
# pas d'erreurs, on peut travailler
# on récupère la config associée au thread
thread_name =
threading.current_thread
(
).name
logger =
config[thread_name]["config"
]["logger"
]
# on exécute la requête
réponse =
None
try
:
# le cas le + simple - admindata est déjà en session
if
session.get
(
'client_id'
) is
not
None
:
# on récupère les informations de la session
client_id =
session.get
(
'client_id'
)
admindata =
AdminData
(
).fromdict
(
session.get
(
'admindata'
))
# log
logger.write
(
f"[index_controller] client [
{client_id}
], données fiscales prises en session\n"
)
else
:
# récupération des données de l'administration fiscale
admindata =
config["layers"
]["dao"
].get_admindata
(
)
# mise en session de admindata
session['admindata'
] =
admindata.asdict
(
)
# on donne un n° au client et on met ce n° en session
# cela va nous permettre de le suivre dans les logs du serveur
client_id =
os.urandom
(
12
).hex(
)
session['client_id'
] =
client_id
# log
logger.write
(
f"[index_controller] client [
{client_id}
], données fiscales prises dans la couche dao\n"
)
# calcul de l'impôt
taxpayer =
TaxPayer
(
).fromdict
(
{'marié'
: marié, 'enfants'
: enfants, 'salaire'
: salaire})
config["layers"
]["métier"
].calculate_tax
(
taxpayer, admindata)
# on rend la réponse au client
return
{"réponse"
: {"result"
: taxpayer.asdict
(
)}}, status.HTTP_200_OK
except
(
BaseException, ImpôtsError) as
erreur:
# on rend la réponse au client
return
{"réponse"
: {"erreurs"
: [f"
{erreur}
"
]}}, status.HTTP_500_INTERNAL_SERVER_ERROR
-
ligne 14 : le contrôleur reçoit la session courante du client web ;
-
lignes 35-38 : si le client a une session, alors celle-ci contient deux clés :
-
[client_id] : un n° de client (ligne 37) ;
-
[admindata] : les données de l’administration fiscale sous le forme d’un dictionnaire (ligne 38) ;
-
-
ligne 35 : on regarde si la session a une des deux clés attendues ;
-
lignes 42-51 : cas où la session du client n’a pas encore été initialisée ;
-
ligne 43 : on récupère les données de l’administration fiscale auprès de la couche [dao] ;
-
ligne 45 : ces données sont mises dans la session sous la forme d’un dictionnaire ;
-
ligne 48 : on affecte un n° aléatoire au client. Ce n° sera différent pour chaque client ;
-
ligne 49 : ce n° est mis en session ;
-
ligne 51 : on logue le fait que les données de l’administration fiscale ont été obtenues par la couche [dao]. Les accès à la couche [dao] sont en général coûteux. C’est pourquoi il faut les limiter. L’idée ici est d’obtenir une fois les données fiscales auprès de la couche [dao], de les mettre en session et d’aller les chercher là lors des requêtes ultérieures du même client. On rappelle que ce n’est pas la meilleure solution. Les données fiscales de l’administration étant les mêmes pour tous les clients, leur place est dans un objet de portée application ;
-
-
lignes 35-40 : cas où la session du client a été initialisée lors d’une précédente requête ;
-
ligne 37 : on récupère le n° du client dans la session ;
-
ligne 38 : on récupère les données fiscales de l’administration dans la session ;
-
ligne 40 : on logue le fait que le client a obtenu les données fiscales de l’administration dans la session ;
-
25-3. Le client web▲
25-3-1. La couche [dao]▲
25-3-1-1. La classe [ImpôtsDaoWithHttpSession]▲
La couche [dao] est implémentée par la classe [ImpôtsDaoWithHttpSession] suivante :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
# imports
import
requests
from
flask_api import
status
from
myutils import
decode_flask_session
from
AbstractImpôtsDao import
AbstractImpôtsDao
from
AdminData import
AdminData
from
ImpôtsError import
ImpôtsError
from
InterfaceImpôtsMétier import
InterfaceImpôtsMétier
from
TaxPayer import
TaxPayer
class
ImpôtsDaoWithHttpSession
(
AbstractImpôtsDao, InterfaceImpôtsMétier):
# constructeur
def
__init__
(
self, config: dict):
# initialisation parent
AbstractImpôtsDao.__init__
(
self, config)
# mémorisation éléments de la configuration
# config générale
self.__config =
config
# serveur
self.__config_server =
config["server"
]
# mode debug
self.__debug =
config["debug"
]
# logger
self.__logger =
None
# cookies
self.__cookies =
None
# méthode inutilisée
def
get_admindata
(
self) ->
AdminData:
pass
# calcul de l'impôt
def
calculate_tax
(
self, taxpayer: TaxPayer, admindata: AdminData =
None
):
# on laisse remonter les exceptions
# paramètres du get
params =
{"marié"
: taxpayer.marié, "enfants"
: taxpayer.enfants, "salaire"
: taxpayer.salaire}
# connexion avec authentification Auth Basic ?
if
self.__config_server['authBasic'
]:
response =
requests.get
(
# URL du serveur interrogé
self.__config_server['urlServer'
],
# paramètres de l'URL
params=
params,
# authentification Basic
auth=(
self.__config_server["user"
]["login"
],
self.__config_server["user"
]["password"
]),
cookies=
self.__cookies)
else
:
# connexion sans authentification Auth Basic
response =
requests.get
(
self.__config_server['urlServer'
], params=
params, cookies=
self.__cookies)
# on récupère les cookies de la réponse s'il y en a
if
response.cookies:
self.__cookies =
response.cookies
# on récupère le cookie de session
session_cookie =
response.cookies.get
(
'session'
)
# on le décode pour le loguer
if
session_cookie:
# logueur
if
not
self.__logger:
self.__logger =
self.__config['logger'
]
# on logue
self.__logger.write
(
f"cookie de session=
{decode_flask_session(session_cookie)}
\n"
)
# mode debug ?
if
self.__debug:
# logueur
if
not
self.__logger:
self.__logger =
self.__config['logger'
]
# on logue
self.__logger.write
(
f"
{response.text}
\n"
)
# code de statut de la réponse HTTP
status_code =
response.status_code
# on met la réponse jSON dans un dictionnaire
résultat =
response.json
(
)
# erreur si code de statut différent de 200 OK
if
status_code !=
status.HTTP_200_OK:
# on sait que les erreurs ont été associées à la clé [erreurs] de la réponse
raise
ImpôtsError
(
87
, résultat['réponse'
]['erreurs'
])
# on sait que le résultat a été associé à la clé [result] de la réponse
# on modifie le paramètre d'entrée avec ce résultat
taxpayer.fromdict
(
résultat["réponse"
]["result"
])
-
ligne 30 : la couche [dao] va gérer un dictionnaire de cookies ;
-
ligne 58 : la propriété [response.cookies] est un dictionnaire rassemblant les cookies envoyés par le serveur dans les entêtes HTTP [Set-Cookie] ;
-
ligne 59 : ces cookies sont mémorisés dans la couche [dao]. Ils seront renvoyés au serveur lors des requêtes ultérieures du même client ;
-
lignes 60-68 : bien que ce ne soit pas indispensable on récupère le cookie de session. Dans le dictionnaire des cookies envoyés par le serveur, le cookie de session est associé à la clé [session] ;
-
lignes 62-68 : on décode le cookie de session pour le loguer ;
-
ligne 68 : nous reviendrons ultérieurement sur la fonction [decode_flask_session] qui décode le cookie de session ;
-
lignes 52 et 57 : à chaque requête du même client, les cookies envoyés par le serveur lui sont renvoyés. C’est de cette façon que la session Flask est maintenue entre le client et le serveur ;
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 :
-
ligne 59, on pourrait écrire :
Sélectionnez1.config[thread_name][
'cookies'
]=
cookies -
ligne 52, on pourrait alors écrire :
Sélectionnez1.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.
Pour cela, nous allons créer une nouvelle classe [ImpôtsDaoWithHttpSessionFactory].
25-3-1-2. La fonction de décodage de la session Flask▲
La fonction [decode_flask_session] est définie dans le script [myutils] :

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
On y définit la fonction [decode_flask_session] de la façon suivante :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
def
decode_flask_session
(
cookie: str) ->
str:
# source : https://www.kirsle.net/wizards/flask-session.cgi
compressed =
False
payload =
cookie
if
payload.startswith
(
'.'
):
compressed =
True
payload =
payload[1
:]
data =
payload.split
(
"."
)[0
]
data =
base64_decode
(
data)
if
compressed:
data =
zlib.decompress
(
data)
return
data.decode
(
"utf-8"
)
-
ligne 2 : l’URL où j’ai trouvé cette fonction ;
-
ligne 1 : le paramètre [cookie] est la chaîne de caractères associée à la clé [session] dans le dictionnaire des cookies reçus par un client web ;
-
lignes 3-16 : je ne commenterai pas ce code que je ne maîtrise pas ;
On ajoute une nouvelle importation dans le fichier [__init__.py] :
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install .
Processing c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\packages
Using legacy setup.py install for myutils, since package 'wheel' is not installed.
Installing collected packages: myutils
Attempting uninstall: myutils
Found existing installation: myutils 0.1
Uninstalling myutils-0.1:
Successfully uninstalled myutils-0.1
Running setup.py install for myutils ... done
Successfully installed myutils-0.1
-
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
from
ImpôtsDaoWithHttpSession import
ImpôtsDaoWithHttpSession
class
ImpôtsDaoWithHttpSessionFactory:
def
__init__
(
self, config: dict):
# on mémorise le paramètre
self.__config =
config
def
new_instance
(
self):
# on rend une instance de la couche [dao]
return
ImpôtsDaoWithHttpSession
(
self.__config)
-
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
def
configure
(
config: dict) ->
dict:
# instanciation des couches de l'applicatuon
# couche dao
from
ImpôtsDaoWithHttpSessionFactory import
ImpôtsDaoWithHttpSessionFactory
dao_factory =
ImpôtsDaoWithHttpSessionFactory
(
config)
# on rend la configuation des couches
return
{
"dao_factory"
: dao_factory
}
-
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 ;
25-3-3. Le script principal du client▲
Le script [main] évolue de la façon suivante par rapport à celui de la version précédente :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
# on configure l'application
import
config
config =
config.configure
(
{})
# dépendances
from
ImpôtsError import
ImpôtsError
import
random
import
sys
import
threading
from
Logger import
Logger
# exécution de la couche [dao] dans un thread
# taxpayers est une liste de contribuables
def
thread_function
(
thread_dao, logger, taxpayers: list):
…
# liste des threads du client
threads =
[]
logger =
None
# code
try
:
…
l_taxpayers =
len(
taxpayers)
while
i <
len(
taxpayers):
…
# chaque thread doit avoir sa propre couche [dao] pour gérer correctement son cookie de session
thread_dao =
dao_factory.new_instance
(
)
# on crée le thread
thread =
threading.Thread
(
target=
thread_function, args=(
thread_dao, logger, thread_taxpayers))
# on l'ajoute à la liste des threads du script principal
threads.append
(
thread)
# on lance le thread - cette opération est asynchrone - on n'attend pas le résultat du thread
thread.start
(
)
# le thread principal attend la fin de tous les threads qu'il a lancés
…
except
BaseException as
erreur:
# affichage de l'erreur
print
(
f"L'erreur suivante s'est produite :
{erreur}
"
)
finally
:
# on ferme le logueur
if
logger:
logger.close
(
)
# on a fini
print
(
"Travail terminé..."
)
# fin des threads qui pourraient encore exister si on s'est arrêté sur erreur
sys.exit
(
)
-
lignes 29-30 : chaque thread a sa couche [dao] ;
25-3-4. Exécution du client▲
Le serveur web est lancé, le SGBD est lancé, le serveur de mails [hMailServer] est lancé. Puis on lance le script [main] du client web. Les logs de l’exécution dans [data/logs/logs.txt] sont alors les suivants :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
2020-07-25 10:21:46.478511, Thread-1 : début du thread [Thread-1] avec 1 contribuable(s)
2020-07-25 10:21:46.479111, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-25 10:21:46.479111, Thread-2 : début du thread [Thread-2] avec 1 contribuable(s)
2020-07-25 10:21:46.480195, Thread-3 : début du thread [Thread-3] avec 2 contribuable(s)
2020-07-25 10:21:46.480195, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-25 10:21:46.481137, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
2020-07-25 10:21:46.481137, Thread-3 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-25 10:21:46.482279, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
2020-07-25 10:21:46.482622, Thread-6 : début du thread [Thread-6] avec 1 contribuable(s)
2020-07-25 10:21:46.482622, Thread-4 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-5 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-6 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-25 10:21:46.725910, Thread-4 : 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_celibataire_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_demi_part":3797.0},"client_id":"fa3c83b82761c83e13217967"}
2020-07-25 10:21:46.725910, Thread-4 : {"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}}}
2020-07-25 10:21:46.725910, Thread-4 : 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}
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}
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_celibataire_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_demi_part":3797.0},"client_id":"700e3f5dc808c7c48f0c9007"}
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}}}
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}
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}
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_celibataire_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_demi_part":3797.0},"client_id":"9e14a5d4a3057f69ab95ab2d"}
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_celibataire_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_demi_part":3797.0},"client_id":"a06e8fd70a44c9e311f4dce0"}
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}}}
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_celibataire_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_demi_part":3797.0},"client_id":"28c38df998f67685b3a482b8"}
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}}}
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}
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}}}
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}
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}
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}
2020-07-25 10:21:47.529699, Thread-2 : fin du thread [Thread-2]
2020-07-25 10:21:47.531905, Thread-1 : fin du thread [Thread-1]
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_celibataire_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_demi_part":3797.0},"client_id":"38499b63076516c02f2770ec"}
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}}}
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}}}
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}
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}
2020-07-25 10:21:47.538828, Thread-3 : fin du thread [Thread-3]
2020-07-25 10:21:47.538828, Thread-6 : fin du thread [Thread-6]
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}}}
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}
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}
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}}}
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}
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}
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}}}
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}
2020-07-25 10:21:48.558715, Thread-5 : fin du thread [Thread-5]
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}}}
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}
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].
Les logs [data/logs/logs.txt] côté serveur sont les suivants :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
2020-07-25 10:21:39.187366, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:40.439093, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:46.502011, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=50000' [GET]>
2020-07-25 10:21:46.504049, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.505452, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=55555' [GET]>
2020-07-25 10:21:46.506257, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.507292, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=0&salaire=100000' [GET]>
2020-07-25 10:21:46.507292, Thread-4 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.508301, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=50000' [GET]>
2020-07-25 10:21:46.509293, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.511808, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=3&salaire=100000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=200000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.719504, Thread-6 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises dans la couche dao
2020-07-25 10:21:46.720003, Thread-6 : [index] {'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}}}
2020-07-25 10:21:46.736108, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=100000' [GET]>
2020-07-25 10:21:46.736108, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.506709, Thread-2 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises dans la couche dao
2020-07-25 10:21:47.507216, Thread-2 : [index] {'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}}}
2020-07-25 10:21:47.507216, Thread-3 : [index_controller] client [28c38df998f67685b3a482b8], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508442, Thread-4 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508940, Thread-3 : [index] {'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}}}
2020-07-25 10:21:47.510506, Thread-4 : [index] {'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}}}
2020-07-25 10:21:47.511513, Thread-5 : [index_controller] client [a06e8fd70a44c9e311f4dce0], données fiscales prises dans la couche dao
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}}}
2020-07-25 10:21:47.520727, Thread-7 : [index_controller] client [38499b63076516c02f2770ec], données fiscales prises dans la couche dao
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}}}
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]>
2020-07-25 10:21:47.531736, Thread-9 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises en session
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}}}
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]>
2020-07-25 10:21:47.542488, Thread-10 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
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}}}
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]>
2020-07-25 10:21:47.553628, Thread-11 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.736910, Thread-8 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
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}}}
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]>
2020-07-25 10:21:47.748226, Thread-12 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:48.554695, Thread-11 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
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}}}
2020-07-25 10:21:48.748753, Thread-12 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
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.
25-4. Tests de la couche [dao]▲
Comme nous l’avons fait dans les versions précédentes nous testons la couche [dao] du client :
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 ;
La classe de test [TestHttpClientDao] est la suivante :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
import
unittest
from
Logger import
Logger
class
TestHttpClientDao
(
unittest.TestCase):
def
test_1
(
self) ->
None
:
from
TaxPayer import
TaxPayer
# {'marié': 'oui', 'enfants': 2, 'salaire': 55555,
# 'impôt': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
taxpayer =
TaxPayer
(
).fromdict
(
{"marié"
: "oui"
, "enfants"
: 2
, "salaire"
: 55555
})
dao.calculate_tax
(
taxpayer)
# vérification
self.assertAlmostEqual
(
taxpayer.impôt, 2815
, delta=
1
)
self.assertEqual
(
taxpayer.décôte, 0
)
self.assertEqual
(
taxpayer.réduction, 0
)
self.assertAlmostEqual
(
taxpayer.taux, 0.14
, delta=
0.01
)
self.assertEqual
(
taxpayer.surcôte, 0
)
…
if
__name__
==
'__main__'
:
# on configure l'application
import
config
config =
config.configure
(
{})
# logger
logger =
Logger
(
config["logsFilename"
])
# on le mémorise dans la config
config["logger"
] =
logger
# on récupère la factory de la couche [dao]
dao_factory =
config["layers"
]["dao_factory"
]
# on crée une instance de la couche [dao]
dao =
dao_factory.new_instance
(
)
# on exécute les méthodes de test
print
(
"tests en cours..."
)
unittest.main
(
)
-
on crée une configuration d’exécution pour ce test ;
-
on lance le serveur web avec tout son environnement ;
-
on exécute le test ;
Les résultats sont les suivants :
2.
3.
4.
5.
6.
7.
8.
9.
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
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 3.392s
OK
Process finished with exit code 0