Skip to content

23. Ejercicio práctico: version 6

23.1. Introducción

Volvemos ahora a nuestra aplicación de cálculo de impuestos. Vamos a crear diferentes aplicaciones web a partir de ella.

En el version 5 de nuestro ejercicio de aplicación, los datos de la administración tributaria se almacenaban en una base de datos. Este version 5 incluía dos aplicaciones distintas, pero con capas en común:

  • una aplicación que calculaba el impuesto en modo |batch| para los contribuyentes registrados en un archivo de texto;
  • una aplicación que calculaba el impuesto en modo |interactivo| para los contribuyentes cuya información se introducía mediante el teclado;

La version 5 de la aplicación de cálculo del impuesto por lotes (modo batch) tenía la siguiente arquitectura:

Image

Finalmente, la versión web version de esta aplicación tendrá la siguiente arquitectura:

Image

  • el cliente web [1] se dirige al servidor web [2], que se comunica con SGBD y [3];
  • el servidor web [2] conserva las capas [métier], [8] y [dao], [9] de la aplicación inicial;
  • La aplicación original conserva su script principal [4] y sus capas [métier] y [15]. Las capas [métier], [8] y [15] son idénticas;
  • la comunicación cliente/servidor requiere dos capas adicionales:
    • la capa [web] [7] que implementa la aplicación web;
    • la capa [dao] [5], cliente de la aplicación web [7];

En la capa final version, el cálculo del impuesto por lotes podrá realizarse de dos maneras:

  • el cálculo funcional del impuesto se realiza mediante la capa [métier] del servidor. El script [main] utilizará este método;
  • el cálculo específico del impuesto se realiza mediante la capa [métier] del cliente. El script [main2] utilizará este método;

A partir de ahora, vamos a desarrollar varias aplicaciones cliente/servidor del tipo anterior, cada una de las cuales ilustrará una o varias nuevas tecnologías de desarrollo web.

23.2. El servidor web de cálculo del impuesto

23.2.1. Version 1

Image

El script [server_01] es la siguiente aplicación web:

Image

  • En [1], se utiliza un URL configurado en el que se pasan tres valores:
    • [marié] (sí/no) para indicar si el contribuyente está casado;
    • [enfants]: el número de hijos del contribuyente;
    • [salaire]: salario anual del contribuyente;
  • en [2], el servidor web devuelve una cadena jSON que indica el importe del impuesto a pagar con sus diferentes componentes;

La arquitectura de la aplicación es la siguiente:

Image

  • el navegador [1] consulta al servidor [2]. El script [server_01] implementa la capa [web] [2] del servidor;
  • las capas [3-8] son las que ya se utilizan en la |version 5| de la aplicación de cálculo de impuestos. Las retomamos tal cual;
    • la capa [métier] [3] se define |aquí|;
    • la capa [dao] [4] se define |aquí|;

La aplicación web [server_01] se configura mediante tres scripts:

  • [config], que configura toda la aplicación;
  • [config_database], que configura el acceso a la base de datos. Trabajaremos con SGBD, MySQL y PostgreSQL;
  • [config_layers], que configura las capas de la aplicación;

El script [config] es el siguiente:


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

    # paso 1 ------
    # carpeta de este archivo
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # ruta raíz
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    # dependencias absolutas
    absolute_dependencies = [
        # carpetas del proyecto
        # 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",
        # Constantes, tramos
        f"{root_dir}/impots/v05/entities",
        # IndexController
        f"{script_dir}/../controllers",
        # scripts [config_database, config_layers]
        script_dir,
    ]
    # se establece la ruta del sistema
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # paso 2 ------
    # configuración de la aplicación
    # lista de usuarios autorizados a utilizar la aplicación
    config['users'] = [
        {
            "login""admin",
            "password""admin"
        }
    ]

    # paso 3 ------
    # configuración de la base de datos
    import config_database
    config["database"] = config_database.configure(config)

    # paso 4 ------
    # instanciación de las capas de la aplicación
    import config_layers
    config['layers'] = config_layers.configure(config)

    # se aplica la configuración
    return config
  • La función [configure] recibe un diccionario [config] como parámetro (línea 1) y lo devuelve como resultado (línea 54) tras haber ampliado su contenido. Hace tiempo que se podría haber dicho que no era necesario devolver el resultado [config]. De hecho, [config] es una referencia de diccionario que el código llamante comparte con el código llamado. Por lo tanto, el código llamante ya posee esta referencia (línea 1) y es innecesario volver a proporcionársela (línea 54). Por lo tanto, escribir:

