23. Toepassingsopdracht: versie 6
23.1. Inleiding
We keren nu terug naar onze applicatie voor belastingberekening. We gaan hieromheen verschillende webapplicaties bouwen.
In versie 5 van onze praktijkopdracht waren de gegevens van de belastingdienst opgeslagen in een database. Deze versie 5 bestond uit twee afzonderlijke applicaties die echter gemeenschappelijke lagen hadden:
- een applicatie die de belasting in |batch|-modus berekende voor belastingplichtigen die in een tekstbestand waren opgeslagen;
- een applicatie die de belasting in |interactieve| modus berekende voor belastingplichtigen waarvan de gegevens via het toetsenbord werden ingevoerd;
Versie 5 van de applicatie voor de berekening van de belasting in batchmodus had de volgende architectuur:

Uiteindelijk zal de webversie van deze applicatie de volgende architectuur hebben:

- de webclient [1] communiceert met de webserver [2], die op zijn beurt communiceert met de SGBD en [3];
- de webserver [2] behoudt de lagen [métier], [8] en [dao], [9] van de oorspronkelijke applicatie;
- De oorspronkelijke toepassing behoudt zijn hoofdscript [4] en zijn lagen [métier] en [15]. De lagen [métier], [8] en [15] zijn identiek;
- voor de communicatie tussen client en server zijn twee extra lagen nodig:
- de laag [web] [7] die de webapplicatie implementeert;
- de laag [dao] [5], die als client fungeert voor de webapplicatie [7];
In de definitieve versie kan de berekening van de belasting in batches op twee manieren plaatsvinden:
- de zakelijke berekening van de belasting gebeurt via de serverlaag [métier]. Het script [main] zal deze methode gebruiken;
- de bedrijfsspecifieke berekening van de belasting gebeurt via de [métier]-laag van de client. Het script [main2] zal deze methode gebruiken;
Vanaf nu gaan we verschillende client/server-toepassingen van het bovenstaande type ontwikkelen, die elk één of meerdere nieuwe webontwikkelingstechnologieën illustreren.
23.2. De webserver voor belastingberekening
23.2.1. Versie 1

Het script [server_01] is de volgende webtoepassing:

- In [1] wordt een geconfigureerde URL gebruikt, waarin drie waarden worden doorgegeven:
- [marié] (ja / nee) om aan te geven of de belastingplichtige gehuwd is;
- [enfants]: het aantal kinderen van de belastingplichtige;
- [salaire]: het jaarsalaris van de belastingplichtige;
- in [2] stuurt de webserver een tekenreeks jSON terug die het bedrag van de te betalen belasting met de verschillende componenten aangeeft;
De architectuur van de applicatie is als volgt:

