Skip to content

23. Esercizio pratico: Versione 6

23.1. Introduzione

Torniamo ora alla nostra applicazione per il calcolo delle imposte. Realizzeremo varie applicazioni web basate su di essa.

Nella versione 5 dell'esercizio sulla nostra applicazione, i dati dell'autorità fiscale erano memorizzati in un database. Questa versione 5 consisteva in due applicazioni separate che condividevano livelli comuni:

  • un'applicazione che calcolava le imposte in modalità |batch| per i contribuenti memorizzati in un file di testo;
  • un'applicazione che calcolava le imposte in modalità |interattiva| per i contribuenti le cui informazioni venivano inserite tramite la tastiera;

La versione 5 dell'applicazione per il calcolo delle imposte in batch presentava la seguente architettura:

Image

Alla fine, la versione web di questa applicazione avrà la seguente architettura:

Image

  • il client web [1] comunica con il server web [2], che a sua volta comunica con il DBMS [3];
  • il server web [2] mantiene i livelli [business] [8] e [DAO] [9] dell'applicazione originale;
  • L'applicazione originale mantiene il proprio script principale [4] e il proprio livello [business] [15]. I livelli [business] [8] e [15] sono identici;
  • la comunicazione client/server richiede due livelli aggiuntivi:
    • il livello [web] [7], che implementa l'applicazione web;
    • il livello [DAO] [5], che funge da client per l'applicazione web [7];

Nella versione finale, il calcolo delle imposte in batch può essere eseguito in due modi:

  • la logica di business per il calcolo delle imposte è gestita dal livello [business] del server. Lo script [main] utilizzerà questo metodo;
  • la logica di business per il calcolo delle imposte è gestita dal livello [business] del client. Lo script [main2] utilizzerà questo metodo;

D'ora in poi, svilupperemo diverse applicazioni client/server del tipo sopra descritto, ciascuna delle quali illustrerà una o più nuove tecnologie di sviluppo web.

23.2. Il server web per il calcolo delle imposte

23.2.1. Versione 1

Image

Lo script [server_01] è la seguente applicazione web:

Image

  • In [1], utilizziamo un URL parametrizzato in cui passiamo tre valori:
    • [married] (sì/no) per indicare se il contribuente è sposato;
    • [children]: il numero di figli del contribuente;
    • [salary]: lo stipendio annuale del contribuente;
  • In [2], il server web restituisce una stringa JSON che fornisce l'importo dell'imposta dovuta insieme alle sue varie componenti;

L'architettura dell'applicazione è la seguente:

Image

  • il browser [1] interroga il server [2]. Lo script [server_01] implementa il livello [web] [2] del server;
  • i livelli [3-8] sono quelli già utilizzati nella |versione 5| dell'applicazione di calcolo delle imposte. Li riutilizziamo così come sono;
    • Il livello [business] [3] è definito |qui|;
    • il livello [DAO] [4] è definito |qui|;

L'applicazione web [server_01] è configurata utilizzando tre script:

  • [config], che configura l'intera applicazione;
  • [config_database], che configura l'accesso al database. Lavoreremo con i DBMS MySQL e PostgreSQL;
  • [config_layers], che configura i livelli dell'applicazione;

Lo script [config] è il seguente:

def configure(config: dict) -> dict:
    import os

    #  step 1 ------
    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))
    #  root path
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    #  absolute dependencies
    absolute_dependencies = [
        #  project files
        #  BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        #  InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        #  AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        #  ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        #  AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  IndexController
        f"{script_dir}/../controllers",
        #  scripts [config_database, config_layers]
        script_dir,
    ]
    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration
    #  list of users authorized to use the application
    config['users'] = [
        {
            "login": "admin",
            "password": "admin"
        }
    ]

    #  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
  • La funzione [configure] accetta come argomento un dizionario [config] (riga 1) e lo restituisce come risultato (riga 54) dopo averne arricchito il contenuto. Si sarebbe potuto far notare già da tempo che non era necessario restituire il risultato [config]. Infatti, [config] è un riferimento a un dizionario che il codice chiamante condivide con il codice chiamato. Il codice chiamante possiede quindi già questo riferimento (riga 1) e non c'è bisogno di restituirlo nuovamente (riga 54). Pertanto, scrivendo:

config=[module].configure(config) (1)

è ridondante. È sufficiente scrivere:


[module].configure(config) (2)

Ciononostante, ho mantenuto lo stile di scrittura (1) perché ho ritenuto che potesse illustrare meglio il fatto che il codice chiamato modifica il dizionario [config].

  • riga 1: il dizionario [config] ricevuto dalla funzione [configure] ha una chiave ‘sgbd’ il cui valore è tratto dalla lista [‘mysql’, ‘pgres’]. [mysql] significa che il database utilizzato è gestito da MySQL, mentre ‘pgres’ significa che il database utilizzato è gestito da PostgreSQL;
  • Righe 4–27: elenchiamo tutte le directory contenenti gli elementi necessari per l’applicazione web. Faranno parte del Python Path dell’applicazione (righe 30–31);
  • righe 33–40: solo determinati utenti potranno accedere all’applicazione. Qui abbiamo un elenco con un unico utente;
  • righe 43–46: lo script [config_database] crea la configurazione per il database in uso;
  • riga 46: la configurazione creata dallo script [config_database] è un dizionario che memorizziamo nella configurazione generale associata alla chiave ‘database’;
  • righe 48–51: lo script [config_layers] istanzia i livelli dell'applicazione web. Restituisce un dizionario che viene memorizzato nella configurazione generale sotto la chiave ‘layers’;

