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, ad eccezione dei seguenti dettagli:

  • il client web invierà più richieste HTTP contemporaneamente. Nella versione precedente, queste richieste venivano inviate in sequenza. Il server poteva quindi elaborare solo una singola richiesta alla volta;
  • il server sarà multithread: sarà in grado di elaborare più richieste contemporaneamente;
  • Per monitorare l'esecuzione di queste richieste, il server web sarà dotato di un logger che registrerà i momenti chiave dell'elaborazione delle richieste in un file di testo;
  • il server invierà un'e-mail all'amministratore dell'applicazione quando incontra un problema che gli impedisce di avviarsi, tipicamente un problema con il database associato al server web;

L'architettura dell'applicazione rimane invariata:

Image

La struttura delle directory degli script è la seguente:

Image

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

24.2. Le utility

Image

24.2.1. La classe [Logger]

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

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

from ImpôtsError import ImpôtsError


class Logger:
    #  class attribute
    verrou = threading.RLock()

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

    #  writing a log
    def write(self, message: str):
        #  current date / time
        today = date.today()
        now = datetime.time(datetime.now())
        #  thread name
        thread_name = current_thread().name
        #  you don't want to be disturbed while writing to the log file
        #  we request the class's synchronization object (= lock) - only one thread will get it
        Logger.verrou.acquire()
        try:
            #  log entry
            self.__resource.write(f"{today} {now}, {thread_name} : {message}")
            #  write immediately - otherwise the text will only be written when the write stream is closed
            #  we want to track logs over time
            self.__resource.flush()
        finally:
            #  release the synchronization object (= lock) so that another thread can obtain it
            Logger.verrou.release()

    #  freeing up resources
    def close(self):
        #  close file
        if self.__resource:
            self.__resource.close()
  • Righe 10–11: Definiamo un attributo di classe. Un attributo di classe è una proprietà condivisa da tutte le istanze della classe. Si fa riferimento ad esso utilizzando la notazione [Classe.attributo_di_classe] (righe 30, 39). L'attributo di classe [lock] fungerà da oggetto di sincronizzazione per tutti i thread che eseguono il codice nelle righe 31–36;
  • righe 14–19: Il costruttore riceve il percorso assoluto del file di log. Questo 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 messaggio vengono aggiunte due informazioni:
    • riga 24: la data corrente;
    • riga 25: l'ora corrente;
    • riga 27: il nome del thread che scrive il log. È importante ricordare che un'applicazione web serve più utenti contemporaneamente. A ogni richiesta viene assegnato un thread per eseguirla. Se questo thread viene messo in pausa — tipicamente per un'operazione di I/O (rete, file, database) — allora il processore viene ceduto a un altro thread. A causa di queste possibili interruzioni, non possiamo essere certi che un thread riesca a scrivere una riga nel file di log senza essere interrotto. Esiste quindi il rischio che i log di due thread diversi possano confondersi. Il rischio è basso, forse addirittura nullo, ma abbiamo comunque deciso di mostrare come sincronizzare l’accesso di due thread a una risorsa condivisa, 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. È infatti unica: un attributo di classe è unico per tutte le istanze della classe;
    • Al tempo T1, un thread denominato Thread1 ottiene la chiave. Può quindi eseguire la riga 33;
    • Al tempo T2, il thread Thread1 viene messo in pausa prima ancora di aver finito di scrivere il log;
    • Al tempo T3, anche il thread Thread2, che ha acquisito il processore, deve scrivere un log. Raggiunge quindi la riga 30, dove richiede la chiave della porta d'ingresso. Gli viene comunicato che un altro thread la possiede già. Viene quindi automaticamente messo in pausa. Questo accadrà per tutti i thread che richiedono questa chiave;
    • Al tempo T4, il thread Thread1, che era stato messo in pausa, riacquista il processore. Quindi termina 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 funziona con un buffer. L'operazione [write] alla riga 33 scrive su questo buffer ma non direttamente sul file. Il buffer viene quindi scaricato sul file in determinate condizioni:
        • il buffer è pieno;
        • il descrittore di file subisce un'operazione [close] o [flush];
  • riga 36: forziamo la scrittura della riga di log nel file. Lo facciamo perché vogliamo vedere i log dei diversi thread intercalati. Se non lo facessimo, i log di un singolo thread verrebbero scritti tutti contemporaneamente — quando il descrittore viene chiuso alla riga 45. Sarebbe quindi molto più difficile vedere che alcuni thread sono stati arrestati: dovremmo controllare i timestamp nei log;
  • riga 39: il thread Thread1 restituisce il blocco che gli era stato assegnato. Ora può essere assegnato a un altro thread;
  • riga 22: il metodo [write] è quindi sincronizzato: solo un 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 procedere alla riga successiva. La mantiene fino a quando non la restituisce (riga 39);
  • righe 41–45: il metodo [close] libera le risorse allocate al descrittore del file di log;

