Skip to content

20. Toepassingsopdracht: versie 5

Image

We gaan drie applicaties ontwikkelen:

  • toepassing 1 initialiseert de database die het bestand [admindata.json] uit versie 4 gaat vervangen;
  • toepassing 2 berekent de belastingen in batchmodus;
  • toepassing 3 berekent de belastingen in interactieve modus;

20.1. Toepassing 1: initialisatie van de database

Toepassing 1 zal de volgende architectuur hebben:

Image

Dit is een doorontwikkeling van de architectuur van versie 4 (paragraaf |Versie 4|): de belastinggegevens worden in een database opgeslagen in plaats van in een bestand jSON. De laag [dao] zal worden aangepast om deze wijziging door te voeren.

20.1.1. Het bestand [admindata.json]

Image

Het bestand [admindata.json] is hetzelfde als in versie 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
}

We gaan de sleutels uit dit woordenboek gebruiken als kolommen in de database.

20.1.2. Databases aanmaken

Zoals is getoond in de paragraaf |het aanmaken van een database MySQL|, maken we een database MySQL aan met de naam [dbimpots-2019], eigendom van de gebruiker [admimpots] met wachtwoord [mdpimpots]. In [phpMyAdmin] levert dit het volgende op:

Image

Evenzo, zoals is aangetoond in de paragraaf |het aanmaken van een database PostgreSQL|, maken we een database aan met de naam PostgreSQL, genaamd [dbimpots-2019], die eigendom is van de gebruiker [admimpots] met het wachtwoord [mdpimpots]. In [pgAdmin] levert dit het volgende op:

Image

De databases zijn aangemaakt, maar bevatten voorlopig nog geen tabellen. Deze worden aangemaakt door de ORM en [sqlalchemy].

20.1.3. De entiteiten die door [sqlalchemy] worden toegewezen

We gaan twee tabellen aanmaken om de gegevens van [admindata.json] te bevatten:

De door [sqlalchemy] gedefinieerde tabel [tbtranches] zal de gegevens uit de tabellen [limites, coeffr, coeffn] van het woordenboek [admindata.json] verzamelen:


    # de tabel met belastingschijven
    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)
                           )

De tabel [tbconstantes], gedefinieerd door [sqlalchemy], bevat de constanten uit het woordenboek [admindata.json]:


    # de tabel met constanten
    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)
                             )

De entiteiten die aan deze twee tabellen worden gekoppeld, zijn de volgende:

Image

De entiteit [Constantes] bevat de constanten uit het woordenboek [admindata.json]:


from BaseEntity import BaseEntity


# containerklasse voor gegevens van de belastingdienst
class Constantes(BaseEntity):
    # sleutels die zijn uitgesloten van de klasseoverzicht
    excluded_keys = ["_sa_instance_state"]

    # toegestane sleutels
    @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"]
  • regel 5: de klasse [Constantes] is een uitbreiding van de klasse [BaseEntity];
  • regel 7: via mapping [sqlalchemy] krijgt de klasse [Constante] de eigenschap [_sa_instance_state]. We sluiten deze uit van het woordenboek [asdict] van de entiteit;
  • regels 11-23: de eigenschappen van de entiteit. We hebben de namen overgenomen die in het woordenboek [admindata.json] worden gebruikt om het schrijven van de code te vergemakkelijken;

De entiteit [Tranche] omvat een rij van de drie tabellen [limites, coeffr, coeffn] uit het woordenboek [admindata.json]:


from BaseEntity import BaseEntity


# containerklasse voor gegevens van de belastingdienst
class Tranche(BaseEntity):
    # sleutels die zijn uitgesloten van de klasseoverzicht
    excluded_keys = ["_sa_instance_state"]

    # toegestane sleutels
    @staticmethod
    def get_allowed_keys() -> list:
        return ["id", "limite", "coeffr", "coeffn"]
  • regel 5: de klasse [Tranche] is een uitbreiding van de klasse [BaseEntity];
  • regel 7: de eigenschap [_sa_instance_state], toegevoegd door [sqlalchemy], wordt uitgesloten van de eigenschappen van het woordenboek [asdict] van de entiteit;
  • regels 10-12: de eigenschappen van de klasse;

