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:

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

- 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

Lo script [server_01] è la seguente applicazione web:

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

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

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:

Ecco alcuni esempi di esecuzione (avete avviato l’applicazione [server_01] e quella SGBD, quindi richiedete l’URL http://localhost:5000/ con un browser):


Ecco un esempio di esecuzione nella console di Postman:

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

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

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

- in [6-7] inseriamo le credenziali presenti nello script [config]:
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):

- come con Postman, Firefox ha ricevuto dal server la risposta HTTP con le intestazioni HTTP:
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:

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:

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

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

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]

Torniamo all’architettura client/server utilizzata:

- 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¶m2=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:

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

- 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:
Tutti gli 11 test sono stati superati.