IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Introduction à Python 3 et au framework web Flask par l'exemple


précédentsommairesuivant

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 :

Image non disponible

25-2. Le serveur web

L’arborescence des scripts du serveur est la suivante :

Image non disponible

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] :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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

Image non disponible

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 :

 
Sélectionnez
1.
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électionnez
    1.
    config[thread_name]['cookies']=cookies
    
  • ligne 52, on pourrait alors écrire :

     
    Sélectionnez
    1.
    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] :

Image non disponible

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 :

 
Sélectionnez
1.
import myutils

On y définit la fonction [decode_flask_session] de la façon suivante :

 
Sélectionnez
1.
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] :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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ésexcept 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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

Image non disponible

La classe de test sera exécutée dans l’environnement suivant :

Image non disponible
  • la configuration [2] est identique à la configuration [1] que nous venons d’étudier ;

La classe de test [TestHttpClientDao] est la suivante :

 
Sélectionnez
1.
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()

Les résultats sont les suivants :

 
Sélectionnez
1.
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

précédentsommairesuivant

Licence Creative Commons
Le contenu de cet article est rédigé par Serge Tahé et est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2020 Developpez.com.