Skip to content

24. Exercício prático: Versão 7

24.1. Introdução

A versão 7 da aplicação de cálculo de impostos é idêntica à versão 6, exceto pelos seguintes detalhes:

  • o cliente web enviará múltiplas solicitações HTTP simultaneamente. Na versão anterior, estas solicitações eram enviadas sequencialmente. O servidor só podia, portanto, processar uma única solicitação de cada vez;
  • o servidor será multithread: poderá processar várias solicitações simultaneamente;
  • Para acompanhar a execução destas solicitações, o servidor web será equipado com um registador que irá registar momentos-chave no processamento das solicitações num ficheiro de texto;
  • o servidor enviará um e-mail ao administrador da aplicação quando encontrar um problema que o impeça de iniciar, normalmente uma questão relacionada com a base de dados associada ao servidor web;

A arquitetura da aplicação permanece inalterada:

Image

A estrutura de diretórios dos scripts é a seguinte:

Image

A pasta [http-servers/02] é criada inicialmente através da cópia da pasta [http-servers/01]. Em seguida, são feitas alterações nessa pasta.

24.2. Os utilitários

Image

24.2.1. A classe [Logger]

A classe [Logger] permite que determinadas ações do servidor web sejam registadas num ficheiro de texto:

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()
  • Linhas 10–11: Definimos um atributo de classe. Um atributo de classe é uma propriedade partilhada por todas as instâncias da classe. É referenciado utilizando a notação [Classe.atributo_da_classe] (linhas 30, 39). O atributo de classe [lock] servirá como objeto de sincronização para todos os threads que executam o código nas linhas 31–36;
  • linhas 14–19: O construtor recebe o caminho absoluto do ficheiro de registo. Este ficheiro é então aberto, e o descritor de ficheiro recuperado é armazenado na classe;
  • linha 17: o ficheiro de registo é aberto no modo «append» (a). Cada linha escrita será acrescentada ao final do ficheiro;
  • linhas 22–39: o método [write] permite que uma mensagem passada como parâmetro seja gravada no ficheiro de registo. Duas informações são anexadas a esta mensagem:
    • linha 24: a data atual;
    • linha 25: a hora atual;
    • linha 27: o nome do thread que está a escrever o registo. É importante lembrar aqui que uma aplicação web atende vários utilizadores simultaneamente. A cada pedido é atribuído um thread para o executar. Se este thread for pausado — normalmente para uma operação de E/S (rede, ficheiros, base de dados) —, o processador é transferido para outro thread. Devido a estas possíveis interrupções, não podemos ter a certeza de que uma thread conseguirá escrever uma linha no ficheiro de registo sem ser interrompida. Existe, portanto, o risco de que os registos de duas threads diferentes possam ficar misturados. O risco é baixo, talvez até nulo, mas decidimos, mesmo assim, mostrar como sincronizar o acesso de duas threads a um recurso partilhado, neste caso o ficheiro de registo;
  • linha 30: antes de escrever, a thread solicita a chave da porta de entrada. A chave solicitada é a criada na linha 11. É, de facto, única: um atributo de classe é único para todas as instâncias da classe;
    • No momento T1, uma thread chamada Thread1 obtém a chave. Pode então executar a linha 33;
    • No momento T2, a thread Thread1 é pausada antes mesmo de terminar de escrever o log;
    • No momento T3, a thread Thread2, que adquiriu o processador, também deve escrever um registo. Chega assim à linha 30, onde solicita a chave da porta da frente. É informada de que outra thread já a possui. É então automaticamente pausada. Este será o caso para todas as threads que solicitarem esta chave;
    • No momento T4, o thread Thread1, que estava em pausa, recupera o processador. Em seguida, conclui a gravação do registo;
  • Linhas 32–36: A gravação no ficheiro de registo ocorre em duas etapas:
  • linha 33: o descritor de ficheiro obtido na linha 17 funciona com um buffer. A operação [write] na linha 33 escreve neste buffer, mas não diretamente no ficheiro. O buffer é então descarregado para o ficheiro sob certas condições:
        • o buffer está cheio;
        • o descritor de ficheiro é submetido a uma operação [close] ou [flush];
  • linha 36: forçamos a linha de registo a ser gravada no ficheiro. Fazemos isto porque queremos ver os registos das diferentes threads intercalados. Se não o fizermos, os registos de uma única thread serão todos gravados ao mesmo tempo — quando o descritor for fechado na linha 45. Seria então muito mais difícil perceber que certas threads foram interrompidas: teríamos de verificar os carimbos de data/hora nos registos;
  • linha 39: a thread Thread1 devolve o bloqueio que lhe foi atribuído. Agora, este pode ser atribuído a outra thread;
  • linha 22: o método [write] é, portanto, sincronizado: apenas uma thread de cada vez escreve no ficheiro de registo. A chave do mecanismo está na linha 30: aconteça o que acontecer, apenas uma thread recupera a chave para avançar para a linha seguinte. Mantém-na até a devolver (linha 39);
  • linhas 41–45: o método [close] liberta os recursos alocados ao descritor do ficheiro de registo;

