Skip to content

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 pedidos sequencialmente se tivessem de processar N contribuintes. A ideia aqui é enviar um único pedido que englobe os N contribuintes. Para cada um deles, é necessário enviar as informações [marié, enfants, salaire]. Estas podem ser enviadas como parâmetros:

  • da URL. Teremos então uma URL longa e pouco significativa;
  • no corpo (body) da solicitação HTTP. Sabe-se que este corpo fica oculto ao utilizador que utiliza um navegador;

Em ambos os casos, é possível utilizar uma solicitação [GET] ou [POST]. Utilizaremos uma solicitação POST com os parâmetros encapsulados no corpo da solicitação HTTP.

A arquitetura cliente/servidor não sofreu alterações:

Image

28.2. O servidor web

Image

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 do jSON para o XML é muito simples.

28.2.1. Configuração

A configuração do [config, config_database, config_layers] mantém-se semelhante à das versões anteriores. Não voltaremos a abordá-la.

28.2.2. O script principal [main]

O script [main] é idêntico ao da pasta [http-servers/02] que copiámos. Há apenas uma diferença:


# Página inicial URL
@app.route('/', methods=['POST'])
@auth.login_required
def index():

  • linha 2: agora, o URL é obtido através 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 publicação — aguardamos uma lista de dicionários
    msg_erreur = None
    list_dict_taxpayers = None
    # o corpo jSON do POST
    request_text = request.data
    try:
        # que se transforma numa lista de dicionários
        list_dict_taxpayers = json.loads(request_text)
    except BaseException as erreur:
        # regista-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):
        # regista-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

    # verificam-se os TaxPayers um a 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 irá eliminar os casos em que os parâmetros não sejam
            # propriedades da classe TaxPayer, bem como os casos em que os seus valores
            # estejam incorretos
            TaxPayer().fromdict(dict_taxpayer)
        except BaseException as erreur:
            msg_erreur = f"{erreur}"
        # algumas chaves têm de estar presentes no dicionário
        if not msg_erreur:
            # as chaves [marié, enfants, salaire] têm de 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)

    # foram processados todos os contribuintes — existem 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, é possível prosseguir
    # recuperação dos dados da administração fiscal
    admindata = config["admindata"]
    métier = config["layers"]["métier"]
    try:
        # processamos os TaxPayer um a 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)
            # guardamos 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:
      • o pedido [request] do cliente;
      • a configuração [config] do servidor;
  • linhas 14-18: recupera-se o corpo do POST. Os parâmetros encapsulados no corpo da solicitação HTTP podem ser codificados de diferentes formas. Já nos deparámos com uma delas: [x-www-form-urlencoded]. Vamos utilizar aqui outra codificação: jSON;
  • linha 18: [request.data] permite recuperar o corpo (body) do pedido HTTP. Aqui recuperamos texto e sabemos que esse texto provém de jSON, que representa uma lista de dicionários [marié, enfants, salaire];
  • linhas 19-24: recupera-se esta lista de dicionários;
  • linhas 22-24: se a recuperação do jSON falhar, regista-se o erro;
  • linhas 26-28: se se verificar que o objeto recuperado não é uma lista ou que é uma lista vazia, regista-se o erro;
  • linhas 29-38: se tiver sido recuperada uma lista, verifica-se se se trata efetivamente de uma lista de dicionários;
  • linhas 40-43: se tiver ocorrido um erro, paramos aqui e enviamos uma resposta de erro ao cliente;
  • linhas 45-69: verifica-se agora cada um dos dicionários:
    • devem conter as chaves [marié, enfants, salaire];
    • devem permitir construir um objeto [TaxPayer] válido;
  • linhas 65-69: se tiver sido detetado um erro num dicionário, este é inserido nesse mesmo dicionário, associado à chave «erro»;
  • linhas 72-75: os dicionários com erros foram reunidos na lista [list_erreurs]. Se esta lista não estiver vazia, é enviada numa resposta de erro ao cliente;
  • linha 77: chegados a este ponto, sabemos que podemos criar uma lista de objetos do tipo [TaxPayer] a partir do corpo do pedido enviado 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 deste [TaxPayer];
  • linha 91: sabe-se que o [taxpayer] foi alterado pelo cálculo do imposto. Transforma-se-o num dicionário e adiciona-se-o a uma lista de resultados;
  • linha 93: envia-se esta 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-mail [hMailServer];
  • iniciamos o cliente Postman, bem como a sua consola (Ctrl-Alt-C);

Image

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

Image

  • no [7], analisamos os cabeçalhos HTTP que o cliente Postman irá enviar ao servidor;
  • em [8], verifica-se que ele lhe vai enviar um cabeçalho [Content-Type] indicando que a solicitação contém um corpo codificado em jSON. Isto resulta da escolha [5] feita anteriormente;

Image

  • em [9-12]: inserimos na solicitação os identificadores esperados pelo servidor;

Enviamos esta solicitação. A resposta do servidor é a seguinte:

Image

  • em [3], recebemos de jSON;
  • em [4], o imposto dos contribuintes;

Vamos analisar na consola do Postman (Ctrl-Alt-C) o diálogo cliente/servidor que ocorreu:

O cliente Postman enviou o seguinte texto:

POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 03c4aa28-5a5d-4bb5-ac51-7ad51968c71d
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 824

[
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 55555
  },
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 50000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 50000
  },
  {
    "marié": "non",
    "enfants": 2,
    "salaire": 100000
  },
  {
    "marié": "non",
    "enfants": 3,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 5,
    "salaire": 100000
  },
  {
    "marié": "non",
    "enfants": 0,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 30000
  },
  {
    "marié": "non",
    "enfants": 0,
    "salaire": 200000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 200000
  }
]
  • linha 1: o POST para o servidor;
  • linha 2: o cabeçalho de autenticação HTTP;
  • linha 3: o cliente informa ao servidor que lhe está a enviar uma cadeia jSON e que essa cadeia tem 824 octetos (linha 11);
  • linhas 13-69: o corpo jSON do pedido;

O servidor respondeu-lhe 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: o pedido foi bem-sucedido;
  • linha 2: o corpo da resposta do servidor é uma cadeia de caracteres jSON. Esta tem 1461 octetos (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: enviámos a cadeia [abc], que não é uma cadeia 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: enviemos uma cadeia 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 cadeia 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: enviemos uma lista de dicionários com um dicionário que não tenha 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: enviemos uma lista de dicionários com um dicionário com chaves em falta:


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: enviemos uma lista de dicionários com um dicionário que tem as chaves corretas, mas algumas com valores errados:


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

Image

A pasta [http-clients/05] (versão 10) é obtida inicialmente através da cópia da pasta [http-clients/02] (versão 7). Em seguida, é 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 em modo em massa
    def calculate_tax_in_bulk_mode(self, taxpayers: list) -> list:
        # permite que as exceções sejam propagadas

        # converte os contribuintes numa lista de dicionários
        # só se mantêm as propriedades [marié, enfants, salaire]
        list_dict_taxpayers = list(
            map(lambda taxpayer:
                taxpayer.asdict(included_keys=[
                    '_TaxPayer__marié',
                    '_TaxPayer__enfants',
                    '_TaxPayer__salaire']),
                taxpayers))

        # ligação ao 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:
            # registo
            if not self.__logger:
                self.__logger = self.__config['logger']
            # a registar
            self.__logger.write(f"{response.text}\n")
        # código de estado da resposta HTTP
        status_code = response.status_code
        # a resposta jSON é colocada num dicionário
        résultat = response.json()
        # erro se o código de estado 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 tal como estava na versão 7 e noutras 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 através de uma função [map];
  • linhas 34-38: a função lambda utilizada transforma um objeto do tipo [TaxPayer] num dicionário do tipo [dict] com as únicas chaves [marié, enfants, salaire]. Para tal, utiliza-se o parâmetro denominado [included_keys] do método [BaseEntity.asdict]. Recorde-se que, para conhecer os nomes exatos das propriedades a incluir nos parâmetros [excluded_keys, included_keys], é necessário utilizar o dicionário predefinido [taxpayer.__dict__];
  • linhas 41-48: ligação ao servidor e obtenção da respetiva resposta HTTP;
  • linhas 44, 48:
    • utiliza-se o método estático [requests.post] para enviar um POST para o servidor;
    • utiliza-se o parâmetro denominado [json] para indicar que o corpo do POST é uma cadeia jSON. Isto terá duas consequências:
      • o objeto atribuído ao parâmetro denominado [json], neste caso uma lista de dicionários, será transformado numa cadeia jSON;
      • o cabeçalho
Content-Type: application/json

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 encontram-se numa lista de dicionários;
  • linhas 67-69: estes resultados são utilizados para atualizar a lista inicial de contribuintes [taxpayers] inicialmente recebida 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 pelos threads criados pelo cliente, é alterada. O resto do código permanece inalterado.


# execução da camada [dao] num thread
# «taxpayers» é uma lista de contribuintes
def thread_function(dao: ImpôtsDaoWithHttpClient, logger: Logger, taxpayers: list):
    # registo do início da thread
    thread_name = threading.current_thread().name
    nb_taxpayers = len(taxpayers)
    # registo
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    # está a ser calculado o imposto dos contribuintes
    dao.calculate_tax_in_bulk_mode(taxpayers)
    # registo
    logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")
  • linhas 9-10: enquanto anteriormente havia um ciclo 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], ao 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 se agrupam os contribuintes numa única consulta HTTP;

Primeiro, a versão 6. Para comparar as duas versões, definimos a propriedade [sleep_time] do servidor para zero, para que não haja espera forçada das threads. Os registos 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 a zero). Os registos 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. Esta nova versão 10 proporciona, assim, um ganho de cerca de 57 % em relação à versão 7.

28.3.4. Testes da camada [dao] do cliente

Image

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, 'majoração': 0, 'redução': 0, 'abatimento': 0, 'taxa': 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 a aplicação
    import config
    config = config.configure({})

    # registo
    logger = Logger(config["logsFilename"])
    # armazenar na configuração
    config["logger"] = logger
    # recuperar a camada [dao]
    dao = config["layers"]["dao"]

    # executam-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 se passa uma lista (presença dos parênteses retos) de um contribuinte;

Todos os testes são bem-sucedidos.