Skip to content

23. Esercizio pratico: versione 6

23.1. Introduzione

Torniamo ora alla nostra applicazione per il calcolo delle imposte. Costruiremo attorno ad essa diverse applicazioni web.

Nella versione 5 del nostro esercizio pratico, i dati dell’amministrazione fiscale erano archiviati in un database. Questa versione 5 comprendeva due applicazioni distinte ma con livelli in comune:

  • un’applicazione che calcolava l’imposta in modalità |batch| per i contribuenti registrati in un file di testo;
  • un’applicazione che calcolava l’imposta in modalità |interattiva| per i contribuenti le cui informazioni venivano inserite tramite tastiera;

La versione 5 dell’applicazione per il calcolo dell’imposta in modalità batch presentava la seguente architettura:

Image

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

Image

  • il client web [1] si rivolge al server web [2], il quale a sua volta comunica con SGBD e [3];
  • il server web [2] mantiene i livelli [métier], [8] e [dao], [9] dell’applicazione iniziale;
  • L'applicazione iniziale mantiene il proprio script principale [4] e i propri livelli [métier] e [15]. I livelli [métier], [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], client dell’applicazione web [7];

Nella versione finale, il calcolo dell’imposta in batch potrà avvenire in due modi:

  • il calcolo business dell’imposta viene effettuato dal livello [métier] del server. Lo script [main] utilizzerà questo metodo;
  • il calcolo dell’imposta viene effettuato dal livello [métier] 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 dell’imposta

23.2.1. Versione 1

Image

Lo script [server_01] è la seguente applicazione web:

Image

  • in [1] si utilizza una URL configurata in cui vengono passati tre valori:
    • [marié] (sì / no) per indicare se il contribuente è coniugato;
    • [enfants]: il numero di figli del contribuente;
    • [salaire]: lo stipendio annuale del contribuente;
  • in [2], il server web restituisce una stringa jSON che indica l’importo dell’imposta da pagare con le sue diverse 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 per il calcolo delle imposte. Li riprendiamo così come sono;
    • il livello [métier] [3] è definito |qui|;
    • il livello [dao] [4] è definito |qui|;

L’applicazione web [server_01] è configurata tramite tre script:

  • [config], che configura l’intera applicazione;
  • [config_database], che configura l’accesso al database. Si lavorerà con SGBD, MySQL e PostgreSQL;
  • [config_layers], che configura i livelli dell’applicazione;

Lo script [config] è il seguente:


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

    # fase 1 ------
    # cartella di questo file
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # percorso radice
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    # dipendenze assolute
    absolute_dependencies = [
        # cartelle del progetto
        # 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",
        # Costanti, fasce
        f"{root_dir}/impots/v05/entities",
        # IndexController
        f"{script_dir}/../controllers",
        # script [config_database, config_layers]
        script_dir,
    ]
    # si imposta il syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # fase 2 ------
    # configurazione dell'applicazione
    # elenco degli utenti autorizzati a utilizzare l'applicazione
    config['users'] = [
        {
            "login""admin",
            "password""admin"
        }
    ]

    # fase 3 ------
    # configurazione del database
    import config_database
    config["database"] = config_database.configure(config)

    # fase 4 ------
    # istanziazione dei livelli dell'applicazione
    import config_layers
    config['layers'] = config_layers.configure(config)

    # si esegue la configurazione
    return config
  • La funzione [configure] riceve come parametro un dizionario [config] (riga 1) e lo restituisce come risultato (riga 54) dopo averne arricchito il contenuto. Si sarebbe potuto dire già da tempo che non era necessario restituire il risultato [config]. Infatti, [config] è un riferimento al dizionario che il codice chiamante condivide con il codice chiamato. Il codice chiamante possiede quindi già questo riferimento (riga 1) ed è inutile restituirglielo nuovamente (riga 54). Pertanto, scrivere:

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

è ridondante. È sufficiente scrivere:


[module].configure(config) (2)

