31. Web clients for the JSON and XML services of version 12
We will write three console client applications for the JSON and XML services of the web server we just wrote. We will reuse the client/server architecture from version 11:

We will write three console scripts:
- the [main] and [main3] scripts will use the server’s [business] layer;
- the [main2] script will use the client’s [business] layer;
31.1. The directory structure of the client scripts
The [http-clients/07] folder is initially created by copying the [http-clients/06] folder. It is then modified.

- in [1]: data used or created by the client;
- in [2], the client’s configuration and console scripts;
- in [3], the client’s [dao] layer;
- in [4], the test folder for the client’s [dao] layer;
31.2. The clients' [dao] layer


31.2.1. Interface
The [dao] layer will implement the following [InterfaceImpôtsDaoWithHttpSession] interface:
from abc import abstractmethod
from AbstractTaxDao import AbstractTaxDao
from AdminData import AdminData
from TaxPayer import TaxPayer
class TaxDaoInterfaceWithHttpSession(AbstractTaxDao):
# Calculate tax per unit
@abstractmethod
def calculate_tax(self, taxpayer: TaxPayer):
pass
# Calculate tax in batches
@abstractmethod
def calculate_tax_in_bulk_mode(self, taxpayers: list):
pass
# Initialize a session
@abstractmethod
def init_session(self, session_type: str):
pass
# end of session
@abstractmethod
def end_session(self):
pass
# authentication
@abstractmethod
def authenticate_user(self, user: str, password: str):
pass
# list of simulations
@abstractmethod
def get_simulations(self) -> list:
pass
# delete a simulation
@abstractmethod
def delete_simulation(self, id: int) -> list:
pass
# retrieve the data needed to calculate the tax
@abstractmethod
def get_admindata(self) -> AdminData:
pass
Each method of the interface corresponds to a service URL on the tax calculation server.
- line 7: the interface extends the [AbstractDao] class, which manages access to the file system;
The mapping between methods and service URLs is defined in the [config] configuration file:
# the tax calculation server
"server": {
"urlServer": "http://127.0.0.1:5000",
"user": {
"login": "admin",
"password": "admin"
},
"url_services": {
"calculate-tax": "/calculate-tax",
"get-admindata": "/get-admindata",
"calculate-tax-in-bulk-mode": "/calculate-taxes",
"init-session": "/init-session",
"end-session": "/end-session",
"authenticate-user": "/authenticate-user",
"get-simulations": "/list-simulations",
"delete-simulation": "/delete-simulation"
}
31.2.2. Implementation
The [InterfaceImpôtsDaoWithHttpSession] interface is implemented by the following [ImpôtsDaoWithHttpSession] class:
# imports
import json
import requests
import xmltodict
from flask_api import status
from AbstractTaxesDao import AbstractTaxesDao
from AdminData import AdminData
from TaxError import TaxError
from TaxDaoInterfaceWithHttpSession import TaxDaoInterfaceWithHttpSession
from TaxPayer import TaxPayer
class TaxDaoWithHttpSession(InterfaceTaxDaoWithHttpSession):
# constructor
def __init__(self, config: dict):
# parent initialization
AbstractTaxDao.__init__(self, config)
# storing configuration elements
# General configuration
self.__config = config
# server
self.__config_server = config["server"]
# services
self.__config_services = config["server"]['url_services']
# debug mode
self.__debug = config["debug"]
# logger
self.__logger = None
# cookies
self.__cookies = None
# session type (json, xml)
self.__session_type = None
# request/response stage
def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
# [method]: HTTP GET or POST method
# [url_service]: service URL
# [data]: POST parameters in x-www-form-urlencoded format
# [json]: POST parameters in JSON
# [cookies]: cookies to include in the request
# We must have an XML or JSON session; otherwise, we won't be able to handle the response
if self.__session_type not in ['json', 'xml']:
raise ImpôtsError(73, "There is no valid session currently active")
# connection
if method == "GET":
# GET
response = requests.get(url_service, cookies=self.__cookies)
else:
# POST
response = requests.post(url_service, data=data_value, json=json_value, cookies=self.__cookies)
# debug mode?
if self.__debug:
# logger
if not self.__logger:
self.__logger = self.__config['logger']
# logging
self.__logger.write(f"{response.text}\n")
# result
if self.__session_type == "json":
result = json.loads(response.text)
else: # xml
result = xmltodict.parse(response.text[39:])['root']
# retrieve cookies from the response if any are present
if response.cookies:
self.__cookies = response.cookies
# status code
status_code = response.status_code
# if status code is not 200 OK
if status_code != status.HTTP_200_OK:
raise ImpôtsError(35, result['response'])
# return the result
return result['response']
def init_session(self, session_type: str):
# Set the session type
self.__session_type = session_type
# service URL
config_server = self.__config_server
url_service = f"{config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"
# Execute request
self.get_response("GET", service_url)
…
- lines 16–34: the class constructor;
- line 19: the parent class is initialized;
- lines 21–28: certain configuration data is stored;
- lines 29–34: three properties used in the class’s methods are created;
- lines 36–82: the [get_response] method factors out what is common to all methods in the [dao] layer: sending an HTTP request and retrieving the HTTP response from the server;
- lines 38–42: definition of the 5 parameters of the [get_response] method;
- line 42: note that because the server maintains a session, the client needs to read/send cookies;
- lines 44–46: we verify that there is indeed a valid active session;
- line 51: GET case. The received cookies are sent back;
- line 54: POST case. This can have two types of parameters:
- the [x-www-form-urlencoded] type. This is the case for the URLs [/calculate-tax] and [/authenticate-user]. We then use the [data_value] parameter received by the method;
- the [json] type. This is the case for the URL [/calculate-taxes]. We then use the [json_value] parameter received by the method;
Here too, the session cookie is returned.
- lines 56–62: if in [debug] mode, the server’s response is logged. This log is important because it allows us to know exactly what the server returned;
- lines 64–68: depending on whether we are in JSON or XML mode, the server’s text response is converted into a dictionary. Let’s take the example of the URL [/init-session]:
The JSON response is as follows:
2020-08-03 11:45:21.218116, MainThread: {"action": "init-session", "status": 700, "response": ["session started with JSON response type"]}
The XML response is as follows:
2020-08-03 11:45:54.671871, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><status>700</status><response>session started with XML response type</response></root>
The code in lines 64–68 ensures that, in both cases, [result] contains a dictionary with the keys [action, status, response];
- lines 70–72: if the response contains cookies, they are retrieved. These must be sent back with the next request;
- lines 74–79: if the HTTP status of the response is not 200, an exception is raised with the error message contained in result[‘response’]. This can be a single error or a list of errors;
- lines 81–82: return the server’s response to the calling code;
[init_session]
- line 84: the [init_session] method is used to set the type of session (JSON or XML) that the client wants to start with the server;
- line 86: The desired session type is stored within the class. In fact, all methods require this information to correctly decode the server’s response;
- lines 88-90: using the application configuration, the service URL to be queried is determined;
- line 93: the service URL is queried. The result of the [get_response] method is not retrieved:
- if it throws an exception, then the operation has failed. The exception is not handled here and will be propagated directly to the calling code, which will then terminate the client with an error message;
- if it does not throw an exception, then the session initialization was successful;
[authenticate_user]
def authenticate_user(self, user: str, password: str):
# service URL
config_server = self.__config_server
service_url = f"{config_server['urlServer']}{self.__config_services['authenticate-user']}"
post_params = {
"user": user,
"password": password
}
# execute request
self.get_response("POST", url_service, post_params)
- The [authenticate_user] method is used to authenticate with the server. To do this, it receives the login credentials [user, password] on line 1;
- lines 2–4: we determine the service URL to query;
- lines 5–8: the POST parameters, since the URL [/authenticate-user] expects a POST request with the parameters [user, password];
- line 11: the request is executed. Again, we do not retrieve the server’s response. It is the exception thrown by [get_response] that indicates whether the operation was successful or not;
[calculate_tax]
def calculate_tax(self, taxpayer: TaxPayer):
# service URL
config_server = self.__config_server
url_service = f"{config_server['urlServer']}{self.__config_services['calculate-tax']}"
# POST parameters
post_params = {
"married": taxpayer.married,
"children": taxpayer.children,
"salary": taxpayer.salary
}
# execute query
response = self.get_response("POST", url_service, post_params)
# Update the TaxPayer with the response
taxpayer.fromdict(response)
- The [calculate_tax] method calculates the tax for a taxpayer [taxpayer] passed as a parameter. This parameter is modified by the method (line 15) and therefore constitutes the method’s result;
- lines 2–4: we define the service URL to be queried;
- lines 6–10: the parameters for the POST request to be sent. The service URL [/calculate-tax] expects a POST request with the parameters [married, children, salary];
- lines 12–13: the request is executed and the server’s response is retrieved. The service URL [/calculate-tax] returns a dictionary with the tax keys [tax, discount, surcharge, reduction, rate];
- line 15: the obtained dictionary [response] is used to update the taxpayer [taxpayer];
[calculate_tax_in_bulk_mode]
# Calculate tax in bulk mode
def calculate_tax_in_bulk_mode(self, taxpayers: list):
# let exceptions be raised
# convert the taxpayers into a list of dictionaries
# keep only the [married, children, salary] properties
list_dict_taxpayers = list(
map(lambda taxpayer:
taxpayer.asdict(included_keys=[
'_TaxPayer__married',
'_TaxPayer__children',
'_TaxPayer__salary']),
taxpayers))
# service URL
config_server = self.__config_server
service_url = f"{config_server['urlServer']}{self.__config_services['calculate-tax-in-bulk-mode']}"
# execute request
list_dict_taxpayers2 = self.get_response("POST", url_service, data_value=None, json_value=list_dict_taxpayers)
# when there is only one taxpayer and we are in an XML session, [list_dict_taxpayers2] is not a list
# in this case, we convert it to a list
if not isinstance(list_dict_taxpayers2, list):
list_dict_taxpayers2 = [list_dict_taxpayers2]
# we update the initial list of taxpayers with the received results
for i in range(len(taxpayers)):
# Update taxpayers[i]
taxpayers[i].fromdict(list_dict_taxpayers2[i])
# Here, the [taxpayers] parameter has been updated with the results from the server
- Line 2: The method receives a list of taxpayers of type TaxPayer;
- lines 7–13: This list of [TaxPayer] elements is converted into a list of dictionaries [spouse, children, salary];
- lines 15–17: the service URL is set;
- lines 19–20: a POST request is executed, with a JSON body consisting of the list of dictionaries created in line 7. The server’s response is retrieved;
- lines 23–24: tests revealed an issue when the session is of type XML:
- if the initial list of taxpayers has N elements (N>1), the result is a list of N dictionaries of type [OrderedDict];
- if the initial list has only one element, the result is not a list but a single element of type [OrderedDict];
- lines 23–24: if this is the case (1 element), we convert the result into a list of 1 element;
- lines 25–28: this list of received dictionaries contains the tax amount for each taxpayer in the initial list. We then update each of them with the received results;
[get_simulations]
def get_simulations(self) -> list:
# service URL
config_server = self.__config_server
url_service = f"{config_server['urlServer']}{self.__config_services['get-simulations']}"
# execute request
return self.get_response("GET", url_service)
- line 1: the method requests the list of simulations performed in the current session;
- line 2: the method returns the server's response;
[delete_simulation]
def delete_simulation(self, id: int) -> list:
# service URL
config_server = self.__config_server
service_url = f"{config_server['urlServer']}{self.__config_services['delete-simulation']}/{id}"
# execute request
return self.get_response("GET", url_service)
- line 1: the method deletes the simulation whose ID is passed;
- line 7: it returns the server's response, the list of simulations remaining after the requested deletion;
[get-admindata]
def get_admindata(self) -> AdminData:
# we let exceptions propagate
# service URL
config_server = self.__config_server
service_url = f"{config_server['urlServer']}{self.__config_services['get-admindata']}"
# execute request
result = self.get_response("GET", url_service)
# The result is a dictionary of string values if the session is XML
if self.__session_type == 'xml':
# new dictionary
result2 = {}
# convert everything to numbers
for key, value in result.items():
# some elements of the dictionary are lists
if isinstance(value, list):
values = []
for value2 in value:
values.append(float(value2))
result2[key] = values
else:
# other simple elements
result2[key] = float(value)
else:
result2 = result
# result of type AdminData
return AdminData().fromdict(result2)
- line 1: the method requests the tax constants from the server to calculate the tax;
- line 29: it returns a [AdminData] type;
- line 9: we retrieve the server’s response in the form of a dictionary. Tests show that there is an issue when the session is an XML session: instead of being numeric values, the values in the dictionary are strings. We had reported this issue during the study of the [xmltodict] module and found that this was normal behavior. [xmltodict] has no type information in the XML stream it is given. That said, in this specific case, all values in the received dictionary must be converted to numeric. This dictionary contains three lists [limites, coeffr, coeffn] and a series of numeric properties;
- lines 13–25: creation of a [result2] dictionary with numeric values from the [result] dictionary with string-type values;
- line 29: the dictionary [result2] is used to initialize a [AdminData] type;
31.2.3. The [dao] layer factory
Our clients will be multi-threaded. Since the [dao] layer is implemented by a class with read/write state (= read/write properties), each thread must have its own [dao] layer, or else access to shared data between threads must be synchronized. Here we choose the first solution. We use a [ImpôtsDaoWithHttpSessionFactory] class capable of creating instances of the [dao] layer:
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)
31.3. Client configuration

