23. Exercício prático: Versão 6
23.1. Introdução
Voltamos agora à nossa aplicação de cálculo de impostos. Iremos criar várias aplicações web em torno dela.
Na versão 5 do nosso exercício de aplicação, os dados da autoridade fiscal estavam armazenados numa base de dados. Esta versão 5 consistia em duas aplicações separadas que partilhavam camadas comuns:
- um aplicativo que calculava impostos no modo |batch| para contribuintes armazenados num ficheiro de texto;
- uma aplicação que calculava impostos no modo |interativo| para contribuintes cujas informações eram introduzidas através do teclado;
A versão 5 da aplicação de cálculo de impostos em lote tinha a seguinte arquitetura:

Em última análise, a versão web deste aplicativo terá a seguinte arquitetura:

- o cliente web [1] comunica com o servidor web [2], que comunica com o SGBD [3];
- o servidor web [2] mantém as camadas [negócio] [8] e [DAO] [9] da aplicação original;
- A aplicação original mantém o seu script principal [4] e a sua camada [de negócios] [15]. As camadas [de negócios] [8] e [15] são idênticas;
- a comunicação cliente/servidor requer duas camadas adicionais:
- a camada [web] [7], que implementa a aplicação web;
- a camada [DAO] [5], que atua como cliente para a aplicação web [7];
Na versão final, o cálculo de impostos em lote pode ser realizado de duas formas:
- a lógica de negócio para o cálculo de impostos é tratada pela camada [de negócio] do servidor. O script [main] utilizará este método;
- a lógica de negócio para o cálculo de impostos é tratada pela camada [de negócio] do cliente. O script [main2] utilizará este método;
A partir de agora, iremos desenvolver várias aplicações cliente/servidor do tipo acima descrito, cada uma ilustrando uma ou mais novas tecnologias de desenvolvimento web.
23.2. O servidor web de cálculo de impostos
23.2.1. Versão 1

O script [server_01] é a seguinte aplicação web:

- Em [1], utilizamos um URL parametrizado no qual passamos três valores:
- [married] (sim/não) para indicar se o contribuinte é casado;
- [children]: o número de filhos que o contribuinte tem;
- [salary]: o salário anual do contribuinte;
- Em [2], o servidor web devolve uma cadeia JSON que fornece o montante do imposto devido, juntamente com os seus vários componentes;
A arquitetura da aplicação é a seguinte:

