29. Application exercise: version 11
29.1. Introduction
In previous versions of the client/server tax calculation application, the [métier] layer that implements the business rules for this calculation was server-side. We now propose to move it to the client side. What is the benefit? Some of the work previously performed by the server will be moved to the client side. Consider a scenario where a server is queried by N clients; N tax business calculations will be performed by the clients. In previous versions, the server performed these N business calculations. Because it no longer performs the business calculation, the server will respond more quickly to its clients requests and will therefore be able to serve more of them simultaneously.
The client/server architecture becomes as follows:

- the [métier] [10] layer has been duplicated as [12] on the client;
- a new script [main2] [11] has been added to the client;
The web client will have two ways to calculate the tax for the list of taxpayers found in [3]:
- Use the method from the previous version. It uses the server's [métier] and [10] layers. The [main] script will use this method;
- simply request the tax administration data from the server ([2-4]) and then use the client-side [métier] and [12] layers;
We will compare the performance of the two methods.
29.2. The web server
The web server directory structure will be as follows:

- The [http-servers/06] folder is initially created by copying the [http-servers/05] folder. We will indeed retain the features of the previous version 10. We will simply add a new feature to it. This is represented by the presence of a new controller [get_admindata_controller] [1]. The other controller, [calculate_tax_controller], is none other than the old controller [index_controller], which has been renamed;
29.3. Configuration
The server will offer two URL services:
- [/calculate-tax] to calculate the tax for a list of taxpayers passed in the body of a POST. It therefore corresponds to the URL [/] of the previous version 10;
- [/get-admindata] outputs the jSON data string from the tax administration;
The configuration [config] associates each of these URL with the controller that processes it:
# web application controller dictionary
import calculate_tax_controller, get_admindata_controller
controllers = {"calculate-tax": calculate_tax_controller, "get-admindata": get_admindata_controller}
config['controllers'] = controllers
29.4. The main script [main]
The main script [main] restructures the script [main] from the previous version:
# a mysql or pgres parameter is expected
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
sgbd = sys.argv[1].lower()
erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# configure the application
import config
config = config.configure({'sgbd': sgbd})
# dependencies
…
# 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
…
…
# if there has been an error, we stop
if erreur:
sys.exit(2)
# the Flask application can be started
app = Flask(__name__)
# main controller
def main_controller() -> tuple:
# the requested action is retrieved
dummy, action=request.path.split('/')
logger = None
try:
# logger
logger = Logger(config["logsFilename"])
# we store it in a config associated with the thread
thread_config = {"logger": logger}
thread_name = threading.current_thread().name
config[thread_name] = {"config": thread_config}
# log the request
logger.write(f"[index] requête : {request}\n")
# the thread is interrupted if requested
sleep_time = config["sleep_time"]
if sleep_time != 0:
# pause is randomized so that some threads are interrupted and others not
aléa = randint(0, 1)
if aléa == 1:
# log before break
logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
# break
time.sleep(sleep_time)
# the request is executed by the request controller
controller = config['controllers'][action]
résultat, status_code = controller.execute(request, config)
# was there a fatal error?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# send an e-mail to the application administrator
config_mail = config["adminMail"]
config_mail["logger"] = logger
SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
# we log the answer
logger.write(f"[index] {résultat}\n")
# we send the answer
print(résultat)
return json_response(résultat, status_code)
except BaseException as erreur:
# log the error if possible
if logger:
logger.write(f"[index] {erreur}")
# we prepare the response to the customer
résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
# we send the answer
return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
finally:
# close the log file if it has been opened
if logger:
logger.close()
# tAX CALCULATION
@app.route('/calculate-tax', methods=['POST'])
@auth.login_required
def calculate_tax():
# we hand over to the main controller
return main_controller()
# obtain data from tax authorities
@app.route('/get-admindata', methods=['GET'])
@auth.login_required
def get_admindata():
# we hand over to the main controller
return main_controller()
# hand only
if __name__ == '__main__':
# start the server
app.config.update(ENV="development", DEBUG=True)
app.run(threaded=True)
- lines 88–93: The function [calculate_tax] processes URL and [/calculate-tax];
- lines 95–100: the function [get_admindata] processes URL and [/get-admindata];
- these two functions do nothing on their own. They immediately hand control over to the main controller [main_controller] in lines 37–86;
- Lines 37–86: The main controller [main_controller] is nothing more than the function [index] from the previous version, with one minor difference: where the [index] function processed only a single URL, here [main_controller] processes two URL. It must therefore have these processed by one of the two [calculate_tax_controller, get_admin,data_controller] controllers;
- lines 39–40: we retrieve the requested action [calculate_tax] or [get_admindata]. This information is in the path of URL [request.path]. Depending on the case, [request.path] is either [/get-admindata] or [/calculate_tax]. Splitting line 40 will yield two elements:
- the empty string for the part preceding the /;
- the name of the requested action for the part following the /;
- Lines 62–63: Once the action for URL has been retrieved, we know which controller to use to process URL. This information is in the [config] configuration;
29.5. The controllers
The [calculate_tax_controller] controller is none other than the [index_controller] controller from the previous version.
The [get_admindata_controller] controller is as follows:
from flask_api import status
from werkzeug.local import LocalProxy
def execute(request: LocalProxy, config: dict) -> tuple:
# we return the answer
return {"réponse": {"result": config["admindata"].asdict()}}, status.HTTP_200_OK
- URL [/get-admindata] must return the string jSON from the tax administration data;
- line 6: this data was retrieved by the main script [main] and placed in the dictionary [config] as an object [AdminData]. We return the dictionary of this object;
29.6. Postman Tests
We start the web server, SGBD, and the mail server, [hMailServer]. Then, using a Postman client, we calculate the tax for several taxpayers:

In the Postman console, the client/server dialogue is as follows:
POST /calculate-tax HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 5e71461a-fec8-4315-85e8-41721de939e5
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 824
[
{
"marié": "oui",
"enfants": 2,
"salaire": 55555
},
…
{
"marié": "oui",
"enfants": 3,
"salaire": 200000
}
]
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 1461
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 29 Jul 2020 07:02:07 GMT
{"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347…]}}
Now let’s request URL [/get-admindata] with a GET:

The client/server dialogue in the Postman console is as follows:
GET /get-admindata HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 4af342c4-7ecb-4ab2-9e12-d653f81da424
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 596
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 29 Jul 2020 07:07:24 GMT
{"réponse": {"result": {"limites": [9964.0, 27519.0, 73779.0, 156244.0, 93749.0], "coeffr": [0.0, 0.14, 0.3, 0.41, 0.45], "coeffn": [0.0, 1394.96, 5798.0, 13913.7, 20163.4], "plafond_decote_couple": 1970.0, "valeur_reduc_demi_part": 3797.0, "plafond_revenus_celibataire_pour_reduction": 21037.0, "plafond_qf_demi_part": 1551.0, "abattement_dixpourcent_max": 12502.0, "plafond_impot_celibataire_pour_decote": 1595.0, "plafond_decote_celibataire": 1196.0, "plafond_revenus_couple_pour_reduction": 42074.0, "id": 1, "abattement_dixpourcent_min": 437.0, "plafond_impot_couple_pour_decote": 2627.0}}}
29.7. The web client


The [http-clients/06] file is initially created by copying the [http-clients/05] file. The modification work essentially consists of:
- modifying the [config_layers] configuration so that it now includes a [métier] layer. Previously, it had only a [dao] layer;
- adding a new method to the [dao] layer;
- write a [main2] script that will rely on the client’s [métier] layer to calculate taxpayers’ taxes;
29.7.1. Configuration of client layers
Layer configuration occurs at two points:
- in the [config] configuration, which must include the folder containing the implementation of the [métier] layer in the client dependencies. This folder was already included in the 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, slices
f"{root_dir}/impots/v05/entities",
# ImpôtsDaoWithHttpClient
f"{script_dir}/../services",
# configuration scripts
script_dir,
# Logger
f"{root_dir}/impots/http-servers/02/utilities",
]
Then the file [config_layers] must be modified:
def configure(config: dict) -> dict:
# instantiation of applicatuon layers
# layer [métier]
from ImpôtsMétier import ImpôtsMétier
métier = ImpôtsMétier()
# layer dao
from ImpôtsDaoWithHttpClient import ImpôtsDaoWithHttpClient
dao = ImpôtsDaoWithHttpClient(config)
# make the layer configuration
return {
"dao": dao,
"métier": métier
}
- lines 4-6: instantiation of the [métier] layer;
- lines 13-16: the [métier] layer is rendered in the layer dictionary;
29.7.2. Implementation of the [dao] layer

