Skip to content

20. Exercício prático: versão 5

Image

Vamos desenvolver três aplicações:

  • a aplicação 1 irá inicializar a base de dados que substituirá o ficheiro [admindata.json] da versão 4;
  • a aplicação 2 fará o cálculo dos impostos em modo batch;
  • a aplicação 3 fará o cálculo dos impostos em modo interativo;

20.1. Aplicação 1: inicialização da base de dados

A aplicação 1 terá a seguinte arquitetura:

Image

Trata-se de uma evolução da arquitetura da versão 4 (parágrafo |Versão 4|): os dados fiscais serão encontrados numa base de dados, em vez de se encontrarem num ficheiro jSON. A camada [dao] será atualizada para implementar esta alteração.

20.1.1. O ficheiro [admindata.json]

Image

O ficheiro [admindata.json] é o mesmo que existia na versão 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 utilizar como colunas da base de dados as chaves deste dicionário.

20.1.2. Criação das bases de dados

Tal como foi demonstrado no parágrafo |criação de uma base de dados MySQL|, criamos uma base de dados MySQL denominada [dbimpots-2019], propriedade do utilizador [admimpots], com a palavra-passe [mdpimpots]. Em [phpMyAdmin], o resultado é o seguinte:

Image

Da mesma forma, tal como foi demonstrado no parágrafo |criação de uma base de dados PostgreSQL|, criamos uma base de dados PostgreSQL denominada [dbimpots-2019], propriedade do utilizador [admimpots], com a palavra-passe [mdpimpots]. Em [pgAdmin], o resultado é o seguinte:

Image

As bases de dados são criadas, mas, por enquanto, não têm nenhuma tabela. Estas serão criadas pelo ORM e pelo [sqlalchemy].

20.1.3. As entidades mapeadas pelo [sqlalchemy]

Vamos criar duas tabelas para encapsular os dados do [admindata.json]:

Definida por [sqlalchemy], a tabela [tbtranches] reunirá os dados das tabelas [limites, coeffr, coeffn] do dicionário [admindata.json]:


    # a tabela de escalões de imposto
    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 pela tabela [sqlalchemy], a tabela [tbconstantes] irá reunir as constantes do dicionário [admindata.json]:


    # a tabela 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)
                             )

As entidades que serão mapeadas com estas duas tabelas serão as seguintes:

Image

A entidade [Constantes] encapsula as constantes do dicionário [admindata.json]:


from BaseEntity import BaseEntity


# classe-recipiente dos dados da administração fiscal
class Constantes(BaseEntity):
    # chaves excluídas do estado da classe
    excluded_keys = ["_sa_instance_state"]

    # chaves 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"]
  • linha 5: a classe [Constantes] estende a classe [BaseEntity];
  • linha 7: através do mapeamento [sqlalchemy], a classe [Constante] irá receber a propriedade [_sa_instance_state]. Excluímos-a do dicionário [asdict] da entidade;
  • linhas 11-23: as propriedades da entidade. Retomámos os nomes utilizados no dicionário [admindata.json] para facilitar a escrita do código;

A entidade [Tranche] encapsula uma linha das três tabelas [limites, coeffr, coeffn] do dicionário [admindata.json]:


from BaseEntity import BaseEntity


# classe-recipiente dos dados da administração fiscal
class Tranche(BaseEntity):
    # chaves excluídas do estado da classe
    excluded_keys = ["_sa_instance_state"]

    # chaves autorizadas
    @staticmethod
    def get_allowed_keys() -> list:
        return ["id", "limite", "coeffr", "coeffn"]
  • linha 5: a classe [Tranche] estende a classe [BaseEntity];
  • linha 7: exclui-se das propriedades do dicionário [asdict] da entidade a propriedade [_sa_instance_state] adicionada por [sqlalchemy];
  • linhas 10-12: as propriedades da classe;

O mapeamento entre as entidades [Constantes, Tranche] e as tabelas [constantes, tranches] será o seguinte:

Image



    # a tabela 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)
                             )

    # a tabela das faixas de imposto
    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)
                           )
    # os mapeamentos
    from Tranche import Tranche
    mapper(Tranche, tranches_table)

    from Constantes import Constantes
    mapper(Constantes, constantes_table)
  • os mapeamentos são definidos nas linhas 24-29. Nestas, omitiu-se estabelecer as correspondências entre as propriedades das entidades mapeadas e as tabelas da base de dados. Tal é possível quando os nomes das colunas das tabelas são idênticos aos das propriedades às quais devem ser associadas. Por este motivo, incluímos nas tabelas os nomes das propriedades das entidades mapeadas. Isto facilita a escrita do código e a sua compreensão;

20.1.4. O ficheiro de configuração do [sqlalchemy]

Image

Acabámos de detalhar uma parte da configuração do [sqlalchemy]. O ficheiro [config_database] na sua totalidade é o seguinte:


def configure(config: dict) -> dict:
    # configuração do SQLAlchemy
    from sqlalchemy import create_engine, Table, Column, Integer, MetaData, Float
    from sqlalchemy.orm import mapper, sessionmaker

    # cadeias de ligação às bases de dados utilizadas
    connection_strings = {
        'mysql': "mysql+mysqlconnector://admimpots:mdpimpots@localhost/dbimpots-2019",
        'pgres': "postgresql+psycopg2://admimpots:mdpimpots@localhost/dbimpots-2019"
    }
    # cadeia de ligação à base de dados utilizada
    engine = create_engine(connection_strings[config['sgbd']])

    # metadados
    metadata = MetaData()

    # a tabela 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)
                             )

    # a tabela das faixas de imposto
    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)
                           )
    # os mapeamentos
    from Tranche import Tranche
    mapper(Tranche, tranches_table)

    from Constantes import Constantes
    mapper(Constantes, constantes_table)

    # a fábrica de sessões
    session_factory = sessionmaker()
    session_factory.configure(bind=engine)

    # uma sessão
    session = session_factory()

    # registam-se determinadas informações
    config['database'] = {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
                          "constantes_table": constantes_table, "session": session}

    # resultado
    return config
  • linha 1: a função [configure] recebe como parâmetro um dicionário cuja chave [sgbd] indica-lhe qual o SGBD a utilizar: MySQL (mysql) ou PostgreSQL (pgres);
  • linhas 6-12: seleciona-se a base de dados solicitada pela configuração;
  • linhas 14-44: mapeamentos de entidades/tabelas. Estes mapeamentos são simples, pois não existe qualquer ligação entre as tabelas [tranches] e [constantes]. São independentes. Não há, portanto, nenhuma chave estrangeira de uma para a outra para gerir;
  • linhas 46-51: cria-se a sessão de trabalho [session] da aplicação;
  • linhas 53-58: as informações úteis são inseridas no dicionário de configuração, que é devolvido;

20.1.5. A camada [dao]

Voltemos à arquitetura da aplicação 1 a construir:

Image

A camada [dao] [1] deve ler o ficheiro [admindata.json] [2] e transferir o seu conteúdo para uma das bases de dados [3, 4];

Image

A camada [dao] apresenta a interface [1] e é implementada pela classe [2].

A interface [InterfaceDao4TransferAdminData2Database] é a seguinte:


# importações
from abc import ABC, abstractmethod


# interface InterfaceImpôtsUI
class InterfaceDao4TransferAdminData2Database(ABC):
    # transferência de dados fiscais para uma base de dados
    @abstractmethod
    def transfer_admindata_in_database(self:object):
        pass
  • linhas 8-10: a interface apresenta apenas um método [transfer_admindata_in_database] sem parâmetros. Como este método necessita de parâmetros (que ficheiro?, que base de dados?), isso significa que estes serão passados para o construtor das classes que implementam esta interface;

A classe [DaoTransferAdminDataFromJsonFile2Database] implementa a interface [InterfaceDao4TransferAdminData2Database] da seguinte forma:


# importações
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

    # transferência
    def transfer_admindata_in_database(self) -> None:
        # inicializações
        session = None
        config = self.config

        try:
            # recuperação dos dados da administração fiscal
            with codecs.open(config["admindataFilename"], "r", "utf8") as fd:
                # transferência do conteúdo para um dicionário
                admindata = json.load(fd)

            # recuperação da configuração da base de dados
            database = config["database"]

            # eliminação das duas tabelas da base de dados
            # checkfirst=True: verifica primeiro se a tabela existe
            database["tranches_table"].drop(database["engine"], checkfirst=True)
            database["constantes_table"].drop(database["engine"], checkfirst=True)

            # recriação das tabelas a partir dos mapeamentos
            database["metadata"].create_all(database["engine"])

            # a sessão [sqlalchemy] atual
            session = database["session"]

            # preenche-se a tabela das faixas de imposto
            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]}))
            # preenchimento da tabela 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"]
            }))

            # validação da sessão [sqlalchemy]
            session.commit()
        except (IntegrityError, DatabaseError, InterfaceError) as erreur:
            # a exceção é relançada sob outra forma
            raise ImpôtsError(17, f"{erreur}")
        finally:
            # liberam-se os recursos da sessão
            if session:
                session.close()
  • linha 13: a classe [DaoTransferAdminDataFromJsonFile2Database] implementa a interface [InterfaceDao4TransferAdminData2Database];
  • linhas 15-17: o construtor da classe recebe como parâmetro o dicionário de configuração. Serão utilizadas as seguintes chaves:
    • [admindataFilename] (linha 27): o nome do ficheiro jSON que contém os dados da administração fiscal a transferir para a base de dados;
    • [database], linha 32: a configuração [sqlalchemy] da aplicação;
  • linhas 34-37: eliminação das tabelas [constantes] e [tranches], caso existam;
  • linhas 39-40: recriação das duas tabelas;
  • linha 43: recupera-se a sessão [sqlalchemy] presente na configuração;
  • linhas 45-51: as tabelas [limites, coeffr, coeffn] do dicionário [admindata] são inseridas na sessão. Para tal, são inseridas na sessão instâncias da entidade [Tranche];
  • linhas 52-64: uma instância da entidade [Constantes] é colocada na sessão;
  • linhas 66-67: a sessão é validada. Se os dados da sessão ainda não se encontrassem na base de dados, são inseridos nesse momento;
  • linhas 68-70: gestão de um eventual erro;
  • linhas 71-74: a sessão é encerrada. Isto é possível porque a camada [dao] é utilizada apenas uma vez;

20.1.6. Configuração da aplicação

Image

A aplicação é configurada por três ficheiros [1]:

  • [config] é o ficheiro de configuração geral. É este ficheiro que configura a aplicação [main]. Para tal, conta com a ajuda dos outros dois ficheiros:
    • [config_database], que já analisámos e que configura o ORM e o [sqlalchemy];
    • [config_layers], que configura as camadas da aplicação;

O ficheiro [config] é o seguinte:


def configure(config: dict) -> dict:
    # [config] com a chave [sgbd], cujo valor é:
    # [mysql] para gerir uma base de dados MySQL
    # [pgres] para gerir uma base de dados PostgreSQL

    import os

    # passo 1 ---
    # define-se o Python Path da aplicação

    # caminho absoluto da pasta deste script
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # root_dir (a alterar, se necessário)
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # caminhos absolutos das dependências
    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",
        # pastas locais
        f"{script_dir}",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{script_dir}/../../entities",
    ]

    # definir o syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # etapa 2 ------
    # conclui-se a configuração da aplicação
    config.update({
        # caminhos absolutos dos ficheiros de dados
        "admindataFilename"f"{script_dir}/../../data/input/admindata.json"
    })

    # passo 3 ------
    # configuração da base de dados
    import config_database
    config = config_database.configure(config)

    # etapa 4 ------
    # instanciação das camadas da aplicação
    import config_layers
    config = config_layers.configure(config)

    # aplicamos a configuração
    return config
  • linhas 8-36: constrói-se o Python Path da aplicação;
  • linhas 38-43: insere-se na configuração o caminho do ficheiro [admindata.json];
  • linhas 45-48: configuração do ficheiro [sqlalchemy];
  • linhas 50-53: instanciamento das camadas da aplicação;
  • linha 56: devolve-se a configuração geral;

O ficheiro [config_layers] é o seguinte:


def configure(config: dict) -> dict:
    # instanciação da camada [dao]
    from DaoTransferAdminDataFromJsonFile2Database import DaoTransferAdminDataFromJsonFile2Database
    config['dao'] = DaoTransferAdminDataFromJsonFile2Database(config)

    # retornamos a configuração
    return config
  • linhas 3-4: instanciação da camada [dao]. Vimos que o construtor da classe [DaoTransferAdminDataFromJsonFile2Database] esperava, como parâmetro, o dicionário da configuração geral da aplicação;
  • linha 4: a referência à camada [dao] é inserida na configuração;
  • linha 7: a configuração é devolvida;

20.1.7. O script [main] da aplicação

Image

Image

O script principal [main] é o seguinte:


# aguarda-se um parâmetro mysql ou 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()

# configuramos a aplicação
import config
config = config.configure({'sgbd': sgbd})

# o syspath está definido — já é possível efetuar as importações
from ImpôtsError import ImpôtsError

# recuperamos a camada [dao]
dao = config["dao"]

# código
try:
    # transferência de dados para a base de dados
    dao.transfer_admindata_in_database()
except ImpôtsError as ex1:
    # é apresentado o erro
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # exibe o erro
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # fim
    print("Terminé...")
  • linhas 1-10: aguarda-se um parâmetro. Verifica-se se este existe e se está correto;
  • linhas 12-14: configura-se a aplicação (geral, SQLAlchemy, camadas) passando como parâmetro o tipo de SGBD escolhido;
  • linhas 19-20: vamos precisar da camada [dao]. Recuperamo-la;
  • linha 25: efetuamos a transferência para a base de dados. Todas as informações necessárias para o método [transfer_admindata_in_database] estão disponíveis nas propriedades da camada [dao] da linha 20. É aí que o método irá buscá-las;

Após a execução com a base MySQL, esta contém os seguintes elementos (phpMyAdmin):

Image

Image

Image

Na coluna [3], podem ver-se os valores atribuídos por MySQL à chave primária [id]. A numeração começa em 1. A captura de ecrã acima foi obtida após várias execuções do script.

Image

Image

Com a base de dados PostgreSQL, os resultados são os seguintes:

Image

  • clica-se com o botão direito do rato em [1] e, em seguida, em [2-3];
  • em [4], os dados das faixas de imposto aparecem corretamente;

Repetimos o mesmo procedimento para a tabela de constantes [tbconstantes]:

Image

Image

Image

20.2. Aplicação 2: cálculo do imposto em modo batch

Image

20.2.1. Arquitetura

A aplicação de cálculo do imposto da versão 4 utilizava a seguinte arquitetura:

Image

A camada [dao] implementa uma interface [InterfaceImpôtsDao]. Criámos uma classe que implementa essa interface:

  • [ImpôtsDaoWithAdminDataInJsonFile], que iria buscar os dados fiscais a um ficheiro jSON. Esta era a versão 3;

Vamos implementar a interface [InterfaceImpôtsDao] através de uma nova classe, [ImpotsDaoWithTaxAdminDataInDatabase], que irá buscar os dados da administração fiscal numa base de dados. A camada [dao], tal como anteriormente, gravará os resultados num ficheiro jSON e irá buscar os dados dos contribuintes num ficheiro de texto. Sabemos que, se continuarmos a respeitar a interface [InterfaceImpôtsDao], a camada [métier] não terá de ser alterada.

A nova arquitetura será a seguinte:

Image

20.2.2. Configuração da aplicação

Image

O ficheiro de configuração [config_database] mantém-se tal como estava na aplicação 1. A configuração [config] inclui novos elementos:


    # etapa 2 ------
    # conclui-se a configuração da aplicação
    config.update({
        # caminhos absolutos dos ficheiros de dados
        "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"
    })
  • linhas 6-8: os caminhos absolutos dos ficheiros de texto utilizados pela aplicação 2;

A configuração das camadas [config_layers] evolui da seguinte forma:


def configure(config: dict) -> dict:
    # instanciação da camada DAO
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    config["dao"] = ImpotsDaoWithAdminDataInDatabase(config)

    # instanciação da camada [métier]
    from ImpôtsMétier import ImpôtsMétier
    config['métier'] = ImpôtsMétier()

    # a configuração é guardada
    return config
  • linhas 3-4: a camada [dao] é agora implementada pela classe [ImpotsDaoWithAdminDataInDatabase]. Esta classe é nova, mas implementa a mesma interface [InterfaceDao] que a versão 4 do exercício de aplicação;
  • linhas 7-8: a camada [métier] é implementada pela classe [ImpôtsMétier]. Esta é a classe utilizada na versão 4 do exercício prático;

20.2.3. A camada [dao]

Image

A classe de implementação [ImpotsDaoWithAdminDataInDatabase] da interface [InterfaceImpôtsDao] será a seguinte:


# importações
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):
    # construtor
    def __init__(self, config: dict):
        # config["taxPayersFilename"]: o nome do ficheiro de texto dos contribuintes
        # config["taxPayersResultsFilename"]: o nome do ficheiro jSON dos resultados
        # config["errorsFilename"]: regista os erros encontrados em taxPayersFilename
        # config["database"]: configuração da base de dados

        # inicialização da classe Parent
        AbstractImpôtsDao.__init__(self, config)
        # armazenamento de parâmetros
        self.__config = config
        # dados de administração
        self.__admindata = None

    # implementação da interface
    def get_admindata(self):
        # O admindata foi memorizado?
        if self.__admindata:
            return self.__admindata
        # é efetuada uma consulta em BD
        session = None
        config = self.__config
        try:
            # uma sessão
            database_config = config["database"]
            session = database_config["session"]

            # está a ser lida a tabela de escalões de imposto
            tranches = session.query(Tranche).all()

            # é lida a tabela de constantes (apenas uma linha)
            constantes = session.query(Constantes).first()

            # cria-se a instância admindata
            admindata = AdminData()
            # criam-se as tabelas de limites, 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))
            # adicionam-se as constantes
            admindata.fromdict(constantes.asdict())
            # guarda-se a instância «admindata»
            self.__admindata = admindata
            # retorna-se o valor
            return self.__admindata
        except (IntegrityError, DatabaseError, InterfaceError) as erreur:
            # lança-se novamente a exceção sob outra forma
            raise ImpôtsError(27, f"{erreur}")
        finally:
            # encerra-se a sessão
            if session:
                session.close()

Notas

  • linha 11: a classe [ImpotsDaoWithAdminDataInDatabase] herda da classe [AbstractImpôtsDao] apresentada na versão 4. Sabe-se que esta última implementa a interface [InterfaceDao] apresentada nessa mesma versão. É o cumprimento desta interface que nos permite não alterar a camada [métier];
  • linha 13: o construtor da classe recebe como parâmetro o dicionário da configuração da aplicação;
  • linha 20: a classe pai [] é inicializada. Ela implementa parcialmente a interface [InterfaceDao]:
    • [get_taxpayers_data] lê o ficheiro [taxpayersdata.txt], que contém os dados dos contribuintes;
    • [write_taxpayers_results] grava os resultados no ficheiro jSON [résultats.json];
    • [get_admindata] não está implementada;
  • linha 22: guarda-se a configuração passada como parâmetros;
  • linha 27: implementação do método [get_admindata] da interface [InterfaceDao]:
  • linhas 28-30: o método [get_admindata] recupera os dados da administração fiscal num objeto do tipo [AdminData] e armazena esse objeto em [self.__admindata]. Se o método [get_admindata] for chamado várias vezes, a base de dados não é consultada repetidamente. É consultada apenas na primeira vez. Nas vezes seguintes, é devolvido o objeto [self.__admindata];
  • linhas 36-37: recupera-se a sessão [sqlalchemy], que foi criada durante a configuração da aplicação pelo método [config_database];
  • linhas 40: recuperam-se as faixas de imposto numa lista;
  • linhas 43: recuperam-se as constantes do cálculo do imposto;
  • linha 46: cria-se uma instância da classe [AdminData]. Recorde-se que esta deriva de [BaseEntity];
  • linhas 48-54: inicializam-se os tabuletos [limites, coeffr, coeffn] da instância [AdminData];
  • linhas 55-56: inicializam-se as restantes propriedades de [AdminData] com as constantes do cálculo do imposto. Tivemos o cuidado de atribuir os mesmos nomes às propriedades das classes [AdminData] e [Constantes], o que simplifica o código;
  • linhas 57-58: a instância [AdminData] é armazenada na camada [dao] para ser devolvida nas próximas chamadas ao método [get_admindata];
  • linha 60: devolve-se o valor solicitado pelo código chamador;
  • linhas 61-63: gestão de um eventual erro;
  • linhas 64-67: a base de dados é alvo de apenas uma única consulta. Por isso, é possível encerrar a sessão [sqlalchemy];

