25. Ejercicio práctico: version 8
25.1. Introducción
Vamos a escribir una nueva aplicación cliente/servidor. La novedad del servidor es que gestionará una sesión. En lugar de colocar los datos de la administración tributaria en un objeto de ámbito [application], los colocaremos en un objeto de ámbito [session]. Al hacerlo, se reduce el rendimiento del código. Cuando un objeto puede ser compartido en modo de solo lectura por todos los usuarios, es preferible convertirlo en un objeto de ámbito [application] en lugar de uno de ámbito [session]. Se gana, como mínimo, en ancho de banda, ya que así se reduce el tamaño de la cookie de sesión. Pero queremos mostrar una aplicación cliente/servidor en la que el cliente y el servidor intercambian una cookie de sesión.
La arquitectura de la aplicación no cambia:

25.2. El servidor web
El árbol de scripts del servidor es el siguiente:

La carpeta [http-servers/03] se obtiene inicialmente copiando la carpeta [http-servers/02]. A continuación, se realizan modificaciones.
25.2.1. La configuración
Es la misma que en el |version anterior| con algunas modificaciones en el script [config]:
# dépendances absolues
absolute_dependencies = [
# dossiers du projet
# 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, Tranches
f"{root_dir}/impots/v05/entities",
# index_controller
f"{script_dir}/../controllers",
# scripts [config_database, config_layers]
script_dir,
# Logger, SendAdminMail
f"{root_dir}/impots/http-servers/02/utilities",
]
- línea 17: vamos a reescribir un controlador para la función [index] que procesa el URL / ;
- línea 21: utilizamos las utilidades del |version anterior| ;
25.2.2. El script principal [main]
El nuevo script [main] introduce algunas modificaciones en el script principal [main] del anterior version:
# l'application Flask peut démarrer
app = Flask(__name__)
# clé secrète de la session
app.secret_key = os.urandom(12).hex()
- línea 4: se crea una clave secreta para la aplicación. Sabemos que esta es necesaria para gestionar las sesiones;
A continuación, ya no se solicitan los datos fiscales en el código de [main]. Se eliminan las siguientes líneas:
# recuperación de datos de la administración tributaria
erreur = False
try:
# admindata será un dato de ámbito de aplicación de solo lectura
config["admindata"] = config["layers"]["dao"].get_admindata()
# 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 errores
log = f"L'erreur suivante s'est produite : {ex}"
# consola
print(log)
# archivo de registros
logger.write(f"{log}\n")
# correo electrónico al administrador
send_adminmail(config, log)
Por otra parte, el controlador [index_controller] admite un parámetro adicional, la sesión de Flask:
from flask import request, Flask, session
….
# se ejecuta la consulta mediante un controlador
résultat, status_code = index_controller.execute(request, session, config)
25.2.3. El controlador [index_controller]
El controlador [index_controller] ahora gestiona una sesión:
# importación de dependencias
import os
import re
import threading
from flask_api import status
from werkzeug.local import LocalProxy
# URL configurada: /?casado=xx&hijos=yy&salario=zz
from AdminData import AdminData
from ImpôtsError import ImpôtsError
def execute(request: LocalProxy, session: LocalProxy, config: dict) -> tuple:
# dependencias
from TaxPayer import TaxPayer
# al principio sin errores
erreurs = []
…
# ¿Hay errores?
if erreurs:
# se devuelve un mensaje de error al cliente
return {"réponse": {"erreurs": erreurs}}, status.HTTP_400_BAD_REQUEST
# sin errores, se puede trabajar
# se recupera el config asociado al hilo
thread_name = threading.current_thread().name
logger = config[thread_name]["config"]["logger"]
# se ejecuta la consulta
réponse = None
try:
# el caso más sencillo: admindata ya está en sesión
if session.get('client_id') is not None:
# se recupera la información de la sesión
client_id = session.get('client_id')
admindata = AdminData().fromdict(session.get('admindata'))
# registro
logger.write(f"[index_controller] client [{client_id}], données fiscales prises en session\n")
else:
# recuperación de los datos de la administración tributaria
admindata = config["layers"]["dao"].get_admindata()
# inicio de sesión de admindata
session['admindata'] = admindata.asdict()
# se asigna un número al cliente y se introduce dicho número en la sesión
# esto nos permitirá realizar un seguimiento en los registros del servidor
client_id = os.urandom(12).hex()
session['client_id'] = client_id
# registro
logger.write(f"[index_controller] client [{client_id}], données fiscales prises dans la couche dao\n")
# 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 {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
except (BaseException, ImpôtsError) as erreur:
# se envía la respuesta al cliente
return {"réponse": {"erreurs": [f"{erreur}"]}}, status.HTTP_500_INTERNAL_SERVER_ERROR
- línea 14: el controlador recibe la sesión actual del cliente web;
- líneas 35-38: si el cliente tiene una sesión, esta contiene dos claves:
- [client_id]: un número de cliente (línea 37);
- [admindata]: los datos de la administración tributaria en forma de diccionario (línea 38);
- línea 35: se comprueba si la sesión tiene alguna de las dos claves esperadas;
- líneas 42-51: caso en el que la sesión del cliente aún no se ha inicializado;
- línea 43: se recuperan los datos de la administración fiscal de la capa [dao];
- línea 45: estos datos se introducen en la sesión en forma de diccionario;
- línea 48: se asigna un número aleatorio al cliente. Este número será diferente para cada cliente;
- línea 49: este número se introduce en la sesión;
- línea 51: se registra que los datos de la administración fiscal se han obtenido a través de la capa [dao]. Los accesos a la capa [dao] suelen ser costosos. Por eso hay que limitarlos. La idea aquí es obtener una vez los datos fiscales de la capa [dao], guardarlos en la sesión y recuperarlos de allí en las consultas posteriores del mismo cliente. Recordamos que esta no es la mejor solución. Dado que los datos fiscales de la administración son los mismos para todos los clients, su lugar está en un objeto de ámbito de aplicación;
- líneas 35-40: caso en el que la sesión del cliente se ha inicializado durante una solicitud anterior;
- línea 37: se recupera el n.º de cliente de la sesión;
- línea 38: se recuperan los datos fiscales de la administración en la sesión;
- línea 40: se registra que el cliente ha obtenido los datos fiscales de la administración en la sesión;
25.3. El cliente web

