Skip to content

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:

Image

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

Image

  • 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

Image

Het script [server_01] is de volgende webtoepassing:

Image

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

Image

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

Image

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:

Image

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

Image

Image

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

Image


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

Image

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

Image

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:

GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: e65e2a28-4fe3-423b-88b3-b3e5a83092b1
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 401 UNAUTHORIZED
Content-Type: text/html; charset=utf-8
Content-Length: 19
WWW-Authenticate: Basic realm="Authentication Required"
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 07:05:37 GMT

Unauthorized Access
  • 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:

Image

  • in [6-7] vullen we de inloggegevens in die in het script [config] staan: Image

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

Image

  • Net als bij Postman heeft Firefox het antwoord HTTP van de server ontvangen met de headers HTTP:
1
2
3
4
HTTP/1.0 401 UNAUTHORIZED
WWW-Authenticate: Basic realm="Authentication Required"

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:

Image

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:

Image

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

Image

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]:
# geldige gegevens: id, getrouwd, kinderen, salaris
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# er worden foutieve rijen aangemaakt
# onvoldoende waarden
11,12
# onjuiste waarden
x,x,x,x
  • 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

Image

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]

Image

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

Image

  • 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&param2=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:

Image

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

Image

  • we maken een uitvoerconfiguratie aan voor een consolescript, niet voor een test UnitTest;

Wanneer deze configuratie wordt uitgevoerd, krijgt men 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/http-clients/01/tests/TestHttpClientDao.py
tests en cours...
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
....{"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}}}
...{"réponse": {"result": {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}}}
....
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}}}
----------------------------------------------------------------------
Ran 11 tests in 0.130s

OK

Process finished with exit code 0

De 11 tests zijn geslaagd.