20.2.4. Teste da camada [dao]

Na versão 4 desta aplicação, criámos uma classe de teste da camada [métier]. Mais precisamente, esta testava simultaneamente as camadas [métier] e [dao]. Retomamos este teste para verificar se a camada [dao] funciona conforme o esperado. Com efeito, a camada [métier] não sofre alterações.

Image

Image

O teste [TestDaoMétier] é o seguinte:


import unittest


class TestDaoMétier(unittest.TestCase):

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

        # {'casado': 'sim', 'filhos': 2, 'salário': 55555,
        # 'imposto': 2814, 'majoração': 0, 'redução': 0, 'abatimento': 0, 'taxa': 0,14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        métier.calculate_tax(taxpayer, admindata)
        # verificação
        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': 'sim', 'filhos': 3, 'salário': 200000,
        # 'imposto': 42842, 'sobretaxa': 17283, 'desconto': 0, 'redução': 0, 'taxa': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        métier.calculate_tax(taxpayer, admindata)
        # verificações
        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__':
    # é esperado um parâmetro mysql ou 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()

    # a aplicação está a ser configurada
    import config
    config = config.configure({'sgbd': sgbd})
    # camada de negócio
    métier = config['métier']
    try:
        # admindata
        admindata = config['dao'].get_admindata()
    except BaseException as ex:
        # visualização
        print((f"L'erreur suivante s'est produite : {ex}"))
        # fim
        sys.exit()
    # envia-se o parâmetro recebido pelo script
    sys.argv.pop()
    # executam-se os métodos de teste
    print("tests en cours...")
   unittest.main()
  • Não vamos voltar aos 11 testes descritos no parágrafo |teste da camada [métier] versão 4|;
  • linhas 37-66: vamos executar o script dos testes como uma aplicação normal e não como um teste UnitTest. É a linha 66 que irá acionar o framework UnitTest. Nos testes anteriores, utilizávamos o método [setUp] para configurar a execução de cada teste. Repetíamos 11 vezes a mesma configuração, uma vez que a função [setUp] é executada antes de cada teste. Aqui, fazemos a configuração uma única vez. Consiste em definir as variáveis globais [métier], na linha 53, e [admindata], na linha 56, que serão posteriormente utilizadas pelos métodos de [TestDaoMétier], na linha 12, por exemplo;
  • linhas 39-47: o script de teste aguarda um parâmetro [mysql / pgres] que indica se se utiliza uma base MySQL ou PostgreSQL;
  • linhas 50-51: o teste é configurado;
  • linha 53: recupera-se a camada [métier] na configuração;
  • linha 56: faz-se o mesmo com a camada [dao]. Recupera-se então a instância [admindata], que encapsula os dados necessários ao cálculo do imposto;
  • os testes revelaram que o método [unittest.main()] da linha 66 não ignorava o parâmetro [mysql / pgres] recebido pelo script, mas atribuía-lhe um significado diferente. A linha 63 faz com que este método deixe de ter qualquer parâmetro;

Criamos duas configurações de execução:

Image

Image

Se executarmos uma destas duas configurações, obtemos os seguintes 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
  • linhas 5 e 7: os 11 testes foram bem-sucedidos;

Recorde-se que estes testes verificam apenas 11 casos de cálculo do imposto. O seu sucesso pode, no entanto, ser suficiente para nos dar confiança na camada [dao].

20.2.5. O script principal

Image

Image

