Skip to content

31. Clients web para los servicios jSON y XML de version 12

Vamos a escribir tres aplicaciones de consola clientes de los servicios jSON y XML del servidor web que acabamos de escribir. Retomamos la arquitectura cliente/servidor de la version 11:

Image

Escribiremos tres scripts de consola:

  • los scripts [main] y [main3] utilizarán la capa [métier] del servidor;
  • el script [main2] utilizará la capa [métier] del cliente;

31.1. El árbol de scripts de clients

La carpeta [http-clients/07] se obtiene inicialmente copiando la carpeta [http-clients/06]. A continuación, se modifica.

Image

  • en [1]: los datos utilizados o creados por el cliente;
  • en [2], la configuración y los scripts de consola del cliente;
  • en [3], la capa [dao] del cliente;
  • en [4], la carpeta de pruebas de la capa [dao] del cliente;

31.2. La capa [dao] de clients

Image

Image

31.2.1. Interfaz

La capa [dao] implementará la siguiente interfaz [InterfaceImpôtsDaoWithHttpSession]:


from abc import abstractmethod

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from TaxPayer import TaxPayer

class InterfaceImpôtsDaoWithHttpSession(AbstractImpôtsDao):

    # cálculo del impuesto por unidad
    @abstractmethod
    def calculate_tax(self, taxpayer: TaxPayer):
        pass

    # cálculo del impuesto por lotes
    @abstractmethod
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        pass

    # inicialización de una sesión
    @abstractmethod
    def init_session(self, type_session: str):
        pass

    # fin de sesión
    @abstractmethod
    def end_session(self):
        pass

    # autenticación
    @abstractmethod
    def authenticate_user(self, user: str, password: str):
        pass

    # lista de simulaciones
    @abstractmethod
    def get_simulations(self) -> list:
        pass

    # eliminar una simulación
    @abstractmethod
    def delete_simulation(self, id: int) -> list:
        pass

    # obtener los datos para el cálculo del impuesto
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass

Cada método de la interfaz corresponde a un servicio URL del servidor de cálculo de impuestos.

  • línea 7: la interfaz extiende la clase [AbstractDao] que gestiona los accesos al sistema de archivos;