I log scritti nel file di log avranno questo aspetto:

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

24.2.2. La classe [SendAdminMail]

La classe [SendAdminMail] consente di inviare un messaggio all'amministratore dell'applicazione quando l'applicazione va in crash.

Image

La classe [SendAdminMail] è configurata nello script [config] [2] come segue:

        #  server config SMTP
        "adminMail": {
            #  server SMTP
            "smtp-server": "localhost",
            #  server port SMTP
            "smtp-port": "25",
            #  director
            "from": "guest@localhost.com",
            "to": "guest@localhost.com",
            #  mail subject
            "subject": "plantage du serveur de calcul d'impôts",
            #  tls to True if server SMTP requires authorization, False otherwise
            "tls": False
        }

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

#  imports
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):
        #  sends message to smtp server config['smtp-server'] on port config[smtp-port]
        #  if config['tls'] is true, TLS support will be used
        #  mail is sent from config['from']
        #  for recipient config['to']
        #  message has subject config['subject']
        #  a logger reference can be found in config['logger']

        #  retrieve logger from config - can be None
        logger = config["logger"]
        #  server SMTP
        server = None
        #  we send the message
        try:
            #  the SMTP server
            server = smtplib.SMTP(config["smtp-server"])
            #  verbose mode
            server.set_debuglevel(verbose)
            #  secure connection?
            if config['tls']:
                #  start of safety dialogue
                server.starttls()
                #  authentication
                server.login(config["user"], config["password"])
            #  construction of a Multipart message - this is the message that will be sent
            msg = MIMEText(message)
            msg['From'] = config["from"]
            msg['To'] = config["to"]
            msg['Date'] = formatdate(localtime=True)
            msg['Subject'] = config["subject"]
            #  we send the message
            server.send_message(msg)
            #  log - the logger may not exist
            if logger:
                logger.write(f"[SendAdminMail] Message envoyé à [{config['to']}] : [{message}]\n")
        except BaseException as erreur:
            #  log- the logger may not exist
            if logger:
                logger.write(
                    f"[SendAdminMail] Erreur [{erreur}] lors de l'envoi à [{config['to']}] du message [{message}] : \n")
        finally:
            #  we're done - we release the resources mobilized by the function
            if server:
                server.quit()
  • righe 24-54: questo è il codice già trattato nell'esempio |smtp/02|;
  • riga 20: recuperiamo il riferimento di un logger. Questo viene utilizzato alle righe 45 e 49;

24.3. Il server web

24.3.1. Configurazione

La configurazione del server è molto simile a quella del server discusso in precedenza. Solo il file [config.py] è leggermente cambiato:

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

    #  step 1 ------

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

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

    #  absolute dependencies
    absolute_dependencies = [
        #  project files
        #  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",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  IndexController
        f"{root_dir}/impots/http-servers/01/controllers",
        #  scripts [config_database, config_layers]
        script_dir,
        #  Logger, SendAdminMail
        f"{script_dir}/../utilities",
    ]
    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration
    config.update({
        #  users authorized to use the application
        "users": [
            {
                "login": "admin",
                "password": "admin"
            }
        ],
        #  log file
        "logsFilename": f"{script_dir}/../data/logs/logs.txt",
        #  server config SMTP
        "adminMail": {
            #  server SMTP
            "smtp-server": "localhost",
            #  server port SMTP
            "smtp-port": "25",
            #  director
            "from": "guest@localhost.com",
            "to": "guest@localhost.com",
            #  mail subject
            "subject": "plantage du serveur de calcul d'impôts",
            #  tls to True if server SMTP requires authorization, False otherwise
            "tls": False
        },
        #  thread pause time in seconds
        "sleep_time": 0
    })

    #  step 3 ------
    #  database configuration
    import config_database
    config["database"] = config_database.configure(config)

    #  step 4 ------
    #  instantiation of application layers
    import config_layers
    config['layers'] = config_layers.configure(config)

    #  we return the configuration
    return config
  • righe 40–66: aggiungiamo al dizionario di configurazione del server 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 vedere meglio i thread in azione, ne forzeremo alcuni a mettersi in pausa. [sleep_time] è la durata della pausa espressa in secondi;
  • righe 27–28: si noti che stiamo utilizzando [index_controller] della precedente versione 6;

