28. Exercício da aplicação: versão 10
28.1. Introdução
Nos exemplos de clientes para o servidor de cálculo de impostos, os threads enviam N pedidos sequencialmente se tiverem de processar N contribuintes. A ideia aqui é enviar um único pedido que englobe os N contribuintes. Para cada um deles, as informações [casado, filhos, salário] devem ser enviadas. Estas podem ser enviadas como parâmetros:
- na URL. Isto resulta numa URL longa e sem sentido;
- no corpo da solicitação HTTP. Sabemos que este corpo fica oculto do utilizador que utiliza um navegador;
Em ambos os casos, pode utilizar uma solicitação [GET] ou [POST]. Utilizaremos uma solicitação POST com os parâmetros incorporados no corpo da solicitação HTTP.
A arquitetura cliente/servidor não mudou:

28.2. O servidor web

A pasta [http-servers/05] é inicialmente criada através da cópia da pasta [http-servers/02]. Voltamos às trocas JSON entre o cliente e o servidor. Vimos que a mudança de JSON para XML é muito simples.
28.2.1. Configuração
A configuração [config, config_database, config_layers] permanece a mesma das versões anteriores. Não vamos voltar a abordá-la.
28.2.2. O script principal [main]
O script [main] é idêntico ao da pasta [http-servers/02] que copiámos. Apenas uma coisa difere:
- linha 2: o URL / é agora acedido através de um pedido POST;
28.2.3. O [index_controller]
O [index_controller] evolui da seguinte forma:
- Linha 9: O controlador recebe:
- o pedido do cliente;
- a configuração do servidor [config];
- linhas 14–18: Recuperamos o corpo da solicitação POST. Os parâmetros encapsulados no corpo da solicitação HTTP podem ser codificados de várias maneiras. Já vimos uma delas: [x-www-form-urlencoded]. Aqui, usaremos outra codificação: JSON;
- linha 18: [request.data] recupera o corpo da solicitação HTTP. Aqui, recuperamos texto, e sabemos que este texto é JSON representando uma lista de dicionários [casado, filhos, salário];
- linhas 19–24: recuperamos esta lista de dicionários;
- linhas 22–24: se a recuperação do JSON falhar, registamos o erro;
- linhas 26–28: se verificarmos que o objeto recuperado não é uma lista ou é uma lista vazia, registamos o erro;
- linhas 29–38: se uma lista foi recuperada com sucesso, verificamos se é de facto uma lista de dicionários;
- linhas 40–43: se ocorreu um erro, paramos aqui e enviamos uma resposta de erro ao cliente;
- linhas 45–69: verificamos agora cada um dos dicionários:
- eles devem conter as chaves [casado, filhos, salário];
- devem permitir-nos construir um objeto [TaxPayer] válido;
- linhas 65–69: se for detetado um erro num dicionário, este é adicionado ao mesmo dicionário sob a chave «error»;
- linhas 72–75: os dicionários que contêm erros foram reunidos na lista [list_errors]. Se esta lista não estiver vazia, é enviada numa resposta de erro ao cliente;
- linha 77: nesta altura, 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: calculamos o imposto para este [TaxPayer];
- linha 91: sabemos que [taxpayer] foi modificado pelo cálculo do imposto. Convertemo-lo num dicionário e adicionamo-lo a uma lista de resultados;
- linha 93: esta lista de resultados é enviada ao cliente;
28.2.4. Teste do servidor
Iremos testar o servidor utilizando um cliente Postman:
- Iniciamos o servidor web, o SGBD e o servidor de e-mail [hMailServer];
- Iniciamos o cliente Postman e a sua consola (Ctrl-Alt-C);

- em [1]: enviamos uma solicitação [POST];
- em [2]: a URL do servidor;
- em [3]: o corpo da solicitação HTTP;
- em [5]: especificamos que este corpo deve ser enviado como uma string JSON;
- em [4]: mudamos para o modo [raw] para poder copiar e colar uma string JSON;
- em [6]: coloque a string JSON retirada de um dos ficheiros [results.json] para as diferentes versões. Em seguida, para cada contribuinte, mantenha apenas as propriedades [casado, salário, filhos];

- em [7], analisamos 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. Isto deve-se à escolha feita anteriormente em [5];