Tuttavia, ho mantenuto il primo tipo di scrittura perché ho ritenuto che illustrasse meglio il fatto che il codice chiamato modificasse il dizionario [config].

  • riga 1: il dizionario [config] ricevuto dalla funzione [configure] ha una chiave ‘sgbd’ il cui valore è tratto dall’elenco [‘mysql’, ‘pgres’]. [mysql] indica che il database utilizzato è gestito da MySQL, mentre ‘pgres’ indica che il database utilizzato è gestito da PostgreSQL;
  • righe 4-27: vengono elencate tutte le cartelle contenenti gli elementi necessari all’applicazione web. Queste faranno parte del Python Path dell’applicazione (righe 30-31);
  • righe 33-40: si consentirà l’accesso all’applicazione solo ad alcuni utenti. In questo caso si ha un elenco con un unico utente;
  • righe 43-46: è lo script [config_database] che crea la configurazione del database utilizzato;
  • riga 46: la configurazione creata dallo script [config_database] è un dizionario che viene inserito nella configurazione generale associato alla chiave ‘database’;
  • righe 48-51: lo script [config_layers] istanzia i livelli dell’applicazione web. Restituisce un dizionario che viene inserito nella configurazione generale associato alla chiave ‘layers’;

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


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

    # stringhe di connessione ai database utilizzati
    connection_strings = {
        'mysql': "mysql+mysqlconnector://admimpots:mdpimpots@localhost/dbimpots-2019",
        'pgres': "postgresql+psycopg2://admimpots:mdpimpots@localhost/dbimpots-2019"
    }
    # stringa di connessione al database in uso
    engine = create_engine(connection_strings[config['sgbd']])

    # metadati
    metadata = MetaData()

    # la tabella delle costanti
    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)
                             )

    # tabella delle fasce d'imposta
    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)
                           )
    # mappature
    from Tranche import Tranche
    mapper(Tranche, tranches_table)

    from Constantes import Constantes
    mapper(Constantes, constantes_table)

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

    # una sessione
    session = session_factory()

    # si salvano alcune informazioni e le si restituiscono in un dizionario
    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. Riprendiamo uno |script| già visto in precedenza:


def configure(config: dict) -> dict:
    # istanziazione dei livelli dell'applicazione
    
    # DAO
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    dao = ImpotsDaoWithAdminDataInDatabase(config)
    
    # logica di business
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    # si inseriscono le istanze dei livelli in un dizionario che viene restituito al codice chiamante
    return {
        "dao": dao,
        "métier": métier
    }
  • riga 6: il livello [dao] è implementato con un database;
  • [ImpotsDaoWithAdminDataInDatabase] è stato definito |qui|;
  • [ImpôtsMétier] è stato definito |qui|;

Lo script principale [server_01] è il seguente:


# si attende un parametro mysql o pgres
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()

# si configura l'applicazione
import config
config = config.configure({'sgbd': sgbd})

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

# recupero dei dati dall'amministrazione fiscale
try:
    # admindata sarà un dato a livello di applicazione in sola lettura
    admindata = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# Applicazione Flask
app = Flask(__name__)


# Home URL: /?sposato=xx&figli=yy&stipendio=zz
@app.route('/', methods=['GET'])
def index():
    # inizialmente nessun errore
    erreurs = []
    # la richiesta deve avere tre parametri in URL
    if len(request.args) != 3:
        erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")

    # si recupera lo stato civile 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")

    # si recupera il numero di figli dall’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)

    # si recupera lo stipendio dal file 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)

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

    # Ci sono degli errori?
    if erreurs:
        # si invia una risposta di errore al cliente
        résultats = {"réponse": {"erreurs": erreurs}}
        return json_response(résultats, status.HTTP_400_BAD_REQUEST)

    # nessun errore, si può procedere
    # calcolo dell'imposta
    taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, admindata)
    # si invia la risposta al cliente
    return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)


