Skip to content

20. Ejercicio práctico: version 5

Image

Vamos a desarrollar tres aplicaciones:

  • la aplicación 1 inicializará la base de datos que sustituirá al archivo [admindata.json] del version 4;
  • la aplicación 2 realizará el cálculo de los impuestos en modo batch;
  • la aplicación 3 realizará el cálculo de los impuestos en modo interactivo;

20.1. Aplicación 1: inicialización de la base de datos

La aplicación 1 tendrá la siguiente arquitectura:

Image

Se trata de una evolución de la arquitectura de version 4 (apartado |Version 4|): los datos fiscales se encontrarán en una base de datos en lugar de estar en un archivo jSON. La capa [dao] se actualizará para implementar este cambio.

20.1.1. El archivo [admindata.json]

Image

El archivo [admindata.json] es el mismo que en version 4:


{
    "limites": [9964, 27519, 73779, 156244, 0],
    "coeffr": [0, 0.14, 0.3, 0.41, 0.45],
    "coeffn": [0, 1394.96, 5798, 13913.69, 20163.45],
    "plafond_qf_demi_part": 1551,
    "plafond_revenus_celibataire_pour_reduction": 21037,
    "plafond_revenus_couple_pour_reduction": 42074,
    "valeur_reduc_demi_part": 3797,
    "plafond_decote_celibataire": 1196,
    "plafond_decote_couple": 1970,
    "plafond_impot_couple_pour_decote": 2627,
    "plafond_impot_celibataire_pour_decote": 1595,
    "abattement_dixpourcent_max": 12502,
    "abattement_dixpourcent_min": 437
}

Vamos a utilizar como columnas de la base de datos las claves de este diccionario.

20.1.2. Creación de las bases de datos

Tal y como se ha mostrado en el apartado |creación de una base de datos MySQL|, creamos una base de datos MySQL denominada [dbimpots-2019], propiedad del usuario [admimpots] con contraseña [mdpimpots]. En [phpMyAdmin] esto da como resultado lo siguiente:

Image

Del mismo modo, tal y como se ha mostrado en el apartado |creación de una base de datos PostgreSQL|, creamos una base de datos PostgreSQL denominada [dbimpots-2019], propiedad del usuario [admimpots] con la contraseña [mdpimpots]. En [pgAdmin] esto da como resultado lo siguiente:

Image

Las bases de datos se han creado, pero por el momento no tienen ninguna tabla. Estas serán creadas por ORM y [sqlalchemy].

20.1.3. Las entidades mapeadas por [sqlalchemy]

Vamos a crear dos tablas para encapsular los datos de [admindata.json]:

Definida por [sqlalchemy], la tabla [tbtranches] recopilará los datos de las tablas [limites, coeffr, coeffn] del diccionario [admindata.json]:


    # la table des tranches de l'impôt
    tranches_table = Table("tbtranches", metadata,
                           Column('id', Integer, primary_key=True),
                           Column('limite', Float, nullable=False),
                           Column('coeffr', Float, nullable=False),
                           Column('coeffn', Float, nullable=False)
                           )

Definida por [sqlalchemy], la tabla [tbconstantes] recopilará las constantes del diccionario [admindata.json]:


    # la table des constantes
    constantes_table = Table("tbconstantes", metadata,
                             Column('id', Integer, primary_key=True),
                             Column('plafond_qf_demi_part', Float, nullable=False),
                             Column('plafond_revenus_celibataire_pour_reduction', Float, nullable=False),
                             Column('plafond_revenus_couple_pour_reduction', Float, nullable=False),
                             Column('valeur_reduc_demi_part', Float, nullable=False),
                             Column('plafond_decote_celibataire', Float, nullable=False),
                             Column('plafond_decote_couple', Float, nullable=False),
                             Column('plafond_impot_celibataire_pour_decote', Float, nullable=False),
                             Column('plafond_impot_couple_pour_decote', Float, nullable=False),
                             Column('abattement_dixpourcent_max', Float, nullable=False),
                             Column('abattement_dixpourcent_min', Float, nullable=False)
                             )

Las entidades que se asignarán a estas dos tablas serán las siguientes:

Image

La entidad [Constantes] encapsula las constantes del diccionario [admindata.json]:


from BaseEntity import BaseEntity