La correspondencia entre los métodos y los servicios URL se establece en el archivo de configuración [config]:


        # le serveur de calcul de l'impôt
        "server": {
            "urlServer""http://127.0.0.1:5000",
            "user": {
                "login""admin",
                "password""admin"
            },
            "url_services": {
                "calculate-tax""/calculer-impot",
                "get-admindata""/get-admindata",
                "calculate-tax-in-bulk-mode""/calculer-impots",
                "init-session""/init-session",
                "end-session""/fin-session",
                "authenticate-user""/authentifier-utilisateur",
                "get-simulations""/lister-simulations",
                "delete-simulation""/supprimer-simulation"
            }

31.2.2. Implementación

La interfaz [InterfaceImpôtsDaoWithHttpSession] se implementa mediante la siguiente clase [ImpôtsDaoWithHttpSession]:


# importaciones
import json

import requests
import xmltodict
from flask_api import status

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDaoWithHttpSession import InterfaceImpôtsDaoWithHttpSession
from TaxPayer import TaxPayer

class ImpôtsDaoWithHttpSession(InterfaceImpôtsDaoWithHttpSession):

    # fabricante
    def __init__(self, config: dict):
        # inicialización del padre
        AbstractImpôtsDao.__init__(self, config)
        # almacenamiento de elementos de la configuración
        # config general
        self.__config = config
        # servidor
        self.__config_server = config["server"]
        # servicios
        self.__config_services = config["server"]['url_services']
        # modo debug
        self.__debug = config["debug"]
        # registrador
        self.__logger = None
        # cookies
        self.__cookies = None
        # tipo de sesión (json, xml)
        self.__session_type = None

        # etapa de solicitud/respuesta
     def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
        # [method]: método HTTP GET o POST
        # [url_service]: URL de servicio
        # [data]: parámetros del POST en x-www-form-urlencoded
        # [json]: parámetros de POST en json
        # [cookies]: cookies que se deben incluir en la solicitud

        # se debe tener una sesión XML o JSON; de lo contrario, no se podrá gestionar la respuesta
        if self.__session_type not in ['json''xml']:
            raise ImpôtsError(73"il n'y a pas de session valide en cours")

        # conexión
        if method == "GET":
            # GET
            response = requests.get(url_service, cookies=self.__cookies)
        else:
            # POST
            response = requests.post(url_service, data=data_value, json=json_value, cookies=self.__cookies)

        # modo debug ?
        if self.__debug:
            # iniciar sesión
            if not self.__logger:
                self.__logger = self.__config['logger']
            # iniciar sesión
            self.__logger.write(f"{response.text}\n")

        # resultado
        if self.__session_type == "json":
            résultat = json.loads(response.text)
        else:  # xml
            résultat = xmltodict.parse(response.text[39:])['root']

        # se recuperan las cookies de la respuesta si las hay
        if response.cookies:
            self.__cookies = response.cookies

        # código de estado
        status_code = response.status_code

        # si el código de estado es distinto de 200 OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(35, résultat['réponse'])

        # se devuelve el resultado
        return résultat['réponse']

    def init_session(self, session_type: str):
        # se anota el tipo de sesión
        self.__session_type = session_type

        # url de servicio
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        # ejecución de la consulta
        self.get_response("GET", url_service)

  • líneas 16-34: el constructor de la clase;
  • línea 19: se inicializa la clase padre;
  • líneas 21-28: se almacenan ciertos datos de la configuración;
  • líneas 29-34: se crean tres propiedades utilizadas en los métodos de la clase;
  • líneas 36-82: el método [get_response] factoriza lo que es común a todos los métodos de la capa [dao]: el envío de una solicitud HTTP y la recuperación de la respuesta HTTP del servidor;
  • líneas 38-42: definición de los 5 parámetros del método [get_response];
  • línea 42: cabe señalar que, dado que el servidor mantiene una sesión, el cliente necesita leer y reenviar cookies;
  • líneas 44-46: se comprueba que efectivamente hay una sesión válida en curso;
  • línea 51: caso del GET. Se devuelven las cookies recibidas;
  • línea 54: caso del POST. Este puede tener dos tipos de parámetros:
    • el tipo [x-www-form-urlencoded]. Este es el caso de URL, [/calculer-impot] y [/authentifier-utilisateur]. En este caso, se utiliza el parámetro [data_value] recibido por el método;
    • el tipo [json]. Este es el caso de URL y [/calculer-impots]. En ese caso, se utiliza el parámetro [json_value] recibido por el método;

Aquí también se devuelve la cookie de sesión.

  • líneas 56-62: si se está en modo [debug], se registra la respuesta del servidor. Este registro es importante porque permite saber exactamente qué ha devuelto el servidor;
  • líneas 64-68: dependiendo de si se está en modo json o xml, la respuesta de texto del servidor se transforma en un diccionario. Tomemos el ejemplo de URL [/init-session]:

La respuesta jSON es la siguiente:


2020-08-03 11:45:21.218116, MainThread : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}

La respuesta XML es la siguiente:


2020-08-03 11:45:54.671871, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>

El código de las líneas 64-68 garantiza que, en ambos casos, se obtenga en [résultat] un diccionario con las claves [action, état, réponse];

  • líneas 70-72: si la respuesta contiene cookies, se recuperan. Habrá que reenviarlas en la próxima solicitud;
  • líneas 74-79: si el estado HTTP de la respuesta es distinto de 200, se lanza una excepción con el mensaje de error contenido en resultado[’réponse’]. Puede tratarse de un error o de una lista de errores;
  • líneas 81-82: se devuelve la respuesta del servidor al código llamante;

[init_session]

  • línea 84: el método [init_session] sirve para establecer el tipo de sesión json o xml que el cliente desea iniciar con el servidor;
  • línea 86: se indica el tipo de sesión deseado dentro de la clase. De hecho, todos los métodos necesitan esta información para descodificar correctamente la respuesta del servidor;
  • líneas 88-90: mediante la configuración de la aplicación, se determina el servicio URL al que hay que consultar;
  • línea 93: se consulta el servicio URL. No se recupera el resultado del método [get_response]:
    • si lanza una excepción, la operación ha fallado. La excepción no se gestiona aquí y se transmitirá directamente al código llamante, que detendrá el cliente con un mensaje de error;
    • si no lanza una excepción, significa que la inicialización de la sesión se ha realizado correctamente;

[authenticate_user]


    def authenticate_user(self, user: str, password: str):
        # url de servicio
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['authenticate-user']}"
        post_params = {
            "user": user,
            "password": password
        }

        # ejecución de consulta
        self.get_response("POST", url_service, post_params)
  • el método [authenticate_user] sirve para autenticarse ante el servidor. Para ello, recibe las credenciales de conexión [user, password] en la línea 1;
  • líneas 2-4: se determina el servicio URL al que se va a consultar;
  • líneas 5-8: los parámetros del POST, ya que el URL [/authentifier-utilisateur] espera un POST con parámetros [user, password];
  • línea 11: se ejecuta la solicitud. Una vez más, no se recupera la respuesta del servidor. Es la excepción lanzada por [get_response] la que indica si se ha tenido éxito o no;

