Skip to content

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:

Image

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.

Image

  • 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

Image

Image

31.2.1. Interface

The [dao] layer will implement the following [InterfaceImpôtsDaoWithHttpSession] interface:

from abc import abstractmethod

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from TaxPayer import TaxPayer

class InterfaceImpôtsDaoWithHttpSession(AbstractImpôtsDao):

    #  tax calculation per unit
    @abstractmethod
    def calculate_tax(self, taxpayer: TaxPayer):
        pass

    #  batch tax calculation
    @abstractmethod
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        pass

    #  session initialization
    @abstractmethod
    def init_session(self, type_session: 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

    #  obtain data for tax calculations
    @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:


        # le serveur de calcul de l'impôt
        "server": {
            "urlServer""http://127.0.0.1:5000",
            "user": {
                "login""admin",
                "password""admin"
            },
            "url_services": {
                "calculate-tax""/calculer-impot",
                "get-admindata""/get-admindata",
                "calculate-tax-in-bulk-mode""/calculer-impots",
                "init-session""/init-session",
                "end-session""/fin-session",
                "authenticate-user""/authentifier-utilisateur",
                "get-simulations""/lister-simulations",
                "delete-simulation""/supprimer-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 AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDaoWithHttpSession import InterfaceImpôtsDaoWithHttpSession
from TaxPayer import TaxPayer

class ImpôtsDaoWithHttpSession(InterfaceImpôtsDaoWithHttpSession):

    #  manufacturer
    def __init__(self, config: dict):
        #  parent initialization
        AbstractImpôtsDao.__init__(self, config)
        #  saving configuration items
        #  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

        # étape request / response
     def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
        #  [method]: HTTP GET or POST method
        #  [url_service] : URL of service
        #  [data]: POST parameters in x-www-form-urlencoded
        #  [json]: POST parameters in json
        #  [cookies]: cookies to include in the request

        #  you must have a XML or JSON session, otherwise you won't be able to handle the response
        if self.__session_type not in ['json', 'xml']:
            raise ImpôtsError(73, "il n'y a pas de session valide en cours")

        #  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']
            #  log on
            self.__logger.write(f"{response.text}\n")

        #  result
        if self.__session_type == "json":
            résultat = json.loads(response.text)
        else:  #  xml
            résultat = xmltodict.parse(response.text[39:])['root']

        #  retrieve response cookies, if any
        if response.cookies:
            self.__cookies = response.cookies

        #  status code
        status_code = response.status_code

        #  if status code other than 200 OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(35, résultat['réponse'])

        #  we return the result
        return résultat['réponse']

    def init_session(self, session_type: str):
        #  note 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}"

        #  request execution
        self.get_response("GET", url_service)

  • 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", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}

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><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></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
        url_service = f"{config_server['urlServer']}{self.__config_services['authenticate-user']}"
        post_params = {
            "user": user,
            "password": password
        }

        #  request execution
        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 = {
            "marié": taxpayer.marié,
            "enfants": taxpayer.enfants,
            "salaire": taxpayer.salaire
        }

        #  request execution
        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]

    #  bulk tax calculation
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        #  we let the exceptions rise

        #  transform taxpayers into a list of dictionaries
        #  we keep only the properties [married, children, salary]
        list_dict_taxpayers = list(
            map(lambda taxpayer:
                taxpayer.asdict(included_keys=[
                    '_TaxPayer__marié',
                    '_TaxPayer__enfants',
                    '_TaxPayer__salaire']),
                taxpayers))

        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['calculate-tax-in-bulk-mode']}"

        #  request execution
        list_dict_taxpayers2 = self.get_response("POST", url_service, data_value=None, json_value=list_dict_taxpayers)
        #  when there's only one taxpayer and you're in an xml session, [list_dict_taxpayers2] isn't a list
        #  in this case we make a list
        if not isinstance(list_dict_taxpayers2, list):
            list_dict_taxpayers2 = [list_dict_taxpayers2]
        #  the initial taxpayer list is updated with the results received
        for i in range(len(taxpayers)):
            #  taxpayers[i] update
            taxpayers[i].fromdict(list_dict_taxpayers2[i])
        #  here the [taxpayers] parameter has been updated with the server results
  • 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]

1
2
3
4
5
6
7
    def get_simulations(self) -> list:
        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['get-simulations']}"

        #  request execution
        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]