The [dao] layer will implement the following [InterfaceImpôtsDaoWithHttpClient] interface:
from abc import abstractmethod
from AbstractImpôtsDao import AbstractImpôtsDao
class InterfaceImpôtsDaoWithHttpClient(AbstractImpôtsDao):
# tAX CALCULATION
@abstractmethod
def calculate_tax_in_bulk_mode(self, taxpayers: list):
pass
- line 5: the [InterfaceImpôtsDaoWithHttpClient] interface inherits from the abstract class [AbstractImpôtsDao], which manages access to the client’s file system. Note that it has an abstract method [get_admindata];
- lines 7–10: the method [calculate_tax_in_bulk_mode] that we defined in the previous version allows the tax to be calculated for a list of taxpayers;
This interface is implemented by the following [ImpôtsDaoWithHttpClient] class:
# imports
import json
import requests
from flask_api import status
from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDaoWithHttpClient import InterfaceImpôtsDaoWithHttpClient
class ImpôtsDaoWithHttpClient(InterfaceImpôtsDaoWithHttpClient):
# 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
def get_admindata(self) -> AdminData:
# we let the exceptions rise
# url of service
config_server = self.__config_server
config_services = config_server['url_services']
url_service = f"{config_server['urlServer']}{config_services['get-admindata']}"
# connection
if config_server['authBasic']:
response = requests.get(url_service,
auth=(
config_server["user"]["login"],
config_server["user"]["password"]))
else:
response = requests.get(url_service)
# mode debug ?
if self.__debug:
# logger
if not self.__logger:
self.__logger = self.__config['logger']
# log on
self.__logger.write(f"{response.text}\n")
# status code
status_code = response.status_code
# result in the form of a dictionary
résultat = json.loads(response.text)
# if status code other than 200 OK
if status_code != status.HTTP_200_OK:
raise ImpôtsError(58, résultat['réponse']['erreurs'])
# return the result (a dictionary)
return AdminData().fromdict(résultat["réponse"]["result"])
# bulk tax calculation
def calculate_tax_in_bulk_mode(self, taxpayers: list):
# we let the exceptions rise
…
- line 13: the class [ImpôtsDaoWithHttpClient] implements the interface [InterfaceImpôtsDaoWithHttpClient]. It therefore derives from the class [AbstractImpôtsDao];
- lines 65–66: the [calculate_tax_in_bulk_mode] method discussed in the previous version;
- lines 29–62: the [get_admindata] method, which the parent class [AbstractImpôtsDao] has declared as abstract. It is therefore implemented in the child class;
- lines 33-35: the URL of the web service that the [get-admindata] method must query is determined. These service URLs are defined in the client configuration [config]:
# tax calculation server
"server": {
"urlServer": "http://127.0.0.1:5000",
"authBasic": True,
"user": {
"login": "admin",
"password": "admin"
},
"url_services": {
"calculate-tax": "/calculate-tax",
"get-admindata": "/get-admindata"
}
},
- (continued)
- lines 9–12: the two URL instances on the web server;
- lines 37–44: the service URL is queried synchronously;
- lines 46–42: if the configuration requires it, the server’s response is logged;
- line 57: we know that the server sent a string from a dictionary;
- lines 58-60: if the response status HTTP is not 200, then an exception is thrown;
- lines 61–62: we return the [AdminData] object encapsulating the tax administration data sent by the server;
29.8. The [main, main2] scripts
The [main] script is the one from the previous version. It uses the [calculate_tax_in_bulk_mode] method from the [dao] layer and therefore uses the server’s [métier] layer;
The [main2] script does the same thing as the [main] script but uses the [métier] layer on the client:
# configure the application
import config
config = config.configure({})
# dependencies
from ImpôtsError import ImpôtsError
from Logger import Logger
logger = None
# code
try:
# logger
logger = Logger(config["logsFilename"])
# it is stored in the config
config["logger"] = logger
# start log
logger.write("début du calcul de l'impôt des contribuables\n")
# retrieve the [dao] layer
dao = config["layers"]["dao"]
# we get the taxpayers back
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# taxpayers?
if not taxpayers:
raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
# we retrieve data from the tax authorities
admindata = dao.get_admindata()
# taxpayer tax calculation using the [métier] layer
métier = config['layers']['métier']
for taxpayer in taxpayers:
métier.calculate_tax(taxpayer, admindata)
# save the results in the jSON file
dao.write_taxpayers_results(taxpayers)
# except BaseException as error:
# # error display
# print(f "The following error has occurred: {error}")
finally:
# close the logger
if logger:
# end log
logger.write("fin du calcul de l'impôt des contribuables\n")
# closing the logger
logger.close()
# we're done
print("Travail terminé...")
- lines 26-27: retrieve data from the tax authority’s server;
- lines 28-31: then the taxpayers' tax is calculated locally;
29.9. Client tests
In each of the [main, main2] scripts, we log the start and end of the script. This allows us to calculate the script’s execution time. Let’s make some predictions:
- the [main] script from the previous version:
- creates N threads that run simultaneously;
- each thread processes a batch of taxpayers for whom it calculates the tax via a single query to the server;
- because the N threads run simultaneously, the N+1 request is launched before the Nth request has received its response. Thus, the N requests cost more than a single request but probably not much more. There are also 11 (the number of taxpayers) business calculations on the server;
- the script [main2] from this version:
- makes a single request to the server;
- performs 11 business calculations locally on the client;
The business calculations will take the same amount of time whether on the server or the client. The difference will therefore lie in the requests. We can therefore expect the execution time of [main] to be slightly longer than that of [main2].
We launch the version 11 server, the SGBD server, and the [hMailServer] mail server. On the server side, we set the [sleep_time] parameter to zero so that both tests are run under the same conditions.
Run 1 [main]
Running [main] produces the following logs:
2020-07-29 14:35:50.016079, MainThread : début du calcul de l'impôt des contribuables
2020-07-29 14:35:50.016079, Thread-1 : début du calcul de l'impôt des 1 contribuables
2020-07-29 14:35:50.016079, Thread-2 : début du calcul de l'impôt des 4 contribuables
2020-07-29 14:35:50.016079, Thread-3 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.016079, Thread-4 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.024426, Thread-5 : début du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.050473, Thread-1 : {"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.050473, Thread-1 : fin du calcul de l'impôt des 1 contribuables
2020-07-29 14:35:50.050473, Thread-3 : {"réponse": {"results": [{"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-3 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, Thread-5 : {"réponse": {"results": [{"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-5 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, Thread-2 : {"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-2 : fin du calcul de l'impôt des 4 contribuables
2020-07-29 14:35:50.051214, Thread-4 : {"réponse": {"results": [{"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}]}}
2020-07-29 14:35:50.051214, Thread-4 : fin du calcul de l'impôt des 2 contribuables
2020-07-29 14:35:50.051214, MainThread : fin du calcul de l'impôt des contribuables
The execution time was [051214-016079] nanoseconds (line 17 – line 1), i.e., 35 milliseconds and 135 nanoseconds.
We can see that between the first request sent to the server and the last response received by the client, the duration is the same: [051214-016079] (line 15 – line 1), 35 milliseconds and 135 nanoseconds.
Execution 2 [main2]
The execution of [main2] yields the following logs:
2020-07-29 14:41:03.303520, MainThread : début du calcul de l'impôt des contribuables
2020-07-29 14:41:03.345084, MainThread : {"réponse": {"result": {"limites": [9964.0, 27519.0, 73779.0, 156244.0, 13500.0], "coeffr": [0.0, 0.14, 0.3, 0.41, 0.45], "coeffn": [0.0, 1394.96, 5798.0, 13913.7, 20163.4], "plafond_decote_couple": 1970.0, "valeur_reduc_demi_part": 3797.0, "plafond_revenus_celibataire_pour_reduction": 21037.0, "plafond_qf_demi_part": 1551.0, "abattement_dixpourcent_max": 12502.0, "plafond_impot_celibataire_pour_decote": 1595.0, "plafond_decote_celibataire": 1196.0, "plafond_revenus_couple_pour_reduction": 42074.0, "id": 1, "abattement_dixpourcent_min": 437.0, "plafond_impot_couple_pour_decote": 2627.0}}}
2020-07-29 14:41:03.349975, MainThread : fin du calcul de l'impôt des contribuables
The execution time was [349975-303520] nanoseconds (line 3 - line 1), i.e., 46 milliseconds and 455 nanoseconds. Quite unexpectedly, [main] is faster than [main2].
We see that the single request for [main2] took [345084-303520] (line 2 – line 1), i.e., 41 milliseconds and 564 nanoseconds. The tax calculation then took [349975-345084] (line 3 – line 2), i.e., 4 milliseconds and 91 nanoseconds. It is the HTTP request that accounts for the execution time. Surprisingly, we see here that the single request for [main2] took longer than the four simultaneous requests for [main] and [35 millisecondes].
On the server side, the logs are as follows:
2020-07-29 14:35:27.047721, MainThread : [serveur] démarrage du serveur
2020-07-29 14:35:27.140927, MainThread : [serveur] connexion à la base de données réussie
2020-07-29 14:35:28.790716, MainThread : [serveur] démarrage du serveur
2020-07-29 14:35:28.847518, MainThread : [serveur] connexion à la base de données réussie
2020-07-29 14:35:50.039178, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.039178, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.043220, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.044307, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.045796, Thread-2 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 3, 'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'taux': 0.3, 'décôte': 0, 'réduction': 0}, {'marié': 'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.045796, Thread-3 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 2, 'salaire': 55555, 'impôt': 2814, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.046825, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/calculate-tax' [POST]>
2020-07-29 14:35:50.046825, Thread-6 : [index] {'réponse': {'results': [{'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'taux': 0.14, 'décôte': 384, 'réduction': 347}, {'marié': 'oui', 'enfants': 3, 'salaire': 50000, 'impôt': 0, 'surcôte': 0, 'taux': 0.14, 'décôte': 720, 'réduction': 0}, {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41, 'décôte': 0, 'réduction': 0}, {'marié': 'non', 'enfants': 3, 'salaire': 100000, 'impôt': 16782, 'surcôte': 7176, 'taux': 0.41, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.046825, Thread-4 : [index] {'réponse': {'results': [{'marié': 'non', 'enfants': 0, 'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'taux': 0.45, 'décôte': 0, 'réduction': 0}, {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:35:50.046825, Thread-5 : [index] {'réponse': {'results': [{'marié': 'non', 'enfants': 0, 'salaire': 100000, 'impôt': 22986, 'surcôte': 0, 'taux': 0.41, 'décôte': 0, 'réduction': 0}, {'marié': 'oui', 'enfants': 2, 'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'taux': 0.0, 'décôte': 0, 'réduction': 0}]}}
2020-07-29 14:41:03.341582, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/get-admindata' [GET]>
2020-07-29 14:41:03.341582, Thread-7 : [index] {'réponse': {'result': {'limites': [9964.0, 27519.0, 73779.0, 156244.0, 13500.0], 'coeffr': [0.0, 0.14, 0.3, 0.41, 0.45], 'coeffn': [0.0, 1394.96, 5798.0, 13913.7, 20163.4], 'plafond_decote_couple': 1970.0, 'valeur_reduc_demi_part': 3797.0, 'plafond_revenus_celibataire_pour_reduction': 21037.0, 'plafond_qf_demi_part': 1551.0, 'abattement_dixpourcent_max': 12502.0, 'plafond_impot_celibataire_pour_decote': 1595.0, 'plafond_decote_celibataire': 1196.0, 'plafond_revenus_couple_pour_reduction': 42074.0, 'id': 1, 'abattement_dixpourcent_min': 437.0, 'plafond_impot_couple_pour_decote': 2627.0}}}
- line 5: the first request from client [main];
- line 14: the last response to client [main]. There are 6 milliseconds and 647 nanoseconds between the two;
- lines 15–16: the single request from client [main2]. The response is instantaneous;