24.3.2. Lo script principale [main]

Lo script principale [main] è il seguente:

#  a mysql or pgres parameter is expected
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()

#  configure the application
import config
config = config.configure({'sgbd': sgbd})

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

#  authentication manager
auth = HTTPBasicAuth()


@auth.verify_password
def verify_password(login, password):
    #  user list
    users = config['users']
    #  browse this list
    for user in users:
        if user['login'] == login and user['password'] == password:
            return True
    #  we didn't find
    return False


#  send an e-mail to the administrator
def send_adminmail(config: dict, message: str):
    #  send an e-mail to the application administrator
    config_mail = config["adminMail"]
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)


#  check log file
logger = None
erreur = False
message_erreur = None
try:
    #  logger
    logger = Logger(config["logsFilename"])
except BaseException as exception:
    #  log console
    print(f"L'erreur suivante s'est produite : {exception}")
    #  we note the error
    erreur = True
    message_erreur = f"{exception}"
#  store the logger in the config
config['logger'] = logger
#  error handling
if erreur:
    #  mail to administrator
    send_adminmail(config, message_erreur)
    #  end of application
    sys.exit(1)

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

#  data recovery from tax authorities
erreur = False
try:
    #  admindata will be read-only application data
    config["admindata"] = config["layers"]["dao"].get_admindata()
    #  success log
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    #  we note the error
    erreur = True
    #  error log
    log = f"L'erreur suivante s'est produite : {ex}"
    #  console
    print(log)
    #  log file
    logger.write(f"{log}\n")
    #  mail to administrator
    send_adminmail(config, log)

#  the main thread no longer needs the logger
logger.close()

#  if there has been an error, we stop
if erreur:
    sys.exit(2)

#  the Flask application can be started
app = Flask(__name__)


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


#  hand only
if __name__ == '__main__':
    #  start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • righe 1-10: lo script si aspetta un parametro [mysql / pgres] che specifichi il DBMS da utilizzare;
  • righe 12–14: l'applicazione viene configurata (Python Path, livelli, database);
  • righe 16–28: dipendenze richieste dall'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: prepariamo la configurazione dell'e-mail;
    • invieremo l'e-mail;
  • righe 54–74: verifichiamo la presenza del file di log;
  • righe 70–74: se non siamo riusciti ad aprire il file di log, inviamo un'e-mail all'amministratore e usciamo;
  • righe 76–79: registriamo l'avvio del server;
  • righe 81–98: recuperiamo i dati dell'amministrazione fiscale dal database;
  • righe 88–98: se non siamo riusciti a recuperare questi dati, registriamo l'errore sia sulla console che nel file di log;
  • righe 100–101: il thread principale non registrerà più (i thread creati non useranno lo stesso descrittore di file);
  • righe 103–105: se non siamo riusciti a connetterci al database, ci fermiamo;
  • riga 122: il server viene avviato in modalità multithread;

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