Lo script [config_database] è quello già utilizzato nella |versione 5|. Lo riportiamo qui a titolo di riferimento:

def configure(config: dict) -> dict:
    #  sqlalchemy configuration
    from sqlalchemy import create_engine, Table, Column, Integer, MetaData, Float
    from sqlalchemy.orm import mapper, sessionmaker

    #  connection chains to the databases used
    connection_strings = {
        'mysql': "mysql+mysqlconnector://admimpots:mdpimpots@localhost/dbimpots-2019",
        'pgres': "postgresql+psycopg2://admimpots:mdpimpots@localhost/dbimpots-2019"
    }
    #  connection chain to the database used
    engine = create_engine(connection_strings[config['sgbd']])

    #  metadata
    metadata = MetaData()

    #  the constants table
    constantes_table = Table("tbconstantes", metadata,
                             Column('id', Integer, primary_key=True),
                             Column('plafond_qf_demi_part', Float, nullable=False),
                             Column('plafond_revenus_celibataire_pour_reduction', Float, nullable=False),
                             Column('plafond_revenus_couple_pour_reduction', Float, nullable=False),
                             Column('valeur_reduc_demi_part', Float, nullable=False),
                             Column('plafond_decote_celibataire', Float, nullable=False),
                             Column('plafond_decote_couple', Float, nullable=False),
                             Column('plafond_impot_celibataire_pour_decote', Float, nullable=False),
                             Column('plafond_impot_couple_pour_decote', Float, nullable=False),
                             Column('abattement_dixpourcent_max', Float, nullable=False),
                             Column('abattement_dixpourcent_min', Float, nullable=False)
                             )

    #  tax bracket table
    tranches_table = Table("tbtranches", metadata,
                           Column('id', Integer, primary_key=True),
                           Column('limite', Float, nullable=False),
                           Column('coeffr', Float, nullable=False),
                           Column('coeffn', Float, nullable=False)
                           )
    #  mappings
    from Tranche import Tranche
    mapper(Tranche, tranches_table)

    from Constantes import Constantes
    mapper(Constantes, constantes_table)

    #  the factory session
    session_factory = sessionmaker()
    session_factory.configure(bind=engine)

    #  a session
    session = session_factory()

    #  certain information is recorded and rendered in a dictionary
    return {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
            "constantes_table": constantes_table, "session": session}

Lo script [config_layers] configura i livelli del server web. Riutilizziamo uno |script| che abbiamo già visto in precedenza:

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

    #  dao
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    dao = ImpotsDaoWithAdminDataInDatabase(config)

    #  business
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    #  put layer instances in a dictionary and return them to the calling code
    return {
        "dao": dao,
        "métier": métier
    }
  • Riga 6: Il livello [dao] è implementato utilizzando un database;
  • [ImpotsDaoWithAdminDataInDatabase] è stato definito |qui|;
  • [BusinessTaxes] è stato definito |qui|;

Lo script principale [server_01] è il seguente:

#  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 ImpôtsError import ImpôtsError
from TaxPayer import TaxPayer
import re
from flask import request
from myutils import json_response
from flask import Flask
from flask_api import status

#  data recovery from tax authorities
try:
    #  admindata will be read-only application data
    admindata = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

#  flask application
app = Flask(__name__)


#  Home URL : /?married=xx&children=yy&salary=zz
@app.route('/', methods=['GET'])
def index():
    #  initially no errors
    erreurs = []
    #  the query must have three parameters in the URL
    if len(request.args) != 3:
        erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")

    #  retrieve marital status in URL
    marié = request.args.get('marié')
    if marié is None:
        erreurs.append("paramètre [marié] manquant")
    else:
        marié = marié.strip().lower()
        erreur = marié != "oui" and marié != "non"
        if erreur:
            erreurs.append(f"paramétre marié [{marié}] invalide")

    #  retrieve the number of children in the URL
    enfants = request.args.get('enfants')
    if enfants is None:
        erreurs.append("paramètre [enfants] manquant")
    else:
        enfants = enfants.strip()
        match = re.match(r"^\d+", enfants)
        if not match:
            erreurs.append(f"paramétre enfants [{enfants}] invalide")
        else:
            enfants = int(enfants)

    #  the salary is retrieved from the URL
    salaire = request.args.get('salaire')
    if salaire is None:
        erreurs.append("paramètre [salaire] manquant")
    else:
        salaire = salaire.strip()
        match = re.match(r"^\d+", salaire)
        if not match:
            erreurs.append(f"paramétre salaire [{salaire}] invalide")
        else:
            salaire = int(salaire)

    #  invalid parameters in the URL?
    for key in request.args.keys():
        if key not in ['marié', 'enfants', 'salaire']:
            erreurs.append(f"paramètre [{key}] invalide")

    #  mistakes?
    if erreurs:
        #  an error response is sent to the client
        résultats = {"réponse": {"erreurs": erreurs}}
        return json_response(résultats, status.HTTP_400_BAD_REQUEST)

    #  no mistakes, we can work
    #  tAX CALCULATION
    taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, admindata)
    #  we send the response to the customer
    return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)