- de browser [1] doet een verzoek aan de server [2]. Het script [server_01] implementeert de serverlagen [web] en [2];
- de lagen [3-8] zijn dezelfde die al in |versie 5| van de belastingberekeningsapplicatie worden gebruikt. We nemen ze ongewijzigd over;
- de laag [métier] [3] wordt |hier| gedefinieerd;
- de laag [dao] [4] wordt |hier| gedefinieerd;
De webapplicatie [server_01] wordt geconfigureerd met behulp van drie scripts:
- [config], dat de gehele applicatie configureert;
- [config_database], dat de toegang tot de database configureert. We zullen werken met de scripts SGBD, MySQL en PostgreSQL;
- [config_layers], waarmee de lagen van de applicatie worden geconfigureerd;
Het script [config] is als volgt:
def configure(config: dict) -> dict:
import os
# stap 1 ------
# map van dit bestand
script_dir = os.path.dirname(os.path.abspath(__file__))
# hoofdmap
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# absolute afhankelijkheden
absolute_dependencies = [
# projectmappen
# BaseEntity, MyException
f"{root_dir}/classes/02/entities",
# InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
f"{root_dir}/impots/v04/interfaces",
# AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
f"{root_dir}/impots/v04/services",
# ImpotsDaoWithAdminDataInDatabase
f"{root_dir}/impots/v05/services",
# AdminData, ImpôtsError, TaxPayer
f"{root_dir}/impots/v04/entities",
# Constanten, segmenten
f"{root_dir}/impots/v05/entities",
# IndexController
f"{script_dir}/../controllers",
# scripts [config_database, config_layers]
script_dir,
]
# we stellen de syspath in
from myutils import set_syspath
set_syspath(absolute_dependencies)
# stap 2 ------
# configuratie van de applicatie
# lijst van gebruikers die de applicatie mogen gebruiken
config['users'] = [
{
"login": "admin",
"password": "admin"
}
]
# stap 3 ------
# databaseconfiguratie
import config_database
config["database"] = config_database.configure(config)
# stap 4 ------
# instantiëren van de applicatielagen
import config_layers
config['layers'] = config_layers.configure(config)
# de configuratie wordt doorgegeven
return config
- De functie [configure] ontvangt een woordenboek [config] als parameter (regel 1) en geeft dit als resultaat terug (regel 54) nadat de inhoud ervan is aangevuld. Men had al lang kunnen zeggen dat het niet nodig was om het resultaat [config] terug te geven. [config] is namelijk een woordenboekreferentie die de aanroepende code deelt met de aangeroepen code. De aanroepende code beschikt dus al over deze referentie (regel 1) en het is zinloos om deze nogmaals door te geven (regel 54). Schrijf daarom:
config=[module].configure(config) (1)
is overbodig. Het volstaat om te schrijven:
[module].configure(config) (2)
Toch heb ik de eerste schrijfwijze (1) behouden, omdat ik dacht dat deze wellicht beter liet zien dat de aangeroepen code het woordenboek [config] wijzigde.
- regel 1: het woordenboek [config] dat door de functie [configure] wordt ontvangen, heeft een sleutel ‘sgbd’ waarvan de waarde uit de lijst [‘mysql’, ‘pgres’] wordt gehaald. [mysql] betekent dat de gebruikte database wordt beheerd door MySQL, terwijl ‘pgres’ betekent dat de gebruikte database wordt beheerd door PostgreSQL;
- regels 4-27: hier worden alle mappen opgesomd die elementen bevatten die nodig zijn voor de webapplicatie. Deze zullen deel uitmaken van het Python Path van de applicatie (regels 30-31);
- regels 33-40: we staan alleen bepaalde gebruikers toe om toegang te krijgen tot de applicatie. Hier hebben we een lijst met één enkele gebruiker;
- regels 43-46: het script [config_database] stelt de configuratie van de gebruikte database samen;
- regel 46: de door het script [config_database] opgebouwde configuratie is een woordenboek dat wordt opgeslagen in de algemene configuratie onder de sleutel ‘database’;
- regels 48-51: het script [config_layers] instantiëert de lagen van de webapplicatie. Het retourneert een woordenboek dat wordt opgeslagen in de algemene configuratie, gekoppeld aan de sleutel ‘layers’;
Het script [config_database] is hetzelfde script dat al in |versie 5| werd gebruikt. Ter herinnering geven we het hier nogmaals weer:
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 toewijzingen
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()
# we slaan bepaalde gegevens op en geven ze terug in een woordenboek
return {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
"constantes_table": constantes_table, "session": session}
Het script [config_layers] configureert de lagen van de webserver. We gebruiken hier een |script| dat we al eerder zijn tegengekomen:
def configure(config: dict) -> dict:
# instantiëren van de applicatielagen
# DAO
from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
dao = ImpotsDaoWithAdminDataInDatabase(config)
# bedrijfslogica
from ImpôtsMétier import ImpôtsMétier
métier = ImpôtsMétier()
# de instanties van de lagen worden in een dictionary geplaatst, die vervolgens aan de aanroepende code wordt teruggegeven
return {
"dao": dao,
"métier": métier
}
- regel 6: de laag [dao] wordt geïmplementeerd met een database;
- [ImpotsDaoWithAdminDataInDatabase] is |hier| gedefinieerd;
- [ImpôtsMétier] is |hier| gedefinieerd;
Het hoofdscript [server_01] is als volgt:
# er wordt een parameter mysql of pgres 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})
# afhankelijkheden
from ImpôtsError import ImpôtsError
from TaxPayer import TaxPayer
import re
from flask import request
from myutils import json_response
from flask import Flask
from flask_api import status
# gegevens ophalen bij de belastingdienst
try:
# admindata wordt een alleen-lezen variabele op applicatieniveau
admindata = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
print(f"L'erreur suivante s'est produite : {erreur}")
sys.exit(1)
# Flask-applicatie
app = Flask(__name__)
# Home URL: /?getrouwd=xx&kinderen=yy&salaris=zz
@app.route('/', methods=['GET'])
def index():
# in eerste instantie geen fouten
erreurs = []
# het verzoek moet drie parameters bevatten in URL
if len(request.args) != 3:
erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")
# de burgerlijke staat wordt opgehaald uit de URL
marié = request.args.get('marié')
if marié is None:
erreurs.append("paramètre [marié] manquant")
else:
marié = marié.strip().lower()
erreur = marié != "oui" and marié != "non"
if erreur:
erreurs.append(f"paramétre marié [{marié}] invalide")
# het aantal kinderen wordt opgehaald uit de URL
enfants = request.args.get('enfants')
if enfants is None:
erreurs.append("paramètre [enfants] manquant")
else:
enfants = enfants.strip()
match = re.match(r"^\d+", enfants)
if not match:
erreurs.append(f"paramétre enfants [{enfants}] invalide")
else:
enfants = int(enfants)
# het salaris wordt opgehaald uit het bestand URL
salaire = request.args.get('salaire')
if salaire is None:
erreurs.append("paramètre [salaire] manquant")
else:
salaire = salaire.strip()
match = re.match(r"^\d+", salaire)
if not match:
erreurs.append(f"paramétre salaire [{salaire}] invalide")
else:
salaire = int(salaire)
# ongeldige parameters in het bestand URL?
for key in request.args.keys():
if key not in ['marié', 'enfants', 'salaire']:
erreurs.append(f"paramètre [{key}] invalide")
# Fouten?
if erreurs:
# er wordt een foutmelding naar de klant gestuurd
résultats = {"réponse": {"erreurs": erreurs}}
return json_response(résultats, status.HTTP_400_BAD_REQUEST)
# geen fouten, we kunnen doorgaan
# belastingberekening
taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
config["layers"]["métier"].calculate_tax(taxpayer, admindata)
# het antwoord wordt naar de klant verzonden
return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)
# alleen main
if __name__ == '__main__':
# de Flask-server wordt gestart
app.config.update(ENV="development", DEBUG=True)
app.run()
- regels 1-10: we halen de parameter op die aangeeft welke SGBD moet worden gebruikt;
- regels 12-14: met deze informatie kunnen we de applicatie configureren. Met name wordt het Python-pad samengesteld;
- regels 16-23: met het nieuwe Python-pad importeren we de benodigde elementen;
- regels 25-31: we halen de gegevens van de belastingdienst op waarmee de belasting kan worden berekend;
- regels 33-34: instantiëren van de Flask-applicatie;
- regel 38: de Flask-applicatie verwerkt alleen de URL [/]. Ze verwacht een URL die als volgt is geconfigureerd: [/ ?marié=xx&enfants=yy&salaire=zz] met:
- xx: ja / nee;
- yy: aantal kinderen;
- zz: jaarsalaris;
- regels 40-89: de geldigheid van de parameters van het URL-bestand wordt gecontroleerd;
- regel 41: de foutmeldingen worden verzameld in de lijst [erreurs];
- regel 43: misschien herinner je je nog dat de parameters van de geconfigureerde URL te vinden zijn in [request.args] (zie |hier|):
- het object [request] is het Flask-object dat in regel 20 is geïmporteerd;
- het object [request.args] gedraagt zich als een woordenboek;
- regels 43-44: er wordt gecontroleerd of er precies drie parameters zijn (niet minder, niet meer);
- regels 46-49: er wordt gecontroleerd of de parameter [marié] aanwezig is in URL;
- regels 50-54: als deze aanwezig is, wordt gecontroleerd of de kleine letters van de waarde, zonder de spaties aan het begin en einde, ‘ja’ of ‘nee’ zijn;
- regels 56-59: er wordt gecontroleerd of de parameter [enfants] in URL voorkomt;
- regels 60-66: als deze aanwezig is, wordt gecontroleerd of de waarde ervan een positief geheel getal is;
- regel 66: vergeet niet dat de parameters van URL en hun waarden tekenreeksen zijn. De waarde van de parameter [enfants] wordt omgezet naar ‘int’;
- regels 68-78: voor de parameter [salaire] worden dezelfde controles uitgevoerd als voor de parameter [enfants];
- regels 81-83: er wordt gecontroleerd of er geen andere parameters dan [‘marié, ‘enfants’, ‘salaire’] in URL voorkomen;
- regels 85-89: als na al deze controles de lijst [erreurs] niet leeg is, sturen we deze foutenlijst naar de klant in de vorm van een tekenreeks jSON en de statuscode [400 Bad Request];
Aangezien we later vaak een tekenreeks jSON als antwoord naar de klant zullen moeten sturen, zijn de paar regels die hiervoor nodig zijn ondergebracht in de module [myutils.py] die we al eerder hebben gebruikt:

Het script [myutils.py] ziet er nu als volgt uit:
# imports
import json
import os
import sys
from flask import make_response
def set_syspath(absolute_dependencies: list):
# absolute_dependencies: een lijst met absolute mapnamen
….
# een antwoord genereren HTTP jSON
def json_response(réponse: dict, status_code: int) -> tuple:
# hoofdtekst van het antwoord HTTP
response = make_response(json.dumps(réponse, ensure_ascii=False))
# de hoofdtekst van het antwoord HTTP is afkomstig van jSON
response.headers['Content-Type'] = 'application/json; charset=utf-8'
# het antwoord HTTP wordt verzonden
return response, status_code
- regel 16: de functie [json_response] verwacht twee parameters:
- [réponse]: het woordenboek waarvan de tekenreeks jSON naar de webclient moet worden verzonden;
- [status_code]: de statuscode HTTP van het antwoord;
- regel 18: de hoofdtekst jSON van het antwoord wordt vastgelegd;
- regel 20: de header HTTP wordt toegevoegd, die de webclient laat weten dat hij jSON zal ontvangen;
- regel 22: het antwoord HTTP wordt naar de aanroepende code verzonden. Het is aan deze code om het naar de webclient te verzenden;
Het bestand [__init__.py] verandert als volgt:
from .myutils import set_syspath, json_response
De nieuwe versie van [myutils] wordt geïnstalleerd onder de modules met machinebereik met het commando [pip install .] in een Pycharm-terminal:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install .
Processing c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\packages
Using legacy setup.py install for myutils, since package 'wheel' is not installed.
Installing collected packages: myutils
Attempting uninstall: myutils
Found existing installation: myutils 0.1
Uninstalling myutils-0.1:
Successfully uninstalled myutils-0.1
Running setup.py install for myutils ... done
Successfully installed myutils-0.1
- regel 1: je moet in de map [packages] zijn om deze instructie in te voeren;
De code van het script [server_01] gaat als volgt verder:
…
# fouten?
if erreurs:
# er wordt een foutmelding naar de klant verzonden
résultats = {"réponse": {"erreurs": erreurs}}
return json_response(résultats, status.HTTP_400_BAD_REQUEST)
# geen fouten, we kunnen verdergaan
# belastingberekening
taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': enfants, 'salaire': salaire})
config["layers"]["métier"].calculate_tax(taxpayer, admindata)
# het antwoord wordt naar de klant verzonden
return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)
- regel 10: wanneer je hier bent, zijn de verwachte parameters in URL aanwezig en correct;
- regel 10: we maken het object [TaxPayer] aan dat de belastingplichtige modelleert;
- regel 11: de laag [métier] wordt gevraagd de belasting te berekenen. Ter herinnering: de door de laag [métier] berekende elementen worden ingevoegd in het object [taxpayer] dat als parameter wordt doorgegeven;
- regel 13: het antwoord wordt naar de webclient verzonden in de vorm van een tekenreeks jSON. Dit is de tekenreeks jSON uit een woordenboek. Gekoppeld aan de sleutel [result] wordt hierin het woordenboek van het object [taxpayer] geplaatst. Het object [taxpayer] zelf kon niet worden toegevoegd, omdat dit niet serialiseerbaar is naar jSON;
We maken twee uitvoeringsconfiguraties aan, één voor MySQL en één voor PostgreSQL:

Hier volgen enkele uitvoervoorbeelden (u hebt de applicatie [server_01] en de gebruikte SGBD gestart en vervolgens vraagt u de URL http://localhost:5000/ op met een browser):


Hier volgt een voorbeeld van een uitvoering in de Postman-console:

GET /?mari%C3%A9=xx&enfants=yy&salaire=zz HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: e4c5df8c-4bd6-4250-b789-b7b164db4eff
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 134
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 06:15:44 GMT
{"réponse": {"erreurs": ["paramètre marié [xx] invalide", "paramètre enfants [yy] invalide", "paramètre salaire [zz] invalide"]}}
- regel 1: er wordt een onjuiste URL opgevraagd;
- regel 10: de server reageert met status 400 BAD REQUEST;
23.2.2. Versie 2

In versie 2 van de server wordt de verwerking van de URL geïsoleerd in de module [index_controller] [5]:
# afhankelijkheden importeren
import re
from flask_api import status
from werkzeug.local import LocalProxy
# URL met de volgende parameters: /?getrouwd=xx&kinderen=yy&salaris=zz
def execute(request: LocalProxy, config: dict) -> tuple:
# afhankelijke personen
from TaxPayer import TaxPayer
# in eerste instantie geen fouten
erreurs = []
# het verzoek moet drie parameters bevatten
if len(request.args) != 3:
erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")
# de burgerlijke staat van de URL wordt opgehaald
marié = request.args.get('marié')
if marié is None:
erreurs.append("paramètre [marié] manquant")
else:
marié = marié.strip().lower()
erreur = marié != "oui" and marié != "non"
if erreur:
erreurs.append(f"paramétre marié [{marié}] invalide")
# het aantal kinderen van de URL wordt opgehaald
enfants = request.args.get('enfants')
if enfants is None:
erreurs.append("paramètre [enfants] manquant")
else:
enfants = enfants.strip()
match = re.match(r"^\d+", enfants)
if not match:
erreurs.append(f"paramétre enfants {enfants} invalide")
else:
enfants = int(enfants)
# het salaris van URL wordt opgehaald
salaire = request.args.get('salaire')
if salaire is None:
erreurs.append("paramètre [salaire] manquant")
else:
salaire = salaire.strip()
match = re.match(r"^\d+", salaire)
if not match:
erreurs.append(f"paramétre salaire {salaire} invalide")
else:
salaire = int(salaire)
# zijn er nog andere parameters in de URL?
for key in request.args.keys():
if not key in ['marié', 'enfants', 'salaire']:
erreurs.append(f"paramètre [{key}] invalide")
# fouten?
if erreurs:
# er wordt een foutmelding naar de klant gestuurd
résultats = {"réponse": {"erreurs": erreurs}}
return résultats, status.HTTP_400_BAD_REQUEST
# geen fouten, we kunnen verdergaan
# belastingberekening
taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
config["layers"]["métier"].calculate_tax(taxpayer, config["admindata"])
# het antwoord wordt naar de klant verzonden
return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
- regel 9: de functie [execute] ontvangt twee parameters:
- [request]: de aanvraag HTTP van de client;
- [config]: het configuratiewoordenboek van de applicatie;
Het script [server_02] 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})
# afhankelijkheden
from ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
import index_controller
# gegevens ophalen bij de belastingdienst
try:
# admindata wordt een alleen-lezen variabele op applicatieniveau
config['admindata'] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
print(f"L'erreur suivante s'est produite : {erreur}")
sys.exit(1)
# Flask-applicatie
app = Flask(__name__)
# Home URL: /?getrouwd=xx&kind=yy&salaris=zz
@app.route('/', methods=['GET'])
def index():
# de aanvraag wordt uitgevoerd
résultat, statusCode = index_controller.execute(request, config)
# het antwoord wordt verzonden
return json_response(résultat, statusCode)
# alleen main
if __name__ == '__main__':
# de server wordt gestart
app.config.update(ENV="development", DEBUG=True)
app.run()
- regel 36-41: verwerking van de route / ;
- regel 39: gebruik van de functie [IndexController.execute];
We zullen voortaan deze techniek gebruiken: elke route wordt verwerkt door een eigen module.
De uitvoerresultaten zijn dezelfde als bij versie 1.
23.2.3. Versie 3