config=[module].configure(config) (1)

es redundante. Basta con escribir:


[module].configure(config) (2)

No obstante, he mantenido el tipo (1) de escritura porque pensé que quizá mostraba mejor que el código llamado modificaba el diccionario [config].

  • línea 1: el diccionario [config] recibido por la función [configure] tiene una clave «sgbd» cuyo valor se toma de la lista [‘mysql’, ‘pgres’]. [mysql] significa que la base de datos utilizada es gestionada por MySQL, mientras que «pgres» significa que la base de datos utilizada es gestionada por PostgreSQL;
  • líneas 4-27: se enumeran todas las carpetas que contienen los elementos necesarios para la aplicación web. Formarán parte del Python Path de la aplicación (líneas 30-31);
  • líneas 33-40: solo se permitirá el acceso a la aplicación a determinados usuarios. Aquí tenemos una lista con un único usuario;
  • líneas 43-46: es el script [config_database] el que crea la configuración de la base de datos utilizada;
  • línea 46: la configuración creada por el script [config_database] es un diccionario que se almacena en la configuración general asociado a la clave «database»;
  • líneas 48-51: el script [config_layers] instancia las capas de la aplicación web. Devuelve un diccionario que se almacena en la configuración general asociado a la clave «layers»;

El script [config_database] es el mismo que ya se utilizó en el |version 5|. Lo repetimos a modo de recordatorio:


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

    # cadenas de conexión a las bases de datos utilizadas
    connection_strings = {
        'mysql': "mysql+mysqlconnector://admimpots:mdpimpots@localhost/dbimpots-2019",
        'pgres': "postgresql+psycopg2://admimpots:mdpimpots@localhost/dbimpots-2019"
    }
    # cadena de conexión a la base de datos en uso
    engine = create_engine(connection_strings[config['sgbd']])

    # metadatos
    metadata = MetaData()

    # la tabla de constantes
    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)
                             )

    # tabla de tramos impositivos
    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)
                           )
    # las asignaciones
    from Tranche import Tranche
    mapper(Tranche, tranches_table)

    from Constantes import Constantes
    mapper(Constantes, constantes_table)

    # la fábrica de sesiones
    session_factory = sessionmaker()
    session_factory.configure(bind=engine)

    # una sesión
    session = session_factory()

    # se guarda cierta información y se devuelve en un diccionario
    return {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
            "constantes_table": constantes_table, "session": session}

El script [config_layers] configura las capas del servidor web. Se retoma un |script| ya visto:


def configure(config: dict) -> dict:
    # instanciación de las capas de la aplicación
    
    # dao
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    dao = ImpotsDaoWithAdminDataInDatabase(config)
    
    # negocio
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    # se colocan las instancias de las capas en un diccionario que se devuelve al código llamante
    return {
        "dao": dao,
        "métier": métier
    }
  • línea 6: la capa [dao] se implementa con una base de datos;
  • [ImpotsDaoWithAdminDataInDatabase] se ha definido |aquí|;
  • [ImpôtsMétier] se ha definido |aquí|;

El script principal [server_01] es el siguiente:


# se espera un parámetro mysql o pgres
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()

# se configura la aplicación
import config
config = config.configure({'sgbd': sgbd})

# dependencias
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

# recuperación de datos de la administración tributaria
try:
    # admindata será un dato de ámbito de la aplicación de solo lectura
    admindata = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# aplicación Flask
app = Flask(__name__)


# Home URL : /?casado=xx&hijos=yy&salario=zz
@app.route('/', methods=['GET'])
def index():
    # al principio no hay errores
    erreurs = []
    # la consulta debe tener tres parámetros en el URL
    if len(request.args) != 3:
        erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")

    #: se recupera el estado civil en el 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")

    # se recupera el número de hijos en el 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)

    # se recupera el salario en el 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)

    # ¿Hay parámetros no válidos en el URL?
    for key in request.args.keys():
        if key not in ['marié', 'enfants', 'salaire']:
            erreurs.append(f"paramètre [{key}] invalide")

    # ¿Hay errores?
    if erreurs:
        # se envía una respuesta de error al cliente
        résultats = {"réponse": {"erreurs": erreurs}}
        return json_response(résultats, status.HTTP_400_BAD_REQUEST)

    # sin errores, se puede trabajar
    # cálculo del impuesto
    taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, admindata)
    # se envía la respuesta al cliente
    return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)


