24. Exercício prático: versão 7
24.1. Introdução
A versão 7 do aplicativo de cálculo de impostos é idêntica à versão 6, com as seguintes diferenças:
- o cliente web lançará simultaneamente várias solicitações HTTP. Na versão anterior, essas solicitações eram lançadas sequencialmente. O servidor processava, portanto, apenas uma única solicitação por vez;
- o servidor será multithread: poderá processar várias solicitações simultaneamente;
- para acompanhar a execução dessas solicitações, equiparemos o servidor web com um registrador de logs, com o qual registraremos em um arquivo de texto os momentos importantes do processamento das solicitações;
- o servidor enviará um e-mail ao administrador da aplicação quando encontrar um problema que o impeça de iniciar, normalmente um problema com o banco de dados associado ao servidor web;
A arquitetura da aplicação não muda:
A estrutura dos scripts é a seguinte:

A pasta [http-servers/02] é obtida inicialmente por meio da cópia da pasta [http-servers/01]. Em seguida, são feitas alterações nessa pasta.
24.2. Os utilitários

24.2.1. A classe [Logger]
A classe [Logger] permitirá registrar em um arquivo de texto determinadas ações do servidor web:
import codecs
import threading
from datetime import date, datetime
from threading import current_thread
from ImpôtsError import ImpôtsError
class Logger:
# atributo de classe
verrou = threading.RLock()
# construtor
def __init__(self, logs_filename: str):
try:
# abrindo o arquivo no modo de acréscimo (a)
self.__resource = codecs.open(logs_filename, "a", "utf-8")
except BaseException as erreur:
raise ImpôtsError(18, f"{erreur}")
# gravação de um log
def write(self, message: str):
# data/hora atual
today = date.today()
now = datetime.time(datetime.now())
# nome do thread
thread_name = current_thread().name
# não queremos ser interrompidos enquanto estivermos gravando no arquivo de logs
# solicita-se o objeto de sincronização (= o bloqueio) da classe — apenas uma thread o obterá
Logger.verrou.acquire()
try:
# gravação no log
self.__resource.write(f"{today} {now}, {thread_name} : {message}")
# gravação imediata — caso contrário, o texto só será gravado ao fechar o fluxo de gravação
# ou queremos acompanhar os logs ao longo do tempo
self.__resource.flush()
finally:
# liberamos o objeto de sincronização (= o bloqueio) para que outra thread possa obtê-lo
Logger.verrou.release()
# liberação dos recursos
def close(self):
# fechamento do arquivo
if self.__resource:
self.__resource.close()
- linhas 10-11: define-se um atributo de classe. Um atributo de classe é uma propriedade compartilhada por todas as instâncias da classe. Ele é referenciado pela notação [Classe.attribut_de_classe] (linhas 30, 39). O atributo de classe [verrou] servirá como objeto de sincronização para todas as threads que executam o código das linhas 31-36;
- linhas 14-19: o construtor recebe o nome absoluto do arquivo de logs. Esse arquivo é então aberto e o descritor de arquivo recuperado é armazenado na classe;
- linha 17: o arquivo de logs é aberto no modo ‘append’ (a). Cada linha gravada será adicionada ao final do arquivo;
- linhas 22-39: o método [write] permite gravar no arquivo de logs uma mensagem passada como parâmetro. A essa mensagem são anexadas duas informações:
- linha 24: a data do dia;
- linha 25: a hora atual;
- linha 27: o nome do thread que está gravando o log. Não se deve esquecer aqui que uma aplicação web atende a vários usuários ao mesmo tempo. A cada solicitação é atribuído um thread para executá-la. Se esse thread for pausado, normalmente para uma operação de entrada/saída (rede, arquivos, banco de dados), o processador será alocado a outro thread. Devido a essas possíveis interrupções, não é possível ter certeza de que um thread conseguirá gravar uma linha no arquivo de logs sem ser interrompido. Há, portanto, o risco de que os logs de dois threads diferentes se misturem. O risco é baixo, talvez até nulo, mas decidimos, mesmo assim, mostrar como sincronizar o acesso de dois threads a um recurso comum, neste caso, o arquivo de logs;
- linha 30: antes de gravar, o thread solicita a chave da porta de entrada. A chave solicitada é aquela criada na linha 11. Ela é, de fato, única: um atributo de classe é único para todas as instâncias da classe;
- no momento T1, um thread Thread1 obtém a chave. Ele pode então executar a linha 33;
- no momento T2, o thread Thread1 é pausado antes mesmo de ter concluído a gravação do log;
- No momento T3, o thread Thread2, que obteve o processador, também precisa gravar um log. Ele chega então à linha 30, onde solicita a chave da porta de entrada. Recebe a resposta de que outro thread já a possui. É, então, automaticamente colocado em pausa. O mesmo ocorrerá com todos os threads que solicitarem essa chave;
- no momento T4, o thread Thread1, que havia sido colocado em pausa, recupera o processador. Ele então conclui a gravação do log;
- linhas 32-36: a gravação no arquivo de logs ocorre em duas etapas:
- linha 33: o descritor de arquivo obtido na linha 17 trabalha com um buffer. A operação [write] da linha 33 grava nesse buffer, mas não diretamente no arquivo. O buffer é então esvaziado no arquivo sob certas condições:
- o buffer está cheio;
- o descritor de arquivo é submetido a uma operação [close] ou [flush];
- linha 36: força-se a gravação da linha de log no arquivo. Fazemos isso porque queremos ver os logs das diferentes threads intercalados entre si. Se não fizermos isso, os logs de uma thread serão todos gravados ao mesmo tempo ao fechar o descritor, na linha 45. Seria então muito mais difícil perceber que algumas threads foram interrompidas: seria necessário verificar os horários nos logs;
- linha 39: o thread Thread1 devolve a chave que lhe foi atribuída. Ela poderá ser atribuída a outro thread;
- linha 22: o método [write] está, portanto, sincronizado: apenas um thread por vez grava no arquivo de logs. A chave do mecanismo está na linha 30: aconteça o que acontecer, apenas um thread obtém a chave para avançar para a próxima linha. Ele a mantém até devolvê-la (linha 39);
- linhas 41-45: o método [close] permite liberar os recursos alocados ao descritor do arquivo de logs;
Os logs gravados no arquivo de logs terão a seguinte aparência:
24.2.2. A classe [SendAdminMail]
A classe [SendAminMail] permite enviar uma mensagem ao administrador do aplicativo quando este “trava”.