#  hand only
if __name__ == '__main__':
    #  start the Flask server
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • righe 1–10: recuperare il parametro che indica quale DBMS utilizzare;
  • righe 12–14: con queste informazioni, possiamo configurare l'applicazione. In particolare, viene costruito il Python Path;
  • righe 16–23: con il nuovo Python Path, importiamo i moduli necessari;
  • righe 25–31: recupero dei dati dall'autorità fiscale per calcolare l'imposta;
  • righe 33–34: istanziamento dell'applicazione Flask;
  • Riga 38: l'applicazione Flask serve solo l'URL [/]. Si aspetta un URL formattato come segue: [/ ?married=xx&children=yy&salary=zz], dove:
    • xx: sì / no;
    • yy: numero di figli;
    • zz: stipendio annuo;
  • righe 40–89: verifichiamo la validità dei parametri dell'URL;
  • riga 41: accumuleremo i messaggi di errore nell'elenco [errors];
  • riga 43: ricorderete che i parametri dell'URL si trovano in [request.args] (vedi |qui|):
    • l'oggetto [request] è l'oggetto Flask importato alla riga 20;
    • l'oggetto [request.args] si comporta come un dizionario;
  • righe 43–44: verifichiamo che ci siano esattamente tre parametri (né meno, né di più);
  • righe 46–49: controlliamo che il parametro [married] sia presente nell'URL;
  • righe 50–54: se è presente, controlliamo che il suo valore in minuscolo, privato degli spazi iniziali e finali, sia “yes” o “no”;
  • righe 56–59: controlliamo che il parametro [children] sia presente nell'URL;
  • righe 60–66: se presente, controlliamo che il suo valore sia un numero intero positivo;
  • riga 66: ricordiamo che i parametri URL e i loro valori sono stringhe. Il valore del parametro [children] viene convertito in un ‘int’;
  • righe 68–78: per il parametro [salary], eseguiamo gli stessi controlli effettuati per il parametro [children];
  • righe 81–83: verifichiamo che nell’URL non siano presenti parametri diversi da [‘married’, ‘children’, ‘salary’];
  • righe 85–89: se, dopo tutti questi controlli, l'elenco [errors] non è vuoto, inviamo questo elenco di errori al client come stringa JSON insieme al codice di stato [400 Bad Request];

Poiché in seguito avremo spesso bisogno di inviare una stringa JSON in risposta al client, le poche righe necessarie a questo scopo sono state inserite nel modulo [myutils.py] che abbiamo già utilizzato:

Image

Lo script [myutils.py] diventa il seguente:

#  imports
import json
import os
import sys

from flask import make_response


def set_syspath(absolute_dependencies: list):
    #  absolute_dependencies: a list of absolute folder names

    .


#  response generation HTTP jSON
def json_response(réponse: dict, status_code: int) -> tuple:
    #  response body HTTP
    response = make_response(json.dumps(réponse, ensure_ascii=False))
    #  response body HTTP is jSON
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    #  we send the HTTP response
    return response, status_code
  • Riga 16: La funzione [json_response] richiede due parametri:
    • [response]: il dizionario contenente la stringa JSON da inviare al client web;
    • [status_code]: il codice di stato HTTP della risposta;
  • riga 18: impostiamo il corpo JSON della risposta;
  • riga 20: aggiungiamo l'intestazione HTTP che indica al client web che riceverà JSON;
  • riga 22: inviamo la risposta HTTP al codice chiamante. Spetta al codice chiamante inviarla al client web;

Il file [__init__.py] cambia come segue:


from .myutils import set_syspath, json_response

La nuova versione di [myutils] viene installata tra i moduli a livello di macchina utilizzando il comando [pip install .] in un terminale PyCharm:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install .
Processing c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\packages
Using legacy setup.py install for myutils, since package 'wheel' is not installed.
Installing collected packages: myutils
  Attempting uninstall: myutils
    Found existing installation: myutils 0.1
    Uninstalling myutils-0.1:
      Successfully uninstalled myutils-0.1
    Running setup.py install for myutils ... done
Successfully installed myutils-0.1
  • Riga 1: Per inserire questo comando devi trovarti nella cartella [packages];