De koppeling tussen de entiteiten [Constantes, Tranche] en de tabellen [constantes, tranches] is als volgt:

Image



    # de tabel met constanten
    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)
                             )

    # de tabel met belastingschijven
    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)
                           )
    # de mappings
    from Tranche import Tranche
    mapper(Tranche, tranches_table)

    from Constantes import Constantes
    mapper(Constantes, constantes_table)
  • de toewijzingen zijn gemaakt in de regels 24-29. Daarin is nagelaten om de overeenkomsten aan te geven tussen de eigenschappen van de toegewezen entiteiten en de tabellen in de database. Dit is mogelijk wanneer de namen van de kolommen in de tabellen dezelfde zijn als die van de eigenschappen waaraan ze moeten worden gekoppeld. Om deze reden hebben we in de tabellen de namen van de eigenschappen van de toegewezen entiteiten overgenomen. Dit vergemakkelijkt het schrijven en begrijpen van de code;

20.1.4. Het configuratiebestand van [sqlalchemy]

Image

We hebben zojuist een deel van de configuratie van [sqlalchemy] toegelicht. Het volledige bestand [config_database] ziet er als volgt uit:


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

    # verbindingsstrings voor de gebruikte databases
    connection_strings = {
        'mysql': "mysql+mysqlconnector://admimpots:mdpimpots@localhost/dbimpots-2019",
        'pgres': "postgresql+psycopg2://admimpots:mdpimpots@localhost/dbimpots-2019"
    }
    # verbindingsstring naar de gebruikte database
    engine = create_engine(connection_strings[config['sgbd']])

    # metadata
    metadata = MetaData()

    # de tabel met constanten
    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)
                             )

    # de tabel met belastingschijven
    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)
                           )
    # de mappings
    from Tranche import Tranche
    mapper(Tranche, tranches_table)

    from Constantes import Constantes
    mapper(Constantes, constantes_table)

    # de sessiefabriek
    session_factory = sessionmaker()
    session_factory.configure(bind=engine)

    # een sessie
    session = session_factory()

    # bepaalde gegevens worden opgeslagen
    config['database'] = {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
                          "constantes_table": constantes_table, "session": session}

    # resultaat
    return config
  • regel 1: de functie [configure] ontvangt als parameter een woordenboek waarvan de sleutel [sgbd] aangeeft welke SGBD moet worden gebruikt: MySQL (mysql) of PostgreSQL (pgres);
  • regels 6-12: de door de configuratie gevraagde database wordt geselecteerd;
  • regels 14-44: toewijzingen van entiteiten aan tabellen. Deze toewijzingen zijn eenvoudig omdat er geen koppeling bestaat tussen de tabellen [tranches] en [constantes]. Ze staan los van elkaar. Er hoeft dus geen vreemde sleutel van de ene naar de andere te worden beheerd;
  • regels 46-51: de werksessie [session] van de applicatie wordt aangemaakt;
  • regels 53-58: de relevante informatie wordt in het configuratiedictionary geplaatst en dit wordt teruggestuurd;

20.1.5. De laag [dao]

Laten we terugkeren naar de architectuur van applicatie 1 die moet worden gebouwd:

Image

De laag [dao] [1] moet het bestand [admindata.json] [2] lezen en de inhoud ervan overbrengen naar een van de databases [3, 4];

Image

De laag [dao] biedt de interface [1] en wordt geïmplementeerd door de klasse [2].

De interface [InterfaceDao4TransferAdminData2Database] ziet er als volgt uit:


# imports
from abc import ABC, abstractmethod