# solo modalità manuale
if __name__ == '__main__':
    # si avvia il server Flask
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • righe 1-10: si recupera il parametro che indica quale SGBD utilizzare;
  • righe 12-14: con queste informazioni è possibile configurare l’applicazione. In particolare, viene creato il Python Path;
  • righe 16-23: con il nuovo Python Path, si importano gli elementi necessari;
  • righe 25-31: si recuperano i dati dell’amministrazione fiscale che consentono di calcolare l’imposta;
  • righe 33-34: istanziamento dell’applicazione Flask;
  • riga 38: l’applicazione Flask gestisce solo il URL [/]. Si aspetta un URL configurato come segue [/ ?marié=xx&enfants=yy&salaire=zz] con:
    • xx: sì / no;
    • yy: numero di figli;
    • zz: stipendio annuo;
  • righe 40-89: si verifica la validità dei parametri di URL;
  • riga 41: si accumulano i messaggi di errore nell’elenco [erreurs];
  • riga 43: si ricorda che i parametri dell'oggetto URL configurato 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: si verifica che i parametri siano esattamente tre (né meno, né di più);
  • righe 46-49: si verifica che il parametro [marié] sia presente in URL;
  • righe 50-54: se è presente, si verifica che il suo valore in minuscolo, privato degli spazi iniziali e finali, sia «sì» o «no»;
  • righe 56-59: si verifica che il parametro [enfants] sia presente nel file URL;
  • righe 60-66: se presente, si verifica che il suo valore sia un numero intero positivo;
  • riga 66: non bisogna dimenticare che i parametri di URL e i relativi valori sono stringhe di caratteri. Il valore del parametro [enfants] viene convertito in ‘int’;
  • righe 68-78: per il parametro [salaire], si eseguono gli stessi controlli effettuati per il parametro [enfants];
  • righe 81-83: si verifica che non vi siano parametri diversi da [‘marié, ‘enfants’, ‘salaire’] all’interno di URL;
  • righe 85-89: se, dopo tutte queste verifiche, l’elenco [erreurs] non è vuoto, si invia tale elenco di errori al cliente sotto forma di una stringa jSON e del codice di stato [400 Bad Request];

Poiché in seguito avremo spesso occasione di inviare una stringa jSON in risposta al cliente, le poche righe necessarie a tale invio sono state raggruppate nel modulo [myutils.py] che abbiamo già utilizzato:

Image

Lo script [myutils.py] diventa il seguente:


# importazioni
import json
import os
import sys

from flask import make_response


def set_syspath(absolute_dependencies: list):
    # absolute_dependencies: un elenco di nomi assoluti di cartelle

    ….


# generazione di una risposta HTTP jSON
def json_response(réponse: dict, status_code: int) -> tuple:
    # corpo della risposta HTTP
    response = make_response(json.dumps(réponse, ensure_ascii=False))
    # corpo della risposta HTTP è di jSON
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    # si invia la risposta HTTP
    return response, status_code
  • riga 16: la funzione [json_response] richiede due parametri:
    • [réponse]: il dizionario di cui occorre inviare la stringa jSON al client web;
    • [status_code]: il codice di stato HTTP della risposta;
  • riga 18: si imposta il corpo jSON della risposta;
  • riga 20: si aggiunge l'intestazione HTTP che indica al client web che riceverà jSON;
  • riga 22: si invia la risposta HTTP al codice chiamante. Spetta a quest’ultimo inviarla al client web;

Il file [__init__.py] si evolve come segue:


from .myutils import set_syspath, json_response

La nuova versione di [myutils] viene installata tra i moduli a livello di macchina con 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 digitare questa istruzione è necessario trovarsi nella cartella [packages];

Il codice dello script [server_01] prosegue come segue:



    # Ci sono errori?
    if erreurs:
        # si invia una risposta di errore al cliente
        résultats = {"réponse": {"erreurs": erreurs}}
        return json_response(résultats, status.HTTP_400_BAD_REQUEST)

    # nessun errore, si può procedere
    # calcolo dell'imposta
    taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, admindata)
    # si invia la risposta al cliente
    return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)

  • riga 10: a questo punto, i parametri previsti nello script URL sono presenti e corretti;
  • riga 10: si crea l’oggetto [TaxPayer] che rappresenta il contribuente;
  • riga 11: si richiede al livello [métier] di calcolare l’imposta. Si ricorda che gli elementi calcolati dal livello [métier] vengono inseriti nell’oggetto [taxpayer] passato come parametro;
  • riga 13: la risposta viene inviata al client web sotto forma di stringa jSON. Si tratta della stringa jSON di un dizionario. Associato alla chiave [result], vi si inserisce il dizionario dell’oggetto [taxpayer]. Non è stato possibile inserire l’oggetto [taxpayer] stesso poiché non è serializzabile in jSON;

