Skip to content

30. Practical Exercise: Version 12

In this chapter, we will write a web application following the MVC (Model-View-Controller) architecture. The application will be able to return responses in three formats: JSON, XML, and HTML. There is a significant increase in complexity between what we are about to do and what we have done previously. We will reuse most of the concepts covered so far and will detail all the steps leading to the final application.

30.1. MVC Architecture

We will implement the MVC (Model–View–Controller) architectural pattern as follows:

Image

Processing a client request will proceed as follows:

  • 1 - Request

The requested URLs will be in the form http://machine:port/action/param1/param2/… The [Main Controller] will use a configuration file to "route" the request to the correct controller. To do this, it will use the [action] field of the URL. The rest of the URL [param1/param2/…] consists of optional parameters that will be passed to the action. The "C" in MVC here refers to the chain [Main Controller, Controller / Action]. If no controller can handle the requested action, the web server will respond that the requested URL was not found.

  • 2 - Processing
  • The selected action [2a] can use the parameters that the [Main Controller] has passed to it. These may come from two sources:
      • the path [/param1/param2/…] of the URL,
      • from parameters posted in the body of the client’s request;
    • When processing the user's request, the action may require the [business] layer [2b]. Once the client's request has been processed, it may trigger various responses. A classic example is:
      • an error response if the request could not be processed correctly;
      • a confirmation response otherwise;
    • the [Controller / Action] will return its response [2c] to the main controller along with a status code. These status codes will uniquely represent the current state of the application. It will be either a success code or an error code;
  • 3 - Response
    • Depending on whether the client requested a JSON, XML, or HTML response, the [Main Controller] will instantiate [3a] the appropriate response type and instruct it to send the response to the client. The [Main Controller] will pass it both the response and the status code provided by the [Controller/Action] that was executed;
    • if the desired response is of the JSON or XML type, the selected response will format the response from the [Controller/Action] that was provided to it and send it [3c]. The client capable of processing this response can be a Python console script or a JavaScript script embedded in an HTML page;
    • if the desired response is of the HTML type, the selected response will select [3b] one of the HTML views [Vuei] using the status code provided to it. This is the V in MVC. A single view corresponds to a single state code. This view V will display the response from the [Controller / Action] that was executed. It wraps the data from this response in HTML, CSS, and JavaScript. This data is called the view model. This is the M in MVC. The client is most often a browser;

Now, let’s clarify the relationship between MVC web architecture and layered architecture. Depending on how the model is defined, these two concepts may or may not be related. Let’s consider a single-layer MVC web application:

Image

In the example above, the [Controller / Action] each incorporate parts of the [business] and [DAO] layers. In the [web] layer, we do have an MVC architecture, but the application as a whole does not have a layered architecture. Here, there is only one layer—the web layer—which handles everything.

Now, let’s consider a multi-layer web architecture:

Image

The [Web] layer can be implemented without following the MVC model. We then have a multi-layer architecture, but the Web layer does not implement the MVC model.

For example, in the .NET world, the [web] layer above can be implemented with ASP.NET MVC, resulting in a layered architecture with an MVC-style [web] layer. Having done this, we can replace this ASP.NET MVC layer with a classic ASP.NET layer (WebForms) while keeping the rest (business logic, DAO, driver) unchanged. We then have a layered architecture with a [web] layer that is no longer MVC-based.

In MVC, we said that the M model was that of the V view, i.e., the set of data displayed by the V view. Another definition of the M model in MVC is given:

Image

Many authors consider that what lies to the right of the [web] layer forms the M model of MVC. To avoid ambiguity, we can refer to:

  • the domain model when referring to everything to the right of the [web] layer;
  • the view model when referring to the data displayed by a view V;

In what follows, when we refer to the model, we will always be referring to the view model.

30.2. Client/Server Application Architecture

The web application will have the following architecture:

Image

  • In [1], the web server will have two types of clients:
    • in [2], a console client that will exchange JSON and XML with the server;
    • in [3], a browser that will receive HTML from the server and display it;
  • The web server [1] retains the [business] and [DAO] layers from previous versions;
  • the web client [2] will be updated to account for the web application’s new service URLs;
  • The HTML application displayed by the browser must be written from scratch;

We will develop the application in several phases:

  • We will develop the JSON version of the server. We will test the server’s service URLs one by one using a Postman client. This method allows us to build the framework of the web server without worrying about the application’s views (=HTML);
  • After testing the JSON server with Postman, we will test it with a console client;
  • then we will move on to the XML version of the server. We have seen that switching from JSON to XML is straightforward;
  • finally, we will move on to the HTML version of the server. We will build an MVC architecture and define the views to be displayed. The HTML application will be tested using both the Postman client and a standard browser;

30.3. The server code directory structure

