30. Ejercicio práctico: version 12
En este capítulo vamos a escribir una aplicación web que siga la arquitectura MVC (Modelo-Vista-Controlador). La aplicación podrá entregar sus respuestas en tres formatos: jSON, XML, HTML. Hay un salto de complejidad entre lo que vamos a hacer ahora y lo que se ha hecho anteriormente. Vamos a reutilizar la mayoría de los conceptos vistos hasta ahora y vamos a detallar todos los pasos que conducen a la aplicación final.
30.1. Arquitectura MVC
Vamos a implementar el modelo de arquitectura denominado MVC (Modelo – Vista – Controlador) de la siguiente manera:
El procesamiento de una solicitud de un cliente se desarrollará de la siguiente manera:
- 1 - solicitud
Las URL solicitadas tendrán el formato http://máquina:puerto/acción/param1/param2/… El [Contrôleur principal] utilizará un archivo de configuración para «enrutar» la solicitud hacia el controlador adecuado. Para ello, utilizará el campo [action] del URL. El resto del URL [param1/param2/…] está formado por parámetros opcionales que se transmitirán a la acción. La C de MVC es aquí la cadena [Contrôleur principal, Contrôleur / Action]. Si ningún controlador puede procesar la acción solicitada, el servidor web responderá que no se ha encontrado la acción solicitada.
- 2 - Procesamiento
- La acción seleccionada [2a] puede utilizar los parámetros parami que le ha transmitido [Contrôleur principal]. Estos pueden proceder de dos fuentes:
- la ruta [/param1/param2/…] del URL,
- de los parámetros enviados en el cuerpo de la solicitud del cliente;
- en el procesamiento de la solicitud del usuario, la acción puede necesitar la capa [métier] [2b]. Una vez procesada la solicitud del cliente, esta puede generar diversas respuestas. Un ejemplo clásico es:
- una respuesta de error si la solicitud no se ha podido procesar correctamente;
- una respuesta de confirmación en caso contrario;
- [Contrôleur / Action] devolverá su respuesta [2c] al controlador principal, junto con un código de estado. Estos códigos de estado representarán de forma única el estado en el que se encuentra la aplicación. Será un código de éxito o un código de error;
- 3 - respuesta
- dependiendo de si el cliente ha solicitado una respuesta jSON, XML o HTML, el [Contrôleur principal] instanciará [3a] con el tipo de respuesta adecuado y le pedirá que envíe la respuesta al cliente. El [Contrôleur principal] le transmitirá tanto la respuesta como el código de estado proporcionados por el [Contrôleur / Action] que se ha ejecutado;
- si la respuesta deseada es de tipo jSON o XML, la respuesta seleccionada dará formato a la respuesta del [Contrôleur / Action] que se le ha proporcionado y la enviará a [3c]. El cliente capaz de procesar esta respuesta puede ser un script de consola de Python o un script Javascript alojado en una página HTML;
- si la respuesta deseada es de tipo HTML, la respuesta seleccionada elegirá [3b] una de las vistas HTML [Vuei] utilizando el código de estado que se le ha proporcionado. Es la V de MVC. A cada código de estado le corresponde una única vista. Esta vista V mostrará la respuesta del [Contrôleur / Action] que se ha ejecutado. Presenta los datos de esta respuesta con HTML, CSS y Javascript. A estos datos se les denomina el modelo de la vista. Es la M de MVC. El cliente suele ser, en la mayoría de los casos, un navegador;
Ahora, precisemos la relación entre la arquitectura web MVC y la arquitectura por capas. Según la definición que se dé al modelo, estos dos conceptos están relacionados o no. Tomemos una aplicación web MVC de una sola capa:

En el ejemplo anterior, cada uno de los [Contrôleur / Action] integra una parte de las capas [métier] y [dao]. En la capa [web] sí que hay una arquitectura MVC, pero el conjunto de la aplicación no tiene una arquitectura en capas. Aquí solo hay una capa, la capa web, que lo hace todo.
Ahora, consideremos una arquitectura web multicapa:

La capa [web] puede implementarse sin seguir el modelo MVC. En ese caso, se trata efectivamente de una arquitectura multicapa, pero la capa web no implementa el modelo MVC.
Por ejemplo, en el mundo .NET, la capa [web]se puede implementar con ASP.NET MVC y entonces se obtiene una arquitectura en capas con una capa [web] de tipo MVC. Una vez hecho esto, se puede sustituir esta capa ASP.NET MVC por una capa ASP.NET clásica (WebForms) manteniendo el resto (negocio, DAO, controlador) tal cual. De este modo, se obtiene una arquitectura por capas con una capa [web] que ya no es de tipo MVC.
En MVC, dijimos que el modelo M era el de la vista V, c.a.d. El conjunto de datos mostrados por la vista V. Se da otra definición del modelo M de MVC:

Muchos autores consideran que lo que se encuentra a la derecha de la capa [web] forma el modelo M del MVC. Para evitar ambigüedades, se puede hablar:
- del modelo del dominio cuando nos referimos a todo lo que se encuentra a la derecha de la capa [web];
- del modelo de la vista cuando nos referimos a los datos mostrados por una vista V;
En lo que sigue, cuando hablemos de modelo, siempre nos referiremos al modelo de la vista.
30.2. Arquitectura de la aplicación cliente/servidor
La aplicación web tendrá la siguiente arquitectura:
- en [1], el servidor web tendrá dos tipos de clients:
- en [2], un cliente de consola que intercambiará jSON y XML con el servidor;
- en [3], un navegador que recibirá HTML del servidor y lo mostrará;
- el servidor web [1] conserva las capas [métier] y [dao] de las versiones anteriores;
- el cliente web [2] evolucionará para tener en cuenta las nuevas URL del servicio de la aplicación web;
- la aplicación HTML que se muestra en el navegador debe escribirse por completo;
Desarrollaremos la aplicación en varias fases:
- vamos a desarrollar el servidor version jSON. Probaremos las URL del servicio del servidor una tras otra con un cliente Postman. Este método nos permite construir la estructura del servidor web sin preocuparnos por las vistas (=HTML) de la aplicación;
- tras probar el servidor jSON con Postman, lo probaremos con un cliente de consola;
- luego pasaremos a las version y XML del servidor. Hemos visto que el paso de la jSON a la XML fue trivial;
- por último, pasaremos a version HTML del servidor. Construiremos una arquitectura MVC y definiremos las vistas que se mostrarán. La aplicación HTML se probará tanto con el cliente Postman como con un navegador convencional;
30.3. El árbol de directorios del código del servidor