Il codice dello script [server_01] prosegue come segue:

    
    #  mistakes?
    if erreurs:
        #  an error response is sent to the client
        résultats = {"réponse": {"erreurs": erreurs}}
        return json_response(résultats, status.HTTP_400_BAD_REQUEST)

    #  no mistakes, we can work
    #  tAX CALCULATION
    taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, admindata)
    #  we send the response to the client
    return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)
  • riga 10: a questo punto, i parametri previsti nell'URL sono presenti e corretti;
  • riga 10: creiamo l'oggetto [TaxPayer] che modella il contribuente;
  • riga 11: chiediamo al livello [business] di calcolare l'imposta. Si noti che gli elementi calcolati dal livello [business] vengono inseriti nell'oggetto [taxpayer] passato come parametro;
  • riga 13: la risposta viene inviata al client web come stringa JSON. Si tratta della stringa JSON di un dizionario. Associato alla chiave [result], inseriamo il dizionario dell'oggetto [taxpayer]. Non abbiamo potuto inserire l'oggetto [taxpayer] stesso perché non è serializzabile in JSON;

Creiamo due configurazioni di esecuzione, una per MySQL e l'altra per PostgreSQL:

Image

Ecco alcuni esempi di esecuzione (avete avviato l'applicazione [server_01] e il DBMS, quindi avete richiesto l'URLhttp://localhost:5000/ utilizzando un browser):

Image

Image

Ecco un esempio della richiesta nella console di Postman:

Image


GET /?mari%C3%A9=xx&enfants=yy&salaire=zz HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: e4c5df8c-4bd6-4250-b789-b7b164db4eff
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
 
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 134
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 06:15:44 GMT
 
{"réponse": {"erreurs": ["paramètre marié [xx] invalide", "paramètre enfants [yy] invalide", "paramètre salaire [zz] invalide"]}}
  • riga 1: è stato richiesto un URL errato;
  • riga 10: il server risponde con lo stato 400 BAD REQUEST;

23.2.2. Versione 2

Image

La versione 2 del server isola l'elaborazione degli URL nel modulo [index_controller] [5]:

#  import dependencies
import re

from flask_api import status
from werkzeug.local import LocalProxy


#  URL set: /?married=xx&children=yy&salary=zz
def execute(request: LocalProxy, config: dict) -> tuple:
    #  dependencies
    from TaxPayer import TaxPayer

    #  initially no errors
    erreurs = []
    #  the query must have three parameters
    if len(request.args) != 3:
        erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")

    #  we retrieve the marital status of the URL
    marié = request.args.get('marié')
    if marié is None:
        erreurs.append("paramètre [marié] manquant")
    else:
        marié = marié.strip().lower()
        erreur = marié != "oui" and marié != "non"
        if erreur:
            erreurs.append(f"paramétre marié [{marié}] invalide")

    #  we retrieve the number of children in URL
    enfants = request.args.get('enfants')
    if enfants is None:
        erreurs.append("paramètre [enfants] manquant")
    else:
        enfants = enfants.strip()
        match = re.match(r"^\d+", enfants)
        if not match:
            erreurs.append(f"paramétre enfants {enfants} invalide")
        else:
            enfants = int(enfants)

    #  we recover the URL salary
    salaire = request.args.get('salaire')
    if salaire is None:
        erreurs.append("paramètre [salaire] manquant")
    else:
        salaire = salaire.strip()
        match = re.match(r"^\d+", salaire)
        if not match:
            erreurs.append(f"paramétre salaire {salaire} invalide")
        else:
            salaire = int(salaire)

    #  other parameters in the URL?
    for key in request.args.keys():
        if not key in ['marié', 'enfants', 'salaire']:
            erreurs.append(f"paramètre [{key}] invalide")

    #  mistakes?
    if erreurs:
        #  an error response is sent to the client
        résultats = {"réponse": {"erreurs": erreurs}}
        return résultats, status.HTTP_400_BAD_REQUEST

    #  no mistakes, we can work
    #  tAX CALCULATION
    taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, config["admindata"])
    #  we send the response to the customer
    return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
  • riga 9: la funzione [execute] riceve due parametri:
    • [request]: la richiesta HTTP del client;
    • [config]: il dizionario di configurazione dell'applicazione;

Lo script [server_02] è il seguente:

#  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 ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
import index_controller

#  data recovery from tax authorities
try:
    #  admindata will be read-only application data
    config['admindata'] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

#  flask application
app = Flask(__name__)


#  Home URL : /?married=xx&child=yy&salary=zz
@app.route('/', methods=['GET'])
def index():
    #  execute the query
    résultat, statusCode = index_controller.execute(request, config)
    #  we send the answer
    return json_response(résultat, statusCode)


#  hand only
if __name__ == '__main__':
    #  start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • righe 36–41: gestione della route /;
  • riga 39: utilizzo della funzione [IndexController.execute];

Ora useremo questa tecnica: ogni route sarà gestita dal proprio modulo.

I risultati dell'esecuzione sono gli stessi della versione 1.

23.2.3. Versione 3

La versione 3 introduce il concetto di autenticazione.

Lo script [server_03] diventa il seguente:

#  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 ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
from flask_httpauth import HTTPBasicAuth
import index_controller

#  data recovery from tax authorities
try:
    #  config['admindata'] will be read-only application data
    config["admindata"] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

#  authentication manager
auth = HTTPBasicAuth()


#  authentication method
@auth.verify_password
def verify_credentials(login: str, password: str) -> bool:
    #  user list
    users = config['users']
    #  browse this list
    for user in users:
        if user['login'] == login and user['password'] == password:
            return True
    #  we didn't find
    return False