[calculate_tax]


    def calculate_tax(self, taxpayer: TaxPayer):
        # url de servicio
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['calculate-tax']}"
        # parámetros de POST
        post_params = {
            "marié": taxpayer.marié,
            "enfants": taxpayer.enfants,
            "salaire": taxpayer.salaire
        }

        # ejecución de la solicitud
        response = self.get_response("POST", url_service, post_params)
        # actualización de TaxPayer con la respuesta
        taxpayer.fromdict(response)
  • el método [calculate_tax] permite calcular el impuesto de un contribuyente [taxpayer] pasado como parámetro. Este parámetro es modificado por el método (línea 15) y constituye, por tanto, el resultado del método;
  • líneas 2-4: se define el servicio URL al que se va a consultar;
  • líneas 6-10: los parámetros del POST que se va a enviar. De hecho, el servicio URL espera un POST con los parámetros [marié, enfants, salaire];
  • líneas 12-13: se ejecuta la solicitud y se recupera la respuesta del servidor. El servicio URL del servicio [/calculer-impot] devuelve un diccionario con las claves [impôt, décôte, surcôte, réduction, taux] del impuesto;
  • línea 15: el diccionario [response] obtenido se utiliza para actualizar el contribuyente [taxpayer];

[calculate_tax_in_bulk_mode]


    # cálculo del impuesto en modo masivo
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        # se permiten excepciones

        # se transforman los contribuyentes en una lista de diccionarios
        # solo se conservan las propiedades [marié, enfants, salaire]
        list_dict_taxpayers = list(
            map(lambda taxpayer:
                taxpayer.asdict(included_keys=[
                    '_TaxPayer__marié',
                    '_TaxPayer__enfants',
                    '_TaxPayer__salaire']),
                taxpayers))

        # url de servicio
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['calculate-tax-in-bulk-mode']}"

        # ejecución de la solicitud
        list_dict_taxpayers2 = self.get_response("POST", url_service, data_value=None, json_value=list_dict_taxpayers)
        # cuando solo hay un contribuyente y se está en una sesión xml, [list_dict_taxpayers2] no es una lista
        # en este caso se crea una lista
        if not isinstance(list_dict_taxpayers2, list):
            list_dict_taxpayers2 = [list_dict_taxpayers2]
        # actualizamos la lista inicial de contribuyentes con los resultados recibidos
        for i in range(len(taxpayers)):
            # actualización de contribuyentes[i]
            taxpayers[i].fromdict(list_dict_taxpayers2[i])
        # aquí se ha actualizado el parámetro [taxpayers] con los resultados del servidor
  • línea 2: el método recibe una lista de contribuyentes de tipo TaxPayer;
  • líneas 7-13: esta lista de elementos de tipo [TaxPayer] se transforma en una lista de diccionarios [marié, enfants, salaire];
  • líneas 15-17: se establece el URL de servicio;
  • líneas 19-20: se ejecuta una solicitud POST cuyo cuerpo jSON está formado por la lista de diccionarios creada en la línea 7. Se recupera la respuesta del servidor;
  • líneas 23-24: las pruebas han revelado un problema cuando la sesión es de tipo XML:
    • si la lista inicial de contribuyentes tiene N elementos (N>1), se obtiene como resultado una lista de N diccionarios de tipo [OrderedDict];
    • si la lista inicial solo tiene un elemento, no se obtiene una lista, sino un elemento de tipo [OrderedDict];
  • líneas 23-24: si nos encontramos en este último caso (1 elemento), transformamos el resultado en una lista de 1 elemento;
  • líneas 25-28: esta lista de diccionarios recibidos contiene el impuesto de cada contribuyente de la lista inicial. A continuación, se actualiza cada uno de ellos con los resultados recibidos;

[get_simulations]


    def get_simulations(self) -> list:
        # url de servicio
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['get-simulations']}"

        # ejecución de la consulta
        return self.get_response("GET", url_service)
  • línea 1: el método solicita la lista de simulaciones realizadas en la sesión actual;
  • línea 2: el método devuelve la respuesta del servidor;

[delete_simulation]


    def delete_simulation(self, id: int) -> list:
        # url de servicio
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['delete-simulation']}/{id}"

        # ejecución de la consulta
        return self.get_response("GET", url_service)
  • línea 1: el método elimina la simulación cuyo identificador se le pasa;
  • línea 7: devuelve la respuesta del servidor, la lista de simulaciones restantes tras la eliminación solicitada;