Image

  • in [1: the web server as a whole;
  • in [2]: for now, we will ignore the [static, templates, tests_views] folders, which pertain to the HTML version of the server. Outside of this folder, we will find the main script [main] and its configuration;
  • in [3], the web server controllers. These will be class instances;
 
  • in [4], the server’s HTTP response will be handled by classes;
  • in [5], we retain the log file from the previous servers;

When we build the HTML version of the server, other folders will come into play:

 
  • in [6], the static elements of the HTML application;
  • in [7], the HTML application templates broken down into views [9] and view fragments [8];
  • in [9], the classes implementing the view models;

30.4. The application’s service URLs

To build the web server, we will proceed as follows:

  • Based on the views of the HTML application, we will define the actions that the web application must implement. We will use the actual views here, but these could simply be views on paper;
  • Based on these actions, we will define the HTML application’s service URLs;
  • we will implement these service URLs using a server that returns JSON. This allows us to define the framework of the web server without worrying about the HTML pages to be served. We will test these service URLs using Postman;
  • We will then test our JSON server with a console client;
  • Once the JSON server has been validated, we will move on to writing the HTML application;

The first view will be the authentication view:

Image

  • the action leading to this first view will be called [init-session] [1];
  • Clicking the [Validate] button will trigger the [authenticate-user] action with two posted parameters [2-3];

The tax calculation view:

Image

  • In [1], the [authenticate-user] action that led to this view;
  • at [2], clicking the [Validate] button triggers the execution of the [calculate-tax] action with three posted parameters [2-5];
  • Clicking the link [6] triggers the action [list-simulations] without parameters;
  • Clicking the link [7] triggers the [end-session] action without parameters;

The third view displays the simulations performed by the authenticated user:

Image

  • in [3], the action [list-simulations] that led to this view;
  • in [2], clicking the [Delete] link triggers the [delete-simulation] action with a parameter: the number of the simulation to be deleted from the list;
  • clicking the [3] link triggers the [display-tax-calculation] action without parameters, which re-displays the tax calculation view;
  • Clicking the [4] link triggers the [end-session] action without parameters;

With this initial information, we can define the server’s various service URLs:

Action
Role
Execution Context
/init-session
Used to set the type (json, xml, html) of the desired responses
GET request
Can be sent at any time
/authenticate-user
Authorizes or denies a user's login
POST request.
The request must have two posted parameters [user, password]
Can only be issued if the session type (json, xml, html) is known
/calculate-tax
Performs a tax calculation simulation
POST request.
The request must have three posted parameters [married, children, salary]
Can only be issued if the session type (json, xml, html) is known and the user is authenticated
/list-simulations
Request to view the list of simulations performed since the start of the session
GET request.
Can only be issued if the session type (json, xml, html) is known and the user is authenticated
/delete-simulation/number
Deletes a simulation from the list of simulations
GET request.
Can only be issued if the session type (json, xml, html) is known and the user is authenticated
/display-tax-calculation
Displays the HTML page for the tax calculation
GET request.
Can only be issued if the session type (json, xml, html) is known and the user is authenticated
/end-session
Ends the simulation session.
Technically, the old web session is deleted and a new session is created
Can only be issued if the session type (json, xml, html) is known and the user is authenticated

These various service URLs will be used for both the HTML server and the JSON or XML servers. Two URLs will be used exclusively for the latter two servers: these are the URLs from the previous version of the client/web server that we are reusing here:

Action
Role
Execution context
/get-admindata
Returns tax data used to calculate the tax
GET request.
Used only if the session type is json or xml. The user must be authenticated
/calculate-taxes
Calculates the tax for a list of taxpayers posted in JSON
GET request.
Used only if the session type is json or xml. The user must be authenticated

All controllers associated with these actions will proceed in the same way:

  • they will check their parameters. These are found in the object:
    • [request.path] for parameters present in the URL in the form [/action/param1/param2/…];
    • in the [request.form] object for those transmitted as [x-www-form-urlencoded] in the request body;
    • in the [request.data] object for those transmitted as JSON in the request body;
  • A controller is similar to a function or method that checks the validity of its parameters. For the controller, however, it’s a bit more complicated:
    • the expected parameters may be missing;
    • The parameters retrieved by the controller are strings. If the expected parameter is a number, then the controller must verify that the parameter’s string actually represents a number;
    • Once verified that the expected parameters are present and syntactically correct, you must verify that they are valid in the current execution context. This context is present in the session. The authentication example is an example of an execution context. Certain actions should only be processed once the client has been authenticated. Generally, a key in the session indicates whether this authentication has taken place or not;
    • once the previous checks have been completed, the secondary controller can proceed. This parameter verification process is very important. We cannot accept a client sending us arbitrary data at any point during the application’s lifecycle. We must maintain full control over the application’s lifecycle;
    • Once its work is done, the secondary controller returns a dictionary with the keys [action, state, response] to the main controller that called it:
      • [action] is the action that has just been executed;
      • [state] is a three-digit number indicating the result of the action’s processing:
    • [x00] indicates successful processing;
    • [x01] indicates a processing failure;
  • [response] is the dictionary of results in the form {‘response’:object}. The object will have different structures depending on the action being processed;

We will now review the various controllers—or, in other words, the different actions these controllers handle—that drive the web application’s workflow.

30.5. Server Configuration

Image

The database configuration [config_database] and the server layer configuration [config_layers] are identical to those in previous versions. The [config] file now includes new information:


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"

    # 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",
        # Logger, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
        # scripts [config_database, config_layers]
        script_dir,
        # controllers
        "{script_dir}/../controllers",
        # HTTP responses
        f"{script_dir}/../responses",
        # view templates
        f"{script_dir}/../models_for_views",
    ]

    # Set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # web server dependencies

    # controllers
    from DisplayTaxCalculationController import DisplayTaxCalculationController
    from UserAuthController import UserAuthController
    from CalculateTaxesController import CalculateTaxesController
    from CalculateTaxesController import CalculateTaxesController
    from EndSessionController import EndSessionController
    from GetAdminDataController import GetAdminDataController
    from InitSessionController import InitSessionController
    from ListSimulationsController import ListSimulationsController
    from MainController import MainController
    from DeleteSimulationController import DeleteSimulationController

    # HTTP responses
    from HtmlResponse import HtmlResponse
    from JsonResponse import JsonResponse
    from XmlResponse import XmlResponse

    # view models
    from ModelForAuthentificationView import ModelForAuthentificationView
    from ModelForTaxCalculationView import ModelForTaxCalculationView
    from ModelForErrorView import ModelForErrorView
    from ModelForSimulationListView import ModelForSimulationListView

    # Step 2 ------
    # application configuration
    config.update({
        # users authorized to use the application
        "users": [
            {
                "login": "admin",
                "password": "admin"
            }
        ],

        # log file
        "logsFilename": f"{script_dir}/../data/logs/logs.txt",

        # SMTP server configuration
        "adminMail": {
            # SMTP server
            "smtp-server": "localhost",
            # SMTP server port
            "smtp-port": "25",
            # administrator
            "from": "guest@localhost.com",
            "to": "guest@localhost.com",
            # email subject
            "subject": "tax calculation server crash",
            # Set TLS to True if the SMTP server requires authentication, otherwise set it to False
            "tls": False
        },

        # thread pause duration in seconds
        "sleep_time": 0,

        # allowed actions and their controllers
        "controllers": {
            # initialization of a calculation session
            "init-session": InitSessionController(),
            # user authentication
            "authenticate-user": AuthenticateUserController(),
            # calculating taxes in individual mode
            "calculate-tax": CalculateTaxController(),
            # Calculate tax in batch mode
            "calculate-taxes": CalculateTaxesController(),
            # list of simulations
            "list-simulations": ListSimulationsController(),
            # Delete a simulation
            "delete-simulation": DeleteSimulationController(),
            # end of calculation session
            "end-session": EndSessionController(),
            # display the tax calculation view
            "display-tax-calculation": DisplayTaxCalculationController(),
            # retrieve data from the tax administration
            "get-admindata": GetAdminDataController(),
            # main controller
            "main-controller": MainController()
        },

        # different response types (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

        # HTML views and their templates depend on the state returned by the controller
        "views": [
            {
                # authentication view
                "states": [
                    # /init-session success
                    700,
                    # /authenticate-user failure
                    201
                ],
                "view_name": "views/authentication-view.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                # tax calculation view
                "statuses": [
                    # /authenticate-user success
                    200,
                    # /calculate-tax success
                    300,
                    # /calculate-tax failure
                    301,
                    # /display-tax-calculation
                    800
                ],
                "view_name": "views/tax-calculation-view.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                # view of the list of simulations
                "statements": [
                    # /list-simulations
                    500,
                    # /delete-simulation
                    600
                ],
                "view_name": "views/simulation-list-view.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],

        # unexpected error view
        "error-view": {
            "view_name": "views/error-view.html",
            "model_for_view": ModelForErrorsView()
        },

        # redirects
        "redirects": [
            {
                "statuses": [
                    400,  # /logout successful
                ],
                # redirect to
                "to": "/init-session/html",
            }
        ],
    }
    )

    # Step 3 ------
    # database configuration
    import config_database
    config["database"] = config_database.configure(config)

    # Step 4 ------
    # Instantiating the application layers
    import config_layers
    config['layers'] = config_layers.configure(config)

    # we return the configuration
    return config
  • Up to line 41, we see standard elements;
  • lines 43–66: by line 43, the server’s Python Path is defined. We can then import the project’s dependencies:
    • lines 45–55: the list of controllers;
    • lines 57–60: the list of HTTP responses;
    • lines 62–66: the list of view templates;
  • lines 68–189: the application configuration with a series of constants;
    • lines 71–98: we are already familiar with these lines from previous versions;
    • lines 101–122: the controller dictionary:
      • the keys are the names of the actions;
      • the values are an instance of the controller responsible for handling that action. Each controller is instantiated as a single instance (singleton). The same instance will be executed by different server threads. Therefore, care must be taken with shared data that each controller might want to modify;
    • lines 125–129: the dictionary of the three possible HTTP responses:
      • the keys are the type of response requested by the client (JSON, XML, HTML);
      • the values are an instance of the HTTP response. Each response generator is instantiated as a single instance (singleton). The same generator will be executed by different server threads. Care must therefore be taken with shared data that each generator might want to modify;
    • lines 132–186: configuration of HTML views. For now, we’ll ignore these lines;
  • lines 191–202: we have already encountered these lines in previous versions;

30.6. The path of a client request within the server

Image

We will follow the path of a client request arriving at the server through to the HTTP response sent back. It follows the MVC server’s flow.

30.6.1. The [main] script

The [main] script is identical in many ways to that of previous versions. We are nevertheless providing it in its entirety to ensure we start on the right foot:


# we expect a mysql or pgres parameter
import sys

syntax = f"{sys.argv[0]} mysql / pgres"
error = len(sys.argv) != 2
if not error:
    dbms = sys.argv[1].lower()
    error = dbsystem != "mysql" and dbsystem != "pgres"
if error:
    print(f"syntax: {syntax}")
    sys.exit()

# configure the application
import config
config = config.configure({'db': db})

# dependencies
from flask import request, Flask, session, url_for, redirect
from flask_api import status
from SendAdminMail import SendAdminMail
from myutils import json_response
from Logger import Logger
import threading
import time
from random import randint
from TaxError import TaxError
import os

# Send an email to the administrator
def send_adminmail(config: dict, message: str):
    # Send an email to the application administrator
    config_mail = config["adminMail"]
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)

# check the log file
logger = None
error = False
error_message = None
try:
    # Logger
    logger = Logger(config["logsFilename"])
except BaseException as exception:
    # console log
    print(f"The following error occurred: {exception}")
    # log the error
    error = True
    error_message = f"{exception}"
# store the logger in the config
config['logger'] = logger
# error handling
if error:
    # email to the administrator
    send_adminmail(config, error_message)
    # end of application
    sys.exit(1)

# startup log
log = "[server] server startup"
logger.write(f"{log}\n")
print(log)

# Retrieving data from the tax authority
error = False
try:
    # admindata will be a read-only application-scope data object
    config["admindata"] = config["layers"]["dao"].get_admindata().asdict()
    # success log
    logger.write("[server] database connection successful\n")
except ImpôtsError as ex:
    # log the error
    error = True
    # error log
    log = f"The following error occurred: {ex}"
    # console
    print(log)
    # log file
    logger.write(f"{log}\n")
    # email to the administrator
    send_adminmail(config, log)

# the main thread no longer needs the logger
logger.close()

# if there was an error, stop
if error:
    sys.exit(2)

# Flask application
app = Flask(__name__, template_folder="templates", static_folder="static")
# session secret key
app.secret_key = os.urandom(12).hex()

# the front controller
def front_controller() -> tuple:
    # process the request
    logger = None
    

@app.route('/', methods=['GET'])
def index() -> tuple:
    # redirect to /init-session/html
    return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)

# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    # execute the controller associated with the action
    return front_controller()

# authenticate-user
@app.route('/authenticate-user', methods=['POST'])
def authenticate_user() -> tuple:
    # execute the controller associated with the action
    return front_controller()

# calculate-tax
@app.route('/calculate-tax', methods=['POST'])
def calculate_tax() -> tuple:
    # execute the controller associated with the action
    return front_controller()

# list-simulations
@app.route('/list-simulations', methods=['GET'])
def list_simulations() -> tuple:
    # execute the controller associated with the action
    return front_controller()

# delete-simulation
@app.route('/delete-simulation/<int:number>', methods=['GET'])
def delete_simulation(number: int) -> tuple:
    # execute the controller associated with the action
    return front_controller()

# end-session
@app.route('/end-session', methods=['GET'])
def end_session() -> tuple:
    # execute the controller associated with the action
    return front_controller()

# display-tax-calculation
@app.route('/display-tax-calculation', methods=['GET'])
def display_tax_calculation() -> tuple:
    # execute the controller associated with the action
    return front_controller()

# get-admindata
@app.route('/get-admindata/<int:number>', methods=['GET'])
def get_admindata() -> tuple:
    # execute the controller associated with the action
    return front_controller()

# main only
if __name__ == '__main__':
    # start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • lines 1–92: all these lines have already been covered and explained;
  • line 92: the server will manage a session. We therefore need a secret key. For each user, we will store two pieces of information in the session:
    • whether the user has successfully authenticated;
    • Every time it performs a tax calculation, the results of that calculation will be placed in a list called the user's simulation list. This list will be stored in the session;
  • Lines 100–151: the list of the server’s service URLs. The associated functions act as a filter: any URLs not present in this list will be rejected by the Flask server with a [404 NOT FOUND] error. Once this filtering is complete, the request is systematically forwarded to a ‘Front Controller’ implemented by the [front_controller] function in lines 94–98, which we will discuss shortly;
  • Lines 100–103: handling the [/] route. The entry point for the web application will be the URL on line 107. Therefore, on line 103, we redirect the client to this URL:
  • The [url_for] function is imported on line 18. It has two parameters here:
      • the first parameter is the name of one of the routing functions, in this case the one on line 107. We can see that this function expects a parameter [type_response], which is the response type (json, xml, html) requested by the client;
      • the second parameter takes the name of the parameter from line 107, [type_response], and assigns a value to it. If there were other parameters, we would repeat the operation for each of them;
      • it returns the URL associated with the function designated by the two parameters provided to it. Here, this will return the URL from line 106, where the parameter is replaced by its value [/init-session/html];
    • The [redirect] function was imported on line 18. Its role is to send an HTTP redirect header to the client:
      • the first parameter is the URL to which the client should be redirected;
      • the second parameter is the status code of the HTTP response sent to the client. The code [status.HTTP_302_FOUND] corresponds to an HTTP redirect;

The [ front_controller] function in lines 94–98 performs the initial processing of the client’s request:


# the front controller
def front_controller() -> tuple:
    # process the request
    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"[ front_controller] request: {request}\n")
        # we terminate the thread if requested
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            # The pause is random so that some threads are interrupted and others are not
            random = randint(0, 1)
            if random = 1:
                # log before pause
                logger.write(f"[ front_controller] thread paused for {sleep_time} second(s)\n")
                # pause
                time.sleep(sleep_time)
        # forward the request to the main controller
        main_controller = config['controllers']["main-controller"]
        result, status_code = main_controller.execute(request, session, config)
        # log the result sent to the client
        log = f"[front_controller] {result}\n"
        logger.write(log)
        # Was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # Send an email to the application administrator
            send_adminmail(config, log)
        # Determine the desired response type
        if session.get('typeResponse') is None:
            # The response type has not yet been set—it will be JSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        # build the response to send
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, result)
        # send the response
        return response, status_code
    except BaseException as error:
        # This is an unexpected error—log the error if possible
        if logger:
            logger.write(f"[ front_controller] {error}")
        # prepare the response to the client
        result = {"response": {"errors": [f"{error}"]}}
        # Send a JSON response
        return json_response(result, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # Close the log file if it was opened
        if logger:
            logger.close()
  • Lines 1–57: We are familiar with this code. For example, this was the code for the function named [main] in the [main] script of the previous version. One thing to note is the controller used on lines 25–26:
  • line 25: we retrieve the controller instance associated with the name [main-controller] from the configuration. These are the following lines:

    # web server dependencies
    # controllers
    
    from MainController import MainController

    # authorized actions and their controllers
        "controllers": {
            ,
            # main controller
            "main-controller": MainController()
        },
  • (continued)
    • line 10 above, note that we are retrieving an instance of the class;
  • line 26: we ask the controller [MainController] to process the request;
  • lines 30–45: the response returned by the [MainController] is sent to the client. We’ll come back to these lines a little later;

The job of the [front_controller] function and then the [MainController] class is to handle the tasks common to all requests:

In the diagram above, we are still in phase 1 of request processing. The main controller [MainController] will continue with step 1.

Image

30.6.2. The main controller [MainController]

The main controller [MainController] continues the work started by the [front_controller] function:

All controllers implement the following [InterfaceController] [2] interface:

Image


from abc import ABC, abstractmethod

from werkzeug.local import LocalProxy

class InterfaceController(ABC):

    @abstractmethod
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        pass
  • The [InterfaceController] interface defines only the single [execute] method on line 8. This method takes three parameters:
    • [request]: the client’s request;
    • [session]: the client’s session;
    • [config]: the application configuration;

The [execute] method returns a two-element tuple:

  • the first is the results dictionary in the form {‘action’: action, ‘status’: status, ‘response’: results};
  • the second is the HTTP status code to return to the client;

The main controller [MainController] [1] implements the [InterfaceController] interface as follows:


# import dependencies

from flask_api import status
from werkzeug.local import LocalProxy

# web application controllers
from InterfaceController import InterfaceController

class MainController(InterfaceController):
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # retrieve the path elements
        params = request.path.split('/')
        action = params[1]

        # errors
        error = False
        # The session type must be known before certain actions
        type_response = session.get('typeResponse')
        if type_response is None and action != "init-session":
            # log the error
            result = {"action": action, "status": 101,
                        "response": ["No session in progress. Start with action [init-session]"]}
            error = True
        # For certain actions, you must be authenticated
        user = session.get('user')
        if not error and user is None and action not in ["init-session", "authenticate-user"]:
            # Log the error
            result = {"action": action, "status": 101,
                        "response": [f"action [{action}] requested by unauthenticated user"]}
            error = True
        # Are there any errors?
        if error:
            # return an error message
            return result, status.HTTP_400_BAD_REQUEST
        else:
            # execute the controller associated with the action
            controller = config["controllers"][action]
            result, status_code = controller.execute(request, session, config)
            return result, status_code

The [MainController] performs the initial checks to validate the request.

  • lines 11–13: The controller begins by retrieving the action requested by the client. Recall that service URLs are of the form [/action/param1/param2/…] and that this URL is in [request.path];
  • lines 17–23: the [init-session] action is used to initialize the response type (json, xml, html) requested by the client. This information is stored in the session under the key [responseType]. Therefore, if the action is not [init-session], the session must contain the key [responseType]; otherwise, the request is invalid;
  • lines 21-22: the structure of the result returned by each controller, in this case an error result:
    • [action]: is the name of the current action. This will allow us to retrieve its name when logging the request result;
    • [status]: is a three-digit status code:
        • [x00] for a success;
        • [x01] for a failure;
  • [response]: is the response to the request. Its nature is specific to each request;
  • lines 24–30: the [authenticate-user] action is used to authenticate the user. If successful, a [user=True] key is added to the user’s session. Certain service URLs are accessible only to an authenticated user. This is what is checked here;
  • line 26: only the [init-session] and [authenticate-user] actions can be performed by a user who has not yet been authenticated;
  • lines 28–29: the response to send in case of an error;
  • lines 32–34: if either of the two previous errors occurred, then the error response is sent to the client with HTTP status 400 BAD REQUEST;
  • lines 35–39: if no error occurred, control is passed to the controller responsible for handling the current action. Its instance is found in the application configuration;

The [MainController] class continues the work of the [front_controller] function: together, they handle everything that can be factored out of request processing, waiting until the last moment to pass the request to a specific controller. The division of code between the [front_controller] function and the [MainController] class is entirely subjective. Here I wanted to preserve the structure of the previous version: the [front_controller] function already existed under the name [main]. In practice, one could:

  • put everything in the [front_controller] function and eliminate the [MainController] class;
  • put everything in the [MainController] class and eliminate the [front_controller] function. I would tend to choose this solution because it has the advantage of streamlining the code of the main script [main];

30.7. Action-specific processing

Let’s return to the application’s MVC architecture:

Image

We are still at step 1 above. If there were no errors, step 2 will begin. The request has been forwarded to the controller specific to the action requested by the request. Let’s assume this action is [/init-session] defined by the route:


# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    # execute the controller associated with the action
    return front_controller()

This action is linked to a controller in the [config] configuration:


        # allowed actions and their controllers
        "controllers": {
            # Initialization of a calculation session
            "init-session": InitSessionController(),
            
        },

The [InitSessionController] (line 4) then takes over. Its code is as follows:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class InitSessionController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # retrieve the path elements
        dummy, action, type_response = request.path.split('/')

        # initially, no errors
        error = False
        # Check the response type
        if response_type is not in config['responses'].keys():
            error = True
            result = {"action": action, "status": 701,
                        "response": [f"invalid parameter [type={type_response}]"]}
        # if no error
        if not error:
            # set the session type in the Flask session
            session['typeResponse'] = type_response
            result = {"action": action, "status": 700,
                        "response": [f"session started with response type {type_response}"]}
            return result, status.HTTP_200_OK
        else:
            return result, status.HTTP_400_BAD_REQUEST
  • line 6: like the other controllers, the [InitSessionController] implements the [InterfaceController] interface;
  • line 10: the URL is of type [/init-session/type_response]. We retrieve the [init-session] action and the desired response type;
  • line 15: the desired response type can only be one of those present in the response configuration:

        # the different response types (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },
  • if this is not the case, an error response 701 is prepared (line 17);
  • lines 20–25: case where the desired response type is valid;
  • line 22: the desired response type is stored in the session. This is because we will need to remember it for subsequent requests;
  • lines 23–24: prepare a 700 success response;
  • line 25: the success response is returned to the caller;
  • line 27: if an error occurred, the error response is returned to the caller;

30.8. Generating the server’s HTTP response

Let’s return to the application’s MVC architecture:

Image

We have just covered steps 1 and 2. We encountered three status codes:

  • 700: /init-session succeeded;
  • 701: /init-session failed;
  • 101: invalid request, either because the session has not been initialized or because the user is not authenticated;

Let’s examine how the server’s response will be sent to the client during step 3 above. This happens in the [front_controller] function of the [main] script:


# the front controller
def front_controller() -> tuple:
    # process the request
    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"[ front_controller] request: {request}\n")
        # we terminate the thread if requested
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            # The pause is random so that some threads are interrupted and others are not
            random = randint(0, 1)
            if random = 1:
                # log before pause
                logger.write(f"[ front_controller] thread paused for {sleep_time} second(s)\n")
                # pause
                time.sleep(sleep_time)
        # forward the request to the main controller
        main_controller = config['controllers']["main-controller"]
        result, status_code = main_controller.execute(request, session, config)
        # log the result sent to the client
        log = f"[front_controller] {result}\n"
        logger.write(log)
        # Was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # Send an email to the application administrator
            send_adminmail(config, log)
        # Determine the desired response type
        if session.get('typeResponse') is None:
            # The response type has not yet been set—it will be JSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        # we construct the response to be sent
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, result)
        # send the response
        return response, status_code
    except BaseException as error:
        # This is an unexpected error—log the error if possible
        if logger:
            logger.write(f"[ front_controller] {error}")
        # prepare the response for the client
        result = {"response": {"errors": [f"{error}"]}}
        # Send a JSON response
        return json_response(result, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # Close the log file if it was opened
        if logger:
            logger.close()
  • We are now on line 26: the main controller has returned its error response;
  • lines 27–29: regardless of the main controller’s response (success or failure), this response is logged in the log file;
  • lines 30–33: as in previous versions, if the HTTP status is [500 INTERNAL SERVER ERROR], we send an email to the application administrator with the error log;
  • lines 34–39: We send the HTTP response, and the result returned by the controller is placed in the body of this response. We need to know in what format (JSON, XML, HTML) the client wants this response. We look for the desired response type in the session. If it is not there, we arbitrarily set this type to JSON;
  • lines 40–43: the HTTP response is constructed;

In the configuration file, each response type (json, xml, html) has been associated with a class instance:


        # the different response types (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

The response classes are located in the [responses] folder of the server directory tree:

Image

Each response class implements the following [InterfaceResponse] interface:


from abc import ABC, abstractmethod

from flask.wrappers import Response
from werkzeug.local import LocalProxy

class InterfaceResponse(ABC):

    @abstractmethod
    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            result: dict) -> (Response, int):
        pass
  • lines 8–11: the [InterfaceResponse] interface defines a single method [build_http_response] with the following parameters:
    • [request, session, config]: these are the parameters received by the action controller;
    • [result, status_code]: these are the results produced by the action controller;