- en [1: el servidor web en su totalidad;
- en [2]: por el momento ignoraremos las carpetas [static, templates, tests_views] que se refieren a version y HTML del servidor. Fuera de esta carpeta encontraremos el script principal [main] y su configuración;
- en [3], los controladores del servidor web. Serán instancias de clases;
![]() | ![]() |
- en [4], la respuesta HTTP del servidor será gestionada por clases;
- en [5], conservamos el archivo de registros de los servidores anteriores;
Cuando construyamos el version HTML del servidor, intervendrán otras carpetas:
![]() | ![]() |
- en [6], los elementos estáticos de la aplicación HTML;
- en [7], las plantillas de la aplicación HTML descompuestas en vistas [9] y en fragmentos de vista [8];
- en [9], las clases que implementan los modelos de las vistas;
30.4. Los URL del servicio de la aplicación
Para crear el servidor web, procederemos de la siguiente manera:
- a partir de las vistas de la aplicación HTML, definiremos las acciones que debe implementar la aplicación web. Aquí utilizaremos las vistas reales, pero podrían ser simplemente vistas en papel;
- a partir de estas acciones, definiremos los URL de servicio de la aplicación HTML;
- vamos a implementar estos servicios con un servidor que sirva páginas. Esto permite definir la estructura del servidor web sin preocuparnos por las páginas que se van a servir. Probaremos estos servicios URL con Postman;
- luego probaremos nuestro servidor jSON con un cliente de consola;
- una vez que el servidor jSON haya sido validado, pasaremos a la escritura de la aplicación HTML;
La primera vista será la de autenticación:

- la acción que conduce a esta primera vista se llamará [init-session] [1];
- Al hacer clic en el botón [Valider] se activará la acción [authentifier-utilisateur] con dos parámetros enviados [2-3];
La vista del cálculo del impuesto:

- en [1], la acción [authentifier-utilisateur] que ha llevado a esta vista;
- en [2], al hacer clic en el botón [Valider] se activa la ejecución de la acción [calculer-impot] con tres parámetros enviados [2-5];
- al hacer clic en el enlace [6] se activa la acción [lister-simulations] sin parámetros;
- al hacer clic en el enlace [7] se activa la acción [fin-session] sin parámetros;
La tercera vista es la de las simulaciones realizadas por el usuario autenticado:

- en [3], la acción [lister-simulations] que ha llevado a esta vista;
- en [2], al hacer clic en el enlace [Supprimer] se activa la acción [supprimer-simulation] con un parámetro, el número de la simulación que se va a eliminar de la lista;
- al hacer clic en el enlace [3] se activa la acción [afficher-calcul-impot] sin parámetros, que vuelve a mostrar la vista del cálculo del impuesto;
- al hacer clic en el enlace [4] se activa la acción [fin-session] sin parámetros;
Con esta información inicial, podemos definir las diferentes acciones de servicio del servidor URL:
Acción | Función | Contexto de ejecución |
/init-session | Sirve para establecer el tipo (json, xml, html) de las respuestas deseadas | Solicitud GET Se puede emitir en cualquier momento |
/autenticar-usuario | Autoriza o no a un usuario a conectarse | Solicitud POST. La solicitud debe tener dos parámetros enviados [user, password] Solo se puede emitir si se conoce el tipo de sesión (json, xml, html) |
/calcular-impuestos | Realiza una simulación del cálculo de impuestos | Consulta POST. La consulta debe tener tres parámetros enviados [marié, enfants, salaire] Solo se puede emitir si se conoce el tipo de sesión (json, xml, html) y el usuario está autenticado |
/lister-simulations | Solicita ver la lista de simulaciones realizadas desde el inicio de la sesión | Solicitud GET. Solo se puede emitir si se conoce el tipo de sesión (json, xml, html) y el usuario está autenticado |
/eliminar-simulación/número | Elimina una simulación de la lista de simulaciones | Consulta GET. Solo se puede emitir si se conoce el tipo de sesión (json, xml, html) y el usuario está autenticado |
/mostrar-cálculo-impuestos | Muestra la página HTML del cálculo del impuesto | Consulta GET. Solo se puede emitir si se conoce el tipo de sesión (json, xml, html) y el usuario está autenticado |
/fin-session | Finaliza la sesión de simulaciones. | Técnicamente, se elimina la sesión web anterior y se crea una nueva Solo se puede emitir si se conoce el tipo de sesión (json, xml, html) y el usuario está autenticado |
Estos diferentes URL de servicio se utilizarán tanto para el servidor HTML como para los servidores jSON o XML. Dos URL se utilizarán únicamente para estos dos últimos servidores: se trata de las URL de la version anterior del cliente/servidor web que retomamos aquí:
Acción | Función | Contexto de ejecución |
/get-admindata | Proporciona los datos fiscales que permiten el cálculo del impuesto | Consulta GET. Solo se utiliza si el tipo de sesión es json o xml. El usuario debe estar autenticado |
/calcular-impuestos | Realiza el cálculo del impuesto de una lista de contribuyentes enviada en jSON | Consulta GET. Solo se utiliza si el tipo de sesión es json o xml. El usuario debe estar autenticado |
Todos los controladores asociados a estas acciones procederán de la misma manera:
- comprobarán sus parámetros. Estos se encuentran en el objeto:
- [request.path] para los parámetros presentes en el URL en forma de [/action/param1/param2/…];
- en el objeto [request.form] para los que se transmiten en [x-www-form-urlencoded] en el cuerpo de la solicitud;
- en el objeto [request.data] para aquellos que se transmiten en jSON en el cuerpo de la solicitud;
- un controlador es similar a una función o método que comprueba la validez de sus parámetros. Sin embargo, en el caso del controlador es un poco más complicado:
- los parámetros esperados pueden estar ausentes;
- los parámetros recuperados por el controlador son cadenas de caracteres. Si el parámetro esperado es un número, el controlador debe verificar que la cadena de caracteres del parámetro sea efectivamente la de un número;
- una vez verificado que los parámetros esperados están presentes y son sintácticamente correctos, hay que comprobar que sean válidos en el contexto de ejecución actual. Este contexto está presente en la sesión. El ejemplo de la autenticación es un ejemplo de contexto de ejecución. Algunas acciones solo deben procesarse una vez que el cliente se ha autenticado. Por lo general, una clave en la sesión indica si esta autenticación se ha producido o no;
- una vez realizadas las comprobaciones anteriores, el controlador secundario puede trabajar. Esta labor de verificación de los parámetros es muy importante. No se puede aceptar que un cliente nos envíe cualquier cosa en cualquier momento de la vida de la aplicación. Debemos controlar totalmente la vida de esta;
- una vez realizado su trabajo, el controlador secundario devuelve un diccionario con las claves [action, état, réponse] al controlador principal que lo ha llamado:
- [action] es la acción que se acaba de ejecutar;
- [état] es un número de tres dígitos que indica el resultado del procesamiento de la acción:
- [x00] indicará que el procesamiento se ha realizado con éxito;
- [x01] indicará un fallo en el procesamiento;
- [réponse] es el diccionario de resultados en formato {‘respuesta’:objeto}. El objeto tendrá estructuras diferentes según la acción procesada;
Ahora vamos a repasar los diferentes controladores o, lo que viene a ser lo mismo, las diferentes acciones que estos controladores procesan y que marcan el ritmo de la aplicación web.
30.5. Configuración del servidor

La configuración de la base de datos [config_database], así como la de las capas del servidor [config_layers], son idénticas a las de las versiones anteriores. El archivo [config] incluye nueva información:
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
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",
# Registrador, SendAdminMail
f"{root_dir}/impots/http-servers/02/utilities",
# scripts [config_database, config_layers]
script_dir,
# controladores
f"{script_dir}/../controllers",
# respuestas HTTP
f"{script_dir}/../responses",
# plantillas de vistas
f"{script_dir}/../models_for_views",
]
# se establece la ruta del sistema
from myutils import set_syspath
set_syspath(absolute_dependencies)
# dependencias del servidor web
# los controladores
from AfficherCalculImpotController import AfficherCalculImpotController
from AuthentifierUtilisateurController import AuthentifierUtilisateurController
from CalculerImpotController import CalculerImpotController
from CalculerImpotsController import CalculerImpotsController
from FinSessionController import FinSessionController
from GetAdminDataController import GetAdminDataController
from InitSessionController import InitSessionController
from ListerSimulationsController import ListerSimulationsController
from MainController import MainController
from SupprimerSimulationController import SupprimerSimulationController
# las respuestas HTTP
from HtmlResponse import HtmlResponse
from JsonResponse import JsonResponse
from XmlResponse import XmlResponse
# las plantillas de las vistas
from ModelForAuthentificationView import ModelForAuthentificationView
from ModelForCalculImpotView import ModelForCalculImpotView
from ModelForErreursView import ModelForErreursView
from ModelForListeSimulationsView import ModelForListeSimulationsView
# paso 2 ------
# configuración de la aplicación
config.update({
# usuarios autorizados a utilizar la aplicación
"users": [
{
"login": "admin",
"password": "admin"
}
],
# archivo de registros
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# config servidor SMTP
"adminMail": {
# servidor SMTP
"smtp-server": "localhost",
# puerto del servidor SMTP
"smtp-port": "25",
# administrador
"from": "guest@localhost.com",
"to": "guest@localhost.com",
# asunto del correo
"subject": "plantage du serveur de calcul d'impôts",
# tls en True si el servidor SMTP requiere autorización, en False en caso contrario
"tls": False
},
# duración de la pausa del hilo en segundos
"sleep_time": 0,
# acciones autorizadas y sus controladores
"controllers": {
# inicialización de una sesión de cálculo
"init-session": InitSessionController(),
# autenticación de un usuario
"authentifier-utilisateur": AuthentifierUtilisateurController(),
# cálculo del impuesto en modo individual
"calculer-impot": CalculerImpotController(),
# cálculo del impuesto en modo por lotes
"calculer-impots": CalculerImpotsController(),
# lista de simulaciones
"lister-simulations": ListerSimulationsController(),
# eliminación de una simulación
"supprimer-simulation": SupprimerSimulationController(),
# fin de la sesión de cálculo
"fin-session": FinSessionController(),
# visualización de la vista de cálculo del impuesto
"afficher-calcul-impot": AfficherCalculImpotController(),
# Obtención de datos de la administración tributaria
"get-admindata": GetAdminDataController(),
# controlador principal
"main-controller": MainController()
},
# los diferentes tipos de respuesta (json, xml, html)
"responses": {
"json": JsonResponse(),
"html": HtmlResponse(),
"xml": XmlResponse()
},
# las vistas HTML y sus plantillas dependen del estado devuelto por el controlador
"views": [
{
# vista de autenticación
"états": [
# /init-session éxito
700,
# /autenticar-usuario fallido
201
],
"view_name": "views/vue-authentification.html",
"model_for_view": ModelForAuthentificationView()
},
{
# vista del cálculo del impuesto
"états": [
# /autenticar-usuario correcto
200,
# /calcular-impuesto correcto
300,
# /calcular-impuesto error
301,
# /mostrar-cálculo-impuesto
800
],
"view_name": "views/vue-calcul-impot.html",
"model_for_view": ModelForCalculImpotView()
},
{
# vista de la lista de simulaciones
"états": [
# /listar-simulaciones
500,
# /eliminar-simulación
600
],
"view_name": "views/vue-liste-simulations.html",
"model_for_view": ModelForListeSimulationsView()
}
],
# vista de errores inesperados
"view-erreurs": {
"view_name": "views/vue-erreurs.html",
"model_for_view": ModelForErreursView()
},
# redireccionamientos
"redirections": [
{
"états": [
400, # /fin-de-sesión-correcta
],
# redirección a
"to": "/init-session/html",
}
],
}
)
# 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 devuelve la configuración
return config
- hasta la línea 41, encontramos elementos habituales;
- líneas 43-66: al llegar a la línea 43, se define el Python Path del servidor. A continuación, se pueden importar las dependencias del proyecto:
- líneas 45-55: la lista de controladores;
- líneas 57-60: la lista de respuestas HTTP;
- líneas 62-66: la lista de plantillas de vista;
- líneas 68-189: la configuración de la aplicación con una serie de constantes;
- líneas 71-98: ya conocemos estas líneas, que aparecen en versiones anteriores;
- líneas 101-122: el diccionario de controladores:
- las claves son los nombres de las acciones;
- los valores son una instancia del controlador que debe gestionar esa acción. Cada controlador solo se instancia en un único ejemplar (singleton). La misma instancia será ejecutada por diferentes subprocesos del servidor. Por lo tanto, habrá que prestar atención a los datos compartidos que cada controlador pudiera querer modificar;
- líneas 125-129: el diccionario de las tres respuestas posibles HTTP:
- las claves son el tipo de respuesta deseado por el cliente (jSON, xml, html);
- los valores son una instancia de la respuesta HTTP. Cada generador de respuesta solo se instancia en un único ejemplar (singleton). El mismo generador será ejecutado por diferentes subprocesos del servidor. Por lo tanto, habrá que prestar atención a los datos compartidos que cada generador pudiera querer modificar;
- líneas 132-186: configuración de las vistas HTML. Por el momento, ignoramos estas líneas;
- líneas 191-202: ya hemos visto estas líneas en versiones anteriores;
30.6. Recorrido de una solicitud del cliente dentro del servidor

