Skip to content

23. Ejercicio práctico: versión 6

23.1. Introducción

Ahora volvemos a nuestra aplicación de cálculo de impuestos. Vamos a desarrollar diferentes aplicaciones web a partir de ella.

En la versión 5 de nuestro ejercicio práctico, los datos de la administración tributaria se almacenaban en una base de datos. Esta versión 5 incluía dos aplicaciones distintas, pero con capas en común:

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

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

Image

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

Image

  • el cliente web [1] se comunica con el servidor web [2], el cual a su vez 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], que actúa como cliente de la aplicación web [7];

En la versión final, 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 del impuesto se realiza mediante la capa [métier] del cliente. El script [main2] utilizará este método;

A partir de ahora, desarrollaremos varias aplicaciones cliente/servidor del tipo mencionado anteriormente, cada una de las cuales ilustrará una o varias nuevas tecnologías de desarrollo web.

23.2. El servidor web de cálculo de impuestos

23.2.1. Versión 1

Image

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

Image

  • En [1], se utiliza un URL configurado al que se le 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 monto 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 utilizaron en la |versión 5| de la aplicación de cálculo de impuestos. Las retomamos tal como están;
    • 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 los scripts 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) después de haber enriquecido su contenido. Hace tiempo que se podría haber señalado que no era necesario devolver el resultado [config]. De hecho, [config] es una referencia de diccionario que el código que realiza la llamada comparte con el código al que se llama. Por lo tanto, el código que realiza la llamada ya cuenta con 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)

Sin embargo, mantuve el primer estilo de escritura porque pensé que tal vez 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 proviene de la lista [‘mysql’, ‘pgres’]. [mysql] significa que la base de datos utilizada es administrada por MySQL, mientras que «pgres» significa que la base de datos utilizada es administrada por PostgreSQL;
  • líneas 4-27: se enumeran todas las carpetas que contienen los elementos necesarios para la aplicación web. Estas formarán parte de la ruta de Python de la aplicación (líneas 30-31);
  • líneas 33-40: solo se permitirá el acceso a la aplicación a ciertos usuarios. Aquí hay una lista con un único usuario;
  • líneas 43-46: el script [config_database] es el que genera la configuración de la base de datos utilizada;
  • línea 46: la configuración generada 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 la |versión 5|. Lo volvemos a presentar 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)
                             )

    # la 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| que ya se ha mencionado:


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 que lo invocó
    return {
        "dao": dao,
        "métier": métier
    }
  • línea 6: la capa [dao] se implementa con una base de datos;
  • [ImpotsDaoWithAdminDataInDatabase] se definió |aquí|;
  • [ImpôtsMétier] se definió |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 alcance 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__)


# Inicio URL: /?casado=xx&hijos=yy&salario=zz
@app.route('/', methods=['GET'])
def index():
    # al principio no hay errores
    erreurs = []
    # la solicitud debe tener tres parámetros en 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 invá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)

    # No hay errores, se puede seguir trabajando
    # 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 modo manual
if __name__ == '__main__':
    # iniciamos 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 se debe utilizar;
  • líneas 12-14: con esta información, se puede configurar la aplicación. En particular, se construye la ruta de Python;
  • líneas 16-23: con la nueva ruta de Python, 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: se instancian la aplicación Flask;
  • línea 38: la aplicación Flask solo atiende a 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 verifica la validez de los parámetros del URL;
  • línea 41: se acumularán los mensajes de error en la lista [erreurs];
  • línea 43: tal vez recordemos que los parámetros del objeto URL 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 verifica que haya exactamente tres parámetros (ni menos, ni más);
  • líneas 46-49: se verifica que el parámetro [marié] esté presente en el URL;
  • líneas 50-54: si está presente, se verifica que su valor en minúsculas, sin los espacios al principio ni al final, sea «sí» o «no»;
  • líneas 56-59: se verifica que el parámetro [enfants] se encuentre en el URL;
  • líneas 60-66: si está presente, se verifica 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 convierte a «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 verifica que no haya otros parámetros distintos de [‘marié, ‘enfants’, ‘salaire’] en URL;
  • líneas 85-89: si después de todas estas verificaciones la lista [erreurs] no está vacía, se envía esta lista de errores al cliente en forma de una cadena jSON y el código de estado [400 Bad Request];

Dado que más adelante tendremos muchas ocasiones de enviar una cadena jSON como respuesta al cliente, las pocas líneas necesarias para este envío se han extraído al 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 se debe enviar 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 agrega el encabezado HTTP, que le indica al cliente web que recibirá jSON;
  • línea 22: se envía la respuesta HTTP al código que realiza la llamada. Este se encarga de enviarla al cliente web;

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


from .myutils import set_syspath, json_response

