28. Exercício prático: versão 10
28.1. Introdução
Nos exemplos de clientes do servidor de cálculo de impostos, os threads enviavam N solicitações sequencialmente caso precisassem processar N contribuintes. A ideia aqui é enviar uma única solicitação que englobe os N contribuintes. Para cada um deles, é necessário enviar as informações [marié, enfants, salaire]. Elas podem ser enviadas como parâmetros:
- como parâmetros da solicitação URL. Assim, teremos uma solicitação longa URL com pouca informação relevante;
- no corpo (body) da solicitação HTTP. Sabe-se que esse corpo fica oculto para o usuário que utiliza um navegador;
Em ambos os casos, é possível usar uma solicitação [GET] ou [POST]. Usaremos uma solicitação POST com os parâmetros encapsulados no corpo da solicitação HTTP.
A arquitetura cliente/servidor não sofreu alterações:

28.2. O servidor web

A pasta [http-servers/05] é obtida inicialmente por cópia da pasta [http-servers/02]. Voltamos às trocas de dados jSON entre o cliente e o servidor. Vimos que passar de jSON para XML é muito simples.
28.2.1. Configuração
A configuração do [config, config_database, config_layers] permanece semelhante à das versões anteriores. Não vamos nos deter nisso novamente.
28.2.2. O script principal [main]
O script [main] é idêntico ao da pasta [http-servers/02] que copiamos. Há apenas uma diferença:
# Página inicial URL
@app.route('/', methods=['POST'])
@auth.login_required
def index():
…
- linha 2: agora o URL é obtido por meio de um POST;
28.2.3. O controlador [index_controller]
O controlador [index_controller] evolui da seguinte forma:
# importação de dependências
import json
from flask_api import status
from werkzeug.local import LocalProxy
def execute(request: LocalProxy, config: dict) -> tuple:
# dependências
from ImpôtsError import ImpôtsError
from TaxPayer import TaxPayer
# recuperamos o corpo da postagem — aguardamos uma lista de dicionários
msg_erreur = None
list_dict_taxpayers = None
# o corpo jSON do POST
request_text = request.data
try:
# que é transformado em uma lista de dicionários
list_dict_taxpayers = json.loads(request_text)
except BaseException as erreur:
# registra-se o erro
msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
# temos uma lista não vazia?
if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
# observa-se o erro
msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
# temos uma lista de dicionários?
if not msg_erreur:
erreur = False
i = 0
while not erreur and i < len(list_dict_taxpayers):
erreur = not isinstance(list_dict_taxpayers[i], dict)
i += 1
# erro?
if erreur:
msg_erreur = "le corps du POST doit être une liste de dictionnaires"
# erro?
if msg_erreur:
# envia-se uma resposta de erro ao cliente
résultats = {"réponse": {"erreurs": [msg_erreur]}}
return résultats, status.HTTP_400_BAD_REQUEST
# verificamos os TaxPayers um por um
# inicialmente, sem erros
list_erreurs = []
for dict_taxpayer in list_dict_taxpayers:
# cria-se um TaxPayer a partir de dict_taxpayer
msg_erreur = None
try:
# a operação seguinte eliminará os casos em que os parâmetros não sejam
# propriedades da classe TaxPayer, bem como os casos em que seus valores
# estejam incorretos
TaxPayer().fromdict(dict_taxpayer)
except BaseException as erreur:
msg_erreur = f"{erreur}"
# algumas chaves devem estar presentes no dicionário
if not msg_erreur:
# as chaves [marié, enfants, salaire] devem estar presentes no dicionário
keys = dict_taxpayer.keys()
if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
# erros?
if msg_erreur:
# observa-se o erro no próprio TaxPayer
dict_taxpayer['erreur'] = msg_erreur
# adiciona-se o TaxPayer à lista de erros
list_erreurs.append(dict_taxpayer)
# Todos os contribuintes foram processados — há erros?
if list_erreurs:
# envia-se uma resposta de erro ao cliente
résultats = {"réponse": {"erreurs": list_erreurs}}
return résultats, status.HTTP_400_BAD_REQUEST
# sem erros, podemos prosseguir
# recuperação dos dados da administração fiscal
admindata = config["admindata"]
métier = config["layers"]["métier"]
try:
# processamos os TaxPayer um por um
list_taxpayers = []
for dict_taxpayer in list_dict_taxpayers:
# cálculo do imposto
taxpayer = TaxPayer().fromdict(
{'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
'salário': dict_taxpayer['salaire']})
métier.calculate_tax(taxpayer, admindata)
# armazenamos o resultado como um dicionário
list_taxpayers.append(taxpayer.asdict())
# envia-se a resposta ao cliente
return {"réponse": {"results": list_taxpayers}}, status.HTTP_200_OK
except ImpôtsError as erreur:
# envia-se uma resposta de erro ao cliente
return {"réponse": {"erreurs": f"[{erreur}]"}}, status.HTTP_500_INTERNAL_SERVER_ERROR
- linha 9: o controlador recebe:
- a solicitação [request] do cliente;
- a configuração [config] do servidor;
- linhas 14-18: recupera-se o corpo da solicitação POST. Os parâmetros encapsulados no corpo da solicitação HTTP podem ser codificados de diferentes maneiras. Já encontramos uma delas: [x-www-form-urlencoded]. Aqui, vamos usar outra codificação: jSON;
- linha 18: [request.data] permite recuperar o corpo (body) da solicitação HTTP. Aqui, recuperamos um texto e sabemos que esse texto pertence ao jSON, que representa uma lista de dicionários [marié, enfants, salaire];
- linhas 19-24: recuperamos essa lista de dicionários;
- linhas 22-24: se a recuperação do jSON não ocorreu corretamente, registra-se o erro;
- linhas 26-28: se for constatado que o objeto recuperado não é uma lista ou que é uma lista vazia, registra-se o erro;
- linhas 29-38: se uma lista foi recuperada com sucesso, verifica-se se trata de fato de uma lista de dicionários;
- linhas 40-43: se houver erro, paramos por aqui e enviamos uma resposta de erro ao cliente;
- linhas 45-69: verifica-se agora cada um dos dicionários:
- eles devem conter as chaves [marié, enfants, salaire];
- eles devem permitir a construção de um objeto [TaxPayer] válido;
- linhas 65-69: se for detectado um erro em um dicionário, ele é inserido nesse mesmo dicionário, associado à chave ‘erro’;
- linhas 72-75: os dicionários com erros foram acumulados na lista [list_erreurs]. Se essa lista não estiver vazia, ela é enviada em uma resposta de erro ao cliente;
- linha 77: chegando a este ponto, sabemos que podemos criar uma lista de objetos do tipo [TaxPayer] a partir do corpo da solicitação enviada pelo cliente;
- linhas 84-91: processamos a lista de dicionários recebidos;
- linha 86: a partir de um dicionário, criamos um objeto [TaxPayer];
- linha 89: calcula-se o imposto desse [TaxPayer];
- linha 91: sabe-se que o [taxpayer] foi alterado pelo cálculo do imposto. Ele é transformado em dicionário e adicionado a uma lista de resultados;
- linha 93: envia-se essa lista de resultados ao cliente;
28.2.4. Testes do servidor
Vamos testar o servidor com um cliente Postman:
- iniciamos o servidor web, o SGBD, e o servidor de e-mails [hMailServer];
- iniciamos o cliente Postman e sua console (Ctrl-Alt-C);