Vamos a seguir el recorrido de una solicitud de cliente que llega al servidor hasta la respuesta HTTP enviada a continuación. Sigue el recorrido del servidor MVC.
30.6.1. El script [main]
El script [main] es idéntico en muchos aspectos al de las versiones anteriores. No obstante, lo incluimos íntegramente para partir de una base sólida:
# 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 flask import request, Flask, session, url_for, redirect
from flask_api import status
from SendAdminMail import SendAdminMail
from myutils import json_response
from Logger import Logger
import threading
import time
from random import randint
from ImpôtsError import ImpôtsError
import os
# envío de un correo electrónico al administrador
def send_adminmail(config: dict, message: str):
# se envía un correo electrónico al administrador de la aplicación
config_mail = config["adminMail"]
config_mail["logger"] = config['logger']
SendAdminMail.send(config_mail, message)
# comprobación del archivo de registros
logger = None
erreur = False
message_erreur = None
try:
# registrador
logger = Logger(config["logsFilename"])
except BaseException as exception:
# registro de consola
print(f"L'erreur suivante s'est produite : {exception}")
# se anota el error
erreur = True
message_erreur = f"{exception}"
# se almacena el registrador en el config
config['logger'] = logger
# gestión del error
if erreur:
# correo electrónico al administrador
send_adminmail(config, message_erreur)
# fin de la aplicación
sys.exit(1)
# registro de inicio
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)
# Recuperación de datos de la administración tributaria
erreur = False
try:
# admindata será un dato de ámbito de la aplicación de solo lectura
config["admindata"] = config["layers"]["dao"].get_admindata().asdict()
# registro de éxito
logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
# se registra el error
erreur = True
# registro de error
log = f"L'erreur suivante s'est produite : {ex}"
# consola
print(log)
# archivo de registros
logger.write(f"{log}\n")
# correo al administrador
send_adminmail(config, log)
# el hilo principal ya no necesita el registrador
logger.close()
# si se ha producido un error, se detiene
if erreur:
sys.exit(2)
# aplicación Flask
app = Flask(__name__, template_folder="templates", static_folder="static")
# clave secreta de la sesión
app.secret_key = os.urandom(12).hex()
# el controlador frontal
def front_controller() -> tuple:
# se procesa la solicitud
logger = None
…
@app.route('/', methods=['GET'])
def index() -> tuple:
# redirección a /init-session/html
return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)
# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# autenticar-usuario
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# calcular-impuesto
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# listar-simulaciones
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# eliminar-simulación
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# fin-sesión
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# mostrar-cálculo-impuestos
@app.route('/afficher-calcul-impot', methods=['GET'])
def afficher_calcul_impot() -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# get-admindata
@app.route('/get-admindata/<int:numero>', methods=['GET'])
def get_admindata() -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# solo manual
if __name__ == '__main__':
# se inicia el servidor
app.config.update(ENV="development", DEBUG=True)
app.run(threaded=True)
- líneas 1-92: todas estas líneas ya se han visto y explicado;
- línea 92: el servidor gestionará una sesión. Por lo tanto, necesitamos una clave secreta. Para cada usuario, incluiremos dos datos en la sesión:
- si el usuario se ha autenticado correctamente;
- cada vez que realice un cálculo de impuestos, los resultados de dicho cálculo se colocarán en una lista que llamaremos la lista de simulaciones del usuario. Esta lista se colocará en la sesión;
- líneas 100-151: la lista de URL de servicio del servidor. Las funciones asociadas sirven de filtro: todas las URL que no estén presentes en esta lista serán rechazadas por el servidor Flask con el error [404 NOT FOUND]. Una vez superado este filtrado, la solicitud se transmite sistemáticamente a un «Front Controller» implementado por la función de las líneas 94-98, que presentaremos en breve;
- líneas 100-103: gestión de la ruta [/]. El punto de entrada de la aplicación web será la URL de la línea 107. Así, en la línea 103, redirigimos al cliente hacia esta URL:
- la función [url_for] se importa en la línea 18. Aquí tiene dos parámetros:
- el primer parámetro es el nombre de una de las funciones de enrutamiento, en este caso la de la línea 107. Vemos que esta función espera un parámetro [type_response], que es el tipo (json, xml, html) de respuesta deseado por el cliente;
- el segundo parámetro toma el nombre del parámetro de la línea 107, [type_response], y le asigna un valor. Si hubiera otros parámetros, se repetiría la operación para cada uno de ellos;
- devuelve el URL asociado a la función designada por los dos parámetros que se le han proporcionado. En este caso, dará como resultado el URL de la línea 106, donde el parámetro se sustituye por su valor [/init-session/html];
- la función [redirect] se ha importado en la línea 18. Su función es enviar un encabezado de redireccionamiento HTTP al cliente:
- el primer parámetro es el URL al que debe redirigirse el cliente;
- el segundo parámetro es el código de estado de la respuesta HTTP enviada al cliente. El código [status.HTTP_302_FOUND] corresponde a una redirección HTTP;
La función [front_controller] de las líneas 94-98 realiza los primeros tratamientos de la solicitud del cliente:
# el controlador frontal
def front_controller() -> tuple:
# se procesa la solicitud
logger = None
try:
# registrador
logger = Logger(config["logsFilename"])
# se almacena en una config asociada al hilo
thread_config = {"logger": logger}
thread_name = threading.current_thread().name
config[thread_name] = {"config": thread_config}
# se registra la solicitud
logger.write(f"[ front_controller] requête : {request}\n")
# se interrumpe el hilo si así se ha solicitado
sleep_time = config["sleep_time"]
if sleep_time != 0:
# la pausa es aleatoria para que algunos subprocesos se interrumpan y otros no
aléa = randint(0, 1)
if aléa == 1:
# registro antes de la pausa
logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
# pausa
time.sleep(sleep_time)
# se reenvía la solicitud al controlador principal
main_controller = config['controllers']["main-controller"]
résultat, status_code = main_controller.execute(request, session, config)
# se registra el resultado enviado al cliente
log = f"[front_controller] {résultat}\n"
logger.write(log)
# ¿Se ha producido un error fatal?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# se envía un correo electrónico al administrador de la aplicación
send_adminmail(config, log)
# se determina el tipo deseado para la respuesta
if session.get('typeResponse') is None:
# el tipo de sesión aún no se ha establecido; será jSON
type_response = 'json'
else:
type_response = session['typeResponse']
# se construye la respuesta que se va a enviar
response_builder = config["responses"][type_response]
response, status_code = response_builder \
.build_http_response(request, session, config, status_code, résultat)
# se envía la respuesta
return response, status_code
except BaseException as erreur:
# se trata de un error inesperado: se registra el error si es posible
if logger:
logger.write(f"[ front_controller] {erreur}")
# se prepara la respuesta para el cliente
résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
# se envía una respuesta en jSON
return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
finally:
# se cierra el archivo de registros si se ha abierto
if logger:
logger.close()
- líneas 1-57: conocemos este código. Era, por ejemplo, el código de la función denominada [main] en el script [main] de la anterior version. Solo hay que destacar una cosa, el controlador utilizado en las líneas 25-26:
- línea 25: se recupera de la configuración la instancia del controlador asociada al nombre [main-controller]. Se trata de las siguientes líneas:
# dependencias del servidor web
# los controladores
…
from MainController import MainController
# acciones permitidas y sus controladores
"controllers": {
…,
# controlador principal
"main-controller": MainController()
},
- (continuación)
- línea 10 anterior, se observará que se recupera una instancia de clase;
- línea 26: se solicita al controlador [MainController] que procese la solicitud;
- líneas 30-45: la respuesta devuelta por el controlador [MainController] se envía al cliente. Volveremos sobre estas líneas un poco más adelante;
La función [front_controller] y, a continuación, la clase [MainController] se encargan de realizar el trabajo común a todas las solicitudes:
En el esquema anterior, todavía nos encontramos en la fase 1 del procesamiento de la solicitud. El controlador principal [MainController] continuará con el paso 1.
30.6.2. El controlador principal [MainController]
El controlador principal [MainController] continúa el trabajo iniciado por la función [front_controller]:
Todos los controladores implementan la siguiente interfaz [InterfaceController] [2]:

from abc import ABC, abstractmethod
from werkzeug.local import LocalProxy
class InterfaceController(ABC):
@abstractmethod
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
pass
- La interfaz [InterfaceController] solo define el único método [execute] de la línea 8. Este método recibe tres parámetros:
- [request]: la solicitud del cliente;
- [session]: la sesión del cliente;
- [config]: la configuración de la aplicación;
El método [execute] devuelve una tupla de dos elementos:
- el primero es el diccionario de resultados en forma de {‘acción’: acción, ‘estado’: estado, ‘respuesta’: resultados};
- el segundo es el código de estado HTTP que se debe devolver al cliente;
El controlador principal [MainController] [1] implementa la interfaz [InterfaceController] de la siguiente manera:
# importación de dependencias
from flask_api import status
from werkzeug.local import LocalProxy
# controladores de la aplicación web
from InterfaceController import InterfaceController
class MainController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# se recuperan los elementos de path
params = request.path.split('/')
action = params[1]
# errores
erreur = False
# el tipo de sesión debe conocerse antes de realizar ciertas acciones
type_response = session.get('typeResponse')
if type_response is None and action != "init-session":
# se registra el error
résultat = {"action": action, "état": 101,
"réponse": ["pas de session en cours. Commencer par action [init-session]"]}
erreur = True
# para ciertas acciones es necesario estar autenticado
user = session.get('user')
if not erreur and user is None and action not in ["init-session", "authentifier-utilisateur"]:
# se registra el error
résultat = {"action": action, "état": 101,
"réponse": [f"action [{action}] demandée par utilisateur non authentifié"]}
erreur = True
# ¿hay errores?
if erreur:
# se devuelve un mensaje de error
return résultat, status.HTTP_400_BAD_REQUEST
else:
# se ejecuta el controlador asociado a la acción
controller = config["controllers"][action]
résultat, status_code = controller.execute(request, session, config)
return résultat, status_code
El controlador [MainController] realiza las primeras comprobaciones de la validez de la solicitud.
- Líneas 11-13: el controlador comienza recuperando la acción solicitada por el cliente. Recordemos que los URL de servicio tienen la forma [/action/param1/param2/…] y que este URL se encuentra en [request.path];
- líneas 17-23: la acción [init-session] sirve para inicializar el tipo de respuesta (json, xml, html) deseado por el cliente. Esta información se incluye en la sesión asociada a la clave [typeRéponse]. Por lo tanto, si la acción no es [init-session], la sesión debe contener la clave [typeRéponse]; de lo contrario, la solicitud es errónea;
- líneas 21-22: la estructura del resultado devuelto por cada controlador, en este caso un resultado de error:
- [action]: es el nombre de la acción en curso. Esto permitirá disponer de su nombre cuando se registre el resultado de la solicitud;
- [état]: es un código de estado de tres dígitos:
- [x00] para un resultado satisfactorio;
- [x01] para un error;
- [réponse]: es la respuesta a la solicitud. Su naturaleza es específica para cada solicitud;
- líneas 24-30: la acción [authentifier-utilisateur] sirve para autenticar al usuario. Si tiene éxito, se introduce una clave [user=True] en la sesión del usuario. Algunas URL de servicio solo son accesibles para un usuario autenticado. Esto es lo que se comprueba aquí;
- línea 26: solo las acciones [init-session] y [authentifier-utilisateur] pueden ser realizadas por un usuario que aún no se haya autenticado;
- líneas 28-29: el resultado que se debe enviar en caso de error;
- líneas 32-34: si se ha producido alguno de los dos errores anteriores, se envía la respuesta de error al cliente con el estado HTTP 400 BAD REQUEST;
- líneas 35-39: si no se ha producido ningún error, se pasa el control al controlador encargado de procesar la acción en curso. Su instancia se encuentra en la configuración de la aplicación;
La clase [MainController] continúa el trabajo de la función [front_controller]: entre ambas, reúnen todo lo que se puede factorizar en el procesamiento de las solicitudes, esperando hasta el último momento para pasar la solicitud a un controlador específico. La distribución del código entre la función [front_controller] y la clase [MainController] es totalmente subjetiva. En este caso, he querido conservar lo ya logrado en la anterior version: la función [front_controller] ya existía con el nombre [main]. En la práctica, se podría:
- ponerlo todo en la función [front_controller] y eliminar la clase [MainController];
- ponerlo todo en la clase [MainController] y eliminar la función [front_controller]. Yo me decantaría más bien por esta solución, ya que tiene la ventaja de aligerar el código del script principal [main];
30.7. Procesamiento específico de una acción
Volvamos a la arquitectura MVC de la aplicación:

Todavía nos encontramos en el paso 1 anterior. Si no se ha producido ningún error, comenzará el paso 2. La solicitud se ha enviado al controlador específico de la acción solicitada en la misma. Supongamos que dicha acción es [/init-session], definida por la ruta:
# inicialización de sesión
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
Esta acción está vinculada a un controlador en la configuración [config]:
# acciones permitidas y sus controladores
"controllers": {
# inicialización de una sesión de cálculo
"init-session": InitSessionController(),
…
},
Por lo tanto, el controlador [InitSessionController] (línea 4) toma el control. Su código es el siguiente:
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class InitSessionController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# se recuperan los elementos de path
dummy, action, type_response = request.path.split('/')
# al principio no hay error
erreur = False
# verificación del tipo de respuesta
if type_response not in config['responses'].keys():
erreur = True
résultat = {"action": action, "état": 701,
"réponse": [f"paramètre [type={type_response}] invalide"]}
# si no hay error
if not erreur:
# se establece el tipo de sesión en la sesión de Flask
session['typeResponse'] = type_response
résultat = {"action": action, "état": 700,
"réponse": [f"session démarrée avec le type de réponse {type_response}"]}
return résultat, status.HTTP_200_OK
else:
return résultat, status.HTTP_400_BAD_REQUEST
- línea 6: al igual que los demás controladores, el controlador [InitSessionController] implementa la interfaz [InterfaceController];
- línea 10: el URL es de tipo [/init-session/type_response]. Se recupera la acción [init-session] y el tipo de respuesta deseado;
- línea 15: el tipo de respuesta deseado solo puede ser uno de los que figuran en la configuración de respuestas:
# los diferentes tipos de respuesta (json, xml, html)
"responses": {
"json": JsonResponse(),
"html": HtmlResponse(),
"xml": XmlResponse()
},
- si no es así, se prepara una respuesta de error 701 (línea 17);
- líneas 20-25: caso en el que el tipo de respuesta deseado es válido;
- línea 22: se guarda el tipo de respuesta deseado. De hecho, habrá que recordarlo para las solicitudes siguientes;
- líneas 23-24: se prepara una respuesta de éxito 700;
- línea 25: la respuesta de éxito se devuelve al código llamante;
- línea 27: si se ha producido un error, se devuelve la respuesta de error al código llamante;
30.8. Elaboración de la respuesta HTTP del servidor
Volvamos a la arquitectura MVC de la aplicación:

Acabamos de ver los pasos 1 y 2. Nos hemos encontrado con tres códigos de estado:
- 700: /init-session se ha realizado correctamente;
- 701: /init-session ha fallado;
- 101: solicitud no válida, ya sea porque la sesión no se ha inicializado o porque el usuario no está autenticado;
Veamos cómo se envía la respuesta del servidor al cliente en el paso 3 anterior. Esto ocurre en la función [front_controller] del script [main]:
# el controlador frontal
def front_controller() -> tuple:
# se procesa la solicitud
logger = None
try:
# registrador
logger = Logger(config["logsFilename"])
# se almacena en una config asociada al hilo
thread_config = {"logger": logger}
thread_name = threading.current_thread().name
config[thread_name] = {"config": thread_config}
# se registra la solicitud
logger.write(f"[ front_controller] requête : {request}\n")
# se interrumpe el hilo si así se ha solicitado
sleep_time = config["sleep_time"]
if sleep_time != 0:
# la pausa es aleatoria para que algunos subprocesos se interrumpan y otros no
aléa = randint(0, 1)
if aléa == 1:
# registro antes de la pausa
logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
# pausa
time.sleep(sleep_time)
# se reenvía la solicitud al controlador principal
main_controller = config['controllers']["main-controller"]
résultat, status_code = main_controller.execute(request, session, config)
# se registra el resultado enviado al cliente
log = f"[front_controller] {résultat}\n"
logger.write(log)
# ¿Se ha producido un error fatal?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# se envía un correo electrónico al administrador de la aplicación
send_adminmail(config, log)
# se determina el tipo deseado para la respuesta
if session.get('typeResponse') is None:
# el tipo de sesión aún no se ha establecido; será jSON
type_response = 'json'
else:
type_response = session['typeResponse']
# se construye la respuesta que se va a enviar
response_builder = config["responses"][type_response]
response, status_code = response_builder \
.build_http_response(request, session, config, status_code, résultat)
# se envía la respuesta
return response, status_code
except BaseException as erreur:
# se trata de un error inesperado: se registra el error si es posible
if logger:
logger.write(f"[ front_controller] {erreur}")
# se prepara la respuesta para el cliente
résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
# se envía una respuesta en jSON
return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
finally:
# se cierra el archivo de registros si se ha abierto
if logger:
logger.close()
- Estamos en la línea 26: el controlador principal ha devuelto su respuesta de error;
- líneas 27-29: sea cual sea la respuesta del controlador principal (éxito o fallo), esta respuesta se registra en el archivo de logs;
- líneas 30-33: al igual que en versiones anteriores, si el estado HTTP es [500 INTERNAL SERVER ERROR], se envía un correo electrónico al administrador de la aplicación con el registro del error;
- líneas 34-39: se enviará la respuesta HTTP y el resultado devuelto por el controlador se incluirá en el cuerpo de dicha respuesta. Necesitamos saber en qué formato (json, xml, html) quiere el cliente esta respuesta. Buscamos el tipo de respuesta deseado en la sesión. Si no está ahí, entonces fijamos arbitrariamente este tipo en jSON;
- líneas 40-43: se construye la respuesta HTTP;
En el archivo de configuración, cada tipo de respuesta (json, xml, html) se ha asociado a una instancia de clase:
# los diferentes tipos de respuesta (json, xml, html)
"responses": {
"json": JsonResponse(),
"html": HtmlResponse(),
"xml": XmlResponse()
},
Las clases de respuesta se encuentran en la carpeta [responses] del árbol de directorios del servidor:

Cada clase de respuesta implementa la siguiente interfaz [InterfaceResponse]:
from abc import ABC, abstractmethod
from flask.wrappers import Response
from werkzeug.local import LocalProxy
class InterfaceResponse(ABC):
@abstractmethod
def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
résultat: dict) -> (Response, int):
pass
- líneas 8-11: la interfaz [InterfaceResponse] define un único método [build_http_response] con los siguientes parámetros:
- [request, session, config]: son los parámetros recibidos por el controlador de la acción;
- [résultat, status_code]: son los resultados generados por el controlador de la acción;
Vamos a presentar la respuesta jSON. Es generada por la siguiente clase [JsonResponse]:
import json
from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy
from InterfaceResponse import InterfaceResponse
class JsonResponse(InterfaceResponse):
def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
résultat: dict) -> (Response, int):
# resultados: el diccionario de resultados
# status_code: el código de estado de la respuesta HTTP
# se devuelve la respuesta HTTP
response = make_response(json.dumps(résultat, ensure_ascii=False))
response.headers['Content-Type'] = 'application/json; charset=utf-8'
return response, status_code
Conocemos este código, con el que nos hemos encontrado en numerosas ocasiones. Es el código de la función [json_response] del módulo [myutils].
30.9. Primeras pruebas
En el código analizado, hemos encontrado tres códigos de estado:
- 700: /init-session se ha realizado correctamente;
- 701: /init-session ha fallado;
- 101: solicitud no válida, ya sea porque la sesión no se ha inicializado o porque el usuario no está autenticado;
Vamos a intentar obtenerlos con una sesión jSON.
- iniciamos el servidor web, el SGBD y el servidor de correo;
- iniciamos un cliente Postman;
Prueba 1
En primer lugar, mostramos una solicitud no válida porque la sesión no se ha inicializado:

- [1-2]: la solicitud [POST http://localhost:5000/authentifier-utilisateur] es una ruta válida:
# autenticar-usuario
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
pero solo se acepta si la sesión se ha iniciado previamente con la acción [/init-session].
Ejecutemos la consulta y veamos el resultado enviado por el servidor:

- [1-2]: se ha obtenido una respuesta jSON. Cuando el cliente aún no ha establecido el tipo de respuesta, el servidor utiliza jSON para responder;
- [3-5]: el diccionario jSON de la respuesta;
- [action]: la acción que se ha ejecutado;
- [état]: el código de estado de la respuesta. Un código [x01] indica un error;
- [réponse]: se adapta a cada acción. En este caso, contiene un mensaje de error;
Ahora iniciemos una sesión con un tipo de respuesta incorrecto:

- [1-2] es una ruta correcta:
# iniciar sesión
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
Por lo tanto, entrará en el túnel de procesamiento de solicitudes del servidor MVC. Sin embargo, debería ser rechazada durante este procesamiento porque el tipo de sesión solicitado es incorrecto.
La respuesta es la siguiente:

- en [4], un código de error [x01];
- en [5], la explicación del error;
Ahora, inicialicemos una sesión jSON:

La respuesta es la siguiente:

Ahora, inicialicemos una sesión XML. La respuesta jSON será sustituida por una respuesta XML generada por la siguiente clase [XmlResponse]:
import xmltodict
from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy
from InterfaceResponse import InterfaceResponse
from Logger import Logger
class XmlResponse(InterfaceResponse):
def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
résultat: dict) -> (Response, int):
# resultados: el diccionario de resultados
# status_code: el código de estado de la respuesta HTTP
# resultado: el diccionario que se va a convertir en cadena XML
xml_string = xmltodict.unparse({"root": résultat})
# se devuelve la respuesta HTTP
response = make_response(xml_string)
response.headers['Content-Type'] = 'application/xml; charset=utf-8'
return response, status_code
Se trata de código que conocemos, el de la función [xml_response] del módulo compartido [myutils].
Inicializamos una sesión XML:

El resultado del servidor es entonces el siguiente:

Obtenemos la misma respuesta que en jSON, pero esta vez la respuesta se presenta en XML.
30.10. La acción [authentifier-utilisateur]
La acción [authentifier-utilisateur] permite autenticar a un usuario que desee utilizar la aplicación de cálculo de impuestos. Su ruta se define de la siguiente manera en el script [main]:
# autenticar-usuario
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
El servidor espera dos parámetros enviados:
- [user]: el identificador del usuario;
- [password]: su contraseña;
La lista de usuarios autorizados se define en la configuración [config]:
# usuarios autorizados a utilizar la aplicación
"users": [
{
"login": "admin",
"password": "admin"
}
],
Aquí tenemos una lista con un solo elemento.
La acción [authentifier-utilisateur] es procesada por el controlador [AuthentifierUtilisateurController] siguiente:
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
from Logger import Logger
class AuthentifierUtilisateurController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# se recuperan los elementos de path
dummy, action = request.path.split('/')
# los parámetros de POST
post_params = request.form
# código de estado de la respuesta HTTP
status_code = None
# al principio no hay errores
erreur = False
erreurs = []
# se necesita un POST con dos parámetros
if len(post_params) != 2:
erreur = True
status_code = status.HTTP_400_BAD_REQUEST
erreurs.append("méthode POST requise, paramètre [action] dans l'URL, paramètres postés [user, password]")
if not erreur:
# se recuperan los parámetros del POST
# parámetro [user]
user = post_params.get("user")
if user is None:
erreur = True
erreurs.append("paramètre [user] manquant")
# parámetro [password]
password = post_params.get("password")
if password is None:
erreur = True
erreurs.append("paramètre [password] manquant")
# ¿error?
if erreur:
status_code = status.HTTP_400_BAD_REQUEST
# ¿error?
if not erreur:
# se comprueba la validez del par (usuario, contraseña)
users = config['users']
i = 0
nbusers = len(users)
trouvé = False
while not trouvé and i < nbusers:
trouvé = user == users[i]["login"] and password == users[i]["password"]
i += 1
# ¿Encontrado?
if not trouvé:
# se registra el error
erreur = True
status_code = status.HTTP_401_UNAUTHORIZED
erreurs.append(f"Echec de l'authentification")
else:
# se anota en la sesión que se ha encontrado el usuario
session["user"] = True
# ya está
if not erreur:
# retorno sin error
résultat = {"action": action, "état": 200, "réponse": f"Authentification réussie"}
return résultat, status.HTTP_200_OK
else:
# retorno con error
return {"action": action, "état": 201, "réponse": erreurs}, status_code
- línea 14: se recuperan los parámetros de POST;
- línea 19: la lista de errores encontrados en la solicitud;
- líneas 20-24: se comprueba que efectivamente hay dos parámetros enviados;
- líneas 27-31: se comprueba si existe un parámetro [users];
- líneas 32-36: se comprueba la presencia de un parámetro [password];
- líneas 38-39: si los parámetros enviados son erróneos, se prepara una respuesta HTTP 400 BAD REQUEST;
- líneas 40-58: se comprueba que las credenciales [user, password] correspondan a un usuario autorizado para utilizar la aplicación;
- líneas 51-55: si el usuario (nombre de usuario, contraseña) no está autorizado a utilizar la aplicación, se prepara una respuesta HTTP 401 UNAUTHORIZED;
- líneas 56-58: si está autorizado, se anota con la clave [user] en la sesión que se ha autenticado;
Cabe señalar que si el usuario se autenticó con las credenciales [identifiants1] y no consigue autenticarse con las credenciales [identifiants2], seguirá estando autenticado con las credenciales [identifiants1].
Realicemos pruebas con Postman:
- iniciamos el servidor web, el SGBD y el servidor de correo;
- con el cliente Postman:
- iniciamos una sesión jSON;
- luego nos autenticamos;
Estos son los diferentes casos.
Caso 1: POST sin parámetros enviados

- en [3-5], el POST no tiene cuerpo;
El resultado de la solicitud es el siguiente:

- en [2], se obtuvo una respuesta HTTP 400 BAD REQUEST;
- al introducir [5], se obtuvo un código de error [201];
Caso 2: POST con identificadores erróneos

- en [6], los identificadores son incorrectos;
El servidor envía la siguiente respuesta:

- en [2], la respuesta HTTP 401 UNAUTHORIZED;
- en [5], la respuesta de error;
Caso 2: POST con credenciales correctas

- en [6], las credenciales son correctas;
La respuesta del servidor es la siguiente:
- en [2], una respuesta HTTP 200 OK;
- en [5], la respuesta de éxito;
30.11. La acción [calculer_impot]
La acción [calculer_impot] permite calcular el impuesto de un contribuyente. Su ruta se define de la siguiente manera en el script [main]:
# calcular-impuestos
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
El servidor espera tres parámetros enviados:
- [marié]: sí / no;
- [enfants]: número de hijos del contribuyente;
- [salaire]: salario anual del contribuyente;
El controlador [CalculerImpotController] procesa la acción [calculer_impot]:
import re
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
from TaxPayer import TaxPayer
class CalculerImpotController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# se recuperan los elementos de path
dummy, action = request.path.split('/')
# sin errores al inicio
erreur = False
erreurs = []
# los parámetros de POST
post_params = request.form
# se necesita un POST con tres parámetros
if len(post_params) != 3:
erreur = True
erreurs.append(
"méthode POST requise avec les paramètres postés [marié, enfants, salaire]")
# se analizan los parámetros enviados
if not erreur:
# parámetro emparejado
marié = post_params.get("marié")
if marié is None:
erreurs.append("paramètre [marié] manquant")
else:
# ¿Es válido el parámetro?
marié = marié.lower()
if marié != "oui" and marié != "non":
erreur = True
erreurs.append(f"valeur [{marié}] invalide pour le paramètre [marié (oui/non)]")
# parámetro [enfants]
enfants = post_params.get("enfants")
if enfants is None:
erreur = True
erreurs.append("paramètre [enfants] manquant")
else:
# ¿Es válido el parámetro?
enfants = enfants.strip()
match = re.match(r"\d+", enfants)
if not match:
erreur = True
erreurs.append(f"valeur [{enfants}] invalide pour le paramètre [enfants (entier>=0)]")
# parámetro salario
salaire = post_params.get("salaire")
if salaire is None:
erreur = True
erreurs.append("paramètre [salaire] manquant")
else:
# ¿Es válido el parámetro?
salaire = salaire.strip()
match = re.match(r"\d+", salaire)
if not match:
erreur = True
erreurs.append(f"valeur [{salaire}] invalide pour le paramètre [salaire (entier>=0)]")
# ¿Error?
if erreur:
status_code = status.HTTP_400_BAD_REQUEST
résultat = {"action": action, "état": 301, "réponse": erreurs}
# se devuelve el resultado
return résultat, status_code
# cálculo del impuesto
# se recupera la capa [métier] y el diccionario [adminData]
métier = config["layers"]["métier"]
admin_data = config["admindata"]
# cálculo del impuesto
taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
métier.calculate_tax(taxpayer, admin_data)
# n.º de la simulación
id_simulation = session.get('id_simulation', 0)
id_simulation += 1
session['id_simulation'] = id_simulation
# se introduce el resultado en la sesión en forma de diccionario de un TaxPayer
simulation = taxpayer.fromdict({'id': id_simulation}).asdict()
# se añade el resultado a la lista de simulaciones ya realizadas y se guarda esta en la sesión
simulations = session.get("simulations", [])
simulations.append(simulation)
session["simulations"] = simulations
# resultado
résultat = {"action": action, "état": 300, "réponse": simulation}
status_code = status.HTTP_200_OK
# se muestra el resultado
return résultat, status_code
- línea 13: se recupera el nombre de la acción en curso;
- línea 17: se acumulan los errores en una lista;
- línea 19: se recuperan los parámetros enviados. Estos se envían con el formato [x-www-form-urlencoded] y por eso se recuperan en [request.form]. Si se hubieran enviado como jSON, los habríamos recuperado como [request.data];
- líneas 21-24: se comprueba que efectivamente hay tres parámetros enviados;
- líneas 27-36: comprobación de la presencia y la validez del parámetro enviado [marié];
- líneas 37-48: comprobación de la existencia y la validez del parámetro enviado [enfants];
- líneas 49-60: comprobación de la presencia y la validez del parámetro enviado [salaire];
- líneas 62-66: si se ha producido un error, se envía una respuesta de error 400 BAD REQUEST con un código de estado [301];
- líneas 69-71: si no se ha producido ningún error, se prepara el cálculo del impuesto. Para ello,
- línea 70: se recupera una referencia de la capa [métier];
- línea 71: se recuperan los datos de la administración tributaria en la configuración del servidor;
- líneas 72-74: se calcula el impuesto del contribuyente;
- líneas 75-77: se cuenta el número de cálculos de impuestos realizados por el usuario;
- línea 76: se recupera en la sesión el número del último cálculo realizado. Aquí se denomina [simulation] al resultado de un cálculo;
- línea 77: se incrementa el número de la última simulación;
- línea 78: se vuelve a introducir este número en la sesión;
- líneas 79-84: para realizar un seguimiento de los cálculos realizados por el usuario, vamos a introducir en su sesión la lista de simulaciones que ha realizado;
- línea 80: una simulación será el diccionario de un objeto TaxPayer cuya propiedad [id] tendrá como valor el número de la simulación;
- líneas 82-84: la simulación actual se añade a la lista de simulaciones presente en la sesión;
- líneas 86-87: se prepara una respuesta HTTP de éxito;
- línea 90: se devuelve el resultado;
Hagamos algunas pruebas: se inician el servidor web, el SGBD, el servidor de correo y un cliente Postman.
Caso 1: realizar un cálculo de impuestos cuando la sesión no está inicializada