- o navegador [1] consulta o servidor [2]. O script [server_01] implementa a camada [web] [2] do servidor;
- as camadas [3-8] são as já utilizadas na |versão 5| da aplicação de cálculo de impostos. Reutilizamo-las tal como estão;
- A camada [negócio] [3] é definida |aqui|;
- a camada [DAO] [4] é definida |aqui|;
A aplicação web [server_01] é configurada utilizando três scripts:
- [config], que configura toda a aplicação;
- [config_database], que configura o acesso à base de dados. Iremos trabalhar com os SGBDs MySQL e PostgreSQL;
- [config_layers], que configura as camadas da aplicação;
O script [config] é o seguinte:
- A função [configure] recebe um dicionário [config] como argumento (linha 1) e devolve-o como resultado (linha 54) após enriquecer o seu conteúdo. Já há muito se poderia ter salientado que não era necessário devolver o resultado [config]. Com efeito, [config] é uma referência a um dicionário que o código de chamada partilha com o código chamado. O código chamador, portanto, já possui esta referência (linha 1), e não há necessidade de a devolver novamente (linha 54). Assim, escrever:
config=[module].configure(config) (1)
é redundante. Basta escrever:
[module].configure(config) (2)
No entanto, mantive o estilo de escrita (1) porque achei que poderia ilustrar melhor que o código chamado modifica o dicionário [config].
- linha 1: o dicionário [config] recebido pela função [configure] tem uma chave «sgbd» cujo valor é retirado da lista [«mysql», «pgres»]. [mysql] significa que a base de dados utilizada é gerida pelo MySQL, enquanto «pgres» significa que a base de dados utilizada é gerida pelo PostgreSQL;
- Linhas 4–27: listamos todos os diretórios que contêm elementos necessários para a aplicação web. Estes farão parte do Python Path da aplicação (linhas 30–31);
- linhas 33–40: apenas determinados utilizadores terão permissão para aceder à aplicação. Aqui, temos uma lista com um único utilizador;
- linhas 43–46: o script [config_database] cria a configuração para a base de dados que está a ser utilizada;
- linha 46: a configuração criada pelo script [config_database] é um dicionário que armazenamos na configuração geral associada à chave «database»;
- linhas 48–51: o script [config_layers] instancia as camadas da aplicação web. Ele retorna um dicionário que é armazenado na configuração geral sob a chave «layers»;
O script [config_database] é o mesmo que já foi utilizado na |versão 5|. Incluímo-lo aqui para referência:
O script [config_layers] configura as camadas do servidor web. Reutilizamos um |script| que já vimos anteriormente:
- Linha 6: A camada [dao] é implementada utilizando uma base de dados;
- [ImpotsDaoWithAdminDataInDatabase] foi definido |aqui|;
- [BusinessTaxes] foi definido |aqui|;
O script principal [server_01] é o seguinte:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | |
- linhas 1–10: recuperar o parâmetro que indica qual o SGBD a utilizar;
- linhas 12–14: Com esta informação, podemos configurar a aplicação. Em particular, o Python Path é construído;
- linhas 16–23: Com o novo Python Path, importamos os módulos necessários;
- linhas 25–31: recuperam dados da autoridade fiscal para calcular o imposto;
- linhas 33–34: instanciamento da aplicação Flask;
- Linha 38: A aplicação Flask apenas serve a URL [/]. Espera uma URL formatada da seguinte forma: [/ ?married=xx&children=yy&salary=zz], onde:
- xx: sim / não;
- yy: número de filhos;
- zz: salário anual;
- linhas 40–89: verificamos a validade dos parâmetros da URL;
- linha 41: vamos acumular mensagens de erro na lista [errors];
- linha 43: deve lembrar-se de que os parâmetros da URL se encontram em [request.args] (ver |aqui|):
- o objeto [request] é o objeto Flask importado na linha 20;
- o objeto [request.args] comporta-se como um dicionário;
- linhas 43–44: verificamos se existem exatamente três parâmetros (nem menos, nem mais);
- linhas 46–49: verificamos se o parâmetro [married] está presente na URL;
- linhas 50–54: se estiver presente, verificamos se o seu valor em minúsculas, sem espaços à esquerda e à direita, é «yes» ou «no»;
- linhas 56–59: verificamos se o parâmetro [children] está na URL;
- linhas 60–66: se estiver presente, verificamos se o seu valor é um número inteiro positivo;
- linha 66: lembre-se de que os parâmetros da URL e os seus valores são cadeias de caracteres. O valor do parâmetro [children] é convertido para um «int»;
- linhas 68–78: Para o parâmetro [salary], realizamos as mesmas verificações que para o parâmetro [children];
- linhas 81–83: verificamos se não existem outros parâmetros além de [‘married’, ‘children’, ‘salary’] na URL;
- linhas 85–89: se, após todas estas verificações, a lista [errors] não estiver vazia, enviamos esta lista de erros ao cliente como uma cadeia de caracteres JSON juntamente com o código de estado [400 Bad Request];
Uma vez que, mais tarde, teremos frequentemente de enviar uma string JSON em resposta ao cliente, as poucas linhas necessárias para tal foram integradas no módulo [myutils.py] que já utilizámos:

O script [myutils.py] fica assim:
- Linha 16: A função [json_response] espera dois parâmetros:
- [response]: o dicionário que contém a cadeia JSON a enviar para o cliente web;
- [status_code]: o código de estado HTTP da resposta;
- linha 18: definimos o corpo JSON da resposta;
- linha 20: adicionamos o cabeçalho HTTP que informa ao cliente web que receberá JSON;
- linha 22: enviamos a resposta HTTP para o código de chamada. Cabe ao código de chamada enviá-la para o cliente web;
O ficheiro [__init__.py] é alterado da seguinte forma:
from .myutils import set_syspath, json_response
A nova versão do [myutils] é instalada entre os módulos do sistema utilizando o comando [pip install .] num terminal do PyCharm:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install .
Processing c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\packages
Using legacy setup.py install for myutils, since package 'wheel' is not installed.
Installing collected packages: myutils
Attempting uninstall: myutils
Found existing installation: myutils 0.1
Uninstalling myutils-0.1:
Successfully uninstalled myutils-0.1
Running setup.py install for myutils ... done
Successfully installed myutils-0.1
- Linha 1: Deve estar na pasta [packages] para introduzir este comando;
O código do script [server_01] continua da seguinte forma:
- linha 10: nesta altura, os parâmetros esperados na URL estão presentes e corretos;
- linha 10: criamos o objeto [TaxPayer] que modela o contribuinte;
- linha 11: solicitamos à camada [business] que calcule o imposto. Note-se que os elementos calculados pela camada [business] são inseridos no objeto [taxpayer] passado como parâmetro;
- linha 13: a resposta é enviada ao cliente web como uma cadeia JSON. Esta é a cadeia JSON de um dicionário. Associado à chave [result], colocamos o dicionário do objeto [taxpayer]. Não foi possível colocar o próprio objeto [taxpayer] porque não é serializável em JSON;
Criamos duas configurações de execução, uma para MySQL e outra para PostgreSQL:

Aqui estão alguns exemplos de execução (já iniciou a aplicação [server_01] e o SGBD, e depois solicitou a URL http://localhost:5000/ utilizando um navegador):


Aqui está um exemplo da solicitação na consola do Postman:

GET /?mari%C3%A9=xx&enfants=yy&salaire=zz HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: e4c5df8c-4bd6-4250-b789-b7b164db4eff
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 134
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 06:15:44 GMT
{"réponse": {"erreurs": ["paramètre marié [xx] invalide", "paramètre enfants [yy] invalide", "paramètre salaire [zz] invalide"]}}
- linha 1: foi solicitada uma URL incorreta;
- linha 10: o servidor responde com o estado 400 BAD REQUEST;
23.2.2. Versão 2

A versão 2 do servidor isola o processamento de URLs no módulo [index_controller] [5]:
- linha 9: a função [execute] recebe dois parâmetros:
- [request]: o pedido HTTP do cliente;
- [config]: o dicionário de configuração da aplicação;
O script [server_02] é o seguinte:
- linhas 36–41: tratamento da rota /;
- linha 39: utilização da função [IndexController.execute];
Vamos agora utilizar esta técnica: cada rota será tratada pelo seu próprio módulo.
Os resultados da execução são os mesmos da versão 1.
23.2.3. Versão 3
A versão 3 introduz o conceito de autenticação.
O script [server_03] passa a ser o seguinte:
- linha 21: importar um manipulador de autenticação. Existem vários tipos de autenticação para um servidor web. O que estamos a utilizar aqui chama-se [HTTP Basic]. Cada tipo de autenticação segue um diálogo cliente/servidor específico;
- linha 33: cria uma instância do manipulador de autenticação;
- linha 37: a anotação [@auth.verify_password] marca a função a ser executada quando o manipulador de autenticação quiser verificar o nome de utilizador e a palavra-passe enviados pelo cliente de acordo com o protocolo [HTTP Basic];
- linha 55: a anotação [@auth.login_required] marca uma rota para a qual o cliente web deve ser autenticado. Se o cliente web ainda não tiver enviado as suas credenciais, o servidor web irá solicitá-las automaticamente utilizando o protocolo HTTP Basic;
O módulo [flask_httpauth] deve estar instalado:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\01\flask>pip install flask_httpauth
Collecting flask_httpauth
Downloading Flask_HTTPAuth-4.1.0-py2.py3-none-any.whl (5.8 kB)
Requirement already satisfied: Flask in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from flask_httpauth) (1.1.2)
Requirement already satisfied: itsdangerous>=0.24 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (1.1.0)
Requirement already satisfied: click>=5.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (7.1.2)
Requirement already satisfied: Jinja2>=2.10.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (2.11.2)
Requirement already satisfied: Werkzeug>=0.15 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (1.0.1)
Requirement already satisfied: MarkupSafe>=0.23 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Jinja2>=2.10.1->Flask->flask_httpauth) (1.1.1
)
Installing collected packages: flask-httpauth
Successfully installed flask-httpauth-4.1.0
Vamos ver o que acontece na consola do Postman. Você:
- crie uma configuração de execução;
- inicie a aplicação web;
- inicie a base de dados da sua escolha;
- solicite a URL [/] com o Postman;
O diálogo cliente/servidor na consola do Postman é o seguinte:
- Linha 10: O servidor responde que não estamos autorizados a aceder ao URL [/];
- Linha 13: Indica-nos qual o protocolo de autenticação a utilizar, neste caso o protocolo de Autenticação Básica;
É possível configurar o Postman para enviar as credenciais do utilizador de acordo com o protocolo de Autenticação Básica:

- em [6-7] introduzimos as credenciais presentes no script [config]:
config['users'] = [
{
"login": "admin",
"password": "admin"
}
]
O diálogo cliente/servidor na consola do Postman fica assim:
GET / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 5ce20822-e87c-4eef-a2f4-b9eaec38d881
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 203
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 07:20:01 GMT
{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]", "paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
- Linha 2: O cliente Postman envia as credenciais do utilizador [admin / admin] de forma encriptada;
- linha 17: o servidor responde corretamente. Ele reporta erros porque os parâmetros [casado, filhos, salário] não foram enviados (linha 1), mas não reporta um erro de autenticação;
Agora vamos solicitar a URL / utilizando um navegador (Firefox abaixo):

- Tal como no Postman, o Firefox recebeu a resposta HTTP do servidor com os seguintes cabeçalhos HTTP:
O Firefox, tal como outros navegadores, não interrompe a caixa de diálogo quando recebe estes cabeçalhos. Solicita ao utilizador as credenciais pedidas pelo servidor. No exemplo acima, basta digitar admin / admin para receber a resposta do servidor:

23.3. O cliente web do servidor de cálculo de impostos
23.3.1. Introdução
Na secção anterior, o cliente web para o servidor de cálculo de impostos era um navegador. Nesta secção, o cliente web será um script de consola. A arquitetura fica da seguinte forma:

- o cliente web é composto pelas camadas [1-2];
- o servidor web é composto pelas camadas [3-9]. Conforme mencionado na secção anterior;
Por isso, precisamos de escrever as camadas [1-2].
A camada [dao] [2] deve ser capaz de comunicar com o servidor web [3]. Agora compreendemos o protocolo HTTP e poderíamos escrever, utilizando o módulo [pycurl] que já estudámos, por exemplo, um script que comunica com o servidor web [3]. No entanto, existem módulos especializados na comunicação cliente/servidor HTTP. Vamos utilizar um deles, o módulo [requests]:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\01\flask>pip install requests
Collecting requests
Downloading requests-2.24.0-py2.py3-none-any.whl (61 kB)
|| 61 kB 137 kB/s
Collecting idna<3,>=2.5
Downloading idna-2.10-py2.py3-none-any.whl (58 kB)
|| 58 kB 692 kB/s
Collecting chardet<4,>=3.0.2
Downloading chardet-3.0.4-py2.py3-none-any.whl (133 kB)
|| 133 kB 1.3 MB/s
Collecting urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1
Downloading urllib3-1.25.9-py2.py3-none-any.whl (126 kB)
|| 126 kB 1.1 MB/s
Collecting certifi>=2017.4.17
Downloading certifi-2020.6.20-py2.py3-none-any.whl (156 kB)
|| 156 kB 1.1 MB/s
Installing collected packages: idna, chardet, urllib3, certifi, requests
Successfully installed certifi-2020.6.20 chardet-3.0.4 idna-2.10 requests-2.24.0 urllib3-1.25.9
A estrutura de diretórios para os scripts do cliente web é a seguinte:

O script irá implementar a aplicação de cálculo de impostos em modo de lote descrita na |versão 1|. A versão mais recente desta aplicação é a |versão 5|. Aqui fica um resumo de como funciona:
- os contribuintes para os quais o imposto será calculado estão listados no ficheiro de texto [taxpayersdata.txt]:
- Os resultados são guardados em dois ficheiros:
- O ficheiro de texto [errors.txt] lista os erros detetados no ficheiro do contribuinte:
Analyse du fichier C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-clients\01\main/../data/input/taxpayersdata.txt
Ligne 15, not enough values to unpack (expected 4, got 2)
Ligne 17, MyException[1, L'identifiant d'une entité <class 'TaxPayer.TaxPayer'> doit être un entier >=0]
- (continuação)
- O ficheiro JSON [results.json] contém os resultados do cálculo dos impostos para os vários contribuintes:
[
{
"id": 0,
"marié": "oui",
"enfants": 2,
"salaire": 55555,
"impôt": 2814,
"surcôte": 0,
"taux": 0.14,
"décôte": 0,
"réduction": 0
},
{
"id": 1,
"marié": "oui",
"enfants": 2,
"salaire": 50000,
"impôt": 1384,
"surcôte": 0,
"taux": 0.14,
"décôte": 384,
"réduction": 347
},
…
]
23.3.2. Configuração do cliente Web

