25. Application exercise: version 8
25.1. Introduction
We are going to write a new client/server application. The new feature of the server is that it will manage a session. Instead of placing the tax administration data in a [application] scope object, we will place it in a [session] scope object. Doing so will degrade the code’s performance. When an object can be shared in read-only mode by all users, it is preferable to make it a [application] scope object rather than a [session] scope object. At a minimum, this saves bandwidth since it reduces the size of the session cookie. However, we want to demonstrate a client/server application where the client and server exchange a session cookie.
The application architecture remains unchanged:

25.2. The web server
The server’s script directory structure is as follows:

The [http-servers/03] folder is initially created by copying the [http-servers/02] folder. Modifications are then made.
25.2.1. The configuration
It is the same as in the |previous version| with a few modifications in the [config] script:
# absolute dependencies
absolute_dependencies = [
# project files
# 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",
# Constants, Ranges
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",
]
- line 17: we will rewrite a controller for the [index] function that processes the URL / ;
- line 21: we use the utilities from the |previous version| ;
25.2.2. The main script [main]
The new script [main] introduces a few changes to the main script [main] from the previous version:
# the Flask application can be started
app = Flask(__name__)
# session secret key
app.secret_key = os.urandom(12).hex()
- line 4: a secret key is created for the application. We know this is necessary for session management;
Next, tax data is no longer requested in the [main] code. The following lines are removed:
# data recovery from tax authorities
erreur = False
try:
# admindata will be read-only application data
config["admindata"] = config["layers"]["dao"].get_admindata()
# success log
logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
# we note the error
erreur = True
# error log
log = f"L'erreur suivante s'est produite : {ex}"
# console
print(log)
# log file
logger.write(f"{log}\n")
# mail to administrator
send_adminmail(config, log)
Additionally, the [index_controller] controller accepts an additional parameter, the Flask session:
from flask import request, Flask, session
….
# the request is executed by a controller
résultat, status_code = index_controller.execute(request, session, config)
25.2.3. The [index_controller] controller
The [index_controller] controller now manages a session:
# import dependencies
import os
import re
import threading
from flask_api import status
from werkzeug.local import LocalProxy
# URL set: /?married=xx&children=yy&salary=zz
from AdminData import AdminData
from ImpôtsError import ImpôtsError
def execute(request: LocalProxy, session: LocalProxy, config: dict) -> tuple:
# dependencies
from TaxPayer import TaxPayer
# initially no errors
erreurs = []
…
# mistakes?
if erreurs:
# an error response is returned to the client
return {"réponse": {"erreurs": erreurs}}, status.HTTP_400_BAD_REQUEST
# no mistakes, we can work
# retrieve the config associated with the thread
thread_name = threading.current_thread().name
logger = config[thread_name]["config"]["logger"]
# execute the query
réponse = None
try:
# the simplest case - admindata is already in session
if session.get('client_id') is not None:
# retrieve session information
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:
# data recovery from tax authorities
admindata = config["layers"]["dao"].get_admindata()
# admindata session
session['admindata'] = admindata.asdict()
# we give the customer a number and put it in the session
# this will allow us to track it in the server logs
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")
# tAX CALCULATION
taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
config["layers"]["métier"].calculate_tax(taxpayer, admindata)
# we return the answer to the customer
return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
except (BaseException, ImpôtsError) as erreur:
# we return the answer to the customer
return {"réponse": {"erreurs": [f"{erreur}"]}}, status.HTTP_500_INTERNAL_SERVER_ERROR
- line 14: the controller receives the current session from the web client;
- lines 35-38: if the client has a session, then it contains two keys:
- [client_id]: a client ID (line 37);
- [admindata]: tax administration data in the form of a dictionary (line 38);
- line 35: we check if the session contains one of the two expected keys;
- lines 42–51: case where the client’s session has not yet been initialized;
- line 43: retrieve data from the tax authority from the [dao] layer;
- line 45: this data is placed in the session in the form of a dictionary;
- line 48: a random number is assigned to the client. This number will be different for each client;
- line 49: this number is placed in the session;
- Line 51: We log the fact that the tax authority data was retrieved via the [dao] layer. Access to the [dao] layer is generally costly. That is why it must be limited. The idea here is to retrieve the tax data once from the [dao] layer, store it in session, and retrieve it from there during subsequent requests from the same client. Note that this is not the best solution. Since the administration’s tax data is the same for all clients instances, it belongs in an application-scope object;
- lines 35–40: case where the client’s session was initialized during a previous request;
- line 37: retrieve the client ID from the session;
- line 38: retrieve the administration’s tax data from the session;
- line 40: we log the fact that the client has obtained the tax data from the administration in the session;
25.3. The web client

