20. Toepassingsopdracht: versie 5

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:

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]

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:

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:

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:

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:

…
# 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]

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:

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

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

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


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



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.


Met de database PostgreSQL zijn de resultaten als volgt:

- 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]:



20.2. Toepassing 2: belastingberekening in batchmodus

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

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:

20.2.2. Configuratie van de applicatie

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]

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.


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:


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


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:

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

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.


- 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.