Skip to content

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:

Image

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

Image

  • 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

Image

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

Image

  • 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:

Image

  • 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:

def configure(config: dict) -> dict:
    import os

    #  step 1 ------
    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))
    #  root path
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    #  absolute dependencies
    absolute_dependencies = [
        #  project files
        #  BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        #  InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        #  AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        #  ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        #  AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  IndexController
        f"{script_dir}/../controllers",
        #  scripts [config_database, config_layers]
        script_dir,
    ]
    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration
    #  list of users authorized to use the application
    config['users'] = [
        {
            "login": "admin",
            "password": "admin"
        }
    ]

    #  step 3 ------
    #  database configuration
    import config_database
    config["database"] = config_database.configure(config)

    #  step 4 ------
    #  instantiation of application layers
    import config_layers
    config['layers'] = config_layers.configure(config)

    #  we return the configuration
    return config
  • 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:

def configure(config: dict) -> dict:
    #  sqlalchemy configuration
    from sqlalchemy import create_engine, Table, Column, Integer, MetaData, Float
    from sqlalchemy.orm import mapper, sessionmaker

    #  connection chains to the databases used
    connection_strings = {
        'mysql': "mysql+mysqlconnector://admimpots:mdpimpots@localhost/dbimpots-2019",
        'pgres': "postgresql+psycopg2://admimpots:mdpimpots@localhost/dbimpots-2019"
    }
    #  connection chain to the database used
    engine = create_engine(connection_strings[config['sgbd']])

    #  metadata
    metadata = MetaData()

    #  the constants table
    constantes_table = Table("tbconstantes", metadata,
                             Column('id', Integer, primary_key=True),
                             Column('plafond_qf_demi_part', Float, nullable=False),
                             Column('plafond_revenus_celibataire_pour_reduction', Float, nullable=False),
                             Column('plafond_revenus_couple_pour_reduction', Float, nullable=False),
                             Column('valeur_reduc_demi_part', Float, nullable=False),
                             Column('plafond_decote_celibataire', Float, nullable=False),
                             Column('plafond_decote_couple', Float, nullable=False),
                             Column('plafond_impot_celibataire_pour_decote', Float, nullable=False),
                             Column('plafond_impot_couple_pour_decote', Float, nullable=False),
                             Column('abattement_dixpourcent_max', Float, nullable=False),
                             Column('abattement_dixpourcent_min', Float, nullable=False)
                             )

    #  tax bracket table
    tranches_table = Table("tbtranches", metadata,
                           Column('id', Integer, primary_key=True),
                           Column('limite', Float, nullable=False),
                           Column('coeffr', Float, nullable=False),
                           Column('coeffn', Float, nullable=False)
                           )
    #  mappings
    from Tranche import Tranche
    mapper(Tranche, tranches_table)

    from Constantes import Constantes
    mapper(Constantes, constantes_table)

    #  the factory session
    session_factory = sessionmaker()
    session_factory.configure(bind=engine)

    #  a session
    session = session_factory()

    #  certain information is recorded and rendered in a dictionary
    return {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
            "constantes_table": constantes_table, "session": session}

O script [config_layers] configura as camadas do servidor web. Reutilizamos um |script| que já vimos anteriormente:

def configure(config: dict) -> dict:
    #  instantiation of application layers

    #  dao
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    dao = ImpotsDaoWithAdminDataInDatabase(config)

    #  business
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    #  put layer instances in a dictionary and return them to the calling code
    return {
        "dao": dao,
        "métier": métier
    }
  • 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:

#  a mysql or pgres parameter is expected
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

#  configure the application
import config
config = config.configure({'sgbd': sgbd})

#  dependencies
from ImpôtsError import ImpôtsError
from TaxPayer import TaxPayer
import re
from flask import request
from myutils import json_response
from flask import Flask
from flask_api import status

#  data recovery from tax authorities
try:
    #  admindata will be read-only application data
    admindata = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

#  flask application
app = Flask(__name__)


