25. Esercizio pratico: versione 8
25.1. Introduzione
Scriveremo una nuova applicazione client/server. La novità del server è che gestirà una sessione. Invece di inserire i dati dell’amministrazione fiscale in un oggetto con ambito [application], li inseriremo in un oggetto con ambito [session]. In questo modo, si registra un calo delle prestazioni del codice. Quando un oggetto può essere condiviso in sola lettura da tutti gli utenti, è preferibile definirlo come oggetto con ambito [application] piuttosto che con ambito [session]. Si ottiene almeno un risparmio in termini di larghezza di banda, poiché in questo modo si riduce la dimensione del cookie di sessione. Tuttavia, vogliamo illustrare un’applicazione client/server in cui il client e il server si scambiano un cookie di sessione.
L’architettura dell’applicazione rimane invariata:

25.2. Il server web
La struttura degli script del server è la seguente:

La cartella [http-servers/03] viene inizialmente ottenuta copiando la cartella [http-servers/02]. Successivamente si procedono alle modifiche.
25.2.1. La configurazione
È la stessa della |versione precedente| con alcune modifiche nello script [config]:
# 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, Intervalli
f"{root_dir}/impots/v05/entities",
# index_controller
f"{script_dir}/../controllers",
# script [config_database, config_layers]
script_dir,
# Logger, SendAdminMail
f"{root_dir}/impots/http-servers/02/utilities",
]
- riga 17: si riscriverà un controller per la funzione [index] che gestisce il file URL / ;
- riga 21: si utilizzano le utilità della |versione precedente|;
25.2.2. Lo script principale [main]
Il nuovo script [main] apporta alcune modifiche allo script principale [main] della versione precedente:
# L'applicazione Flask può avviarsi
app = Flask(__name__)
# chiave segreta della sessione
app.secret_key = os.urandom(12).hex()
- riga 4: si crea una chiave segreta per l’applicazione. Si sa che questa è necessaria per gestire le sessioni;
Inoltre, i dati fiscali non vengono più richiesti nel codice di [main]. Vengono eliminate le seguenti righe:
# recupero dei dati dell'amministrazione fiscale
erreur = False
try:
# admindata sarà un dato a livello di applicazione in sola lettura
config["admindata"] = config["layers"]["dao"].get_admindata()
# log di esito positivo
logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
# si registra l'errore
erreur = True
# log degli errori
log = f"L'erreur suivante s'est produite : {ex}"
# console
print(log)
# file di log
logger.write(f"{log}\n")
# e-mail all'amministratore
send_adminmail(config, log)
Inoltre, il controller [index_controller] accetta un parametro aggiuntivo, la sessione Flask:
from flask import request, Flask, session
….
# la richiesta viene eseguita da un controller
résultat, status_code = index_controller.execute(request, session, config)
25.2.3. Il controller [index_controller]
Il controller [index_controller] ora gestisce una sessione:
# importazione delle dipendenze
import os
import re
import threading
from flask_api import status
from werkzeug.local import LocalProxy
# URL configurata: /?sposato=xx&figli=yy&stipendio=zz
from AdminData import AdminData
from ImpôtsError import ImpôtsError
def execute(request: LocalProxy, session: LocalProxy, config: dict) -> tuple:
# dipendenze
from TaxPayer import TaxPayer
# inizialmente nessun errore
erreurs = []
…
# errori?
if erreurs:
# viene restituito un messaggio di errore al cliente
return {"réponse": {"erreurs": erreurs}}, status.HTTP_400_BAD_REQUEST
# nessun errore, si può lavorare
# si recupera la configurazione associata al thread
thread_name = threading.current_thread().name
logger = config[thread_name]["config"]["logger"]
# si esegue la richiesta
réponse = None
try:
# il caso più semplice: admindata è già in sessione
if session.get('client_id') is not None:
# si recuperano le informazioni della sessione
client_id = session.get('client_id')
admindata = AdminData().fromdict(session.get('admindata'))
# log
logger.write(f"[index_controller] client [{client_id}], données fiscales prises en session\n")
else:
# recupero dei dati dell'amministrazione fiscale
admindata = config["layers"]["dao"].get_admindata()
# Accesso di admindata
session['admindata'] = admindata.asdict()
# si assegna un numero al cliente e si inserisce tale numero nella sessione
# questo ci consentirà di tracciarlo nei log del server
client_id = os.urandom(12).hex()
session['client_id'] = client_id
# log
logger.write(f"[index_controller] client [{client_id}], données fiscales prises dans la couche dao\n")
# 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 {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
except (BaseException, ImpôtsError) as erreur:
# si invia la risposta al cliente
return {"réponse": {"erreurs": [f"{erreur}"]}}, status.HTTP_500_INTERNAL_SERVER_ERROR
- riga 14: il controller riceve la sessione corrente del client web;
- righe 35-38: se il client ha una sessione, questa contiene due chiavi:
- [client_id]: un numero cliente (riga 37);
- [admindata]: i dati dell'amministrazione fiscale sotto forma di dizionario (riga 38);
- riga 35: si verifica se la sessione contiene una delle due chiavi previste;
- righe 42-51: caso in cui la sessione del cliente non sia ancora stata inizializzata;
- riga 43: si recuperano i dati dell’amministrazione fiscale dal livello [dao];
- riga 45: questi dati vengono inseriti nella sessione sotto forma di dizionario;
- riga 48: si assegna un numero casuale al cliente. Questo numero sarà diverso per ogni cliente;
- riga 49: questo numero viene inserito nella sessione;
- riga 51: si registra il fatto che i dati dell’amministrazione fiscale sono stati ottenuti tramite il livello [dao]. Gli accessi al livello [dao] sono in genere costosi. Per questo motivo è necessario limitarli. L’idea qui è quella di ottenere una volta i dati fiscali dal livello [dao], di inserirli nella sessione e di recuperarli da lì durante le successive richieste dello stesso cliente. Si ricorda che questa non è la soluzione migliore. Poiché i dati fiscali dell’amministrazione sono gli stessi per tutti i clienti, il loro posto è in un oggetto a ambito dell’applicazione;
- righe 35-40: caso in cui la sessione del cliente sia stata inizializzata durante una richiesta precedente;
- riga 37: si recupera il numero del cliente dalla sessione;
- riga 38: si recuperano i dati fiscali dell’amministrazione dalla sessione;
- riga 40: si registra il fatto che il cliente ha ottenuto i dati fiscali dell’amministrazione nella sessione;
25.3. Il client web