25.3.1. La capa [dao]
25.3.1.1. La clase [ImpôtsDaoWithHttpSession]
La capa [dao] está implementada por la siguiente clase [ImpôtsDaoWithHttpSession]:
# importaciones
import requests
from flask_api import status
from myutils import decode_flask_session
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ôtsDaoWithHttpSession(AbstractImpôtsDao, InterfaceImpôtsMétier):
# constructor
def __init__(self, config: dict):
# inicialización del elemento principal
AbstractImpôtsDao.__init__(self, config)
# almacenamiento de elementos de la configuración
# config general
self.__config = config
# servidor
self.__config_server = config["server"]
# modo debug
self.__debug = config["debug"]
# registrador
self.__logger = None
# cookies
self.__cookies = None
# método no utilizado
def get_admindata(self) -> AdminData:
pass
# cálculo del impuesto
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
# se permite que se propaguen las excepciones
# parámetros de get
params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
# ¿Conexión con autenticación Auth Basic?
if self.__config_server['authBasic']:
response = requests.get(
# URL del servidor consultado
self.__config_server['urlServer'],
# parámetros del URL
params=params,
# autenticación Basic
auth=(
self.__config_server["user"]["login"],
self.__config_server["user"]["password"]),
cookies=self.__cookies)
else:
# conexión sin autenticación Auth Basic
response = requests.get(self.__config_server['urlServer'], params=params, cookies=self.__cookies)
# se recuperan las cookies de la respuesta, si las hay
if response.cookies:
self.__cookies = response.cookies
# se recupera la cookie de sesión
session_cookie = response.cookies.get('session')
# se descodifica para iniciar sesión
if session_cookie:
# registrador
if not self.__logger:
self.__logger = self.__config['logger']
# se inicia sesión
self.__logger.write(f"cookie de session={decode_flask_session(session_cookie)}\n")
# ¿modo debug?
if self.__debug:
# registrador
if not self.__logger:
self.__logger = self.__config['logger']
# se está registrando
self.__logger.write(f"{response.text}\n")
# 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ínea 30: la capa [dao] gestionará un diccionario de cookies;
- línea 58: la propiedad [response.cookies] es un diccionario que recopila las cookies enviadas por el servidor en los encabezados HTTP [Set-Cookie];
- línea 59: estas cookies se almacenan en la capa [dao]. Se reenviarán al servidor en las solicitudes posteriores del mismo cliente;
- líneas 60-68: aunque no es imprescindible, se recupera la cookie de sesión. En el diccionario de cookies enviadas por el servidor, la cookie de sesión está asociada a la clave [session];
- líneas 62-68: se descodifica la cookie de sesión para iniciar sesión;
- línea 68: volveremos más adelante sobre la función [decode_flask_session] que descodifica la cookie de sesión;
- líneas 52 y 57: en cada solicitud del mismo cliente, se le devuelven las cookies enviadas por el servidor. Así es como se mantiene la sesión de Flask entre el cliente y el servidor;
Ahora hay que recordar que la capa [dao] se ejecutará simultáneamente en varios subprocesos. Por lo tanto, hay que revisar todas las propiedades de la instancia de clase y comprobar si el acceso simultáneo a estas propiedades plantea algún problema. Aquí hemos añadido la propiedad [self.__cookies], en la línea 30. Esta propiedad se modifica en la línea 59. Por lo tanto, tenemos un acceso de escritura a un dato compartido por todos los subprocesos. Sin embargo, este acceso plantea un problema: cada subproceso que representa a un cliente determinado tiene su propia cookie de sesión. De hecho, en ella hay un número de cliente (=hilo) único para cada cliente. Si no hacemos nada, el hilo T2 puede sobrescribir las cookies del hilo T1.
Ya hemos visto un método para gestionar este problema: se pueden crear, en el archivo [config] pasado como parámetro al constructor (línea 17), claves diferentes para cada subproceso. Por ejemplo, se puede utilizar como clave el nombre del subproceso:
- en la línea 59, se podría escribir:
config[thread_name][‘cookies’]=cookies
- en la línea 52, se podría escribir entonces:
cookies=config[thread_name][‘cookies’]
Aquí vamos a utilizar una técnica diferente: cada subproceso (=cliente) tendrá su propia capa [dao]. De este modo, la línea 59 ya no plantea ningún problema, ya que las cookies utilizadas son las de un único cliente.
Para ello, vamos a crear una nueva clase [ImpôtsDaoWithHttpSessionFactory].
25.3.1.2. La función de decodificación de la sesión Flask
La función [decode_flask_session] se define en el script [myutils]:

Ya hemos estudiado el script |myutils|. Este script es un módulo de ámbito de máquina que los diferentes scripts de este curso pueden importar con la instrucción:
En él se define la función [decode_flask_session] de la siguiente manera:
def decode_flask_session(cookie: str) -> str:
# fuente: https://www.kirsle.net/wizards/flask-session.cgi
compressed = False
payload = cookie
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data.decode("utf-8")
- línea 2: URL, donde encontré esta función;
- línea 1: el parámetro [cookie] es la cadena de caracteres asociada a la clave [session] en el diccionario de cookies recibidas por un cliente web;
- líneas 3-16: no voy a comentar este código, ya que no lo domino;
Añadimos una nueva importación en el archivo [__init__.py]:
from .myutils import set_syspath, json_response, decode_flask_session
La nueva version de [myutils] se instala entre los módulos de ámbito de máquina con el comando [pip install .] en un terminal Pycharm:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install .
Processing c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\packages
Using legacy setup.py install for myutils, since package 'wheel' is not installed.
Installing collected packages: myutils
Attempting uninstall: myutils
Found existing installation: myutils 0.1
Uninstalling myutils-0.1:
Successfully uninstalled myutils-0.1
Running setup.py install for myutils ... done
Successfully installed myutils-0.1
- línea 1: hay que estar en la carpeta [packages] para escribir esta instrucción;
25.3.1.3. La clase [ImpôtsDaoWithHttpSessionFactory]
La clase [ImpôtsDaoWithHttpSessionFactory] es la siguiente:
from ImpôtsDaoWithHttpSession import ImpôtsDaoWithHttpSession
class ImpôtsDaoWithHttpSessionFactory:
def __init__(self, config: dict):
# se almacena el parámetro
self.__config = config
def new_instance(self):
# se devuelve una instancia de la capa [dao]
return ImpôtsDaoWithHttpSession(self.__config)
- La clase [ImpôtsDaoWithHttpSessionFactory] permite crear una nueva implementación de la capa [dao] con el método [new_instance] de las líneas 10-12;
25.3.2. La configuración
El script [config_layers], que configura las capas del cliente web, se modifica de la siguiente manera:
def configure(config: dict) -> dict:
# instanciación de las capas de la aplicación
# capa dao
from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
dao_factory = ImpôtsDaoWithHttpSessionFactory(config)
# se establece la configuración de las capas
return {
"dao_factory": dao_factory
}
- líneas 5-6: en lugar de instanciar una única capa [dao] como se hacía anteriormente, se instancia una «fábrica» de esta capa (fábrica = fábrica de producción de objetos, en este caso la capa [dao]);
- líneas 9-11: se devuelve la configuración de las capas;
25.3.3. El script principal del cliente
El script [main] evoluciona de la siguiente manera con respecto al anterior version:
# se configura la aplicación
import config
config = config.configure({})
# dependencias
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger
# ejecución de la capa [dao] en un subproceso
# taxpayers es una lista de contribuyentes
def thread_function(thread_dao, logger, taxpayers: list):
…
# lista de subprocesos del cliente
threads = []
logger = None
# código
try:
…
l_taxpayers = len(taxpayers)
while i < len(taxpayers):
…
# cada subproceso debe tener su propia capa [dao] para gestionar correctamente su cookie de sesión
thread_dao = dao_factory.new_instance()
# se crea el hilo
thread = threading.Thread(target=thread_function, args=(thread_dao, logger, thread_taxpayers))
# se añade a la lista de subprocesos del script principal
threads.append(thread)
# se inicia el subproceso (esta operación es asíncrona); no se espera el resultado del subproceso
thread.start()
# el hilo principal espera a que finalicen todos los hilos que ha iniciado
…
except BaseException as erreur:
# se muestra el error
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# se cierra el registrador
if logger:
logger.close()
# se ha terminado
print("Travail terminé...")
# fin de los subprocesos que aún podrían existir si se ha detenido por un error
sys.exit()
- líneas 29-30: cada subproceso tiene su capa [dao];
25.3.4. Ejecución del cliente
Se inicia el servidor web, se inicia el SGBD, se inicia el servidor de correo [hMailServer]. A continuación, se inicia el script [main] del cliente web. Los registros de ejecución en [data/logs/logs.txt] son entonces los siguientes:
2020-07-25 10:21:46.478511, Thread-1 : début du thread [Thread-1] avec 1 contribuable(s)
2020-07-25 10:21:46.479111, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-25 10:21:46.479111, Thread-2 : début du thread [Thread-2] avec 1 contribuable(s)
2020-07-25 10:21:46.480195, Thread-3 : début du thread [Thread-3] avec 2 contribuable(s)
2020-07-25 10:21:46.480195, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-25 10:21:46.481137, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
2020-07-25 10:21:46.481137, Thread-3 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-25 10:21:46.482279, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
2020-07-25 10:21:46.482622, Thread-6 : début du thread [Thread-6] avec 1 contribuable(s)
2020-07-25 10:21:46.482622, Thread-4 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-5 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-6 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-25 10:21:46.725910, Thread-4 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"fa3c83b82761c83e13217967"}
2020-07-25 10:21:46.725910, Thread-4 : {"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}}}
2020-07-25 10:21:46.725910, Thread-4 : fin du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:46.726960, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:47.514108, Thread-3 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,24999.5],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"700e3f5dc808c7c48f0c9007"}
2020-07-25 10:21:47.514610, Thread-3 : {"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}}}
2020-07-25 10:21:47.514939, Thread-3 : fin du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}
2020-07-25 10:21:47.514939, Thread-3 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000}
2020-07-25 10:21:47.527211, Thread-5 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"9e14a5d4a3057f69ab95ab2d"}
2020-07-25 10:21:47.527211, Thread-2 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,22500.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"a06e8fd70a44c9e311f4dce0"}
2020-07-25 10:21:47.527211, Thread-5 : {"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}}}
2020-07-25 10:21:47.527211, Thread-1 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"28c38df998f67685b3a482b8"}
2020-07-25 10:21:47.527211, Thread-2 : {"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}}}
2020-07-25 10:21:47.528341, Thread-5 : fin du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.528341, Thread-1 : {"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}}}
2020-07-25 10:21:47.528842, Thread-2 : fin du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}
2020-07-25 10:21:47.529349, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000}
2020-07-25 10:21:47.529699, Thread-1 : fin du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.529699, Thread-2 : fin du thread [Thread-2]
2020-07-25 10:21:47.531905, Thread-1 : fin du thread [Thread-1]
2020-07-25 10:21:47.536121, Thread-6 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,93749.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"38499b63076516c02f2770ec"}
2020-07-25 10:21:47.537161, Thread-3 : {"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}}}
2020-07-25 10:21:47.537161, Thread-6 : {"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}}}
2020-07-25 10:21:47.538156, Thread-3 : fin du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.538557, Thread-6 : fin du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.538828, Thread-3 : fin du thread [Thread-3]
2020-07-25 10:21:47.538828, Thread-6 : fin du thread [Thread-6]
2020-07-25 10:21:47.546198, Thread-5 : {"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}}}
2020-07-25 10:21:47.546198, Thread-5 : fin du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.546198, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000}
2020-07-25 10:21:47.739643, Thread-4 : {"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}}}
2020-07-25 10:21:47.739643, Thread-4 : fin du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.740668, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000}
2020-07-25 10:21:48.557469, Thread-5 : {"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}}}
2020-07-25 10:21:48.558715, Thread-5 : fin du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}
2020-07-25 10:21:48.558715, Thread-5 : fin du thread [Thread-5]
2020-07-25 10:21:48.753025, Thread-4 : {"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}}}
2020-07-25 10:21:48.753318, Thread-4 : fin du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-25 10:21:48.753540, Thread-4 : fin du thread [Thread-4]
- Tenemos un total de 6 subprocesos, es decir, 6 clients (líneas 1, 3, 4, 6, 8, 9) que consultan simultáneamente el servidor de cálculo de impuestos;
- vamos a seguir el subproceso [Thread-4], que gestiona 3 contribuyentes (línea 6). Realizará secuencialmente tres solicitudes al servidor de cálculo de impuestos;
- línea 10: la primera solicitud de [Thread-4];
- línea 13: [Thread-4] ha recibido la respuesta a su primera solicitud. En ella encuentra una cookie de sesión en la que figura el n.º [fa3c83b82761c83e13217967] que le ha asignado el servidor;
- línea 14: el impuesto del primer contribuyente;
- línea 16: [Thread-4] realiza una solicitud para el segundo contribuyente;
- línea 43: [Thread-4] recibe el impuesto del segundo contribuyente;
- línea 45: [Thread-4] realiza una consulta para el tercer contribuyente;
- línea 49: [Thread-4] recibe el impuesto del tercer contribuyente;
- línea 51: [Thread-4] ha terminado su trabajo;
Ahora veamos cómo se han procesado las tres solicitudes de [Thread-4] en el servidor. Podremos seguirlo en los registros del servidor gracias a su número de cliente [fa3c83b82761c83e13217967].
Los registros de [data/logs/logs.txt] en el servidor son los siguientes:
2020-07-25 10:21:39.187366, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:40.439093, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:46.502011, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=50000' [GET]>
2020-07-25 10:21:46.504049, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.505452, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=55555' [GET]>
2020-07-25 10:21:46.506257, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.507292, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=0&salaire=100000' [GET]>
2020-07-25 10:21:46.507292, Thread-4 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.508301, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=50000' [GET]>
2020-07-25 10:21:46.509293, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.511808, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=3&salaire=100000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=200000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.719504, Thread-6 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises dans la couche dao
2020-07-25 10:21:46.720003, Thread-6 : [index] {'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}}}
2020-07-25 10:21:46.736108, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=100000' [GET]>
2020-07-25 10:21:46.736108, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.506709, Thread-2 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises dans la couche dao
2020-07-25 10:21:47.507216, Thread-2 : [index] {'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}}}
2020-07-25 10:21:47.507216, Thread-3 : [index_controller] client [28c38df998f67685b3a482b8], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508442, Thread-4 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508940, Thread-3 : [index] {'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}}}
2020-07-25 10:21:47.510506, Thread-4 : [index] {'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}}}
2020-07-25 10:21:47.511513, Thread-5 : [index_controller] client [a06e8fd70a44c9e311f4dce0], données fiscales prises dans la couche dao
2020-07-25 10:21:47.514939, Thread-5 : [index] {'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}}}
2020-07-25 10:21:47.520727, Thread-7 : [index_controller] client [38499b63076516c02f2770ec], données fiscales prises dans la couche dao
2020-07-25 10:21:47.523162, Thread-7 : [index] {'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}}}
2020-07-25 10:21:47.530835, Thread-9 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=2&salaire=100000' [GET]>
2020-07-25 10:21:47.531736, Thread-9 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises en session
2020-07-25 10:21:47.531905, Thread-9 : [index] {'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}}}
2020-07-25 10:21:47.541899, Thread-10 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=30000' [GET]>
2020-07-25 10:21:47.542488, Thread-10 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:47.542488, Thread-10 : [index] {'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}}}
2020-07-25 10:21:47.553628, Thread-11 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=non&enfants=0&salaire=200000' [GET]>
2020-07-25 10:21:47.553628, Thread-11 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.736910, Thread-8 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:47.737191, Thread-8 : [index] {'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}}}
2020-07-25 10:21:47.748226, Thread-12 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=5&salaire=100000' [GET]>
2020-07-25 10:21:47.748226, Thread-12 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:48.554695, Thread-11 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:48.555070, Thread-11 : [index] {'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}}}
2020-07-25 10:21:48.748753, Thread-12 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:48.748753, Thread-12 : [index] {'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}}}
- El cliente [fa3c83b82761c83e13217967] aparece por primera vez en la línea 14: para calcular el impuesto, el servidor tuvo que recuperar de la base de datos los datos de la administración tributaria;
- luego volvemos a encontrar al cliente [fa3c83b82761c83e13217967] en la línea 36. En esta ocasión, el servidor encuentra los datos de la administración tributaria en la sesión, lo que le evita un acceso, posiblemente costoso, a la capa [dao];
- encontramos al cliente [fa3c83b82761c83e13217967] por tercera vez en la línea 42, donde, una vez más, el servidor utiliza la sesión del cliente;
Este ejemplo ilustra claramente la utilidad de la sesión para un cliente: en ella se almacenan datos compartidos por todas las solicitudes de ese cliente y cuya obtención resulta costosa.
Por parte del cliente, los resultados en el archivo [data/output/résultats.json] son los mismos que en las versiones anteriores.
25.4. Pruebas de la capa [dao]
Al igual que en las |versiones anteriores|, probamos la capa [dao] del cliente:

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

- la configuración [2] es idéntica a la configuración [1] que acabamos de estudiar;
La clase de prueba [TestHttpClientDao] es la siguiente:
import unittest
from Logger import Logger
class TestHttpClientDao(unittest.TestCase):
def test_1(self) -> None:
from TaxPayer import TaxPayer
# {'casado': 'sí', 'hijos': 2, 'salario': 55555,
# 'impuesto': 2814, 'recargo': 0, 'descuento': 0, 'reducción': 0, 'tipo': 0,14}
taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
dao.calculate_tax(taxpayer)
# verificación
self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
self.assertEqual(taxpayer.décôte, 0)
self.assertEqual(taxpayer.réduction, 0)
self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
self.assertEqual(taxpayer.surcôte, 0)
…
if __name__ == '__main__':
# se configura la aplicación
import config
config = config.configure({})
# registrador
logger = Logger(config["logsFilename"])
# se almacena en config
config["logger"] = logger
# se recupera la fábrica de la capa [dao]
dao_factory = config["layers"]["dao_factory"]
# se crea una instancia de la capa [dao]
dao = dao_factory.new_instance()
# ejecutamos los métodos de prueba
print("tests en cours...")
unittest.main()
- Creamos una |configuración de ejecución| para esta prueba;
- se inicia el servidor web con todo su entorno;
- ejecutamos la prueba;
Los resultados son los siguientes:
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/03/tests/TestHttpClientDao.py
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 3.392s
OK
Process finished with exit code 0