#  Home URL : /?married=xx&children=yy&salary=zz
@app.route('/', methods=['GET'])
def index():
    #  initially no errors
    erreurs = []
    #  the query must have three parameters in the URL
    if len(request.args) != 3:
        erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")

    #  retrieve marital status in URL
    marié = request.args.get('marié')
    if marié is None:
        erreurs.append("paramètre [marié] manquant")
    else:
        marié = marié.strip().lower()
        erreur = marié != "oui" and marié != "non"
        if erreur:
            erreurs.append(f"paramétre marié [{marié}] invalide")

    #  retrieve the number of children in the URL
    enfants = request.args.get('enfants')
    if enfants is None:
        erreurs.append("paramètre [enfants] manquant")
    else:
        enfants = enfants.strip()
        match = re.match(r"^\d+", enfants)
        if not match:
            erreurs.append(f"paramétre enfants [{enfants}] invalide")
        else:
            enfants = int(enfants)

    #  the salary is retrieved from the URL
    salaire = request.args.get('salaire')
    if salaire is None:
        erreurs.append("paramètre [salaire] manquant")
    else:
        salaire = salaire.strip()
        match = re.match(r"^\d+", salaire)
        if not match:
            erreurs.append(f"paramétre salaire [{salaire}] invalide")
        else:
            salaire = int(salaire)

    #  invalid parameters in the URL?
    for key in request.args.keys():
        if key not in ['marié', 'enfants', 'salaire']:
            erreurs.append(f"paramètre [{key}] invalide")

    #  mistakes?
    if erreurs:
        #  an error response is sent to the client
        résultats = {"réponse": {"erreurs": erreurs}}
        return json_response(résultats, status.HTTP_400_BAD_REQUEST)

    #  no mistakes, we can work
    #  tAX CALCULATION
    taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, admindata)
    #  we send the response to the customer
    return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)


#  hand only
if __name__ == '__main__':
    #  start the Flask server
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • 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:

Image

O script [myutils.py] fica assim:

#  imports
import json
import os
import sys

from flask import make_response


def set_syspath(absolute_dependencies: list):
    #  absolute_dependencies: a list of absolute folder names

    .


#  response generation HTTP jSON
def json_response(réponse: dict, status_code: int) -> tuple:
    #  response body HTTP
    response = make_response(json.dumps(réponse, ensure_ascii=False))
    #  response body HTTP is jSON
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    #  we send the HTTP response
    return response, status_code
  • 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:

    
    #  mistakes?
    if erreurs:
        #  an error response is sent to the client
        résultats = {"réponse": {"erreurs": erreurs}}
        return json_response(résultats, status.HTTP_400_BAD_REQUEST)

    #  no mistakes, we can work
    #  tAX CALCULATION
    taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, admindata)
    #  we send the response to the client
    return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)
  • 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:

Image

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):

Image

Image

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

Image


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

Image

A versão 2 do servidor isola o processamento de URLs no módulo [index_controller] [5]:

#  import dependencies
import re

from flask_api import status
from werkzeug.local import LocalProxy


#  URL set: /?married=xx&children=yy&salary=zz
def execute(request: LocalProxy, config: dict) -> tuple:
    #  dependencies
    from TaxPayer import TaxPayer

    #  initially no errors
    erreurs = []
    #  the query must have three parameters
    if len(request.args) != 3:
        erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")

    #  we retrieve the marital status of the URL
    marié = request.args.get('marié')
    if marié is None:
        erreurs.append("paramètre [marié] manquant")
    else:
        marié = marié.strip().lower()
        erreur = marié != "oui" and marié != "non"
        if erreur:
            erreurs.append(f"paramétre marié [{marié}] invalide")

    #  we retrieve the number of children in URL
    enfants = request.args.get('enfants')
    if enfants is None:
        erreurs.append("paramètre [enfants] manquant")
    else:
        enfants = enfants.strip()
        match = re.match(r"^\d+", enfants)
        if not match:
            erreurs.append(f"paramétre enfants {enfants} invalide")
        else:
            enfants = int(enfants)

    #  we recover the URL salary
    salaire = request.args.get('salaire')
    if salaire is None:
        erreurs.append("paramètre [salaire] manquant")
    else:
        salaire = salaire.strip()
        match = re.match(r"^\d+", salaire)
        if not match:
            erreurs.append(f"paramétre salaire {salaire} invalide")
        else:
            salaire = int(salaire)

    #  other parameters in the URL?
    for key in request.args.keys():
        if not key in ['marié', 'enfants', 'salaire']:
            erreurs.append(f"paramètre [{key}] invalide")

    #  mistakes?
    if erreurs:
        #  an error response is sent to the client
        résultats = {"réponse": {"erreurs": erreurs}}
        return résultats, status.HTTP_400_BAD_REQUEST

    #  no mistakes, we can work
    #  tAX CALCULATION
    taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, config["admindata"])
    #  we send the response to the customer
    return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
  • 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:

#  a mysql or pgres parameter is expected
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

#  configure the application
import config
config = config.configure({'sgbd': sgbd})

#  dependencies
from ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
import index_controller

#  data recovery from tax authorities
try:
    #  admindata will be read-only application data
    config['admindata'] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

#  flask application
app = Flask(__name__)


#  Home URL : /?married=xx&child=yy&salary=zz
@app.route('/', methods=['GET'])
def index():
    #  execute the query
    résultat, statusCode = index_controller.execute(request, config)
    #  we send the answer
    return json_response(résultat, statusCode)


#  hand only
if __name__ == '__main__':
    #  start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • 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:

#  a mysql or pgres parameter is expected
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

#  configure the application
import config
config = config.configure({'sgbd': sgbd})

#  dependencies
from ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
from flask_httpauth import HTTPBasicAuth
import index_controller

#  data recovery from tax authorities
try:
    #  config['admindata'] will be read-only application data
    config["admindata"] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

#  authentication manager
auth = HTTPBasicAuth()


#  authentication method
@auth.verify_password
def verify_credentials(login: str, password: str) -> bool:
    #  user list
    users = config['users']
    #  browse this list
    for user in users:
        if user['login'] == login and user['password'] == password:
            return True
    #  we didn't find
    return False


#  flask application
app = Flask(__name__)


#  Home URL : /?married=xx&child=yy&salary=zz
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    #  execute the query
    résultat, statusCode = index_controller.execute(request, config)
    #  we send the answer
    return json_response(résultat, statusCode)


#  hand only
if __name__ == '__main__':
    #  start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • 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:

GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: e65e2a28-4fe3-423b-88b3-b3e5a83092b1
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 401 UNAUTHORIZED
Content-Type: text/html; charset=utf-8
Content-Length: 19
WWW-Authenticate: Basic realm="Authentication Required"
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 07:05:37 GMT

Unauthorized Access
  • 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:

Image

  • em [6-7] introduzimos as credenciais presentes no script [config]: Image

    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):

Image

  • Tal como no Postman, o Firefox recebeu a resposta HTTP do servidor com os seguintes cabeçalhos HTTP:
1
2
3
4
HTTP/1.0 401 UNAUTHORIZED
WWW-Authenticate: Basic realm="Authentication Required"

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:

Image

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:

Image

  • 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:

Image

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]:
# données valides : id, marié, enfants, salaire
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# on crée des lignes erronées
# pas assez de valeurs
11,12
# des valeurs erronées
x,x,x,x
  • 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

Image

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:

def configure(config: dict) -> dict:
    import os

    #  step 1 ------

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  root path
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    #  absolute dependencies
    absolute_dependencies = [
        #  project files
        #  BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        #  InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        #  AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        #  ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        #  AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        #  configuration scripts
        script_dir,
    ]

    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration with constants
    config.update({
        "taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
        "resultsFilename": f"{script_dir}/../data/output/résultats.json",
        "errorsFilename": f"{script_dir}/../data/output/errors.txt",
        "server": {
            "urlServer": "http://127.0.0.1:5000/",
            "authBasic": True,
            "user": {
                "login": "admin",
                "password": "admin"
            }
        }
    }
    )

    #  step 3 ------
    #  layer instantiation
    import config_layers
    config['layers'] = config_layers.configure(config)

    #  we return the configuration
    return config
  • 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:

def configure(config: dict) -> dict:
    #  instantiation of applicatuon layers

    #  dao layer
    from ImpôtsDaoWithHttpClient import ImpôtsDaoWithHttpClient
    dao = ImpôtsDaoWithHttpClient(config)

    #  make the layer configuration
    return {
        "dao": dao
    }
  • 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|:

#  configure the application
import config
config = config.configure({})

#  dependencies
from ImpôtsError import ImpôtsError

#  code
try:
    #  we recover the [dao] layer
    dao = config["layers"]["dao"]
    #  reading taxpayer data
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #  taxpayers?
    if not taxpayers:
        raise ImpôtsError(f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #  tax calculation
    for taxpayer in taxpayers:
        #  taxpayer is both an input and output parameter
        #  taxpayer will be modified
        dao.calculate_tax(taxpayer)
    #  writing results to a text file
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError  as erreur:
    #  error display
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  completed
    print("Travail terminé...")
  • 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]

Image

Vamos rever a arquitetura cliente/servidor utilizada:

Image

  • 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:

#  imports
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):

    #  manufacturer
    def __init__(self, config: dict):
        #  parent initialization
        AbstractImpôtsDao.__init__(self, config)
        #  parameter memory
        self.__config_server = config["server"]

    #  unused [AbstractImpôtsDao] method
    def get_admindata(self) -> AdminData:
        pass

    #  tAX CALCULATION
    def calculate_tax(self: object, taxpayer: TaxPayer, admindata: AdminData = None):
        #  we let the exceptions rise
        #  get parameters
        params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
        #  connection with Auth Basic authentication?
        if self.__config_server['authBasic']:
            response = requests.get(
                #  URL of the queried server
                self.__config_server['urlServer'],
                #  URL parameters
                params=params,
                #  basic authentication
                auth=(
                    self.__config_server["user"]["login"],
                    self.__config_server["user"]["password"]))
        else:
            #  connection without Auth Basic authentication
            response = requests.get(self.__config_server['urlServer'], params=params)
        #  check
        print(response.text)
        #  response status code HTTP
        status_code = response.status_code
        #  we put the response jSON in a dictionary
        résultat = response.json()
        #  error if status code other than 200 OK
        if status_code != status.HTTP_200_OK:
            #  we know that the errors have been associated with the [errors] key in the response
            raise ImpôtsError(87, résultat['réponse']['erreurs'])
        #  we know that the result has been associated with the [result] key in the response
        #  modify the input parameter with this result
        taxpayer.fromdict(résultat["réponse"]["result"])
  • 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&param2=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:

Image

  • 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:

Image

  • A configuração [2] é idêntica à configuração [1], que acabámos de analisar;

A classe de teste [TestHttpClientDao] é a seguinte:

import unittest


class TestHttpClientDao(unittest.TestCase):

    def test_1(self) -> None:
        from TaxPayer import TaxPayer

        #  { 'married': 'yes', 'children': 2, 'salary': 55555,
        #  tax': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        #  check
        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)

    

    def test_11(self) -> None:
        from TaxPayer import TaxPayer

        #  { 'married': 'yes', 'children': 3, 'salary': 200000,
        #  tax': 42842, 'surcôte': 17283, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        dao.calculate_tax(taxpayer)
        #  checks
        self.assertAlmostEqual(taxpayer.impôt, 42842, 1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 17283, delta=1)


if __name__ == '__main__':

    #  configure the application
    import config
    config = config.configure({})

    #  dao layer
    dao = config['layers']['dao']

    #  test methods are executed
    print("tests en cours...")
    unittest.main()

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|:

Image

  • 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:

C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-clients/01/tests/TestHttpClientDao.py
tests en cours...
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
....{"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}}}
...{"réponse": {"result": {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}}}
....
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}}}
----------------------------------------------------------------------
Ran 11 tests in 0.130s

OK

Process finished with exit code 0

Todos os 11 testes foram aprovados.