# clase contenedora de los datos de la administración tributaria
class Constantes(BaseEntity):
    # claves excluidas del estado de la clase
    excluded_keys = ["_sa_instance_state"]

    # claves autorizadas
    @staticmethod
    def get_allowed_keys() -> list:
        return ["id",
                "plafond_qf_demi_part",
                "plafond_revenus_celibataire_pour_reduction",
                "plafond_revenus_couple_pour_reduction",
                "valeur_reduc_demi_part",
                "plafond_decote_celibataire",
                "plafond_decote_couple",
                "plafond_decote_couple",
                "plafond_impot_celibataire_pour_decote",
                "plafond_impot_couple_pour_decote",
                "abattement_dixpourcent_max",
                "abattement_dixpourcent_min"]
  • línea 5: la clase [Constantes] extiende la clase [BaseEntity];
  • línea 7: mediante la asignación [sqlalchemy], la clase [Constante] recibirá la propiedad [_sa_instance_state]. La excluimos del diccionario [asdict] de la entidad;
  • líneas 11-23: las propiedades de la entidad. Se han retomado los nombres utilizados en el diccionario [admindata.json] para facilitar la escritura del código;

La entidad [Tranche] encapsula una línea de las tres tablas [limites, coeffr, coeffn] del diccionario [admindata.json]:


from BaseEntity import BaseEntity


# clase contenedora de los datos de la administración tributaria
class Tranche(BaseEntity):
    # claves excluidas del informe de la clase
    excluded_keys = ["_sa_instance_state"]

    # claves autorizadas
    @staticmethod
    def get_allowed_keys() -> list:
        return ["id", "limite", "coeffr", "coeffn"]
  • línea 5: la clase [Tranche] extiende la clase [BaseEntity];
  • línea 7: se excluye de las propiedades del diccionario [asdict] de la entidad la propiedad [_sa_instance_state] añadida por [sqlalchemy];
  • líneas 10-12: las propiedades de la clase;

La correspondencia entre las entidades [Constantes, Tranche] y las tablas [constantes, tranches] será la siguiente:

Image



    # la tabla de constantes
    constantes_table = Table("tbconstantes", metadata,
                             Column('id', Integer, primary_key=True),
                             Column('plafond_qf_demi_part', Float, nullable=False),
                             Column('plafond_revenus_celibataire_pour_reduction', Float, nullable=False),
                             Column('plafond_revenus_couple_pour_reduction', Float, nullable=False),
                             Column('valeur_reduc_demi_part', Float, nullable=False),
                             Column('plafond_decote_celibataire', Float, nullable=False),
                             Column('plafond_decote_couple', Float, nullable=False),
                             Column('plafond_impot_celibataire_pour_decote', Float, nullable=False),
                             Column('plafond_impot_couple_pour_decote', Float, nullable=False),
                             Column('abattement_dixpourcent_max', Float, nullable=False),
                             Column('abattement_dixpourcent_min', Float, nullable=False)
                             )

    # tabla de tramos del impuesto
    tranches_table = Table("tbtranches", metadata,
                           Column('id', Integer, primary_key=True),
                           Column('limite', Float, nullable=False),
                           Column('coeffr', Float, nullable=False),
                           Column('coeffn', Float, nullable=False)
                           )
    # las asignaciones
    from Tranche import Tranche
    mapper(Tranche, tranches_table)

    from Constantes import Constantes
    mapper(Constantes, constantes_table)
  • Las asignaciones se realizan en las líneas 24-29. Se ha omitido establecer las correspondencias entre las propiedades de las entidades asignadas y las tablas de la base de datos. Esto es posible cuando los nombres de las columnas de las tablas son los mismos que los de las propiedades a las que deben asociarse. Por este motivo, hemos incluido en las tablas los nombres de las propiedades de las entidades asignadas. Esto facilita la escritura del código y su comprensión;

20.1.4. El archivo de configuración de [sqlalchemy]

Image