# interface InterfaceImpôtsUI
class InterfaceDao4TransferAdminData2Database(ABC):
    # overdracht van belastinggegevens naar een database
    @abstractmethod
    def transfer_admindata_in_database(self:object):
        pass
  • regels 8-10: de interface bevat slechts één methode, [transfer_admindata_in_database], zonder parameters. Aangezien deze methode parameters nodig heeft (welk bestand?, welke database?), betekent dit dat deze zullen worden doorgegeven aan de constructor van de klassen die deze interface implementeren;

De klasse [DaoTransferAdminDataFromJsonFile2Database] implementeert de interface [InterfaceDao4TransferAdminData2Database] als volgt:


# import
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):

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

    # overdracht
    def transfer_admindata_in_database(self) -> None:
        # initialisaties
        session = None
        config = self.config

        try:
            # de gegevens van de belastingdienst worden opgehaald
            with codecs.open(config["admindataFilename"], "r", "utf8") as fd:
                # de inhoud wordt overgebracht naar een woordenboek
                admindata = json.load(fd)

            # de configuratie van de database wordt opgehaald
            database = config["database"]

            # verwijdering van de twee tabellen uit de database
            # checkfirst=True: controleert eerst of de tabel bestaat
            database["tranches_table"].drop(database["engine"], checkfirst=True)
            database["constantes_table"].drop(database["engine"], checkfirst=True)

            # tabellen opnieuw aanmaken op basis van de toewijzingen
            database["metadata"].create_all(database["engine"])

            # de huidige sessie [sqlalchemy]
            session = database["session"]

            # de tabel met belastingschijven wordt gevuld
            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]}))
            # de tabel met constanten wordt ingevuld
            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"]
            }))

            # sessievalidatie [sqlalchemy]
            session.commit()
        except (IntegrityError, DatabaseError, InterfaceError) as erreur:
            # de uitzondering wordt opnieuw gegenereerd in een andere vorm
            raise ImpôtsError(17, f"{erreur}")
        finally:
            # de sessiebronnen worden vrijgegeven
            if session:
                session.close()
  • regel 13: de klasse [DaoTransferAdminDataFromJsonFile2Database] implementeert de interface [InterfaceDao4TransferAdminData2Database];
  • regels 15-17: de constructor van de klasse ontvangt het configuratiedictionary als parameter. De volgende sleutels worden gebruikt:
    • [admindataFilename] (regel 27): de naam van het bestand jSON dat de gegevens van de belastingdienst bevat die naar de database moeten worden overgebracht;
    • [database] regel 32: de configuratie [sqlalchemy] van de applicatie;
  • regels 34-37: verwijdering van de tabellen [constantes] en [tranches] indien deze bestaan;
  • regels 39-40: de twee tabellen opnieuw aanmaken;
  • regel 43: de sessie [sqlalchemy] uit de configuratie wordt opgehaald;
  • regels 45-51: de tabellen [limites, coeffr, coeffn] uit het woordenboek [admindata] worden in de sessie geplaatst. Hiervoor worden instanties van de entiteit [Tranche] in de sessie geplaatst;
  • regels 52-64: een instantie van de entiteit [Constantes] wordt in de sessie geplaatst;
  • regels 66-67: de sessie wordt gevalideerd. Als de gegevens van de sessie nog niet in de database stonden, worden ze op dat moment daarin opgeslagen;
  • regels 68-70: afhandeling van een eventuele fout;
  • regels 71-74: de sessie wordt afgesloten. Dit is mogelijk omdat de laag [dao] slechts één keer wordt gebruikt;

20.1.6. Configuratie van de applicatie

Image

De applicatie wordt geconfigureerd door drie bestanden [1]:

  • [config] is het algemene configuratiebestand. Dit bestand configureert de applicatie [main]. Het wordt daarbij ondersteund door de twee andere bestanden:
    • [config_database], dat we al hebben besproken en dat de bestanden ORM en [sqlalchemy] configureert;
    • [config_layers], dat de lagen van de applicatie configureert;

Het bestand [config] ziet er als volgt uit:


def configure(config: dict) -> dict:
    # [config] heeft de sleutel [sgbd] met de waarde:
    # [mysql] voor het beheer van een database MySQL
    # [pgres] voor het beheer van een database met de sleutel PostgreSQL

    import os

    # stap 1 ---
    # we stellen het Python-pad van de applicatie in

    # absoluut pad naar de map van dit script
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # root_dir (eventueel aan te passen)
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # absolute paden van de afhankelijkheden
    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",
        # lokale mappen
        f"{script_dir}",
        f"{script_dir}/../../interfaces",
        f"{script_dir}/../../services",
        f"{script_dir}/../../entities",
    ]

    # het syspath instellen
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # stap 2 ------
    # de configuratie van de applicatie wordt voltooid
    config.update({
        # absolute paden naar de gegevensbestanden
        "admindataFilename"f"{script_dir}/../../data/input/admindata.json"
    })

    # stap 3 ------
    # databaseconfiguratie
    import config_database
    config = config_database.configure(config)

    # stap 4 ------
    # instantiëren van de applicatielagen
    import config_layers
    config = config_layers.configure(config)

    # de configuratie wordt doorgegeven
    return config
  • regels 8-36: hier wordt het Python-pad van de applicatie samengesteld;
  • regels 38-43: het pad naar het bestand [admindata.json] wordt in de configuratie opgenomen;
  • regels 45-48: configuratie van [sqlalchemy];
  • regels 50-53: de lagen van de applicatie worden geïnstantieerd;
  • regel 56: de algemene configuratie wordt teruggegeven;

Het bestand [config_layers] ziet er als volgt uit:


def configure(config: dict) -> dict:
    # instantie van de laag [dao]
    from DaoTransferAdminDataFromJsonFile2Database import DaoTransferAdminDataFromJsonFile2Database
    config['dao'] = DaoTransferAdminDataFromJsonFile2Database(config)

    # de configuratie wordt teruggestuurd
    return config
  • regels 3-4: instantiëren van de laag [dao]. We hebben gezien dat de constructor van de klasse [DaoTransferAdminDataFromJsonFile2Database] als parameter het woordenboek van de algemene configuratie van de applicatie verwacht;
  • regel 4: de verwijzing naar de laag [dao] wordt in de configuratie opgenomen;
  • regel 7: de configuratie wordt teruggestuurd;

20.1.7. Het script [main] van de applicatie

Image

Image

Het hoofdscript [main] luidt als volgt:


# er wordt gewacht op een MySQL- of PostgreSQL-parameter
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()

# de applicatie wordt geconfigureerd
import config
config = config.configure({'sgbd': sgbd})

# het syspath is ingesteld – we kunnen de imports uitvoeren
from ImpôtsError import ImpôtsError

# de laag wordt opgehaald [dao]
dao = config["dao"]

# code
try:
    # gegevensoverdracht naar de database
    dao.transfer_admindata_in_database()
except ImpôtsError as ex1:
    # er wordt een foutmelding weergegeven
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # de fout wordt weergegeven
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # einde
    print("Terminé...")
  • regels 1-10: er wordt gewacht op een parameter. Er wordt gecontroleerd of deze aanwezig en correct is;
  • regels 12-14: de applicatie wordt geconfigureerd (algemeen, SQLAlchemy, lagen) door het gekozen type SGBD als parameter door te geven;
  • regels 19-20: we hebben de laag [dao] nodig. We halen deze op;
  • regel 25: we voeren de overdracht naar de database uit. Alle informatie die nodig is voor de methode [transfer_admindata_in_database] is beschikbaar in de eigenschappen van de laag [dao] uit regel 20. Daar haalt de methode deze gegevens vandaan;

Na uitvoering met de basis MySQL bevat deze de volgende elementen (phpMyAdmin):

Image

Image

Image

In kolom [3] zijn de waarden te zien die door MySQL zijn toegewezen aan de primaire sleutel [id]. De nummering begint bij 1. De bovenstaande schermafbeelding is gemaakt na meerdere uitvoeringen van het script.

Image

Image

Met de database PostgreSQL zijn de resultaten als volgt:

Image

  • klik met de rechtermuisknop op [1] en vervolgens op [2-3];
  • in [4] zijn de gegevens van de belastingschijven inderdaad aanwezig;

We herhalen dit voor de constantentabel [tbconstantes]:

Image

Image

Image

20.2. Toepassing 2: belastingberekening in batchmodus

Image

20.2.1. Architectuur

De toepassing voor belastingberekening van versie 4 maakte gebruik van de volgende architectuur:

Image

De laag [dao] implementeert een interface [InterfaceImpôtsDao]. We hebben een klasse gebouwd die deze interface implementeert:

  • [ImpôtsDaoWithAdminDataInJsonFile], die de belastinggegevens uit een bestand jSON ophaalde. Dit was versie 3;

We gaan de interface [InterfaceImpôtsDao] implementeren via een nieuwe klasse [ImpotsDaoWithTaxAdminDataInDatabase], die de gegevens van de belastingdienst uit een database ophaalt. De laag [dao] zal, net als voorheen, de resultaten naar een bestand jSON schrijven en de gegevens van de belastingplichtigen uit een tekstbestand ophalen. We weten dat als we de interface [InterfaceImpôtsDao] blijven volgen, de laag [métier] niet hoeft te worden aangepast.

De nieuwe architectuur ziet er als volgt uit:

Image

20.2.2. Configuratie van de applicatie

Image

Het configuratiebestand [config_database] blijft hetzelfde als in applicatie 1. De configuratie [config] bevat nieuwe elementen:


    # stap 2 ------
    # de configuratie van de applicatie wordt voltooid
    config.update({
        # absolute paden van de gegevensbestanden
        "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"
    })
  • regels 6-8: de absolute paden van de tekstbestanden die door applicatie 2 worden gebruikt;

De configuratie van de lagen [config_layers] verandert als volgt:


def configure(config: dict) -> dict:
    # instantiëren van de DAO-laag
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    config["dao"] = ImpotsDaoWithAdminDataInDatabase(config)

    # instantiëren van de [métier]-laag
    from ImpôtsMétier import ImpôtsMétier
    config['métier'] = ImpôtsMétier()

    # de configuratie wordt teruggegeven
    return config
  • regels 3-4: de laag [dao] wordt nu geïmplementeerd door de klasse [ImpotsDaoWithAdminDataInDatabase]. Deze klasse is nieuw, maar implementeert dezelfde interface [InterfaceDao] als versie 4 van de applicatieopdracht;
  • regels 7-8: de laag [métier] wordt geïmplementeerd door de klasse [ImpôtsMétier]. Dit is de klasse die in versie 4 van de praktijkopdracht wordt gebruikt;

20.2.3. De laag [dao]

Image

De implementatieklasse [ImpotsDaoWithAdminDataInDatabase] van de interface [InterfaceImpôtsDao] ziet er als volgt uit:


# importen
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"]: de naam van het tekstbestand met de belastingplichtigen
        # config["taxPayersResultsFilename"]: de naam van het jSON-bestand met de resultaten
        # config["errorsFilename"]: registreert de fouten die zijn aangetroffen in taxPayersFilename
        # config["database"]: configuratie van de database

        # initialisatie van de klasse Parent
        AbstractImpôtsDao.__init__(self, config)
        # parameter opslaan
        self.__config = config
        # admindata
        self.__admindata = None

    # implementatie van de interface
    def get_admindata(self):
        # is admindata opgeslagen?
        if self.__admindata:
            return self.__admindata
        # er wordt een verzoek gedaan in BD
        session = None
        config = self.__config
        try:
            # een sessie
            database_config = config["database"]
            session = database_config["session"]

            # de tabel met belastingschijven wordt gelezen
            tranches = session.query(Tranche).all()

            # de tabel met constanten wordt gelezen (slechts 1 rij)
            constantes = session.query(Constantes).first()

            # we maken de instantië admindata aan
            admindata = AdminData()
            # daarin worden de limiettabellen aangemaakt: 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))
            # daarin worden de constanten toegevoegd
            admindata.fromdict(constantes.asdict())
            # we slaan admindata op
            self.__admindata = admindata
            # de waarde wordt teruggegeven
            return self.__admindata
        except (IntegrityError, DatabaseError, InterfaceError) as erreur:
            # de uitzondering wordt opnieuw gegenereerd in een andere vorm
            raise ImpôtsError(27, f"{erreur}")
        finally:
            # de sessie wordt afgesloten
            if session:
                session.close()

