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 an [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 an [application] scope object rather than a [session] scope object. We gain at least some bandwidth since this reduces the size of the session cookie. But 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 directory structure of the server scripts 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 changes in the [config] script:
# absolute dependencies
absolute_dependencies = [
# project folders
# BaseEntity, MyException
f"{root_dir}/classes/02/entities",
# TaxDaoInterface, TaxBusinessInterface, TaxUiInterface
f"{root_dir}/taxes/v04/interfaces",
# AbstractTaxDao, TaxConsole, TaxBusiness
f"{root_dir}/taxes/v04/services",
# TaxDaoWithAdminDataInDatabase
f"{root_dir}/taxes/v05/services",
# AdminData, TaxError, TaxPayer
f"{root_dir}/impots/v04/entities",
# Constants, Brackets
f"{root_dir}/taxes/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 handles the / URL;
- line 21: we use the utilities from the |previous version|;
25.2.2. The main script [main]
The new [main] script introduces a few changes to the main [main] script from the previous version:
# The Flask application can start
app = Flask(__name__)
# session secret key
app.secret_key = os.urandom(12).hex()
- Line 4: We create a secret key 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:
# Retrieving data from the tax authority
error = False
try:
# admindata will be a read-only application-scope variable
config["admindata"] = config["layers"]["dao"].get_admindata()
# success log
logger.write("[server] database connection successful\n")
except ImpôtsError as ex:
# Log the error
error = True
# error log
log = f"The following error occurred: {ex}"
# console
print(log)
# log file
logger.write(f"{log}\n")
# email to the administrator
send_adminmail(config, log)
Additionally, the [index_controller] controller accepts an additional parameter, the Flask session:
from flask import request, Flask, session
….
# execute the request via a controller
result, 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
# Parameterized URL: /?married=xx&children=yy&salary=zz
from AdminData import AdminData
from TaxError import TaxError
def execute(request: LocalProxy, session: LocalProxy, config: dict) -> tuple:
# dependencies
from TaxPayer import TaxPayer
# initially no errors
errors = []
…
# any errors?
if errors:
# return an error response to the client
return {"response": {"errors": errors}}, status.HTTP_400_BAD_REQUEST
# no errors, we can proceed
# retrieve the configuration associated with the thread
thread_name = threading.current_thread().name
logger = config[thread_name]["config"]["logger"]
# execute the request
response = None
try:
# the simplest case - admindata is already logged in
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}], tax data retrieved during session\n")
else:
# Retrieve data from the tax administration
admindata = config["layers"]["dao"].get_admindata()
# load admindata into the session
session['admindata'] = admindata.asdict()
# assign a number to the client and store that number in the session
# This will allow us to track them in the server logs
client_id = os.urandom(12).hex()
session['client_id'] = client_id
# log
logger.write(f"[index_controller] client [{client_id}], tax data retrieved from the DAO layer\n")
# tax calculation
taxpayer = TaxPayer().fromdict({'married': married, 'children': children, 'salary': salary})
config["layers"]["business"].calculate_tax(taxpayer, admindata)
# return the response to the client
return {"response": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
except (BaseException, TaxError) as error:
# return the response to the client
return {"response": {"errors": [f"{error}"]}}, 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 tax authority data 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 stored in the session;
- line 51: we log the fact that the tax authority data was retrieved by 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 from the [dao] layer once, store it in the session, and fetch it from there during subsequent requests from the same client. Note that this is not the best solution. Since the tax data from the administration is the same for all clients, 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: we retrieve the administration’s tax data from the session;
- line 40: we log the fact that the client has obtained the administration’s tax data from 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 AbstractTaxDao import AbstractTaxDao
from AdminData import AdminData
from TaxError import TaxError
from BusinessTaxInterface import BusinessTaxInterface
from TaxPayer import TaxPayer
class TaxDaoWithHttpSession(AbstractTaxDao, BusinessTaxInterface):
# constructor
def __init__(self, config: dict):
# Initialize parent
AbstractTaxDao.__init__(self, config)
# store configuration elements
# general config
self.__config = config
# server
self.__config_server = config["server"]
# debug mode
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):
# let exceptions propagate
# get parameters
params = {"married": taxpayer.married, "children": taxpayer.children, "salary": taxpayer.salary}
# connection with Basic Authentication?
if self.__config_server['authBasic']:
response = requests.get(
# URL of the server being queried
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 Basic authentication
response = requests.get(self.__config_server['urlServer'], params=params, cookies=self.__cookies)
# retrieve cookies from the response if any
if response.cookies:
self.__cookies = response.cookies
# retrieve the session cookie
session_cookie = response.cookies.get('session')
# decode it to log it
if session_cookie:
# logger
if not self.__logger:
self.__logger = self.__config['logger']
# log
self.__logger.write(f"session cookie={decode_flask_session(session_cookie)}\n")
# debug mode?
if self.__debug:
# logger
if not self.__logger:
self.__logger = self.__config['logger']
# logging
self.__logger.write(f"{response.text}\n")
# HTTP response status code
status_code = response.status_code
# put the JSON response into a dictionary
result = response.json()
# error if status code is not 200 OK
if status_code != status.HTTP_200_OK:
# we know that errors have been associated with the [errors] key in the response
raise TaxError(87, result['response']['errors'])
# We know that the result has been associated with the [result] key in the response
# we modify the input parameter with this result
taxpayer.fromdict(result["response"]["result"])
- line 30: the [dao] layer will manage a dictionary of cookies;
- line 58: the [response.cookies] property is a dictionary containing the cookies sent by the server in the [Set-Cookie] HTTP headers;
- 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 strictly necessary, 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 see if 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) 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 [config] file 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’ll use a different technique: each thread (=client) will have its own [dao] layer. This way, line 59 is no longer an issue 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:
In it, we define the [decode_flask_session] function 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 [cookie] parameter is the string associated with the [session] key 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 of [myutils] is installed among the machine-wide 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 the 'wheel' package is not installed.
Installing collected packages: myutils
Attempting to 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):
# store the parameter
self.__config = config
def new_instance(self):
# return an instance of the [dao] layer
return TaxDaoWithHttpSession(self.__config)
- The [ImpôtsDaoWithHttpSessionFactory] class allows us 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 the application layers
# DAO layer
from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
dao_factory = ImpôtsDaoWithHttpSessionFactory(config)
# return 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 factory, in this case the [DAO] layer);
- lines 9-11: we return the layer configuration;
25.3.3. The client’s main script
The [main] script has changed as follows compared to the previous version:
# 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
# executing 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))
# add it to the list of threads in the main script
threads.append(thread)
# Start the thread—this operation is asynchronous—we do not wait for the thread's result
thread.start()
# The main thread waits for all the threads it has started to finish
…
except BaseException as error:
# display the error
print(f"The following error occurred: {error}")
finally:
# close the logger
if logger:
logger.close()
# we're done
print("Work completed...")
# Terminate any threads that might still be running if we stopped due to an error
sys.exit()
- lines 29-30: each thread has its own [dao] layer;
25.3.4. Client execution
The web server is launched, the DBMS is launched, the mail server [hMailServer] is launched. Then we launch the web client's [main] script. The execution logs in [data/logs/logs.txt] are then as follows:
2020-07-25 10:21:46.478511, Thread-1: start of thread [Thread-1] with 1 taxpayer(s)
2020-07-25 10:21:46.479111, Thread-1: Start of tax calculation for {"id": 1, "married": "yes", "children": 2, "salary": 55555}
2020-07-25 10:21:46.479111, Thread-2: Start of thread [Thread-2] with 1 taxpayer(s)
2020-07-25 10:21:46.480195, Thread-3: Start of thread [Thread-3] with 2 taxpayer(s)
2020-07-25 10:21:46.480195, Thread-2: Start of tax calculation for {"id": 2, "married": "yes", "children": 2, "salary": 50000}
2020-07-25 10:21:46.481137, Thread-4: Start of thread [Thread-4] with 3 taxpayers
2020-07-25 10:21:46.481137, Thread-3: Start of tax calculation for {"id": 3, "married": "yes", "children": 3, "salary": 50000}
2020-07-25 10:21:46.482279, Thread-5: Start of thread [Thread-5] with 3 taxpayers
2020-07-25 10:21:46.482622, Thread-6: Start of thread [Thread-6] with 1 taxpayer
2020-07-25 10:21:46.482622, Thread-4: Start of tax calculation for {"id": 5, "married": "no", "children": 3, "salary": 100000}
2020-07-25 10:21:46.485923, Thread-5: Start of tax calculation for {"id": 8, "married": "no", "children": 0, "salary": 100000}
2020-07-25 10:21:46.485923, Thread-6: Start of tax calculation for {"id": 11, "married": "yes", "children": 3, "salary": 200000}
2020-07-25 10:21:46.725910, Thread-4: session cookie={"admindata":{"max_10_percent_allowance":12502.0,"min_10_percent_allowance":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,"limits":[9964.0,27519.0,73779.0,156244.0,90000.0],"single_tax_credit_threshold":1196.0,"couple_tax_credit_threshold":1970.0,"single_tax_threshold_for_tax_credit":1595.0,"couple_tax_threshold_for_tax_credit":2627.0,"half-share_income_limit":1551.0,"single_income_limit_for_reduction":21037.0,"couple_income_limit_for_reduction":42074.0,"half-share_reduction_value":3797.0},"client_id":"fa3c83b82761c83e13217967"}
2020-07-25 10:21:46.725910, Thread-4 : {"response": {"result": {"married": "no", "children": 3, "salary": 100000, "tax": 16782, "surcharge": 7176, "rate": 0.41, "discount": 0, "reduction": 0}}}
2020-07-25 10:21:46.725910, Thread-4: End of tax calculation for {"id": 5, "married": "no", "children": 3, "salary": 100000, "tax": 16782, "surcharge": 7176, "rate": 0.41, "discount": 0, "reduction": 0}
2020-07-25 10:21:46.726960, Thread-4: Starting tax calculation for {"id": 6, "married": "yes", "children": 3, "salary": 100000}
2020-07-25 10:21:47.514108, Thread-3: session cookie={"admindata":{"max_10_percent_allowance":12502.0,"min_10_percent_allowance":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,"limits":[9964.0,27519.0,73779.0,156244.0,24999.5],"single_tax_credit_threshold":1196.0,"couple_tax_credit_threshold":1970.0,"single_tax_threshold_for_tax_credit":1595.0,"couple_tax_threshold_for_tax_credit":2627.0,"half-share_income_limit":1551.0,"single_income_limit_for_reduction":21037.0,"couple_income_limit_for_reduction":42074.0,"half-share_reduction_value":3797.0},"client_id":"700e3f5dc808c7c48f0c9007"}
2020-07-25 10:21:47.514610, Thread-3 : {"response": {"result": {"married": "yes", "children": 3, "salary": 50000, "tax": 0, "surcharge": 0, "rate": 0.14, "discount": 720, "reduction": 0}}}
2020-07-25 10:21:47.514939, Thread-3: End of tax calculation for {"id": 3, "married": "yes", "children": 3, "salary": 50000, "tax": 0, "surcharge": 0, "rate": 0.14, "discount": 720, "reduction": 0}
2020-07-25 10:21:47.514939, Thread-3: Start of tax calculation for {"id": 4, "married": "no", "children": 2, "salary": 100000}
2020-07-25 10:21:47.527211, Thread-5: session cookie={"admindata":{"max_10_percent_allowance":12502.0,"min_10_percent_allowance":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,"limits":[9964.0,27519.0,73779.0,156244.0,90000.0],"single_tax_credit_threshold":1196.0,"couple_tax_credit_threshold":1970.0,"single_tax_threshold_for_tax_credit":1595.0,"couple_tax_threshold_for_tax_credit":2627.0,"half-share_income_limit":1551.0,"single_income_limit_for_reduction":21037.0,"couple_income_limit_for_reduction":42074.0,"half-share_reduction_value":3797.0},"client_id":"9e14a5d4a3057f69ab95ab2d"}
2020-07-25 10:21:47.527211, Thread-2: session cookie={"admindata":{"max_10_percent_deduction":12502.0,"min_10_percent_deduction":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,"limits":[9964.0,27519.0,73779.0,156244.0,22500.0],"single_tax_credit_threshold":1196.0,"couple_tax_credit_threshold":1970.0,"single_tax_threshold_for_tax_credit":1595.0,"couple_tax_threshold_for_tax_credit":2627.0,"half-share_income_limit":1551.0,"single_income_limit_for_reduction":21037.0,"couple_income_limit_for_reduction":42074.0,"half-share_reduction_value":3797.0},"client_id":"a06e8fd70a44c9e311f4dce0"}
2020-07-25 10:21:47.527211, Thread-5 : {"response": {"result": {"married": "no", "children": 0, "salary": 100000, "tax": 22986, "surcharge": 0, "rate": 0.41, "discount": 0, "reduction": 0}}}
2020-07-25 10:21:47.527211, Thread-1: session cookie={"admindata":{"max_10_percent_deduction":12502.0,"min_10_percent_deduction":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,"limits":[9964.0,27519.0,73779.0,156244.0,90000.0],"single_tax_credit_threshold":1196.0,"couple_tax_credit_threshold":1970.0,"single_tax_threshold_for_tax_credit":1595.0,"couple_tax_threshold_for_tax_credit":2627.0,"half-share_income_limit":1551.0,"single_income_limit_for_reduction":21037.0,"couple_income_limit_for_reduction":42074.0,"half-share_reduction_value":3797.0},"client_id":"28c38df998f67685b3a482b8"}
2020-07-25 10:21:47.527211, Thread-2 : {"response": {"result": {"married": "yes", "children": 2, "salary": 50000, "tax": 1384, "surcharge": 0, "rate": 0.14, "discount": 384, "reduction": 347}}}
2020-07-25 10:21:47.528341, Thread-5: end of tax calculation for {"id": 8, "married": "no", "children": 0, "salary": 100000, "tax": 22986, "surcharge": 0, "rate": 0.41, "discount": 0, "reduction": 0}
2020-07-25 10:21:47.528341, Thread-1: {"response": {"result": {"married": "yes", "children": 2, "salary": 55555, "tax": 2814, "surcharge": 0, "rate": 0.14, "discount": 0, "reduction": 0}}}
2020-07-25 10:21:47.528842, Thread-2: end of tax calculation for {"id": 2, "married": "yes", "children": 2, "salary": 50000, "tax": 1384, "surcharge": 0, "rate": 0.14, "discount": 384, "reduction": 347}
2020-07-25 10:21:47.529349, Thread-5: Start of tax calculation for {"id": 9, "married": "yes", "children": 2, "salary": 30000}
2020-07-25 10:21:47.529699, Thread-1: end of tax calculation for {"id": 1, "married": "yes", "children": 2, "salary": 55555, "tax": 2814, "surcharge": 0, "rate": 0.14, "discount": 0, "reduction": 0}
2020-07-25 10:21:47.529699, Thread-2: end of thread [Thread-2]
2020-07-25 10:21:47.531905, Thread-1: end of thread [Thread-1]
2020-07-25 10:21:47.536121, Thread-6: session cookie={"admindata":{"max_10_percent_discount":12502.0,"min_10_percent_discount":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,"limits":[9964.0,27519.0,73779.0,156244.0,93749.0],"single_tax_credit_threshold":1196.0,"couple_tax_credit_threshold":1970.0,"single_tax_threshold_for_tax_credit":1595.0,"couple_tax_threshold_for_tax_credit":2627.0,"half-share_income_limit":1551.0,"single_income_limit_for_reduction":21037.0,"couple_income_limit_for_reduction":42074.0,"half-share_reduction_value":3797.0},"client_id":"38499b63076516c02f2770ec"}
2020-07-25 10:21:47.537161, Thread-3 : {"response": {"result": {"married": "no", "children": 2, "salary": 100000, "tax": 19884, "surcharge": 4480, "rate": 0.41, "discount": 0, "reduction": 0}}}
2020-07-25 10:21:47.537161, Thread-6 : {"response": {"result": {"married": "yes", "children": 3, "salary": 200000, "tax": 42842, "surcharge": 17283, "rate": 0.41, "discount": 0, "reduction": 0}}}
2020-07-25 10:21:47.538156, Thread-3: End of tax calculation for {"id": 4, "married": "no", "children": 2, "salary": 100000, "tax": 19884, "surcharge": 4480, "rate": 0.41, "discount": 0, "reduction": 0}
2020-07-25 10:21:47.538557, Thread-6: End of tax calculation for {"id": 11, "married": "yes", "children": 3, "salary": 200000, "tax": 42842, "surcharge": 17283, "rate": 0.41, "discount": 0, "reduction": 0}
2020-07-25 10:21:47.538828, Thread-3: end of thread [Thread-3]
2020-07-25 10:21:47.538828, Thread-6: end of thread [Thread-6]
2020-07-25 10:21:47.546198, Thread-5: {"response": {"result": {"married": "yes", "children": 2, "salary": 30000, "tax": 0, "surcharge": 0, "rate": 0.0, "discount": 0, "reduction": 0}}}
2020-07-25 10:21:47.546198, Thread-5: end of tax calculation for {"id": 9, "married": "yes", "children": 2, "salary": 30000, "tax": 0, "surcharge": 0, "rate": 0.0, "discount": 0, "reduction": 0}
2020-07-25 10:21:47.546198, Thread-5: Start of tax calculation for {"id": 10, "married": "no", "children": 0, "salary": 200000}
2020-07-25 10:21:47.739643, Thread-4: {"response": {"result": {"married": "yes", "children": 3, "salary": 100000, "tax": 9200, "surcharge": 2180, "rate": 0.3, "discount": 0, "reduction": 0}}}
2020-07-25 10:21:47.739643, Thread-4: End of tax calculation for {"id": 6, "married": "yes", "children": 3, "salary": 100000, "tax": 9200, "surcharge": 2180, "rate": 0.3, "discount": 0, "reduction": 0}
2020-07-25 10:21:47.740668, Thread-4: Starting tax calculation for {"id": 7, "married": "yes", "children": 5, "salary": 100000}
2020-07-25 10:21:48.557469, Thread-5: {"response": {"result": {"married": "no", "children": 0, "salary": 200000, "tax": 64210, "surcharge": 7498, "rate": 0.45, "discount": 0, "reduction": 0}}}
2020-07-25 10:21:48.558715, Thread-5: end of tax calculation for {"id": 10, "married": "no", "children": 0, "salary": 200000, "tax": 64210, "surcharge": 7498, "rate": 0.45, "discount": 0, "reduction": 0}
2020-07-25 10:21:48.558715, Thread-5: end of thread [Thread-5]
2020-07-25 10:21:48.753025, Thread-4 : {"response": {"result": {"married": "yes", "children": 5, "salary": 100000, "tax": 4230, "surcharge": 0, "rate": 0.14, "discount": 0, "reduction": 0}}}
2020-07-25 10:21:48.753318, Thread-4: End of tax calculation for {"id": 7, "married": "yes", "children": 5, "salary": 100000, "tax": 4230, "surcharge": 0, "rate": 0.14, "discount": 0, "reduction": 0}
2020-07-25 10:21:48.753540, Thread-4: end of thread [Thread-4]
- There are a total of 6 threads, meaning 6 clients (lines 1, 3, 4, 6, 8, 9) that simultaneously query the tax calculation server;
- we will follow thread [Thread-4], which handles 3 taxpayers (line 6). It will make three sequential requests to the tax calculation server;
- Line 10: [Thread-4]’s first request;
- 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 amount 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 its client ID [fa3c83b82761c83e13217967].
The server-side logs [data/logs/logs.txt] are as follows:
2020-07-25 10:21:39.187366, MainThread: [server] server startup
2020-07-25 10:21:40.439093, MainThread: [server] server startup
2020-07-25 10:21:46.502011, Thread-2: [index] request: <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=50000' [GET]>
2020-07-25 10:21:46.504049, Thread-2: [index] thread paused for 1 second(s)
2020-07-25 10:21:46.505452, Thread-3: [index] request: <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=55555' [GET]>
2020-07-25 10:21:46.506257, Thread-3: [index] Thread paused for 1 second
2020-07-25 10:21:46.507292, Thread-4: [index] request: <Request 'http://127.0.0.1:5000/?marié=non&enfants=0&salaire=100000' [GET]>
2020-07-25 10:21:46.507292, Thread-4: [index] Thread paused for 1 second
2020-07-25 10:21:46.508301, Thread-5: [index] request: <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=50000' [GET]>
2020-07-25 10:21:46.509293, Thread-5: [index] Thread paused for 1 second
2020-07-25 10:21:46.511808, Thread-6: [index] request: <Request 'http://127.0.0.1:5000/?marié=non&enfants=3&salaire=100000' [GET]>
2020-07-25 10:21:46.517604, Thread-7: [index] request: <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=200000' [GET]>
2020-07-25 10:21:46.517604, Thread-7: [index] Thread paused for 1 second
2020-07-25 10:21:46.719504, Thread-6: [index_controller] client [fa3c83b82761c83e13217967], tax data retrieved from the DAO layer
2020-07-25 10:21:46.720003, Thread-6 : [index] {'response': {'result': {'married': 'no', 'children': 3, 'salary': 100000, 'tax': 16782, 'surcharge': 7176, 'rate': 0.41, 'discount': 0, 'reduction': 0}}}
2020-07-25 10:21:46.736108, Thread-8: [index] request: <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=100000' [GET]>
2020-07-25 10:21:46.736108, Thread-8 : [index] paused thread for 1 second(s)
2020-07-25 10:21:47.506709, Thread-2: [index_controller] client [700e3f5dc808c7c48f0c9007], tax data retrieved from the DAO layer
2020-07-25 10:21:47.507216, Thread-2: [index] {'response': {'result': {'married': 'yes', 'children': 3, 'salary': 50000, 'tax': 0, 'surcharge': 0, 'rate': 0.14, 'discount': 720, 'reduction': 0}}}
2020-07-25 10:21:47.507216, Thread-3: [index_controller] client [28c38df998f67685b3a482b8], tax data retrieved from the DAO layer
2020-07-25 10:21:47.508442, Thread-4: [index_controller] client [9e14a5d4a3057f69ab95ab2d], tax data retrieved from the DAO layer
2020-07-25 10:21:47.508940, Thread-3: [index] {'response': {'result': {'married': 'yes', 'children': 2, 'salary': 55555, 'tax': 2814, 'surcharge': 0, 'rate': 0.14, 'discount': 0, 'reduction': 0}}}
2020-07-25 10:21:47.510506, Thread-4: [index] {'response': {'result': {'married': 'no', 'children': 0, 'salary': 100000, 'tax': 22986, 'surcharge': 0, 'rate': 0.41, 'discount': 0, 'reduction': 0}}}
2020-07-25 10:21:47.511513, Thread-5: [index_controller] client [a06e8fd70a44c9e311f4dce0], tax data retrieved from the DAO layer
2020-07-25 10:21:47.514939, Thread-5 : [index] {'response': {'result': {'married': 'yes', 'children': 2, 'salary': 50000, 'tax': 1384, 'surcharge': 0, 'rate': 0.14, 'discount': 384, 'reduction': 347}}}
2020-07-25 10:21:47.520727, Thread-7: [index_controller] client [38499b63076516c02f2770ec], tax data retrieved from the DAO layer
2020-07-25 10:21:47.523162, Thread-7 : [index] {'response': {'result': {'married': 'yes', 'children': 3, 'salary': 200000, 'tax': 42842, 'surcharge': 17283, 'rate': 0.41, 'discount': 0, 'reduction': 0}}}
2020-07-25 10:21:47.530835, Thread-9: [index] request: <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], tax data retrieved during session
2020-07-25 10:21:47.531905, Thread-9 : [index] {'response': {'result': {'married': 'no', 'children': 2, 'salary': 100000, 'tax': 19884, 'surcharge': 4480, 'rate': 0.41, 'discount': 0, 'reduction': 0}}}
2020-07-25 10:21:47.541899, Thread-10: [index] request: <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], tax data retrieved during session
2020-07-25 10:21:47.542488, Thread-10 : [index] {'response': {'result': {'married': 'yes', 'children': 2, 'salary': 30000, 'tax': 0, 'surcharge': 0, 'rate': 0.0, 'discount': 0, 'reduction': 0}}}
2020-07-25 10:21:47.553628, Thread-11: [index] request: <Request 'http://127.0.0.1:5000/?marié=non&enfants=0&salaire=200000' [GET]>
2020-07-25 10:21:47.553628, Thread-11 : [index] thread paused for 1 second(s)
2020-07-25 10:21:47.736910, Thread-8: [index_controller] client [fa3c83b82761c83e13217967], tax data retrieved during session
2020-07-25 10:21:47.737191, Thread-8: [index] {'response': {'result': {'married': 'yes', 'children': 3, 'salary': 100000, 'tax': 9200, 'surcharge': 2180, 'rate': 0.3, 'discount': 0, 'reduction': 0}}}
2020-07-25 10:21:47.748226, Thread-12: [index] request: <Request 'http://127.0.0.1:5000/?marié=oui&enfants=5&salaire=100000' [GET]>
2020-07-25 10:21:47.748226, Thread-12 : [index] thread paused for 1 second(s)
2020-07-25 10:21:48.554695, Thread-11: [index_controller] client [9e14a5d4a3057f69ab95ab2d], tax data retrieved during session
2020-07-25 10:21:48.555070, Thread-11: [index] {'response': {'result': {'married': 'no', 'children': 0, 'salary': 200000, 'tax': 64210, 'surcharge': 7498, 'rate': 0.45, 'discount': 0, 'reduction': 0}}}
2020-07-25 10:21:48.748753, Thread-12: [index_controller] client [fa3c83b82761c83e13217967], tax data retrieved during session
2020-07-25 10:21:48.748753, Thread-12 : [index] {'response': {'result': {'married': 'yes', 'children': 5, 'salary': 100000, 'tax': 4230, 'surcharge': 0, 'rate': 0.14, 'discount': 0, 'reduction': 0}}}
- The client [fa3c83b82761c83e13217967] is encountered 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 see the client [fa3c83b82761c83e13217967] again on line 36. This time, the server retrieves the tax authority data from the session, which saves it a potentially costly access to the [DAO] layer;
- We encounter the client [fa3c83b82761c83e13217967] a third time on line 42, where once again the server uses the client’s session;
This example clearly illustrates 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 file [data/output/results.json] are the same as in previous versions.
25.4. Testing the [DAO] layer
As we did in the |previous versions|, we test the client’s [dao] layer:

The test class will be executed in the following environment:

- Configuration [2] is identical to configuration [1], which 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, 'surcharge': 0, 'discount': 0, 'reduction': 0, 'rate': 0.14}
taxpayer = TaxPayer().fromdict({"married": "yes", "children": 2, "salary": 55555})
dao.calculate_tax(taxpayer)
# verification
self.assertAlmostEqual(taxpayer.tax, 2815, delta=1)
self.assertEqual(taxpayer.discount, 0)
self.assertEqual(taxpayer.reduction, 0)
self.assertAlmostEqual(taxpayer.rate, 0.14, delta=0.01)
self.assertEqual(taxpayer.surcharge, 0)
…
if __name__ == '__main__':
# configure the application
import config
config = config.configure({})
# logger
logger = Logger(config["logsFilename"])
# Store it 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()
# run the test methods
print("Tests in progress...")
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 in progress...
...........
----------------------------------------------------------------------
Ran 11 tests in 3.392s
OK
Process finished with exit code 0