Si creano 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 quella SGBD, quindi richiedete l’URL http://localhost:5000/ con un browser):

Image

Image

Ecco un esempio di esecuzione 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: viene richiesto un URL non corretto;
  • 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 di URL nel modulo [index_controller] [5]:


# importazione delle dipendenze
import re

from flask_api import status
from werkzeug.local import LocalProxy


# URL con i seguenti parametri: /?sposato=xx&figli=yy&stipendio=zz
def execute(request: LocalProxy, config: dict) -> tuple:
    # persone a carico
    from TaxPayer import TaxPayer

    # inizialmente nessun errore
    erreurs = []
    # la richiesta deve avere tre parametri
    if len(request.args) != 3:
        erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")

    # si recupera lo stato civile di 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")

    # si recupera il numero di figli di 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)

    # si recupera lo stipendio dell'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)

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

    # Ci sono errori?
    if erreurs:
        # si invia una risposta di errore al cliente
        résultats = {"réponse": {"erreurs": erreurs}}
        return résultats, status.HTTP_400_BAD_REQUEST

    # nessun errore, si può procedere
    # calcolo dell'imposta
    taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, config["admindata"])
    # si invia la risposta al cliente
    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:


# si attende un parametro mysql o pgres
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()

# si configura l'applicazione
import config
config = config.configure({'sgbd': sgbd})

# dipendenze
from ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
import index_controller

# recupero dei dati dall'amministrazione fiscale
try:
    # admindata sarà un dato a livello di applicazione in sola lettura
    config['admindata'] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# Applicazione Flask
app = Flask(__name__)


# Home URL: /?marié=xx&enfant=yy&salaire=zz
@app.route('/', methods=['GET'])
def index():
    # si esegue la richiesta
    résultat, statusCode = index_controller.execute(request, config)
    # si invia la risposta
    return json_response(résultat, statusCode)


# solo main
if __name__ == '__main__':
    # si avvia il server
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • righe 36-41: elaborazione della strada / ;
  • riga 39: utilizzo della funzione [IndexController.execute];

D'ora in poi utilizzeremo questa tecnica: ogni percorso sarà elaborato da un modulo a esso dedicato.

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

23.2.3. Versione 3

Image

La versione 3 introduce il concetto di autenticazione.

Lo script [server_03] diventa il seguente:


# si attende un parametro mysql o pgres
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()

# si configura l'applicazione
import config
config = config.configure({'sgbd': sgbd})

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

# recupero dei dati dall'amministrazione fiscale
try:
    # config[‘admindata’] sarà un dato a livello di applicazione in sola lettura
    config["admindata"] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# gestore di autenticazione
auth = HTTPBasicAuth()


# metodo di autenticazione
@auth.verify_password
def verify_credentials(login: str, password: str) -> bool:
    # elenco degli utenti
    users = config['users']
    # si scorre questo elenco
    for user in users:
        if user['login'] == login and user['password'] == password:
            return True
    # non trovato
    return False


# applicazione Flask
app = Flask(__name__)


# Home URL: /?sposato=xx&figlio=yy&stipendio=zz
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    # si esegue la richiesta
    résultat, statusCode = index_controller.execute(request, config)
    # si invia la risposta
    return json_response(résultat, statusCode)


# solo main
if __name__ == '__main__':
    # si avvia il server
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • riga 21: si importa un gestore di autenticazione. Esistono diversi tipi di autenticazione presso un server web. Quello che utilizziamo qui si chiama [HTTP Basic]. Ogni tipo di autenticazione segue un preciso dialogo client/server;
  • riga 33: si crea un'istanza del gestore di autenticazione;
  • riga 37: l’annotazione [@auth.verify_password] contrassegna la funzione da eseguire quando il gestore di autenticazione deve 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 secondo 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 con la console Postman. È necessario:

  • crea una configurazione di esecuzione;
  • avviate l’applicazione web;
  • avviate il SGBD di vostra scelta;
  • richiedete 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 a URL [/];
  • riga 13: ci indica il protocollo di autenticazione da utilizzare, in questo caso il protocollo denominato Autenticazione Basic;