1
2
3
4
5
6
7
    def delete_simulation(self, id: int) -> list:
        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['delete-simulation']}/{id}"

        #  request execution
        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 the exceptions rise

        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['get-admindata']}"

        #  request execution
        résultat = self.get_response("GET", url_service)

        #  result is a dictionary of str values if xml session
        if self.__session_type == 'xml':
            #  new dictionary
            résultat2 = {}
            #  we take everything digital
            for key, value in résultat.items():
                #  some dictionary elements are lists
                if isinstance(value, list):
                    values = []
                    for value2 in value:
                        values.append(float(value2))
                    résultat2[key] = values
                else:
                    #  others simple elements
                    résultat2[key] = float(value)
        else:
            résultat2 = résultat
        #  result type AdminData
        return AdminData().fromdict(résultat2)
  • 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):
        #  the parameter
        self.__config = config

    def new_instance(self):
        #  render an instance of the [dao] layer
        return ImpôtsDaoWithHttpSession(self.__config)

31.3. Client configuration

Image

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

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  root path
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    #  absolute dependencies
    absolute_dependencies = [
        #  project files
        #  BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        #  InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        #  AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        #  ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        #  AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  ImpôtsDaoWithHttpSession, ImpôtsDaoWithHttpSessionFactory, InterfaceImpôtsDaoWithHttpSession
        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 ------
    #  application configuration with constants
    config.update({
        #  taxpayer file
        "taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
        #  results file
        "resultsFilename": f"{script_dir}/../data/output/résultats.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": "/calculer-impot",
                "get-admindata": "/get-admindata",
                "calculate-tax-in-bulk-mode": "/calculer-impots",
                "init-session": "/init-session",
                "end-session": "/fin-session",
                "authenticate-user": "/authentifier-utilisateur",
                "get-simulations": "/lister-simulations",
                "delete-simulation": "/supprimer-simulation"
            }
        },
        #  debug mode
        "debug": True
    }
    )

    #  step 3 ------
    #  layer instantiation
    import config_layers
    config['layers'] = config_layers.configure(config)

    #  we return the configuration
    return config

The [config_layers] file is as follows:

def configure(config: dict) -> dict:
    #  instantiation of applicatuon layers

    #  layer [profession]
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    #  factory dao layer
    from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
    dao_factory = ImpôtsDaoWithHttpSessionFactory(config)

    #  make the layer configuration
    return {
        "dao_factory": dao_factory,
        "métier": métier
    }
  • 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]:

#  a json or xml parameter is expected
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    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 a [dao] layer instance
    dao = dao_factory.new_instance()
    #  session type
    session_type = config['session_type']
    #  number of taxpayers
    nb_taxpayers = len(taxpayers)
    #  log
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    #  initialize the session
    dao.init_session(session_type)
    #  authenticate
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    #  taxpayers' taxes are calculated
    dao.calculate_tax_in_bulk_mode(taxpayers)
    #  end of session
    dao.end_session()
    #  log
    logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")

#  list of client threads
threads = []
logger = None
#  code
try:
    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it 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 factory
    dao_factory = config["layers"]["dao_factory"]
    #  create an instance of the [dao] layer
    dao = dao_factory.new_instance()
    #  reading taxpayer data
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #  taxpayers?
    if not taxpayers:
        raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #  multi-threaded tax calculation for taxpayers
    i = 0
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        #  each thread will process from 1 to 4 contributors
        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)]
        #  i is incremented for the next thread
        i += nb_taxpayers
        #  create the thread
        thread = threading.Thread(target=thread_function, args=(config, thread_taxpayers))
        #  we add it to the list of threads in the main script
        threads.append(thread)
        #  we launch the thread - this operation is asynchronous - we don't wait for the thread's result
        thread.start()
    #  the main thread waits for all threads it has launched to finish
    for thread in threads:
        thread.join()
    #  here all threads have finished their work - each has modified one or more objects [taxpayer]
    #  save the results in the jSON file
    dao.write_taxpayers_results(taxpayers)
    #  end
except BaseException as erreur:
    #  error display
    print(f"L'erreur suivante s'est produite : {erreur}")
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é...")
    #  end of threads that might still exist if we stopped on 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 : début du calcul de l'impôt des contribuables