A classe [SendAdminMail] é configurada no script [config] [2] da seguinte maneira:
# configuração do servidor SMTP
"adminMail": {
# servidor SMTP
"smtp-server": "localhost",
# porta do servidor SMTP
"smtp-port": "25",
# administrador
"from": "guest@localhost.com",
"to": "guest@localhost.com",
# assunto do e-mail
"subject": "plantage du serveur de calcul d'impôts",
# tls definido como True se o servidor SMTP exigir autenticação; caso contrário, definido como False
"tls": False
}
A classe [SendAdminMail] recebe o dicionário das linhas 2 a 13, bem como a configuração para o envio do e-mail. A classe é a seguinte:
# importações
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):
# envia mensagem para o servidor SMTP config['smtp-server'] na porta config[smtp-port]
# se config['tls'] for verdadeiro, o suporte TLS será utilizado
# o e-mail é enviado em nome de config['from']
# para o destinatário config['to']
# a mensagem tem como assunto config['subject']
# encontra-se a referência de um logger na config['logger']
# recupera-se o logger na configuração — pode ser igual a None
logger = config["logger"]
# servidor SMTP
server = None
# envia-se a mensagem
try:
# o servidor SMTP
server = smtplib.SMTP(config["smtp-server"])
# modo detalhado
server.set_debuglevel(verbose)
# conexão segura?
if config['tls']:
# início do diálogo de segurança
server.starttls()
# autenticação
server.login(config["user"], config["password"])
# criação de uma mensagem Multipart — é essa mensagem que será enviada
msg = MIMEText(message)
msg['From'] = config["from"]
msg['To'] = config["to"]
msg['Date'] = formatdate(localtime=True)
msg['Subject'] = config["subject"]
# envio da mensagem
server.send_message(msg)
# log — o logger pode não existir
if logger:
logger.write(f"[SendAdminMail] Message envoyé à [{config['to']}] : [{message}]\n")
except BaseException as erreur:
# log — o logger pode não existir
if logger:
logger.write(
f"[SendAdminMail] Erreur [{erreur}] lors de l'envoi à [{config['to']}] du message [{message}] : \n")
finally:
# concluído — liberamos os recursos utilizados pela função
if server:
server.quit()
- linhas 24-54: encontramos o código já estudado no exemplo |smtp/02|;
- linha 20: recupera-se a referência de um logger. Ela é utilizada nas linhas 45 e 49;
24.3. O servidor web