Acabamos de detallar una parte de la configuración de [sqlalchemy]. El archivo [config_database] en su totalidad es el siguiente:


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

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

    # metadatos
    metadata = MetaData()

    # la tabla de constantes
    constantes_table = Table("tbconstantes", metadata,
                             Column('id', Integer, primary_key=True),
                             Column('plafond_qf_demi_part', Float, nullable=False),
                             Column('plafond_revenus_celibataire_pour_reduction', Float, nullable=False),
                             Column('plafond_revenus_couple_pour_reduction', Float, nullable=False),
                             Column('valeur_reduc_demi_part', Float, nullable=False),
                             Column('plafond_decote_celibataire', Float, nullable=False),
                             Column('plafond_decote_couple', Float, nullable=False),
                             Column('plafond_impot_celibataire_pour_decote', Float, nullable=False),
                             Column('plafond_impot_couple_pour_decote', Float, nullable=False),
                             Column('abattement_dixpourcent_max', Float, nullable=False),
                             Column('abattement_dixpourcent_min', Float, nullable=False)
                             )

    # tabla de tramos impositivos
    tranches_table = Table("tbtranches", metadata,
                           Column('id', Integer, primary_key=True),
                           Column('limite', Float, nullable=False),
                           Column('coeffr', Float, nullable=False),
                           Column('coeffn', Float, nullable=False)
                           )
    # las asignaciones
    from Tranche import Tranche
    mapper(Tranche, tranches_table)

    from Constantes import Constantes
    mapper(Constantes, constantes_table)

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

    # una sesión
    session = session_factory()

    # se registra cierta información
    config['database'] = {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
                          "constantes_table": constantes_table, "session": session}

    # resultado
    return config
  • línea 1: la función [configure] recibe como parámetro un diccionario cuya clave [sgbd] le indica qué SGBD debe utilizar: MySQL (mysql) o PostgreSQL (pgres);
  • líneas 6-12: se selecciona la base de datos solicitada por la configuración;
  • líneas 14-44: asignaciones de entidades/tablas. Estas asignaciones son sencillas, ya que no existe ningún vínculo entre las tablas [tranches] y [constantes]. Son independientes. Por lo tanto, no hay que gestionar ninguna clave externa de una sobre la otra;
  • líneas 46-51: se crea la sesión de trabajo [session] de la aplicación;
  • líneas 53-58: la información útil se introduce en el diccionario de configuración y este se devuelve;

20.1.5. La capa [dao]

Volvamos a la arquitectura de la aplicación 1 que hay que construir:

Image

La capa [dao] [1] debe leer el archivo [admindata.json] [2] y transferir su contenido a una de las bases [3, 4];

Image

La capa [dao] presenta la interfaz [1] y está implementada por la clase [2].

La interfaz [InterfaceDao4TransferAdminData2Database] es la siguiente:


# importaciones
from abc import ABC, abstractmethod


# interfaz InterfaceImpôtsUI
class InterfaceDao4TransferAdminData2Database(ABC):
    # transferencia de datos fiscales a una base de datos
    @abstractmethod
    def transfer_admindata_in_database(self:object):
        pass
  • líneas 8-10: la interfaz solo presenta un método [transfer_admindata_in_database] sin parámetros. Dado que este método necesita parámetros (¿qué archivo?, ¿qué base de datos?), esto significa que estos se pasarán al constructor de las clases que implementan esta interfaz;

La clase [DaoTransferAdminDataFromJsonFile2Database] implementa la interfaz [InterfaceDao4TransferAdminData2Database] de la siguiente manera:


# importaciones
import codecs
import json

from sqlalchemy.exc import DatabaseError, IntegrityError, InterfaceError

from Constantes import Constantes
from ImpôtsError import ImpôtsError
from InterfaceDao4TransferAdminData2Database import InterfaceDao4TransferAdminData2Database
from Tranche import Tranche


