Skip to content

24. Esercizio pratico: versione 7

24.1. Introduzione

La versione 7 dell’applicazione per il calcolo delle imposte è identica alla versione 6, salvo i seguenti dettagli:

  • il client web invierà contemporaneamente più richieste HTTP. Nella versione precedente queste richieste venivano inviate in sequenza. Il server elaborava quindi in ogni momento una sola richiesta;
  • il server sarà multithread: potrà elaborare più richieste contemporaneamente;
  • per monitorare l’esecuzione di queste richieste, il server web sarà dotato di un logger con cui verranno registrati in un file di testo i momenti salienti dell’elaborazione delle richieste;
  • il server invierà un’e-mail all’amministratore dell’applicazione qualora si verifichi un problema che ne impedisca l’avvio, tipicamente un problema con il database associato al server web;

L’architettura dell’applicazione rimane invariata:

La struttura degli script è la seguente:

Image

La cartella [http-servers/02] viene inizialmente ottenuta copiando la cartella [http-servers/01]. Successivamente vi vengono apportate delle modifiche.

24.2. Le utilità

Image

24.2.1. La classe [Logger]

La classe [Logger] consentirà di registrare in un file di testo alcune azioni del server web:


import codecs
import threading
from datetime import date, datetime
from threading import current_thread

from ImpôtsError import ImpôtsError


class Logger:
    # attributo di classe
    verrou = threading.RLock()

    # costruttore
    def __init__(self, logs_filename: str):
        try:
            # si apre il file in modalità append (a)
            self.__resource = codecs.open(logs_filename, "a", "utf-8")
        except BaseException as erreur:
            raise ImpôtsError(18, f"{erreur}")

    # scrittura di un log
    def write(self, message: str):
        # data/ora corrente
        today = date.today()
        now = datetime.time(datetime.now())
        # nome del thread
        thread_name = current_thread().name
        # non si desidera essere interrotti durante la scrittura nel file di log
        # si richiede l'oggetto di sincronizzazione (= il blocco) della classe: un solo thread lo otterrà
        Logger.verrou.acquire()
        try:
            # scrittura nel log
            self.__resource.write(f"{today} {now}, {thread_name} : {message}")
            # scrittura immediata – altrimenti il testo verrà scritto solo alla chiusura del flusso di scrittura
            # oppure si desidera monitorare i log nel tempo
            self.__resource.flush()
        finally:
            # si libera l'oggetto di sincronizzazione (= il blocco) affinché un altro thread possa ottenerlo
            Logger.verrou.release()

    # rilascio delle risorse
    def close(self):
        # chiusura del file
        if self.__resource:
            self.__resource.close()
  • righe 10-11: si definisce un attributo di classe. Un attributo di classe è una proprietà condivisa da tutte le istanze della classe. Si fa riferimento ad esso con la notazione [Classe.attribut_de_classe] (righe 30, 39). L’attributo di classe [verrou] fungerà da oggetto di sincronizzazione per tutti i thread che eseguono il codice delle righe 31-36;
  • righe 14-19: il costruttore riceve il percorso assoluto del file di log. Il file viene quindi aperto e il descrittore di file recuperato viene memorizzato nella classe;
  • riga 17: il file di log viene aperto in modalità «append» (a). Ogni riga scritta verrà aggiunta alla fine del file;
  • righe 22-39: il metodo [write] consente di scrivere nel file di log un messaggio passato come parametro. A questo vengono aggiunte due informazioni:
    • riga 24: la data odierna;
    • riga 25: l’ora corrente;
    • riga 27: il nome del thread che scrive il log. Non bisogna dimenticare che un’applicazione web serve più utenti contemporaneamente. A ogni richiesta viene assegnato un thread per l’esecuzione. Se questo thread viene messo in pausa, tipicamente per un’operazione di I/O (rete, file, database), allora il processore verrà assegnato a un altro thread. A causa di queste possibili interruzioni, non si può essere certi che un thread riesca a scrivere una riga nel file di log senza essere interrotto. Si rischia quindi di vedere i log di due thread diversi mescolarsi. Il rischio è basso, forse addirittura nullo, ma abbiamo comunque deciso di mostrare come sincronizzare l’accesso di due thread a una risorsa comune, in questo caso il file di log;
  • riga 30: prima di scrivere, il thread richiede la chiave della porta d’ingresso. La chiave richiesta è quella creata alla riga 11. È effettivamente unica: un attributo di classe è unico per tutte le istanze della classe;
    • al tempo T1, un thread Thread1 ottiene la chiave. Può quindi eseguire la riga 33;
    • al tempo T2, il thread Thread1 viene messo in pausa prima ancora di aver terminato la scrittura del log;
    • al tempo T3, anche il thread Thread2, che ha ottenuto il processore, deve scrivere un log. Giunge così alla riga 30, dove richiede la chiave della porta d’ingresso. Gli viene risposto che un altro thread la possiede già. Viene quindi automaticamente messo in pausa. Lo stesso accadrà a tutti i thread che richiederanno questa chiave;
    • al tempo T4, il thread Thread1, che era stato messo in pausa, riacquista il processore. A questo punto completa la scrittura del log;
  • righe 32-36: la scrittura nel file di log avviene in due fasi:
  • riga 33: il descrittore di file ottenuto alla riga 17 opera con un buffer. L’operazione [write] della riga 33 scrive in questo buffer ma non direttamente nel file. Il buffer viene poi svuotato nel file a determinate condizioni:
        • il buffer è pieno;
        • il descrittore di file è oggetto di un'operazione [close] o [flush];
  • riga 36: si forza la scrittura della riga di log nel file. Si procede in questo modo perché si desidera vedere i log dei diversi thread intercalarsi tra loro. Se non si facesse così, i log di un thread verrebbero scritti tutti contemporaneamente alla chiusura del descrittore, riga 45. Sarebbe quindi molto più difficile rendersi conto che alcuni thread sono stati arrestati: bisognerebbe controllare gli orari nei log;
  • riga 39: il thread Thread1 restituisce la chiave che gli era stata assegnata. Questa potrà essere assegnata a un altro thread;
  • riga 22: il metodo [write] è quindi sincronizzato: un solo thread alla volta scrive nel file di log. La chiave del meccanismo è la riga 30: qualunque cosa accada, solo un thread recupera la chiave per passare alla riga successiva. La conserva finché non la restituisce (riga 39);
  • righe 41-45: il metodo [close] consente di liberare le risorse allocate al descrittore del file di log;