#  Home URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    logger = None
    try:
        #  logger
        logger = Logger(config["logsFilename"])
        #  we store it in a config associated with the thread
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        #  log the request
        logger.write(f"[index] requête : {request}\n")
        #  the thread is interrupted if requested
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            #  pause is randomized so that some threads are interrupted and others not
            aléa = randint(0, 1)
            if aléa == 1:
                #  log before break
                logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                #  break
                time.sleep(sleep_time)
        #  the request is executed by a controller
        résultat, status_code = index_controller.execute(request, config)
        #  was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            #  send an e-mail to the application administrator
            config_mail = config["adminMail"]
            config_mail["logger"] = logger
            SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
        #  we log the answer
        logger.write(f"[index] {résultat}\n")
        #  we send the answer
        return json_response(résultat, status_code)
    except BaseException as erreur:
        #  log the error if possible
        if logger:
            logger.write(f"[index] {erreur}")
        #  we prepare the response to the customer
        résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
        #  we send the answer
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        #  close the log file if it has been opened
        if logger:
            logger.close()
  • Riga 4: la funzione eseguita quando un utente richiede l'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. Tenetelo sempre presente quando il codice accede a una risorsa condivisa da tutti i thread. In questo caso, quella risorsa è il file di log: tutti i thread vi scrivono;
  • riga 8: creiamo un'istanza del logger. Pertanto, tutti i thread avranno 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 alcun effetto sui logger degli altri thread;
  • Righe 9–12: memorizziamo il logger nel dizionario [config] dell'applicazione sotto una chiave che prende il nome dal thread. Pertanto, se ci sono n thread in esecuzione simultanea, verranno create n voci nel dizionario [config]. [config] è una risorsa condivisa tra tutti i thread. Pertanto, potrebbe essere necessaria la sincronizzazione. Ho fatto un'ipotesi qui. Ho ipotizzato che se due thread creassero simultaneamente le loro voci nel file [config] e uno di essi fosse interrotto dall'altro, ciò non avrebbe alcun impatto. Il thread interrotto potrebbe completare in seguito la creazione della voce. Se i test dimostrassero che questa ipotesi è falsa, l'accesso alla riga 12 dovrebbe essere sincronizzato;
  • riga 10: inseriamo il logger in un dizionario;
  • riga 11: [threading.current_thread()] è il thread che esegue questa riga e quindi il thread che esegue la funzione [index]. Ne registriamo il nome. Ogni thread ha un nome univoco;
  • riga 12: memorizziamo la configurazione del thread. D'ora in poi, procederemo sempre come segue: se ci sono informazioni che non possono essere condivise tra i thread, saranno comunque inserite nella configurazione generale, ma associate al nome del thread;
  • riga 14: registriamo la richiesta che stiamo attualmente eseguendo;
  • righe 15–24: mettiamo in pausa in modo casuale alcuni thread in modo che cedano il processore a un altro thread;
    • riga 16: recuperiamo la durata della pausa (in secondi) dalla configurazione;
    • riga 17: si verifica una pausa solo se la durata della pausa non è 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: registriamo il fatto che il thread sta per essere messo in pausa;
    • riga 24: il thread viene messo in pausa per [sleep_time] secondi;
  • riga 26: quando il thread si riattiva, fa eseguire la richiesta al modulo [index_controller];
  • righe 28–32: se questa esecuzione causa un [500 INTERNAL SERVER ERROR], viene inviata un'e-mail all'amministratore;
    • righe 30-31: configuriamo il dizionario [config_mail] che passeremo alla classe [SendAdminMail];
    • riga 32: il messaggio inviato all'amministratore è la stringa JSON del risultato che verrà inviato al client;
  • righe 33–34: registriamo la risposta che verrà inviata al client (riga 36);
  • righe 37–44: gestiamo eventuali eccezioni;
  • righe 39–40: se il logger esiste, registriamo l'errore verificatosi;
  • righe 47–48: chiudiamo il logger se esiste. In definitiva, il thread crea un logger all'inizio della richiesta e lo chiude una volta che la richiesta è 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

Avviamo il server Flask, il server di posta |hMailServer| e il client di posta |Thunderbird|. Non avviamo il DBMS. 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] è 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)]]

Utilizzando Thunderbird, controllare le e-mail dell'amministratore [guest@localhost.com]:

Image

Quindi avvia il DBMS e richiedi l'URL [http://127.0.0.1:5000/?mari%C3%A9=oui&enfants=3&salaire=200000]. I log diventano come segue:


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/?marié=oui&enfants=3&salaire=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: si noti che il server si avvia due volte perché la modalità [Debug=True] innesca un secondo avvio;
  • righe 5-6: i log ci danno un'idea del tempo di esecuzione di una richiesta, qui 2,293 millisecondi;