[get-admindata]


    def get_admindata(self) -> AdminData:
        # se permiten excepciones

        # url de servicio
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['get-admindata']}"

        # ejecución de la consulta
        résultat = self.get_response("GET", url_service)

        # el resultado es un diccionario de valores de tipo str si hay sesión xml
        if self.__session_type == 'xml':
            # nuevo diccionario
            résultat2 = {}
            # se convierte todo a formato numérico
            for key, value in résultat.items():
                # algunos elementos del diccionario son listas
                if isinstance(value, list):
                    values = []
                    for value2 in value:
                        values.append(float(value2))
                    résultat2[key] = values
                else:
                    # otros son simples elementos
                    résultat2[key] = float(value)
        else:
            résultat2 = résultat
        # resultado de tipo AdminData
        return AdminData().fromdict(résultat2)
  • línea 1: el método solicita al servidor las constantes fiscales que permiten calcular el impuesto;
  • línea 29: devuelve un tipo [AdminData];
  • línea 9: se recupera la respuesta del servidor en forma de diccionario. Las pruebas muestran que hay un problema cuando la sesión es una sesión XML: en lugar de ser valores numéricos, los valores del diccionario son cadenas de caracteres. Ya habíamos señalado este problema durante el análisis del módulo [xmltodict] y constatamos que se trataba de un funcionamiento normal. [xmltodict] no tiene información de tipos en el flujo XML que se le proporciona. Dicho esto, en este caso concreto, hay que convertir a numérico todos los valores del diccionario recibido. Este contiene tres listas [limites, coeffr, coeffn] y una serie de propiedades numéricas;
  • líneas 13-25: creación de un diccionario [résultat2] con valores numéricos a partir del diccionario [résultat] con valores de tipo cadena de caracteres;
  • línea 29: el diccionario [resultat2] se utiliza para inicializar un tipo [AdminData];

31.2.3. La fábrica de la capa [dao]

Nuestros clients serán multihilo. Dado que la capa [dao] está implementada por una clase con estado de lectura/escritura (= propiedades de lectura/escritura), cada subproceso debe tener su propia capa [dao] o, de lo contrario, es necesario sincronizar el acceso a los datos compartidos entre los subprocesos. En este caso, optamos por la primera solución. Utilizamos una clase [ImpôtsDaoWithHttpSessionFactory] capaz de crear instancias de la capa [dao]:


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)

31.3. Configuración de clients

Image

Los clients se configuran mediante los archivos [config] y [config_layers]. El archivo [config] es el siguiente:


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

    # paso 1 ------

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

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

    # dependencias absolutas
    absolute_dependencies = [
        # carpetas del proyecto
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        # AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        # ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        # AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        # Constantes, tramos
        f"{root_dir}/impots/v05/entities",
        # ImpôtsDaoWithHttpSession, ImpôtsDaoWithHttpSessionFactory, InterfaceImpôtsDaoWithHttpSession
        f"{script_dir}/../services",
        # scripts de configuración
        script_dir,
        # Registrador
        f"{root_dir}/impots/http-servers/02/utilities",
    ]

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

    # paso 2 ------
    # configuración de la aplicación con constantes
    config.update({
        # archivo de contribuyentes
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        # archivo de resultados
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        # archivo de errores
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        # archivo de registros
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # servidor de cálculo de impuestos
        "server": {
            "urlServer""http://127.0.0.1:5000",
            "user": {
                "login""admin",
                "password""admin"
            },
            "url_services": {
                "calculate-tax""/calculer-impot",
                "get-admindata""/get-admindata",
                "calculate-tax-in-bulk-mode""/calculer-impots",
                "init-session""/init-session",
                "end-session""/fin-session",
                "authenticate-user""/authentifier-utilisateur",
                "get-simulations""/lister-simulations",
                "delete-simulation""/supprimer-simulation"
            }
        },
        # modo debug
        "debug"True
    }
    )

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

    # se realiza la configuración
    return config

El archivo [config_layers] es el siguiente:


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

    # capa [métier]
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    # fábrica de la capa dao
    from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
    dao_factory = ImpôtsDaoWithHttpSessionFactory(config)

    # se devuelve la configuración de las capas
    return {
        "dao_factory": dao_factory,
        "métier": métier
    }
  • los clients no tendrán acceso directo a la capa [dao]. Para obtenerlo, deberán pasar por la fábrica de la capa [dao];

31.4. El cliente [main]

El cliente [main] permite probar los URL y [/init-session, /authentifier-utilisateur, /calculer-impots, /fin-session]:


# se espera un parámetro json o xml
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

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

# 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(config: dict, taxpayers: list):
    # 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()
    # tipo de sesión
    session_type = config['session_type']
    # número de contribuyentes
    nb_taxpayers = len(taxpayers)
    # registro
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    # se inicializa la sesión
    dao.init_session(session_type)
    # se realiza la autenticación
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    # se calcula el impuesto de los contribuyentes
    dao.calculate_tax_in_bulk_mode(taxpayers)
    # fin de sesión
    dao.end_session()
    # registro
    logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")