class DaoTransferAdminDataFromJsonFile2Database(InterfaceDao4TransferAdminData2Database):

    # fabricante
    def __init__(self, config: dict):
        self.config = config

    # transferencia
    def transfer_admindata_in_database(self) -> None:
        # inicializaciones
        session = None
        config = self.config

        try:
            # se recuperan los datos de la administración tributaria
            with codecs.open(config["admindataFilename"], "r", "utf8") as fd:
                # transferencia del contenido a un diccionario
                admindata = json.load(fd)

            # se recupera la configuración de la base de datos
            database = config["database"]

            # eliminación de las dos tablas de la base de datos
            # checkfirst=True: comprueba primero si la tabla existe
            database["tranches_table"].drop(database["engine"], checkfirst=True)
            database["constantes_table"].drop(database["engine"], checkfirst=True)

            # recreación de las tablas a partir de las asignaciones
            database["metadata"].create_all(database["engine"])

            # la sesión [sqlalchemy] actual
            session = database["session"]

            # se rellena la tabla de tramos del impuesto
            limites = admindata["limites"]
            coeffr = admindata["coeffr"]
            coeffn = admindata["coeffn"]
            for i in range(len(limites)):
                session.add(Tranche().fromdict(
                    {"limite": limites[i], "coeffr": coeffr[i], "coeffn": coeffn[i]}))
            # se rellena la tabla de constantes
            session.add(Constantes().fromdict({
                'plafond_qf_demi_part': admindata["plafond_qf_demi_part"],
                'plafond_revenus_celibataire_pour_reduction': admindata["plafond_revenus_celibataire_pour_reduction"],
                'plafond_revenus_couple_pour_reduction': admindata["plafond_revenus_couple_pour_reduction"],
                'valeur_reduc_demi_part': admindata["valeur_reduc_demi_part"],
                'plafond_decote_celibataire': admindata["plafond_decote_celibataire"],
                'plafond_decote_couple': admindata["plafond_decote_couple"],
                'plafond_impot_celibataire_pour_decote': admindata["plafond_impot_celibataire_pour_decote"],
                'plafond_impot_couple_pour_decote': admindata["plafond_impot_couple_pour_decote"],
                'abattement_dixpourcent_max': admindata["abattement_dixpourcent_max"],
                'abattement_dixpourcent_min': admindata["abattement_dixpourcent_min"]
            }))

            # validación de la sesión [sqlalchemy]
            session.commit()
        except (IntegrityError, DatabaseError, InterfaceError) as erreur:
            # Se vuelve a lanzar la excepción de otra forma
            raise ImpôtsError(17, f"{erreur}")
        finally:
            # se liberan los recursos de la sesión
            if session:
                session.close()
  • línea 13: la clase [DaoTransferAdminDataFromJsonFile2Database] implementa la interfaz [InterfaceDao4TransferAdminData2Database];
  • líneas 15-17: el constructor de la clase recibe como parámetro el diccionario de configuración. Se utilizarán las siguientes claves:
    • [admindataFilename] (línea 27): el nombre del archivo jSON que contiene los datos de la administración tributaria que se van a transferir a la base de datos;
    • [database] línea 32: la configuración [sqlalchemy] de la aplicación;
  • líneas 34-37: eliminación de las tablas [constantes] y [tranches] si existen;
  • líneas 39-40: recreación de las dos tablas;
  • línea 43: se recupera la sesión [sqlalchemy] presente en la configuración;
  • líneas 45-51: las tablas [limites, coeffr, coeffn] del diccionario [admindata] se colocan en la sesión. Para ello, se colocan en la sesión instancias de la entidad [Tranche];
  • líneas 52-64: se introduce en la sesión una instancia de la entidad [Constantes];
  • líneas 66-67: se valida la sesión. Si los datos de la sesión aún no estaban en la base de datos, se introducen en ese momento;
  • líneas 68-70: gestión de un posible error;
  • líneas 71-74: se cierra la sesión. Esto es posible porque la capa [dao] solo se utiliza una vez;

20.1.6. Configuración de la aplicación

Image

La aplicación se configura mediante tres archivos [1]:

  • [config] es el archivo de configuración general. Es el que configura la aplicación [main]. Para ello, se ayuda de los otros dos archivos:
    • [config_database], que ya hemos analizado y que configura ORM y [sqlalchemy];
    • [config_layers], que configura las capas de la aplicación;

El archivo [config] es el siguiente:


def configure(config: dict) -> dict:
    # [config] tiene la clave [sgbd] que vale:
    # [mysql] para gestionar una base MySQL
    # [pgres] para gestionar una base PostgreSQL

    import os

    # paso 1 ---
    # se configura el Python Path de la aplicación

    # ruta absoluta de la carpeta de este script
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # root_dir (a cambiar si es necesario)
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # rutas absolutas de las dependencias
    absolute_dependencies = [
        # 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",
        # AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # carpetas locales
        f"{script_dir}",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{script_dir}/../../entities",
    ]

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

    # paso 2 ------
    # se completa la configuración de la aplicación
    config.update({
        # rutas absolutas de los archivos de datos
        "admindataFilename"f"{script_dir}/../../data/input/admindata.json"
    })

    # paso 3 ------
    # Configuración de la base de datos
    import config_database
    config = config_database.configure(config)

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

    # se convierte en config
    return config
  • líneas 8-36: se compila el Python Path de la aplicación;
  • líneas 38-43: se introduce en la configuración la ruta del archivo [admindata.json];
  • líneas 45-48: configuración de [sqlalchemy];
  • líneas 50-53: instanciación de las capas de la aplicación;
  • línea 56: se devuelve la configuración general;