25.3.1. The [dao] layer
25.3.1.1. The [ImpôtsDaoWithHttpSession] class
The [dao] layer is implemented by the following [ImpôtsDaoWithHttpSession] class:
# 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):
# manufacturer
def __init__(self, config: dict):
# parent initialization
AbstractImpôtsDao.__init__(self, config)
# saving configuration items
# config general
self.__config = config
# server
self.__config_server = config["server"]
# mode debug
self.__debug = config["debug"]
# logger
self.__logger = None
# cookies
self.__cookies = None
# unused method
def get_admindata(self) -> AdminData:
pass
# tAX CALCULATION
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
# we let the exceptions rise
# get parameters
params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
# connection with Auth Basic authentication?
if self.__config_server['authBasic']:
response = requests.get(
# URL of the queried server
self.__config_server['urlServer'],
# URL parameters
params=params,
# basic authentication
auth=(
self.__config_server["user"]["login"],
self.__config_server["user"]["password"]),
cookies=self.__cookies)
else:
# connection without Auth Basic authentication
response = requests.get(self.__config_server['urlServer'], params=params, cookies=self.__cookies)
# retrieve response cookies, if any
if response.cookies:
self.__cookies = response.cookies
# retrieve the session cookie
session_cookie = response.cookies.get('session')
# we decode it to log it
if session_cookie:
# logger
if not self.__logger:
self.__logger = self.__config['logger']
# log on
self.__logger.write(f"cookie de session={decode_flask_session(session_cookie)}\n")
# mode debug ?
if self.__debug:
# logger
if not self.__logger:
self.__logger = self.__config['logger']
# log on
self.__logger.write(f"{response.text}\n")
# response status code HTTP
status_code = response.status_code
# we put the response jSON in a dictionary
résultat = response.json()
# error if status code other than 200 OK
if status_code != status.HTTP_200_OK:
# we know that the errors were associated with the [erreurs] key in the response
raise ImpôtsError(87, résultat['réponse']['erreurs'])
# we know that the result has been associated with the [result] key in the response
# modify the input parameter with this result
taxpayer.fromdict(résultat["réponse"]["result"])
- line 30: the [dao] layer will manage a cookie dictionary;
- Line 58: The property [response.cookies] is a dictionary containing the cookies sent by the server in the headers HTTP and [Set-Cookie];
- line 59: these cookies are stored in the [dao] layer. They will be sent back to the server during subsequent requests from the same client;
- lines 60–68: although not essential, we retrieve the session cookie. In the dictionary of cookies sent by the server, the session cookie is associated with the key [session];
- lines 62–68: we decode the session cookie to log the user in;
- line 68: we will return later to the [decode_flask_session] function, which decodes the session cookie;
- lines 52 and 57: with each request from the same client, the cookies sent by the server are returned to it. This is how the Flask session is maintained between the client and the server;
We must now remember that the [dao] layer will be executed simultaneously by multiple threads. We must therefore examine all the properties of the class instance and determine whether simultaneous access to these properties poses a problem. Here we have added the [self.__cookies] property on line 30. This property is modified on line 59. We therefore have write access to data shared by all threads. However, this access poses a problem: each thread representing a given client has its own session cookie. In fact, it contains a unique client ID (=thread ID) for each client. If we do nothing, thread T2 can overwrite the cookies of thread T1.
We have already seen a method to handle this problem: we can create different keys for each thread in the file [config] passed as a parameter to the constructor (line 17). For example, we can use the thread name as the key:
- line 59, we could write:
config[thread_name][‘cookies’]=cookies
- on line 52, we could then write:
cookies=config[thread_name][‘cookies’]
Here, we will use a different technique: each thread (=client) will have its own [dao] layer. Thus, line 59 is no longer a problem because the cookies used are those of a single client.
To do this, we will create a new class [ImpôtsDaoWithHttpSessionFactory].
25.3.1.2. The Flask session decoding function
The [decode_flask_session] function is defined in the [myutils] script:

We have already studied the |myutils| script. This script is a machine-scope module that the various scripts in this course can import using the statement:
The [decode_flask_session] function is defined there as follows:
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")
- line 2: the URL where I found this function;
- line 1: the parameter [cookie] is the string associated with the key [session] in the dictionary of cookies received by a web client;
- lines 3-16: I won’t comment on this code, as I don’t fully understand it;
We add a new import to the [__init__.py] file:
from .myutils import set_syspath, json_response, decode_flask_session
The new version module is installed alongside the machine-scope modules using the [pip install .] command in a PyCharm terminal:
(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
- Line 1: You must be in the [packages] folder to enter this command;
25.3.1.3. The [ImpôtsDaoWithHttpSessionFactory] class
The [ImpôtsDaoWithHttpSessionFactory] class is as follows:
from ImpôtsDaoWithHttpSession import ImpôtsDaoWithHttpSession
class ImpôtsDaoWithHttpSessionFactory:
def __init__(self, config: dict):
# the parameter
self.__config = config
def new_instance(self):
# render an instance of the [dao] layer
return ImpôtsDaoWithHttpSession(self.__config)
- The [ImpôtsDaoWithHttpSessionFactory] class allows you to create a new implementation of the [dao] layer using the [new_instance] method in lines 10–12;
25.3.2. Configuration
The [config_layers] script, which configures the web client layers, is modified as follows:
def configure(config: dict) -> dict:
# instantiation of applicatuon layers
# layer dao
from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
dao_factory = ImpôtsDaoWithHttpSessionFactory(config)
# make the layer configuration
return {
"dao_factory": dao_factory
}
- lines 5–6: instead of instantiating a single [dao] layer as was done previously, we instantiate a “factory” for this layer (factory = object production facility, in this case the [dao] layer);
- lines 9-11: we render the layer configuration;
25.3.3. The main client script
The [main] script has evolved as follows compared to the previous version script:
# configure the application
import config
config = config.configure({})
# dependencies
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger
# execution of the [dao] layer in a thread
# taxpayers is a list of taxpayers
def thread_function(thread_dao, logger, taxpayers: list):
…
# list of client threads
threads = []
logger = None
# code
try:
…
l_taxpayers = len(taxpayers)
while i < len(taxpayers):
…
# each thread must have its own [dao] layer to properly manage its session cookie
thread_dao = dao_factory.new_instance()
# create the thread
thread = threading.Thread(target=thread_function, args=(thread_dao, logger, thread_taxpayers))
# we add it to the list of threads in the main script
threads.append(thread)
# we launch the thread - this operation is asynchronous - we don't wait for the thread's result
thread.start()
# the main thread waits for all threads it has launched to finish
…
except BaseException as erreur:
# error display
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# close the logger
if logger:
logger.close()
# we're done
print("Travail terminé...")
# end of threads that might still exist if we stopped on error
sys.exit()
- lines 29-30: each thread has its own [dao] layer;
25.3.4. Client execution
The web server is launched, SGBD is launched, and the mail server [hMailServer] is launched. Then we launch the web client script [main]. The execution logs in [data/logs/logs.txt] are then as follows:
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]
- There are a total of 6 threads, so 6 clients threads (lines 1, 3, 4, 6, 8, 9) that simultaneously query the tax calculation server;
- We will follow the [Thread-4] thread, which handles 3 taxpayers (line 6). It will make three sequential requests to the tax calculation server;
- line 10: the first request from [Thread-4];
- line 13: [Thread-4] has received the response to its first request. Inside, it finds a session cookie containing the number [fa3c83b82761c83e13217967] assigned to it by the server;
- line 14: the tax for the first taxpayer;
- line 16: [Thread-4] makes a request for the second taxpayer;
- line 43: [Thread-4] receives the tax amount for the second taxpayer;
- line 45: [Thread-4] makes a request for the third taxpayer;
- line 49: [Thread-4] receives the tax for the third taxpayer;
- line 51: [Thread-4] has finished its work;
Now, let’s look at how the three requests from [Thread-4] were processed on the server side. We can track them in the server logs using their client ID, [fa3c83b82761c83e13217967].
The server-side logs for [data/logs/logs.txt] are as follows:
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}}}
- We find client [fa3c83b82761c83e13217967] for the first time on line 14: to calculate the tax, the server had to retrieve data from the tax authority’s database;
- then we find the client [fa3c83b82761c83e13217967] on line 36. This time, the server finds the tax authority data in the session, which saves it from a potentially costly access to the [dao] layer;
- We encounter client [fa3c83b82761c83e13217967] a third time on line 42, where once again the server uses the client’s session;
This example clearly demonstrates the value of the session for a client: it stores data shared by all of that client’s requests, which is costly to retrieve.
On the client side, the results in the [data/output/résultats.json] file are the same as for the previous versions.
25.4. Testing the [dao] layer
As we did in |previous versions|, we are testing the client’s [dao] layer:

The test class will be executed in the following environment:

- The [2] configuration is identical to the [1] configuration we just examined;
The [TestHttpClientDao] test class is as follows:
import unittest
from Logger import Logger
class TestHttpClientDao(unittest.TestCase):
def test_1(self) -> None:
from TaxPayer import TaxPayer
# { 'married': 'yes', 'children': 2, 'salary': 55555,
# tax': 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)
# check
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__':
# configure the application
import config
config = config.configure({})
# logger
logger = Logger(config["logsFilename"])
# it is stored in the config
config["logger"] = logger
# retrieve the [dao] layer factory
dao_factory = config["layers"]["dao_factory"]
# create an instance of the [dao] layer
dao = dao_factory.new_instance()
# test methods are executed
print("tests en cours...")
unittest.main()
- We create an |execution configuration| for this test;
- We start the web server with its entire environment;
- run the test;
The results are as follows:
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