# solo manual
if __name__ == '__main__':
    # se inicia el servidor Flask
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • líneas 1-10: se recupera el parámetro que indica qué SGBD utilizar;
  • líneas 12-14: con esta información, se puede configurar la aplicación. En particular, se crea el Python Path;
  • líneas 16-23: con el nuevo Python Path, se importan los elementos que se necesitan;
  • líneas 25-31: se recuperan los datos de la administración tributaria que permiten calcular el impuesto;
  • líneas 33-34: instanciación de la aplicación Flask;
  • línea 38: la aplicación Flask solo sirve el URL [/]. Espera un URL configurado de la siguiente manera [/ ?marié=xx&enfants=yy&salaire=zz] con:
    • xx: sí / no;
    • yy: número de hijos;
    • zz: salario anual;
  • líneas 40-89: se comprueba la validez de los parámetros de URL;
  • línea 41: se acumularán los mensajes de error en la lista [erreurs];
  • línea 43: quizá recordemos que los parámetros del URL configurado se encuentran en [request.args] (ver |aquí|):
    • el objeto [request] es el objeto Flask importado en la línea 20;
    • el objeto [request.args] se comporta como un diccionario;
  • líneas 43-44: se comprueba que hay exactamente tres parámetros (ni más ni menos);
  • líneas 46-49: se comprueba que el parámetro [marié] esté presente en URL;
  • líneas 50-54: si está presente, se comprueba que su valor en minúsculas, sin los espacios al principio y al final, sea «sí» o «no»;
  • líneas 56-59: se comprueba que el parámetro [enfants] se encuentra en URL;
  • líneas 60-66: si está presente, se comprueba que su valor sea un número entero positivo;
  • línea 66: no hay que olvidar que los parámetros de URL y sus valores son cadenas de caracteres. El valor del parámetro [enfants] se transforma en «int»;
  • líneas 68-78: para el parámetro [salaire], se realizan las mismas comprobaciones que para el parámetro [enfants];
  • líneas 81-83: se comprueba que no haya otros parámetros distintos de [‘marié, ‘enfants’, ‘salaire’] en URL;
  • líneas 85-89: si tras todas estas comprobaciones la lista [erreurs] no está vacía, se envía esta lista de errores al cliente en forma de cadena jSON y el código de estado [400 Bad Request];

Dado que, más adelante, tendremos a menudo la oportunidad de enviar una cadena jSON como respuesta al cliente, las pocas líneas necesarias para este envío se han factorizado en el módulo [myutils.py] que ya hemos utilizado:

Image

El script [myutils.py] queda así:


# importaciones
import json
import os
import sys

from flask import make_response


def set_syspath(absolute_dependencies: list):
    # absolute_dependencies: una lista de nombres absolutos de carpetas

    ….


# generación de una respuesta HTTP jSON
def json_response(réponse: dict, status_code: int) -> tuple:
    # cuerpo de la respuesta HTTP
    response = make_response(json.dumps(réponse, ensure_ascii=False))
    # cuerpo de la respuesta HTTP es de jSON
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    # se envía la respuesta HTTP
    return response, status_code
  • línea 16: la función [json_response] espera dos parámetros:
    • [réponse]: el diccionario cuya cadena jSON debe enviarse al cliente web;
    • [status_code]: el código de estado HTTP de la respuesta;
  • línea 18: se establece el cuerpo jSON de la respuesta;
  • línea 20: se añade el encabezado HTTP que indica al cliente web que va a recibir jSON;
  • línea 22: se envía la respuesta HTTP al código llamante. Corresponde a este enviarla al cliente web;

El archivo [__init__.py] evoluciona de la siguiente manera:


from .myutils import set_syspath, json_response

El nuevo version de [myutils] se instala entre los módulos de ámbito de máquina con el comando [pip install .] en un terminal 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
  • línea 1: hay que estar en la carpeta [packages] para escribir esta instrucción;