A configuração é realizada utilizando dois scripts:
- [config], que trata de toda a configuração fora das camadas de arquitetura;
- [config_layers], que trata da configuração das camadas de arquitetura;
O script [config] é o seguinte:
- linha 1: a função [configure] recebe como parâmetro o dicionário a ser preenchido com informações de configuração. Este dicionário pode já estar pré-preenchido ou vazio. Aqui, estará vazio;
- linhas 40–42: os caminhos absolutos dos três ficheiros de texto geridos pela camada [dao];
- linhas 43-50: associadas à chave [server], as informações que a camada [dao] precisa de saber sobre o servidor web com o qual deve comunicar:
- linha 44: o URL do serviço web;
- linha 45: a chave [authBasic] é definida como True se o acesso à URL exigir autenticação Basic;
- linhas 46–49: as credenciais do utilizador que se autenticará caso a autenticação seja necessária;
- linhas 56–57: instanciamos as camadas — neste caso, a única camada [dao] — e colocamos as referências das camadas em [config] sob a chave [layers];
O script [config_layers] é o seguinte:
- Linha 1: A função [configure] recebe o dicionário que configura a aplicação;
- linhas 4–6: a camada [dao] é instanciada. Na linha 6, passamos-lhe a configuração da aplicação, onde encontrará as informações de que necessita;
- linhas 8–11: é devolvido um dicionário contendo a referência à camada [dao];
23.3.3. O script principal [main]
O script principal [main] é uma variante do da |versão 5|:
- linhas 2-3: a aplicação está configurada;
- linha 13: a camada [dao] fornece a lista de contribuintes para os quais os impostos devem ser calculados;
- linha 21: a camada [dao] calcula o imposto para cada um deles;
- linha 23: os resultados são guardados num ficheiro JSON;
23.3.4. Implementação da camada [dao]

Vamos rever a arquitetura cliente/servidor utilizada:

- em [2, 6], vemos que a camada [dao] tem duas funções:
- acede ao sistema de ficheiros tanto para ler os dados dos contribuintes como para gravar os resultados dos cálculos fiscais. Já temos uma classe |AbstractImpôtsDao| que pode fazer isso. Está em uso desde a |versão 4|;
- comunica com o servidor web [3];
Na |versão 5|, o script principal [main] [1] comunicava diretamente com a camada [business] [4]. Preferimos não alterar este script. Para o conseguir, iremos garantir que a camada [DAO] [2] implementa a interface da camada [business] [4]. Desta forma, o script principal [main] parecerá comunicar diretamente com a camada [business] [4] e poderá ignorar completamente o facto de esta se encontrar noutro computador.
Uma definição da classe que implementa a camada [DAO] [2] poderia ser a seguinte:
class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
- A classe [TaxDaoWithHttpClient]:
- herda da classe [AbstractTaxDao], o que lhe permite gerir a comunicação com o sistema de ficheiros [6];
- implementa a interface [InterfaceImpôtsMétier] para não ter de alterar o script principal [main] da |versão 5|;
O código completo da classe [TaxDaoWithHttpClient] é o seguinte:
- linhas 21–23: A classe [AbstractTaxDao] (linha 12) possui um método abstrato [get_admindata]. Temos de o implementar, mesmo que não o utilizemos (os dados de administração são geridos pelo servidor, não pelo cliente);
- linha 26: o método [calculate_tax] pertence à interface [InterfaceImpôtsMétier] (linha 12). Temos de o implementar;
- linha 15: o construtor recebe o dicionário de configuração da aplicação como seu único parâmetro;
- linhas 16–17: a classe pai [AbstractTaxDao] é inicializada passando-lhe, também aqui, a configuração da aplicação. Encontrará aí os nomes dos três ficheiros de texto que precisa de gerir;
- linhas 18–19: as informações relativas ao servidor web de cálculo de impostos são armazenadas localmente dentro da classe;
- linha 26: o método [calculate_tax] recebe um objeto do tipo |Taxpayer| como parâmetro. Para cumprir a assinatura do método [InterfaceImpôtsMétier.calculate_tax], recebe também um parâmetro [admindata], que deve encapsular os dados da administração fiscal. No lado do cliente, não dispomos destes dados. Este parâmetro permanecerá sempre [None]. Esta solução alternativa sugere que a classe [ImpôtsMétier] foi inicialmente mal concebida:
- a assinatura de [calculate_tax] deveria ter sido simplesmente:
def calculate_tax(self, taxpayer: TaxPayer)
e o parâmetro [admindata: AdminData] deveria ter sido passado para o construtor da classe;
- linha 27: o código do método [calculate_tax] não foi encapsulado num bloco try / catch / finally. Isto significa que quaisquer exceções não serão tratadas e serão propagadas para o código de chamada, neste caso o script [main]. Este script intercepta todas as exceções propagadas a partir da camada [dao];
- Linha 28: O cálculo do imposto é realizado no lado do servidor. Por isso, precisaremos de comunicar com ele. Fazemos isso utilizando o módulo [requests] importado na linha 2;
- linhas 31–43: para enviar um pedido GET ao servidor web, utilizamos o método [requests.get]:
- linhas 33–34: o primeiro parâmetro do método é o URL a contactar;
- linhas 35–40: os outros dois parâmetros são parâmetros nomeados cuja ordem não importa;
- linhas 35-36: o valor do parâmetro nomeado [params] deve ser um dicionário contendo as informações a incluir na URL na forma [/url?param1=value1¶m2=value2&…];
- linha 29: o dicionário que contém os três parâmetros [married, children, salary] que o servidor web espera. Não precisamos de nos preocupar com a codificação (chamada urlencoded) a que estes parâmetros devem ser submetidos. O [requests] trata disso;
- linhas 37–40: o parâmetro denominado [auth] é uma tupla de dois elementos (login, password). Representa as credenciais para a autenticação Basic;
- linhas 44–45: estas duas linhas destinam-se apenas a fins didáticos (vamos comentá-las assim que a depuração estiver concluída):
- [response] representa a resposta HTTP do servidor;
- [response.text] representa o texto do documento contido nesta resposta. Durante a depuração, é útil verificar o que o servidor nos enviou;
- linha 47: [response.status_code] é o código de estado HTTP da resposta recebida. O nosso servidor envia apenas três:
- 200 OK
- 400 BAD REQUEST
- 500 ERRO INTERNO DO SERVIDOR
- linha 49: o nosso servidor envia sempre JSON, mesmo em caso de erro. A função [response.json()] cria um dicionário a partir da cadeia JSON recebida. Vamos rever as duas formas possíveis para a cadeia JSON:
{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]", "paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
{"réponse": {"result": {"id": 0, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
- linhas 51–53: se o código de estado não for 200, é lançada uma exceção com as mensagens de erro incluídas na resposta;
- linha 56: recupera o dicionário produzido pelo cálculo do imposto e utiliza-o para atualizar o parâmetro de entrada [contribuinte];
23.3.5. Execução
Para executar o cliente:
- inicie o servidor [server_03] com o SGBD da sua escolha;
- execute o script [main] do cliente;
Os resultados estarão na pasta [data/output]. São os mesmos da versão 5.
23.4. Testes da camada [dao]
Voltemos à arquitetura de aplicações cliente/servidor:

- no código do cliente, garantimos que a camada [dao] [1] fornece a mesma interface que a camada [business] [3]. Por isso, utilizaremos a classe de teste |TestDaoMétier|, que estudámos anteriormente, para testar a camada [business] [3];
A classe de teste será executada no seguinte ambiente:

- A configuração [2] é idêntica à configuração [1], que acabámos de analisar;
A classe de teste [TestHttpClientDao] é a seguinte:
Esta classe é semelhante à que já foi estudada na versão 4 da aplicação.
- linhas 40-41: configurar o ambiente de teste;
- linha 44: recuperamos uma referência à camada [DAO];
- linhas 47-48: executamos os testes;
Para executar os testes, criamos uma |configuração de execução|:

- Criamos uma configuração de execução para um script de consola, não para um UnitTest;
Ao executar esta configuração, obtêm-se os seguintes resultados:
Todos os 11 testes foram aprovados.