Clients are configured using the [config] and [config_layers] files. The [config] file is as follows:
def configure(config: dict) -> dict:
import os
# step 1 ------
# directory of this file
script_dir = os.path.dirname(os.path.abspath(__file__))
# root directory
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# absolute dependencies
absolute_dependencies = [
# project directories
# 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, tax brackets
f"{root_dir}/taxes/v05/entities",
# TaxDaoWithHttpSession, TaxDaoWithHttpSessionFactory, TaxDaoWithHttpSessionInterface
f"{script_dir}/../services",
# configuration scripts
script_dir,
# Logger
f"{root_dir}/impots/http-servers/02/utilities",
]
# Set the syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# step 2 ------
# Configure the application with constants
config.update({
# taxpayers file
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
# results file
"resultsFilename": f"{script_dir}/../data/output/results.json",
# error file
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
# log file
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# tax calculation server
"server": {
"urlServer": "http://127.0.0.1:5000",
"user": {
"login": "admin",
"password": "admin"
},
"url_services": {
"calculate-tax": "/calculate-tax",
"get-admindata": "/get-admindata",
"calculate-tax-in-bulk-mode": "/calculate-taxes",
"init-session": "/init-session",
"end-session": "/end-session",
"authenticate-user": "/authenticate-user",
"get-simulations": "/list-simulations",
"delete-simulation": "/delete-simulation"
}
},
# debug mode
"debug": True
}
)
# step 3 ------
# instantiating layers
import config_layers
config['layers'] = config_layers.configure(config)
# return the configuration
return config
The [config_layers] file is as follows:
def configure(config: dict) -> dict:
# instantiate the application layers
# [business] layer
from BusinessTaxes import BusinessTaxes
business = BusinessTaxes()
# DAO layer factory
from TaxDaoWithHttpSessionFactory import TaxDaoWithHttpSessionFactory
dao_factory = ImpôtsDaoWithHttpSessionFactory(config)
# Return the layer configuration
return {
"dao_factory": dao_factory,
"business": business
}
- Clients will not have direct access to the [dao] layer. To gain access, they must go through the [dao] layer's factory;
31.4. The [main] client
The [main] client allows you to test the URLs [/init-session, /authenticate-user, /calculate-taxes, /end-session]:
# Expects a JSON or XML parameter
import sys
syntax = f"{sys.argv[0]} json / xml"
error = len(sys.argv) != 2
if not error:
session_type = sys.argv[1].lower()
error = session_type != "json" and session_type != "xml"
if error:
print(f"syntax: {syntax}")
sys.exit()
# configure the application
import config
config = config.configure({"session_type": session_type})
# 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(config: dict, taxpayers: list):
# retrieve the [dao] layer factory
dao_factory = config['layers']['dao_factory']
# create an instance of the [dao] layer
dao = dao_factory.new_instance()
# session type
session_type = config['session_type']
# number of taxpayers
nb_taxpayers = len(taxpayers)
# log
logger.write(f"Starting to calculate the tax for {nb_taxpayers} taxpayers\n")
# Initialize the session
dao.init_session(session_type)
# authenticate
dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
# Calculate the tax for the taxpayers
dao.calculate_tax_in_bulk_mode(taxpayers)
# end of session
dao.end_session()
# log
logger.write(f"Tax calculation for {nb_taxpayers} taxpayers completed\n")
# list of client threads
threads = []
logger = None
# code
try:
# logger
logger = Logger(config["logsFilename"])
# store it in the config
config["logger"] = logger
# start log
logger.write("Start of taxpayer tax calculation\n")
# retrieve the [dao] layer factory
dao_factory = config["layers"]["dao_factory"]
# create an instance of the [dao] layer
dao = dao_factory.new_instance()
# read taxpayer data
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# Are there any taxpayers?
if not taxpayers:
raise ImpôtsError(36, f"No valid taxpayers in the file {config['taxpayersFilename']}")
# Calculate taxpayers' taxes using multiple threads
i = 0
l_taxpayers = len(taxpayers)
while i < len(taxpayers):
# each thread will process 1 to 4 taxpayers
nb_taxpayers = min(l_taxpayers - i, random.randint(1, 4))
# The list of taxpayers processed by the thread
thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
# Increment i for the next thread
i += nb_taxpayers
# create the thread
thread = threading.Thread(target=thread_function, args=(config, 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
for thread in threads:
thread.join()
# Here, all threads have finished their work—each has modified one or more [taxpayer] objects
# we save the results to the JSON file
dao.write_taxpayers_results(taxpayers)
# end
except BaseException as error:
# display the error
print(f"The following error occurred: {error}")
finally:
# close the logger
if logger:
# End log
logger.write("End of taxpayer tax calculation\n")
# Close the logger
logger.close()
# we're done
print("Work completed...")
# Terminate any threads that might still be running if the program was terminated due to an error
sys.exit()
- lines 4-11: the client expects a parameter specifying the session type, JSON or XML, to use with the server;
- lines 13-15: the client is configured;
- lines 48–104: this code is familiar. It has been used many times. It distributes the taxpayers for whom we want to calculate the tax across multiple threads;
- line 26: the [thread_function] method is the method executed by each thread to calculate the tax for the taxpayers assigned to it;
- lines 27–30: each thread has its own [dao] layer;
- The tax calculation is performed in four steps:
- lines 37–38: initialization of a session (JSON or XML) with the server;
- lines 39–40: authentication with the server;
- lines 41–42: tax calculation;
- lines 43–44: closing the session with the server;
When this code is executed in [json] mode, the following logs are generated:
2020-08-03 14:28:34.320751, MainThread: start of tax calculation for taxpayers
2020-08-03 14:28:34.328749, Thread-1: start of tax calculation for the 4 taxpayers
2020-08-03 14:28:34.328749, Thread-2: Start of tax calculation for the 4 taxpayers
2020-08-03 14:28:34.333592, Thread-3: Start of tax calculation for the 3 taxpayers
2020-08-03 14:28:34.368651, Thread-2: {"action": "init-session", "status": 700, "response": ["session started with json response type"]}
2020-08-03 14:28:34.375699, Thread-1: {"action": "init-session", "status": 700, "response": ["session started with JSON response type"]}
2020-08-03 14:28:34.377432, Thread-3 : {"action": "init-session", "status": 700, "response": ["session started with json response type"]}
2020-08-03 14:28:34.385653, Thread-2: {"action": "authenticate-user", "status": 200, "response": "Authentication successful"}
2020-08-03 14:28:34.392656, Thread-1: {"action": "authenticate-user", "status": 200, "response": "Authentication successful"}
2020-08-03 14:28:34.396377, Thread-3: {"action": "authenticate-user", "status": 200, "response": "Authentication successful"}
2020-08-03 14:28:34.406528, Thread-2: {"action": "calculate-taxes", "status": 1500, "response": [{"married": "no", "children": 3, "salary": 100000, "tax": 16782, "surcharge": 7176, "rate": 0.41, "discount": 0, "reduction": 0, "id": 1}, {"married": "yes", "children": 3, "salary": 100000, "tax": 9,200, "surcharge": 2,180, "rate": 0.3, "discount": 0, "reduction": 0, "id": 2}, {"married": "yes", "children": 5, "salary": 100000, "tax": 4230, "surcharge": 0, "rate": 0.14, "discount": 0, "reduction": 0, "id": 3}, {"married": "no", "children": 0, "salary": 100000, "tax": 22986, "surcharge": 0, "rate": 0.41, "discount": 0, "reduction": 0, "id": 4}]}
2020-08-03 14:28:34.413837, Thread-1: {"action": "calculate-taxes", "status": 1500, "response": [{"married": "yes", "children": 2, "salary": 55555, "tax": 2814, "surcharge": 0, "rate": 0.14, "discount": 0, "reduction": 0, "id": 1}, {"married": "yes", "children": 2, "salary": 50000, "tax": 1384, "surcharge": 0, "rate": 0.14, "discount": 384, "reduction": 347, "id": 2}, {"married": "yes", "children": 3, "salary": 50000, "tax": 0, "surcharge": 0, "rate": 0.14, "discount": 720, "reduction": 0, "id": 3}, {"married": "no", "children": 2, "salary": 100000, "tax": 19884, "surcharge": 4480, "rate": 0.41, "discount": 0, "reduction": 0, "id": 4}]}
2020-08-03 14:28:34.416695, Thread-3 : {"action": "calculate-taxes", "status": 1500, "response": [{"married": "yes", "children": 2, "salary": 30000, "tax": 0, "surcharge": 0, "rate": 0.0, "discount": 0, "reduction": 0, "id": 1}, {"married": "no", "children": 0, "salary": 200000, "tax": 64210, "surcharge": 7498, "rate": 0.45, "discount": 0, "reduction": 0, "id": 2}, {"married": "yes", "children": 3, "salary": 200000, "tax": 42842, "surcharge": 17283, "rate": 0.41, "discount": 0, "reduction": 0, "id": 3}]}
2020-08-03 14:28:34.425747, Thread-2 : {"action": "end-session", "status": 400, "response": "session reset"}
2020-08-03 14:28:34.425747, Thread-2: Tax calculation for the 4 taxpayers complete
2020-08-03 14:28:34.428956, Thread-1: {"action": "end-session", "status": 400, "response": "session reset"}
2020-08-03 14:28:34.428956, Thread-1: End of tax calculation for the 4 taxpayers
2020-08-03 14:28:34.428956, Thread-3: {"action": "end-session", "status": 400, "response": "session reset"}
2020-08-03 14:28:34.428956, Thread-3: End of tax calculation for 3 taxpayers
2020-08-03 14:28:34.428956, MainThread: Tax calculation for taxpayers complete
The above shows the execution path of thread [Thread-2].
If we run [main] in XML mode, the logs are as follows:
2020-08-03 14:32:48.495316, MainThread: start of tax calculation for taxpayers
2020-08-03 14:32:48.496452, Thread-1: Start of tax calculation for the 2 taxpayers
2020-08-03 14:32:48.498992, Thread-2: Start of tax calculation for the 2 taxpayers
2020-08-03 14:32:48.498992, Thread-3: Start of tax calculation for 4 taxpayers
2020-08-03 14:32:48.498992, Thread-4: Start of tax calculation for the 3 taxpayers
2020-08-03 14:32:48.538637, Thread-1: <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><status>700</status><response>session started with xml response type</response></root>
2020-08-03 14:32:48.540783, Thread-4: <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><status>700</status><response>session started with response type xml</response></root>
2020-08-03 14:32:48.547811, Thread-3: <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><status>700</status><response>session started with response type xml</response></root>
2020-08-03 14:32:48.547811, Thread-2: <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><status>700</status><response>session started with response type xml</response></root>
2020-08-03 14:32:48.555184, Thread-1: <?xml version="1.0" encoding="utf-8"?>
<root><action>authenticate-user</action><status>200</status><response>Authentication successful</response></root>
2020-08-03 14:32:48.564793, Thread-2: <?xml version="1.0" encoding="utf-8"?>
<root><action>authenticate-user</action><status>200</status><response>Authentication successful</response></root>
2020-08-03 14:32:48.564793, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authenticate-user</action><status>200</status><response>Authentication successful</response></root>
2020-08-03 14:32:48.568333, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authenticate-user</action><status>200</status><response>Authentication successful</response></root>
2020-08-03 14:32:48.568333, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculate-taxes</action><status>1500</status><response><married>yes</married><children>2</children><salary>55555</salary><tax>2814</tax><surcharge>0</surcharge><rate>0.14</rate><discount>0</discount><reduction>0</reduction><id>1</id></response><response><married>yes</married><children>2</children><salary>50000</salary><tax>1384</tax><surcharge>0</surcharge><rate>0.14</rate><discount>384</discount><reduction>347</reduction><id>2</id></response></root>
2020-08-03 14:32:48.579205, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculate-taxes</action><status>1500</status><response><married>yes</married><children>3</children><salary>50000</salary><tax>0</tax><surcharge>0</surcharge><rate>0.14</rate><discount>720</discount><reduction>0</reduction><id>1</id></response><response><married>no</married><children>2</children><salary>100000</salary><tax>19884</tax><surcharge>4480</surcharge><rate>0.41</rate><discount>0</discount><reduction>0</reduction><id>2</id></response></root>
2020-08-03 14:32:48.579205, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculate-taxes</action><status>1500</status><response><married>no</married><children>3</children><salary>100000</salary><tax>16782</tax><surcharge>7176</surcharge><rate>0.41</rate><discount>0</discount><reduction>0</reduction><id>1</id></response><response><married>yes</married><children>3</children><salary>100000</salary><tax>9200</tax><surcharge>2180</surcharge><rate>0.3</rate><discount>0</discount><reduction>0</reduction><id>2</id></response><response><married>yes</married><children>5</children><salary>100000</salary><tax>4230</tax><surcharge>0</surcharge><rate>0.14</rate><discount>0</discount><reduction>0</reduction><id>3</id></response><response><married>no</married><children>0</children><salary>100000</salary><tax>22986</tax><surcharge>0</surcharge><rate>0.41</rate><discount>0</discount><reduction>0</reduction><id>4</id></response></root>
2020-08-03 14:32:48.588051, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculate-taxes</action><status>1500</status><response><married>yes</married><children>2</children><salary>30000</salary><tax>0</tax><surcharge>0</surcharge><rate>0.0</rate><discount>0</discount><reduction>0</reduction><id>1</id></response><response><married>no</married><children>0</children><salary>200000</salary><tax>64210</tax><surcharge>7498</surcharge><rate>0.45</rate><discount>0</discount><reduction>0</reduction><id>2</id></response><response><married>yes</married><children>3</children><salary>200000</salary><tax>42842</tax><surcharge>17283</surcharge><rate>0.41</rate><discount>0</discount><reduction>0</reduction><id>3</id></response></root>
2020-08-03 14:32:48.594058, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>end-session</action><status>400</status><response>session reset</response></root>
2020-08-03 14:32:48.595198, Thread-1: Tax calculation for the 2 taxpayers complete
2020-08-03 14:32:48.595198, Thread-2: <?xml version="1.0" encoding="utf-8"?>
<root><action>end-session</action><status>400</status><response>session reset</response></root>
2020-08-03 14:32:48.595198, Thread-2: End of tax calculation for the two taxpayers
2020-08-03 14:32:48.595198, Thread-3: <?xml version="1.0" encoding="utf-8"?>
<root><action>end-session</action><status>400</status><response>session reset</response></root>
2020-08-03 14:32:48.595198, Thread-3: End of tax calculation for the 4 taxpayers
2020-08-03 14:32:48.603351, Thread-4: <?xml version="1.0" encoding="utf-8"?>
<root><action>end-session</action><status>400</status><response>session reset</response></root>
2020-08-03 14:32:48.603351, Thread-4: End of tax calculation for the 3 taxpayers
2020-08-03 14:32:48.603351, MainThread: Tax calculation for taxpayers complete
Above is the thread trace for [Thread-2].
31.5. The client [main2]

The client [main2] allows you to test the URLs [/init-session, /authenticate-user, /get-admindata, /end-session]:
# Expects a JSON or XML parameter
import sys
syntax = f"{sys.argv[0]} json / xml"
error = len(sys.argv) != 2
if not error:
session_type = sys.argv[1].lower()
error = session_type != "json" and session_type != "xml"
if error:
print(f"syntax: {syntax}")
sys.exit()
# configure the application
import config
config = config.configure({"session_type": session_type})
# dependencies
from ImpôtsError import ImpôtsError
from Logger import Logger
logger = None
# code
try:
# logger
logger = Logger(config["logsFilename"])
# store it in the config
config["logger"] = logger
# start log
logger.write("Start of taxpayer tax calculation\n")
# retrieve the factory from the [dao] layer
dao_factory = config['layers']['dao_factory']
# create an instance of the [dao] layer
dao = dao_factory.new_instance()
# retrieve the taxpayers
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# Are there any taxpayers?
if not taxpayers:
raise ImpôtsError(36, f"No valid taxpayers in the file {config['taxpayersFilename']}")
# session type
session_type = config['session_type']
# initialize the session
dao.init_session(session_type)
# authenticate
dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
# retrieve data from the tax administration
admindata = dao.get_admindata()
# end session
dao.end_session()
# calculate taxpayers' taxes using the [business] layer
business = config['layers']['business']
for taxpayer in taxpayers:
business.calculate_tax(taxpayer, admindata)
# Save the results to the JSON file
dao.write_taxpayers_results(taxpayers)
except BaseException as error:
# display the error
print(f"The following error occurred: {error}")
finally:
# close the logger
if logger:
# End log
logger.write("End of taxpayer tax calculation\n")
# Close the logger
logger.close()
# we're done
print("Work completed...")
- lines 1-11: retrieve the [json, xml] parameter that sets the type of session to establish with the server;
- lines 13-15: we configure the client;
- lines 30-33: we create a [dao] layer;
- lines 34-35: using it, we retrieve the list of taxpayers for whom the tax must be calculated;
- we then go through the four steps of the dialogue with the server;
- lines 41–42: a session is started with the server;
- lines 43–44: we authenticate with the server;
- lines 45-46: we request the tax constants from the server to calculate the tax;
- lines 47–48: the session with the server is closed;
- lines 49–52: using these constants, we are able to calculate the taxpayers’ tax using the local [business] layer on the client;
- lines 53–54: the results are saved;
For an XML session, the results are as follows:
2020-08-03 14:44:43.194294, MainThread: start of taxpayer tax calculation
2020-08-03 14:44:43.231633, MainThread: <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><status>700</status><response>session started with xml response type</response></root>
2020-08-03 14:44:43.240872, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>authenticate-user</action><status>200</status><response>Authentication successful</response></root>
2020-08-03 14:44:43.250061, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>get-admindata</action><status>1000</status><response><limits>9964.0</limits><limits>27519.0</limits><limits>73779.0</limits><limits>156244.0</limits><limits>93749.0</limits><coeffr>0.0</coeffr><coeffr>0.14</coeffr><coeffr>0.3</coeffr><coeffr>0.41</coeffr><coeffr>0.45</coeffr><coeffn>0.0</coeffn><coeffn>1394.96</coeffn><coeffn>5798.0</coeffn><coeffn>13913.7</coeffn><coeffn>20163.4</coeffn><minimum_10_percent_deduction>437.0</minimum_10_percent_deduction><couple_tax_ceiling_for_discount>2627.0</couple_tax_ceiling_for_discount><couple_discount_ceiling>1970.0</couple_discount_ceiling><half-share_reduction_value>3797.0</half-share_reduction_value><single_income_ceiling_for_reduction>21037.0</single_income_ceiling_for_reduction><id>1</id><maximum_10_percent_deduction>12502.0</maximum_10_percent_deduction><single_tax_ceiling_for_discount>1595.0</single_tax_ceiling_for_discount><single_discount_ceiling>1196.0</single_tax_credit_ceiling><couple_income_ceiling_for_reduction>42074.0</couple_income_ceiling_for_reduction><half-share_tax_credit_ceiling>1551.0</half-share_tax_credit_ceiling></response></root>
2020-08-03 14:44:43.269850, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>end-session</action><status>400</status><response>session reset</response></root>
2020-08-03 14:44:43.269850, MainThread: End of taxpayer tax calculation
31.6. The client [main3]
The client [main3] allows you to test the URLs [/init-session, /calculate-taxes, /get-simulations, /delete-simulation, /end-session]:

# Expects a JSON or XML parameter
import sys
syntax = f"{sys.argv[0]} json / xml"
error = len(sys.argv) != 2
if not error:
session_type = sys.argv[1].lower()
error = session_type != "json" and session_type != "xml"
if error:
print(f"syntax: {syntax}")
sys.exit()
# configure the application
import config
config = config.configure({"session_type": session_type})
# dependencies
from ImpôtsError import ImpôtsError
import sys
from Logger import Logger
logger = None
# code
try:
# logger
logger = Logger(config["logsFilename"])
# store it in the config
config["logger"] = logger
# start log
logger.write("Start of taxpayer tax calculation\n")
# retrieve the [dao] layer factory
dao_factory = config["layers"]["dao_factory"]
# create an instance of the [dao] layer
dao = dao_factory.new_instance()
# read taxpayer data
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# Are there any taxpayers?
if not taxpayers:
raise ImpôtsError(36, f"No valid taxpayers in the file {config['taxpayersFilename']}")
# Calculate taxpayers' taxes
# number of taxpayers
nb_taxpayers = len(taxpayers)
# log
logger.write(f"Starting to calculate taxes for {nb_taxpayers} taxpayers\n")
# Initialize the session
dao.init_session(session_type)
# authenticate
dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
# Calculate the tax for the taxpayers
dao.calculate_tax_in_bulk_mode(taxpayers)
# request the list of simulations
simulations = dao.get_simulations()
# delete every other one
for i in range(len(simulations)):
if i % 2 == 0:
# delete the simulation
dao.delete_simulation(simulations[i]['id'])
# end of session
dao.end_session()
# check the logs to see the different results (debug mode=True)
except BaseException as error:
# display the error
print(f"The following error occurred: {error}")
finally:
# Close the logger
if logger:
# End log
logger.write("End of taxpayer tax calculation\n")
# Close the logger
logger.close()
# we're done
print("Work completed...")
- lines 1-11: retrieve the session type from the script parameters;
- lines 13-15: we configure the application;
- lines 25-50: code that has already been explained at one point or another;
- lines 51-52: we request the list of simulations performed in the current session;
- lines 53-57: delete every other simulation;
- lines 58–59: the session is terminated;
During a jSON session, the logs are as follows:
2020-08-03 15:01:52.702297, MainThread: Start of tax calculation for taxpayers
2020-08-03 15:01:52.702297, MainThread: Start of tax calculation for the 11 taxpayers
2020-08-03 15:01:52.734806, MainThread: {"action": "init-session", "status": 700, "response": ["session started with JSON response type"]}
2020-08-03 15:01:52.747961, MainThread: {"action": "authenticate-user", "status": 200, "response": "Authentication successful"}
2020-08-03 15:01:52.765721, MainThread : {"action": "calculate-taxes", "status": 1500, "response": [{"married": "yes", "children": 2, "salary": 55555, "tax": 2814, "surcharge": 0, "rate": 0.14, "discount": 0, "reduction": 0, "id": 1}, {"married": "yes", "children": 2, "salary": 50000, "tax": 1384, "surcharge": 0, "rate": 0.14, "discount": 384, "reduction": 347, "id": 2}, {"married": "yes", "children": 3, "salary": 50000, "tax": 0, "surcharge": 0, "rate": 0.14, "discount": 720, "reduction": 0, "id": 3}, {"married": "no", "children": 2, "salary": 100000, "tax": 19884, "surcharge": 4480, "rate": 0.41, "discount": 0, "reduction": 0, "id": 4}, {"married": "no", "children": 3, "salary": 100000, "tax": 16782, "surcharge": 7176, "rate": 0.41, "discount": 0, "reduction": 0, "id": 5}, {"married": "yes", "children": 3, "salary": 100000, "tax": 9200, "surcharge": 2180, "rate": 0.3, "discount": 0, "reduction": 0, "id": 6}, {"married": "yes", "children": 5, "salary": 100000, "tax": 4230, "surcharge": 0, "rate": 0.14, "discount": 0, "reduction": 0, "id": 7}, {"married": "no", "children": 0, "salary": 100000, "tax": 22986, "surcharge": 0, "rate": 0.41, "discount": 0, "reduction": 0, "id": 8}, {"married": "yes", "children": 2, "salary": 30000, "tax": 0, "surcharge": 0, "rate": 0.0, "discount": 0, "reduction": 0, "id": 9}, {"married": "no", "children": 0, "salary": 200000, "tax": 64210, "surcharge": 7498, "rate": 0.45, "discount": 0, "reduction": 0, "id": 10}, {"married": "yes", "children": 3, "salary": 200000, "tax": 42,842, "surcharge": 17,283, "rate": 0.41, "discount": 0, "reduction": 0, "id": 11}]}
2020-08-03 15:01:52.785505, MainThread : {"action": "list-simulations", "status": 500, "response": [{"discount": 0, "children": 2, "id": 1, "tax": 2814, "married": "yes", "reduction": 0, "salary": 55555, "surcharge": 0, "rate": 0.14}, {"discount": 384, "children": 2, "id": 2, "tax": 1384, "married": "yes", "deduction": 347, "salary": 50000, "surcharge": 0, "rate": 0.14}, {"discount": 720, "children": 3, "id": 3, "tax": 0, "married": "yes", "reduction": 0, "salary": 50000, "surcharge": 0, "rate": 0.14}, {"discount": 0, "children": 2, "id": 4, "tax": 19884, "married": "no", "deduction": 0, "salary": 100000, "surcharge": 4480, "rate": 0.41}, {"discount": 0, "children": 3, "id": 5, "tax": 16782, "married": "no", "reduction": 0, "salary": 100000, "surcharge": 7176, "rate": 0.41}, {"discount": 0, "children": 3, "id": 6, "tax": 9200, "married": "yes", "deduction": 0, "salary": 100000, "surcharge": 2180, "rate": 0.3}, {"discount": 0, "children": 5, "id": 7, "tax": 4230, "married": "yes", "reduction": 0, "salary": 100000, "surcharge": 0, "rate": 0.14}, {"discount": 0, "children": 0, "id": 8, "tax": 22986, "married": "no", "deduction": 0, "salary": 100000, "surcharge": 0, "rate": 0.41}, {"discount": 0, "children": 2, "id": 9, "tax": 0, "married": "yes", "reduction": 0, "salary": 30000, "surcharge": 0, "rate": 0.0}, {"discount": 0, "children": 0, "id": 10, "tax": 64,210, "married": "no", "reduction": 0, "salary": 200,000, "surcharge": 7,498, "rate": 0.45}, {"discount": 0, "children": 3, "id": 11, "tax": 42842, "married": "yes", "deduction": 0, "salary": 200000, "surcharge": 17283, "rate": 0.41}]}
2020-08-03 15:01:52.801475, MainThread : {"action": "delete-simulation", "status": 600, "response": [{"discount": 384, "children": 2, "id": 2, "tax": 1384, "married": "yes", "reduction": 347, "salary": 50000, "surcharge": 0, "rate": 0.14}, {"discount": 720, "children": 3, "id": 3, "tax": 0, "married": "yes", "reduction": 0, "salary": 50000, "surcharge": 0, "rate": 0.14}, {"discount": 0, "children": 2, "id": 4, "tax": 19884, "married": "no", "deduction": 0, "salary": 100000, "surcharge": 4480, "rate": 0.41}, {"discount": 0, "children": 3, "id": 5, "tax": 16782, "married": "no", "reduction": 0, "salary": 100000, "surcharge": 7176, "rate": 0.41}, {"discount": 0, "children": 3, "id": 6, "tax": 9200, "married": "yes", "deduction": 0, "salary": 100000, "surcharge": 2180, "rate": 0.3}, {"discount": 0, "children": 5, "id": 7, "tax": 4230, "married": "yes", "reduction": 0, "salary": 100000, "surcharge": 0, "rate": 0.14}, {"discount": 0, "children": 0, "id": 8, "tax": 22986, "married": "no", "deduction": 0, "salary": 100000, "surcharge": 0, "rate": 0.41}, {"discount": 0, "children": 2, "id": 9, "tax": 0, "married": "yes", "reduction": 0, "salary": 30000, "surcharge": 0, "rate": 0.0}, {"discount": 0, "children": 0, "id": 10, "tax": 64,210, "married": "no", "reduction": 0, "salary": 200,000, "surcharge": 7,498, "rate": 0.45}, {"discount": 0, "children": 3, "id": 11, "tax": 42842, "married": "yes", "deduction": 0, "salary": 200000, "surcharge": 17283, "rate": 0.41}]}
2020-08-03 15:01:52.810129, MainThread: {"action": "delete-simulation", "status": 600, "response": [{"discount": 384, "children": 2, "id": 2, "tax": 1384, "married": "yes", "reduction": 347, "salary": 50000, "surcharge": 0, "rate": 0.14}, {"discount": 0, "children": 2, "id": 4, "tax": 19884, "married": "no", "deduction": 0, "salary": 100000, "surcharge": 4480, "rate": 0.41}, {"discount": 0, "children": 3, "id": 5, "tax": 16782, "married": "no", "reduction": 0, "salary": 100000, "surcharge": 7176, "rate": 0.41}, {"discount": 0, "children": 3, "id": 6, "tax": 9200, "married": "yes", "deduction": 0, "salary": 100000, "surcharge": 2180, "rate": 0.3}, {"discount": 0, "children": 5, "id": 7, "tax": 4230, "married": "yes", "reduction": 0, "salary": 100000, "surcharge": 0, "rate": 0.14}, {"discount": 0, "children": 0, "id": 8, "tax": 22986, "married": "no", "deduction": 0, "salary": 100000, "surcharge": 0, "rate": 0.41}, {"discount": 0, "children": 2, "id": 9, "tax": 0, "married": "yes", "reduction": 0, "salary": 30000, "surcharge": 0, "rate": 0.0}, {"discount": 0, "children": 0, "id": 10, "tax": 64,210, "married": "no", "reduction": 0, "salary": 200,000, "surcharge": 7,498, "rate": 0.45}, {"discount": 0, "children": 3, "id": 11, "tax": 42842, "married": "yes", "deduction": 0, "salary": 200000, "surcharge": 17283, "rate": 0.41}]}
2020-08-03 15:01:52.818803, MainThread: {"action": "delete-simulation", "status": 600, "response": [{"discount": 384, "children": 2, "id": 2, "tax": 1384, "married": "yes", "reduction": 347, "salary": 50000, "surcharge": 0, "rate": 0.14}, {"discount": 0, "children": 2, "id": 4, "tax": 19884, "married": "no", "deduction": 0, "salary": 100000, "surcharge": 4480, "rate": 0.41}, {"discount": 0, "children": 3, "id": 6, "tax": 9200, "married": "yes", "deduction": 0, "salary": 100000, "surcharge": 2180, "rate": 0.3}, {"discount": 0, "children": 5, "id": 7, "tax": 4230, "married": "yes", "reduction": 0, "salary": 100000, "surcharge": 0, "rate": 0.14}, {"discount": 0, "children": 0, "id": 8, "tax": 22986, "married": "no", "deduction": 0, "salary": 100000, "surcharge": 0, "rate": 0.41}, {"discount": 0, "children": 2, "id": 9, "tax": 0, "married": "yes", "reduction": 0, "salary": 30000, "surcharge": 0, "rate": 0.0}, {"discount": 0, "children": 0, "id": 10, "tax": 64,210, "married": "no", "reduction": 0, "salary": 200,000, "surcharge": 7,498, "rate": 0.45}, {"discount": 0, "children": 3, "id": 11, "tax": 42842, "married": "yes", "deduction": 0, "salary": 200000, "surcharge": 17283, "rate": 0.41}]}
2020-08-03 15:01:52.834604, MainThread: {"action": "delete-simulation", "status": 600, "response": [{"discount": 384, "children": 2, "id": 2, "tax": 1384, "married": "yes", "reduction": 347, "salary": 50000, "surcharge": 0, "rate": 0.14}, {"discount": 0, "children": 2, "id": 4, "tax": 19884, "married": "no", "deduction": 0, "salary": 100000, "surcharge": 4480, "rate": 0.41}, {"discount": 0, "children": 3, "id": 6, "tax": 9200, "married": "yes", "deduction": 0, "salary": 100000, "surcharge": 2180, "rate": 0.3}, {"discount": 0, "children": 0, "id": 8, "tax": 22986, "married": "no", "deduction": 0, "salary": 100000, "surcharge": 0, "rate": 0.41}, {"discount": 0, "children": 2, "id": 9, "tax": 0, "married": "yes", "reduction": 0, "salary": 30000, "surcharge": 0, "rate": 0.0}, {"discount": 0, "children": 0, "id": 10, "tax": 64,210, "married": "no", "reduction": 0, "salary": 200,000, "surcharge": 7,498, "rate": 0.45}, {"discount": 0, "children": 3, "id": 11, "tax": 42842, "married": "yes", "deduction": 0, "salary": 200000, "surcharge": 17283, "rate": 0.41}]}
2020-08-03 15:01:52.843803, MainThread: {"action": "delete-simulation", "status": 600, "response": [{"discount": 384, "children": 2, "id": 2, "tax": 1384, "married": "yes", "reduction": 347, "salary": 50000, "surcharge": 0, "rate": 0.14}, {"discount": 0, "children": 2, "id": 4, "tax": 19884, "married": "no", "deduction": 0, "salary": 100000, "surcharge": 4480, "rate": 0.41}, {"discount": 0, "children": 3, "id": 6, "tax": 9200, "married": "yes", "deduction": 0, "salary": 100000, "surcharge": 2180, "rate": 0.3}, {"discount": 0, "children": 0, "id": 8, "tax": 22986, "married": "no", "deduction": 0, "salary": 100000, "surcharge": 0, "rate": 0.41}, {"discount": 0, "children": 0, "id": 10, "tax": 64210, "married": "no", "reduction": 0, "salary": 200000, "surcharge": 7498, "rate": 0.45}, {"discount": 0, "children": 3, "id": 11, "tax": 42842, "married": "yes", "deduction": 0, "salary": 200000, "surcharge": 17283, "rate": 0.41}]}
2020-08-03 15:01:52.851855, MainThread: {"action": "delete-simulation", "status": 600, "response": [{"discount": 384, "children": 2, "id": 2, "tax": 1384, "married": "yes", "reduction": 347, "salary": 50000, "surcharge": 0, "rate": 0.14}, {"discount": 0, "children": 2, "id": 4, "tax": 19884, "married": "no", "deduction": 0, "salary": 100000, "surcharge": 4480, "rate": 0.41}, {"discount": 0, "children": 3, "id": 6, "tax": 9200, "married": "yes", "deduction": 0, "salary": 100000, "surcharge": 2180, "rate": 0.3}, {"discount": 0, "children": 0, "id": 8, "tax": 22986, "married": "no", "deduction": 0, "salary": 100000, "surcharge": 0, "rate": 0.41}, {"discount": 0, "children": 0, "id": 10, "tax": 64210, "married": "no", "reduction": 0, "salary": 200000, "surcharge": 7498, "rate": 0.45}]}
2020-08-03 15:01:52.863165, MainThread : {"action": "end-session", "status": 400, "response": "session reset"}
2020-08-03 15:01:52.863165, MainThread: End of taxpayer tax calculation
- line 6: we have 11 simulations;
- line 12: after the various deletions, there are only 5 left;
31.7. The [Test2HttpClientDaoWithSession] test class

The [Test2HttpClientDaoWithSession] class tests the [dao] layer of the clients as follows:
import unittest
from ImpôtsError import ImpôtsError
from Logger import Logger
from TaxPayer import TaxPayer
class Test2HttpClientDaoWithSession(unittest.TestCase):
def test_init_session_json(self) -> None:
print('test_init_session_json')
error = False
try:
dao.init_session('json')
except ImpôtsError as ex:
print(ex)
error = True
# there should be no error
self.assertFalse(error)
def test_init_session_xml(self) -> None:
print('test_init_session_xml')
error = False
try:
dao.init_session('xml')
except ImpôtsError as ex:
print(ex)
error = True
# There should be no error
self.assertFalse(error)
def test_init_session_xxx(self) -> None:
print('test_init_session_xxx')
error = False
try:
dao.init_session('xxx')
except ImpôtsError as ex:
print(ex)
error = True
# there must be an error
self.assertTrue(error)
def test_authenticate_user_success(self) -> None:
print('test_authenticate_user_success')
# initialize session
dao.init_session('json')
# test
error = False
try:
dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
except ImpôtsError as ex:
print(ex)
error = True
# there should be no error
self.assertFalse(error)
def test_authenticate_user_failed(self) -> None:
print('test_authenticate_user_failed')
# initialize session
dao.init_session('json')
# test
error = False
try:
dao.authenticate_user('x', 'y')
except ImpôtsError as ex:
print(ex)
error = True
# there must be an error
self.assertTrue(error)
def test_get_simulations(self) -> None:
print('test_get_simulations')
# initialize session
dao.init_session('json')
# authentication
dao.authenticate_user('admin', 'admin')
# calculate tax
# {'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)
# get_simulations
simulations = dao.get_simulations()
# checks
# there must be 1 simulation
self.assertEqual(1, len(simulations))
simulation = simulations[0]
# verification of the calculated tax
self.assertAlmostEqual(simulation['tax'], 2815, delta=1)
self.assertEqual(simulation['discount'], 0)
self.assertEqual(simulation['reduction'], 0)
self.assertAlmostEqual(simulation['rate'], 0.14, delta=0.01)
self.assertEqual(simulation['surcharge'], 0)
def test_delete_simulation(self) -> None:
print('test_delete_simulation')
# init session
dao.init_session('json')
# authentication
dao.authenticate_user('admin', 'admin')
# calculate tax
taxpayer = TaxPayer().fromdict({"married": "yes", "children": 2, "salary": 55555})
dao.calculate_tax(taxpayer)
# get_simulations
simulations = dao.get_simulations()
# delete_simulation
dao.delete_simulation(simulations[0]['id'])
# get_simulations
simulations = dao.get_simulations()
# verification - there should be no more simulations
self.assertEqual(0, len(simulations))
# we delete a simulation that doesn't exist
error = False
try:
dao.delete_simulation(100)
except ImpôtsError as ex:
print(ex)
error = True
# there must be an error
self.assertTrue(error)
if __name__ == '__main__':
# Configure the application
import config
config = config.configure({})
# Logger
logger = Logger(config["logsFilename"])
# Store it in the config
config["logger"] = logger
# [DAO] layer
dao_factory = config['layers']['dao_factory']
dao = dao_factory.new_instance()
# Run the test methods
print("Tests in progress...")
unittest.main()
- The [dao] layer sends a request to the server, receives its response, and formats it to return it to the calling code. When the server sends a response with a status code other than 200, the [dao] layer raises an exception. Therefore, a number of tests involve checking whether an exception occurred or not;
- lines 9–18: we initialize a JSON session. There should be no errors;
- lines 20–29: We initialize an XML session. There should be no error;
- lines 31–40: We initialize a session with an incorrect type. An error must occur;
- lines 42–54: We authenticate with the correct credentials. There should be no error;
- lines 56–68: authenticate using incorrect credentials. An error must occur;
- lines 70–92: we calculate the tax and then request the list of simulations. We should get one. Additionally, we verify that this simulation contains the requested tax;
- lines 94–119: a simulation is created and then deleted. Then an attempt is made to delete a simulation even though there are no simulations left. An error must occur;
- lines 121–137: the test is run as a standard console script;
- lines 122–124: We configure the application;
- lines 126–129: we configure the logger. This will allow us to track the logs;
- lines 131–133: we instantiate the [DAO] layer that will be tested;
- lines 135–137: run the tests;
The console output is 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/07/tests/Test2HttpClientDaoWithSession.py
tests in progress...
test_authenticate_user_failed
..MyException[35, ["Authentication failed"]]
test_authenticate_user_success
test_delete_simulation
MyException[35, ["Simulation #[100] does not exist"]]
test_get_simulations
test_init_session_json
test_init_session_xml
test_init_session_xxx
MyException[73, there is no valid session currently active]
----------------------------------------------------------------------
Ran 7 tests in 0.171s
OK
Process finished with exit code 0