Os registos gravados no ficheiro de registo terão o seguinte aspeto:

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

24.2.2. A classe [SendAdminMail]

A classe [SendAdminMail] permite enviar uma mensagem ao administrador da aplicação quando esta falha.

Image

A classe [SendAdminMail] é configurada no script [config] [2] da seguinte forma:

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

A classe [SendAdminMail] recebe o dicionário das linhas 2–13, bem como a configuração de envio de e-mail. A classe é a seguinte:

#  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()
  • linhas 24-54: este é o código já abordado no exemplo |smtp/02|;
  • linha 20: recuperamos a referência de um logger. Isto é utilizado nas linhas 45 e 49;

24.3. O servidor web

24.3.1. Configuração

A configuração do servidor é muito semelhante à do servidor discutido anteriormente. Apenas o ficheiro [config.py] sofreu uma ligeira alteração:

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
  • linhas 40–66: Adicionamos elementos relacionados com o logger (linha 49) e aqueles relacionados com o envio de um e-mail de alerta ao administrador da aplicação (linhas 51–63) ao dicionário de configuração do servidor;
  • linha 65: para ver melhor os threads em ação, vamos forçar alguns deles a fazer uma pausa. [sleep_time] é a duração da pausa expressa em segundos;
  • linhas 27–28: Note que estamos a utilizar o [index_controller] da versão 6 anterior;

24.3.2. O script principal [main]

O script principal [main] é o seguinte:

#  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)
  • linhas 1-10: o script espera um parâmetro [mysql / pgres] que especifique o SGBD a utilizar;
  • linhas 12–14: a aplicação é configurada (Python Path, camadas, base de dados);
  • linhas 16–28: dependências exigidas pela aplicação;
  • linhas 30-43: gestão da autenticação;
  • linhas 46–51: uma função que envia um e-mail ao administrador da aplicação;
  • a função espera dois parâmetros:
      • config: um dicionário com as chaves [adminMail] e [logger];
      • a mensagem a enviar;
    • linhas 49–50: preparamos a configuração do e-mail;
    • enviamos o e-mail;
  • linhas 54–74: verificamos se o ficheiro de registo existe;
  • linhas 70–74: se não foi possível abrir o ficheiro de registo, enviamos um e-mail ao administrador e saímos;
  • linhas 76–79: registamos o arranque do servidor;
  • linhas 81–98: recuperamos os dados da administração fiscal da base de dados;
  • linhas 88–98: se não foi possível recuperar estes dados, registamos o erro tanto na consola como no ficheiro de registo;
  • linhas 100–101: O segmento principal deixará de registar (os segmentos criados não utilizarão o mesmo descritor de ficheiro);
  • linhas 103–105: se não foi possível estabelecer ligação à base de dados, interrompemos;
  • linha 122: o servidor é iniciado no modo multithread;

A função [index] (linha 114) é a seguinte:

#  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()
  • Linha 4: a função executada quando um utilizador solicita a URL /. Como o servidor é multithread (linha 112), será criada uma thread para executar a função. Esta thread pode ser interrompida e pausada a qualquer momento para retomar a execução um pouco mais tarde. Tenha sempre isto em mente quando o código aceder a um recurso partilhado por todas as threads. Neste caso, esse recurso é o ficheiro de registo: todas as threads escrevem nele;
  • linha 8: criamos uma instância do logger. Assim, todas as threads terão uma instância diferente do logger. No entanto, todos estes loggers apontam para o mesmo ficheiro de registo. É importante notar que, quando uma thread fecha o seu logger, isso não tem efeito sobre os loggers das outras threads;
  • Linhas 9–12: Armazenamos o logger no dicionário [config] da aplicação sob uma chave com o nome da thread. Assim, se houver n threads a executar-se simultaneamente, serão criadas n entradas no dicionário [config]. [config] é um recurso partilhado entre todas as threads. Por conseguinte, poderá ser necessária sincronização. Fiz aqui uma suposição. Presumi que, se duas threads criassem simultaneamente as suas entradas no ficheiro [config] e uma delas fosse interrompida pela outra, isso não teria qualquer impacto. A thread interrompida poderia, posteriormente, concluir a criação da entrada. Se os testes demonstrassem que esta suposição é falsa, o acesso à linha 12 teria de ser sincronizado;
  • linha 10: colocamos o logger num dicionário;
  • linha 11: [threading.current_thread()] é a thread que está a executar esta linha e, portanto, a thread que está a executar a função [index]. Registamos o seu nome. Cada thread tem um nome único;
  • linha 12: armazenamos a configuração da thread. A partir de agora, procederemos sempre da seguinte forma: se houver informações que não possam ser partilhadas entre threads, estas serão colocadas na configuração geral, mas associadas ao nome da thread;
  • linha 14: registamos o pedido que estamos a executar atualmente;
  • linhas 15–24: colocamos aleatoriamente em pausa certas threads para que cedam o processador a outra thread;
    • linha 16: recuperamos a duração da pausa (em segundos) da configuração;
    • linha 17: ocorre uma pausa apenas se a duração da pausa não for 0;
    • linha 19: um número inteiro aleatório no intervalo [0, 1]. Portanto, apenas os valores 0 e 1 são possíveis;
    • linha 20: o thread é pausado apenas se o número aleatório for 1;
    • linha 22: registamos o facto de que o thread está prestes a ser pausado;
    • linha 24: o thread é pausado por [sleep_time] segundos;
  • linha 26: quando o thread acorda, o módulo [index_controller] executa a solicitação;
  • linhas 28–32: se esta execução causar um [500 INTERNAL SERVER ERROR], é enviado um e-mail ao administrador;
    • linhas 30-31: configuramos o dicionário [config_mail] que iremos passar para a classe [SendAdminMail];
    • linha 32: a mensagem enviada ao administrador é a string JSON do resultado que será enviado ao cliente;
  • linhas 33–34: registamos a resposta que será enviada ao cliente (linha 36);
  • linhas 37–44: tratamos quaisquer exceções;
  • linhas 39–40: se o logger existir, registamos o erro que ocorreu;
  • linhas 47–48: fechamos o logger, caso exista. Em última análise, o thread cria um logger no início da solicitação e o fecha assim que a solicitação for processada;

24.3.3. O controlador [index_controller]

O controlador [index_controller] que executa as solicitações é o da versão anterior:

Image

24.3.4. Execução

Iniciamos o servidor Flask, o servidor de e-mail |hMailServer| e o cliente de e-mail |Thunderbird|. Não iniciamos o SGBD. O servidor pára com os seguintes registos de consola:


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 

O ficheiro de registo [logs.txt] é o seguinte:


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

Utilizando o Thunderbird, verifique os e-mails do administrador [guest@localhost.com]:

Image

Em seguida, inicie o DBMS e solicite a URL [http://127.0.0.1:5000/?mari%C3%A9=oui&enfants=3&salaire=200000]. Os registos ficam da seguinte forma:


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}}}
  • linhas 1-4: note que o servidor inicia duas vezes porque o modo [Debug=True] desencadeia um segundo arranque;
  • linhas 5-6: os registos dão-nos uma ideia do tempo de execução de um pedido, neste caso 2,293 milissegundos;

24.4. O cliente web

Image

O diretório [http-clients/02] é criado através da cópia do diretório [http-clients/01]. Em seguida, fazemos algumas modificações.

24.4.1. A configuração

A configuração [config] da aplicação [http-clients/02] é idêntica à da aplicação [http-clients/01], com algumas pequenas diferenças:

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
  • linhas 31-32: vamos usar o mesmo logger |Logger| que o utilizado para o servidor;
  • linha 49: o caminho absoluto para o ficheiro de registo;
  • linha 60: o modo [debug=True] é utilizado para registar as respostas do servidor web no ficheiro de registo;