El código del script [server_01] continúa de la siguiente manera:



    # ¿Hay errores?
    if erreurs:
        # se envía una respuesta de error al cliente
        résultats = {"réponse": {"erreurs": erreurs}}
        return json_response(résultats, status.HTTP_400_BAD_REQUEST)

    # sin errores, se puede trabajar
    # cálculo del impuesto
    taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, admindata)
    # se envía la respuesta al cliente
    return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)

  • línea 10: una vez aquí, los parámetros esperados en el URL están presentes y son correctos;
  • línea 10: se crea el objeto [TaxPayer] que modela al contribuyente;
  • línea 11: se solicita a la capa [métier] que calcule el impuesto. Recordemos que los elementos calculados por la capa [métier] se insertan en el objeto [taxpayer] pasado como parámetro;
  • línea 13: la respuesta se envía al cliente web en forma de cadena jSON. Esta es la cadena jSON de un diccionario. Asociado a la clave [result], se coloca el diccionario del objeto [taxpayer]. No se pudo colocar el propio objeto [taxpayer], ya que este no es serializable en jSON;

Se crean dos configuraciones de ejecución, una para MySQL y otra para PostgreSQL:

Image

A continuación se muestran algunos ejemplos de ejecución (ha iniciado la aplicación [server_01] y se ha utilizado SGBD, y luego solicita URL http://localhost:5000/ con un navegador):

Image

Image

Aquí tienes un ejemplo de ejecución en la consola de 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"]}}
  • línea 1: se solicita un URL incorrecto;
  • línea 10: el servidor responde con el estado 400 BAD REQUEST;

23.2.2. Version 2

Image

La version 2 del servidor aísla el procesamiento de la URL en el módulo [index_controller] [5]:


# importación de dependencias
import re

from flask_api import status
from werkzeug.local import LocalProxy


# URL configurado: /?casado=xx&hijos=yy&salario=zz
def execute(request: LocalProxy, config: dict) -> tuple:
    # dependientes
    from TaxPayer import TaxPayer

    # al principio no hay errores
    erreurs = []
    # la consulta debe tener tres parámetros
    if len(request.args) != 3:
        erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")

    # se recupera el estado civil de 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")

    # se recupera el número de hijos del 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)

    # se recupera el salario de 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)

    # ¿Hay otros parámetros en el URL?
    for key in request.args.keys():
        if not key in ['marié', 'enfants', 'salaire']:
            erreurs.append(f"paramètre [{key}] invalide")

    # ¿Hay errores?
    if erreurs:
        # se envía una respuesta de error al cliente
        résultats = {"réponse": {"erreurs": erreurs}}
        return résultats, status.HTTP_400_BAD_REQUEST

    # sin errores, se puede trabajar
    # cálculo del impuesto
    taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, config["admindata"])
    # se envía la respuesta al cliente
    return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
  • línea 9: la función [execute] recibe dos parámetros:
    • [request]: la solicitud HTTP del cliente;
    • [config]: el diccionario de configuración de la aplicación;

El script [server_02] es el siguiente:


# se espera un parámetro mysql o pgres
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()

# se configura la aplicación
import config
config = config.configure({'sgbd': sgbd})

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

# recuperación de datos de la administración tributaria
try:
    # admindata será un dato de ámbito de la aplicación de solo lectura
    config['admindata'] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# aplicación Flask
app = Flask(__name__)


# Home URL : /?casado=xx&hijo=yy&salario=zz
@app.route('/', methods=['GET'])
def index():
    # se ejecuta la consulta
    résultat, statusCode = index_controller.execute(request, config)
    # se envía la respuesta
    return json_response(résultat, statusCode)


# solo mano
if __name__ == '__main__':
    #: se inicia el servidor
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • líneas 36-41: el procesamiento de la ruta /;
  • línea 39: uso de la función [IndexController.execute];

A partir de ahora utilizaremos esta técnica: cada ruta será procesada por un módulo propio.

Los resultados de la ejecución son los mismos que para version 1.

23.2.3. Version 3

La version 3 introduce el concepto de autenticación.

El script [server_03] queda así:


# se espera un parámetro mysql o pgres
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()

# se configura la aplicación
import config
config = config.configure({'sgbd': sgbd})

# dependencias
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

# recuperación de datos de la administración tributaria
try:
    # config[‘admindata’] será un dato de ámbito de la aplicación de solo lectura
    config["admindata"] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# gestor de autenticación