El archivo [config_layers] es el siguiente:


def configure(config: dict) -> dict:
    # instanciación de la capa [dao]
    from DaoTransferAdminDataFromJsonFile2Database import DaoTransferAdminDataFromJsonFile2Database
    config['dao'] = DaoTransferAdminDataFromJsonFile2Database(config)

    # se devuelve config
    return config
  • líneas 3-4: instanciación de la capa [dao]. Hemos visto que el constructor de la clase [DaoTransferAdminDataFromJsonFile2Database] esperaba como parámetro el diccionario de la configuración general de la aplicación;
  • línea 4: la referencia a la capa [dao] se incluye en la configuración;
  • línea 7: se devuelve la configuración;

20.1.7. El script [main] de la aplicación

Image

Image

El script principal [main] es el siguiente:


# se espera un parámetro mysql o pgres
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

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

# se establece la ruta del sistema (syspath); ya se pueden realizar las importaciones
from ImpôtsError import ImpôtsError

# se recupera la capa [dao]
dao = config["dao"]

# código
try:
    # se transfieren los datos a la base
    dao.transfer_admindata_in_database()
except ImpôtsError as ex1:
    # se muestra el error
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # se muestra el error
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # fin
    print("Terminé...")
  • líneas 1-10: se espera un parámetro. Se comprueba que esté presente y sea correcto;
  • líneas 12-14: se configura la aplicación (general, SQLAlchemy, capas) pasando como parámetro el tipo de SGBD elegido;
  • líneas 19-20: vamos a necesitar la capa [dao]. La recuperamos;
  • línea 25: se realiza la transferencia a la base de datos. Toda la información necesaria para el método [transfer_admindata_in_database] está disponible en las propiedades de la capa [dao] de la línea 20. Es ahí donde la buscará;

Tras la ejecución con la base MySQL, esta contiene los siguientes elementos (phpMyAdmin):

Image

Image

Image

En la columna [3] se ven los valores asignados por MySQL a la clave primaria [id]. La numeración comienza en 1. La captura de pantalla anterior se obtuvo tras varias ejecuciones del script.

Image

Image

Con la base PostgreSQL, los resultados son los siguientes:

Image

  • Hacemos clic con el botón derecho en [1] y, a continuación, en [2-3];
  • en [4], se ven correctamente los datos de los tramos impositivos;

Repetimos el mismo proceso para la tabla de constantes [tbconstantes]:

Image

Image

Image

20.2. Aplicación 2: cálculo del impuesto en modo batch

Image

20.2.1. Arquitectura

La aplicación de cálculo de impuestos de version 4 utilizaba la siguiente arquitectura:

Image

La capa [dao] implementa una interfaz [InterfaceImpôtsDao]. Hemos creado una clase que implementa esta interfaz:

  • [ImpôtsDaoWithAdminDataInJsonFile], que recuperaba los datos fiscales de un archivo jSON. Se trataba de la version 3;

Vamos a implementar la interfaz [InterfaceImpôtsDao] mediante una nueva clase [ImpotsDaoWithTaxAdminDataInDatabase] que recuperará los datos de la administración tributaria de una base de datos. La capa [dao], al igual que antes, escribirá los resultados en un archivo jSON y buscará los datos de los contribuyentes en un archivo de texto. Sabemos que si seguimos respetando la interfaz [InterfaceImpôtsDao], la capa [métier] no tendrá que modificarse.

La nueva arquitectura será la siguiente:

Image

20.2.2. Configuración de la aplicación

Image

El archivo de configuración [config_database] sigue siendo el mismo que en la aplicación 1. La configuración [config] incorpora nuevos elementos:


    # étape 2 ------
    # on complète la configuration de l'application
    config.update({
        # chemins absolus des fichiers de données
        "admindataFilename": f"{script_dir}/../../data/input/admindata.json",
        "taxpayersFilename": f"{script_dir}/../../data/input/taxpayersdata.txt",
        "errorsFilename": f"{script_dir}/../../data/output/errors.txt",
        "resultsFilename": f"{script_dir}/../../data/output/résultats.json"
    })
  • líneas 6-8: las rutas absolutas de los archivos de texto utilizados por la aplicación 2;

La configuración de las capas [config_layers] evoluciona de la siguiente manera:


def configure(config: dict) -> dict:
    # instanciación de la capa dao
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    config["dao"] = ImpotsDaoWithAdminDataInDatabase(config)

    # instanciación de capa [métier]
    from ImpôtsMétier import ImpôtsMétier
    config['métier'] = ImpôtsMétier()

    # se devuelve el config
    return config
  • líneas 3-4: la capa [dao] ahora se implementa mediante la clase [ImpotsDaoWithAdminDataInDatabase]. Esta clase es nueva, pero implementa la misma interfaz [InterfaceDao] que la version 4 del ejercicio de aplicación;
  • líneas 7-8: la capa [métier] está implementada por la clase [ImpôtsMétier]. Es la clase utilizada en la version 4 del ejercicio de aplicación;

20.2.3. La capa [dao]

La clase de implementación [ImpotsDaoWithAdminDataInDatabase] de la interfaz [InterfaceImpôtsDao] será la siguiente:


# importaciones
from sqlalchemy.exc import DatabaseError, IntegrityError, InterfaceError

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from Constantes import Constantes
from ImpôtsError import ImpôtsError
from Tranche import Tranche


class ImpotsDaoWithAdminDataInDatabase(AbstractImpôtsDao):
    # constructor
    def __init__(self, config: dict):
        # config["taxPayersFilename"]: el nombre del archivo de texto de los contribuyentes
        # config["taxPayersResultsFilename"]: el nombre del archivo jSON de los resultados
        # config["errorsFilename"]: registra los errores encontrados en taxPayersFilename
        # config["database"]: configuración de la base de datos

        # inicialización de la clase Parent
        AbstractImpôtsDao.__init__(self, config)
        # almacenamiento de parámetros
        self.__config = config
        # admindata
        self.__admindata = None

    # implementación de la interfaz
    def get_admindata(self):
        # ¿Se ha memorizado admindata?
        if self.__admindata:
            return self.__admindata
        # se realiza una consulta en BD
        session = None
        config = self.__config
        try:
            # una sesión
            database_config = config["database"]
            session = database_config["session"]

            # se lee la tabla de tramos del impuesto
            tranches = session.query(Tranche).all()

            # se lee la tabla de constantes (una sola línea)
            constantes = session.query(Constantes).first()

            # se crea la instancia admindata
            admindata = AdminData()
            # se crean en ella las tablas de límites, coeffR, coeffN
            limites = admindata.limites = []
            coeffr = admindata.coeffr = []
            coeffn = admindata.coeffn = []
            for tranche in tranches:
                limites.append(float(tranche.limite))
                coeffr.append(float(tranche.coeffr))
                coeffn.append(float(tranche.coeffn))
            # se añaden las constantes
            admindata.fromdict(constantes.asdict())
            # se guarda admindata
            self.__admindata = admindata
            # se devuelve el valor
            return self.__admindata
        except (IntegrityError, DatabaseError, InterfaceError) as erreur:
            # se vuelve a lanzar la excepción de otra forma
            raise ImpôtsError(27, f"{erreur}")
        finally:
            # se cierra la sesión
            if session:
                session.close()

Notas

  • línea 11: la clase [ImpotsDaoWithAdminDataInDatabase] hereda de la clase [AbstractImpôtsDao] presentada en la version 4. Sabemos que esta última implementa la interfaz [InterfaceDao] presentada en esta misma version. Es el cumplimiento de esta interfaz lo que nos permite no cambiar la capa [métier];
  • línea 13: el constructor de la clase recibe como parámetro el diccionario de configuración de la aplicación;
  • línea 20: se inicializa la clase padre []. Esta implementa parcialmente la interfaz [InterfaceDao]:
    • [get_taxpayers_data] lee el archivo [taxpayersdata.txt], que contiene los datos de los contribuyentes;
    • [write_taxpayers_results] escribe los resultados en el archivo jSON [résultats.json];
    • [get_admindata] no está implementado;
  • línea 22: se almacena la configuración pasada como parámetros;
  • línea 27: implementación del método [get_admindata] de la interfaz [InterfaceDao]:
  • líneas 28-30: el método [get_admindata] recupera los datos de la administración tributaria en un objeto de tipo [AdminData] y almacena este objeto en [self.__admindata]. Si se llama al método [get_admindata] varias veces, no se consulta la base de datos varias veces. Solo se consulta la primera vez. Las veces siguientes, se devuelve el objeto [self.__admindata];
  • líneas 36-37: se recupera la sesión [sqlalchemy] que se creó durante la configuración de la aplicación mediante [config_database];
  • líneas 40: se recuperan los tramos del impuesto en una lista;
  • líneas 43: se recuperan las constantes del cálculo del impuesto;
  • línea 46: se crea una instancia de la clase [AdminData]. Recordemos que deriva de [BaseEntity];
  • líneas 48-54: se inicializan las tablas [limites, coeffr, coeffn] de la instancia [AdminData];
  • líneas 55-56: se inicializan las demás propiedades de [AdminData] con las constantes del cálculo del impuesto. Se ha tenido cuidado de dar los mismos nombres a las propiedades de las clases [AdminData] y [Constantes], lo que simplifica el código;
  • líneas 57-58: la instancia [AdminData] se almacena en la capa [dao] para devolverla en las próximas llamadas al método [get_admindata];
  • línea 60: se devuelve el valor solicitado por el código llamante;
  • líneas 61-63: gestión de un posible error;
  • líneas 64-67: la base de datos solo es objeto de una única consulta. Por lo tanto, se puede cerrar la sesión [sqlalchemy];