La respuesta es la siguiente:

Caso 2: realizar un cálculo de impuestos sin estar autenticado
En primer lugar, se inicia una sesión jSON con [/init-session/json]. A continuación, se realiza la misma consulta que anteriormente. La respuesta es entonces la siguiente:

Caso 3: realizar un cálculo de impuestos con parámetros faltantes
Inicializamos una sesión jSON, nos autenticamos y luego realizamos la siguiente consulta:

- en [5], falta el parámetro [marié];
La respuesta es la siguiente:
Caso 4: realizar un cálculo de impuestos con parámetros erróneos


La respuesta del servidor es la siguiente:

Caso 4: realizar un cálculo de impuestos con parámetros correctos

La respuesta del servidor es la siguiente:

30.12. La acción [lister-simulations]
La acción [lister-simulations] permite a un usuario conocer la lista de simulaciones que ha realizado desde el inicio de la sesión. Su ruta se define de la siguiente manera en el script [main]:
# listar-simulaciones
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
El servidor no espera ningún parámetro. La acción [lister-simulations] es procesada por el controlador [ListerSimulationsController] siguiente:
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class ListerSimulationsController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# se recuperan los elementos de path
dummy, action = request.path.split('/')
# se recupera la lista de simulaciones de la sesión
simulations = session.get("simulations", [])
# se muestra el resultado
return {"action": action, "état": 500,
"réponse": simulations}, status.HTTP_200_OK
- línea 13: se toma la lista de simulaciones de la sesión;
- líneas 15-16: se devuelve una respuesta de éxito;
Realicemos la siguiente prueba con Postman:
- iniciamos una sesión jSON;
- nos autenticamos;
- realizamos dos cálculos de impuestos;
- solicitamos la lista de simulaciones;
La solicitud es la siguiente:
- en [3], no hay ningún parámetro;
La respuesta del servidor es la siguiente:

- en [4], la lista de simulaciones del usuario;
30.13. La acción [supprimer-simulation]
La acción [supprimer-simulation] permite a un usuario eliminar una de las simulaciones de su lista de simulaciones. Su ruta se define de la siguiente manera en el script [main]:
# eliminar-simulación
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
El servidor espera un único parámetro: el número de la simulación que se va a eliminar. La acción [supprimer-simulation] es procesada por el controlador [SupprimerSimulationController] siguiente:
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class SupprimerSimulationController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# se recuperan los elementos de path
dummy, action, numéro = request.path.split('/')
# el parámetro [numéro] es un entero positivo o nulo según su ruta
numéro = int(numéro)
# la simulación id=número debe existir en la lista de simulaciones
simulations = session.get("simulations", [])
liste_simulations = list(filter(lambda simulation: simulation['id'] == numéro, simulations))
if not liste_simulations:
msg_erreur = f"la simulation n° [{numéro}] n'existe pas"
# se devuelve el error
return {"action": action, "état": 601, "réponse": [msg_erreur]}, status.HTTP_400_BAD_REQUEST
# eliminación de la simulación id=número
simulation = liste_simulations.pop(0)
simulations.remove(simulation)
# se vuelven a incluir las simulaciones en la sesión
session["simulations"] = simulations
# se muestra el resultado
return {"action": action, "état": 600, "réponse": simulations}, status.HTTP_200_OK
- línea 10: se recuperan los dos elementos de la ruta de la solicitud. Se recuperan como cadenas de caracteres;
- línea 13: el parámetro [numéro] se transforma en un entero. Sabemos que esto es posible gracias a la firma de su ruta,
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
Además, sabemos que es un entero >=0. De hecho, no puede haber un URL ni un [/supprimer-simulation/-4]. El servidor Flask rechaza estos valores;
- línea 15: recuperamos la lista de simulaciones de la sesión;
- línea 16: con la función [filter], se busca la simulación que tenga id==número. Se obtiene un objeto [filter] que se convierte al tipo [list];
- líneas 17-20: si el filtro no ha devuelto ningún resultado, significa que la simulación que se debe eliminar no existe. Se devuelve un mensaje de error que lo indica;
- líneas 21-23: se elimina la simulación devuelta por el filtro;
- línea 25: se vuelve a introducir la nueva lista de simulaciones en la sesión;
- línea 27: se devuelve en la respuesta la nueva lista de simulaciones;
Realizamos una prueba de éxito y una prueba de fallo. Hacemos simulaciones y luego solicitamos la lista de simulaciones:

- las simulaciones tienen aquí los números 2 y 3;
Solicitamos eliminar la simulación con el n.º 3.

La respuesta es la siguiente:
Ahora, repitamos la misma operación (eliminación de la simulación de id=3). La respuesta es entonces la siguiente:


30.14. La acción [fin-session]
La acción [fin-session] permite a un usuario finalizar su sesión de simulaciones. Su ruta se define de la siguiente manera en el script [main]:
# fin de sesión
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
El servidor no espera ningún parámetro. La acción es procesada por el controlador [FinSessionController] siguiente:
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class FinSessionController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# se recuperan los elementos de path
dummy, action = request.path.split('/')
# se eliminan todas las claves de la sesión actual
session.clear()
# se devuelve el resultado
return {"action": action, "état": 400, "réponse": "session réinitialisée"}, status.HTTP_200_OK
- línea 13: se eliminan todas las claves de la sesión. Esto elimina:
- [typeResponse]: el tipo de respuestas HTTP (json, xml, html);
- [id_simulation]: número de la última simulación realizada;
- [simulations]: la lista de simulaciones del usuario;
- [user]: indicador de que el usuario ha sido autenticado;
- se devuelve la respuesta;
Cabe preguntarse cómo se devolverá la respuesta HTTP de la línea 15, ahora que el tipo de respuesta ya no está en la sesión. Para saberlo, hay que volver a la función |front_controller| del script principal [main] y modificarla de la siguiente manera:
…
# on not# se anota el tipo de respuesta deseado si esta información se encuentra en la sesión
type_response1 = session.get('typeResponse', None)
# se reenvía la solicitud al controlador principal
main_controller = config['controllers']["main-controller"]
résultat, status_code = main_controller.execute(request, session, config)
# se registra el resultado enviado al cliente
log = f"[front_controller] {résultat}\n"
logger.write(log)
# ¿Se ha producido un error grave?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# se envía un correo electrónico al administrador de la aplicación
send_adminmail(config, log)
# se determina el tipo deseado para la respuesta
type_response2=session.get('typeResponse')
if type_response2 is None and type_response1 is None:
# el tipo de sesión aún no se ha establecido; será jSON
type_response = 'json'
elif type_response2 is not None:
# se conoce el tipo de respuesta y está en la sesión
type_response = type_response2
else:
type_response=type_response1
# se construye la respuesta que se va a enviar
response_builder = config["responses"][type_response]
response, status_code = response_builder \
.build_http_response(request, session, config, status_code, résultat)
# se envía la respuesta
return response, status_code
- línea 3: se almacena el tipo de respuesta actualmente en la sesión;
- línea 6: se ejecuta la acción. Si se trata de:
- [fin-session], la clave [typeResponse] ya no se encuentra en la sesión;
- [init-session], la clave [typeResponse] de la sesión puede haber cambiado de valor;;
- líneas 14-20: se debe enviar la respuesta HTTP. Necesitamos saber en qué formato:
- líneas 16-18: si el tipo de respuesta no está definido ni por [type_response1] de la línea 3, ni por [type_response2] de la línea 15, entonces el tipo de respuesta no estaba definido ni antes ni después de la acción. En ese caso, se utiliza jSON (línea 18);
- líneas 19-21: si existe [type_response2], el tipo en la sesión después de la acción, entonces es este tipo el que hay que utilizar;
- líneas 22-23: de lo contrario, es [type_response1], el tipo de respuesta antes de la acción (esta es necesariamente [fin-session]), el que hay que utilizar;
30.15. La acción [get-admindata]
Ahora abordamos las dos URL reservadas para los servicios jSON y XML:
Acción | Función | Contexto de ejecución |
/get-admindata | Muestra los datos fiscales necesarios para calcular el impuesto | Consulta GET. Solo se utiliza si el tipo de sesión es json o xml. El usuario debe estar autenticado |
/calcular-impuestos | Realiza el cálculo del impuesto de una lista de contribuyentes registrados en jSON | Consulta GET. Solo se utiliza si el tipo de sesión es json o xml. El usuario debe estar autenticado |
URL [/get-admindata] se define en las rutas del script principal [main] de la siguiente manera:
# get-admindata
@app.route('/get-admindata', methods=['GET'])
def get_admindata() -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
La ruta [/get-admindata] es procesada por el controlador [GetAdminDataController] siguiente:
# importación de dependencias
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class GetAdminDataController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# se recuperan los elementos de path
dummy, action = request.path.split('/')
# solo se aceptan las sesiones json y xml
type_response = session.get('typeResponse')
if type_response != 'json' and type_response != 'xml':
# se devuelve una respuesta de error
return {
"action": action,
"état": 1001,
"réponse": ["cette action n'est possible que pour les sessions json ou xml"]
}, status.HTTP_400_BAD_REQUEST
else:
# se devuelve una respuesta de éxito
return {"action": action, "état": 1000, "réponse": config["adminData"].asdict()}, status.HTTP_200_OK
- líneas 13-21: se comprueba que se está en una sesión json o xml;
- línea 24: se devuelve el diccionario de datos de la administración tributaria que, desde el inicio del servidor, se había colocado en la configuración:
# admindata será un dato de ámbito de aplicación de solo lectura
config["admindata"] = config["layers"]["dao"].get_admindata()
Utilicemos un cliente Postman y solicitemos URL [/get-admindata], tras iniciar una sesión jSON y habernos autenticado:

La respuesta del servidor es la siguiente:

30.16. La acción [calculer-impots]
La acción [calculer-impots] calcula los impuestos de una lista de contribuyentes que se encuentra en el cuerpo de la solicitud en forma de cadena jSON. Ya conocemos esta acción: se llamaba [calculate_tax_in_bulk_mode] en la anterior version.
Su ruta es la siguiente:
# cálculo del impuesto por lotes
@app.route('/calculer-impots', methods=['POST'])
def calculer_impots():
# se ejecuta el controlador asociado a la acción
return front_controller()
Esta acción es procesada por el controlador [CalculerImpotsController] siguiente:
import json
from flask_api import status
from werkzeug.local import LocalProxy
from ImpôtsError import ImpôtsError
from InterfaceController import InterfaceController
from TaxPayer import TaxPayer
class CalculerImpotsController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# se recuperan los elementos de path
dummy, action = request.path.split('/')
# solo se aceptan las sesiones json y xml
type_response = session.get('typeResponse')
if type_response != 'json' and type_response != 'xml':
# se devuelve una respuesta de error
return {
"action": action,
"état": 1501,
"réponse": ["cette action n'est possible que pour les sessions json ou xml"]
}, status.HTTP_400_BAD_REQUEST
# se recupera el cuerpo de post - se espera una lista de diccionarios
msg_erreur = None
list_dict_taxpayers = None
# el cuerpo jSON del POST
request_text = request.data
try:
# que se transforma en una lista de diccionarios
list_dict_taxpayers = json.loads(request_text)
except BaseException as erreur:
# se observa el error
msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
# ¿tenemos una lista no vacía?
if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
# se observa el error
msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
# ¿Tenemos una lista de diccionarios?
if not msg_erreur:
erreur = False
i = 0
while not erreur and i < len(list_dict_taxpayers):
erreur = not isinstance(list_dict_taxpayers[i], dict)
i += 1
# ¿error?
if erreur:
msg_erreur = "le corps du POST doit être une liste de dictionnaires"
# ¿Error?
if msg_erreur:
# se envía una respuesta de error al cliente
résultats = {"action": action, "état": 1501, "réponse": [msg_erreur]}
return résultats, status.HTTP_400_BAD_REQUEST
# se comprueban los TaxPayers uno por uno
# al principio no hay errores
list_erreurs = []
for dict_taxpayer in list_dict_taxpayers:
# se crea un TaxPayer a partir de dict_taxpayer
msg_erreur = None
try:
# la siguiente operación eliminará los casos en los que los parámetros no sean
# propiedades de la clase TaxPayer, así como los casos en los que sus valores
# son incorrectos
TaxPayer().fromdict(dict_taxpayer)
except BaseException as erreur:
msg_erreur = f"{erreur}"
# ciertas claves deben estar presentes en el diccionario
if not msg_erreur:
# las claves [marié, enfants, salaire] deben estar presentes en el diccionario
keys = dict_taxpayer.keys()
if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
# ¿Hay errores?
if msg_erreur:
# se observa el error en el propio TaxPayer
dict_taxpayer['erreur'] = msg_erreur
# se añade el TaxPayer a la lista de errores
list_erreurs.append(dict_taxpayer)
# se han procesado todos los contribuyentes; ¿hay algún error?
if list_erreurs:
# se envía una respuesta de error al cliente
résultats = {"action": action, "état": 1501, "réponse": list_erreurs}
return résultats, status.HTTP_400_BAD_REQUEST
# sin errores, se puede trabajar
# Recuperación de datos de la administración tributaria
admindata = config["admindata"]
métier = config["layers"]["métier"]
try:
# se procesan los TaxPayer uno por uno
list_taxpayers = []
for dict_taxpayer in list_dict_taxpayers:
# cálculo del impuesto
taxpayer = TaxPayer().fromdict(
{'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
'salaire': dict_taxpayer['salaire']})
métier.calculate_tax(taxpayer, admindata)
# se almacena el resultado como un diccionario
list_taxpayers.append(taxpayer.asdict())
# se añade list_taxpayers a las simulaciones actuales, asignando un número a cada simulación
simulations = session.get("simulations", [])
id_simulation = session.get("id_simulation", 0)
for simulation in list_taxpayers:
# se asigna un número a cada simulación
id_simulation += 1
simulation['id'] = id_simulation
# se añade a la lista actual de simulaciones
simulations.append(simulation)
# se reinicia la sesión
session["simulations"] = simulations
session["id_simulation"] = id_simulation
# se envía la respuesta al cliente
return {"action": action, "état": 1500, "réponse": list_taxpayers}, status.HTTP_200_OK
except ImpôtsError as erreur:
# se envía una respuesta de error al cliente
return {"action": action, "état": 1501, "réponse": [f"{erreur}"]}, status.HTTP_500_INTERNAL_SERVER_ERROR
- líneas 16-24: se comprueba que se está efectivamente en una sesión json o xml
- líneas 26-120: este código nos resulta familiar en general. Es el de la función |index_controller| de la version 10 de la aplicación, que se ha adaptado para cumplir con las especificaciones de la interfaz [InterfaceController] implementada;
- líneas 104-115: el código añadido para tener en cuenta el nuevo entorno de este controlador. Acabamos de realizar cálculos de impuestos. Debemos guardar los resultados en la lista de simulaciones mantenidas en la sesión;
- línea 105: se recupera la lista de simulaciones de la sesión;
- línea 106: se recupera el n.º de la última simulación realizada;
- líneas 107-112: se recorre la lista de diccionarios de los resultados del cálculo del impuesto; a cada uno de ellos se le asigna un número de simulación [id] y cada diccionario se añade a la lista de simulaciones;
- líneas 113-115: la nueva lista de simulaciones, así como el número de la última simulación realizada, se vuelven a poner en sesión;
Realizamos la siguiente prueba de Postman, tras haber iniciado una sesión jSON y habernos autenticado:


La respuesta del servidor es la siguiente:

Si ahora solicitamos la lista de simulaciones:
Observaremos que en la lista de resultados de [/calcul-impots], los contribuyentes no tienen el atributo [id], mientras que en la lista de simulaciones, cada simulación tiene un número que la identifica.