# lista de subprocesos del cliente
threads = []
logger = None
# código
try:
    # registrador
    logger = Logger(config["logsFilename"])
    # se almacena en config
    config["logger"] = logger
    # registro de inicio
    logger.write("début du calcul de l'impôt des contribuables\n")
    # 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()
    # lectura de los datos de los contribuyentes
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # de los contribuyentes?
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # cálculo del impuesto de los contribuyentes con varios subprocesos
    i = 0
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        # cada subproceso procesará de 1 a 4 contribuyentes
        nb_taxpayers = min(l_taxpayers - i, random.randint(14))
        # la lista de contribuyentes procesados por el subproceso
        thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
        # incrementamos i para el siguiente subproceso
        i += nb_taxpayers
        # se crea el hilo
        thread = threading.Thread(target=thread_function, args=(config, 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
    for thread in threads:
        thread.join()
    # aquí todos los subprocesos han terminado su trabajo: cada uno ha modificado uno o varios objetos [taxpayer]
    # los resultados se guardan en el archivo jSON
    dao.write_taxpayers_results(taxpayers)
    # fin
except BaseException as erreur:
    # visualización del error
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # se cierra el registrador
    if logger:
        # registro de fin
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # cierre del registrador
        logger.close()
    # se ha terminado
    print("Travail terminé...")
    # fin de los subprocesos que aún podrían existir si se ha detenido por error
    sys.exit()
  • líneas 4-11: el cliente espera un parámetro que le indique el tipo de sesión, json o xml, que se utilizará con el servidor;
  • líneas 13-15: el cliente está configurado;
  • líneas 48-104: este código es conocido. Se ha utilizado en numerosas ocasiones. Distribuye a los contribuyentes para los que se quiere calcular el impuesto en varios subprocesos;
  • línea 26: el método [thread_function] es el método ejecutado por cada subproceso para calcular el impuesto de los contribuyentes que le han sido asignados;
  • líneas 27-30: cada subproceso tiene su propia capa [dao];
  • el cálculo del impuesto se realiza en cuatro pasos:
    • líneas 37-38: inicialización de una sesión, json o xml, con el servidor;
    • líneas 39-40: autenticación en el servidor;
    • líneas 41-42: cálculo del impuesto;
    • líneas 43-44: cierre de la sesión con el servidor;

Al ejecutar este código en modo [json], se obtienen los siguientes registros:


2020-08-03 14:28:34.320751, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:28:34.328749, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.328749, Thread-2 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.333592, Thread-3 : début du calcul de l'impôt des 3 contribuables
2020-08-03 14:28:34.368651, Thread-2 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.375699, Thread-1 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.377432, Thread-3 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.385653, Thread-2 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.392656, Thread-1 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.396377, Thread-3 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.406528, Thread-2 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}]}
2020-08-03 14:28:34.413837, Thread-1 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}]}
2020-08-03 14:28:34.416695, Thread-3 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 3}]}
2020-08-03 14:28:34.425747, Thread-2 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.425747, Thread-2 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.428956, Thread-1 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.428956, Thread-1 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.428956, Thread-3 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.428956, Thread-3 : fin du calcul de l'impôt des 3 contribuables
2020-08-03 14:28:34.428956, MainThread : fin du calcul de l'impôt des contribuables

Arriba se muestra el recorrido del hilo [Thread-2].

Si se ejecuta [main] en modo XML, los registros son los siguientes:


2020-08-03 14:32:48.495316, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:32:48.496452, Thread-1 : début du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.498992, Thread-2 : début du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.498992, Thread-3 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:32:48.498992, Thread-4 : début du calcul de l'impôt des 3 contribuables
2020-08-03 14:32:48.538637, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.540783, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.547811, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.547811, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.555184, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.564793, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.564793, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.568333, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.568333, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>2</enfants><salaire>55555</salaire><impôt>2814</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>oui</marié><enfants>2</enfants><salaire>50000</salaire><impôt>1384</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>384</décôte><réduction>347</réduction><id>2</id></réponse></root>
2020-08-03 14:32:48.579205, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>3</enfants><salaire>50000</salaire><impôt>0</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>720</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>non</marié><enfants>2</enfants><salaire>100000</salaire><impôt>19884</impôt><surcôte>4480</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse></root>
2020-08-03 14:32:48.579205, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>non</marié><enfants>3</enfants><salaire>100000</salaire><impôt>16782</impôt><surcôte>7176</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>oui</marié><enfants>3</enfants><salaire>100000</salaire><impôt>9200</impôt><surcôte>2180</surcôte><taux>0.3</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse><réponse><marié>oui</marié><enfants>5</enfants><salaire>100000</salaire><impôt>4230</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>0</décôte><réduction>0</réduction><id>3</id></réponse><réponse><marié>non</marié><enfants>0</enfants><salaire>100000</salaire><impôt>22986</impôt><surcôte>0</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>4</id></réponse></root>
2020-08-03 14:32:48.588051, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>2</enfants><salaire>30000</salaire><impôt>0</impôt><surcôte>0</surcôte><taux>0.0</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>non</marié><enfants>0</enfants><salaire>200000</salaire><impôt>64210</impôt><surcôte>7498</surcôte><taux>0.45</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse><réponse><marié>oui</marié><enfants>3</enfants><salaire>200000</salaire><impôt>42842</impôt><surcôte>17283</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>3</id></réponse></root>
2020-08-03 14:32:48.594058, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-1 : fin du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.595198, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-2 : fin du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.595198, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-3 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:32:48.603351, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.603351, Thread-4 : fin du calcul de l'impôt des 3 contribuables
2020-08-03 14:32:48.603351, MainThread : fin du calcul de l'impôt des contribuables

Arriba, la ruta del hilo [Thread-2].

31.5. El cliente [main2]

Image

El cliente [main2] permite probar los URL [/init-session, /authentifier-utilisateur, /get-admindata, /fin-session]:


# se espera un parámetro json o xml
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

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

# dependencias
from ImpôtsError import ImpôtsError
from Logger import Logger

logger = None
# código
try:
    # registrador
    logger = Logger(config["logsFilename"])
    # se almacena en el config
    config["logger"] = logger
    # registro de inicio
    logger.write("début du calcul de l'impôt des contribuables\n")
    # 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()
    # se recuperan los contribuyentes
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # ¿de los contribuyentes?
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # tipo de sesión
    session_type = config['session_type']
    # se inicializa la sesión
    dao.init_session(session_type)
    # se realiza la autenticación
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    # se recuperan los datos de la administración tributaria
    admindata = dao.get_admindata()
    # fin de la sesión
    dao.end_session()
    # cálculo del impuesto de los contribuyentes mediante la capa [métier]
    métier = config['layers']['métier']
    for taxpayer in taxpayers:
        métier.calculate_tax(taxpayer, admindata)
    # se guardan los resultados en el archivo jSON
    dao.write_taxpayers_results(taxpayers)
 except BaseException as erreur:
     # visualización del error
     print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # se cierra el registrador
    if logger:
        # registro de fin
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # cierre del registrador
        logger.close()
    # se ha completado
    print("Travail terminé...")
  • líneas 1-11: se recupera el parámetro [json, xml] que establece el tipo de sesión que se va a establecer con el servidor;
  • líneas 13-15: se configura el cliente;
  • líneas 30-33: se crea una capa [dao];
  • líneas 34-35: con ella, se recupera la lista de contribuyentes para los que hay que calcular el impuesto;
  • a continuación, se repiten los cuatro pasos del diálogo con el servidor;
    • líneas 41-42: se inicia una sesión con el servidor;
    • líneas 43-44: se realiza la autenticación en el servidor;
    • líneas 45-46: se solicitan al servidor las constantes fiscales que permiten calcular el impuesto;
    • líneas 47-48: se cierra la sesión con el servidor;
  • líneas 49-52: con estas constantes, se puede calcular el impuesto de los contribuyentes utilizando la capa [métier] local del cliente;
  • líneas 53-54: se registran los resultados obtenidos;

Para una sesión XML, los resultados son los siguientes:


2020-08-03 14:44:43.194294, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:44:43.231633, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:44:43.240872, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:44:43.250061, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>get-admindata</action><état>1000</état><réponse><limites>9964.0</limites><limites>27519.0</limites><limites>73779.0</limites><limites>156244.0</limites><limites>93749.0</limites><coeffr>0.0</coeffr><coeffr>0.14</coeffr><coeffr>0.3</coeffr><coeffr>0.41</coeffr><coeffr>0.45</coeffr><coeffn>0.0</coeffn><coeffn>1394.96</coeffn><coeffn>5798.0</coeffn><coeffn>13913.7</coeffn><coeffn>20163.4</coeffn><abattement_dixpourcent_min>437.0</abattement_dixpourcent_min><plafond_impot_couple_pour_decote>2627.0</plafond_impot_couple_pour_decote><plafond_decote_couple>1970.0</plafond_decote_couple><valeur_reduc_demi_part>3797.0</valeur_reduc_demi_part><plafond_revenus_celibataire_pour_reduction>21037.0</plafond_revenus_celibataire_pour_reduction><id>1</id><abattement_dixpourcent_max>12502.0</abattement_dixpourcent_max><plafond_impot_celibataire_pour_decote>1595.0</plafond_impot_celibataire_pour_decote><plafond_decote_celibataire>1196.0</plafond_decote_celibataire><plafond_revenus_couple_pour_reduction>42074.0</plafond_revenus_couple_pour_reduction><plafond_qf_demi_part>1551.0</plafond_qf_demi_part></réponse></root>
2020-08-03 14:44:43.269850, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:44:43.269850, MainThread : fin du calcul de l'impôt des contribuables