25.3.1. Il livello [dao]
25.3.1.1. La classe [ImpôtsDaoWithHttpSession]
Il livello [dao] è implementato dalla seguente classe [ImpôtsDaoWithHttpSession]:
# importazioni
import requests
from flask_api import status
from myutils import decode_flask_session
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ôtsDaoWithHttpSession(AbstractImpôtsDao, InterfaceImpôtsMétier):
# costruttore
def __init__(self, config: dict):
# inizializzazione del genitore
AbstractImpôtsDao.__init__(self, config)
# memorizzazione degli elementi di configurazione
# configurazione generale
self.__config = config
# server
self.__config_server = config["server"]
# modalità debug
self.__debug = config["debug"]
# registratore
self.__logger = None
# cookie
self.__cookies = None
# metodo inutilizzato
def get_admindata(self) -> AdminData:
pass
# calcolo dell'imposta
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
# si consentono 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"]),
cookies=self.__cookies)
else:
# connessione senza autenticazione Auth Basic
response = requests.get(self.__config_server['urlServer'], params=params, cookies=self.__cookies)
# si recuperano i cookie dalla risposta, se presenti
if response.cookies:
self.__cookies = response.cookies
# si recupera il cookie di sessione
session_cookie = response.cookies.get('session')
# lo si decodifica per effettuare l'accesso
if session_cookie:
# funzione di accesso
if not self.__logger:
self.__logger = self.__config['logger']
# si effettua il login
self.__logger.write(f"cookie de session={decode_flask_session(session_cookie)}\n")
# modalità debug?
if self.__debug:
# registratore
if not self.__logger:
self.__logger = self.__config['logger']
# si registra
self.__logger.write(f"{response.text}\n")
# 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"])
- riga 30: il livello [dao] gestirà un dizionario di cookie;
- riga 58: la proprietà [response.cookies] è un dizionario che raccoglie i cookie inviati dal server nelle intestazioni HTTP e [Set-Cookie];
- riga 59: questi cookie vengono memorizzati nel livello [dao]. Verranno rinviati al server in occasione delle successive richieste dello stesso client;
- righe 60-68: sebbene non sia indispensabile, si recupera il cookie di sessione. Nel dizionario dei cookie inviati dal server, il cookie di sessione è associato alla chiave [session];
- righe 62-68: si decodifica il cookie di sessione per effettuare il login;
- riga 68: torneremo in seguito sulla funzione [decode_flask_session] che decodifica il cookie di sessione;
- righe 52 e 57: ad ogni richiesta dello stesso client, i cookie inviati dal server gli vengono rinviati. È in questo modo che la sessione Flask viene mantenuta tra il client e il server;
Ora bisogna tenere presente che il livello [dao] verrà eseguito simultaneamente da più thread. È quindi necessario esaminare tutte le proprietà dell’istanza di classe e verificare se l’accesso simultaneo a tali proprietà pone dei problemi. Qui abbiamo aggiunto la proprietà [self.__cookies], alla riga 30. Questa proprietà viene modificata alla riga 59. Abbiamo quindi un accesso in scrittura a un dato condiviso da tutti i thread. Tuttavia, questo accesso pone un problema: ogni thread che rappresenta un determinato client ha il proprio cookie di sessione. Infatti, al suo interno è presente un numero di cliente (=thread) univoco per ogni cliente. Se non si interviene, il thread T2 può sovrascrivere i cookie del thread T1.
Abbiamo già visto un metodo per gestire questo problema: è possibile creare nel file [config], passato come parametro al costruttore (riga 17), chiavi diverse per ogni thread. Ad esempio, si può utilizzare come chiave il nome del thread:
- alla riga 59, si potrebbe scrivere:
config[thread_name][‘cookies’]=cookies
- alla riga 52 si potrebbe quindi scrivere:
cookies=config[thread_name][‘cookies’]
Qui useremo una tecnica diversa: ogni thread (=cliente) avrà il proprio livello [dao]. In questo modo la riga 59 non pone più alcun problema, poiché i cookie utilizzati sono quelli di un unico cliente.
A tal fine, creeremo una nuova classe [ImpôtsDaoWithHttpSessionFactory].
25.3.1.2. La funzione di decodifica della sessione Flask
La funzione [decode_flask_session] è definita nello script [myutils]:

Abbiamo già esaminato lo script |myutils|. Questo script è un modulo a livello di macchina che i vari script di questo corso possono importare con l’istruzione:
In esso si definisce la funzione [decode_flask_session] nel modo seguente:
def decode_flask_session(cookie: str) -> str:
# fonte: https://www.kirsle.net/wizards/flask-session.cgi
compressed = False
payload = cookie
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data.decode("utf-8")
- riga 2: URL, dove ho trovato questa funzione;
- riga 1: il parametro [cookie] è la stringa associata alla chiave [session] nel dizionario dei cookie ricevuti da un client web;
- righe 3-16: non commenterò questo codice che non conosco bene;
Si aggiunge una nuova importazione nel file [__init__.py]:
from .myutils import set_syspath, json_response, decode_flask_session
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];
25.3.1.3. La classe [ImpôtsDaoWithHttpSessionFactory]
La classe [ImpôtsDaoWithHttpSessionFactory] è la seguente:
from ImpôtsDaoWithHttpSession import ImpôtsDaoWithHttpSession
class ImpôtsDaoWithHttpSessionFactory:
def __init__(self, config: dict):
# si memorizza il parametro
self.__config = config
def new_instance(self):
# si crea un'istanza del livello [dao]
return ImpôtsDaoWithHttpSession(self.__config)
- la classe [ImpôtsDaoWithHttpSessionFactory] consente di creare una nuova implementazione del livello [dao] con il metodo [new_instance] delle righe 10-12;
25.3.2. La configurazione
Lo script [config_layers], che configura i livelli del client web, viene modificato come segue:
def configure(config: dict) -> dict:
# istanziazione dei livelli dell'applicazione
# livello DAO
from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
dao_factory = ImpôtsDaoWithHttpSessionFactory(config)
# si esegue la configurazione dei livelli
return {
"dao_factory": dao_factory
}
- righe 5-6: invece di istanziare un unico livello [dao] come avveniva in precedenza, si istanzia una «factory» di tale livello (factory = fabbrica di produzione di oggetti, in questo caso il livello [dao]);
- righe 9-11: si restituisce la configurazione dei livelli;
25.3.3. Lo script principale del client
Lo script [main] presenta le seguenti modifiche rispetto a quello della versione precedente:
# si configura l'applicazione
import config
config = config.configure({})
# dipendenze
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger
# esecuzione del livello [dao] in un thread
# taxpayers è un elenco di contribuenti
def thread_function(thread_dao, logger, taxpayers: list):
…
# elenco dei thread del client
threads = []
logger = None
# codice
try:
…
l_taxpayers = len(taxpayers)
while i < len(taxpayers):
…
# ogni thread deve disporre del proprio livello [dao] per gestire correttamente il proprio cookie di sessione
thread_dao = dao_factory.new_instance()
# si crea il thread
thread = threading.Thread(target=thread_function, args=(thread_dao, logger, thread_taxpayers))
# lo si aggiunge all'elenco dei thread dello script principale
threads.append(thread)
# si avvia il thread – questa operazione è asincrona – non si attende il risultato del thread
thread.start()
# il thread principale attende il completamento di tutti i thread che ha avviato
…
except BaseException as erreur:
# visualizzazione dell'errore
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# si chiude il logger
if logger:
logger.close()
# operazione completata
print("Travail terminé...")
# fine dei thread che potrebbero ancora esistere se ci si è arrestati a causa di un errore
sys.exit()
- righe 29-30: ogni thread ha il proprio livello [dao];
25.3.4. Esecuzione del client
Viene avviato il server web, viene avviato SGBD, viene avviato il server di posta [hMailServer]. Successivamente viene avviato lo script [main] del client web. I log dell’esecuzione in [data/logs/logs.txt] sono quindi i seguenti:
2020-07-25 10:21:46.478511, Thread-1 : début du thread [Thread-1] avec 1 contribuable(s)
2020-07-25 10:21:46.479111, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-25 10:21:46.479111, Thread-2 : début du thread [Thread-2] avec 1 contribuable(s)
2020-07-25 10:21:46.480195, Thread-3 : début du thread [Thread-3] avec 2 contribuable(s)
2020-07-25 10:21:46.480195, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-25 10:21:46.481137, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
2020-07-25 10:21:46.481137, Thread-3 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-25 10:21:46.482279, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
2020-07-25 10:21:46.482622, Thread-6 : début du thread [Thread-6] avec 1 contribuable(s)
2020-07-25 10:21:46.482622, Thread-4 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-5 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-6 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-25 10:21:46.725910, Thread-4 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"fa3c83b82761c83e13217967"}
2020-07-25 10:21:46.725910, Thread-4 : {"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}}}
2020-07-25 10:21:46.725910, Thread-4 : fin du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:46.726960, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:47.514108, Thread-3 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,24999.5],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"700e3f5dc808c7c48f0c9007"}
2020-07-25 10:21:47.514610, Thread-3 : {"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}}}
2020-07-25 10:21:47.514939, Thread-3 : fin du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}
2020-07-25 10:21:47.514939, Thread-3 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000}
2020-07-25 10:21:47.527211, Thread-5 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"9e14a5d4a3057f69ab95ab2d"}
2020-07-25 10:21:47.527211, Thread-2 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,22500.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"a06e8fd70a44c9e311f4dce0"}
2020-07-25 10:21:47.527211, Thread-5 : {"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}}}
2020-07-25 10:21:47.527211, Thread-1 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"28c38df998f67685b3a482b8"}
2020-07-25 10:21:47.527211, Thread-2 : {"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}}}
2020-07-25 10:21:47.528341, Thread-5 : fin du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.528341, Thread-1 : {"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}}}
2020-07-25 10:21:47.528842, Thread-2 : fin du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}
2020-07-25 10:21:47.529349, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000}
2020-07-25 10:21:47.529699, Thread-1 : fin du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.529699, Thread-2 : fin du thread [Thread-2]
2020-07-25 10:21:47.531905, Thread-1 : fin du thread [Thread-1]
2020-07-25 10:21:47.536121, Thread-6 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,93749.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"38499b63076516c02f2770ec"}
2020-07-25 10:21:47.537161, Thread-3 : {"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}}}
2020-07-25 10:21:47.537161, Thread-6 : {"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}}}
2020-07-25 10:21:47.538156, Thread-3 : fin du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.538557, Thread-6 : fin du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.538828, Thread-3 : fin du thread [Thread-3]
2020-07-25 10:21:47.538828, Thread-6 : fin du thread [Thread-6]
2020-07-25 10:21:47.546198, Thread-5 : {"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}}}
2020-07-25 10:21:47.546198, Thread-5 : fin du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.546198, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000}
2020-07-25 10:21:47.739643, Thread-4 : {"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}}}
2020-07-25 10:21:47.739643, Thread-4 : fin du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.740668, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000}
2020-07-25 10:21:48.557469, Thread-5 : {"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}}}
2020-07-25 10:21:48.558715, Thread-5 : fin du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}
2020-07-25 10:21:48.558715, Thread-5 : fin du thread [Thread-5]
2020-07-25 10:21:48.753025, Thread-4 : {"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}}}
2020-07-25 10:21:48.753318, Thread-4 : fin du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-25 10:21:48.753540, Thread-4 : fin du thread [Thread-4]
- ci sono in totale 6 thread, quindi 6 clienti (righe 1, 3, 4, 6, 8, 9) che interrogano simultaneamente il server di calcolo delle imposte;
- seguiremo il thread [Thread-4] che gestisce 3 contribuenti (riga 6). Esso effettuerà in sequenza tre richieste al server di calcolo delle imposte;
- riga 10: la prima richiesta di [Thread-4];
- riga 13: [Thread-4] ha ricevuto la risposta alla sua prima richiesta. Al suo interno trova un cookie di sessione contenente il numero [fa3c83b82761c83e13217967] che gli è stato assegnato dal server;
- riga 14: l’imposta del primo contribuente;
- riga 16: [Thread-4] effettua una richiesta per il secondo contribuente;
- riga 43: [Thread-4] riceve l’imposta del secondo contribuente;
- riga 45: [Thread-4] effettua una richiesta per il terzo contribuente;
- riga 49: [Thread-4] riceve l’imposta del terzo contribuente;
- riga 51: [Thread-4] ha completato il proprio lavoro;
Ora vediamo come sono state elaborate sul lato server le 3 richieste di [Thread-4]. Potremo seguirlo nei log del server grazie al suo numero cliente [fa3c83b82761c83e13217967].
I log di [data/logs/logs.txt] sul lato server sono i seguenti:
2020-07-25 10:21:39.187366, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:40.439093, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:46.502011, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=sì&figli=3&stipendio=50000' [GET]>
2020-07-25 10:21:46.504049, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.505452, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=sì&figli=2&stipendio=55555' [GET]>
2020-07-25 10:21:46.506257, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.507292, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=no&figli=0&stipendio=100000' [GET]>
2020-07-25 10:21:46.507292, Thread-4 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.508301, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=sì&figli=2&stipendio=50000' [GET]>
2020-07-25 10:21:46.509293, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.511808, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=no&figli=3&stipendio=100000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=sì&figli=3&stipendio=200000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.719504, Thread-6 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises dans la couche dao
2020-07-25 10:21:46.720003, Thread-6 : [index] {'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}}}
2020-07-25 10:21:46.736108, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=sì&figli=3&stipendio=100000' [GET]>
2020-07-25 10:21:46.736108, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.506709, Thread-2 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises dans la couche dao
2020-07-25 10:21:47.507216, Thread-2 : [index] {'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}}}
2020-07-25 10:21:47.507216, Thread-3 : [index_controller] client [28c38df998f67685b3a482b8], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508442, Thread-4 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508940, Thread-3 : [index] {'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}}}
2020-07-25 10:21:47.510506, Thread-4 : [index] {'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}}}
2020-07-25 10:21:47.511513, Thread-5 : [index_controller] client [a06e8fd70a44c9e311f4dce0], données fiscales prises dans la couche dao
2020-07-25 10:21:47.514939, Thread-5 : [index] {'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}}}
2020-07-25 10:21:47.520727, Thread-7 : [index_controller] client [38499b63076516c02f2770ec], données fiscales prises dans la couche dao
2020-07-25 10:21:47.523162, Thread-7 : [index] {'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}}}
2020-07-25 10:21:47.530835, Thread-9 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=no&figli=2&stipendio=100000' [GET]>
2020-07-25 10:21:47.531736, Thread-9 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises en session
2020-07-25 10:21:47.531905, Thread-9 : [index] {'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}}}
2020-07-25 10:21:47.541899, Thread-10 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=sì&figli=2&stipendio=30000' [GET]>
2020-07-25 10:21:47.542488, Thread-10 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:47.542488, Thread-10 : [index] {'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}}}
2020-07-25 10:21:47.553628, Thread-11 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=no&figli=0&stipendio=200000' [GET]>
2020-07-25 10:21:47.553628, Thread-11 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.736910, Thread-8 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:47.737191, Thread-8 : [index] {'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}}}
2020-07-25 10:21:47.748226, Thread-12 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=sì&figli=5&stipendio=100000' [GET]>
2020-07-25 10:21:47.748226, Thread-12 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:48.554695, Thread-11 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:48.555070, Thread-11 : [index] {'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}}}
2020-07-25 10:21:48.748753, Thread-12 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:48.748753, Thread-12 : [index] {'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}}}
- il cliente [fa3c83b82761c83e13217967] compare per la prima volta alla riga 14: per calcolare l’imposta, il server ha dovuto recuperare dal database i dati dell’amministrazione fiscale;
- poi ritroviamo il client [fa3c83b82761c83e13217967] alla riga 36. Questa volta, il server trova i dati dell’amministrazione fiscale nella sessione, il che gli evita un accesso, potenzialmente costoso, al livello [dao];
- il client [fa3c83b82761c83e13217967] ricompare una terza volta alla riga 42, dove anche in questo caso il server utilizza la sessione del client;
Questo esempio illustra chiaramente l’utilità della sessione per un cliente: vi si inseriscono dati condivisi da tutte le richieste di quel cliente e che sono costosi da acquisire.
Dal lato client, i risultati nel file [data/output/résultats.json] sono gli stessi delle versioni precedenti.
25.4. Test del livello [dao]
Come abbiamo fatto nelle |versioni precedenti|, testiamo il livello [dao] del client:

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
from Logger import Logger
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)
…
if __name__ == '__main__':
# si configura l'applicazione
import config
config = config.configure({})
# registrazione
logger = Logger(config["logsFilename"])
# lo si memorizza nella configurazione
config["logger"] = logger
# si recupera la factory del livello [dao]
dao_factory = config["layers"]["dao_factory"]
# si crea un'istanza del livello [dao]
dao = dao_factory.new_instance()
# si eseguono i metodi di test
print("tests en cours...")
unittest.main()
- si crea una |configurazione di esecuzione| per questo test;
- si avvia il server web con tutto il suo ambiente;
- si esegue il test;
I risultati sono i seguenti:
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/03/tests/TestHttpClientDao.py
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 3.392s
OK
Process finished with exit code 0