Opmerkingen

  • regel 11: de klasse [ImpotsDaoWithAdminDataInDatabase] erft van de klasse [AbstractImpôtsDao] die in versie 4 wordt gepresenteerd. We weten dat deze laatste de interface [InterfaceDao] implementeert die in dezelfde versie wordt gepresenteerd. Doordat aan deze interface wordt voldaan, hoeven we de laag [métier] niet te wijzigen;
  • regel 13: de constructor van de klasse ontvangt als parameter het woordenboek met de configuratie van de applicatie;
  • regel 20: de bovenliggende klasse [] wordt geïnitialiseerd. Deze implementeert gedeeltelijk de interface [InterfaceDao]:
    • [get_taxpayers_data] leest het bestand [taxpayersdata.txt] dat de gegevens van de belastingplichtigen bevat;
    • [write_taxpayers_results] schrijft de resultaten naar het bestand jSON [résultats.json];
    • [get_admindata] is niet geïmplementeerd;
  • regel 22: de als parameters doorgegeven configuratie wordt opgeslagen;
  • regel 27: implementatie van de methode [get_admindata] van de interface [InterfaceDao]:
  • regels 28-30: de methode [get_admindata] haalt de gegevens van de belastingdienst op in een object van het type [AdminData] en slaat dit object op in [self.__admindata]. Als de methode [get_admindata] meerdere keren wordt aangeroepen, wordt de database niet meerdere keren geraadpleegd. Dit gebeurt slechts de eerste keer. De volgende keren wordt het object [self.__admindata] teruggegeven;
  • regels 36-37: de sessie [sqlalchemy] wordt opgehaald, die bij de configuratie van de applicatie door [config_database] is aangemaakt;
  • regel 40: de belastingschijven worden in een lijst opgehaald;
  • regels 43: de constanten voor de belastingberekening worden opgehaald;
  • regel 46: er wordt een instantie van de klasse [AdminData] aangemaakt. Ter herinnering: deze is afgeleid van [BaseEntity];
  • regels 48-54: de arrays [limites, coeffr, coeffn] van de instantie [AdminData] worden geïnitialiseerd;
  • regels 55-56: de overige eigenschappen van [AdminData] worden geïnitialiseerd met de constanten voor de belastingberekening. Er is ervoor gezorgd dat de eigenschappen van de klassen [AdminData] en [Constantes] dezelfde namen hebben gekregen, wat de code vereenvoudigt;
  • regels 57-58: de instantie [AdminData] wordt opgeslagen in de laag [dao] om deze bij volgende aanroepen van de methode [get_admindata] terug te geven;
  • regel 60: de door de aanroepende code gevraagde waarde wordt teruggegeven;
  • regels 61-63: afhandeling van een eventuele fout;
  • regels 64-67: er wordt slechts één query op de database uitgevoerd. De sessie [sqlalchemy] kan dus worden afgesloten;

20.2.4. Test van de laag [dao]

In versie 4 van deze applicatie hadden we een testklasse voor de laag [métier] gebouwd. Om precies te zijn testte deze zowel de lagen [métier] als [dao]. We gebruiken deze test opnieuw om te controleren of de laag [dao] naar verwachting werkt. De laag [métier] blijft namelijk ongewijzigd.

Image

Image

De test [TestDaoMétier] is als volgt:


import unittest


