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

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

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

Image

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

Image


# 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

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