La nueva versión de [myutils] se instala entre los módulos de ámbito de máquina con el comando [pip install .] en un terminal de 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 representa al contribuyente;
  • línea 11: se le solicita a la capa [métier] que calcule el impuesto. Cabe recordar 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 una cadena jSON. Esta es la cadena jSON de un diccionario. Asociado a la clave [result], se coloca allí el diccionario del objeto [taxpayer]. No se pudo asignar el objeto [taxpayer] en sí mismo, 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 ha utilizado SGBD; luego solicita URL http://localhost:5000/ con un navegador):

Image

Image

A continuación se muestra 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. Versión 2

Image

La versión 2 del servidor aísla el procesamiento de 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 configurada: /?casado=xx&hijos=yy&salario=zz
def execute(request: LocalProxy, config: dict) -> tuple:
    # dependientes
    from TaxPayer import TaxPayer

    # al inicio 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 del 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 seguir trabajando
    # 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 de MySQL o PostgreSQL
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 alcance 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__)


# Inicio 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 en modo manual
if __name__ == '__main__':
    # se inicia el servidor
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • líneas 36-41: 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 en la versión 1.

23.2.3. Versión 3

Image

La versión 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 alcance 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 encontró
    return False


# aplicación Flask
app = Flask(__name__)


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


# solo en modo manual
if __name__ == '__main__':
    # se inicia el servidor
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • línea 21: se importa un administrador 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 controlador de autenticación;
  • línea 37: la anotación [@auth.verify_password] identifica la función que se debe ejecutar cuando el administrador 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] identifica una ruta para la cual 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 básico HTTP;

Se debe instalar 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é sucede con la consola de Postman. Usted:

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

El diálogo entre el cliente y el 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] colocamos las credenciales que se encuentran en el script [config]: Image

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

El diálogo entre el cliente y el servidor en la consola de 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 formato cifrado las credenciales del usuario [admin / admin];
  • línea 17: el servidor responde correctamente. Indica errores porque no se enviaron 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 recibió 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 al recibir estos encabezados. Le pide al usuario las credenciales que solicita el servidor. Basta con escribir admin / admin en el campo anterior 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 sección, 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]. Como se mencionó en el párrafo 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, utilizando 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 por lotes descrita a partir de la |versión 1|. La última versión de esta aplicación es la |versión 5|. Recordemos su funcionamiento:

  • los contribuyentes para quienes se calculará el impuesto se recogen en el archivo de texto [taxpayersdata.txt]:
# datos válidos: id, casado, hijos, salario
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
# se crean filas con errores
# no hay suficientes valores
11,12
# valores erróneos
x,x,x,x
  • los resultados se guardan en dos archivos:
  • el archivo de texto [errors.txt] recopila 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 se debe completar con la información de configuración. Este puede estar ya prellenado 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 colocan 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 establece 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 la |versión 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:
        # «contribuyente» es tanto un parámetro de entrada como de salida
        # «contribuyente» se modificará
        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: se configura la aplicación;
  • línea 13: la capa [dao] proporciona la lista de contribuyentes para quienes se debe 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 contamos con una clase |AbstractImpôtsDao| que sabe hacer esto. Se ha utilizado desde la |versión 4|;
    • se comunica con el servidor web [3];

En la |versión 5|, el script principal [main] [1] se comunicaba directamente con la capa [métier] [4]. No queremos modificar este script. Para ello, haremos que la capa [dao] [2] implemente la interfaz de la capa [métier] [4]. De esta manera, 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 la interacción con el sistema de archivos [6];
    • implementa la interfaz [InterfaceImpôtsMétier] para no tener que modificar el script principal [main] de la |versión 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 permite que las excepciones se propaguen
        # parámetros de la llamada a 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 administrado 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 del impuesto;
  • 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. Del lado del cliente, no contamos con estos datos. Este parámetro siempre se mantendrá como [None]. Esta complicación sugiere que la clase [ImpôtsMétier] se escribió incorrectamente desde el principio:
  • 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 manejarán y se propagarán al código que las invocó, en este caso el script [main]. Este sí intercepta todas las excepciones que se devuelven desde la capa [dao];
  • línea 28: el cálculo del impuesto se realiza en el servidor. Por lo tanto, será necesario comunicarse con él. Esto se hace mediante 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 incluirá en el URL en el 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 es necesario ocuparse de 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 datos de identificación para una autenticación de tipo Basic;
  • líneas 44-45: estas dos líneas tienen únicamente un propósito educativo (se comentarán una vez finalizada 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, es útil verificar lo que el servidor nos ha enviado;
  • 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 para 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 de su elección;
  • ejecute el script [main] del cliente;

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

23.4. Pruebas de la capa [dao]

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

  • En el cliente escrito, nos las arreglamos para 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| que ya hemos estudiado 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 analizar;

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, 'tasa': 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, 'tasa': 0.41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        dao.calculate_tax(taxpayer)
        # verificaciones
        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 similar a la que ya analizamos en la versión 4 de la aplicación.

  • líneas 40-41: se configura el entorno de pruebas;
  • línea 44: se obtiene una referencia a 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 realizado con éxito.