class TestDaoMétier(unittest.TestCase):

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

        # {'getrouwd': 'ja', 'kinderen': 2, 'salaris': 55555,
        # 'belasting': 2814, 'toeslag': 0, 'korting': 0, 'vermindering': 0, 'tarief': 0,14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        métier.calculate_tax(taxpayer, admindata)
        # controle
        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

        # {'getrouwd': 'ja', 'kinderen': 3, 'salaris': 200000,
        # 'belasting': 42842, 'toeslag': 17283, 'korting': 0, 'vermindering': 0, 'tarief': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        métier.calculate_tax(taxpayer, admindata)
        # controles
        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__':
    # er wordt een mysql- of pgres-parameter verwacht
    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()

    # de applicatie wordt geconfigureerd
    import config
    config = config.configure({'sgbd': sgbd})
    # bedrijfslaag
    métier = config['métier']
    try:
        # admindata
        admindata = config['dao'].get_admindata()
    except BaseException as ex:
        # weergave
        print((f"L'erreur suivante s'est produite : {ex}"))
        # einde
        sys.exit()
    # de door het script ontvangen parameter wordt verwijderd
    sys.argv.pop()
    # de testmethoden worden uitgevoerd
    print("tests en cours...")
   unittest.main()
  • We gaan niet terugkomen op de 11 tests die worden beschreven in de paragraaf |test laag [métier] versie 4|;
  • regels 37-66: we gaan het testscript uitvoeren als een normale applicatie en niet als een UnitTest-test. Het is regel 66 die het UnitTest-framework in werking stelt. In de vorige tests gebruikten we de methode [setUp] om de uitvoering van elke test te configureren. We herhaalden dezelfde configuratie 11 keer, aangezien de functie [setUp] vóór elke test wordt uitgevoerd. Hier voeren we de configuratie één keer uit. Deze bestaat uit het definiëren van de globale variabelen [métier] op regel 53 en [admindata] op regel 56, die vervolgens worden gebruikt door de methoden van [TestDaoMétier], bijvoorbeeld op regel 12;
  • regels 39-47: het testscript verwacht een parameter [mysql / pgres] die aangeeft of er een database MySQL of PostgreSQL wordt gebruikt;
  • regels 50-51: de test wordt geconfigureerd;
  • regel 53: de laag [métier] wordt opgehaald uit de configuratie;
  • regel 56: hetzelfde gebeurt met de laag [dao]. Vervolgens wordt de instantie [admindata] opgehaald, die de gegevens bevat die nodig zijn voor de berekening van de belasting;
  • uit tests is gebleken dat de methode [unittest.main()] op regel 66 de door het script ontvangen parameter [mysql / pgres] niet negeerde, maar er een andere betekenis aan toekende. Regel 63 zorgt ervoor dat deze methode geen parameters meer heeft;

We maken twee uitvoeringsconfiguraties aan:

Image

Image

Als we een van deze twee configuraties uitvoeren, krijgen we de volgende resultaten:


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
  • regels 5 en 7: de 11 tests zijn geslaagd;

Laten we niet vergeten dat deze tests slechts 11 gevallen van de belastingberekening controleren. Het feit dat ze geslaagd zijn, kan echter voldoende zijn om ons vertrouwen te geven in de laag [dao].

20.2.5. Het hoofdscript

Image

Image

Het hoofdscript [main] is hetzelfde als in versie 4:


# er wordt gewacht op een mysql- of pgres-parameter
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()

# de applicatie wordt geconfigureerd
import config
config = config.configure({'sgbd': sgbd})

# het syspath is ingesteld – de imports kunnen worden uitgevoerd
from ImpôtsError import ImpôtsError

# de lagen van de applicatie worden opgehaald (deze zijn al geïnstantieerd)
dao = config["dao"]
métier = config["métier"]