I log scritti nel file di log avranno il seguente aspetto:

2020-07-22 20:03:52.992152, Thread-2 : …

24.2.2. La classe [SendAdminMail]

La classe [SendAminMail] consente di inviare un messaggio all’amministratore dell’applicazione quando questa si «blocca».

Image

La classe [SendAdminMail] è configurata nello script [config] [2] nel modo seguente:


        # configurazione del server SMTP
        "adminMail": {
            # server SMTP
            "smtp-server": "localhost",
            # porta del server SMTP
            "smtp-port": "25",
            # amministratore
            "from": "guest@localhost.com",
            "to": "guest@localhost.com",
            # oggetto dell'e-mail
            "subject": "plantage du serveur de calcul d'impôts",
            # tls impostato su True se il server SMTP richiede un'autorizzazione, altrimenti su False
            "tls": False
        }

La classe [SendAdminMail] riceve il dizionario dalle righe 2-13 e la configurazione per l’invio dell’e-mail. La classe è la seguente:


# importazioni
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate


class SendAdminMail:

    # -----------------------------------------------------------------------
    @staticmethod
    def send(config: dict, message: str, verbose: bool = False):
        # invia un messaggio al server SMTP config['smtp-server'] sulla porta config[smtp-port]
        # se config['tls'] è vero, verrà utilizzato il supporto TLS
        # l’e-mail viene inviata a nome di config['from']
        # al destinatario config['to']
        # il messaggio ha come oggetto config['subject']
        # si trova il riferimento a un logger in config['logger']

        # si recupera il logger dalla configurazione - può essere uguale a None
        logger = config["logger"]
        # server SMTP
        server = None
        # si invia il messaggio
        try:
            # il server SMTP
            server = smtplib.SMTP(config["smtp-server"])
            # modalità verbosa
            server.set_debuglevel(verbose)
            # connessione sicura?
            if config['tls']:
                # inizio della procedura di sicurezza
                server.starttls()
                # autenticazione
                server.login(config["user"], config["password"])
            # creazione di un messaggio Multipart - è questo il messaggio che verrà inviato
            msg = MIMEText(message)
            msg['From'] = config["from"]
            msg['To'] = config["to"]
            msg['Date'] = formatdate(localtime=True)
            msg['Subject'] = config["subject"]
            # invio del messaggio
            server.send_message(msg)
            # log - il logger potrebbe non esistere
            if logger:
                logger.write(f"[SendAdminMail] Message envoyé à [{config['to']}] : [{message}]\n")
        except BaseException as erreur:
            # log - il logger potrebbe non esistere
            if logger:
                logger.write(
                    f"[SendAdminMail] Erreur [{erreur}] lors de l'envoi à [{config['to']}] du message [{message}] : \n")
        finally:
            # operazione completata - si liberano le risorse utilizzate dalla funzione
            if server:
                server.quit()
  • righe 24-54: si ritrova il codice già esaminato nell’esempio |smtp/02|;
  • riga 20: si recupera il riferimento di un logger. Questo viene utilizzato alle righe 45 e 49;

24.3. Il server web

Image

24.3.1. Configurazione