È possibile configurare Postman in modo che invii le credenziali dell’utente secondo il protocollo Auth Basic:

Image

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

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

Il dialogo client/server nella console 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 in forma codificata le credenziali dell’utente [admin / admin];
  • riga 17: il server risponde correttamente. Segnala degli errori perché non sono stati inviati i parametri [marié, enfants, salaire] (riga 1), ma non segnala alcun errore di autenticazione;

Ora proviamo a richiedere URL / con un browser (Firefox, come mostrato di seguito):

Image

  • come con Postman, Firefox ha ricevuto dal server la risposta HTTP con le 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. È sufficiente digitare admin / admin per ricevere la risposta dal server:

Image

23.3. Il client web del server di calcolo delle imposte

23.3.1. Introduzione

Nel paragrafo precedente, il client web del server di calcolo delle imposte era un browser. In questa parte, 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 già indicato nel paragrafo precedente;

Dobbiamo quindi scrivere i livelli [1-2].

Il livello [dao] [2] deve essere in grado di comunicare con il server web [3]. Ora conosciamo il protocollo HTTP e potremmo scrivere, ad esempio utilizzando il modulo [pycurl] già studiato, uno script in grado di comunicare con il server web [3]. Tuttavia, esistono moduli specializzati nelle interazioni 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 degli script del client web è la seguente:

Image

Lo script implementerà l’applicazione per il calcolo delle imposte in modalità batch descritta a partire dalla |versione 1|. L’ultima versione di questa applicazione è la |versione 5|. Ricordiamo il suo funzionamento:

  • i contribuenti per i quali verrà calcolata l’imposta sono raccolti nel file di testo [taxpayersdata.txt]:
# dati validi: id, stato civile, figli, stipendio
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
# si creano righe errate
# valori insufficienti
11,12
# valori errati
x,x,x,x
  • i risultati vengono salvati in due file:
  • il file di testo [errors.txt] raccoglie gli errori rilevati nel file dei contribuenti:

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 [résultats.json] raccoglie i risultati dei calcoli dell’imposta dei diversi 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 effettuata tramite due script:

  • [config], che gestisce l’intera configurazione 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

    # fase 1 ------

    # cartella di questo file
    script_dir = os.path.dirname(os.path.abspath(__file__))

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

    # dipendenze assolute
    absolute_dependencies = [
        # cartelle del progetto
        # 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",
        # Costanti, fasce
        f"{root_dir}/impots/v05/entities",
        # ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        # script di configurazione
        script_dir,
    ]

    # impostazione del syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # fase 2 ------
    # configurazione dell'applicazione con le costanti
    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"
            }
        }
    }
    )

    # fase 3 ------
    # istanziazione dei livelli
    import config_layers
    config['layers'] = config_layers.configure(config)

    # si applica la configurazione
    return config
  • riga 1: la funzione [configure] riceve come parametro il dizionario da compilare con le informazioni di configurazione. Questo può essere già precompilato o vuoto. In questo caso, sarà vuoto;
  • righe 40-42: i nomi 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 a URL richiede un’autenticazione di tipo Basic;
    • righe 46-49: le credenziali dell’utente che si autenticherà se viene richiesta l’autenticazione;
  • righe 56-57: si istanziano i livelli, in questo caso l’unico livello [dao], e si inseriscono i riferimenti dei livelli in [config] associati alla chiave [layers];

Lo script [config_layers] è il seguente:


def configure(config: dict) -> dict:
    # istanziazione dei livelli dell'applicazione

    # livello DAO
    from ImpôtsDaoWithHttpClient import ImpôtsDaoWithHttpClient
    dao = ImpôtsDaoWithHttpClient(config)

    # si esegue la configurazione dei livelli
    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 viene passata la configurazione dell'applicazione, nella quale troverà le informazioni di cui ha bisogno;
  • righe 8-11: viene restituito un dizionario in cui è stato inserito il riferimento al livello [dao];