O script principal [main] é o mesmo da versão 4:


# aguarda-se um parâmetro mysql ou 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()

# configura-se a aplicação
import config
config = config.configure({'sgbd': sgbd})

# o syspath está definido — já é possível efetuar as importações
from ImpôtsError import ImpôtsError

# recuperam-se as camadas da aplicação (já estão instanciadas)
dao = config["dao"]
métier = config["métier"]

try:
    # recuperação dos escalões de imposto
    admindata = dao.get_admindata()
    # leitura dos dados dos contribuintes
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # dos contribuintes?
    if not taxpayers:
        raise ImpôtsError(57, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # cálculo do imposto dos contribuintes
    for taxPayer in taxpayers:
        # taxPayer é simultaneamente um parâmetro de entrada e de saída
        # O taxPayer vai ser alterado
        métier.calculate_tax(taxPayer, admindata)
    # gravação dos resultados num ficheiro de texto
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
    # exibição do erro
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # concluído
    print("Travail terminé...")

Notas

  • linhas 1-10: recupera-se o parâmetro [mysql / pgres], que indica o SGBD a utilizar;
  • linhas 12-14: a aplicação é configurada;
  • linhas 16-17: a classe [ImpôtsError] é importada. É necessária na linha 38;
  • linhas 19-21: recuperam-se referências às camadas da aplicação;
  • linha 25: solicitam-se à camada [dao] os dados da administração fiscal. A camada [métier] necessita deles para o cálculo do imposto;
  • linha 27: recuperam-se, numa lista, os dados (ID, estado civil, filhos, salário) dos contribuintes;
  • linhas 29-30: se esta lista estiver vazia, é lançada uma exceção;
  • linhas 32-35: cálculo do imposto dos elementos da lista [taxpayers];
  • linha 37: gravação dos resultados no ficheiro jSON[résultats.json];
  • linhas 38-40: gestão de eventuais erros;

Para a execução do script, criam-se duas |configurações de execução|:

Image

Os resultados obtidos no ficheiro [résultats.json] são os da versão 4.

Image

20.3. Aplicação 3: cálculo do imposto em modo interativo

Apresentamos agora a aplicação que permite calcular o imposto de forma interativa. Trata-se de uma adaptação da aplicação 2 da versão 4.

Image

Image

  • o script [main] inicia o diálogo com o utilizador através do método [ui.run] da camada [ui];
  • a camada [ui]:
    • utiliza a camada [dao] para obter os dados necessários ao cálculo do imposto;
    • solicita ao utilizador as informações relativas ao contribuinte cujo imposto se pretende calcular;
    • utiliza a camada [métier] para efetuar esse cálculo;

O ficheiro [config_layers] instancia uma camada adicional:


def configure(config: dict) -> dict:
    # instanciação da camada DAO
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    config["dao"] = ImpotsDaoWithAdminDataInDatabase(config)

    # instanciação da camada [métier]
    from ImpôtsMétier import ImpôtsMétier
    config['métier'] = ImpôtsMétier()

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

    # enviando a configuração
    return config

A classe [ImpôtsConsole], linhas 11-12, é a mesma que na |versão 4|.

O script principal [main] é o seguinte:


# aguarda-se um parâmetro mysql ou 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()

# configuramos a aplicação
import config
config = config.configure({'sgbd': sgbd})

# o syspath está configurado — já é possível efetuar as importações
from ImpôtsError import ImpôtsError

# recuperar a camada [ui]
ui = config["ui"]

# código
try:
    # execução da camada [ui]
    ui.run()
except ImpôtsError as ex1:
    # é apresentada a mensagem de erro
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # é apresentada a mensagem de erro
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # executado em todos os casos
    print("Travail terminé...")
  • nas linhas 1-10, o script aguarda um parâmetro [mysql / pgres] que indica o SGBD a utilizar;
  • linhas 12-14: a aplicação é configurada;
  • linhas 19-20: recupera-se a camada [ui] da configuração;
  • linha 25: executa-se a camada;

Os resultados são idênticos aos da |versão 4|. Não poderia ser de outra forma, uma vez que todas as interfaces da versão 4 foram respeitadas na versão 5.