Image

La configurazione del server è molto simile a quella del server esaminato in precedenza. Solo il file [config.py] subisce una leggera modifica:


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"{root_dir}/impots/http-servers/01/controllers",
        # script [config_database, config_layers]
        script_dir,
        # Logger, SendAdminMail
        f"{script_dir}/../utilities",
    ]
    # si imposta il syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # fase 2 ------
    # Configurazione dell'applicazione
    config.update({
        # utenti autorizzati a utilizzare l'applicazione
        "users"[
            {
                "login""admin",
                "password""admin"
            }
        ],
        # file di log
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # configurazione del server SMTP
        "adminMail": {
            # server SMTP
            "smtp-server""localhost",
            # porta del server SMTP
            "smtp-port""25",
            # amministratore
            "from""guest@localhost.com",
            "to""guest@localhost.com",
            # oggetto dell'e-mail
            "subject""plantage du serveur de calcul d'impôts",
            # TLS impostato su True se il server SMTP richiede l'autenticazione, altrimenti su False
            "tls"False
        },
        # durata della pausa del thread in secondi
        "sleep_time"0
    })

    # 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
  • righe 40-66: nel dizionario di configurazione del server vengono aggiunti gli elementi relativi al logger (riga 49) e quelli relativi all’invio di un’e-mail di avviso all’amministratore dell’applicazione (righe 51-63);
  • riga 65: per osservare meglio i thread in azione, ne bloccheremo alcuni. [sleep_time] indica la durata del blocco espressa in secondi;
  • righe 27-28: si noti che si utilizza il controller [index_controller] della precedente versione 6;

24.3.2. Lo script principale [main]

Lo script principale [main] è 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 flask import request, Flask
from flask_httpauth import HTTPBasicAuth
import json
import index_controller
from flask_api import status
from SendAdminMail import SendAdminMail
from myutils import json_response
from Logger import Logger
import threading
import time
from random import randint
from ImpôtsError import ImpôtsError

# gestore di autenticazione
auth = HTTPBasicAuth()


@auth.verify_password
def verify_password(login, password):
    # 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


# invio di un'e-mail all'amministratore
def send_adminmail(config: dict, message: str):
    # invio di un'e-mail all'amministratore dell'applicazione
    config_mail = config["adminMail"]
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)


# verifica del file di log
logger = None
erreur = False
message_erreur = None
try:
    # registratore
    logger = Logger(config["logsFilename"])
except BaseException as exception:
    # log della console
    print(f"L'erreur suivante s'est produite : {exception}")
    # si annota l'errore
    erreur = True
    message_erreur = f"{exception}"
# si memorizza il logger nella configurazione
config['logger'] = logger
# gestione dell'errore
if erreur:
    # invio di un'e-mail all'amministratore
    send_adminmail(config, message_erreur)
    # fine dell'applicazione
    sys.exit(1)

# log di avvio
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)

# recupero dei dati dall'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)

# il thread principale non ha più bisogno del logger
logger.close()

# se si è verificato un errore, l'applicazione si arresta
if erreur:
    sys.exit(2)

# l'applicazione Flask può avviarsi
app = Flask(__name__)


# Home URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():



# solo main
if __name__ == '__main__':
    # si avvia il server
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • righe 1-10: lo script attende un parametro [mysql / pgres] che gli indichi quale SGBD utilizzare;
  • righe 12-14: l’applicazione viene configurata (Python Path, livelli, database);
  • righe 16-28: le dipendenze necessarie all’applicazione;
  • righe 30-43: gestione dell'autenticazione;
  • righe 46-51: una funzione che invia un’e-mail all’amministratore dell’applicazione;
  • la funzione richiede due parametri:
      • config: un dizionario con le chiavi [adminMail] e [logger];
      • il messaggio da inviare;
    • righe 49-50: si prepara la configurazione dell'invio;
    • si invia l'e-mail;
  • righe 54-74: si verifica la presenza del file di log;
  • righe 70-74: se non si è riusciti ad aprire il file di log, si invia un'e-mail all'amministratore e si interrompe l'operazione;
  • righe 76-79: si registra l'avvio del server;
  • righe 81-98: si recuperano i dati dell'amministrazione fiscale dal database;
  • righe 88-98: se non si è riusciti a ottenere questi dati, si registra l'errore sia sulla console che nel file di log;
  • righe 100-101: il thread principale non genererà più log (i thread creati non utilizzeranno lo stesso descrittore di file);
  • righe 103-105: se non è stato possibile connettersi al database, il programma si interrompe;
  • riga 122: si avvia il server in modalità multithread;

La funzione [index] (riga 114) è la seguente:


# Home URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    logger = None
    try:
        # registrazione
        logger = Logger(config["logsFilename"])
        # lo si memorizza in una configurazione associata al thread
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        # si registra la richiesta
        logger.write(f"[index] requête : {request}\n")
        # si interrompe il thread se richiesto
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            # la pausa è casuale, in modo che alcuni thread vengano interrotti e altri no
            aléa = randint(0, 1)
            if aléa == 1:
                # registrazione prima della pausa
                logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                # pausa
                time.sleep(sleep_time)
        # la richiesta viene eseguita da un controller
        résultat, status_code = index_controller.execute(request, config)
        # Si è verificato un errore irreversibile?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # si invia un'e-mail all'amministratore dell'applicazione
            config_mail = config["adminMail"]
            config_mail["logger"] = logger
            SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
        # si registra la risposta
        logger.write(f"[index] {résultat}\n")
        # si invia la risposta
        return json_response(résultat, status_code)
    except BaseException as erreur:
        # si registra l'errore, se possibile
        if logger:
            logger.write(f"[index] {erreur}")
        # si prepara la risposta al cliente
        résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
        # si invia la risposta
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # si chiude il file di log se è stato aperto
        if logger:
            logger.close()
  • riga 4: la funzione eseguita quando un utente richiede URL /. Poiché il server è multithread (riga 112), verrà creato un thread per eseguire la funzione. Questo thread può essere interrotto e messo in pausa in qualsiasi momento per riprendere l’esecuzione poco dopo. È importante tenere sempre presente questo aspetto quando il codice accede a una risorsa condivisa da tutti i thread. In questo caso, tale risorsa è il file di log: tutti i thread vi scrivono;
  • riga 8: si crea un’istanza del logger. Pertanto, ogni thread avrà un’istanza diversa del logger. Tuttavia, tutti questi logger puntano allo stesso file di log. È comunque importante notare che quando un thread chiude il proprio logger, ciò non ha alcuna ripercussione sui logger degli altri thread;
  • righe 9-12: si memorizza il logger nel dizionario [config] dell’applicazione, associato a una chiave che riporta il nome del thread. Pertanto, se vi sono n thread in esecuzione simultanea, verranno create n voci nel dizionario [config]. [config] è una risorsa condivisa tra tutti i thread. Potrebbe quindi essere necessaria una sincronizzazione. Qui ho formulato un'ipotesi. Ho supposto che se due thread creassero contemporaneamente la propria voce nel file [config] e uno dei due venisse interrotto dall’altro, ciò non avrebbe alcuna conseguenza. Il thread interrotto avrebbe potuto completare in un secondo momento la creazione della voce. Se l’esperimento dimostrasse che questa ipotesi è errata, sarebbe necessario sincronizzare l’accesso alla riga 12;
  • riga 10: si inserisce il logger in un dizionario;
  • riga 11: [threading.current_thread()] è il thread che esegue questa riga, quindi il thread che esegue la funzione [index]. Ne prendiamo nota del nome. Ogni thread ha un nome univoco;
  • riga 12: si memorizza la configurazione del thread. D'ora in poi procederemo sempre in questo modo: se ci sono informazioni che non possono essere condivise tra i thread, verranno comunque inserite nella configurazione generale, ma associate al nome del thread;
  • riga 14: si registra la richiesta che si sta eseguendo;
  • righe 15-24: in modo casuale si mettono in pausa alcuni thread affinché lascino il processore a un altro thread;
    • riga 16: si recupera la durata della pausa (in secondi) dalla configurazione;
    • riga 17: la pausa viene applicata solo se la durata della pausa è diversa da 0;
    • riga 19: un numero intero casuale nell'intervallo [0, 1]. Pertanto, sono possibili solo i valori 0 e 1;
    • riga 20: il thread viene messo in pausa solo se il numero casuale è 1;
    • riga 22: si registra il fatto che il thread sta per essere interrotto;
    • riga 24: si interrompe il thread per [sleep_time] secondi;
  • riga 26: quando il thread riprende l’esecuzione, fa eseguire la richiesta tramite il modulo [index_controller];
  • righe 28-32: se tale esecuzione provoca un errore di tipo [500 INTERNAL SERVER ERROR], si invia un’e-mail all’amministratore;
    • righe 30-31: si configura il dizionario [config_mail] che verrà passato alla classe [SendAdminMail];
    • riga 32: il messaggio inviato all'amministratore è la stringa jSON del risultato che verrà inviato al cliente;
  • righe 33-34: si registra la risposta che verrà inviata al cliente (riga 36);
  • righe 37-44: gestione di un'eventuale eccezione;
  • righe 39-40: se il logger esiste, si registra l'errore verificatosi;
  • righe 47-48: si chiude il logger, se esiste. In definitiva, il thread crea un logger all'inizio della richiesta e lo chiude una volta che questa è stata elaborata;

24.3.3. Il controller [index_controller]

Il controller [index_controller] che esegue le richieste è quello della versione precedente:

Image

24.3.4. Esecuzione

Si avvia il server Flask, il server di posta |hMailServer| e il client di posta |Thunderbird|. Non si avvia il SGBD. Il server si arresta con i seguenti log di console:


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-servers/02/flask/main.py mysql
[serveur] démarrage du serveur
L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]

Process finished with exit code 2 

Il file di log [logs.txt] è invece il seguente:


2020-07-23 11:51:38.324752, MainThread : [serveur] démarrage du serveur
2020-07-23 11:51:40.355510, MainThread : L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]
2020-07-23 11:51:42.464206, MainThread : [SendAdminMail] Message envoyé à [guest@localhost.com] : [L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]]

Con Thunderbird, si controllano le e-mail dell’amministratore [guest@localhost.com]:

Image

Quindi si avvia SGBD e si richiedono URL e [http://127.0.0.1:5000/?mari%C3%A9=oui&enfants=3&salaire=200000]. I log diventano i seguenti:


2020-07-23 11:56:38.891753, MainThread : [serveur] démarrage du serveur
2020-07-23 11:56:38.987999, MainThread : [serveur] connexion à la base de données réussie
2020-07-23 11:56:40.586747, MainThread : [serveur] démarrage du serveur
2020-07-23 11:56:40.655254, MainThread : [serveur] connexion à la base de données réussie
2020-07-23 11:56:54.528360, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=sì&figli=3&stipendio=200000' [GET]>
2020-07-23 11:56:54.530653, Thread-2 : [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}}}
  • righe 1-4: ricordiamo che ci sono due avvii del server perché la modalità [Debug=True] provoca un secondo avvio;
  • righe 5-6: i log ci danno un'idea del tempo di esecuzione di una richiesta, in questo caso 2,293 millisecondi;

24.4. Il client web

Image

Image

La cartella [http-clients/02] viene ottenuta copiando la cartella [http-clients/01]. Successivamente vengono apportate alcune modifiche.

24.4.1. La configurazione

La configurazione [config] dell’applicazione [http-clients/02] è identica a quella dell’applicazione [http-clients/01], salvo alcune piccole differenze:


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,
        # Logger
        f"{root_dir}/impots/http-servers/02/utilities",
    ]

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

    # fase 2 ------
    # Configurazione dell'applicazione con le costanti
    config.update({
        # file dei contribuenti
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        # file dei risultati
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        # file degli errori
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        # file dei log
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # server di calcolo delle imposte
        "server": {
            "urlServer""http://127.0.0.1:5000/",
            "authBasic"True,
            "user": {
                "login""admin",
                "password""admin"
            }
        },
        # modalità debug
        "debug"True
    }
    )

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

    # si esegue la configurazione
    return config
  • righe 31-32: si utilizzerà lo stesso logger |Logger| di quello utilizzato per il server;
  • riga 49: il percorso assoluto del file di log;
  • riga 60: la modalità [debug=True] serve a scrivere le risposte del server web nel file di log;

24.4.2. Il livello [dao]

Il codice della classe [ImpôtsDaoWithHttpClient] subisce lievi modifiche:


# importazioni

import requests
from flask_api import status




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

    # costruttore
    def __init__(self, config: dict):
        # inizializzazione del genitore
        AbstractImpôtsDao.__init__(self, config)
        # memorizzazione degli elementi della configurazione
        # configurazione generale
        self.__config = config
        # server
        self.__config_server = config["server"]
        # modalità debug
        self.__debug = config["debug"]
        # logger
        self.__logger = 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
        
        # modalità debug?
        if self.__debug:
            # logger
            if not self.__logger:
                self.__logger = self.__config['logger']
            # registrazione nel log
            self.__logger.write(f"{response.text}\n")
        # codice di stato della risposta HTTP
        status_code = response.status_code
        
  • riga 17: si memorizza la configurazione generale. Vedremo in seguito che quando viene eseguito il costruttore della classe [ImpôtsDaoWithHttpClient], il dizionario [config] non contiene ancora la chiave [logger] utilizzata alla riga 37. Per questo motivo non è possibile inizializzare [self.__logger] (riga 23) nel costruttore;
  • riga 21: nella configurazione è stata aggiunta una chiave [debug] che controlla il log delle righe 33-39;
  • riga 34: se ci si trova in modalità [debug];
  • righe 36-37: eventuale inizializzazione della proprietà [self.__logger]. Quando si utilizza il metodo [calculate_tax], la chiave [logger] fa parte del dizionario [config];
  • riga 39: viene registrato il documento di testo associato alla risposta HTTP del server;