#  flask application
app = Flask(__name__)


#  Home URL : /?married=xx&child=yy&salary=zz
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    #  execute the query
    résultat, statusCode = index_controller.execute(request, config)
    #  we send the answer
    return json_response(résultat, statusCode)


#  hand only
if __name__ == '__main__':
    #  start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • riga 21: importare un gestore di autenticazione. Esistono vari tipi di autenticazione per un server web. Quello che stiamo utilizzando qui si chiama [HTTP Basic]. Ogni tipo di autenticazione segue uno specifico dialogo client/server;
  • riga 33: crea un'istanza del gestore di autenticazione;
  • riga 37: l'annotazione [@auth.verify_password] indica la funzione da eseguire quando il gestore di autenticazione vuole verificare il nome utente e la password inviati dal client secondo il protocollo [HTTP Basic];
  • riga 55: l'annotazione [@auth.login_required] contrassegna una route per la quale il client web deve essere autenticato. Se il client web non ha ancora inviato le proprie credenziali, il server web le richiederà automaticamente utilizzando il protocollo HTTP Basic;

È necessario installare il modulo [flask_httpauth]:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\01\flask>pip install flask_httpauth
Collecting flask_httpauth
  Downloading Flask_HTTPAuth-4.1.0-py2.py3-none-any.whl (5.8 kB)
Requirement already satisfied: Flask in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from flask_httpauth) (1.1.2)
Requirement already satisfied: itsdangerous>=0.24 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (1.1.0)
Requirement already satisfied: click>=5.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (7.1.2)
Requirement already satisfied: Jinja2>=2.10.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (2.11.2)
Requirement already satisfied: Werkzeug>=0.15 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (1.0.1)
Requirement already satisfied: MarkupSafe>=0.23 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Jinja2>=2.10.1->Flask->flask_httpauth) (1.1.1
)
Installing collected packages: flask-httpauth
Successfully installed flask-httpauth-4.1.0

Vediamo cosa succede nella console di Postman. Tu:

  • crea una configurazione di esecuzione;
  • avvia l'applicazione web;
  • avvia il database che preferisci;
  • richiedi l'URL [/] con Postman;

Il dialogo client/server nella console di Postman è il seguente:

GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: e65e2a28-4fe3-423b-88b3-b3e5a83092b1
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 401 UNAUTHORIZED
Content-Type: text/html; charset=utf-8
Content-Length: 19
WWW-Authenticate: Basic realm="Authentication Required"
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 07:05:37 GMT

Unauthorized Access
  • Riga 10: Il server risponde che non siamo autorizzati ad accedere all'URL [/];
  • Riga 13: Ci indica quale protocollo di autenticazione utilizzare, in questo caso il protocollo di autenticazione Basic;

È possibile configurare Postman per inviare le credenziali utente secondo il protocollo di autenticazione Basic:

Image

  • nei campi [6-7] inseriamo le credenziali presenti nello script [config]: Image

    config['users'] = [
        {
            "login": "admin",
            "password": "admin"
        }
    ]

Il dialogo client/server nella console di Postman diventa il seguente:


GET / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 5ce20822-e87c-4eef-a2f4-b9eaec38d881
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
 
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 203
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 07:20:01 GMT
 
{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]", "paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
  • Riga 2: il client Postman invia le credenziali utente [admin / admin] in forma crittografata;
  • riga 17: il server risponde correttamente. Segnala degli errori perché i parametri [sposato, figli, stipendio] non sono stati inviati (riga 1), ma non segnala un errore di autenticazione;

Ora richiediamo l'URL / utilizzando un browser (Firefox qui sotto):

Image

  • Come con Postman, Firefox ha ricevuto la risposta HTTP dal server con le seguenti intestazioni HTTP:
1
2
3
4
HTTP/1.0 401 UNAUTHORIZED
WWW-Authenticate: Basic realm="Authentication Required"

Firefox, come altri browser, non interrompe la finestra di dialogo quando riceve queste intestazioni. Chiede all’utente le credenziali richieste dal server. Nell’esempio sopra riportato, basta digitare admin / admin per ricevere la risposta del server:

Image

23.3. Il client web del server di calcolo delle imposte

23.3.1. Introduzione

Nella sezione precedente, il client web per il server di calcolo delle imposte era un browser. In questa sezione, il client web sarà uno script da console. L'architettura diventa la seguente:

Image

  • il client web è costituito dai livelli [1-2];
  • il server web è costituito dai livelli [3-9]. Come menzionato nella sezione precedente;

dobbiamo quindi scrivere i livelli [1-2].

Il livello [dao] [2] deve essere in grado di comunicare con il server web [3]. Ora che comprendiamo il protocollo HTTP, potremmo scrivere, utilizzando ad esempio il modulo [pycurl] che abbiamo già studiato, uno script che comunichi con il server web [3]. Tuttavia, esistono moduli specializzati nella comunicazione client/server HTTP. Ne useremo uno, il modulo [requests]:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\01\flask>pip install requests
Collecting requests
  Downloading requests-2.24.0-py2.py3-none-any.whl (61 kB)
     || 61 kB 137 kB/s