24.4. Il client web

Image

La directory [http-clients/02] viene creata copiando la directory [http-clients/01]. Successivamente apportiamo alcune modifiche.

24.4.1. La configurazione

La configurazione [config] dell'applicazione [http-clients/02] è la stessa di quella dell'applicazione [http-clients/01], con alcune piccole differenze:

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

    #  step 1 ------

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

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

    #  absolute dependencies
    absolute_dependencies = [
        #  project files
        #  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",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        #  configuration scripts
        script_dir,
        #  Logger
        f"{root_dir}/impots/http-servers/02/utilities",
    ]

    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration with constants
    config.update({
        #  taxpayer file
        "taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
        #  results file
        "resultsFilename": f"{script_dir}/../data/output/résultats.json",
        #  error file
        "errorsFilename": f"{script_dir}/../data/output/errors.txt",
        #  log file
        "logsFilename": f"{script_dir}/../data/logs/logs.txt",
        #  tax calculation server
        "server": {
            "urlServer": "http://127.0.0.1:5000/",
            "authBasic": True,
            "user": {
                "login": "admin",
                "password": "admin"
            }
        },
        #  debug mode
        "debug": True
    }
    )

    #  step 3 ------
    #  layer instantiation
    import config_layers
    config['layers'] = config_layers.configure(config)

    #  we return the configuration
    return config
  • righe 31-32: useremo lo stesso logger |Logger| utilizzato per il server;
  • riga 49: il percorso assoluto del file di log;
  • riga 60: la modalità [debug=True] viene utilizzata per scrivere le risposte del server web nel file di log;

24.4.2. Il livello [dao]

Il codice della classe [ImpôtsDaoWithHttpClient] cambia leggermente:

#  imports

import requests
from flask_api import status




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

    #  manufacturer
    def __init__(self, config: dict):
        #  parent initialization
        AbstractImpôtsDao.__init__(self, config)
        #  saving configuration items
        #  general configuration
        self.__config = config
        #  server
        self.__config_server = config["server"]
        #  debug mode
        self.__debug = config["debug"]
        #  logger
        self.__logger = None

    #  unused method
    def get_admindata(self) -> AdminData:
        pass

    #  tAX CALCULATION
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        #  we let the exceptions rise
        
        #  debug mode?
        if self.__debug:
            #  logger
            if not self.__logger:
                self.__logger = self.__config['logger']
            #  log on
            self.__logger.write(f"{response.text}\n")
        #  response status code HTTP
        status_code = response.status_code
        
  • Riga 17: memorizziamo 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. Questo è il motivo per cui non possiamo inizializzare [self.__logger] (riga 23) nel costruttore;
  • riga 21: abbiamo aggiunto una chiave [debug] alla configurazione che controlla la registrazione nelle righe 33–39;
  • riga 34: se siamo in modalità [debug];
  • righe 36–37: inizializzazione opzionale della proprietà [self.__logger]. Quando viene utilizzato il metodo [calculate_tax], la chiave [logger] fa parte del dizionario [config];
  • riga 39: registriamo il documento di testo associato alla risposta HTTP del server;

Il livello [dao] verrà eseguito simultaneamente da più thread. Tuttavia, qui creiamo una singola istanza di questo livello (vedi config_layers). Dobbiamo quindi 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, nel codice sopra riportato, la riga 37 modifica una proprietà dell'istanza della classe. In questo caso, ciò non ha conseguenze perché 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 sviluppa come segue:

#  configure the application

import config
config = config.configure({})

#  dependencies
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger


#  executing the [dao] layer in a thread
#  taxpayers is a list of taxpayers
def thread_function(dao, logger, taxpayers: list):
    