We will now present the JSON response. It is generated by the following [JsonResponse] class:


import json

from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy

from InterfaceResponse import InterfaceResponse

class JsonResponse(InterfaceResponse):

    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            result: dict) -> (Response, int):
        # results: the results dictionary
        # status_code: the status code of the HTTP response

        # return the HTTP response
        response = make_response(json.dumps(result, ensure_ascii=False))
        response.headers['Content-Type'] = 'application/json; charset=utf-8'
        return response, status_code

We are familiar with this code, which we have encountered many times. It is the code for the [json_response] function in the [myutils] module.

30.9. Initial tests

In the code we examined, we encountered three status codes:

  • 700: /init-session succeeded;
  • 701: /init-session failed;
  • 101: invalid request, either because the session has not been initialized or because the user is not authenticated;

We will try to trigger these with a JSON session.

  • We start the web server, the DBMS, and the mail server;
  • We launch a Postman client;

Test 1

First, we’ll demonstrate an invalid request because the session hasn’t been initialized:

Image


# authenticate-user
@app.route('/authenticate-user', methods=['POST'])
def authenticate_user() -> tuple:
    # execute the controller associated with the action
    return front_controller()

but it is only accepted if the session has been initialized beforehand with the [/init-session] action.

Let’s execute the request and see the result sent by the server:

Image

  • [1-2]: we received a JSON response. When the response type has not yet been specified by the client, the server uses JSON to respond;
  • [3-5]: the JSON dictionary of the response;
    • [action]: the action that was executed;
    • [status]: the response status code. A code [x01] indicates an error;
    • [response]: is tailored to each action. Here it contains an error message;

Now let’s initialize a session with an incorrect response type:

Image

  • [1-2] is a valid route:

# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(response_type: str) -> tuple:
    # execute the controller associated with the action
    return front_controller()

It will therefore enter the MVC server's request processing pipeline. However, it should be rejected during this processing because the requested session type is incorrect.

The response is as follows:

Image

  • in [4], an error code [x01];
  • in [5], the error explanation;

Now, let’s initialize a JSON session:

Image

The response is as follows:

Image

Now, let’s initialize an XML session. The JSON response will be replaced by an XML response generated by the following [XmlResponse] class:


import xmltodict
from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy

from InterfaceResponse import InterfaceResponse
from Logger import Logger

class XmlResponse(InterfaceResponse):

    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            result: dict) -> (Response, int):
        # results: the results dictionary
        # status_code: the HTTP response status code

        # result: the dictionary to be converted to an XML string
        xml_string = xmltodict.unparse({"root": result})
        # we return the HTTP response
        response = make_response(xml_string)
        response.headers['Content-Type'] = 'application/xml; charset=utf-8'
        return response, status_code

This is code we’re familiar with—it’s from the [xml_response] function in the shared [myutils] module.

We initialize an XML session:

Image

The server’s response is then as follows:

Image

We get the same response as in JSON, but this time the response is formatted as XML.

30.10. The [authenticate-user] action

The [authenticate-user] action allows you to authenticate a user who wishes to use the tax calculation application. Its route is defined as follows in the [main] script:


# authenticate-user
@app.route('/authenticate-user', methods=['POST'])
def authenticate_user() -> tuple:
    # execute the controller associated with the action
    return front_controller()

The server expects two POST parameters:

  • [user]: the user's ID;
  • [password]: their password;

The list of authorized users is defined in the [config] configuration:


        # Users authorized to use the application
        "users": [
            {
                "login": "admin",
                "password": "admin"
            }
        ],

Here, we have a list with a single element.

The [authenticate-user] action is handled by the following [AuthentifierUtilisateurController] controller:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController
from Logger import Logger