Collecting idna<3,>=2.5
  Downloading idna-2.10-py2.py3-none-any.whl (58 kB)
     || 58 kB 692 kB/s
Collecting chardet<4,>=3.0.2
  Downloading chardet-3.0.4-py2.py3-none-any.whl (133 kB)
     || 133 kB 1.3 MB/s
Collecting urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1
  Downloading urllib3-1.25.9-py2.py3-none-any.whl (126 kB)
     || 126 kB 1.1 MB/s
Collecting certifi>=2017.4.17
  Downloading certifi-2020.6.20-py2.py3-none-any.whl (156 kB)
     || 156 kB 1.1 MB/s
Installing collected packages: idna, chardet, urllib3, certifi, requests
Successfully installed certifi-2020.6.20 chardet-3.0.4 idna-2.10 requests-2.24.0 urllib3-1.25.9

La struttura delle directory per gli script del client web è la seguente:

Image

Lo script implementerà l'applicazione per il calcolo delle imposte in modalità batch descritta nella |versione 1|. L'ultima versione di questa applicazione è la |versione 5|. Ecco un promemoria di come funziona:

  • i contribuenti per i quali verrà calcolata l'imposta sono elencati nel file di testo [taxpayersdata.txt]:
# données valides : id, marié, enfants, salaire
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# on crée des lignes erronées
# pas assez de valeurs
11,12
# des valeurs erronées
x,x,x,x
  • I risultati vengono salvati in due file:
  • Il file di testo [errors.txt] elenca gli errori rilevati nel file del contribuente:

Analyse du fichier C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-clients\01\main/../data/input/taxpayersdata.txt
 
Ligne 15, not enough values to unpack (expected 4, got 2)
Ligne 17, MyException[1, L'identifiant d'une entité <class 'TaxPayer.TaxPayer'> doit être un entier >=0]
  • (continua)
    • Il file JSON [results.json] contiene i risultati del calcolo delle imposte per i vari contribuenti:

[
  {
    "id": 0,
    "marié": "oui",
    "enfants": 2,
    "salaire": 55555,
    "impôt": 2814,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 0,
    "réduction": 0
  },
  {
    "id": 1,
    "marié": "oui",
    "enfants": 2,
    "salaire": 50000,
    "impôt": 1384,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 384,
    "réduction": 347
  },

]

23.3.2. Configurazione del client web

Image

La configurazione viene eseguita utilizzando due script:

  • [config], che gestisce tutte le configurazioni al di fuori dei livelli dell'architettura;
  • [config_layers], che gestisce la configurazione dei livelli dell'architettura;

Lo script [config] è il seguente:

def configure(config: dict) -> dict:
    import os

    #  step 1 ------

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

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

    #  absolute dependencies
    absolute_dependencies = [
        #  project files
        #  BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        #  InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        #  AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        #  ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        #  AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        #  configuration scripts
        script_dir,
    ]

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

    #  step 2 ------
    #  application configuration with constants
    config.update({
        "taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
        "resultsFilename": f"{script_dir}/../data/output/résultats.json",
        "errorsFilename": f"{script_dir}/../data/output/errors.txt",
        "server": {
            "urlServer": "http://127.0.0.1:5000/",
            "authBasic": True,
            "user": {
                "login": "admin",
                "password": "admin"
            }
        }
    }
    )

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

    #  we return the configuration
    return config
  • riga 1: la funzione [configure] accetta come parametro il dizionario da riempire con le informazioni di configurazione. Questo dizionario può essere già precompilato o vuoto. In questo caso, sarà vuoto;
  • righe 40–42: i percorsi assoluti dei tre file di testo gestiti dal livello [dao];
  • righe 43-50: associate alla chiave [server], le informazioni che il livello [dao] deve conoscere sul server web con cui deve comunicare:
    • riga 44: l'URL del servizio web;
    • riga 45: la chiave [authBasic] è impostata su True se l'accesso all'URL richiede l'autenticazione Basic;
    • righe 46–49: le credenziali dell'utente che effettuerà l'autenticazione se richiesta;
  • righe 56–57: istanziamo i livelli — in questo caso, il singolo livello [dao] — e inseriamo i riferimenti ai livelli in [config] sotto la chiave [layers];

Lo script [config_layers] è il seguente:

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

    #  dao layer
    from ImpôtsDaoWithHttpClient import ImpôtsDaoWithHttpClient
    dao = ImpôtsDaoWithHttpClient(config)

    #  make the layer configuration
    return {
        "dao": dao
    }
  • Riga 1: la funzione [configure] riceve il dizionario che configura l'applicazione;
  • righe 4–6: viene istanziato il livello [dao]. Alla riga 6, gli passiamo la configurazione dell'applicazione, dove troverà le informazioni di cui ha bisogno;
  • righe 8–11: viene restituito un dizionario contenente il riferimento al livello [dao];

23.3.3. Lo script principale [main]

Lo script principale [main] è una variante di quello della |versione 5|:

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

#  dependencies
from ImpôtsError import ImpôtsError

