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

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

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

    #  dependencies
    absolute_dependencies = [
        #  project files
        #  BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        #  InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        #  AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        #  ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        #  AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  Logger, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
        #  scripts [config_database, config_layers]
        script_dir,
        #  controllers
        f"{script_dir}/../controllers",
        #  answers HTTP
        f"{script_dir}/../responses",
        #  view models
        f"{script_dir}/../models_for_views",
    ]

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

    #  web server dependencies

    #  controllers
    from AfficherCalculImpotController import AfficherCalculImpotController
    from AuthentifierUtilisateurController import AuthentifierUtilisateurController
    from CalculerImpotController import CalculerImpotController
    from CalculerImpotsController import CalculerImpotsController
    from FinSessionController import FinSessionController
    from GetAdminDataController import GetAdminDataController
    from InitSessionController import InitSessionController
    from ListerSimulationsController import ListerSimulationsController
    from MainController import MainController
    from SupprimerSimulationController import SupprimerSimulationController

    #  answers HTTP
    from HtmlResponse import HtmlResponse
    from JsonResponse import JsonResponse
    from XmlResponse import XmlResponse

    #  view models
    from ModelForAuthentificationView import ModelForAuthentificationView
    from ModelForCalculImpotView import ModelForCalculImpotView
    from ModelForErreursView import ModelForErreursView
    from ModelForListeSimulationsView import ModelForListeSimulationsView

    #  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",

        #  server config SMTP
        "adminMail": {
            #  server SMTP
            "smtp-server": "localhost",
            #  server port SMTP
            "smtp-port": "25",
            #  director
            "from": "guest@localhost.com",
            "to": "guest@localhost.com",
            #  mail subject
            "subject": "plantage du serveur de calcul d'impôts",
            #  tls to True if server SMTP requires authorization, False otherwise
            "tls": False
        },

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

        #  authorized shares and their auditors
        "controllers": {
            #  initialization of a calculation session
            "init-session": InitSessionController(),
            #  user authentication
            "authentifier-utilisateur": AuthentifierUtilisateurController(),
            #  tax calculation in individual mode
            "calculer-impot": CalculerImpotController(),
            #  batch mode tax calculation
            "calculer-impots": CalculerImpotsController(),
            #  list of simulations
            "lister-simulations": ListerSimulationsController(),
            #  deleting a simulation
            "supprimer-simulation": SupprimerSimulationController(),
            #  end of calculation session
            "fin-session": FinSessionController(),
            #  display tax calculation view
            "afficher-calcul-impot": AfficherCalculImpotController(),
            #  obtaining data from tax authorities
            "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 models depend on the state rendered by the controller
        "views": [
            {
                #  authentication view
                "états": [
                    #  /init-session success
                    700,
                    #  /authentifier-user failure
                    201
                ],
                "view_name": "views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                #  tax calculation
                "états": [
                    #  /authentifier-user success
                    200,
                    #  /calculate-tax-success
                    300,
                    #  /calculate-tax failure
                    301,
                    #  /show-tax-calculation
                    800
                ],
                "view_name": "views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                #  view of simulation list
                "états": [
                    #  /lister-simulations
                    500,
                    #  /suppress-simulation
                    600
                ],
                "view_name": "views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],

        #  view of unexpected errors
        "view-erreurs": {
            "view_name": "views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },

        #  redirections
        "redirections": [
            {
                "états": [
                    400,  #  /end-session successful
                ],
                #  redirection to
                "to": "/init-session/html",
            }
        ],
    }
    )

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

    #  step 4 ------
    #  instantiation of 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:

#  a mysql or pgres parameter is expected
import sys

syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

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

#  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 ImpôtsError import ImpôtsError
import os

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

#  check log file
logger = None
erreur = False
message_erreur = None
try:
    #  logger
    logger = Logger(config["logsFilename"])
except BaseException as exception:
    #  log console
    print(f"L'erreur suivante s'est produite : {exception}")
    #  we note the error
    erreur = True
    message_erreur = f"{exception}"
#  store the logger in the config
config['logger'] = logger
#  error handling
if erreur:
    #  mail to administrator
    send_adminmail(config, message_erreur)
    #  end of application
    sys.exit(1)

#  start-up log
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)

#  data recovery from tax authorities
erreur = False
try:
    #  admindata will be read-only application data
    config["admindata"] = config["layers"]["dao"].get_admindata().asdict()
    #  success log
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    #  we note the error
    erreur = True
    #  error log
    log = f"L'erreur suivante s'est produite : {ex}"
    #  console
    print(log)
    #  log file
    logger.write(f"{log}\n")
    #  mail to administrator
    send_adminmail(config, log)

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

#  if there has been an error, we stop
if erreur:
    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:
    #  the request is processed
    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('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

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

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

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

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

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

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

#  hand 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:
    #  we process the request
    logger = None
    try:
        #  logger
        logger = Logger(config["logsFilename"])
        #  we store it in a config associated with the thread
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        #  log the request
        logger.write(f"[ front_controller] requête : {request}\n")
        #  the thread is interrupted if requested
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            #  pause is randomized so that some threads are interrupted and others not
            aléa = randint(0, 1)
            if aléa == 1:
                #  log before break
                logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                #  break
                time.sleep(sleep_time)
        #  forward the request to the main controller
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        #  we log the result sent to the customer
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        #  was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            #  send an e-mail to the application administrator
            send_adminmail(config, log)
        #  determine the desired type of response
        if session.get('typeResponse') is None:
            #  the session type has not yet been set - it will be jSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        #  build the response to be sent
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        #  we send the answer
        return response, status_code
    except BaseException as erreur:
        #  it's an unexpected error - log the error if possible
        if logger:
            logger.write(f"[ front_controller] {erreur}")
        #  we prepare the response to the customer
        résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
        #  we send a response in jSON
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        #  close the log file if it has been 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 shares 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 path elements
        params = request.path.split('/')
        action = params[1]

        #  errors
        erreur = False
        #  session type must be known prior to certain actions
        type_response = session.get('typeResponse')
        if type_response is None and action != "init-session":
            #  we note the error
            résultat = {"action": action, "état": 101,
                        "réponse": ["pas de session en cours. Commencer par action [init-session]"]}
            erreur = True
        #  some actions require authentication
        user = session.get('user')
        if not erreur and user is None and action not in ["init-session", "authentifier-utilisateur"]:
            #  we note the error
            résultat = {"action": action, "état": 101,
                        "réponse": [f"action [{action}] demandée par utilisateur non authentifié"]}
            erreur = True
        #  are there any mistakes?
        if erreur:
            #  an error msg is returned
            return résultat, status.HTTP_400_BAD_REQUEST
        else:
            #  execute the controller associated with the action
            controller = config["controllers"][action]
            résultat, status_code = controller.execute(request, session, config)
            return résultat, 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:

1
2
3
4
5
#  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:


        # actions autorisées et leurs contrôleurs
        "controllers": {
            # initialisation d'une session de calcul
            "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):
        #  path elements are retrieved
        dummy, action, type_response = request.path.split('/')

        #  initially no error
        erreur = False
        #  check response type
        if type_response not in config['responses'].keys():
            erreur = True
            résultat = {"action": action, "état": 701,
                        "réponse": [f"paramètre [type={type_response}] invalide"]}
        #  if no error
        if not erreur:
            #  set the session type in the flask session
            session['typeResponse'] = type_response
            résultat = {"action": action, "état": 700,
                        "réponse": [f"session démarrée avec le type de réponse {type_response}"]}
            return résultat, status.HTTP_200_OK
        else:
            return résultat, 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:

        # les différents types de réponse (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:
    #  the request is processed
    logger = None
    try:
        #  logger
        logger = Logger(config["logsFilename"])
        #  we store it in a config associated with the thread
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        #  log the request
        logger.write(f"[ front_controller] requête : {request}\n")
        #  the thread is interrupted if requested
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            #  pause is randomized so that some threads are interrupted and others not
            aléa = randint(0, 1)
            if aléa == 1:
                #  log before break
                logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                #  break
                time.sleep(sleep_time)
        #  forward the request to the main controller
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        #  we log the result sent to the customer
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        #  was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            #  send an e-mail to the application administrator
            send_adminmail(config, log)
        #  determine the desired type of response
        if session.get('typeResponse') is None:
            #  the session type has not yet been set - it will be jSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        #  build the response to be sent
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        #  we send the answer
        return response, status_code
    except BaseException as erreur:
        #  it's an unexpected error - log the error if possible
        if logger:
            logger.write(f"[ front_controller] {erreur}")
        #  we prepare the response to the customer
        résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
        #  we send a response in jSON
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        #  close the log file if it has been 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:


        # les différents types de réponse (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,
                            résultat: 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,
                            résultat: dict) -> (Response, int):
        #  results: the results dictionary
        #  status_code: status code of the HTTP response

        #  we return the answer HTTP
        response = make_response(json.dumps(résultat, 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('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> 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(type_response: 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,
                            résultat: dict) -> (Response, int):
        #  results: the results dictionary
        #  status_code: status code of the HTTP response

        #  result: the dictionary to be transformed into the XML string
        xml_string = xmltodict.unparse({"root": résultat})
        #  we return the answer HTTP
        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:

1
2
3
4
5
#  authenticate-user
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> 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:


        # utilisateurs autorisés à utiliser l'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 AuthentifierUtilisateurController(InterfaceController):

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

        #  POST parameters
        post_params = request.form
        #  response status code HTTP
        status_code = None
        #  initially no errors
        erreur = False
        erreurs = []
        #  you need a POST with two parameters
        if len(post_params) != 2:
            erreur = True
            status_code = status.HTTP_400_BAD_REQUEST
            erreurs.append("méthode POST requise, paramètre [action] dans l'URL, paramètres postés [user, password]")
        if not erreur:
            #  retrieve POST parameters
            #  parameter [user]
            user = post_params.get("user")
            if user is None:
                erreur = True
                erreurs.append("paramètre [user] manquant")
            #  parameter [password]
            password = post_params.get("password")
            if password is None:
                erreur = True
                erreurs.append("paramètre [password] manquant")
            #  mistake?
            if erreur:
                status_code = status.HTTP_400_BAD_REQUEST
        #  mistake?
        if not erreur:
            #  check the validity of the (user, password) pair
            users = config['users']
            i = 0
            nbusers = len(users)
            trouvé = False
            while not trouvé and i < nbusers:
                trouvé = user == users[i]["login"] and password == users[i]["password"]
                i += 1
            #  found?
            if not trouvé:
                #  we note the error
                erreur = True
                status_code = status.HTTP_401_UNAUTHORIZED
                erreurs.append(f"Echec de l'authentification")
            else:
                #  note in the session that the user has been found
                session["user"] = True
        #  it's over
        if not erreur:
            #  error-free return
            résultat = {"action": action, "état": 200, "réponse": f"Authentification réussie"}
            return résultat, status.HTTP_200_OK
        else:
            #  return with error
            return {"action": action, "état": 201, "réponse": erreurs}, 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:

1
2
3
4
5
#  calculate-tax
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> 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 CalculerImpotController(InterfaceController):

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

        #  no error at start
        erreur = False
        erreurs = []
        #  POST parameters
        post_params = request.form
        #  you need a POST with three parameters
        if len(post_params) != 3:
            erreur = True
            erreurs.append(
                "méthode POST requise avec les paramètres postés [marié, enfants, salaire]")
        #  analyze posted parameters
        if not erreur:
            #  married parameter
            marié = post_params.get("marié")
            if marié is None:
                erreurs.append("paramètre [marié] manquant")
            else:
                #  is the parameter valid?
                marié = marié.lower()
                if marié != "oui" and marié != "non":
                    erreur = True
                    erreurs.append(f"valeur [{marié}] invalide pour le paramètre [marié (oui/non)]")
            #  children] parameter
            enfants = post_params.get("enfants")
            if enfants is None:
                erreur = True
                erreurs.append("paramètre [enfants] manquant")
            else:
                #  is the parameter valid?
                enfants = enfants.strip()
                match = re.match(r"\d+", enfants)
                if not match:
                    erreur = True
                    erreurs.append(f"valeur [{enfants}] invalide pour le paramètre [enfants (entier>=0)]")
            #  salary parameter
            salaire = post_params.get("salaire")
            if salaire is None:
                erreur = True
                erreurs.append("paramètre [salaire] manquant")
            else:
                #  is the parameter valid?
                salaire = salaire.strip()
                match = re.match(r"\d+", salaire)
                if not match:
                    erreur = True
                    erreurs.append(f"valeur [{salaire}] invalide pour le paramètre [salaire (entier>=0)]")
        #  mistake?
        if erreur:
            status_code = status.HTTP_400_BAD_REQUEST
            résultat = {"action": action, "état": 301, "réponse": erreurs}
            #  we return the result
            return résultat, status_code

        #  tAX CALCULATION
        #  retrieve the [business] layer and the [adminData] dictionary
        métier = config["layers"]["métier"]
        admin_data = config["admindata"]
        #  tAX CALCULATION
        taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
        métier.calculate_tax(taxpayer, admin_data)
        #  simulation no
        id_simulation = session.get('id_simulation', 0)
        id_simulation += 1
        session['id_simulation'] = id_simulation
        #  we put the result in session in the form of a TaxPayer dictionary
        simulation = taxpayer.fromdict({'id': id_simulation}).asdict()
        #  we add the result to the list of simulations already carried out and put it in session
        simulations = session.get("simulations", [])
        simulations.append(simulation)
        session["simulations"] = simulations
        #  result
        résultat = {"action": action, "état": 300, "réponse": simulation}
        status_code = status.HTTP_200_OK

        #  we return the result
        return résultat, 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:

1
2
3
4
5
#  lister-simulations
@app.route('/lister-simulations', methods=['GET'])
def lister_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):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  retrieve the list of simulations in the session
        simulations = session.get("simulations", [])
        #  we return the result
        return {"action": action, "état": 500,
                "réponse": 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:

1
2
3
4
5
#  delete-simulation
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: 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 SupprimerSimulationController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action, numéro = request.path.split('/')

        #  parameter [number] is a positive integer or zero according to its route
        numéro = int(numéro)
        #  the simulation id=number must exist in the simulation list
        simulations = session.get("simulations", [])
        liste_simulations = list(filter(lambda simulation: simulation['id'] == numéro, simulations))
        if not liste_simulations:
            msg_erreur = f"la simulation n° [{numéro}] n'existe pas"
            #  we return the error
            return {"action": action, "état": 601, "réponse": [msg_erreur]}, status.HTTP_400_BAD_REQUEST
        #  delete simulation id=number
        simulation = liste_simulations.pop(0)
        simulations.remove(simulation)
        #  put the simulations back in the session
        session["simulations"] = simulations
        #  we return the result
        return {"action": action, "état": 600, "réponse": 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('/supprimer-simulation/<int:numero>', 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:

1
2
3
4
5
#  end of session
@app.route('/fin-session', methods=['GET'])
def fin_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):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  delete all keys in the current session
        session.clear()
        #  we return the result
        return {"action": action, "état": 400, "réponse": "session réinitialisée"}, 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:


…        
         # on not# note the type of response required 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"]
        résultat, status_code = main_controller.execute(request, session, config)
        # we log the result sent to the customer
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        # was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # send an e-mail to the application administrator
            send_adminmail(config, log)
        # determine the desired type of response
        type_response2=session.get('typeResponse')
        if  type_response2 is None and type_response1 is None:
            # the session type has not yet been set - it will be jSON
            type_response = 'json'
        elif type_response2 is not None:
            # the type of response is known and in the session
            type_response = type_response2
        else:
            type_response=type_response1
        # build the response to be sent
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        # we send the answer
        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:

1
2
3
4
5
#  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):
        #  path elements are retrieved
        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':
            #  an error response is returned
            return {
                       "action": action,
                       "état": 1001,
                       "réponse": ["cette action n'est possible que pour les sessions json ou xml"]
                   }, status.HTTP_400_BAD_REQUEST
        else:
            #  a success answer is returned
            return {"action": action, "état": 1000, "réponse": 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 sera une donnée de portée application en lecture seule
    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:

1
2
3
4
5
#  batch tax calculation
@app.route('/calculer-impots', methods=['POST'])
def calculer_impots():
    #  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 ImpôtsError import ImpôtsError
from InterfaceController import InterfaceController
from TaxPayer import TaxPayer

class CalculerImpotsController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        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':
            #  an error response is returned
            return {
                       "action": action,
                       "état": 1501,
                       "réponse": ["cette action n'est possible que pour les sessions json ou xml"]
                   }, status.HTTP_400_BAD_REQUEST

        #  retrieve the body of the post - wait for a list of dictionaries
        msg_erreur = None
        list_dict_taxpayers = None
        #  the jSON body of POST
        request_text = request.data
        try:
            #  which we transform into a list of dictionaries
            list_dict_taxpayers = json.loads(request_text)
        except BaseException as erreur:
            #  we note the error
            msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
        #  do we have a non-empty list?
        if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
            #  we note the error
            msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
        #  do we have a list of dictionaries?
        if not msg_erreur:
            erreur = False
            i = 0
            while not erreur and i < len(list_dict_taxpayers):
                erreur = not isinstance(list_dict_taxpayers[i], dict)
                i += 1
            #  mistake?
            if erreur:
                msg_erreur = "le corps du POST doit être une liste de dictionnaires"
        #  mistake?
        if msg_erreur:
            #  an error response is sent to the client
            résultats = {"action": action, "état": 1501, "réponse": [msg_erreur]}
            return résultats, status.HTTP_400_BAD_REQUEST

        #  check TaxPayers one by one
        #  initially no errors
        list_erreurs = []
        for dict_taxpayer in list_dict_taxpayers:
            #  we create a TaxPayer from dict_taxpayer
            msg_erreur = None
            try:
                #  the following operation will eliminate cases where the parameters are not
                #  properties of the TaxPayer class as well as the cases where their values
                #  are incorrect
                TaxPayer().fromdict(dict_taxpayer)
            except BaseException as erreur:
                msg_erreur = f"{erreur}"
            #  certain keys must be present in the dictionary
            if not msg_erreur:
                #  the keys [married, children, salary] must be present in the dictionary
                keys = dict_taxpayer.keys()
                if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
                    msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
            #  mistakes?
            if msg_erreur:
                #  we note the error in the TaxPayer itself
                dict_taxpayer['erreur'] = msg_erreur
                #  add the TaxPayer to the error list
                list_erreurs.append(dict_taxpayer)

        #  we've processed all the taxpayers - are there any mistakes?
        if list_erreurs:
            #  an error response is sent to the client
            résultats = {"action": action, "état": 1501, "réponse": list_erreurs}
            return résultats, status.HTTP_400_BAD_REQUEST

        #  no mistakes, we can work
        #  data recovery from tax authorities
        admindata = config["admindata"]
        métier = config["layers"]["métier"]
        try:
            #  process the TaxPayer one by one
            list_taxpayers = []
            for dict_taxpayer in list_dict_taxpayers:
                #  tAX CALCULATION
                taxpayer = TaxPayer().fromdict(
                    {'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
                     'salaire': dict_taxpayer['salaire']})
                métier.calculate_tax(taxpayer, admindata)
                #  the result is stored as a dictionary
                list_taxpayers.append(taxpayer.asdict())
            #  we add list_taxpayers to the current simulations, giving each simulation a number
            simulations = session.get("simulations", [])
            id_simulation = session.get("id_simulation", 0)
            for simulation in list_taxpayers:
                #  each simulation is given a number
                id_simulation += 1
                simulation['id'] = id_simulation
                #  we add it to the current list of simulations
                simulations.append(simulation)
            #  we put everything back in session
            session["simulations"] = simulations
            session["id_simulation"] = id_simulation
            #  we send the response to the client
            return {"action": action, "état": 1500, "réponse": list_taxpayers}, status.HTTP_200_OK
        except ImpôtsError as erreur:
            #  an error response is sent to the client
            return {"action": action, "état": 1501, "réponse": [f"{erreur}"]}, 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