20.2.4. Prueba de la capa [dao]

En la version 4 de esta aplicación, habíamos creado una clase de prueba de la capa [métier]. Más concretamente, esta clase probaba tanto las capas [métier] como [dao]. Retomamos esta prueba para verificar que la capa [dao] funciona según lo esperado. De hecho, la capa [métier] no cambia.

Image

Image

La prueba [TestDaoMétier] es la siguiente:


import unittest


class TestDaoMétier(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})
        métier.calculate_tax(taxpayer, admindata)
        # verificación
        self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    

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

        # {'casado': 'sí', 'hijos': 3, 'salario': 200000,
        # 'impuesto': 42842, 'recargo': 17283, 'descuento': 0, 'reducción': 0, 'tipo': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        métier.calculate_tax(taxpayer, admindata)
        # verificaciones
        self.assertAlmostEqual(taxpayer.impôt, 42842, 1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 17283, delta=1)


if __name__ == '__main__':
    # se espera un parámetro mysql o pgres
    import sys
    syntaxe = f"{sys.argv[0]} mysql / pgres"
    erreur = len(sys.argv) != 2
    if not erreur:
        sgbd = sys.argv[1].lower()
        erreur = sgbd != "mysql" and sgbd != "pgres"
    if erreur:
        print(f"syntaxe : {syntaxe}")
        sys.exit()

    # se está configurando la aplicación
    import config
    config = config.configure({'sgbd': sgbd})
    # capa de negocio
    métier = config['métier']
    try:
        # datos de administración
        admindata = config['dao'].get_admindata()
    except BaseException as ex:
        # visualización
        print((f"L'erreur suivante s'est produite : {ex}"))
        # fin
        sys.exit()
    # se envía el parámetro recibido por el script
    sys.argv.pop()
    # se ejecutan los métodos de prueba
    print("tests en cours...")
   unittest.main()
  • No volveremos sobre las 11 pruebas descritas en el apartado |prueba de capa [métier] version 4|;
  • líneas 37-66: vamos a ejecutar el script de pruebas como una aplicación normal y no como una prueba UnitTest. Es la línea 66 la que activará el framework UnitTest. En las pruebas anteriores, utilizábamos el método [setUp] para configurar la ejecución de cada prueba. Repetíamos 11 veces la misma configuración, ya que la función [setUp] se ejecuta antes de cada prueba. Aquí, realizamos la configuración una sola vez. Consiste en definir las variables globales [métier] en la línea 53 y [admindata] en la línea 56, que luego serán utilizadas por los métodos de [TestDaoMétier], por ejemplo, en la línea 12;
  • líneas 39-47: el script de prueba espera un parámetro [mysql / pgres] que indica si se utiliza una base MySQL o PostgreSQL;
  • líneas 50-51: se configura la prueba;
  • línea 53: se recupera la capa [métier] de la configuración;
  • línea 56: se hace lo mismo con la capa [dao]. A continuación, se recupera la instancia [admindata] que encapsula los datos necesarios para el cálculo del impuesto;
  • las pruebas han demostrado que el método [unittest.main()] de la línea 66 no ignoraba el parámetro [mysql / pgres] recibido por el script, sino que le daba un significado diferente. La línea 63 hace que este método ya no tenga ningún parámetro;

Creamos dos configuraciones de ejecución:

Image

Image

Si ejecutamos una de estas dos configuraciones, obtenemos los siguientes resultados:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/v05/tests/TestDaoMétier.py mysql
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 0.001s