23.3.3. Lo script principale [main]

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


# si configura l'applicazione
import config
config = config.configure({})

# dipendenze
from ImpôtsError import ImpôtsError

# codice
try:
    # si recupera il livello [dao]
    dao = config["layers"]["dao"]
    # lettura dei dati dei contribuenti
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # dei contribuenti?
    if not taxpayers:
        raise ImpôtsError(f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # calcolo dell'imposta dei contribuenti
    for taxpayer in taxpayers:
        # «contribuente» è sia un parametro di input che di output
        # il contribuente verrà modificato
        dao.calculate_tax(taxpayer)
    # scrittura dei risultati in un file di testo
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError  as erreur:
    # visualizzazione dell'errore
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # completato
    print("Travail terminé...")
  • righe 2-3: l’applicazione è configurata;
  • riga 13: il livello [dao] fornisce l’elenco dei contribuenti per i quali occorre calcolare l’imposta;
  • 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

Torniamo all’architettura client/server utilizzata:

Image

  • in [2, 6], si nota che il livello [dao] svolge 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. È stata utilizzata fin dalla |versione 4|;
    • interagisce con il server web [3];

Nella |versione 5|, lo script principale [main] [1] comunicava direttamente con il livello [métier] [4]. Non si vorrebbe modificare questo script. A tal fine, faremo in modo che il livello [dao] [2] implementi l’interfaccia del livello [métier] [4]. In questo modo, lo script principale [main] avrà l’impressione di comunicare direttamente con il livello [métier] [4] e potrà ignorare completamente il fatto che quest’ultimo si trovi su un altro computer.

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


class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
  • la classe [ImpôtsDaoWithHttpClient]:
    • eredita dalla classe [AbstractImpôtsDao], il che le consentirà di gestire l’interazione con il sistema di file [6];
    • implementa l’interfaccia [InterfaceImpôtsMétier] per non dover modificare lo script principale [main] della |versione 5|;

Il codice completo della classe [ImpôtsDaoWithHttpClient] è il seguente:


# importazioni
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):

    # costruttore
    def __init__(self, config: dict):
        # inizializzazione del genitore
        AbstractImpôtsDao.__init__(self, config)
        # memorizzazione dei parametri
        self.__config_server = config["server"]

    # metodo non utilizzato di [AbstractImpôtsDao]
    def get_admindata(self) -> AdminData:
        pass

    # calcolo dell'imposta
    def calculate_tax(self: object, taxpayer: TaxPayer, admindata: AdminData = None):
        # si lasciano risalire le eccezioni
        # parametri del get
        params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
        # connessione con autenticazione Auth Basic?
        if self.__config_server['authBasic']:
            response = requests.get(
                # URL del server interrogato
                self.__config_server['urlServer'],
                # parametri di URL
                params=params,
                # autenticazione Basic
                auth=(
                    self.__config_server["user"]["login"],
                    self.__config_server["user"]["password"]))
        else:
            # connessione senza autenticazione Auth Basic
            response = requests.get(self.__config_server['urlServer'], params=params)
        # verifica
        print(response.text)
        # codice di stato della risposta HTTP
        status_code = response.status_code
        # si inserisce la risposta jSON in un dizionario
        résultat = response.json()
        # errore se il codice di stato è diverso da 200 OK
        if status_code != status.HTTP_200_OK:
            # si sa che gli errori sono stati associati alla chiave [erreurs] della risposta
            raise ImpôtsError(87, résultat['réponse']['erreurs'])
        # si sa che il risultato è stato associato alla chiave [result] della risposta
        # si modifica il parametro di input con questo risultato
        taxpayer.fromdict(résultat["réponse"]["result"])
  • righe 21-23: la classe [AbstractImpôtsDao] (riga 12) possiede un metodo astratto [get_admindata]. Siamo obbligati a implementarlo anche se non lo utilizziamo (admindata è gestito dal server e non dal client);
  • riga 26: il metodo [calculate_tax] appartiene all’interfaccia [InterfaceImpôtsMétier] (riga 12). Dobbiamo implementarlo;
  • riga 15: il costruttore riceve come unico parametro il dizionario della configurazione dell’applicazione;
  • righe 16-17: la classe padre [AbstractImpôtsDao] viene inizializzata passandole, anche in questo caso, la configurazione dell’applicazione. In essa troverà i nomi dei tre file di testo che deve gestire;
  • righe 18-19: le informazioni relative al server web per il calcolo dell’imposta vengono memorizzate localmente nella 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. Dal lato client, questi dati non sono disponibili. Questo parametro rimarrà sempre [None]. Questa complicazione suggerisce che la classe [ImpôtsMétier] sia stata inizialmente scritta in modo errato:
  • 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 incapsulato in un try / catch / finally. Ciò significa che eventuali eccezioni non verranno gestite e verranno propagate al codice chiamante, ovvero lo script [main]. Quest’ultimo intercetta correttamente tutte le eccezioni provenienti dal livello [dao];
  • riga 28: il calcolo dell’imposta avviene lato server. Sarà quindi necessario comunicare con esso. Ciò avviene tramite il modulo [requests] importato alla riga 2;
  • righe 31-43: per inviare una richiesta GET al server web, si utilizza 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 inserire nell’URL nella forma [/url ?param1=valeur1&param2=valeur2&…];
    • riga 29: il dizionario contenente i tre parametri [marié, enfants, salaire] che il server web si aspetta. Non è necessario occuparsi della codifica (chiamata urlencoded) a cui devono essere sottoposti questi parametri. Se ne occupa [requests];
    • righe 37-40: il parametro denominato [auth] è una tupla di due elementi (login, password). Rappresenta le credenziali di un'autenticazione di tipo Basic;
  • righe 44-45: queste due righe hanno solo scopo didattico (verranno commentate una volta terminato il debug):
    • [response] rappresenta la risposta HTTP del server;
    • [response.text] rappresenta il testo del documento incapsulato in questa risposta. Durante la fase di 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 BAD REQUEST
    • 500 INTERNAL SERVER ERROR
  • riga 49: il nostro server invia sempre jSON anche in caso di errore. La funzione [response.json()] crea un dizionario a partire dalla stringa jSON ricevuta. Ricordiamo le due forme possibili per la 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 incapsulati nella risposta;
  • riga 56: si recupera il dizionario generato dal calcolo dell’imposta e lo si utilizza per aggiornare il parametro di input [taxpayer];