#  list of client threads
threads = []
logger = None
#  code
try:
    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger
    #  we recover the [dao] layer
    dao = config["layers"]["dao"]
    #  reading taxpayer data
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #  taxpayers?
    if not taxpayers:
        raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #  multi-threaded tax calculation for taxpayers
    i = 0
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        #  each thread will process from 1 to 4 contributors
        nb_taxpayers = min(l_taxpayers - i, random.randint(1, 4))
        #  the list of taxpayers processed by the thread
        thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
        #  increment i for the next thread
        i += nb_taxpayers
        #  create the thread
        thread = threading.Thread(target=thread_function, args=(dao, logger, thread_taxpayers))
        #  we add it to the list of threads in the main script
        threads.append(thread)
        #  we launch the thread - this operation is asynchronous - we don't wait for the thread's result
        thread.start()
    #  the main thread waits for all threads it has launched to finish
    for thread in threads:
        thread.join()
    #  here all threads have finished their work - each has modified one or more objects [taxpayer]
    #  save the results in the jSON file
    dao.write_taxpayers_results(taxpayers)
except BaseException as erreur:
    #  error display
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  close the logger
    if logger:
        logger.close()
    #  we're done
    print("Travail terminé...")
    #  end of threads that might still exist if we stopped on error
    sys.exit()
  • Lo script principale differisce da quello del client precedente in quanto genererà più thread di esecuzione per inviare richieste al server. Il client nella versione 6 inviava tutte le sue richieste in modo sequenziale. La richiesta #i veniva effettuata solo dopo aver ricevuto la risposta alla richiesta #[i-1]. Qui, vogliamo vedere come si comporta il server quando riceve più richieste simultanee. Per questo, abbiamo bisogno di thread;
  • riga 21: i thread generati saranno inseriti in una lista. È importante comprendere che anche lo script [main] viene eseguito da un thread chiamato [MainThread]. Questo thread principale creerà altri thread che saranno responsabili del calcolo dell'imposta per uno o più contribuenti;
  • riga 26: creiamo un logger. Questo sarà condiviso da tutti i thread;
  • riga 32: recuperiamo tutti i contribuenti per i quali è necessario calcolare le imposte;
  • righe 39–51: distribuiamo questi contribuenti su diversi thread;
  • righe 40–41: ogni thread elaborerà da 1 a 4 contribuenti. Questo numero viene determinato in modo casuale;
    • [random.randint(1, 4)] genera in modo casuale un numero dall'elenco [1, 2, 3, 4];
    • il thread non può contenere più di [l-i] contribuenti, dove [l-i] rappresenta il numero di contribuenti a cui non è stato ancora assegnato un thread;
    • prendiamo quindi il minimo dei due valori;
  • riga 43: una volta noto [nb_taxpayers], il numero di contribuenti elaborati dal thread, li prendiamo dall'elenco dei contribuenti:
    • [slice(10,12)] è l'insieme degli indici [10, 11, 12];
    • [contribuenti[slice(10,12)]] è l'elenco [contribuenti[10], contribuenti[11], contribuenti[12] ;
  • riga 45: incrementiamo il valore di i, che controlla il ciclo alla riga 39;
  • riga 47: creiamo un thread:
    • [target=thread_function] imposta la funzione che il thread eseguirà. Si tratta della funzione delle righe 16–17. Essa richiede tre parametri;
    • [ags] è l'elenco dei tre parametri previsti dalla funzione [thread_function];

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

  • 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 con gli altri thread attivi. Qui, eseguirà [thread_function] con gli argomenti forniti;
  • righe 53–54: il thread principale attende il completamento di ciascuno dei thread che ha lanciato. Facciamo un esempio:
    • il thread principale ha lanciato tre thread [th1, th2, th3];
    • Il thread principale attende 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 che th1 finisca. Quando th2 finisce, non succede nulla;
    • Quando th1 termina, il thread principale attende th2. Tuttavia, th2 ha già terminato. Il thread principale passa quindi al thread successivo e attende th3;
    • quando th3 termina, il thread principale ha terminato l'attesa e procede all'esecuzione della riga 57;
  • la riga 57 scrive i risultati nel file dei risultati. Questo è un buon esempio di riferimenti agli oggetti:
    • riga 43: la lista [thread_payers] associata a un thread contiene copie dei riferimenti agli oggetti contenuti nella lista [taxpayers];
    • sappiamo che il calcolo delle imposte modificherà gli oggetti a cui puntano i riferimenti nella lista [thread_payers]. Questi oggetti saranno aggiornati con i risultati del calcolo delle imposte. Tuttavia, i riferimenti stessi non vengono modificati. Pertanto, i riferimenti nella lista iniziale [taxpayers] “vedono” o “puntano” agli oggetti modificati;