24.4.2. A camada [dao]

O código da classe [ImpôtsDaoWithHttpClient] sofre uma ligeira alteração:

#  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
        
  • Linha 17: Armazenamos a configuração geral. Veremos mais tarde que, quando o construtor da classe [ImpôtsDaoWithHttpClient] é executado, o dicionário [config] ainda não contém a chave [logger] utilizada na linha 37. É por isso que não podemos inicializar [self.__logger] (linha 23) no construtor;
  • linha 21: adicionámos uma chave [debug] à configuração que controla o registo nas linhas 33–39;
  • linha 34: se estivermos no modo [debug];
  • linhas 36–37: inicialização opcional da propriedade [self.__logger]. Quando o método [calculate_tax] é utilizado, a chave [logger] faz parte do dicionário [config];
  • linha 39: registamos o documento de texto associado à resposta HTTP do servidor;

A camada [dao] será executada simultaneamente por múltiplas threads. No entanto, aqui criamos uma única instância desta camada (ver config_layers). Devemos, portanto, verificar se o código não envolve acesso de escrita a dados partilhados, tipicamente as propriedades da classe [ImpôtsDaoWithHttpClient] que implementa a camada [dao]. No entanto, no código acima, a linha 37 modifica uma propriedade da instância da classe. Aqui, isto não tem consequências porque todas as threads partilham o mesmo logger. Se não fosse este o caso, o acesso à linha 37 teria de ser sincronizado.

24.4.3. O script principal

O script principal [main] desenvolve-se da seguinte forma:

#  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()
  • O script principal difere do do cliente anterior na medida em que irá gerar múltiplas threads de execução para enviar pedidos ao servidor. O cliente na versão 6 enviava todos os seus pedidos sequencialmente. O pedido #i só era feito depois de recebida a resposta ao pedido #[i-1]. Aqui, queremos ver como o servidor se comporta quando recebe múltiplos pedidos simultâneos. Para isso, precisamos de threads;
  • linha 21: as threads geradas serão colocadas numa lista. É importante compreender que o script [main] também é executado por uma thread chamada [MainThread]. Esta thread principal irá criar outras threads que serão responsáveis pelo cálculo do imposto para um ou mais contribuintes;
  • linha 26: criamos um logger. Este será partilhado por todas as threads;
  • linha 32: recuperamos todos os contribuintes cujos impostos precisam de ser calculados;
  • linhas 39–51: distribuímos estes contribuintes por várias threads;
  • linhas 40–41: cada thread processará 1 a 4 contribuintes. Este número é determinado aleatoriamente;
    • [random.randint(1, 4)] gera aleatoriamente um número da lista [1, 2, 3, 4];
    • o segmento não pode ter mais do que [l-i] contribuintes, em que [l-i] representa o número de contribuintes a quem ainda não foi atribuído um segmento;
    • por isso, tomamos o mínimo dos dois valores;
  • linha 43: uma vez conhecido [nb_taxpayers], o número de contribuintes processados pelo segmento, retiramos estes da lista de contribuintes:
    • [slice(10,12)] é o conjunto de índices [10, 11, 12];
    • [taxpayers[slice(10,12)]] é a lista [taxpayers[10], taxpayers[11], taxpayers[12] ;
  • linha 45: incrementamos o valor de i, que controla o ciclo na linha 39;
  • linha 47: criamos uma thread:
    • [target=thread_function] define a função que o thread irá executar. Esta é a função das linhas 16–17. Ela espera três parâmetros;
    • [ags] é a lista dos três parâmetros esperados pela função [thread_function];

Criar um thread não o executa. Simplesmente cria um objeto;

  • Linhas 48–49: A thread que acabou de ser criada é adicionada à lista de threads criadas pela thread principal;
  • linha 51: a thread é iniciada. Ela será então executada em paralelo com as outras threads ativas. Aqui, ela executará a [thread_function] com os argumentos que lhe foram fornecidos;
  • linhas 53–54: a thread principal aguarda cada uma das threads que lançou. Vejamos um exemplo:
    • a thread principal lançou três threads [th1, th2, th3];
    • A thread principal aguarda cada uma das threads (linhas 53–54) na ordem do ciclo for: [th1, th2, th3];
    • Suponha que as threads terminem na ordem [th2, th1, th3];
    • A thread principal aguarda que th1 termine. Quando th2 termina, nada acontece;
    • Quando th1 termina, o segmento principal aguarda th2. No entanto, th2 já terminou. O segmento principal passa então para o segmento seguinte e aguarda th3;
    • quando th3 termina, a thread principal termina a espera e prossegue para a execução da linha 57;
  • a linha 57 grava os resultados no ficheiro de resultados. Este é um bom exemplo de referências a objetos:
    • linha 43: a lista [thread_payers] associada a um thread contém cópias das referências de objeto contidas na lista [taxpayers];
    • sabemos que o cálculo do imposto irá modificar os objetos apontados pelas referências na lista [thread_payers]. Estes objetos serão atualizados com os resultados do cálculo do imposto. No entanto, as próprias referências não são modificadas. Portanto, as referências na lista inicial [taxpayers] «vêem» ou «apontam» para os objetos modificados;

A função [thread_function] executada pelos threads é a seguinte:

#  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")
  • As funções executadas simultaneamente por múltiplas threads são frequentemente difíceis de escrever: deve verificar sempre que o código não tenta modificar dados partilhados entre threads. Quando isto ocorre, deve implementar o acesso sincronizado aos dados partilhados que estão prestes a ser modificados;
  • Linha 3: A função recebe três parâmetros:
    • [dao]: uma referência à camada [dao]. Estes dados são partilhados;
    • [logger]: uma referência ao logger. Estes dados são partilhados;
    • [taxpayers]: uma lista de contribuintes. Estes dados não são partilhados: cada thread gere uma lista diferente;
  • Vamos analisar as duas referências [dao, logger]:
    • vimos que o objeto apontado pela referência [dao] tinha uma referência [self.__logger] que foi modificada pelas threads, mas para definir um valor comum a todas as threads;
    • a referência [logger] aponta para um descritor de ficheiro. Vimos que poderia haver um problema ao escrever registos no ficheiro. Por esta razão, a escrita no ficheiro foi sincronizada;
  • linhas 5–6: registamos o nome da thread e o número de contribuintes que esta deve gerir;
  • linhas 8–14: cálculo dos impostos dos contribuintes;
  • linha 16: registamos o fim da thread;

24.4.4. Execução

Vamos iniciar o servidor web conforme descrito na secção anterior (servidor web, SGBD, hMailServer, Thunderbird) e, em seguida, executar o script [main] do cliente. Nos ficheiros [data/output/errors.txt, data/output/results.json], obtemos os mesmos resultados que na versão anterior. No ficheiro [data/logs/logs.txt], temos os seguintes registos:


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]
  • Estes registos mostram que foram lançadas cinco threads para calcular os impostos de 11 contribuintes. Estas cinco threads enviaram pedidos simultâneos ao servidor de cálculo de impostos. É importante compreender como isto funciona:
    • O thread [Thread-1] é iniciado primeiro. Quando tem a CPU, executa o código até enviar o seu pedido HTTP. Como tem de aguardar o resultado deste pedido, é automaticamente colocado em espera. Em seguida, perde a CPU e outro thread assume o controlo;
    • linhas 1–10: o mesmo processo repete-se para cada uma das 5 threads. Assim, as 5 threads são lançadas antes mesmo de a thread [Thread-1] ter recebido a sua resposta na linha 11;
  • As threads não terminam na ordem em que foram lançadas. Assim, a thread [Thread-3] termina primeiro, linha 23;

No lado do servidor, os registos no ficheiro [data/logs/logs.txt] são os seguintes:


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}}}
  • Podemos ver que 11 threads processaram os 11 contribuintes;
  • algumas threads foram colocadas em espera (linhas 6, 8, 12, 14, 20, 22) e outras não (linhas 9, 23, 25, 29, 31);

24.5. Testes da camada [DAO]

Tal como fizemos na |versão anterior|, estamos a testar a camada [DAO] do cliente. O princípio é exatamente o mesmo:

Image

A classe de teste será executada no seguinte ambiente:

Image

  • A configuração [2] é idêntica à configuração [1], que acabámos de examinar;

A classe de teste [TestHttpClientDao] é a seguinte:

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()
  • Criamos uma |configuração de execução| para este teste;
  • Iniciamos o servidor web com todo o seu ambiente;
  • executamos o teste;

Os resultados são os seguintes:


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