- em [9-12]: incluímos as credenciais esperadas pelo servidor na solicitação;
Enviamos esta solicitação. A resposta do servidor é a seguinte:

- em [3], recebemos JSON;
- em [4], o imposto dos contribuintes;
Vamos examinar o diálogo cliente/servidor que ocorreu na consola do Postman (Ctrl-Alt-C):
O cliente Postman enviou o seguinte texto:
- linha 1: o pedido POST para o servidor;
- linha 2: o cabeçalho de autenticação HTTP;
- linha 3: o cliente informa ao servidor que está a enviar uma cadeia JSON e que esta cadeia 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: o pedido foi bem-sucedido;
- linha 2: o corpo da resposta do servidor é uma cadeia JSON. Tem 1461 bytes de comprimento (linha 3);
- linha 7: a resposta JSON do servidor;
Agora vamos 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: foi enviada 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: Vamos enviar uma string JSON válida 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: Vamos enviar uma cadeia JSON que é uma lista cujos elementos não são 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 tem 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 que contém 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: Vamos enviar uma lista de dicionários, sendo que um deles contém as chaves corretas, mas alguns têm 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

O ficheiro [http-clients/05] (versão 10) é inicialmente obtido através da cópia do ficheiro [http-clients/02] (versão 7). Em seguida, é modificado.
28.3.1. A camada [dao]
A camada [dao] é implementada pela seguinte classe [ImpôtsDaoWithHttpClient]:
- 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], cujo objetivo é calcular o imposto para uma lista de contribuintes;
- linha 28: [taxpayers] é esta lista de contribuintes;
- linhas 31–39: convertemos uma lista de objetos [TaxPayer] numa lista de dicionários utilizando a função [map];
- linhas 34–38: a função lambda utilizada transforma um objeto do tipo [TaxPayer] num dicionário do tipo [dict] com apenas as chaves [married, children, salary]. Para tal, utilizamos o parâmetro denominado [included_keys] do método [BaseEntity.asdict]. Note que, para determinar os nomes exatos das propriedades a incluir nos parâmetros [excluded_keys, included_keys], deve utilizar o dicionário predefinido [taxpayer.__dict__];
- linhas 41–48: ligar-se ao servidor e recuperar a sua resposta HTTP;
- linhas 44, 48:
- Utilizamos o método estático [requests.post] para enviar um pedido POST ao servidor;
- O parâmetro denominado [json] é utilizado para indicar que o corpo da solicitação POST é uma cadeia de caracteres JSON. Isto terá duas consequências:
- o objeto atribuído ao parâmetro denominado [json], neste caso uma lista de dicionários, será convertido numa cadeia JSON;
- o cabeçalho
será incluído nos cabeçalhos HTTP do POST;
- linha 59: a resposta JSON do servidor é deserializada para o dicionário [result];
- linhas 61–63: qualquer erro enviado pelo servidor é tratado;
- linha 65: os resultados do cálculo do imposto estão numa lista de dicionários;
- linhas 67–69: estes resultados são utilizados para atualizar a lista inicial de contribuintes [taxpayers] originalmente recebida pelo método na 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] evolui da seguinte forma: apenas a função [thread_function] executada pelos threads criados pelo cliente é modificada. O resto do código permanece inalterado.
- linhas 9–10: enquanto anteriormente tínhamos um ciclo que passava cada contribuinte, um por um, para o método [dao.calculate_tax], aqui fazemos uma única chamada ao método [dao.calculate_tax_in_bulk_mode], passando-lhe 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 um pedido HTTP;
- 10 (esta), em que os contribuintes são agrupados numa única solicitação 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 para os 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, aproximadamente 102 milissegundos.
Vamos fazer o mesmo com a versão 10 (tempo de espera do servidor definido como 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, aproximadamente 65 milissegundos. Esta nova versão 10 proporciona, assim, um ganho de desempenho de aproximadamente 57% em relação à versão 7.
28.3.4. Testes da camada [dao] do cliente

O teste [TestHttpClientDao] para o cliente na versão 10 é muito semelhante ao da versão 7:
- linha 14: em vez de chamar o método [dao.calculate_tax], chamamos o método [dao.calculate_tax_in_bulk_mode], passando-lhe uma lista (indicada pelos parênteses retos) de contribuintes;
Todos os testes são aprovados.