La [thread_function] eseguita dai thread è la seguente:

#  executing the [dao] layer in a thread
#  taxpayers is a list of taxpayers
def thread_function(dao, logger, taxpayers: list):
    #  log thread start
    thread_name = threading.current_thread().name
    logger.write(f"début du thread [{thread_name}] avec {len(taxpayers)} contribuable(s)\n")
    #  taxpayers' taxes are calculated
    for taxpayer in taxpayers:
        #  log
        logger.write(f"début du calcul de l'impôt de {taxpayer}\n")
        #  synchronous tax calculation
        dao.calculate_tax(taxpayer)
        #  log
        logger.write(f"fin du calcul de l'impôt de {taxpayer}\n")
    #  log end of thread
    logger.write(f"fin du thread [{thread_name}]\n")
  • Le funzioni eseguite simultaneamente da più thread sono spesso difficili da scrivere: è necessario verificare sempre che il codice non tenti di modificare i dati condivisi tra i thread. Quando ciò si verifica, è necessario implementare un accesso sincronizzato ai dati condivisi che stanno per essere modificati;
  • Riga 3: La funzione riceve tre parametri:
    • [dao]: un riferimento al livello [dao]. Questi dati sono condivisi;
    • [logger]: un riferimento al logger. Questi dati sono condivisi;
    • [taxpayers]: un elenco di contribuenti. Questi dati non sono condivisi: 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 impostare un valore comune a tutti i thread;
    • il riferimento [logger] punta a un descrittore di file. Abbiamo visto che potrebbe esserci un problema durante la scrittura dei log nel file. Per questo motivo, la scrittura nel file è stata sincronizzata;
  • righe 5–6: registriamo il nome del thread e il numero di contribuenti che deve gestire;
  • righe 8–14: calcolo delle imposte dei contribuenti;
  • riga 16: registriamo la fine del thread;

24.4.4. Esecuzione

Avviamo il server web come descritto nella sezione precedente (server web, DBMS, hMailServer, Thunderbird), quindi eseguiamo lo script [main] del client. Nei file [data/output/errors.txt, data/output/results.json], otteniamo gli stessi risultati della versione precedente. Nel file [data/logs/logs.txt], abbiamo 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 le imposte di 11 contribuenti. Questi cinque thread hanno inviato richieste simultanee al server di calcolo delle imposte. È importante capire come funziona:
    • Il thread [Thread-1] viene avviato per primo. Quando ha la CPU, esegue il codice fino a quando non invia la sua richiesta HTTP. Dato che deve attendere il risultato di questa richiesta, viene automaticamente messo in attesa. Quindi perde la CPU e un altro thread la prende in carico;
    • righe 1–10: lo stesso processo si ripete per ciascuno dei 5 thread. Pertanto, i 5 thread vengono avviati prima ancora che il thread [Thread-1] abbia ricevuto la sua risposta alla riga 11;
  • I thread non terminano nell'ordine in cui sono stati avviati. Pertanto, il thread [Thread-3] termina per primo, 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/?marié=oui&enfants=2&salaire=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/?marié=oui&enfants=3&salaire=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/?marié=oui&enfants=2&salaire=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/?marié=non&enfants=3&salaire=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/?marié=oui&enfants=2&salaire=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/?marié=oui&enfants=3&salaire=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/?marié=oui&enfants=5&salaire=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/?marié=non&enfants=0&salaire=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/?marié=oui&enfants=3&salaire=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/?marié=non&enfants=2&salaire=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/?marié=non&enfants=0&salaire=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}}}
  • Possiamo vedere 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|, stiamo testando 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

        #  { 'married': 'yes', 'children': 2, 'salary': 55555,
        #  tax': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        #  check
        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__':
    #  configure the application
    import config
    config = config.configure({})

    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger
    #  we recover the [dao] layer
    dao = config["layers"]["dao"]

    #  test methods are executed
    print("tests en cours...")
    unittest.main()
  • Creiamo una |configurazione di esecuzione| per questo test;
  • Avviamo il server web con il suo intero ambiente;
  • eseguiamo 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