#  code
try:
    #  we recover the [dao] layer
    dao = config["layers"]["dao"]
    #  reading taxpayer data
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #  taxpayers?
    if not taxpayers:
        raise ImpôtsError(f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #  tax calculation
    for taxpayer in taxpayers:
        #  taxpayer is both an input and output parameter
        #  taxpayer will be modified
        dao.calculate_tax(taxpayer)
    #  writing results to a text file
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError  as erreur:
    #  error display
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  completed
    print("Travail terminé...")
  • righe 2-3: l'applicazione è configurata;
  • riga 13: il livello [dao] fornisce l'elenco dei contribuenti per i quali devono essere calcolate le imposte;
  • riga 21: il livello [dao] calcola l'imposta per ciascuno di essi;
  • riga 23: i risultati vengono salvati in un file JSON;

23.3.4. Implementazione del livello [dao]

Image

Rivediamo l'architettura client/server utilizzata:

Image

  • in [2, 6], vediamo che il livello [dao] ha due ruoli:
    • accede al file system sia per leggere i dati dei contribuenti sia per scrivere i risultati dei calcoli fiscali. Abbiamo già una classe |AbstractImpôtsDao| in grado di farlo. È in uso dalla |versione 4|;
    • comunica con il server web [3];

Nella |versione 5|, lo script principale [main] [1] comunicava direttamente con il livello [business] [4]. Preferiremmo non modificare questo script. Per ottenere questo risultato, faremo in modo che il livello [DAO] [2] implementi l'interfaccia del livello [business] [4]. In questo modo, lo script principale [main] sembrerà comunicare direttamente con il livello [business] [4] e potrà ignorare completamente il fatto che si trovi su un'altra macchina.

Una definizione della classe che implementa il livello [DAO] [2] potrebbe essere la seguente:


class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
  • La classe [TaxDaoWithHttpClient]:
    • eredita dalla classe [AbstractTaxDao], il che le permette di gestire la comunicazione con il file system [6];
    • implementa l'interfaccia [InterfaceImpôtsMétier] in modo da non dover modificare lo script principale [main] della |versione 5|;

Il codice completo della classe [TaxDaoWithHttpClient] è il seguente:

#  imports
import requests
from flask_api import status

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsMétier import InterfaceImpôtsMétier
from TaxPayer import TaxPayer


class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):

    #  manufacturer
    def __init__(self, config: dict):
        #  parent initialization
        AbstractImpôtsDao.__init__(self, config)
        #  parameter memory
        self.__config_server = config["server"]

    #  unused [AbstractImpôtsDao] method
    def get_admindata(self) -> AdminData:
        pass

    #  tAX CALCULATION
    def calculate_tax(self: object, taxpayer: TaxPayer, admindata: AdminData = None):
        #  we let the exceptions rise
        #  get parameters
        params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
        #  connection with Auth Basic authentication?
        if self.__config_server['authBasic']:
            response = requests.get(
                #  URL of the queried server
                self.__config_server['urlServer'],
                #  URL parameters
                params=params,
                #  basic authentication
                auth=(
                    self.__config_server["user"]["login"],
                    self.__config_server["user"]["password"]))
        else:
            #  connection without Auth Basic authentication
            response = requests.get(self.__config_server['urlServer'], params=params)
        #  check
        print(response.text)
        #  response status code HTTP
        status_code = response.status_code
        #  we put the response jSON in a dictionary
        résultat = response.json()
        #  error if status code other than 200 OK
        if status_code != status.HTTP_200_OK:
            #  we know that the errors have been associated with the [errors] key in the response
            raise ImpôtsError(87, résultat['réponse']['erreurs'])
        #  we know that the result has been associated with the [result] key in the response
        #  modify the input parameter with this result
        taxpayer.fromdict(résultat["réponse"]["result"])
  • righe 21–23: la classe [AbstractTaxDao] (riga 12) ha un metodo astratto [get_admindata]. Siamo tenuti a implementarlo anche se non lo utilizziamo (admindata è gestito dal server, non dal client);
  • riga 26: il metodo [calculate_tax] appartiene all'interfaccia [InterfaceImpôtsMétier] (riga 12). Dobbiamo implementarlo;
  • riga 15: il costruttore riceve il dizionario di configurazione dell'applicazione come unico parametro;
  • righe 16–17: la classe padre [AbstractTaxDao] viene inizializzata passandole, anche in questo caso, la configurazione dell'applicazione. Qui troverà i nomi dei tre file di testo che deve gestire;
  • righe 18–19: le informazioni relative al server web di calcolo delle imposte sono memorizzate localmente all'interno della classe;
  • riga 26: il metodo [calculate_tax] riceve come parametro un oggetto di tipo |Taxpayer|. Per rispettare la firma del metodo [InterfaceImpôtsMétier.calculate_tax], riceve anche un parametro [admindata], che dovrebbe incapsulare i dati dell'amministrazione fiscale. Sul lato client, non disponiamo di questi dati. Questo parametro rimarrà sempre [None]. Questa soluzione alternativa suggerisce che la classe [ImpôtsMétier] fosse inizialmente mal progettata:
  • la firma di [calculate_tax] avrebbe dovuto essere semplicemente:

def calculate_tax(self, taxpayer: TaxPayer)

e il parametro [admindata: AdminData] avrebbe dovuto essere passato al costruttore della classe;

  • riga 27: il codice del metodo [calculate_tax] non è stato racchiuso in un blocco try / catch / finally. Ciò significa che eventuali eccezioni non verranno gestite e saranno propagate al codice chiamante, in questo caso lo script [main]. Questo script intercetta tutte le eccezioni propagate dal livello [dao];
  • Riga 28: il calcolo dell'imposta viene eseguito sul lato server. Dovremo quindi comunicare con esso. Lo facciamo utilizzando il modulo [requests] importato alla riga 2;
  • righe 31–43: per inviare una richiesta GET al server web, utilizziamo il metodo [requests.get]:
    • righe 33–34: il primo parametro del metodo è l'URL da contattare;
    • righe 35–40: gli altri due parametri sono parametri denominati il cui ordine non ha importanza;
    • righe 35-36: il valore del parametro denominato [params] deve essere un dizionario contenente le informazioni da includere nell'URL nella forma [/url?param1=value1&param2=value2&…];
    • riga 29: il dizionario contenente i tre parametri [married, children, salary] che il server web si aspetta. Non dobbiamo preoccuparci della codifica (chiamata urlencoded) a cui questi parametri devono essere sottoposti. [requests] se ne occupa;
    • righe 37–40: il parametro denominato [auth] è una tupla di due elementi (login, password). Rappresenta le credenziali per l'autenticazione Basic;
  • righe 44–45: queste due righe hanno solo scopo didattico (le commenteremo una volta completato il debug):
    • [response] rappresenta la risposta HTTP del server;
    • [response.text] rappresenta il testo del documento contenuto in questa risposta. Durante il debug, è utile verificare cosa ci ha inviato il server;
  • riga 47: [response.status_code] è il codice di stato HTTP della risposta ricevuta. Il nostro server ne invia solo tre:
    • 200 OK
    • 400 RICHIESTA NON VALIDA
    • 500 ERRORE INTERNO DEL SERVER
  • riga 49: il nostro server invia sempre JSON, anche in caso di errore. La funzione [response.json()] crea un dizionario dalla stringa JSON ricevuta. Esaminiamo le due possibili forme della stringa JSON:

{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]", "paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
{"réponse": {"result": {"id": 0, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
  • righe 51–53: se il codice di stato non è 200, viene generata un'eccezione con i messaggi di errore inclusi nella risposta;
  • riga 56: recupera il dizionario generato dal calcolo delle imposte e lo utilizza per aggiornare il parametro di input [contribuente];

23.3.5. Esecuzione

Per eseguire il client:

  • avviare il server [server_03] con il DBMS di propria scelta;
  • eseguire lo script [main] del client;

I risultati si trovano nella cartella [data/output]. Sono gli stessi della versione 5.

23.4. Test del livello [dao]

Torniamo all'architettura dell'applicazione client/server:

Image

  • nel codice client, ci siamo assicurati che il livello [dao] [1] fornisca la stessa interfaccia del livello [business] [3]. Utilizzeremo quindi la classe di test |TestDaoMétier|, che abbiamo studiato in precedenza, per testare il livello [business] [3];

La classe di test verrà eseguita nel seguente ambiente:

Image

  • La configurazione [2] è identica alla configurazione [1], che abbiamo appena esaminato;

La classe di test [TestHttpClientDao] è la seguente:

import unittest


class TestHttpClientDao(unittest.TestCase):

    def test_1(self) -> None:
        from TaxPayer import TaxPayer

        #  { 'married': 'yes', 'children': 2, 'salary': 55555,
        #  tax': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        #  check
        self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    

    def test_11(self) -> None:
        from TaxPayer import TaxPayer

        #  { 'married': 'yes', 'children': 3, 'salary': 200000,
        #  tax': 42842, 'surcôte': 17283, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        dao.calculate_tax(taxpayer)
        #  checks
        self.assertAlmostEqual(taxpayer.impôt, 42842, 1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 17283, delta=1)


if __name__ == '__main__':

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

    #  dao layer
    dao = config['layers']['dao']

    #  test methods are executed
    print("tests en cours...")
    unittest.main()

Questa classe è simile a quella già studiata nella versione 4 dell'applicazione.

  • righe 40-41: configurazione dell'ambiente di test;
  • riga 44: recuperiamo un riferimento al livello [DAO];
  • righe 47-48: eseguiamo i test;

Per eseguire i test, creiamo una |configurazione di esecuzione|:

Image

  • Creiamo una configurazione di esecuzione per uno script da console, non per un UnitTest;

Quando si esegue questa configurazione, si ottengono i seguenti risultati:

C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-clients/01/tests/TestHttpClientDao.py
tests en cours...
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
....{"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}}}
...{"réponse": {"result": {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}}}
....
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}}}
----------------------------------------------------------------------
Ran 11 tests in 0.130s

OK

Process finished with exit code 0

Tutti gli 11 test sono stati superati.