Skip to content

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:

Image

25.2. Il server web

La struttura degli script del server è la seguente:

Image

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

Image

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

Image

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:

import myutils

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:

Image

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

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