2020-08-03 14:28:34.328749, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.328749, Thread-2 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.333592, Thread-3 : début du calcul de l'impôt des 3 contribuables
2020-08-03 14:28:34.368651, Thread-2 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.375699, Thread-1 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.377432, Thread-3 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.385653, Thread-2 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.392656, Thread-1 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.396377, Thread-3 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.406528, Thread-2 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}]}
2020-08-03 14:28:34.413837, Thread-1 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}]}
2020-08-03 14:28:34.416695, Thread-3 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 3}]}
2020-08-03 14:28:34.425747, Thread-2 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.425747, Thread-2 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.428956, Thread-1 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.428956, Thread-1 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.428956, Thread-3 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.428956, Thread-3 : fin du calcul de l'impôt des 3 contribuables
2020-08-03 14:28:34.428956, MainThread : fin du calcul de l'impôt des contribuables

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 : début du calcul de l'impôt des contribuables
2020-08-03 14:32:48.496452, Thread-1 : début du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.498992, Thread-2 : début du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.498992, Thread-3 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:32:48.498992, Thread-4 : début du calcul de l'impôt des 3 contribuables
2020-08-03 14:32:48.538637, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.540783, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.547811, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.547811, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.555184, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.564793, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.564793, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.568333, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.568333, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>2</enfants><salaire>55555</salaire><impôt>2814</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>oui</marié><enfants>2</enfants><salaire>50000</salaire><impôt>1384</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>384</décôte><réduction>347</réduction><id>2</id></réponse></root>
2020-08-03 14:32:48.579205, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>3</enfants><salaire>50000</salaire><impôt>0</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>720</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>non</marié><enfants>2</enfants><salaire>100000</salaire><impôt>19884</impôt><surcôte>4480</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse></root>
2020-08-03 14:32:48.579205, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>non</marié><enfants>3</enfants><salaire>100000</salaire><impôt>16782</impôt><surcôte>7176</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>oui</marié><enfants>3</enfants><salaire>100000</salaire><impôt>9200</impôt><surcôte>2180</surcôte><taux>0.3</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse><réponse><marié>oui</marié><enfants>5</enfants><salaire>100000</salaire><impôt>4230</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>0</décôte><réduction>0</réduction><id>3</id></réponse><réponse><marié>non</marié><enfants>0</enfants><salaire>100000</salaire><impôt>22986</impôt><surcôte>0</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>4</id></réponse></root>
2020-08-03 14:32:48.588051, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>2</enfants><salaire>30000</salaire><impôt>0</impôt><surcôte>0</surcôte><taux>0.0</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>non</marié><enfants>0</enfants><salaire>200000</salaire><impôt>64210</impôt><surcôte>7498</surcôte><taux>0.45</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse><réponse><marié>oui</marié><enfants>3</enfants><salaire>200000</salaire><impôt>42842</impôt><surcôte>17283</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>3</id></réponse></root>
2020-08-03 14:32:48.594058, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-1 : fin du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.595198, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-2 : fin du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.595198, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-3 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:32:48.603351, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.603351, Thread-4 : fin du calcul de l'impôt des 3 contribuables
2020-08-03 14:32:48.603351, MainThread : fin du calcul de l'impôt des contribuables

Above is the thread trace for [Thread-2].

31.5. The client [main2]

Image

The client [main2] allows you to test the URLs [/init-session, /authenticate-user, /get-admindata, /end-session]:

#  a json or xml parameter is expected
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    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"])
    #  we save it 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 factory
    dao_factory = config['layers']['dao_factory']
    #  create an instance of the [dao] layer
    dao = dao_factory.new_instance()
    #  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']}")
    #  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'])
    #  we retrieve data from the tax authorities
    admindata = dao.get_admindata()
    #  end of session
    dao.end_session()
    #  calculation of taxpayers' taxes by the [business] 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 erreur:
     #  error display
     print(f"L'erreur suivante s'est produite : {erreur}")
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 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 : début du calcul de l'impôt des contribuables
2020-08-03 14:44:43.231633, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:44:43.240872, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:44:43.250061, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>get-admindata</action><état>1000</état><réponse><limites>9964.0</limites><limites>27519.0</limites><limites>73779.0</limites><limites>156244.0</limites><limites>93749.0</limites><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><abattement_dixpourcent_min>437.0</abattement_dixpourcent_min><plafond_impot_couple_pour_decote>2627.0</plafond_impot_couple_pour_decote><plafond_decote_couple>1970.0</plafond_decote_couple><valeur_reduc_demi_part>3797.0</valeur_reduc_demi_part><plafond_revenus_celibataire_pour_reduction>21037.0</plafond_revenus_celibataire_pour_reduction><id>1</id><abattement_dixpourcent_max>12502.0</abattement_dixpourcent_max><plafond_impot_celibataire_pour_decote>1595.0</plafond_impot_celibataire_pour_decote><plafond_decote_celibataire>1196.0</plafond_decote_celibataire><plafond_revenus_couple_pour_reduction>42074.0</plafond_revenus_couple_pour_reduction><plafond_qf_demi_part>1551.0</plafond_qf_demi_part></réponse></root>
2020-08-03 14:44:43.269850, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:44:43.269850, MainThread : fin du calcul de l'impôt des contribuables

31.6. The client [main3]

The client [main3] allows you to test the URLs [/init-session, /calculate-taxes, /get-simulations, /delete-simulation, /end-session]:

Image