Il livello [dao] verrà eseguito simultaneamente da più thread. Tuttavia, in questo caso viene creata una sola istanza di tale livello (cfr. config_layers). È quindi necessario verificare che il codice non comporti l’accesso in scrittura a dati condivisi, tipicamente le proprietà della classe [ImpôtsDaoWithHttpClient] che implementa il livello [dao]. Tuttavia, nella riga 37 sopra riportata viene modificata una proprietà dell’istanza della classe. In questo caso ciò non comporta conseguenze poiché tutti i thread condividono lo stesso logger. Se così non fosse stato, l’accesso alla riga 37 avrebbe dovuto essere sincronizzato.

24.4.3. Lo script principale

Lo script principale [main] si evolve come segue:


# 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(dao, logger, taxpayers: list):
    


# elenco dei thread del client
threads = []
logger = None
# codice
try:
    # logger
    logger = Logger(config["logsFilename"])
    # lo si memorizza nella configurazione
    config["logger"] = logger
    # 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(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # calcolo dell'imposta dei contribuenti con più thread
    i = 0
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        # ogni thread elaborerà da 1 a 4 contribuenti
        nb_taxpayers = min(l_taxpayers - i, random.randint(1, 4))
        # l'elenco dei contribuenti elaborati dal thread
        thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
        # si incrementa i per il thread successivo
        i += nb_taxpayers
        # si crea il thread
        thread = threading.Thread(target=thread_function, args=(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
    for thread in threads:
        thread.join()
    # a questo punto tutti i thread hanno terminato il loro lavoro - ciascuno ha modificato uno o più oggetti [taxpayer]
    # si salvano i risultati nel file jSON
    dao.write_taxpayers_results(taxpayers)
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()
  • lo script principale si distingue da quello del client precedente in quanto genererà diversi thread di esecuzione per effettuare le richieste al server. Il client della versione 6 effettuava tutte le sue richieste in modo sequenziale. La richiesta n. i veniva effettuata solo dopo aver ricevuto la risposta alla richiesta n. [i-1]. In questo caso vogliamo vedere come si comporterà il server quando riceve più richieste simultanee. Per questo abbiamo bisogno dei thread;
  • riga 21: i thread generati verranno inseriti in una lista. È importante comprendere che anche lo script [main] viene eseguito da un thread denominato [MainThread]. Questo thread principale creerà altri thread che avranno il compito di calcolare l’imposta di uno o più contribuenti;
  • riga 26: si crea un logger. Questo sarà condiviso da tutti i thread;
  • riga 32: si recuperano tutti i contribuenti per i quali è necessario calcolare l’imposta;
  • righe 39-51: si distribuiscono questi contribuenti su più thread;
  • righe 40-41: ogni thread elaborerà da 1 a 4 contribuenti. Questo numero viene stabilito in modo casuale;
    • [random.randint(1, 4)] genera casualmente un numero dall’elenco [1, 2, 3, 4];
    • il thread non può avere più di [l-i] contribuenti, dove [l-i] rappresenta il numero di contribuenti a cui non è stato ancora assegnato un thread;
    • si prende quindi il minimo dei due valori;
  • riga 43: una volta che [nb_taxpayers], il numero di contribuenti gestiti dal thread, è noto, si prendono questi ultimi dall’elenco dei contribuenti:
    • [slice(10,12)] è l’insieme degli indici [10, 11, 12];
    • [taxpayers[slice(10,12)]] è l'elenco [taxpayers[10], taxpayers[11], taxpayers[12];
  • riga 45: si incrementa il valore di i che controlla il ciclo della riga 39;
  • riga 47: si crea un thread:
    • [target=thread_function] definisce la funzione che verrà eseguita dal thread. Si tratta della funzione delle righe 16-17. Essa richiede tre parametri;
    • [ags] è l'elenco dei tre parametri richiesti dalla funzione [thread_function];

La creazione di un thread non ne comporta l'esecuzione. Si crea semplicemente un oggetto e basta;

  • righe 48-49: il thread appena creato viene aggiunto all’elenco dei thread creati dal thread principale;
  • riga 51: il thread viene avviato. Verrà quindi eseguito in parallelo agli altri thread attivi. In questo caso, eseguirà la funzione [thread_function] con gli argomenti che gli sono stati forniti;
  • righe 53-54: il thread principale attende il completamento di ciascuno dei thread che ha avviato. Facciamo un esempio:
    • il thread principale ha avviato tre thread [th1, th2, th3];
    • il thread principale rimane in attesa di ciascuno dei thread (righe 53-54) nell’ordine del ciclo for: [th1, th2, th3];
    • supponiamo che i thread terminino nell’ordine [th2, th1, th3];
    • il thread principale attende il completamento di th1. Quando th2 termina, non accade nulla;
    • quando th1 termina, il thread principale entra in attesa di th2. Tuttavia, quest’ultimo è già terminato. Il thread principale passa quindi al thread successivo e attende th3;
    • quando th3 termina, il thread principale ha terminato l’attesa e passa quindi all’esecuzione della riga 57;
  • la riga 57 scrive i risultati ottenuti nel file dei risultati. Abbiamo qui un buon esempio di riferimenti agli oggetti:
    • riga 43: la lista [thread_payers] associata a un thread contiene copie dei riferimenti agli oggetti presenti nella lista [taxpayers];
    • si sa che il calcolo dell’imposta modificherà gli oggetti a cui puntano i riferimenti della lista [thread_payers]. Questi oggetti saranno arricchiti con i risultati del calcolo dell’imposta. Tuttavia, i riferimenti stessi non vengono modificati. Pertanto, i riferimenti della lista iniziale [taxpayers] «vedono» o «puntano» agli oggetti modificati;

La funzione [thread_function] eseguita dai thread è la seguente:


# esecuzione del livello [dao] in un thread
# taxpayers è un elenco di contribuenti
def thread_function(dao, logger, taxpayers: list):
    # log di inizio del thread
    thread_name = threading.current_thread().name
    logger.write(f"début du thread [{thread_name}] avec {len(taxpayers)} contribuable(s)\n")
    # si calcola l'imposta dei contribuenti
    for taxpayer in taxpayers:
        # log
        logger.write(f"début du calcul de l'impôt de {taxpayer}\n")
        # calcolo sincrono dell'imposta
        dao.calculate_tax(taxpayer)
        # log
        logger.write(f"fin du calcul de l'impôt de {taxpayer}\n")
    # log fine del thread
    logger.write(f"fin du thread [{thread_name}]\n")
  • le funzioni eseguite simultaneamente da più thread sono spesso complesse da scrivere: occorre sempre verificare che il codice non tenti di modificare un dato condiviso tra i thread. Quando si verifica quest’ultimo caso, è necessario implementare un accesso sincronizzato al dato condiviso che verrà modificato;
  • riga 3: la funzione riceve tre parametri:
    • [dao]: un riferimento al livello [dao]. Questo dato è condiviso;
    • [logger]: un riferimento al logger. Questo dato è condiviso;
    • [taxpayers]: un elenco di contribuenti. Questo dato non è condiviso: ogni thread gestisce un elenco diverso;
  • esaminiamo i due riferimenti [dao, logger]:
    • abbiamo visto che l’oggetto puntato dal riferimento [dao] aveva un riferimento [self.__logger] che veniva modificato dai thread, ma per inserirvi un valore comune a tutti i thread;
    • il riferimento [logger] punta a un descrittore di file. Abbiamo visto che poteva verificarsi un problema durante la scrittura dei log nel file. Per questo motivo la scrittura nel file è stata sincronizzata;
  • righe 5-6: si registra il nome del thread e il numero di contribuenti che deve gestire;
  • righe 8-14: calcolo dell’imposta dei contribuenti;
  • riga 16: viene registrato il termine del thread;

24.4.4. Esecuzione

Avviamo il server web come nel paragrafo precedente (server web, SGBD, hMailServer, Thunderbird), quindi eseguiamo lo script [main] dal client. Nei file [data/output/errors.txt, data/output/résultats.json] si ottengono gli stessi risultati della versione precedente. Nel file [data/logs/logs.txt] sono presenti i seguenti log:


2020-07-24 10:05:20.942404, Thread-1 : début du thread [Thread-1] avec 1 contribuable(s)
2020-07-24 10:05:20.943458, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-24 10:05:20.943458, Thread-2 : début du thread [Thread-2] avec 3 contribuable(s)
2020-07-24 10:05:20.946502, Thread-3 : début du thread [Thread-3] avec 1 contribuable(s)
2020-07-24 10:05:20.946502, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-24 10:05:20.947003, Thread-3 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000}
2020-07-24 10:05:20.947003, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
2020-07-24 10:05:20.950324, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000}
2020-07-24 10:05:20.948449, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
2020-07-24 10:05:20.953645, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000}
2020-07-24 10:05:20.976143, 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-24 10:05:20.976695, 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-24 10:05:20.976695, Thread-1 : fin du thread [Thread-1]
2020-07-24 10:05:21.973914, 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-24 10:05:21.973914, 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-24 10:05:21.973914, Thread-2 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-24 10:05:21.977130, 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-24 10:05:21.977130, 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-24 10:05:21.977130, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000}
2020-07-24 10:05:21.982634, Thread-3 : {"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-24 10:05:21.982634, 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-24 10:05:21.983134, Thread-3 : 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-24 10:05:21.983134, 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-24 10:05:21.983134, Thread-3 : fin du thread [Thread-3]
2020-07-24 10:05:21.983763, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000}
2020-07-24 10:05:22.008562, 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-24 10:05:22.008562, 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-24 10:05:22.009062, Thread-5 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-24 10:05:22.016848, Thread-5 : {"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-24 10:05:22.017349, Thread-5 : 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-24 10:05:22.017349, Thread-5 : fin du thread [Thread-5]
2020-07-24 10:05:23.008486, Thread-2 : {"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-24 10:05:23.008486, Thread-2 : 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-24 10:05:23.009749, Thread-2 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000}
2020-07-24 10:05:23.011722, 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-24 10:05:23.013723, 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-24 10:05:23.013723, Thread-4 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000}
2020-07-24 10:05:23.024135, Thread-2 : {"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-24 10:05:23.024135, Thread-2 : 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-24 10:05:23.025178, Thread-2 : fin du thread [Thread-2]
2020-07-24 10:05:23.025178, Thread-4 : {"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-24 10:05:23.026191, Thread-4 : 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-24 10:05:23.026191, Thread-4 : fin du thread [Thread-4]
  • questi log mostrano che sono stati avviati cinque thread per calcolare l’imposta di 11 contribuenti. Questi cinque thread hanno inviato richieste simultanee al server di calcolo dell’imposta. È necessario comprendere come funziona:
    • il thread [Thread-1] viene avviato per primo. Quando ha il controllo del processore, procede nell’esecuzione del codice fino a inviare la sua richiesta HTTP. Poiché deve attendere il risultato di tale richiesta, viene automaticamente messo in attesa. A quel punto perde il controllo del processore, che viene acquisito da un altro thread;
    • righe 1-10: lo stesso processo si ripete per ciascuno dei 5 thread. In questo modo i 5 thread vengono avviati prima ancora che il thread [Thread-1] abbia ricevuto la sua risposta (riga 11);
  • i thread non terminano nell'ordine in cui sono stati avviati. Pertanto, è il thread [Thread-3] a terminare per primo, alla riga 23;

Sul lato server, i log nel file [data/logs/logs.txt] sono i seguenti:


2020-07-24 10:05:01.692980, MainThread : [serveur] démarrage du serveur
2020-07-24 10:05:01.877251, MainThread : [serveur] connexion à la base de données réussie
2020-07-24 10:05:03.596162, MainThread : [serveur] démarrage du serveur
2020-07-24 10:05:03.661160, MainThread : [serveur] connexion à la base de données réussie
2020-07-24 10:05:20.968053, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=sì&figli=2&stipendio=50000' [GET]>
2020-07-24 10:05:20.969132, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.970316, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=sì&figli=3&stipendio=100000' [GET]>
2020-07-24 10:05:20.970316, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.971335, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=sì&figli=2&stipendio=55555' [GET]>
2020-07-24 10:05:20.972563, Thread-4 : [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-24 10:05:20.974796, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=no&figli=3&stipendio=100000' [GET]>
2020-07-24 10:05:20.974796, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.976143, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=sì&figli=2&stipendio=30000' [GET]>
2020-07-24 10:05:20.976143, Thread-6 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:21.970615, Thread-2 : [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-24 10:05:21.973914, Thread-3 : [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-24 10:05:21.977130, Thread-6 : [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-24 10:05:21.977130, Thread-5 : [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-24 10:05:22.001693, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=sì&figli=3&stipendio=50000' [GET]>
2020-07-24 10:05:22.003013, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:22.003013, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=sì&figli=5&stipendio=100000' [GET]>
2020-07-24 10:05:22.003013, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:22.005871, Thread-9 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=no&figli=0&stipendio=200000' [GET]>
2020-07-24 10:05:22.006370, Thread-9 : [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-24 10:05:22.014170, Thread-10 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=sì&figli=3&stipendio=200000' [GET]>
2020-07-24 10:05:22.014170, Thread-10 : [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-24 10:05:23.003533, Thread-7 : [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-24 10:05:23.006434, Thread-8 : [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}}}
2020-07-24 10:05:23.018026, Thread-11 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=no&figli=2&stipendio=100000' [GET]>
2020-07-24 10:05:23.019074, Thread-11 : [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-24 10:05:23.021447, Thread-12 : [index] requête : <Request 'http://127.0.0.1:5000/?sposato=no&figli=0&stipendio=100000' [GET]>
2020-07-24 10:05:23.022447, Thread-12 : [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}}}
  • si vede che 11 thread hanno elaborato gli 11 contribuenti;
  • alcuni thread sono stati messi in attesa (righe 6, 8, 12, 14, 20, 22) e altri no (righe 9, 23, 25, 29, 31);

24.5. Test del livello [dao]

Come abbiamo fatto nella |versione precedente|, testiamo il livello [dao] del client. Il principio è esattamente lo stesso:

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({})

    # logger
    logger = Logger(config["logsFilename"])
    # viene memorizzato nella configurazione
    config["logger"] = logger
    # si recupera il livello [dao]
    dao = config["layers"]["dao"]

    # 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/02/tests/TestHttpClientDao.py
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 6.128s

OK

Process finished with exit code 0