auth = HTTPBasicAuth()


# método de autenticación
@auth.verify_password
def verify_credentials(login: str, password: str) -> bool:
    # lista de usuarios
    users = config['users']
    # se recorre esta lista
    for user in users:
        if user['login'] == login and user['password'] == password:
            return True
    # no se ha encontrado
    return False


# aplicación Flask
app = Flask(__name__)


# Home URL : /?casado=xx&hijo=yy&salario=zz
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    # se ejecuta la consulta
    résultat, statusCode = index_controller.execute(request, config)
    # se envía la respuesta
    return json_response(résultat, statusCode)


# solo mano
if __name__ == '__main__':
    # se inicia el servidor
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • línea 21: se importa un gestor de autenticación. Existen varios tipos de autenticación en un servidor web. El que utilizamos aquí se llama [HTTP Basic]. Cada tipo de autenticación sigue un diálogo cliente/servidor específico;
  • línea 33: se crea una instancia del gestor de autenticación;
  • línea 37: la anotación [@auth.verify_password] marca la función que se debe ejecutar cuando el gestor de autenticación quiera verificar el nombre de usuario y la contraseña enviados por el cliente según el protocolo [HTTP Basic];
  • línea 55: la anotación [@auth.login_required] marca una ruta para la que el cliente web debe autenticarse. Si el cliente web aún no ha enviado sus credenciales, el servidor web se las solicitará automáticamente según el protocolo HTTP basic;

Debe instalarse el módulo [flask_httpauth]:


(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

Veamos qué ocurre con la consola Postman. Usted:

  • crea una configuración de ejecución;
  • inicie la aplicación web;
  • ejecuta el SGBD que elijas;
  • solicita el URL [/] con Postman;

El diálogo cliente/servidor en la consola de Postman es el siguiente:

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
  • línea 10: el servidor responde que no tenemos autorización para acceder a URL [/];
  • línea 13: nos indica el protocolo de autenticación que debemos utilizar, en este caso el protocolo denominado Autenticación Básica;

Es posible configurar Postman para que envíe las credenciales del usuario según el protocolo Auth Basic:

Image

  • en [6-7] introducimos las credenciales presentes en el script [config]: Image

    config['users'] = [
        {
            "login": "admin",
            "password": "admin"
        }
    ]

El diálogo cliente/servidor en la consola Postman queda así:


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"]}}
  • línea 2: el cliente Postman envía en forma codificada los identificadores del usuario [admin / admin];
  • línea 17: el servidor responde correctamente. Indica errores porque no se han enviado los parámetros [marié, enfants, salaire] (línea 1), pero no indica ningún error de autenticación;

Ahora solicitemos URL / con un navegador (Firefox a continuación):

Image

  • Al igual que con Postman, Firefox ha recibido la respuesta HTTP del servidor con los encabezados HTTP:
1
2
3
4
HTTP/1.0 401 UNAUTHORIZED
WWW-Authenticate: Basic realm="Authentication Required"

Firefox, al igual que otros navegadores, no detiene el diálogo cuando recibe estos encabezados. Solicita al usuario las credenciales que pide el servidor. Basta con escribir admin / admin para recibir la respuesta del servidor:

Image

23.3. El cliente web del servidor de cálculo de impuestos

23.3.1. Introducción

En el párrafo anterior, el cliente web del servidor de cálculo de impuestos era un navegador. En esta parte, el cliente web será un script de consola. La arquitectura queda así:

Image

  • el cliente web está formado por las capas [1-2];
  • el servidor web está formado por las capas [3-9]. Ya se ha mencionado en el apartado anterior;

Por lo tanto, debemos escribir las capas [1-2].

La capa [dao] [2] debe poder comunicarse con el servidor web [3]. Ahora que conocemos el protocolo HTTP, podríamos escribir, por ejemplo con el módulo [pycurl] que ya hemos estudiado, un script que se comunique con el servidor web [3]. Sin embargo, existen módulos especializados en los diálogos cliente/servidor HTTP. Vamos a utilizar uno de ellos, el 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

La estructura de los scripts del cliente web es la siguiente:

Image

El script implementará la aplicación de cálculo de impuestos en modo batch descrita a partir de |version 1|. El último version de esta aplicación es el |version 5|. Recordemos su funcionamiento:

  • los contribuyentes para los que se va a calcular el impuesto se recogen en el archivo 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
  • los resultados se guardan en dos archivos:
  • el archivo de texto [errors.txt] recoge los errores detectados en el archivo de contribuyentes:

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]
  • (continuación)
    • El archivo jSON [résultats.json] recopila los resultados de los cálculos del impuesto de los distintos contribuyentes:

[
  {
    "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. Configuración del cliente web

Image

La configuración se realiza mediante dos scripts:

  • [config], que se encarga de toda la configuración fuera de las capas de la arquitectura;
  • [config_layers], que se encarga de la configuración de las capas de la arquitectura;

El script [config] es el siguiente:


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

    # paso 1 ------

    # carpeta de este archivo
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # ruta raíz
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # dependencias absolutas
    absolute_dependencies = [
        # carpetas del proyecto
        # 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",
        # Constantes, tramos
        f"{root_dir}/impots/v05/entities",
        # ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        # scripts de configuración
        script_dir,
    ]

    # se establece la ruta del sistema
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # paso 2 ------
    # configuración de la aplicación con constantes
    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"
            }
        }
    }
    )

    # paso 3 ------
    # instanciación de las capas
    import config_layers
    config['layers'] = config_layers.configure(config)

    # se aplica la configuración
    return config
  • línea 1: la función [configure] recibe como parámetro el diccionario que debe rellenarse con la información de configuración. Este puede estar ya rellenado o vacío. En este caso, estará vacío;
  • líneas 40-42: los nombres absolutos de los tres archivos de texto gestionados por la capa [dao];
  • líneas 43-50: asociadas a la clave [server], la información que debe conocer la capa [dao] sobre el servidor web con el que debe comunicarse:
    • línea 44: el URL del servicio web;
    • línea 45: la clave [authBasic] es True si el acceso a URL requiere una autenticación de tipo Basic;
    • líneas 46-49: las credenciales del usuario que se autenticará si se solicita la autenticación;
  • líneas 56-57: se instancian las capas, en este caso la única capa [dao], y se introducen las referencias de las capas en [config] asociadas a la clave [layers];

El script [config_layers] es el siguiente:


def configure(config: dict) -> dict:
    # instanciación de las capas de la aplicación

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

    # se aplica la configuración de las capas
    return {
        "dao": dao
    }
  • línea 1: la función [configure] recibe el diccionario que configura la aplicación;
  • líneas 4-6: se instancia la capa [dao]. En la línea 6, se le pasa la configuración de la aplicación, en la que encontrará la información que necesita;
  • líneas 8-11: se devuelve un diccionario en el que se ha incluido la referencia de la capa [dao];

23.3.3. El script principal [main]

El script principal [main] es una variante del de |version 5|:


# se configura la aplicación
import config
config = config.configure({})

# dependencias
from ImpôtsError import ImpôtsError