OK

Process finished with exit code 0
  • líneas 5 y 7: las 11 pruebas se han superado;

Recordemos que estas pruebas solo verifican 11 casos del cálculo del impuesto. No obstante, su éxito puede bastar para que confiemos en la capa [dao].

20.2.5. El script principal

Image

Image

El script principal [main] es el mismo que en el version 4:


# se espera un parámetro mysql o pgres
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

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

# se establece la ruta del sistema (syspath); ya se pueden realizar las importaciones
from ImpôtsError import ImpôtsError

# se recuperan las capas de la aplicación (ya están instanciadas)
dao = config["dao"]
métier = config["métier"]

try:
    # recuperación de los tramos del impuesto
    admindata = dao.get_admindata()
    # lectura de los datos de los contribuyentes
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # ¿de los contribuyentes?
    if not taxpayers:
        raise ImpôtsError(57, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # cálculo del impuesto de los contribuyentes
    for taxPayer in taxpayers:
        # taxPayer es tanto un parámetro de entrada como de salida
        # taxPayer va a ser modificado
        métier.calculate_tax(taxPayer, admindata)
    # escritura de los resultados en un archivo de texto
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
    # Visualización del error
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # finalizado
    print("Travail terminé...")

Notas

  • líneas 1-10: se recupera el parámetro [mysql / pgres] que indica el SGBD que se debe utilizar;
  • líneas 12-14: se configura la aplicación;
  • líneas 16-17: se importa la clase [ImpôtsError]. La necesitamos en la línea 38;
  • líneas 19-21: se recuperan referencias de las capas de la aplicación;
  • línea 25: se solicitan a la capa [dao] los datos de la administración tributaria. La capa [métier] los necesita para el cálculo del impuesto;
  • línea 27: se recuperan en una lista los datos (id, estado civil, hijos, salario) de los contribuyentes;
  • líneas 29-30: si esta lista está vacía, se lanza una excepción;
  • líneas 32-35: cálculo del impuesto de los elementos de la lista [taxpayers];
  • línea 37: escritura de los resultados en el archivo jSON[résultats.json];
  • líneas 38-40: gestión de posibles errores;

Para la ejecución del script, se crean dos |configuraciones de ejecución|:

Image

Los resultados obtenidos en el archivo [résultats.json] son los de version 4.

Image

20.3. Aplicación 3: cálculo del impuesto en modo interactivo

Ahora presentamos la aplicación que permite calcular el impuesto de forma interactiva. Se trata de una adaptación de la aplicación 2 de version 4.

Image

Image

  • el script [main] inicia el diálogo con el usuario mediante el método [ui.run] de la capa [ui];
  • la capa [ui]:
    • utiliza la capa [dao] para obtener los datos necesarios para calcular el impuesto;
    • solicita al usuario la información relativa al contribuyente cuyo impuesto se desea calcular;
    • utiliza la capa [métier] para realizar dicho cálculo;

El archivo [config_layers] instancia una capa adicional:


def configure(config: dict) -> dict:
    # instanciación de la capa dao
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    config["dao"] = ImpotsDaoWithAdminDataInDatabase(config)

    # instanciación de capa [métier]
    from ImpôtsMétier import ImpôtsMétier
    config['métier'] = ImpôtsMétier()

    # ui
    from ImpôtsConsole import ImpôtsConsole
    config['ui'] = ImpôtsConsole(config)

    # se devuelve config
    return config

La clase [ImpôtsConsole], líneas 11-12, es la misma que en la |version 4|.

El script principal [main] es el siguiente:


# se espera un parámetro mysql o pgres
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

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

# el syspath está configurado; se pueden realizar las importaciones
from ImpôtsError import ImpôtsError

# se recupera la capa [ui]
ui = config["ui"]

# código
try:
    # ejecución de la capa [ui]
    ui.run()
except ImpôtsError as ex1:
    # se muestra el mensaje de error
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # se muestra el mensaje de error
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # ejecutado en todos los casos
    print("Travail terminé...")
  • líneas 1-10: el script espera un parámetro [mysql / pgres] que indique el SGBD que se debe utilizar;
  • líneas 12-14: se configura la aplicación;
  • líneas 19-20: se recupera la capa [ui] de la configuración;
  • línea 25: se ejecuta;

Los resultados son idénticos a los de la |version 4|. No podía ser de otra manera, ya que todas las interfaces de la version 4 se han respetado en la version 5.