24.3.1. Configuração

A configuração do servidor é muito semelhante à do servidor analisado anteriormente. Apenas o arquivo [config.py] sofreu uma pequena alteração:
def configure(config: dict) -> dict:
import os
# etapa 1 ------
# pasta deste arquivo
script_dir = os.path.dirname(os.path.abspath(__file__))
# caminho raiz
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# dependências absolutas
absolute_dependencies = [
# pastas do projeto
# 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",
# Constantes, faixas
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",
]
# definimos o syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# etapa 2 ------
# configuração do aplicativo
config.update({
# usuários autorizados a usar o aplicativo
"users": [
{
"login": "admin",
"password": "admin"
}
],
# arquivo de logs
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# configuração do servidor SMTP
"adminMail": {
# servidor SMTP
"smtp-server": "localhost",
# porta do servidor SMTP
"smtp-port": "25",
# administrador
"from": "guest@localhost.com",
"to": "guest@localhost.com",
# assunto do e-mail
"subject": "plantage du serveur de calcul d'impôts",
# TLS definido como True se o servidor SMTP exigir autenticação; caso contrário, definido como False
"tls": False
},
# duração da pausa do thread em segundos
"sleep_time": 0
})
# etapa 3 ------
# configuração do banco de dados
import config_database
config["database"] = config_database.configure(config)
# etapa 4 ------
# instanciação das camadas da aplicação
import config_layers
config['layers'] = config_layers.configure(config)
# gerando a configuração
return config
- linhas 40-66: adicionam-se ao dicionário de configuração do servidor os elementos relativos ao registrador de logs (linha 49) e aqueles relativos ao envio de um e-mail de alerta ao administrador do aplicativo (linhas 51-63);
- linha 65: para visualizar melhor os threads em ação, vamos forçar alguns deles a serem interrompidos. [sleep_time] é a duração da interrupção expressa em segundos;
- linhas 27-28: observe que estamos usando o controlador [index_controller] da versão 6 anterior;
24.3.2. O script principal [main]
O script principal [main] é o seguinte:
# aguardando um parâmetro mysql ou pgres
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
sgbd = sys.argv[1].lower()
erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# configurando a aplicação
import config
config = config.configure({'sgbd': sgbd})
# dependências
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
# gerenciador de autenticação
auth = HTTPBasicAuth()
@auth.verify_password
def verify_password(login, password):
# lista de usuários
users = config['users']
# percorremos essa lista
for user in users:
if user['login'] == login and user['password'] == password:
return True
# não foi encontrado
return False
# envio de um e-mail ao administrador
def send_adminmail(config: dict, message: str):
# envio de um e-mail ao administrador do aplicativo
config_mail = config["adminMail"]
config_mail["logger"] = config['logger']
SendAdminMail.send(config_mail, message)
# verificação do arquivo de logs
logger = None
erreur = False
message_erreur = None
try:
# registrador
logger = Logger(config["logsFilename"])
except BaseException as exception:
# log da console
print(f"L'erreur suivante s'est produite : {exception}")
# registra-se o erro
erreur = True
message_erreur = f"{exception}"
# armazenar o logger na configuração
config['logger'] = logger
# gestão do erro
if erreur:
# e-mail para o administrador
send_adminmail(config, message_erreur)
# fim da aplicação
sys.exit(1)
# log de inicialização
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)
# recuperação de dados da administração fiscal
erreur = False
try:
# admindata será um dado de escopo da aplicação somente para leitura
config["admindata"] = config["layers"]["dao"].get_admindata()
# log de sucesso
logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
# registro do erro
erreur = True
# registro de erro
log = f"L'erreur suivante s'est produite : {ex}"
# console
print(log)
# arquivo de logs
logger.write(f"{log}\n")
# e-mail para o administrador
send_adminmail(config, log)
# o thread principal não precisa mais do logger
logger.close()
# se houver erro, o processo é interrompido
if erreur:
sys.exit(2)
# o aplicativo Flask pode ser iniciado
app = Flask(__name__)
# Página inicial URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
…
# apenas main
if __name__ == '__main__':
# iniciamos o servidor
app.config.update(ENV="development", DEBUG=True)
app.run(threaded=True)
- linhas 1-10: o script aguarda um parâmetro [mysql / pgres] que indica qual SGBD deve ser utilizado;
- linhas 12-14: o aplicativo é configurado (Python Path, camadas, banco de dados);
- linhas 16-28: as dependências necessárias para o aplicativo;
- linhas 30-43: gerenciamento 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 ser enviada;
- linhas 49-50: prepara-se a configuração do envio;
- enviamos o e-mail;
- linhas 54-74: verifica-se a presença do arquivo de logs;
- linhas 70-74: se não for possível abrir o arquivo de logs, envia-se um e-mail ao administrador e o programa é encerrado;
- linhas 76-79: registra-se o início do servidor;
- linhas 81-98: buscamos os dados da administração fiscal no banco de dados;
- linhas 88-98: se não for possível obter esses dados, registramos o erro tanto no console quanto no arquivo de logs;
- linhas 100-101: o thread principal não registrará mais logs (os threads criados não usarão o mesmo descritor de arquivo);
- linhas 103-105: se não for possível conectar-se ao banco de dados, o programa é encerrado;
- linha 122: iniciamos o servidor no modo multithread;
A função [index] (linha 114) é a seguinte:
# Página inicial URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
logger = None
try:
# registrador
logger = Logger(config["logsFilename"])
# armazenado em uma configuração associada ao thread
thread_config = {"logger": logger}
thread_name = threading.current_thread().name
config[thread_name] = {"config": thread_config}
# registra-se a solicitação
logger.write(f"[index] requête : {request}\n")
# interrompe-se o thread, caso tenha sido solicitado
sleep_time = config["sleep_time"]
if sleep_time != 0:
# a pausa é aleatória para que alguns threads sejam interrompidos e outros não
aléa = randint(0, 1)
if aléa == 1:
# registro antes da pausa
logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
# pausa
time.sleep(sleep_time)
# a consulta é executada por um controlador
résultat, status_code = index_controller.execute(request, config)
# houve algum erro fatal?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# envia-se um e-mail ao administrador do aplicativo
config_mail = config["adminMail"]
config_mail["logger"] = logger
SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
# a resposta é registrada no log
logger.write(f"[index] {résultat}\n")
# a resposta é enviada
return json_response(résultat, status_code)
except BaseException as erreur:
# registra-se o erro, se possível
if logger:
logger.write(f"[index] {erreur}")
# prepara-se a resposta para o cliente
résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
# envia-se a resposta
return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
finally:
# fecha-se o arquivo de logs, caso tenha sido aberto
if logger:
logger.close()
- linha 4: a função executada quando um usuário solicita o URL /. Como o servidor é multithread (linha 112), um thread será criado para executar a função. Esse thread pode, a qualquer momento, ser interrompido e colocado em pausa para retomar sua execução um pouco mais tarde. É preciso sempre lembrar desse ponto quando o código acessa um recurso compartilhado por todas as threads. Esse recurso, neste caso, é o arquivo de logs: todas as threads escrevem nele;
- linha 8: cria-se uma instância do registrador de logs. Portanto, todas as threads terão uma instância diferente do registrador. No entanto, todos esses registradores apontam para o mesmo arquivo de logs. É importante observar que, quando uma thread fecha seu registrador, isso não afeta os registradores das outras threads;
- linhas 9-12: o logger é armazenado no dicionário [config] do aplicativo, associado a uma chave com o nome da thread. Assim, se houver n threads sendo executadas simultaneamente, serão criadas n entradas no dicionário [config]. O [config] é um recurso compartilhado entre todas as threads. Portanto, pode haver a necessidade de sincronização. Aqui, fiz uma suposição. Supus que, se duas threads criassem simultaneamente suas entradas no arquivo [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 a experiência mostrasse que essa hipótese estava errada, seria necessário sincronizar o acesso à linha 12;
- linha 10: colocamos o logger em um dicionário;
- linha 11: [threading.current_thread()] é o thread que executa esta linha, portanto, o thread que executa a função [index]. Anotamos seu nome. Cada thread tem um nome único;
- linha 12: memorizamos a configuração do thread. A partir de agora, sempre procederemos assim: se houver informações que não possam ser compartilhadas entre os threads, elas serão colocadas mesmo assim na configuração geral, mas associadas ao nome do thread;
- linha 14: registramos a solicitação que estamos executando;
- linhas 15-24: aleatoriamente, colocamos alguns threads em pausa para que cedam o processador a outro thread;
- linha 16: recuperamos a duração da pausa (em segundos) da configuração;
- linha 17: só há pausa se a duração da pausa for diferente de 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: a pausa do thread só ocorre se o número aleatório for 1;
- linha 22: registra-se no log que o thread será interrompido;
- linha 24: interrompe-se o thread por [sleep_time] segundos;
- linha 26: quando o thread retorna, ele faz com que a consulta seja executada pelo módulo [index_controller];
- linhas 28-32: se essa execução causar um erro do tipo [500 INTERNAL SERVER ERROR], envia-se um e-mail ao administrador;
- linhas 30-31: configura-se o dicionário [config_mail], que será passado para a classe [SendAdminMail];
- linha 32: a mensagem enviada ao administrador é a string jSON do resultado que será enviado ao cliente;
- linhas 33-34: registramos em log a resposta que será enviada ao cliente (linha 36);
- linhas 37-44: tratamento de uma eventual exceção;
- linhas 39-40: se o logger existir, registra-se o erro que ocorreu;
- linhas 47-48: fechamos o registrador de log, caso exista. Por fim, o thread cria um registrador de log no início da solicitação e o fecha quando esta for processada;
24.3.3. O controlador [index_controller]
O controlador [index_controller], que executa as solicitações, é o da versão anterior:

24.3.4. Execução
Iniciamos o servidor Flask, o servidor de e-mails |hMailServer| e o cliente de e-mail |Thunderbird|. Não iniciamos o SGBD. O servidor é encerrado com os seguintes logs de 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
O arquivo de logs [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)]]
Com o Thunderbird, verificamos os e-mails do administrador [guest@localhost.com]:

Em seguida, executamos o SGBD e solicitamos o URL e o [http://127.0.0.1:5000/?mari%C3%A9=oui&enfants=3&salaire=200000]. Os logs passam a ser os seguintes:
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/?casado=sim&filhos=3&salário=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: vale lembrar que há duas inicializações do servidor, pois o modo [Debug=True] provoca uma segunda inicialização;
- linhas 5-6: os logs nos dão uma ideia do tempo de execução de uma consulta, neste caso, 2,293 milissegundos;
24.4. O cliente web


A pasta [http-clients/02] é obtida por meio da cópia da pasta [http-clients/01]. Em seguida, são feitas algumas modificações.
24.4.1. A configuração
A configuração [config] do aplicativo [http-clients/02] é idêntica à do aplicativo [http-clients/01], com algumas pequenas diferenças:
def configure(config: dict) -> dict:
import os
# etapa 1 ------
# pasta deste arquivo
script_dir = os.path.dirname(os.path.abspath(__file__))
# caminho raiz
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# dependências absolutas
absolute_dependencies = [
# pastas do projeto
# 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",
# Constantes, faixas
f"{root_dir}/impots/v05/entities",
# ImpôtsDaoWithHttpClient
f"{script_dir}/../services",
# scripts de configuração
script_dir,
# Logger
f"{root_dir}/impots/http-servers/02/utilities",
]
# definimos o syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# etapa 2 ------
# configuração do aplicativo com constantes
config.update({
# arquivo de contribuintes
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
# arquivo de resultados
"resultsFilename": f"{script_dir}/../data/output/résultats.json",
# arquivo de erros
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
# arquivo de logs
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# servidor de cálculo de impostos
"server": {
"urlServer": "http://127.0.0.1:5000/",
"authBasic": True,
"user": {
"login": "admin",
"password": "admin"
}
},
# modo de depuração
"debug": True
}
)
# etapa 3 ------
# instanciação das camadas
import config_layers
config['layers'] = config_layers.configure(config)
# fazemos a configuração
return config
- linhas 31-32: vamos usar o mesmo registrador de logs |Logger| utilizado para o servidor;
- linha 49: o caminho absoluto do arquivo de logs;
- linha 60: o modo [debug=True] serve para gravar as respostas do servidor web no arquivo de logs;
24.4.2. A camada [dao]
O código da classe [ImpôtsDaoWithHttpClient] sofreu pequenas alterações:
# importações
import requests
from flask_api import status
…
class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
# construtor
def __init__(self, config: dict):
# inicialização do pai
AbstractImpôtsDao.__init__(self, config)
# armazenamento dos elementos da configuração
# configuração geral
self.__config = config
# servidor
self.__config_server = config["server"]
# modo de depuração
self.__debug = config["debug"]
# registrador
self.__logger = None
# método não utilizado
def get_admindata(self) -> AdminData:
pass
# cálculo do imposto
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
# permite que as exceções sejam propagadas
…
# modo de depuração?
if self.__debug:
# registrador
if not self.__logger:
self.__logger = self.__config['logger']
# registra no log
self.__logger.write(f"{response.text}\n")
# código de status da resposta HTTP
status_code = response.status_code
…
- linha 17: armazena-se a configuração geral. Veremos posteriormente 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 esse motivo que não é possível inicializar [self.__logger] (linha 23) no construtor;
- linha 21: foi adicionada à configuração uma chave [debug] que controla o log das linhas 33 a 39;
- linha 34: se estivermos no modo [debug];
- linhas 36-37: eventual inicialização da propriedade [self.__logger]. Quando o método [calculate_tax] é utilizado, a chave [logger] faz parte do dicionário [config];
- linha 39: registra-se o documento de texto associado à resposta HTTP do servidor;
A camada [dao] será executada simultaneamente por várias threads. No entanto, aqui é criada uma única instância dessa camada (cf. config_layers). Portanto, é necessário verificar se o código não implica acesso de gravação a dados compartilhados, tipicamente as propriedades da classe [ImpôtsDaoWithHttpClient] que implementa a camada [dao]. No entanto, na linha 37 acima, uma propriedade da instância da classe é modificada. Nesse caso, isso não traz consequências, pois todas as threads compartilham o mesmo registrador de log. Se não fosse assim, o acesso à linha 37 deveria ter sido sincronizado.
24.4.3. O script principal
O script principal [main] evolui da seguinte forma:
# configurando o aplicativo
import config
config = config.configure({})
# dependências
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger
# execução da camada [dao] em um thread
# contribuintes é uma lista de contribuintes
def thread_function(dao, logger, taxpayers: list):
…
# lista de threads do cliente
threads = []
logger = None
# código
try:
# registrador
logger = Logger(config["logsFilename"])
# é armazenado na configuração
config["logger"] = logger
# recuperamos a camada [dao]
dao = config["layers"]["dao"]
# leitura dos dados dos contribuintes
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# dos contribuintes?
if not taxpayers:
raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
# cálculo do imposto dos contribuintes com várias threads
i = 0
l_taxpayers = len(taxpayers)
while i < len(taxpayers):
# cada thread processará de 1 a 4 contribuintes
nb_taxpayers = min(l_taxpayers - i, random.randint(1, 4))
# a lista de contribuintes processados pela thread
thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
# incrementamos i para a próxima thread
i += nb_taxpayers
# cria-se a thread
thread = threading.Thread(target=thread_function, args=(dao, logger, thread_taxpayers))
# ele é adicionado à lista de threads do script principal
threads.append(thread)
# inicia-se o thread — essa operação é assíncrona — não se aguarda o resultado do thread
thread.start()
# o thread principal aguarda a conclusão de todos os threads que iniciou
for thread in threads:
thread.join()
# aqui, todas as threads concluíram seu trabalho — cada uma modificou um ou mais objetos [taxpayer]
# os resultados são gravados no arquivo jSON
dao.write_taxpayers_results(taxpayers)
except BaseException as erreur:
# exibição do erro
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# fechamos o logger
if logger:
logger.close()
# concluído
print("Travail terminé...")
# fim dos threads que ainda possam existir caso a execução tenha sido interrompida devido a um erro
sys.exit()
- o script principal se diferencia do do cliente anterior pelo fato de que ele irá gerar várias threads de execução para realizar as solicitações ao servidor. O cliente da versão 6 fazia todas as suas solicitações sequencialmente. A solicitação nº i só era feita após o recebimento da resposta à solicitação nº [i-1]. Aqui, queremos observar como o servidor se comportará ao receber várias solicitações simultâneas. Para isso, precisamos das threads;
- linha 21: as threads geradas serão colocadas em uma lista. É importante entender que o script [main] também é executado por uma thread chamada [MainThread]. Essa thread principal criará outras threads que serão responsáveis por calcular o imposto de um ou mais contribuintes;
- linha 26: cria-se um logger. Ele será compartilhado por todas as threads;
- linha 32: recuperam-se todos os contribuintes cujo imposto deve ser calculado;
- linhas 39-51: esses contribuintes serão distribuídos por várias threads;
- linhas 40-41: cada thread processará de 1 a 4 contribuintes. Esse número é definido aleatoriamente;
- [random.randint(1, 4)] gera aleatoriamente um número da lista [1, 2, 3, 4];
- o thread não pode ter mais do que [l-i] contribuintes, onde [l-i] representa o número de contribuintes aos quais ainda não foi atribuído um thread;
- portanto, considera-se o menor dos dois valores;
- linha 43: uma vez que [nb_taxpayers], o número de contribuintes processados pelo thread, é conhecido, esses contribuintes são retirados da lista de contribuintes:
- [slice(10,12)] é o conjunto dos índices [10, 11, 12];
- [response.text[39:]] é a lista [taxpayers[10], taxpayers[11], taxpayers[12];
- linha 45: incrementa-se o valor de i, que controla o loop da linha 39;
- linha 47: cria-se um thread:
- [target=thread_function] define a função que o thread executará. Trata-se da 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. Isso apenas cria um objeto;
- linhas 48-49: o thread que acaba de ser criado é adicionado à lista de threads criados pelo thread principal;
- linha 51: o thread é iniciado. Ele será então executado em paralelo aos outros threads ativos. Aqui, ele executará a função [thread_function] com os argumentos que lhe foram fornecidos;
- linhas 53-54: o thread principal aguarda cada um dos threads que iniciou. Vejamos um exemplo:
- o thread principal iniciou três threads [th1, th2, th3];
- o thread principal entra em espera por cada um dos threads (linhas 53-54) na ordem do loop for: [th1, th2, th3];
- suponhamos que as threads sejam concluídas na ordem [th2, th1, th3];
- o thread principal aguarda o término de th1. Quando th2 termina, nada acontece;
- quando th1 termina, o thread principal entra em espera por th2. No entanto, este já está concluído. O thread principal passa então para o próximo thread e aguarda th3;
- quando th3 termina, o thread principal encerra sua espera e passa então para a execução da linha 57;
- a linha 57 grava os resultados obtidos no arquivo de resultados. Temos aqui 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 objetos contidos na lista [taxpayers];
- sabe-se que o cálculo do imposto modificará os objetos apontados pelas referências da lista [thread_payers]. Esses 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 da lista inicial [taxpayers] “enxergam” ou “apontam para” os objetos modificados;
A função [thread_function] executada pelas threads é a seguinte:
# execução da camada [dao] em um thread
# taxpayers é uma lista de contribuintes
def thread_function(dao, logger, taxpayers: list):
# registro do início do thread
thread_name = threading.current_thread().name
logger.write(f"début du thread [{thread_name}] avec {len(taxpayers)} contribuable(s)\n")
# está-se calculando o imposto dos contribuintes
for taxpayer in taxpayers:
# registro
logger.write(f"début du calcul de l'impôt de {taxpayer}\n")
# cálculo síncrono do imposto
dao.calculate_tax(taxpayer)
# log
logger.write(f"fin du calcul de l'impôt de {taxpayer}\n")
# registro: fim do thread
logger.write(f"fin du thread [{thread_name}]\n")
- as funções executadas simultaneamente por várias threads costumam ser complexas de escrever: é preciso sempre verificar se o código não tenta modificar um dado compartilhado entre as threads. Quando isso ocorre, é necessário implementar um acesso sincronizado ao dado compartilhado que será modificado;
- linha 3: a função recebe três parâmetros:
- [dao]: uma referência à camada [dao]. Esse dado é compartilhado;
- [logger]: uma referência ao logger. Esse dado é compartilhado;
- [taxpayers]: uma lista de contribuintes. Esse dado não é compartilhado: cada thread gerencia uma lista diferente;
- vamos examinar as duas referências [dao, logger]:
- vimos que o objeto apontado pela referência [dao] possuía uma referência [self.__logger] que era modificada pelas threads, mas para atribuir-lhe um valor comum a todas as threads;
- a referência [logger] aponta para um descritor de arquivo. Vimos que poderia haver um problema ao gravar os logs no arquivo. Por esse motivo, a gravação no arquivo foi sincronizada;
- linhas 5-6: registra-se o nome do thread e o número de contribuintes que ele deve gerenciar;
- linhas 8-14: cálculo do imposto dos contribuintes;
- linha 16: registra-se o fim do thread;
24.4.4. Execução
Vamos iniciar o servidor web como no parágrafo anterior (servidor web, SGBD, hMailServer, Thunderbird) e, em seguida, executar o script [main] do cliente. Nos arquivos [data/output/errors.txt, data/output/résultats.json], obtemos os mesmos resultados que na versão anterior. No arquivo [data/logs/logs.txt], temos os seguintes registros:
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]
- esses logs mostram que cinco threads foram iniciados para calcular o imposto de 11 contribuintes. Esses cinco threads enviaram solicitações simultâneas ao servidor de cálculo de impostos. É preciso entender como isso funciona:
- o thread [Thread-1] é iniciado primeiro. Quando ele tem o processador, avança no código até enviar sua solicitação HTTP. Como precisa aguardar o resultado dessa solicitação, ele é automaticamente colocado em espera. Ele então perde o processador, e outro thread o obtém;
- linhas 1-10: o mesmo processo se repete para cada uma das 5 threads. Assim, as 5 threads são iniciadas antes mesmo que a thread [Thread-1] tenha recebido sua resposta na linha 11;
- os threads não terminam na ordem em que foram iniciados. Assim, é o thread [Thread-3] que termina primeiro, na linha 23;
No lado do servidor, os registros no arquivo [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/?casado=sim&filhos=2&salário=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/?casado=sim&filhos=3&salário=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/?casado=sim&filhos=2&salário=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/?casado=não&filhos=3&salário=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/?casado=sim&filhos=2&salário=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/?casado=sim&filhos=3&salário=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/?casado=sim&filhos=5&salário=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/?casado=não&filhos=0&salário=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/?casado=sim&filhos=3&salário=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/?casado=não&filhos=2&salário=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/?casado=não&filhos=0&salário=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}}}
- percebe-se que 11 threads processaram os 11 contribuintes;
- alguns threads foram colocados em espera (linhas 6, 8, 12, 14, 20, 22) e outros não (linhas 9, 23, 25, 29, 31);
24.5. Testes da camada [dao]
Assim como fizemos na |versão anterior|, estamos testando a camada [dao] do cliente. O princípio é exatamente o mesmo:

A classe de teste será executada no seguinte ambiente:

- a configuração [2] é idêntica à configuração [1] que acabamos de analisar;
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
# {'casado': 'sim', 'filhos': 2, 'salário': 55555,
# 'imposto': 2814, 'sobretaxa': 0, 'abatimento': 0, 'redução': 0, 'alíquota': 0,14}
taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
dao.calculate_tax(taxpayer)
# verificação
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__':
# configuramos o aplicativo
import config
config = config.configure({})
# registrador
logger = Logger(config["logsFilename"])
# é armazenado na configuração
config["logger"] = logger
# recuperamos a camada [dao]
dao = config["layers"]["dao"]
# executa-se os métodos de teste
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