Versie 3 introduceert het concept van authenticatie.
Het script [server_03] ziet er nu als volgt uit:
# 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})
# afhankelijkheden
from ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
from flask_httpauth import HTTPBasicAuth
import index_controller
# gegevens ophalen bij de belastingdienst
try:
# config[‘admindata’] wordt een alleen-lezen applicatie-specifieke variabele
config["admindata"] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
print(f"L'erreur suivante s'est produite : {erreur}")
sys.exit(1)
# authenticatiemanager
auth = HTTPBasicAuth()
# authenticatiemethode
@auth.verify_password
def verify_credentials(login: str, password: str) -> bool:
# lijst met gebruikers
users = config['users']
# deze lijst wordt doorlopen
for user in users:
if user['login'] == login and user['password'] == password:
return True
# niet gevonden
return False
# Flask-applicatie
app = Flask(__name__)
# Home URL: /?getrouwd=xx&kind=yy&salaris=zz
@app.route('/', methods=['GET'])
@auth.login_required
def index():
# we voeren de aanvraag uit
résultat, statusCode = index_controller.execute(request, config)
# het antwoord wordt verzonden
return json_response(résultat, statusCode)
# alleen main
if __name__ == '__main__':
# de server wordt gestart
app.config.update(ENV="development", DEBUG=True)
app.run()
- regel 21: er wordt een authenticatiemanager geïmporteerd. Er bestaan verschillende soorten authenticatie bij een webserver. Degene die we hier gebruiken heet [HTTP Basic]. Elk type authenticatie volgt een specifieke client/server-dialoog;
- regel 33: er wordt een instantie van de authenticatiemanager aangemaakt;
- regel 37: de annotatie [@auth.verify_password] markeert de functie die moet worden uitgevoerd wanneer de authenticatiehandler de door de client verzonden gebruikersnaam en wachtwoord wil verifiëren volgens het protocol [HTTP Basic];
- regel 55: de annotatie [@auth.login_required] markeert een route waarvoor de webclient moet worden geauthenticeerd. Als de webclient zijn inloggegevens nog niet heeft verzonden, zal de webserver deze automatisch opvragen volgens het HTTP basic-protocol;
De module [flask_httpauth] moet zijn geïnstalleerd:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\01\flask>pip install flask_httpauth
Collecting flask_httpauth
Downloading Flask_HTTPAuth-4.1.0-py2.py3-none-any.whl (5.8 kB)
Requirement already satisfied: Flask in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from flask_httpauth) (1.1.2)
Requirement already satisfied: itsdangerous>=0.24 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (1.1.0)
Requirement already satisfied: click>=5.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (7.1.2)
Requirement already satisfied: Jinja2>=2.10.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (2.11.2)
Requirement already satisfied: Werkzeug>=0.15 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (1.0.1)
Requirement already satisfied: MarkupSafe>=0.23 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Jinja2>=2.10.1->Flask->flask_httpauth) (1.1.1
)
Installing collected packages: flask-httpauth
Successfully installed flask-httpauth-4.1.0
Laten we eens kijken wat er gebeurt met de Postman-console. U:
- maak een uitvoerconfiguratie aan;
- start de webapplicatie;
- start de SGBD van je keuze;
- vraag de URL [/] op met Postman;
De client/server-dialoog in de Postman-console ziet er als volgt uit:
- regel 10: de server antwoordt dat we geen toegang hebben tot de URL [/];
- regel 13: de server geeft aan welk authenticatieprotocol we moeten gebruiken, in dit geval het zogenaamde Basic-authenticatieprotocol;
Het is mogelijk om Postman zo te configureren dat het de inloggegevens van de gebruiker volgens het Basic-authenticatieprotocol verstuurt:

- in [6-7] vullen we de inloggegevens in die in het script [config] staan:
config['users'] = [
{
"login": "admin",
"password": "admin"
}
]
De client-serverdialoog in de Postman-console ziet er dan als volgt uit:
GET / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 5ce20822-e87c-4eef-a2f4-b9eaec38d881
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 203
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 07:20:01 GMT
{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]", "paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
- regel 2: de Postman-client verstuurt de inloggegevens van de gebruiker [admin / admin] in gecodeerde vorm;
- regel 17: de server reageert correct. Hij meldt fouten omdat de parameters [marié, enfants, salaire] (regel 1) niet zijn verzonden, maar hij meldt geen authenticatiefout;
Laten we nu URL / opvragen met een browser (hieronder Firefox):

- Net als bij Postman heeft Firefox het antwoord HTTP van de server ontvangen met de headers HTTP:
Firefox en andere browsers onderbreken het dialoogvenster niet wanneer ze deze headers ontvangen. Ze vragen de gebruiker om de door de server gevraagde inloggegevens. Hierboven volstaat het om admin / admin in te voeren om het antwoord van de server te ontvangen:

23.3. De webclient van de belastingberekeningsserver
23.3.1. Inleiding
In de vorige paragraaf was de webclient van de belastingberekeningsserver een browser. In dit deel is de webclient een consolescript. De architectuur ziet er als volgt uit:

- de webclient bestaat uit de lagen [1-2];
- de webserver bestaat uit de lagen [3-9]. Dit is in de vorige paragraaf beschreven;
We moeten dus de lagen [1-2] schrijven.
De laag [dao] [2] moet kunnen communiceren met de webserver [3]. We kennen nu het protocol HTTP en zouden bijvoorbeeld met de reeds besproken module [pycurl] een script kunnen schrijven dat communiceert met de webserver [3]. Er bestaan echter modules die gespecialiseerd zijn in client-serverdialogen HTTP. We gaan een daarvan gebruiken, namelijk de module [requests]:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\01\flask>pip install requests
Collecting requests
Downloading requests-2.24.0-py2.py3-none-any.whl (61 kB)
|| 61 kB 137 kB/s
Collecting idna<3,>=2.5
Downloading idna-2.10-py2.py3-none-any.whl (58 kB)
|| 58 kB 692 kB/s
Collecting chardet<4,>=3.0.2
Downloading chardet-3.0.4-py2.py3-none-any.whl (133 kB)
|| 133 kB 1.3 MB/s
Collecting urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1
Downloading urllib3-1.25.9-py2.py3-none-any.whl (126 kB)
|| 126 kB 1.1 MB/s
Collecting certifi>=2017.4.17
Downloading certifi-2020.6.20-py2.py3-none-any.whl (156 kB)
|| 156 kB 1.1 MB/s
Installing collected packages: idna, chardet, urllib3, certifi, requests
Successfully installed certifi-2020.6.20 chardet-3.0.4 idna-2.10 requests-2.24.0 urllib3-1.25.9
De structuur van de scripts van de webserver is als volgt:

Het script implementeert de in |versie 1| beschreven applicatie voor de berekening van de belasting in batchmodus. De nieuwste versie van deze applicatie is |versie 5|. We herhalen even hoe deze werkt:
- de belastingplichtigen voor wie de belasting wordt berekend, worden verzameld in het tekstbestand [taxpayersdata.txt]:
- de resultaten worden opgeslagen in twee bestanden:
- het tekstbestand [errors.txt] bevat de fouten die in het bestand met belastingplichtigen zijn gedetecteerd:
Analyse du fichier C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-clients\01\main/../data/input/taxpayersdata.txt
Ligne 15, not enough values to unpack (expected 4, got 2)
Ligne 17, MyException[1, L'identifiant d'une entité <class 'TaxPayer.TaxPayer'> doit être un entier >=0]
- (vervolg)
- Het bestand jSON [résultats.json] bevat de resultaten van de belastingberekeningen van de verschillende belastingplichtigen:
[
{
"id": 0,
"marié": "oui",
"enfants": 2,
"salaire": 55555,
"impôt": 2814,
"surcôte": 0,
"taux": 0.14,
"décôte": 0,
"réduction": 0
},
{
"id": 1,
"marié": "oui",
"enfants": 2,
"salaire": 50000,
"impôt": 1384,
"surcôte": 0,
"taux": 0.14,
"décôte": 384,
"réduction": 347
},
…
]
23.3.2. Configuratie van de webclient