31.6. El cliente [main3]

El cliente [main3] permite probar los URL [/init-session, /calculer-impots, /get-simulations, /delete-simulation, /fin-session]:

Image


# se espera un parámetro json o xml
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# configurando la aplicación
import config
config = config.configure({"session_type": session_type})

# dependencias
from ImpôtsError import ImpôtsError
import sys
from Logger import Logger

logger = None
# código
try:
    # registrador
    logger = Logger(config["logsFilename"])
    # se almacena en el config
    config["logger"] = logger
    # registro de inicio
    logger.write("début du calcul de l'impôt des contribuables\n")
    # 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()
    # lectura de los datos de los contribuyentes
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # de los contribuyentes?
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # cálculo del impuesto de los contribuyentes
    # número de contribuyentes
    nb_taxpayers = len(taxpayers)
    # registro
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    # se inicia la sesión
    dao.init_session(session_type)
    # autenticación
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    # se calcula el impuesto de los contribuyentes
    dao.calculate_tax_in_bulk_mode(taxpayers)
    # se solicita la lista de simulaciones
    simulations = dao.get_simulations()
    # se elimina una de cada dos
    for i in range(len(simulations)):
        if i % 2 == 0:
            # se elimina la simulación
            dao.delete_simulation(simulations[i]['id'])
    # fin de sesión
    dao.end_session()
    # consulte los registros para ver los diferentes resultados (modo debug=True)
except BaseException as erreur:
    # visualización del error
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # se cierra el registrador
    if logger:
        # registro de fin
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # cierre del registrador
        logger.close()
    # se ha completado
    print("Travail terminé...")
  • líneas 1-11: se recupera el tipo de sesión en los parámetros del script;
  • líneas 13-15: se configura la aplicación;
  • líneas 25-50: se encuentra código ya explicado en algún momento;
  • líneas 51-52: se solicita la lista de simulaciones realizadas en la sesión actual;
  • líneas 53-57: se elimina una de cada dos simulaciones;
  • líneas 58-59: se cierra la sesión;

Durante una sesión jSON, los registros son los siguientes:


2020-08-03 15:01:52.702297, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 15:01:52.702297, MainThread : début du calcul de l'impôt des 11 contribuables
2020-08-03 15:01:52.734806, MainThread : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 15:01:52.747961, MainThread : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 15:01:52.765721, MainThread : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 5}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 6}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 7}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 8}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 9}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 10}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 11}]}
2020-08-03 15:01:52.785505, MainThread : {"action": "lister-simulations", "état": 500, "réponse": [{"décôte": 0, "enfants": 2, "id": 1, "impôt": 2814, "marié": "oui", "réduction": 0, "salaire": 55555, "surcôte": 0, "taux": 0.14}, {"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 720, "enfants": 3, "id": 3, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.801475, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 720, "enfants": 3, "id": 3, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.810129, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.818803, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.834604, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.843803, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.851855, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}]}
2020-08-03 15:01:52.863165, MainThread : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 15:01:52.863165, MainThread : fin du calcul de l'impôt des contribuables
  • línea 6: tenemos 11 simulaciones;
  • línea 12: tras las diferentes eliminaciones, solo quedan 5;

31.7. La clase de prueba [Test2HttpClientDaoWithSession]

Image

La clase [Test2HttpClientDaoWithSession] prueba la capa [dao] de clients de la siguiente manera:


import unittest

from ImpôtsError import ImpôtsError
from Logger import Logger
from TaxPayer import TaxPayer

class Test2HttpClientDaoWithSession(unittest.TestCase):

    def test_init_session_json(self) -> None:
        print('test_init_session_json')
        erreur = False
        try:
            dao.init_session('json')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # no debe haber ningún error
        self.assertFalse(erreur)

    def test_init_session_xml(self) -> None:
        print('test_init_session_xml')
        erreur = False
        try:
            dao.init_session('xml')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # no debe haber ningún error
        self.assertFalse(erreur)

    def test_init_session_xxx(self) -> None:
        print('test_init_session_xxx')
        erreur = False
        try:
            dao.init_session('xxx')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # debe haber un error
        self.assertTrue(erreur)

    def test_authenticate_user_success(self) -> None:
        print('test_authenticate_user_success')
        # iniciar sesión
        dao.init_session('json')
        # prueba
        erreur = False
        try:
            dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # no debe haber ningún error
        self.assertFalse(erreur)

    def test_authenticate_user_failed(self) -> None:
        print('test_authenticate_user_failed')
        # iniciar sesión
        dao.init_session('json')
        # prueba
        erreur = False
        try:
            dao.authenticate_user('x''y')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # debe haber un error
        self.assertTrue(erreur)

    def test_get_simulations(self) -> None:
        print('test_get_simulations')
        # iniciar sesión
        dao.init_session('json')
        # autenticación
        dao.authenticate_user('admin''admin')
        # cálculo de impuestos
        # {'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)
        # get_simulations
        simulations = dao.get_simulations()
        # verificaciones
        # debe haber 1 simulación
        self.assertEqual(1, len(simulations))
        simulation = simulations[0]
        # verificación del impuesto calculado
        self.assertAlmostEqual(simulation['impôt']2815, delta=1)
        self.assertEqual(simulation['décôte']0)
        self.assertEqual(simulation['réduction']0)
        self.assertAlmostEqual(simulation['taux']0.14, delta=0.01)
        self.assertEqual(simulation['surcôte']0)

    def test_delete_simulation(self) -> None:
        print('test_delete_simulation')
        # iniciar sesión
        dao.init_session('json')
        # autenticación
        dao.authenticate_user('admin''admin')
        # cálculo del impuesto
        taxpayer = TaxPayer().fromdict({"marié""oui""enfants"2"salaire"55555})
        dao.calculate_tax(taxpayer)
        # get_simulations
        simulations = dao.get_simulations()
        # delete_simulation
        dao.delete_simulation(simulations[0]['id'])
        # get_simulations
        simulations = dao.get_simulations()
        # verificación: ya no debe haber simulaciones
        self.assertEqual(0, len(simulations))
        # se elimina una simulación que no existe
        erreur = False
        try:
            dao.delete_simulation(100)
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # debe haber un error
        self.assertTrue(erreur)

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

    # registrar
    logger = Logger(config["logsFilename"])
    # se almacena en la config
    config["logger"] = logger

    # capa [dao]
    dao_factory = config['layers']['dao_factory']
    dao = dao_factory.new_instance()

    # se ejecutan los métodos de prueba
    print("tests en cours...")
    unittest.main()
  • la capa [dao] envía una solicitud al servidor, recibe su respuesta y la formatea para devolverla al código que la invocó. Cuando el servidor envía una respuesta con un código de estado distinto de 200, la capa [dao] lanza una excepción. Además, varias pruebas consisten en determinar si se ha producido una excepción o no;
  • líneas 9-18: se inicializa una sesión jSON. No debe haber ningún error;
  • líneas 20-29: se inicializa una sesión XML. No debe producirse ningún error;
  • líneas 31-40: se inicializa una sesión con un tipo incorrecto. Debe producirse un error;
  • líneas 42-54: se realiza la autenticación con las credenciales correctas. No debe producirse ningún error;
  • líneas 56-68: se realiza la autenticación con credenciales incorrectas. Debe producirse un error;
  • líneas 70-92: se realiza un cálculo de impuestos y luego se solicita la lista de simulaciones. Debe haber una. Además, se comprueba que esta simulación contenga efectivamente el impuesto solicitado;
  • líneas 94-119: se realiza una simulación que luego se elimina. A continuación, se intenta eliminar una simulación cuando ya no hay ninguna. Debe producirse un error;
  • líneas 121-137: la prueba se ejecuta como un script de consola clásico;
  • líneas 122-124: se configura la aplicación;
  • líneas 126-129: se configura el registrador. Esto nos permitirá seguir los registros;
  • líneas 131-133: se instancia la capa [dao] que se va a probar;
  • líneas 135-137: se ejecutan las pruebas;

Los resultados de la consola 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/07/tests/Test2HttpClientDaoWithSession.py
tests en cours...
test_authenticate_user_failed
..MyException[35, ["Echec de l'authentification"]]
test_authenticate_user_success
test_delete_simulation
MyException[35, ["la simulation n° [100] n'existe pas"]]
test_get_simulations
test_init_session_json
test_init_session_xml
test_init_session_xxx
MyException[73, il n'y a pas de session valide en cours]
----------------------------------------------------------------------
Ran 7 tests in 0.171s

OK

Process finished with exit code 0