# código
try:
    # se recupera la capa [dao]
    dao = config["layers"]["dao"]
    # lectura de los datos de los contribuyentes
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # de los contribuyentes?
    if not taxpayers:
        raise ImpôtsError(f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # cálculo del impuesto de los contribuyentes
    for taxpayer in taxpayers:
        # taxpayer es a la vez un parámetro de entrada y de salida
        # se va a modificar el contribuyente
        dao.calculate_tax(taxpayer)
    # escritura de los resultados en un archivo de texto
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError  as erreur:
    # visualización del error
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # finalizado
    print("Travail terminé...")
  • líneas 2-3: la aplicación está configurada;
  • línea 13: la capa [dao] proporciona la lista de contribuyentes para los que hay que calcular el impuesto;
  • línea 21: la capa [dao] calcula el impuesto de cada uno de ellos;
  • línea 23: los resultados se guardan en un archivo jSON;

23.3.4. Implementación de la capa [dao]

Image

Volvamos a la arquitectura cliente/servidor utilizada:

Image

  • en [2, 6], vemos que la capa [dao] tiene dos funciones:
    • accede al sistema de archivos tanto para leer los datos de los contribuyentes como para escribir los resultados de los cálculos del impuesto. Ya disponemos de una clase |AbstractImpôtsDao| que sabe hacer esto. Se ha utilizado desde la |version 4|;
    • se comunica con el servidor web [3];

En la |version 5|, el script principal [main] [1] se comunicaba directamente con la capa [métier] [4]. No queremos cambiar este script. Para ello, haremos que la capa [dao] [2] implemente la interfaz de la capa [métier] [4]. De este modo, el script principal [main] tendrá la impresión de comunicarse directamente con la capa [métier] [4] y podrá ignorar por completo que esta se encuentra en otra máquina.

Una definición de la clase que implementa la capa [dao] [2] podría ser la siguiente:


class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
  • la clase [ImpôtsDaoWithHttpClient]:
    • hereda de la clase [AbstractImpôtsDao], lo que le permitirá gestionar el diálogo con el sistema de archivos [6];
    • implementa la interfaz [InterfaceImpôtsMétier] para no tener que cambiar el script principal [main] de la |version 5|;

El código completo de la clase [ImpôtsDaoWithHttpClient] es el siguiente:


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

    # constructor
    def __init__(self, config: dict):
        # inicialización del padre
        AbstractImpôtsDao.__init__(self, config)
        # almacenamiento de parámetros
        self.__config_server = config["server"]

    # método no utilizado de [AbstractImpôtsDao]
    def get_admindata(self) -> AdminData:
        pass

    # cálculo del impuesto
    def calculate_tax(self: object, taxpayer: TaxPayer, admindata: AdminData = None):
        # se permiten excepciones
        # parámetros de get
        params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
        # ¿Conexión con autenticación Auth Basic?
        if self.__config_server['authBasic']:
            response = requests.get(
                # URL del servidor consultado
                self.__config_server['urlServer'],
                # parámetros del URL
                params=params,
                # autenticación Basic
                auth=(
                    self.__config_server["user"]["login"],
                    self.__config_server["user"]["password"]))
        else:
            # conexión sin autenticación Auth Basic
            response = requests.get(self.__config_server['urlServer'], params=params)
        # verificación
        print(response.text)
        # código de estado de la respuesta HTTP
        status_code = response.status_code
        # se guarda la respuesta jSON en un diccionario
        résultat = response.json()
        # error si el código de estado es distinto de 200 OK
        if status_code != status.HTTP_200_OK:
            # se sabe que los errores se han asociado a la clave [erreurs] de la respuesta
            raise ImpôtsError(87, résultat['réponse']['erreurs'])
        # se sabe que el resultado se ha asociado a la clave [result] de la respuesta
        # se modifica el parámetro de entrada con este resultado
        taxpayer.fromdict(résultat["réponse"]["result"])
  • líneas 21-23: la clase [AbstractImpôtsDao] (línea 12) tiene un método abstracto [get_admindata]. Estamos obligados a implementarlo aunque no lo utilicemos (admindata es gestionado por el servidor, no por el cliente);
  • línea 26: el método [calculate_tax] pertenece a la interfaz [InterfaceImpôtsMétier] (línea 12). Debemos implementarlo;
  • línea 15: el constructor recibe como único parámetro el diccionario de configuración de la aplicación;
  • líneas 16-17: la clase padre [AbstractImpôtsDao] se inicializa pasándole, también aquí, la configuración de la aplicación. En ella encontrará los nombres de los tres archivos de texto que debe gestionar;
  • líneas 18-19: se almacena localmente en la clase la información relativa al servidor web de cálculo de impuestos;
  • línea 26: el método [calculate_tax] recibe como parámetro un objeto de tipo |Taxpayer|. Para respetar la firma del método [InterfaceImpôtsMétier.calculate_tax], también recibe un parámetro [admindata] que se supone que encapsula los datos de la administración tributaria. Por parte del cliente, no disponemos de estos datos. Este parámetro seguirá siendo siempre [None]. Esta contorsión nos lleva a concluir que la clase [ImpôtsMétier] se escribió inicialmente de forma incorrecta:
  • la firma de [calculate_tax] debería haber sido simplemente:

def calculate_tax(self, taxpayer: TaxPayer)

y el parámetro [admindata : AdminData] debería haberse pasado al constructor de la clase;

  • línea 27: el código del método [calculate_tax] no se ha encapsulado en un try / catch / finally. Esto significa que las posibles excepciones no se gestionarán y se propagarán al código llamante, en este caso el script [main]. Este detiene correctamente todas las excepciones que se remiten desde la capa [dao];
  • línea 28: el cálculo del impuesto se realiza en el lado del servidor. Por lo tanto, será necesario comunicarse con él. Esto se hace con el módulo [requests] importado en la línea 2;
  • líneas 31-43: para enviar una solicitud GET al servidor web, se utiliza el método [requests.get]:
    • líneas 33-34: el primer parámetro del método es el URL al que hay que contactar;
    • líneas 35-40: los otros dos parámetros son parámetros con nombre cuyo orden no importa;
    • líneas 35-36: el valor del parámetro denominado [params] debe ser un diccionario que contenga la información que se va a incluir en el URL en formato [/url ?param1=valeur1&param2=valeur2&…];
    • línea 29: el diccionario que contiene los tres parámetros [marié, enfants, salaire] que espera el servidor web. No hay que preocuparse por la codificación (denominada urlencoded) a la que deben someterse estos parámetros. [requests] se encarga de ello;
    • líneas 37-40: el parámetro denominado [auth] es una tupla de dos elementos (login, password). Representa los identificadores de una autenticación de tipo Basic;
  • líneas 44-45: estas dos líneas tienen únicamente un propósito didáctico (se comentarán cuando finalice la depuración):
    • [response] representa la respuesta HTTP del servidor;
    • [response.text] representa el texto del documento encapsulado en esta respuesta. Durante la fase de depuración, resulta útil comprobar lo que nos ha enviado el servidor;
  • línea 47: [response.status_code] es el código de estado HTTP de la respuesta recibida. Nuestro servidor solo envía tres:
    • 200 OK
    • 400 BAD REQUEST
    • 500 INTERNAL SERVER ERROR
  • línea 49: nuestro servidor siempre envía jSON, incluso en caso de error. La función [response.json()] crea un diccionario a partir de la cadena jSON recibida. Recordemos las dos formas posibles de la cadena 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}}}
  • líneas 51-53: si el código de estado no es 200, se lanza una excepción con los mensajes de error encapsulados en la respuesta;
  • línea 56: se recupera el diccionario generado por el cálculo del impuesto y se utiliza para actualizar el parámetro de entrada [taxpayer];