class UserAuthenticationController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # retrieve the path elements
        dummy, action = request.path.split('/')

        # POST parameters
        post_params = request.form
        # HTTP response status code
        status_code = None
        # no errors initially
        error = False
        errors = []
        # A POST request with two parameters is required
        if len(post_params) != 2:
            error = True
            status_code = status.HTTP_400_BAD_REQUEST
            errors.append("POST method required, [action] parameter in the URL, [user, password] parameters posted")
        if not error:
            # retrieve the POST parameters
            # [user] parameter
            user = post_params.get("user")
            if user is None:
                error = True
                errors.append("[user] parameter missing")
            # [password] parameter
            password = post_params.get("password")
            if password is None:
                error = True
                errors.append("[password] parameter missing")
            # Error?
            if error:
                status_code = status.HTTP_400_BAD_REQUEST
        # error?
        if not error:
            # Check the validity of the (user, password) pair
            users = config['users']
            i = 0
            nbusers = len(users)
            found = False
            while not found and i < nbusers:
                found = user == users[i]["login"] and password == users[i]["password"]
                i += 1
            # Found?
            if not found:
                # Log the error
                error = True
                status_code = status.HTTP_401_UNAUTHORIZED
                errors.append(f"Authentication failed")
            else:
                # Mark in the session that the user was found
                session["user"] = True
        # done
        if not error:
            # return without error
            result = {"action": action, "status": 200, "response": f"Authentication successful"}
            return result, status.HTTP_200_OK
        else:
            # return with error
            return {"action": action, "status": 201, "response": errors}, status_code

  • line 14: retrieve the POST parameters;
  • line 19: the list of errors found in the request;
  • lines 20–24: we verify that there are indeed two posted parameters;
  • lines 27–31: check for the presence of a [users] parameter;
  • lines 32–36: check for the presence of a [password] parameter;
  • lines 38–39: if the posted parameters are incorrect, prepare an HTTP 400 BAD REQUEST response;
  • lines 40–58: verify that the credentials [user, password] belong to a user authorized to use the application;
  • lines 51–55: if the user (user, password) is not authorized to use the application, prepare an HTTP 401 UNAUTHORIZED response;
  • lines 56–58: if the user is authorized, we record in the session using the [user] key that they have authenticated;

Note that if the user was authenticated with credentials [credentials1] and fails to authenticate with credentials [credentials2], they remain authenticated with credentials [credentials1].

Let’s run some Postman tests:

  • We start the web server, the DBMS, and the mail server;
  • Using the Postman client:
    • start a JSON session;
    • then authenticate;

Here are different scenarios.

Case 1: POST without posted parameters

Image

  • In [3-5], the POST has no body;

The result of the request is as follows:

Image

  • In [2], we received an HTTP 400 BAD REQUEST response;
  • In [5], we received an error code [201];

Case 2: POST with incorrect credentials

Image

  • In [6], the credentials are incorrect;

The server sends the following response:

Image

  • in [2], the HTTP 401 UNAUTHORIZED response;
  • In [5], the error response;

Case 2: POST with correct credentials

Image

  • In [6], the credentials are correct;

The server's response is as follows:

  • in [2], an HTTP 200 OK response; Image
  • in [5], the success response;

30.11. The [calculate_tax] action

The [calculate_tax] action calculates a taxpayer’s tax. Its route is defined as follows in the [main] script:


# calculate-tax
@app.route('/calculate-tax', methods=['POST'])
def calculate_tax() -> tuple:
    # execute the controller associated with the action
    return front_controller()

The server expects three POST parameters:

  • [married]: yes / no;
  • [children]: number of children of the taxpayer;
  • [salary]: taxpayer's annual salary;

The [CalculateTaxController] controller handles the [calculate_tax] action:


import re

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController
from TaxPayer import TaxPayer

class CalculateTaxController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # retrieve the path elements
        dummy, action = request.path.split('/')

        # No errors at the start
        error = False
        errors = []
        # POST parameters
        post_params = request.form
        # A POST request with three parameters is required
        if len(post_params) != 3:
            error = True
            errors.append(
                "POST method required with the following parameters [married, children, salary]")
        # We analyze the posted parameters
        if not error:
            # married parameter
            married = post_params.get("married")
            if married is None:
                errors.append("[married] parameter missing")
            else:
                # Is the parameter valid?
                married = married.lower()
                if married != "yes" and married != "no":
                    error = True
                    errors.append(f"Invalid value [{married}] for parameter [married (yes/no)]")
            # [children] parameter
            children = post_params.get("children")
            if children is None:
                error = True
                errors.append("parameter [children] missing")
            else:
                # Is the parameter valid?
                children = children.strip()
                match = re.match(r"\d+", children)
                if not match:
                    error = True
                    errors.append(f"Invalid value [{children}] for parameter [children (integer >= 0)]")
            # salary parameter
            salary = post_params.get("salary")
            if salary is None:
                error = True
                errors.append("parameter [salary] missing")
            else:
                # Is the parameter valid?
                salary = salary.strip()
                match = re.match(r"\d+", salary)
                if not match:
                    error = True
                    errors.append(f"Invalid value [{salary}] for parameter [salary (integer >= 0)]")
        # error?
        if error:
            status_code = status.HTTP_400_BAD_REQUEST
            result = {"action": action, "status": 301, "response": errors}
            # return the result
            return result, status_code

        # calculate the tax
        # retrieve the [business] layer and the [adminData] dictionary
        business = config["layers"]["business"]
        admin_data = config["admindata"]
        # calculate tax
        taxpayer = TaxPayer().fromdict({'married': married, 'children': children, 'salary': salary})
        business.calculate_tax(taxpayer, admin_data)
        # simulation ID
        simulation_id = session.get('simulation_id', 0)
        simulation_id += 1
        session['simulation_id'] = simulation_id
        # Store the result in the session as a TaxPayer dictionary
        simulation = taxpayer.fromdict({'id': id_simulation}).asdict()
        # Add the result to the list of simulations already performed and store it in the session
        simulations = session.get("simulations", [])
        simulations.append(simulation)
        session["simulations"] = simulations
        # result
        result = {"action": action, "status": 300, "response": simulation}
        status_code = status.HTTP_200_OK

        # return the result
        return result, status_code
  • line 13: retrieve the name of the current action;
  • line 17: we collect the errors in a list;
  • line 19: retrieve the posted parameters. These are posted in the form [x-www-form-urlencoded], which is why we retrieve them from [request.form]. If they had been posted as JSON, we would have retrieved them from [request.data];
  • lines 21–24: we verify that there are indeed three posted parameters;
  • lines 27–36: check for the presence and validity of the posted parameter [married];
  • lines 37–48: checking for the presence and validity of the posted parameter [children];
  • lines 49–60: check for the presence and validity of the posted parameter [salary];
  • lines 62–66: if there was an error, a 400 BAD REQUEST error response is sent with a status code [301];
  • lines 69–71: if there was no error, prepare to calculate the tax. To do this,
    • line 70: retrieve a reference from the [business] layer;
    • line 71: retrieve data from the tax authority in the server configuration;
  • lines 72–74: the taxpayer’s tax is calculated;
  • lines 75–77: we count the number of tax calculations performed by the user;
    • line 76: retrieve the number of the last calculation performed from the session. Here, we refer to the result of a calculation as [simulation];
    • line 77: the number of the last simulation is incremented;
    • line 78: this number is saved to the session;
  • lines 79–84: to track the calculations performed by the user, we will store the list of simulations they have performed in their session;
  • line 80: a simulation will be the dictionary of a TaxPayer object whose [id] property will have the value of the simulation number;
  • lines 82–84: the current simulation is added to the list of simulations in the session;
  • lines 86-87: we prepare an HTTP success response;
  • line 90: we return the result;