De configuratie gebeurt met behulp van twee scripts:
- [config], dat zorgt voor de volledige configuratie buiten de architectuurlagen om;
- [config_layers], dat de configuratie van de architectuurlagen verzorgt;
Het script [config] is als volgt:
def configure(config: dict) -> dict:
import os
# stap 1 ------
# map van dit bestand
script_dir = os.path.dirname(os.path.abspath(__file__))
# hoofdmap
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# absolute afhankelijkheden
absolute_dependencies = [
# projectmappen
# BaseEntity, MyException
f"{root_dir}/classes/02/entities",
# InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
f"{root_dir}/impots/v04/interfaces",
# AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
f"{root_dir}/impots/v04/services",
# ImpotsDaoWithAdminDataInDatabase
f"{root_dir}/impots/v05/services",
# AdminData, ImpôtsError, TaxPayer
f"{root_dir}/impots/v04/entities",
# Constanten, segmenten
f"{root_dir}/impots/v05/entities",
# ImpôtsDaoWithHttpClient
f"{script_dir}/../services",
# configuratiescripts
script_dir,
]
# het syspath instellen
from myutils import set_syspath
set_syspath(absolute_dependencies)
# stap 2 ------
# de applicatie configureren met constanten
config.update({
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
"resultsFilename": f"{script_dir}/../data/output/résultats.json",
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
"server": {
"urlServer": "http://127.0.0.1:5000/",
"authBasic": True,
"user": {
"login": "admin",
"password": "admin"
}
}
}
)
# stap 3 ------
# instantie van de lagen
import config_layers
config['layers'] = config_layers.configure(config)
# de configuratie wordt doorgevoerd
return config
- regel 1: de functie [configure] ontvangt als parameter het woordenboek dat met de configuratiegegevens moet worden gevuld. Dit kan al vooraf ingevuld of leeg zijn. Hier is het leeg;
- regels 40-42: de absolute namen van de drie tekstbestanden die door de laag [dao] worden beheerd;
- regels 43-50: gekoppeld aan de sleutel [server], de informatie die de laag [dao] moet kennen over de webserver waarmee deze moet communiceren:
- regel 44: de URL van de webservice;
- regel 45: de sleutel [authBasic] is True als voor toegang tot de URL authenticatie van het type Basic vereist is;
- regels 46-49: de inloggegevens van de gebruiker die zich zal authenticeren als authenticatie wordt gevraagd;
- regels 56-57: we instantiëren de lagen, in dit geval de enige laag [dao], en plaatsen de verwijzingen naar de lagen in [config], gekoppeld aan de sleutel [layers];
Het script [config_layers] is als volgt:
def configure(config: dict) -> dict:
# instantie van de lagen van de applicatie
# DAO-laag
from ImpôtsDaoWithHttpClient import ImpôtsDaoWithHttpClient
dao = ImpôtsDaoWithHttpClient(config)
# de configuratie van de lagen wordt weergegeven
return {
"dao": dao
}
- regel 1: de functie [configure] ontvangt het woordenboek waarmee de applicatie wordt geconfigureerd;
- regels 4-6: de laag [dao] wordt geïnstantieerd. In regel 6 wordt de configuratie van de applicatie doorgegeven, waarin de laag de benodigde informatie vindt;
- regels 8-11: er wordt een woordenboek geretourneerd waarin de referentie van de laag [dao] is opgenomen;
23.3.3. Het hoofdscript [main]
Het hoofdscript [main] is een variant van dat van |versie 5|:
# de applicatie configureren
import config
config = config.configure({})
# afhankelijkheden
from ImpôtsError import ImpôtsError
# code
try:
# de laag wordt opgehaald [dao]
dao = config["layers"]["dao"]
# het uitlezen van de gegevens van de belastingplichtigen
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# van de belastingplichtigen?
if not taxpayers:
raise ImpôtsError(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' wordt gewijzigd
dao.calculate_tax(taxpayer)
# 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é...")
- regels 2-3: de toepassing is geconfigureerd;
- regel 13: de laag [dao] levert de lijst met belastingplichtigen voor wie de belasting moet worden berekend;
- regel 21: de laag [dao] berekent de belasting voor elk van hen;
- regel 23: de resultaten worden opgeslagen in een bestand jSON;
23.3.4. Implementatie van de laag [dao]

Laten we nog eens terugkomen op de gebruikte client/server-architectuur:

- in [2, 6] zien we dat de laag [dao] twee functies heeft:
- ze heeft toegang tot het bestandssysteem om zowel de gegevens van de belastingplichtigen te lezen als de resultaten van de belastingberekeningen op te slaan. We hebben al een klasse |AbstractImpôtsDao| die dit kan doen. Deze werd al vanaf |versie 4| gebruikt;
- ze communiceert met de webserver [3];
In |versie 5| communiceerde het hoofdscript [main] [1] rechtstreeks met de laag [métier] [4]. We willen dit script liever niet wijzigen. Daarom zorgen we ervoor dat de laag [dao] [2] de interface van de laag [métier] [4] implementeert. Zo zal het hoofdscript [main] de indruk hebben dat het rechtstreeks communiceert met de laag [métier] [4] en kan het volledig negeren dat deze zich op een andere machine bevindt.
Een definitie van de klasse die de laag [dao] [2] implementeert, zou er als volgt uit kunnen zien:
class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
- de klasse [ImpôtsDaoWithHttpClient]:
- erft van de klasse [AbstractImpôtsDao], waardoor het de communicatie met het bestandssysteem [6] kan afhandelen;
- implementeert de interface [InterfaceImpôtsMétier], zodat het hoofdscript [main] van |versie 5| niet hoeft te worden gewijzigd;
De volledige code van de klasse [ImpôtsDaoWithHttpClient] is als volgt:
# import
import requests
from flask_api import status
from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsMétier import InterfaceImpôtsMétier
from TaxPayer import TaxPayer
class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
# constructor
def __init__(self, config: dict):
# initialisatie van bovenliggende klasse
AbstractImpôtsDao.__init__(self, config)
# parameters opslaan
self.__config_server = config["server"]
# ongebruikte methode van [AbstractImpôtsDao]
def get_admindata(self) -> AdminData:
pass
# belastingberekening
def calculate_tax(self: object, taxpayer: TaxPayer, admindata: AdminData = None):
# uitzonderingen worden doorgegeven
# get-parameters
params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
# verbinding met Auth Basic-authenticatie?
if self.__config_server['authBasic']:
response = requests.get(
# URL van de opgevraagde server
self.__config_server['urlServer'],
# parameters van de URL
params=params,
# Basic-authenticatie
auth=(
self.__config_server["user"]["login"],
self.__config_server["user"]["password"]))
else:
# verbinding zonder Basic-authenticatie
response = requests.get(self.__config_server['urlServer'], params=params)
# verificatie
print(response.text)
# statuscode van het antwoord HTTP
status_code = response.status_code
# het antwoord jSON wordt in een woordenboek opgeslagen
résultat = response.json()
# fout als de statuscode afwijkt van 200 OK
if status_code != status.HTTP_200_OK:
# we weten dat de fouten zijn gekoppeld aan de sleutel [erreurs] van het antwoord
raise ImpôtsError(87, résultat['réponse']['erreurs'])
# het is bekend dat het resultaat is gekoppeld aan de sleutel [result] van het antwoord
# de invoerparameter wordt aangepast met dit resultaat
taxpayer.fromdict(résultat["réponse"]["result"])
- regels 21-23: de klasse [AbstractImpôtsDao] (regel 12) heeft een abstracte methode [get_admindata]. We zijn verplicht deze te implementeren, ook al gebruiken we deze niet (admindata wordt beheerd door de server, niet door de client);
- regel 26: de methode [calculate_tax] behoort tot de interface [InterfaceImpôtsMétier] (regel 12). We moeten deze implementeren;
- regel 15: de constructor ontvangt als enige parameter het woordenboek met de configuratie van de applicatie;
- regels 16-17: de bovenliggende klasse [AbstractImpôtsDao] wordt geïnitialiseerd door ook hier de configuratie van de applicatie door te geven. Daarin vindt ze de namen van de drie tekstbestanden die ze moet beheren;
- regels 18-19: de gegevens over de webserver voor de belastingberekening worden lokaal in de klasse opgeslagen;
- regel 26: de methode [calculate_tax] ontvangt als parameter een object van het type |Taxpayer|. Om de signatuur van de methode [InterfaceImpôtsMétier.calculate_tax] te respecteren, ontvangt deze ook een parameter [admindata], die bedoeld is om de gegevens van de belastingdienst in te kapselen. Aan de clientzijde beschikken we niet over deze gegevens. Deze parameter blijft altijd op [None] staan. Deze onhandige oplossing wijst erop dat de klasse [ImpôtsMétier] aanvankelijk verkeerd is geschreven:
- de handtekening van [calculate_tax] had simpelweg moeten zijn:
def calculate_tax(self, taxpayer: TaxPayer)
en de parameter [admindata : AdminData] had aan de constructor van de klasse moeten worden doorgegeven;
- regel 27: de code van de methode [calculate_tax] is niet ingekapseld in een try / catch / finally. Dit betekent dat eventuele uitzonderingen niet worden afgehandeld en worden doorgegeven aan de aanroepende code, in dit geval het script [main]. Dit script vangt wel alle uitzonderingen op die vanuit de laag [dao] worden doorgegeven;
- regel 28: de belastingberekening vindt plaats aan de serverzijde. Er moet dus communicatie met de server plaatsvinden. Dit gebeurt met de module [requests], die op regel 2 wordt geïmporteerd;
- regels 31-43: om een verzoek GET naar de webserver te sturen, gebruiken we de methode [requests.get]:
- regels 33-34: de eerste parameter van de methode is de URL waarmee contact moet worden gelegd;
- regels 35-40: de twee andere parameters zijn benoemde parameters waarvan de volgorde niet uitmaakt;
- regels 35-36: de waarde van de benoemde parameter [params] moet een woordenboek zijn dat de informatie bevat die in de URL moet worden opgenomen in de vorm [/url ?param1=valeur1¶m2=valeur2&…];
- regel 29: het woordenboek met de drie parameters [marié, enfants, salaire] die de webserver verwacht. We hoeven ons geen zorgen te maken over de codering (ook wel urlencoded genoemd) die deze parameters moeten ondergaan. [requests] zorgt daarvoor;
- regels 37-40: de parameter met de naam [auth] is een tuple met twee elementen (login, password). Deze vertegenwoordigt de inloggegevens voor een Basic-authenticatie;
- regels 44-45: deze twee regels dienen uitsluitend ter illustratie (ze worden uitgecommentarieerd zodra het debuggen is voltooid):
- [response] vertegenwoordigt het antwoord HTTP van de server;
- [response.text] vertegenwoordigt de tekst van het document dat in dit antwoord is ingekapseld. Tijdens het debuggen is het nuttig om te controleren wat de server ons heeft gestuurd;
- regel 47: [response.status_code] is de statuscode HTTP van het ontvangen antwoord. Onze server verstuurt er slechts drie:
- 200 OK
- 400 BAD REQUEST
- 500 INTERNAL SERVER ERROR
- regel 49: onze server verstuurt altijd jSON, zelfs in geval van een fout. De functie [response.json()] maakt een woordenboek aan op basis van de ontvangen tekenreeks jSON. Laten we de twee mogelijke vormen voor de tekenreeks jSON nog eens op een rijtje zetten:
{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]", "paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
{"réponse": {"result": {"id": 0, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
- regels 51-53: als de statuscode niet 200 is, wordt er een uitzondering gegenereerd met de foutmeldingen die in het antwoord zijn opgenomen;
- regel 56: het woordenboek dat door de belastingberekening is gegenereerd, wordt opgehaald en gebruikt om de invoerparameter [taxpayer] bij te werken;
23.3.5. Uitvoering
Om de client uit te voeren:
- start de server [server_03] met de SGBD van uw keuze;
- voer het script [main] van de client uit;
De resultaten zijn te vinden in de map [data/output]. Deze zijn hetzelfde als voor versie 5.
23.4. Tests van de laag [dao]
Laten we terugkeren naar de architectuur van de client/server-toepassing:
- In de geschreven client hebben we ervoor gezorgd dat de laag [dao] [1] dezelfde interface biedt als de laag [métier] [3]. We gaan dus in [4] de reeds besproken testklasse |TestDaoMétier| gebruiken om de laag [métier] [3] te testen;
De testklasse wordt uitgevoerd in de volgende omgeving:

- de configuratie [2] is identiek aan de configuratie [1] die we zojuist hebben bestudeerd;
De testklasse [TestHttpClientDao] is als volgt:
import unittest
class TestHttpClientDao(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})
dao.calculate_tax(taxpayer)
# 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})
dao.calculate_tax(taxpayer)
# 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__':
# de applicatie wordt geconfigureerd
import config
config = config.configure({})
# DAO-laag
dao = config['layers']['dao']
# testmethoden uitvoeren
print("tests en cours...")
unittest.main()
Deze klasse is vergelijkbaar met |die| welke we al in versie 4 van de applicatie hebben besproken.
- regels 40-41: de testomgeving wordt geconfigureerd;
- regel 44: er wordt een verwijzing opgehaald naar de laag [dao];
- regels 47-48: de tests worden uitgevoerd;
Om de tests uit te voeren, maken we een |uitvoeringsconfiguratie| aan:

- we maken een uitvoerconfiguratie aan voor een consolescript, niet voor een test UnitTest;
Wanneer deze configuratie wordt uitgevoerd, krijgt men de volgende resultaten:
De 11 tests zijn geslaagd.