23.3.5. Ejecución

Para ejecutar el cliente:

  • inicie el servidor [server_03] con el SGBD que desee;
  • ejecute el script [main] del cliente;

Los resultados se encontrarán en la carpeta [data/output]. Son los mismos que para el version 5.

23.4. Pruebas de la capa [dao]

Volvamos a la arquitectura de la aplicación cliente/servidor:

  • En el cliente escrito, hemos conseguido que la capa [dao] [1] ofrezca la misma interfaz que la capa [métier] [3]. Por lo tanto, utilizaremos en [4] la clase de pruebas |TestDaoMétier| ya estudiada para probar la capa [métier] [3];

La clase de prueba se ejecutará en el siguiente entorno:

Image

  • la configuración [2] es idéntica a la configuración [1] que acabamos de estudiar;

La clase de prueba [TestHttpClientDao] es la siguiente:


import unittest


class TestHttpClientDao(unittest.TestCase):

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

        # {'casado': 'sí', 'hijos': 2, 'salario': 55555,
        # 'impuesto': 2814, 'recargo': 0, 'descuento': 0, 'reducción': 0, 'tipo': 0,14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        # verificación
        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

        # {'casado': 'sí', 'hijos': 3, 'salario': 200000,
        # 'impuesto': 42842, 'recargo': 17283, 'descuento': 0, 'reducción': 0, 'tipo': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        dao.calculate_tax(taxpayer)
        # comprobaciones
        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__':

    # se configura la aplicación
    import config
    config = config.configure({})

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

    # ejecutamos los métodos de prueba
    print("tests en cours...")
    unittest.main()

Esta clase es análoga a la ya estudiada en la version 4 de la aplicación.

  • líneas 40-41: se configura el entorno de pruebas;
  • línea 44: se recupera una referencia de la capa [dao];
  • líneas 47-48: se ejecutan las pruebas;

Para ejecutar las pruebas, se crea una |configuración de ejecución|:

Image

  • se crea una configuración de ejecución para un script de consola, no para una prueba UnitTest;

Al ejecutar esta configuración, se obtienen los siguientes 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

Las 11 pruebas se han superado.