#  a json or xml parameter is expected
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    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"])
    #  we save it 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 factory
    dao_factory = config["layers"]["dao_factory"]
    #  create an instance of the [dao] layer
    dao = dao_factory.new_instance()
    #  reading taxpayer data
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #  taxpayers?
    if not taxpayers:
        raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #  tax calculation
    #  number of taxpayers
    nb_taxpayers = len(taxpayers)
    #  log
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    #  initialize the session
    dao.init_session(session_type)
    #  authenticate
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    #  taxpayers' taxes are calculated
    dao.calculate_tax_in_bulk_mode(taxpayers)
    #  the list of simulations is requested
    simulations = dao.get_simulations()
    #  we remove one out of two
    for i in range(len(simulations)):
        if i % 2 == 0:
            #  we delete the simulation
            dao.delete_simulation(simulations[i]['id'])
    #  end of session
    dao.end_session()
    #  consult the logs to see the various results (debug mode=True)
except BaseException as erreur:
    #  error display
    print(f"L'erreur suivante s'est produite : {erreur}")
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 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 : début du calcul de l'impôt des contribuables
2020-08-03 15:01:52.702297, MainThread : début du calcul de l'impôt des 11 contribuables
2020-08-03 15:01:52.734806, MainThread : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 15:01:52.747961, MainThread : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 15:01:52.765721, MainThread : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 5}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 6}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 7}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 8}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 9}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 10}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 11}]}
2020-08-03 15:01:52.785505, MainThread : {"action": "lister-simulations", "état": 500, "réponse": [{"décôte": 0, "enfants": 2, "id": 1, "impôt": 2814, "marié": "oui", "réduction": 0, "salaire": 55555, "surcôte": 0, "taux": 0.14}, {"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 720, "enfants": 3, "id": 3, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.801475, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 720, "enfants": 3, "id": 3, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.810129, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.818803, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.834604, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.843803, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.851855, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}]}
2020-08-03 15:01:52.863165, MainThread : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 15:01:52.863165, MainThread : fin du calcul de l'impôt des contribuables
  • line 6: we have 11 simulations;
  • line 12: after the various deletions, there are only 5 left;

31.7. The [Test2HttpClientDaoWithSession] test class

Image

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')
        erreur = False
        try:
            dao.init_session('json')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  there must be no error
        self.assertFalse(erreur)

    def test_init_session_xml(self) -> None:
        print('test_init_session_xml')
        erreur = False
        try:
            dao.init_session('xml')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  here must be no errors
        self.assertFalse(erreur)

    def test_init_session_xxx(self) -> None:
        print('test_init_session_xxx')
        erreur = False
        try:
            dao.init_session('xxx')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  there must be an error
        self.assertTrue(erreur)

    def test_authenticate_user_success(self) -> None:
        print('test_authenticate_user_success')
        #  init session
        dao.init_session('json')
        #  test
        erreur = False
        try:
            dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  there must be no errors
        self.assertFalse(erreur)

    def test_authenticate_user_failed(self) -> None:
        print('test_authenticate_user_failed')
        #  init session
        dao.init_session('json')
        #  test
        erreur = False
        try:
            dao.authenticate_user('x', 'y')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  there must be an error
        self.assertTrue(erreur)

    def test_get_simulations(self) -> None:
        print('test_get_simulations')
        #  init session
        dao.init_session('json')
        #  authentication
        dao.authenticate_user('admin', 'admin')
        #  tax calculation
        #  { 'married': 'yes', 'children': 2, 'salary': 55555,
        #  tax': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        #  get_simulations
        simulations = dao.get_simulations()
        #  checks
        #  there must be 1 simulation
        self.assertEqual(1, len(simulations))
        simulation = simulations[0]
        #  verification of calculated tax
        self.assertAlmostEqual(simulation['impôt'], 2815, delta=1)
        self.assertEqual(simulation['décôte'], 0)
        self.assertEqual(simulation['réduction'], 0)
        self.assertAlmostEqual(simulation['taux'], 0.14, delta=0.01)
        self.assertEqual(simulation['surcôte'], 0)

    def test_delete_simulation(self) -> None:
        print('test_delete_simulation')
        #  init session
        dao.init_session('json')
        #  authentication
        dao.authenticate_user('admin', 'admin')
        #  tax calculation
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 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))
        #  delete a non-existent simulation
        erreur = False
        try:
            dao.delete_simulation(100)
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  there must be an error
        self.assertTrue(erreur)

if __name__ == '__main__':
    #  configure the application
    import config
    config = config.configure({})

    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger

    #  layer [dao]
    dao_factory = config['layers']['dao_factory']
    dao = dao_factory.new_instance()

    #  test methods are executed
    print("tests en cours...")
    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 en cours...
test_authenticate_user_failed
..MyException[35, ["Echec de l'authentification"]]
test_authenticate_user_success
test_delete_simulation
MyException[35, ["la simulation n° [100] n'existe pas"]]
test_get_simulations
test_init_session_json
test_init_session_xml
test_init_session_xxx
MyException[73, il n'y a pas de session valide en cours]
----------------------------------------------------------------------
Ran 7 tests in 0.171s
 
OK
 
Process finished with exit code 0