23.3.5. Esecuzione

Per eseguire il client:

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

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

23.4. Test del livello [dao]

Torniamo all’architettura dell’applicazione client/server:

  • Nel client scritto, siamo riusciti a fare in modo che il livello [dao] [1] offra la stessa interfaccia del livello [métier] [3]. Utilizzeremo quindi, in [4], la classe di test |TestDaoMétier| già esaminata per testare il livello [métier] [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

        # {'sposato': 'sì', 'figli': 2, 'stipendio': 55555,
        # 'imposta': 2814, 'maggiorazione': 0, 'riduzione': 0, 'sconto': 0, 'aliquota': 0,14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        # verifica
        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

        # {'coniugato': 'sì', 'figli': 3, 'stipendio': 200000,
        # 'imposta': 42842, 'maggiorazione': 17283, 'sconto': 0, 'riduzione': 0, 'aliquota': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        dao.calculate_tax(taxpayer)
        # verifiche
        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__':

    # si configura l'applicazione
    import config
    config = config.configure({})

    # livello DAO
    dao = config['layers']['dao']

    # si eseguono i metodi di test
    print("tests en cours...")
    unittest.main()

Questa classe è analoga a quella già esaminata nella versione 4 dell’applicazione.

  • righe 40-41: si configura l’ambiente di test;
  • riga 44: si recupera un riferimento al livello [dao];
  • righe 47-48: si eseguono i test;

Per eseguire i test, si crea una |configurazione di esecuzione|:

Image

  • si crea una configurazione di esecuzione per uno script da console, non per un test 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.