try:
    # de belastingschijven worden opgehaald
    admindata = dao.get_admindata()
    # het inlezen van de gegevens van de belastingplichtigen
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # van de belastingplichtigen?
    if not taxpayers:
        raise ImpôtsError(57, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # berekening van de belasting van de belastingplichtigen
    for taxPayer in taxpayers:
        # taxPayer is zowel een invoer- als een uitvoerparameter
        # taxPayer zal worden gewijzigd
        métier.calculate_tax(taxPayer, admindata)
    # de resultaten worden naar een tekstbestand geschreven
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError as erreur:
    # weergave van de foutmelding
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # voltooid
    print("Travail terminé...")

Opmerkingen

  • regels 1-10: de parameter [mysql / pgres] wordt opgehaald, die aangeeft welk SGBD moet worden gebruikt;
  • regels 12-14: de applicatie wordt geconfigureerd;
  • regels 16-17: de klasse [ImpôtsError] wordt geïmporteerd. Deze is nodig op regel 38;
  • regels 19-21: er worden verwijzingen naar de lagen van de applicatie opgehaald;
  • regel 25: de gegevens van de belastingdienst worden opgevraagd bij de laag [dao]. De laag [métier] heeft deze nodig voor de berekening van de belasting;
  • regel 27: we halen uit een lijst de gegevens (id, gehuwd, kinderen, salaris) van de belastingplichtigen op;
  • regels 29-30: als deze lijst leeg is, wordt er een uitzondering gegenereerd;
  • regels 32-35: berekening van de belasting op basis van de gegevens uit de lijst [taxpayers];
  • regel 37: de resultaten worden weggeschreven naar het bestand jSON[résultats.json];
  • regels 38-40: afhandeling van eventuele fouten;

Voor de uitvoering van het script worden twee |uitvoeringsconfiguraties| aangemaakt:

Image

De resultaten in het bestand [résultats.json] zijn die van versie 4.

Image

20.3. Toepassing 3: interactieve belastingberekening

We introduceren nu de toepassing waarmee de belasting op interactieve wijze kan worden berekend. Dit is een port van toepassing 2 uit versie 4.

Image

Image

  • het script [main] start de dialoog met de gebruiker met behulp van de methode [ui.run] uit de laag [ui];
  • de laag [ui]:
    • gebruikt de laag [dao] om de gegevens op te halen die nodig zijn voor de berekening van de belasting;
    • vraagt de gebruiker om de gegevens van de belastingplichtige voor wie de belasting moet worden berekend;
    • gebruikt de laag [métier] om deze berekening uit te voeren;

Het bestand [config_layers] instantiëert een extra laag:


def configure(config: dict) -> dict:
    # instantiëren van DAO-laag
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    config["dao"] = ImpotsDaoWithAdminDataInDatabase(config)

    # instantiëren van de [métier]-laag
    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)

    # de configuratie wordt teruggestuurd
    return config

De klasse [ImpôtsConsole], regels 11-12, is dezelfde als in |versie 4|.

Het hoofdscript [main] is als volgt:


# we wachten op een MySQL- of PostgreSQL-parameter
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()

# de applicatie wordt geconfigureerd
import config
config = config.configure({'sgbd': sgbd})

# het syspath is geconfigureerd – we kunnen de imports uitvoeren
from ImpôtsError import ImpôtsError

# de laag wordt opgehaald [ui]
ui = config["ui"]

# code
try:
    # de laag [ui] wordt uitgevoerd
    ui.run()
except ImpôtsError as ex1:
    # de foutmelding wordt weergegeven
    print(f"L'erreur 1 suivante s'est produite : {ex1}")
except BaseException as ex2:
    # de foutmelding wordt weergegeven
    print(f"L'erreur 2 suivante s'est produite : {ex2}")
finally:
    # wordt in alle gevallen uitgevoerd
    print("Travail terminé...")
  • regels 1-10: het script verwacht een parameter [mysql / pgres] die aangeeft welke SGBD moet worden gebruikt;
  • regels 12-14: de toepassing wordt geconfigureerd;
  • regels 19-20: de laag [ui] wordt uit de configuratie opgehaald;
  • regel 25: deze wordt uitgevoerd;

De resultaten zijn identiek aan die van |versie 4|. Dat kon ook niet anders, aangezien alle interfaces van versie 4 in versie 5 zijn overgenomen.