Skip to content

27. Application Exercise: Version 9

We return to version 7 of the application exercise, and instead of the client and web server exchanging JSON strings, they will now exchange XML.

The architecture remains the same:

Image

27.1. The web server

Image

The [http-servers/04] folder is created by copying the [http-servers/02] folder, with the exception of the [utilities] subfolder. Then the following elements are changed:

Image

The [config] file is modified as follows:


    # absolute dependencies
    absolute_dependencies = [
        # project folders
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # TaxDaoInterface, TaxBusinessInterface, TaxUiInterface
        f"{root_dir}/taxes/v04/interfaces",
        # AbstractTaxDao, TaxConsole, TaxBusiness
        f"{root_dir}/taxes/v04/services",
        # TaxDaoWithAdminDataInDatabase
        f"{root_dir}/taxes/v05/services",
        # AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/taxes/v04/entities",
        # Constants, brackets
        f"{root_dir}/taxes/v05/entities",
        # index_controller
        f"{root_dir}/impots/http-servers/01/controllers",
        # scripts [config_database, config_layers]
        script_dir,
        # Logger, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
    ]
  • line 21: specify the directory for the utilities that remain in [http-servers/02];

The main script [main] changes as follows:


# Home URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    logger = None
    try:
        # logger
        logger = Logger(config["logsFilename"])
        # store it in a thread-specific configuration
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        # log the request
        logger.write(f"[index] request: {request}\n")
        
        # execute the request via a controller
        result, status_code = index_controller.execute(request, config)
        # Was there a fatal error?
        
        # log the response
        logger.write(f"[index] {result}\n")
        # send the response
        return xml_response(result, status_code)
    except BaseException as error:
        # log the error if possible
        if logger:
            logger.write(f"[index] {error}")
        # Prepare the response for the client
        result = {"response": {"errors": [f"{error}"]}}
        # Send the response
        return xml_response(result, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # Close the log file if it was opened
        if logger:
            logger.close()
  • The only change is on line 23: we now send an XML response;

The [xml_response] function is defined in the [myutils] module:


import xmltodict

def xml_response(result: dict, status_code: int) -> tuple:
    # result: the dictionary to be converted into an XML string
    xmlString = xmltodict.unparse(result)
    # return the HTTP response
    response = make_response(xmlString)
    response.headers['Content-Type'] = 'application/xml; charset=utf-8'
    return response, status_code
  • Line 3: The [xml_response] function takes as parameters:
    • the [result] dictionary to be converted to XML;
    • the status code [status_code] to be returned to the web client;
  • line 5: we use the [xmltodict] library to generate the XML string;
  • line 8: we use the [Content-Type] header to tell the client that we are sending XML;

The [xml_response] function must be imported into the [__init__.py] script:


from .myutils import set_syspath, json_response, decode_flask_session, xml_response

Then the [myutils] module must be included in the machine-wide modules. This is done in a PyCharm terminal using the [pip install .] command (in the packages folder).

27.2. The web client

27.2.1. The code

The [http-clients/04] folder is created by copying the [http-clients/02] client. Then we modify the [ImpôtsDaoWithHttpClient] class as follows:


# imports

import requests
import xmltodict
from flask_api import status

from AbstractTaxesDao import AbstractTaxesDao
from AdminData import AdminData
from TaxError import TaxError
from BusinessTaxInterface import BusinessTaxInterface
from TaxPayer import TaxPayer


class TaxDaoWithHttpClient(AbstractTaxDao, BusinessTaxInterface):

    # constructor
    def __init__(self, config: dict):
        

    # unused method
    def get_admindata(self) -> AdminData:
        pass

    # tax calculation
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        ….
        # HTTP response status code
        status_code = response.status_code
        # Put the XML response into a dictionary
        result = xmltodict.parse(response.text[39:])
        # error if status code is not 200 OK
        ….
  • Line 30: The HTTP response [response] from the web server is now an XML string. The logs show its format:

2020-07-27 15:53:47.886283, Thread-2: <?xml version="1.0" encoding="utf-8"?>
<response><result><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></result></response>

The string [<?xml version="1.0" encoding="utf-8"?>] is 38 characters long. Furthermore, if we look at the log file with a hex editor, we see that after this string there is a newline character \n. Then comes the response <response>…</response>. The XML string we need to convert therefore begins after the first 39 characters of the XML string. It starts at character #39, with the first character numbered 0. This string is obtained using the expression [response.text[39:]].

If we run the client (follow the procedure from the previous examples), we get the same results in the [results.json] file as in the previous versions. The logs are as follows:


2020-07-27 16:21:14.015941, Thread-1: Start of thread [Thread-1] with 2 taxpayer(s)
2020-07-27 16:21:14.016940, Thread-1: Start of tax calculation for {"id": 1, "married": "yes", "children": 2, "salary": 55555}
2020-07-27 16:21:14.016940, Thread-2: Start of thread [Thread-2] with 3 taxpayers
2020-07-27 16:21:14.018939, Thread-2: Start of tax calculation for {"id": 3, "married": "yes", "children": 3, "salary": 50000}
2020-07-27 16:21:14.019979, Thread-3: Start of thread [Thread-3] with 3 taxpayers
2020-07-27 16:21:14.019979, Thread-3: Start of tax calculation for {"id": 6, "married": "yes", "children": 3, "salary": 100000}
2020-07-27 16:21:14.021938, Thread-4: Start of thread [Thread-4] with 2 taxpayers
2020-07-27 16:21:14.021938, Thread-4: Start of tax calculation for {"id": 9, "married": "yes", "children": 2, "salary": 30000}
2020-07-27 16:21:14.021938, Thread-5: Start of thread [Thread-5] with 1 taxpayer
2020-07-27 16:21:14.022939, Thread-5: Start of tax calculation for {"id": 11, "married": "yes", "children": 3, "salary": 200000}
2020-07-27 16:21:14.031942, Thread-1: <?xml version="1.0" encoding="utf-8"?>
<response><result><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></result></response>
2020-07-27 16:21:14.031942, Thread-1: End of tax calculation for {"id": 1, "married": "yes", "children": 2, "salary": 55555, "tax": 2814, "surcharge": 0, "rate": 0.14, "discount": 0, "reduction": 0}
2020-07-27 16:21:14.031942, Thread-1: Start of tax calculation for {"id": 2, "married": "yes", "children": 2, "salary": 50000}
2020-07-27 16:21:14.034941, Thread-4: <?xml version="1.0" encoding="utf-8"?>
<response><result><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></result></response>

2020-07-27 16:21:17.055931, Thread-3: end of thread [Thread-3]
2020-07-27 16:21:17.059930, Thread-2: <?xml version="1.0" encoding="utf-8"?>
<response><result><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></result></response>
2020-07-27 16:21:17.060971, Thread-2: End of tax calculation for {"id": 5, "married": "no", "children": 3, "salary": 100000, "tax": 16782, "surcharge": 7176, "rate": 0.41, "discount": 0, "reduction": 0}
2020-07-27 16:21:17.060971, Thread-2: end of thread [Thread-2]

On the server side, the logs are as follows:


2020-07-27 16:32:04.983020, Thread-46: [index] request: <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=50000' [GET]>
2020-07-27 16:32:04.983020, Thread-46: [index] thread paused for 1 second(s)
2020-07-27 16:32:04.984021, Thread-47: [index] request: <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=55555' [GET]>
2020-07-27 16:32:04.984021, Thread-47: [index] Thread paused for 1 second

2020-07-27 16:32:07.001271, Thread-56: [index] Thread paused for 1 second
2020-07-27 16:32:07.003078, Thread-54: [index] {'response': {'result': {'married': 'yes', 'children': 5, 'salary': 100000, 'tax': 4230, 'surcharge': 0, 'rate': 0.14, 'discount': 0, 'reduction': 0}}}
2020-07-27 16:32:07.006078, Thread-55: [index] {'response': {'result': {'married': 'yes', 'children': 3, 'salary': 200000, 'tax': 42842, 'surcharge': 17283, 'rate': 0.41, 'discount': 0, 'reduction': 0}}}
2020-07-27 16:32:08.002824, Thread-56: [index] {'response': {'result': {'married': 'no', 'children': 2, 'salary': 100000, 'tax': 19884, 'surcharge': 4480, 'rate': 0.41, 'discount': 0, 'reduction': 0}}}
  • The logger continues to write the response dictionary rather than the XML string sent to the client. This is not an error and is intentional;

27.2.2. Testing the client's [DAO] layer

Image

The test class [TestHttpClientDao] is the same as in |version 7| and yields the same results.