- no [1]: enviamos uma solicitação [POST];
- em [2]: o URL do servidor;
- em [3]: o corpo da solicitação HTTP;
- em [5]: indica-se que esse corpo deverá ser enviado na forma de uma string jSON;
- em [4]: entra-se no modo [raw] para poder copiar/colar uma sequência jSON;
- em [6]: cola-se a sequência jSON extraída de um dos arquivos [résultats.json] das diferentes versões. Em seguida, mantêm-se, para cada contribuinte, apenas as propriedades [marié, salaire, enfants];

- no [7], verificamos os cabeçalhos HTTP que o cliente Postman enviará ao servidor;
- em [8], vemos que ele enviará um cabeçalho [Content-Type] indicando que a solicitação contém um corpo codificado em jSON. Isso decorre da escolha [5] feita anteriormente;

- em [9-12]: colocamos na solicitação os identificadores esperados pelo servidor;
Enviamos essa solicitação. A resposta do servidor é a seguinte:

- em [3], recebemos de jSON;
- em [4], o imposto dos contribuintes;
Vamos examinar no console do Postman (Ctrl-Alt-C) o diálogo cliente/servidor que ocorreu:
O cliente Postman enviou o seguinte texto:
- linha 1: o POST para o servidor;
- linha 2: o cabeçalho de autenticação HTTP;
- linha 3: o cliente informa ao servidor que está enviando uma sequência jSON e que essa sequência tem 824 bytes (linha 11);
- linhas 13-69: o corpo jSON da solicitação;
O servidor respondeu com o seguinte texto:
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 1461
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:16:34 GMT
{"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}]}}
- linha 1: a solicitação foi bem-sucedida;
- linha 2: o corpo da resposta do servidor é uma sequência de caracteres jSON. Ela tem 1.461 bytes (linha 3);
- linha 7: a resposta jSON do servidor;
Vamos agora testar alguns casos de erro.
Caso 1: enviamos qualquer coisa
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 47652706-9744-46a0-a682-de010e5406c0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 3
abc
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 125
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:43:27 GMT
{"réponse": {"erreurs": ["le corps du POST n'est pas une chaîne jSON valide : Expecting value: line 1 column 1 (char 0)"]}}
- linha 13: enviamos a sequência [abc], que não é uma sequência jSON válida (linha 3);
- linha 15: o servidor responde com um código de erro 400;
- linha 21: a resposta jSON do servidor;
Caso 2: vamos enviar uma sequência válida jSON que não seja uma lista
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 03b64735-9239-47b3-b92d-be7c9ebc7559
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 17
{"att1":"value1"}
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 97
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:50:11 GMT
{"réponse": {"erreurs": ["le corps du POST n'est pas une liste ou alors cette liste est vide"]}}
Caso 3: enviemos uma sequência jSON que seja uma lista cujos elementos não sejam todos dicionários
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: a1528a5f-777c-413f-b3be-7d4e9955b12a
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 7
[0,1,2]
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 85
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:52:10 GMT
{"réponse": {"erreurs": ["le corps du POST doit être une liste de dictionnaires"]}}
Caso 4: vamos enviar uma lista de dicionários com um dicionário que não possui as chaves corretas
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: ba964d81-c9d9-46ff-a521-b4c4e5639484
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 19
[{"att1":"value1"}]
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 112
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:54:33 GMT
{"réponse": {"erreurs": [{"att1": "value1", "erreur": "MyException[2, la clé [att1] n'est pas autorisée]"}]}}
Caso 5: vamos enviar uma lista de dicionários com um dicionário com chaves ausentes:
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 98aec51d-f37d-4c14-81cd-c7ffcbbcdc65
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 18
[{"marié":"oui"}]
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 125
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:56:40 GMT
{"réponse": {"erreurs": [{"marié": "oui", "erreur": "le dictionnaire doit inclure les clés [marié, enfants, salaire]"}]}}
Caso 6: vamos enviar uma lista de dicionários com um dicionário que possui as chaves corretas, mas algumas delas com valores incorretos:
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 3083e601-dee4-4e15-9ea4-fc0328d0fcf0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 46
[{"marié":"x", "enfants":"x", "salaire":"x"}]
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 167
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:59:32 GMT
{"réponse": {"erreurs": [{"marié": "x", "enfants": "x", "salaire": "x", "erreur": "MyException[31, l'attribut marié [x] doit avoir l'une des valeurs oui / non]"}]}}
28.3. O cliente web

A pasta [http-clients/05] (versão 10) é obtida inicialmente por meio da cópia da pasta [http-clients/02] (versão 7). Em seguida, ela é modificada.
28.3.1. A camada [dao]
A camada [dao] é implementada pela seguinte classe [ImpôtsDaoWithHttpClient]:
# importações
import requests
from flask_api import status
from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsMétier import InterfaceImpôtsMétier
from TaxPayer import TaxPayer
class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
# construtor
def __init__(self, config: dict):
…
# método não utilizado
def get_admindata(self) -> AdminData:
pass
# cálculo do imposto
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
…
# cálculo do imposto no modo em lote
def calculate_tax_in_bulk_mode(self, taxpayers: list) -> list:
# permite que as exceções sejam propagadas
# converte os contribuintes em uma lista de dicionários
# mantém-se apenas as propriedades [marié, enfants, salaire]
list_dict_taxpayers = list(
map(lambda taxpayer:
taxpayer.asdict(included_keys=[
'_TaxPayer__marié',
'_TaxPayer__enfants',
'_TaxPayer__salaire']),
taxpayers))
# conexão com o servidor
config_server = self.__config_server
if config_server['authBasic']:
response = requests.post(config_server['urlServer'], json=list_dict_taxpayers,
auth=(config_server["user"]["login"],
config_server["user"]["password"]))
else:
response = requests.post(config_server['urlServer'], json=list_dict_taxpayers)
# modo de depuração?
if self.__debug:
# registrador
if not self.__logger:
self.__logger = self.__config['logger']
# registrando
self.__logger.write(f"{response.text}\n")
# código de status da resposta HTTP
status_code = response.status_code
# a resposta jSON é inserida em um dicionário
résultat = response.json()
# erro se o código de status for diferente de 200 OK
if status_code != status.HTTP_200_OK:
# sabe-se que os erros foram associados à chave [erreurs] da resposta
raise ImpôtsError(93, résultat['réponse']['erreurs'])
# sabe-se que o resultado foi associado à chave [results] da resposta
list_dict_taxpayers2 = résultat['réponse']['results']
# atualiza-se a lista inicial de contribuintes com os resultados recebidos
for i in range(len(taxpayers)):
# atualização dos contribuintes [i]
taxpayers[i].fromdict(list_dict_taxpayers2[i])
# aqui, o parâmetro [taxpayers] foi atualizado com os resultados do servidor
- linhas 1-26: o código permanece o mesmo da versão 7 e de outras versões;
- linhas 27-70: é introduzido um novo método, [calculate_tax_in_bulk_mode], cuja função é calcular o imposto de uma lista de contribuintes;
- linha 28: [taxpayers] é essa lista de contribuintes;
- linhas 31-39: passa-se de uma lista de objetos do tipo [TaxPayer] para uma lista de dicionários por meio da função [map];
- linhas 34-38: a função lambda utilizada transforma um objeto do tipo [TaxPayer] em um dicionário do tipo [dict] com as únicas chaves [marié, enfants, salaire]. Para isso, utiliza-se o parâmetro denominado [included_keys] do método [BaseEntity.asdict]. Lembre-se de que, para saber os nomes exatos das propriedades a serem inseridas nos parâmetros [excluded_keys, included_keys], é necessário utilizar o dicionário predefinido [taxpayer.__dict__];
- linhas 41-48: conexão com o servidor e obtenção de sua resposta HTTP;
- linhas 44, 48:
- utiliza-se o método estático [requests.post] para enviar um POST ao servidor;
- utiliza-se o parâmetro denominado [json] para indicar que o corpo do POST é uma string jSON. Isso terá duas consequências:
- o objeto atribuído ao parâmetro denominado [json], neste caso uma lista de dicionários, será transformado na string jSON;
- o cabeçalho
será incluído nos cabeçalhos HTTP do POST;
- linha 59: a resposta jSON do servidor é deserializada no dicionário [résultat];
- linhas 61-63: trata-se de um eventual erro enviado pelo servidor;
- linha 65: os resultados do cálculo do imposto estão em uma lista de dicionários;
- linhas 67-69: esses resultados são utilizados para atualizar a lista inicial de contribuintes [taxpayers] recebida inicialmente pelo método, linha 28;
- linha 70: aqui, a lista inicial de contribuintes foi atualizada com os resultados do cálculo do imposto;
28.3.2. O script principal [main]
O script principal [main] sofre as seguintes alterações: apenas a função [thread_function], executada pelas threads criadas pelo cliente, é modificada. O restante do código permanece inalterado.
# execução da camada [dao] em um thread
# “taxpayers” é uma lista de contribuintes
def thread_function(dao: ImpôtsDaoWithHttpClient, logger: Logger, taxpayers: list):
# registro de início da thread
thread_name = threading.current_thread().name
nb_taxpayers = len(taxpayers)
# registro
logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
# está-se calculando o imposto dos contribuintes
dao.calculate_tax_in_bulk_mode(taxpayers)
# registro
logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")
- linhas 9-10: enquanto anteriormente havia um loop que passava sucessivamente cada um dos contribuintes para o método [dao.calculate_tax], aqui faz-se apenas uma única chamada ao método [dao.calculate_tax_in_bulk_mode], para o qual são passados todos os contribuintes;
28.3.3. Execução do cliente
Vamos comparar os tempos de execução das versões:
- 7, em que cada contribuinte é objeto de uma consulta HTTP;
- 10 (esta), em que agrupamos os contribuintes em uma única consulta HTTP;
Primeiro, a versão 6. Para comparar as duas versões, definimos a propriedade [sleep_time] do servidor como zero, para que não haja espera forçada das threads. Os logs do cliente são os seguintes:
2020-07-28 14:20:45.811347, Thread-1 : début du thread [Thread-1] avec 4 contribuable(s)
2020-07-28 14:20:45.811347, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
…
2020-07-28 14:20:45.913065, Thread-3 : 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-28 14:20:45.913065, Thread-3 : fin du thread [Thread-3]
O tempo de execução do cliente para calcular o imposto de 11 contribuintes é, portanto, [913065-811347= 101718], ou seja, cerca de 102 milissegundos.
Vamos fazer o mesmo com a versão 10 (sleep_time do servidor zerado). Os logs do cliente são, então, os seguintes:
2020-07-28 14:25:31.871428, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-07-28 14:25:31.873594, Thread-2 : début du calcul de l'impôt des 3 contribuables
2020-07-28 14:25:31.877429, Thread-3 : début du calcul de l'impôt des 3 contribuables
2020-07-28 14:25:31.882855, Thread-4 : début du calcul de l'impôt des 1 contribuables
2020-07-28 14:25:31.930723, Thread-2 : {"réponse": {"results": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}]}}
….
2020-07-28 14:25:31.935958, Thread-4 : fin du calcul de l'impôt des 1 contribuables
2020-07-28 14:25:31.935958, Thread-1 : fin du calcul de l'impôt des 4 contribuables
O tempo de execução do cliente para calcular o imposto de 11 contribuintes é, portanto, [935958-871428= 64530 ns] (linha 8 – linha 1), ou seja, cerca de 65 milissegundos. Essa nova versão 10 traz, assim, um ganho de cerca de 57% em relação à versão 7.
28.3.4. Testes da camada [dao] do cliente

O teste [TestHttpClientDao] do cliente da versão 10 é muito semelhante ao da versão 7:
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_in_bulk_mode([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"])
# armazenando na configuração
config["logger"] = logger
# recuperando a camada [dao]
dao = config["layers"]["dao"]
# executa-se os métodos de teste
print("tests en cours...")
unittest.main()
- linha 14: em vez de chamar o método [dao.calculate_tax], chama-se o método [dao.calculate_tax_in_bulk_mode], ao qual é passada uma lista (presença dos colchetes) de um contribuinte;
Todos os testes foram aprovados.