Let’s run some tests: the web server, the DBMS, the mail server, and a Postman client are launched.

Case 1: performing a tax calculation while the session is not initialized

Image

The response is as follows:

Image

Case 2: performing a tax calculation without being authenticated

First, we start a JSON session with [/init-session/json]. Then we make the same request as before. The response is as follows:

Image

Case 3: Performing a tax calculation with missing parameters

We initialize a JSON session, authenticate, and then make the following request:

Image

  • in [5], the [married] parameter is missing;

The response is as follows:

Case 4: Calculating tax with incorrect parameters

Image

Image

The server’s response is as follows:

Image

Case 4: Performing a tax calculation with correct parameters

Image

The server's response is as follows:

Image

30.12. The [list-simulations] action

The [list-simulations] action allows a user to view the list of simulations they have performed since the start of the session. Its route is defined as follows in the [main] script:


# list-simulations
@app.route('/list-simulations', methods=['GET'])
def list_simulations() -> tuple:
    # Execute the controller associated with the action
    return front_controller()

The server does not expect any parameters. The [lister-simulations] action is handled by the following [ListerSimulationsController]:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class ListerSimulationsController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # retrieve the path elements
        dummy, action = request.path.split('/')

        # retrieve the list of simulations from the session
        simulations = session.get("simulations", [])
        # return the result
        return {"action": action, "status": 500,
                "response": simulations}, status.HTTP_200_OK
  • line 13: the list of simulations is retrieved from the session;
  • lines 15-16: a success response is returned;

Let’s run the following Postman test:

  • We start a JSON session;
  • We authenticate;
  • Perform two tax calculations;
  • We request the list of simulations;

The request is as follows:

  • in [3], there are no parameters; Image

The server's response is as follows:

Image

  • in [4], the user's list of simulations;

30.13. The [delete-simulation] action

The [delete-simulation] action allows a user to delete one of the simulations from their simulation list. Its route is defined as follows in the [main] script:


# delete-simulation
@app.route('/delete-simulation/<int:number>', methods=['GET'])
def delete_simulation(number: int) -> tuple:
    # execute the controller associated with the action
    return front_controller()

The server expects a single parameter: the number of the simulation to be deleted. The [delete-simulation] action is handled by the following [DeleteSimulationController]:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class DeleteSimulationController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # retrieve the path elements
        dummy, action, number = request.path.split('/')

        # The [number] parameter is a positive integer or zero, based on its modulus
        number = int(number)
        # The simulation with id=number must exist in the list of simulations
        simulations = session.get("simulations", [])
        simulations_list = list(filter(lambda simulation: simulation['id'] == number, simulations))
        if not simulations_list:
            error_message = f"Simulation #[{number}] does not exist"
            # return the error
            return {"action": action, "status": 601, "response": [error_message]}, status.HTTP_400_BAD_REQUEST
        # Delete the simulation with id=number
        simulation = simulations_list.pop(0)
        simulations.remove(simulation)
        # Put the simulations back into the session
        session["simulations"] = simulations
        # return the result
        return {"action": action, "status": 600, "response": simulations}, status.HTTP_200_OK
  • line 10: retrieve the two elements of the request path. They are retrieved as strings;
  • line 13: the [number] parameter is converted to an integer. We know this is possible because of the route’s signature,

@app.route('/delete-simulation/<int:number>', methods=['GET'])

We also know that it is an integer >=0. Indeed, we cannot have a URL like [/delete-simulation/-4]. This is rejected by the Flask server;

  • line 15: we retrieve the list of simulations from the session;
  • line 16: using the [filter] function, we search for the simulation with id==number. We obtain a [filter] object that we convert to a [list];
  • lines 17–20: if the filter returns nothing, then the simulation to be deleted does not exist. We return an error response indicating this;
  • lines 21–23: we delete the simulation returned by the filter;
  • line 25: We restore the new list of simulations to the session;
  • line 27: return the new list of simulations in the response;

We perform a success test and a failure test. We run simulations and then request the list of simulations:

Image

  • The simulations here have numbers 2 and 3;

We request that the simulation with number 3 be removed.

Image

The response is as follows:

Now, let’s repeat the same operation (deleting the simulation with id=3). The response is then as follows:

Image

Image

30.14. The [end-session] action

The [end-session] action allows a user to end their simulation session. Its path is defined as follows in the [main] script:


# end-session
@app.route('/logout', methods=['GET'])
def end_session() -> tuple:
    # execute the controller associated with the action
    return front_controller()

The server expects no parameters. The action is handled by the following [FinSessionController]:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class FinSessionController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # retrieve the path elements
        dummy, action = request.path.split('/')

        # remove all keys from the current session
        session.clear()
        # return the result
        return {"action": action, "status": 400, "response": "session reset"}, status.HTTP_200_OK
  • Line 13: Delete all keys from the session. This deletes:
    • [typeResponse]: the type of HTTP responses (json, xml, html);
    • [simulation_id]: the ID of the last simulation performed;
    • [simulations]: the list of the user’s simulations;
    • [user]: the indicator that the user has been authenticated;
  • return the response;

One might wonder how the HTTP response from line 15 will be returned, now that the response type is no longer in the session. To find out, we need to go back to the |front_controller| function in the main script [main] and modify it as follows:


…        
         # note the desired response type if this information is in the session
        type_response1 = session.get('typeResponse', None)
        # forward the request to the main controller
        main_controller = config['controllers']["main-controller"]
        result, status_code = main_controller.execute(request, session, config)
        # log the result sent to the client
        log = f"[front_controller] {result}\n"
        logger.write(log)
        # Was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # Send an email to the application administrator
            send_adminmail(config, log)
        # Determine the desired response type
        type_response2 = session.get('typeResponse')
        if type_response2 is None and type_response1 is None:
            # The session type has not yet been established—it will be JSON
            type_response = 'json'
        elif type_response2 is not None:
            # the response type is known and in the session
            type_response = type_response2
        else:
            response_type = response_type1
        # we construct the response to be sent
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, result)
        # send the response
        return response, status_code
  • line 3: the type of the response currently in session is stored;
  • line 6: the action is executed. If it is:
    • [end-session], the [typeResponse] key is no longer in the session;
    • [init-session], the [typeResponse] key in the session may have changed value;;
  • lines 14–20: the HTTP response must be sent. We need to know in what form:
    • lines 16–18: if the response type is not defined by either [type_response1] in line 3 or [type_response2] in line 15, then the response type was not defined either before or after the action. We then use JSON (line 18);
    • lines 19–21: if [type_response2] exists—the response type in the session after the action—then that is the type to use;
    • lines 22–23: otherwise, [type_response1], the response type before the action (which must be [end-session]), is the one to use;

30.15. The [get-admindata] action

We will now discuss the two URLs reserved for the JSON and XML services:

Action
Role
Execution context
/get-admindata
Returns the tax data used to calculate the tax
GET request.
Used only if the session type is json or xml. The user must be authenticated
/calculate-taxes
Calculates the tax for a list of taxpayers posted in JSON
GET request.
Used only if the session type is json or xml. The user must be authenticated

The URL [/get-admindata] is defined in the routes of the main script [main] as follows:


# get-admindata
@app.route('/get-admindata', methods=['GET'])
def get_admindata() -> tuple:
    # execute the controller associated with the action
    return front_controller()

The route [/get-admindata] is handled by the following [GetAdminDataController]:


# import dependencies

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class GetAdminDataController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # retrieve the path elements
        dummy, action = request.path.split('/')
        # only json and xml sessions are accepted
        type_response = session.get('typeResponse')
        if type_response != 'json' and type_response != 'xml':
            # return an error response
            return {
                       "action": action,
                       "status": 1001,
                       "response": ["this action is only available for JSON or XML sessions"]
                   }, status.HTTP_400_BAD_REQUEST
        else:
            # return a success response
            return {"action": action, "status": 1000, "response": config["adminData"].asdict()}, status.HTTP_200_OK
  • lines 13-21: we check that we are in a JSON or XML session;
  • line 24: return the tax administration data dictionary, which was placed in the configuration when the server started:

    # admindata will be a read-only application-scope variable
    config["admindata"] = config["layers"]["dao"].get_admindata()

Let’s use a Postman client and request the URL [/get-admindata], after starting a JSON session and authenticating:

Image

The server response is as follows:

Image

30.16. The [calculate-taxes] action

The [calculate-taxes] action calculates the taxes for a list of taxpayers found in the request body as a JSON string. We are already familiar with this action: it was called [calculate_tax_in_bulk_mode] in the previous version.

Its route is as follows:


# batch tax calculation
@app.route('/calculate-taxes', methods=['POST'])
def calculate_taxes():
    # execute the controller associated with the action
    return front_controller()

This action is handled by the following [CalculateTaxesController]:


import json

from flask_api import status
from werkzeug.local import LocalProxy

from TaxError import TaxError
from InterfaceController import InterfaceController
from TaxPayer import TaxPayer

class CalculateTaxesController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # retrieve the path elements
        dummy, action = request.path.split('/')

        # only json and xml sessions are accepted
        type_response = session.get('typeResponse')
        if type_response != 'json' and type_response != 'xml':
            # return an error response
            return {
                       "action": action,
                       "status": 1501,
                       "response": ["this action is only available for JSON or XML sessions"]
                   }, status.HTTP_400_BAD_REQUEST

        # retrieve the body of the POST request - expect a list of dictionaries
        error_message = None
        list_dict_taxpayers = None
        # the JSON body of the POST
        request_text = request.data
        try:
            # which we convert into a list of dictionaries
            list_dict_taxpayers = json.loads(request_text)
        except BaseException as error:
            # log the error
            msg_error = f"The POST body is not a valid JSON string: {error}"
        # Is the list non-empty?
        if not error_message and (not isinstance(taxpayers_list_dict, list) or len(taxpayers_list_dict) == 0):
            # Log the error
            error_message = "The POST body is not a list, or the list is empty"
        # Do we have a list of dictionaries?
        if not error_message:
            error = False
            i = 0
            while not error and i < len(list_dict_taxpayers):
                error = not isinstance(list_dict_taxpayers[i], dict)
                i += 1
            # error?
            if error:
                error_message = "The body of the POST request must be a list of dictionaries"
        # error?
        if error_message:
            # send an error response to the client
            results = {"action": action, "status": 1501, "response": [error_message]}
            return results, status.HTTP_400_BAD_REQUEST

        # check the TaxPayers one by one
        # initially, no errors
        error_list = []
        for tax_payer_dict in tax_payer_dicts:
            # create a TaxPayer from dict_taxpayer
            error_message = None
            try:
                # The following operation will eliminate cases where the parameters are not
                # properties of the TaxPayer class, as well as cases where their values
                # are incorrect
                TaxPayer().fromdict(dict_taxpayer)
            except BaseException as error:
                error_message = f"{error}"
            # certain keys must be present in the dictionary
            if not error_message:
                # the keys [married, children, salary] must be present in the dictionary
                keys = dict_taxpayer.keys()
                if 'married' not in keys or 'children' not in keys or 'salary' not in keys:
                    error_message = "The dictionary must include the keys [married, children, salary]"
            # Any errors?
            if error_message:
                # log the error in the TaxPayer itself
                dict_taxpayer['error'] = error_message
                # add the TaxPayer to the list of errors
                list_errors.append(dict_taxpayer)

        # We've processed all taxpayers—are there any errors?
        if errors_list:
            # send an error response to the client
            results = {"action": action, "status": 1501, "response": errors_list}
            return results, status.HTTP_400_BAD_REQUEST

        # no errors, we can proceed
        # retrieve data from the tax administration
        admindata = config["admindata"]
        business = config["layers"]["business"]
        try:
            # process taxpayers one by one
            list_taxpayers = []
            for dict_taxpayer in list_dict_taxpayers:
                # calculate the tax
                taxpayer = TaxPayer().fromdict(
                    {'married': dict_taxpayer['married'], 'children': dict_taxpayer['children'],
                     'salary': dict_taxpayer['salary']})
                job.calculate_tax(taxpayer, admindata)
                # store the result as a dictionary
                list_taxpayers.append(taxpayer.asdict())
            # add list_taxpayers to the current simulations, assigning a number to each simulation
            simulations = session.get("simulations", [])
            simulation_id = session.get("simulation_id", 0)
            for simulation in list_taxpayers:
                # assign a number to each simulation
                simulation_id += 1
                simulation['id'] = simulation_id
                # add it to the current list of simulations
                simulations.append(simulation)
            # Put everything back into the session
            session["simulations"] = simulations
            session["simulation_id"] = simulation_id
            # send the response to the client
            return {"action": action, "status": 1500, "response": list_taxpayers}, status.HTTP_200_OK
        except TaxError as error:
            # send an error response to the client
            return {"action": action, "status": 1501, "response": [f"{error}"]}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • lines 16-24: we verify that we are indeed in a JSON or XML session
  • lines 26–120: this code is generally familiar to us. It is from the |index_controller| function in version 10 of the application, which has been adapted to meet the specifications of the implemented [InterfaceController] interface;
  • lines 104–115: code added to account for this controller’s new environment. We have just performed tax calculations. We need to store the results in the list of simulations maintained in the session;
  • line 105: we retrieve the list of simulations in the session;
  • line 106: we retrieve the number of the last simulation performed;
  • lines 107–112: we iterate through the list of dictionaries containing the tax calculation results; we assign a simulation ID to each one, and each dictionary is added to the list of simulations;
  • lines 113–115: the new list of simulations and the number of the last simulation performed are returned to the session;

We perform the following Postman test after initializing a JSON session and authenticating:

Image

Image

The server response is as follows:

Image

If we now request the list of simulations:

Note that in the results list for [/calcul-impots], taxpayers do not have an [id] attribute, whereas in the list of simulations, each simulation has a number that identifies it.

Image