24. Toepassingsopdracht: versie 7
24.1. Inleiding
Versie 7 van de belastingberekeningsapplicatie is identiek aan versie 6, op de volgende details na:
- de webclient zal meerdere HTTP-verzoeken tegelijkertijd starten. In de vorige versie werden deze verzoeken achtereenvolgens gestart. De server verwerkte toen op elk moment slechts één verzoek;
- de server wordt multithreaded: hij kan meerdere verzoeken tegelijkertijd verwerken;
- om de uitvoering van deze verzoeken te volgen, wordt de webserver uitgerust met een logger waarmee de belangrijke momenten van de verwerking van de verzoeken in een tekstbestand worden vastgelegd;
- de server zal een e-mail sturen naar de beheerder van de applicatie wanneer hij een probleem tegenkomt waardoor hij niet kan opstarten, meestal een probleem met de database die aan de webserver is gekoppeld;
De architectuur van de applicatie blijft ongewijzigd:
De structuur van de scripts is als volgt:

De map [http-servers/02] wordt eerst aangemaakt door de map [http-servers/01] te kopiëren. Vervolgens worden daar wijzigingen in aangebracht.
24.2. De hulpprogramma’s

24.2.1. De klasse [Logger]
Met de klasse [Logger] kunnen bepaalde acties van de webserver in een tekstbestand worden gelogd:
import codecs
import threading
from datetime import date, datetime
from threading import current_thread
from ImpôtsError import ImpôtsError
class Logger:
# klasseattribuut
verrou = threading.RLock()
# constructor
def __init__(self, logs_filename: str):
try:
# het bestand wordt geopend in de modus 'toevoegen' (a)
self.__resource = codecs.open(logs_filename, "a", "utf-8")
except BaseException as erreur:
raise ImpôtsError(18, f"{erreur}")
# een logboek schrijven
def write(self, message: str):
# datum/tijdstip
today = date.today()
now = datetime.time(datetime.now())
# naam van de thread
thread_name = current_thread().name
# we willen niet gestoord worden terwijl we in het logbestand schrijven
# het synchronisatieobject (= de vergrendeling) van de klasse wordt aangevraagd – slechts één thread krijgt dit
Logger.verrou.acquire()
try:
# het logboek schrijven
self.__resource.write(f"{today} {now}, {thread_name} : {message}")
# onmiddellijk schrijven – anders wordt de tekst pas geschreven wanneer de schrijfstroom wordt gesloten
# of we willen de logbestanden in de tijd volgen
self.__resource.flush()
finally:
# we geven het synchronisatieobject (= de vergrendeling) vrij, zodat een andere thread het kan verkrijgen
Logger.verrou.release()
# vrijgeven van de bronnen
def close(self):
# het bestand sluiten
if self.__resource:
self.__resource.close()
- regels 10-11: hier wordt een klasse-attribuut gedefinieerd. Een klasse-attribuut is een eigenschap die door alle instanties van de klasse wordt gedeeld. Er wordt naar verwezen met de notatie [Classe.attribut_de_classe] (regels 30, 39). Het klasseattribuut [verrou] fungeert als synchronisatieobject voor alle threads die de code van de regels 31-36 uitvoeren;
- regels 14-19: de constructor ontvangt de absolute naam van het logbestand. Dit bestand wordt vervolgens geopend en de opgehaalde bestandsdescriptor wordt in de klasse opgeslagen;
- regel 17: het logbestand wordt geopend in de modus ‘append’ (a). Elke geschreven regel wordt aan het einde van het bestand toegevoegd;
- regels 22-39: met de methode [write] kan een als parameter doorgegeven bericht in het logbestand worden geschreven. Hieraan worden twee gegevens toegevoegd:
- regel 24: de huidige datum;
- regel 25: het huidige tijdstip;
- regel 27: de naam van de thread die het logboek bijwerkt. Hierbij mag niet worden vergeten dat een webapplicatie meerdere gebruikers tegelijk bedient. Aan elk verzoek wordt een thread toegewezen om het uit te voeren. Als deze thread wordt gepauzeerd, meestal voor een invoer-/uitvoerbewerking (netwerk, bestanden, database), dan wordt de processor aan een andere thread toegewezen. Vanwege deze mogelijke onderbrekingen kunnen we er niet zeker van zijn dat een thread erin zal slagen een regel in het logbestand te schrijven zonder te worden onderbroken. Het risico bestaat dan dat logs van twee verschillende threads door elkaar raken. Het risico is klein, misschien zelfs nihil, maar we hebben niettemin besloten te laten zien hoe de toegang van twee threads tot een gemeenschappelijke bron, in dit geval het logbestand, gesynchroniseerd kan worden;
- regel 30: voordat de thread gaat schrijven, vraagt hij de sleutel van de toegangsdeur op. De opgevraagde sleutel is degene die in regel 11 is aangemaakt. Deze is inderdaad uniek: een klasse-attribuut is uniek voor alle instanties van de klasse;
- op tijdstip T1 krijgt de thread Thread1 de sleutel. Hij kan dan regel 33 uitvoeren;
- op tijdstip T2 wordt de thread Thread1 gepauzeerd nog voordat het schrijven van het logboek is voltooid;
- op tijdstip T3 moet de thread Thread2, die de processor heeft gekregen, ook een logboek schrijven. Hij komt dan bij regel 30, waar hij om de sleutel van de voordeur vraagt. Hij krijgt te horen dat een andere thread die al heeft. Hij wordt dan automatisch gepauzeerd. Dit geldt voor alle threads die om deze sleutel vragen;
- op tijdstip T4 krijgt de thread Thread1, die in de wacht was gezet, de processor weer toegewezen. Hij voltooit vervolgens het schrijven van het logboek;
- regels 32-36: het schrijven naar het logbestand gebeurt in twee stappen:
- regel 33: de bestandsdescriptor die in regel 17 is verkregen, werkt met een buffer. De bewerking [write] in regel 33 schrijft naar deze buffer, maar niet rechtstreeks naar het bestand. De buffer wordt vervolgens onder bepaalde voorwaarden naar het bestand geleegd:
- de buffer is vol;
- de bestandsdescriptor ondergaat een bewerking [close] of [flush];
- regel 36: het schrijven van de logregel naar het bestand wordt geforceerd. We doen dit omdat we willen zien hoe de logs van de verschillende threads door elkaar heen staan. Als we dit niet doen, worden de logs van een thread allemaal tegelijk geschreven bij het sluiten van de descriptor, regel 45. Het zou dan veel moeilijker zijn om te zien dat bepaalde threads zijn gestopt: we zouden dan naar de tijdstippen in de logs moeten kijken;
- regel 39: de thread Thread1 geeft de sleutel terug die hij had gekregen. Deze kan nu aan een andere thread worden toegewezen;
- regel 22: de methode [write] is dus gesynchroniseerd: er schrijft slechts één thread tegelijk naar het logbestand. De sleutel van het geheel is regel 30: wat er ook gebeurt, slechts één thread krijgt de sleutel om door te gaan naar de volgende regel. Deze houdt de sleutel vast totdat hij deze teruggeeft (regel 39);
- regels 41-45: met de methode [close] kunnen de aan de descriptor van het logbestand toegewezen bronnen worden vrijgegeven;
De logberichten die in het logbestand worden geschreven, zien er als volgt uit:
24.2.2. De klasse [SendAdminMail]
Met de klasse [SendAminMail] kan een bericht naar de beheerder van de applicatie worden verzonden wanneer deze ‘crasht’.

De klasse [SendAdminMail] wordt in het script [config] [2] als volgt geconfigureerd:
# serverconfiguratie SMTP
"adminMail": {
# server SMTP
"smtp-server": "localhost",
# serverpoort SMTP
"smtp-port": "25",
# beheerder
"from": "guest@localhost.com",
"to": "guest@localhost.com",
# onderwerp van de e-mail
"subject": "plantage du serveur de calcul d'impôts",
# tls op True als de server SMTP authenticatie vereist, anders op False
"tls": False
}
De klasse [SendAdminMail] ontvangt het woordenboek uit de regels 2-13 en de configuratie voor het verzenden van de e-mail. De klasse ziet er als volgt uit:
# imports
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate
class SendAdminMail:
# -----------------------------------------------------------------------
@staticmethod
def send(config: dict, message: str, verbose: bool = False):
# verzendt een bericht naar de SMTP-server config['smtp-server'] op poort config[smtp-port]
# als config['tls'] waar is, wordt de ondersteuning TLS gebruikt
# de e-mail wordt verzonden namens config['from']
# voor de ontvanger config['to']
# het bericht heeft als onderwerp config['subject']
# de verwijzing naar een logger is te vinden in config['logger']
# de logger wordt opgehaald uit de configuratie – kan gelijk zijn aan None
logger = config["logger"]
# server SMTP
server = None
# het bericht wordt verzonden
try:
# de server SMTP
server = smtplib.SMTP(config["smtp-server"])
# uitgebreide modus
server.set_debuglevel(verbose)
# beveiligde verbinding?
if config['tls']:
# begin beveiligingsdialoog
server.starttls()
# authenticatie
server.login(config["user"], config["password"])
# opbouw van een Multipart-bericht – dit bericht wordt verzonden
msg = MIMEText(message)
msg['From'] = config["from"]
msg['To'] = config["to"]
msg['Date'] = formatdate(localtime=True)
msg['Subject'] = config["subject"]
# het bericht wordt verzonden
server.send_message(msg)
# log - de logger bestaat mogelijk niet
if logger:
logger.write(f"[SendAdminMail] Message envoyé à [{config['to']}] : [{message}]\n")
except BaseException as erreur:
# log - de logger bestaat mogelijk niet
if logger:
logger.write(
f"[SendAdminMail] Erreur [{erreur}] lors de l'envoi à [{config['to']}] du message [{message}] : \n")
finally:
# we zijn klaar – we geven de door de functie gebruikte bronnen vrij
if server:
server.quit()
- regels 24-54: hier vinden we de code terug die we al in het voorbeeld |smtp/02| hebben besproken;
- regel 20: hier wordt de referentie van een logboekverzamelaar opgehaald. Deze wordt gebruikt in de regels 45 en 49;
24.3. De webserver

24.3.1. Configuratie

De configuratie van de server lijkt sterk op die van de eerder besproken server. Alleen het bestand [config.py] is enigszins gewijzigd:
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"{root_dir}/impots/http-servers/01/controllers",
# scripts [config_database, config_layers]
script_dir,
# Logger, SendAdminMail
f"{script_dir}/../utilities",
]
# het syspath instellen
from myutils import set_syspath
set_syspath(absolute_dependencies)
# stap 2 ------
# configuratie van de applicatie
config.update({
# gebruikers die de applicatie mogen gebruiken
"users": [
{
"login": "admin",
"password": "admin"
}
],
# logbestand
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# serverconfiguratie SMTP
"adminMail": {
# server SMTP
"smtp-server": "localhost",
# serverpoort SMTP
"smtp-port": "25",
# beheerder
"from": "guest@localhost.com",
"to": "guest@localhost.com",
# onderwerp van de e-mail
"subject": "plantage du serveur de calcul d'impôts",
# TLS op True als de server SMTP authenticatie vereist, anders op False
"tls": False
},
# pauzetijd van de thread in seconden
"sleep_time": 0
})
# 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
- regels 40-66: in het configuratiedictionary van de server worden de elementen toegevoegd die betrekking hebben op de logger (regel 49) en die betreffende het versturen van een waarschuwingsmail naar de beheerder van de applicatie (regels 51-63);
- regel 65: om de threads beter in actie te kunnen zien, zullen we sommige ervan dwingen te stoppen. [sleep_time] is de duur van de stop, uitgedrukt in seconden;
- regels 27-28: merk op dat we de controller [index_controller] uit de vorige versie 6 gebruiken;
24.3.2. Het hoofdscript [main]
Het hoofdscript [main] is als volgt:
# we wachten op een MySQL- of PostgreSQL-parameter
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
sgbd = sys.argv[1].lower()
erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# de applicatie wordt geconfigureerd
import config
config = config.configure({'sgbd': sgbd})
# afhankelijkheden
from flask import request, Flask
from flask_httpauth import HTTPBasicAuth
import json
import index_controller
from flask_api import status
from SendAdminMail import SendAdminMail
from myutils import json_response
from Logger import Logger
import threading
import time
from random import randint
from ImpôtsError import ImpôtsError
# authenticatiemanager
auth = HTTPBasicAuth()
@auth.verify_password
def verify_password(login, password):
# lijst met gebruikers
users = config['users']
# deze lijst doorlopen
for user in users:
if user['login'] == login and user['password'] == password:
return True
# niet gevonden
return False
# er wordt een e-mail naar de beheerder gestuurd
def send_adminmail(config: dict, message: str):
# er wordt een e-mail verzonden naar de beheerder van de applicatie
config_mail = config["adminMail"]
config_mail["logger"] = config['logger']
SendAdminMail.send(config_mail, message)
# het logbestand controleren
logger = None
erreur = False
message_erreur = None
try:
# logboek
logger = Logger(config["logsFilename"])
except BaseException as exception:
# consoollogboek
print(f"L'erreur suivante s'est produite : {exception}")
# de fout wordt genoteerd
erreur = True
message_erreur = f"{exception}"
# de logger wordt opgeslagen in de configuratie
config['logger'] = logger
# foutafhandeling
if erreur:
# e-mail naar de beheerder
send_adminmail(config, message_erreur)
# einde van de toepassing
sys.exit(1)
# opstartlogboek
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)
# ophalen van gegevens van de belastingdienst
erreur = False
try:
# admindata wordt een alleen-lezen applicatie-gegevensveld
config["admindata"] = config["layers"]["dao"].get_admindata()
# succeslogboek
logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
# fout genoteerd
erreur = True
# foutlogboek
log = f"L'erreur suivante s'est produite : {ex}"
# console
print(log)
# logbestand
logger.write(f"{log}\n")
# e-mail naar de beheerder
send_adminmail(config, log)
# de hoofdthread heeft de logger niet meer nodig
logger.close()
# als er een fout is opgetreden, wordt het programma gestopt
if erreur:
sys.exit(2)
# de Flask-applicatie kan starten
app = Flask(__name__)
# Home URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
…
# alleen main
if __name__ == '__main__':
# de server wordt gestart
app.config.update(ENV="development", DEBUG=True)
app.run(threaded=True)
- regels 1-10: het script verwacht een parameter [mysql / pgres] die aangeeft welke SGBD moet worden gebruikt;
- regels 12-14: de applicatie wordt geconfigureerd (Python Path, lagen, database);
- regels 16-28: de benodigde afhankelijkheden voor de applicatie;
- regels 30-43: beheer van de authenticatie;
- regels 46-51: een functie die een e-mail verstuurt naar de beheerder van de applicatie;
- de functie verwacht twee parameters:
- config: een woordenboek met de sleutels [adminMail] en [logger];
- het te verzenden bericht;
- regels 49-50: de configuratie voor het verzenden wordt voorbereid;
- de e-mail wordt verzonden;
- regels 54-74: we controleren of het logbestand aanwezig is;
- regels 70-74: als het niet gelukt is om het logbestand te openen, wordt er een e-mail naar de beheerder gestuurd en wordt het proces gestopt;
- regels 76-79: het opstarten van de server wordt gelogd;
- regels 81-98: we halen de gegevens van de belastingdienst op uit de database;
- regels 88-98: als het niet is gelukt om deze gegevens op te halen, wordt de fout zowel op de console als in het logbestand geregistreerd;
- regels 100-101: de hoofdthread zal geen logboekvermeldingen meer maken (de aangemaakte threads zullen niet dezelfde bestandsdescriptor gebruiken);
- regels 103-105: als er geen verbinding met de database kon worden gemaakt, wordt het programma gestopt;
- regel 122: de server wordt in multithread-modus gestart;
De functie [index] (regel 114) is als volgt:
# Home URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
logger = None
try:
# loggen
logger = Logger(config["logsFilename"])
# wordt opgeslagen in een configuratie die aan de thread is gekoppeld
thread_config = {"logger": logger}
thread_name = threading.current_thread().name
config[thread_name] = {"config": thread_config}
# het verzoek wordt gelogd
logger.write(f"[index] requête : {request}\n")
# de thread wordt onderbroken als daarom is gevraagd
sleep_time = config["sleep_time"]
if sleep_time != 0:
# de pauze is willekeurig, zodat sommige threads worden onderbroken en andere niet
aléa = randint(0, 1)
if aléa == 1:
# logboek vóór de pauze
logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
# pauze
time.sleep(sleep_time)
# de aanvraag wordt door een controller uitgevoerd
résultat, status_code = index_controller.execute(request, config)
# is er een fatale fout opgetreden?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# er wordt een e-mail verzonden naar de beheerder van de applicatie
config_mail = config["adminMail"]
config_mail["logger"] = logger
SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
# het antwoord wordt gelogd
logger.write(f"[index] {résultat}\n")
# het antwoord wordt verzonden
return json_response(résultat, status_code)
except BaseException as erreur:
# de fout wordt gelogd indien mogelijk
if logger:
logger.write(f"[index] {erreur}")
# het antwoord aan de klant wordt opgesteld
résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
# het antwoord wordt verzonden
return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
finally:
# het logbestand wordt gesloten als het geopend was
if logger:
logger.close()
- regel 4: de functie die wordt uitgevoerd wanneer een gebruiker de URL / opvraagt. Omdat de server multithreaded is (regel 112), wordt er een thread aangemaakt om de functie uit te voeren. Deze thread kan op elk moment worden onderbroken en gepauzeerd om de uitvoering iets later te hervatten. Houd dit altijd in gedachten wanneer de code toegang heeft tot een bron die door alle threads wordt gedeeld. Een dergelijke bron is hier het logbestand: alle threads schrijven erin;
- regel 8: er wordt een instantie van de logger aangemaakt. Alle threads zullen dus een afzonderlijke instantie van de logger hebben. Toch verwijzen al deze loggers naar hetzelfde logbestand. Het is echter belangrijk op te merken dat wanneer een thread zijn logger sluit, dit geen invloed heeft op de loggers van de andere threads;
- regels 9-12: de logger wordt opgeslagen in het woordenboek [config] van de applicatie, gekoppeld aan een sleutel met de naam van de thread. Als er dus n threads tegelijkertijd worden uitgevoerd, worden er n vermeldingen aangemaakt in het woordenboek [config]. [config] is een bron die door alle threads wordt gedeeld. Er kan dus behoefte zijn aan synchronisatie. Ik heb hier een aanname gedaan. Ik ben ervan uitgegaan dat als twee threads tegelijkertijd hun invoer in het bestand [config] zouden aanmaken en de ene door de andere zou worden onderbroken, dit geen gevolgen zou hebben. De onderbroken thread zou het aanmaken van de invoer later kunnen voltooien. Als uit de test zou blijken dat deze aanname onjuist was, zou de toegang tot regel 12 moeten worden gesynchroniseerd;
- regel 10: we plaatsen de logger in een woordenboek;
- regel 11: [threading.current_thread()] is de thread die deze regel uitvoert, dus de thread die de functie [index] uitvoert. We noteren de naam ervan. Elke thread heeft een unieke naam;
- regel 12: we slaan de configuratie van de thread op. Vanaf nu gaan we altijd als volgt te werk: als er informatie is die niet tussen de threads kan worden gedeeld, wordt deze toch in de algemene configuratie geplaatst, maar gekoppeld aan de naam van de thread;
- regel 14: we loggen de aanvraag die we op dat moment uitvoeren;
- regels 15-24: we zetten willekeurig bepaalde threads in de pauze, zodat ze de processor vrijgeven aan een andere thread;
- regel 16: de duur van de pauze (in seconden) wordt uit de configuratie opgehaald;
- regel 17: er vindt alleen een pauze plaats als de pauzeduur niet gelijk is aan 0;
- regel 19: een willekeurig geheel getal in het interval [0, 1]. Alleen de waarden 0 en 1 zijn dus mogelijk;
- regel 20: de thread wordt alleen onderbroken als het willekeurige getal 1 is;
- regel 22: er wordt gelogd dat de thread zal worden onderbroken;
- regel 24: de thread wordt gedurende [sleep_time] seconden onderbroken;
- regel 26: wanneer de thread weer actief wordt, laat deze de aanvraag uitvoeren door de module [index_controller];
- regels 28-32: als deze uitvoering een fout van het type [500 INTERNAL SERVER ERROR] veroorzaakt, wordt er een e-mail naar de beheerder gestuurd;
- regels 30-31: het woordenboek [config_mail] wordt geconfigureerd en doorgegeven aan de klasse [SendAdminMail];
- regel 32: het bericht dat naar de beheerder wordt verzonden, is de tekenreeks jSON uit het resultaat dat naar de klant wordt verzonden;
- regels 33-34: we loggen het antwoord dat we naar de klant gaan sturen (regel 36);
- regels 37-44: afhandeling van een eventuele uitzondering;
- regels 39-40: als de logger bestaat, wordt de opgetreden fout gelogd;
- regels 47-48: we sluiten de logger af als deze bestaat. Uiteindelijk maakt de thread een logger aan aan het begin van de aanvraag en sluit deze af wanneer deze is verwerkt;
24.3.3. De controller [index_controller]
De controller [index_controller] die de verzoeken uitvoert, is die van de vorige versie:

24.3.4. Uitvoering
We starten de Flask-server, de e-mailserver |hMailServer| en de e-mailclient |Thunderbird|. We starten SGBD niet. De server stopt met de volgende console-logs:
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-servers/02/flask/main.py mysql
[serveur] démarrage du serveur
L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]
Process finished with exit code 2
Het logbestand [logs.txt] ziet er als volgt uit:
2020-07-23 11:51:38.324752, MainThread : [serveur] démarrage du serveur
2020-07-23 11:51:40.355510, MainThread : L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]
2020-07-23 11:51:42.464206, MainThread : [SendAdminMail] Message envoyé à [guest@localhost.com] : [L'erreur suivante s'est produite : MyException[27, (mysql.connector.errors.InterfaceError) 2003: Can't connect to MySQL server on 'localhost:3306' (10061 Aucune connexion n’a pu être établie car l’ordinateur cible l’a expressément refusée)
(Background on this error at: http://sqlalche.me/e/13/rvf5)]]
Met Thunderbird controleren we de e-mails van de beheerder [guest@localhost.com]:

Vervolgens start men SGBD en vraagt men om URL en [http://127.0.0.1:5000/?mari%C3%A9=oui&enfants=3&salaire=200000]. De logbestanden zien er dan als volgt uit:
2020-07-23 11:56:38.891753, MainThread : [serveur] démarrage du serveur
2020-07-23 11:56:38.987999, MainThread : [serveur] connexion à la base de données réussie
2020-07-23 11:56:40.586747, MainThread : [serveur] démarrage du serveur
2020-07-23 11:56:40.655254, MainThread : [serveur] connexion à la base de données réussie
2020-07-23 11:56:54.528360, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=ja&kinderen=3&salaris=200000' [GET]>
2020-07-23 11:56:54.530653, Thread-2 : [index] {'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}}}
- regels 1-4: ter herinnering: de server wordt twee keer opgestart omdat de modus [Debug=True] een tweede opstart veroorzaakt;
- regels 5-6: de logbestanden geven ons een idee van de uitvoeringstijd van een verzoek, in dit geval 2,293 milliseconden;
24.4. De webclient


Het bestand [http-clients/02] is een kopie van het bestand [http-clients/01]. Vervolgens worden enkele wijzigingen aangebracht.
24.4.1. De configuratie
De configuratie [config] van de applicatie [http-clients/02] is op enkele details na dezelfde als die van de applicatie [http-clients/01]:
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,
# Logger
f"{root_dir}/impots/http-servers/02/utilities",
]
# het syspath instellen
from myutils import set_syspath
set_syspath(absolute_dependencies)
# stap 2 ------
# configuratie van de applicatie met constanten
config.update({
# bestand met belastingplichtigen
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
# resultatenbestand
"resultsFilename": f"{script_dir}/../data/output/résultats.json",
# foutbestand
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
# logbestand
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# de server voor belastingberekening
"server": {
"urlServer": "http://127.0.0.1:5000/",
"authBasic": True,
"user": {
"login": "admin",
"password": "admin"
}
},
# debugmodus
"debug": True
}
)
# stap 3 ------
# instantiëren van de lagen
import config_layers
config['layers'] = config_layers.configure(config)
# de configuratie wordt doorgegeven
return config
- regels 31-32: we gebruiken dezelfde logger |Logger| als die voor de server;
- regel 49: het absolute pad naar het logbestand;
- regel 60: de modus [debug=True] wordt gebruikt om de antwoorden van de webserver naar het logbestand te schrijven;
24.4.2. De laag [dao]
De code van de klasse [ImpôtsDaoWithHttpClient] is enigszins gewijzigd:
# imports
import requests
from flask_api import status
…
class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
# constructor
def __init__(self, config: dict):
# initialisatie van de bovenliggende laag
AbstractImpôtsDao.__init__(self, config)
# opslag van configuratie-elementen
# algemene configuratie
self.__config = config
# server
self.__config_server = config["server"]
# debugmodus
self.__debug = config["debug"]
# logger
self.__logger = None
# niet-gebruikte methode
def get_admindata(self) -> AdminData:
pass
# belastingberekening
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
# uitzonderingen doorgeven
…
# debugmodus?
if self.__debug:
# logger
if not self.__logger:
self.__logger = self.__config['logger']
# loggen
self.__logger.write(f"{response.text}\n")
# statuscode van het antwoord HTTP
status_code = response.status_code
…
- regel 17: de algemene configuratie wordt opgeslagen. We zullen later zien dat wanneer de constructor van de klasse [ImpôtsDaoWithHttpClient] wordt uitgevoerd, het woordenboek [config] nog niet de sleutel [logger] bevat die op regel 37 wordt gebruikt. Om deze reden kan [self.__logger] (regel 23) niet worden geïnitialiseerd in de constructor;
- regel 21: er is in de configuratie een sleutel [debug] toegevoegd die het logboek van de regels 33-39 regelt;
- regel 34: als men zich in de modus [debug] bevindt;
- regels 36-37: eventuele initialisatie van de eigenschap [self.__logger]. Wanneer de methode [calculate_tax] wordt gebruikt, maakt de sleutel [logger] deel uit van het woordenboek [config];
- regel 39: het tekstdocument dat bij het antwoord HTTP van de server hoort, wordt gelogd;
De laag [dao] zal gelijktijdig door meerdere threads worden uitgevoerd. Hier wordt echter slechts één exemplaar van deze laag aangemaakt (zie config_layers). Er moet dus worden gecontroleerd of de code geen schrijftoegang tot gedeelde gegevens impliceert, typisch de eigenschappen van de klasse [ImpôtsDaoWithHttpClient] die de laag [dao] implementeert. In het bovenstaande voorbeeld wijzigt regel 37 echter een eigenschap van de klasse-instantie. In dit geval heeft dit geen gevolgen, omdat alle threads dezelfde logger delen. Als dat niet het geval was geweest, had de toegang tot regel 37 gesynchroniseerd moeten worden.
24.4.3. Het hoofdscript
Het hoofdscript [main] ontwikkelt zich als volgt:
# de applicatie wordt geconfigureerd
import config
config = config.configure({})
# afhankelijkheden
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger
# uitvoering van de laag [dao] in een thread
# taxpayers is een lijst met belastingplichtigen
def thread_function(dao, logger, taxpayers: list):
…
# lijst met threads van de client
threads = []
logger = None
# code
try:
# logger
logger = Logger(config["logsFilename"])
# we slaan het op in de configuratie
config["logger"] = logger
# we halen de laag op [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(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
# de belasting van de belastingplichtigen berekenen met meerdere threads
i = 0
l_taxpayers = len(taxpayers)
while i < len(taxpayers):
# elke thread verwerkt 1 tot 4 belastingplichtigen
nb_taxpayers = min(l_taxpayers - i, random.randint(1, 4))
# de lijst met belastingplichtigen die door de thread zijn verwerkt
thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
# we verhogen i voor de volgende thread
i += nb_taxpayers
# we maken de thread aan
thread = threading.Thread(target=thread_function, args=(dao, logger, thread_taxpayers))
# de thread wordt toegevoegd aan de lijst met threads van het hoofdscript
threads.append(thread)
# de thread wordt gestart – deze bewerking is asynchroon – er wordt niet gewacht op het resultaat van de thread
thread.start()
# de hoofdthread wacht tot alle door hem gestarte threads zijn voltooid
for thread in threads:
thread.join()
# hier hebben alle threads hun werk voltooid – elk heeft een of meer objecten gewijzigd [taxpayer]
# de resultaten worden opgeslagen in het bestand jSON
dao.write_taxpayers_results(taxpayers)
except BaseException as erreur:
# foutmelding
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# de logger wordt afgesloten
if logger:
logger.close()
# we zijn klaar
print("Travail terminé...")
# einde van de threads die nog zouden kunnen bestaan als er bij een fout is gestopt
sys.exit()
- Het hoofdscript onderscheidt zich van dat van de vorige client doordat het meerdere uitvoeringsthreads genereert om de verzoeken naar de server uit te voeren. De client van versie 6 voerde al zijn verzoeken sequentieel uit. Verzoek nr. i werd pas uitgevoerd nadat het antwoord op verzoek nr. [i-1] was ontvangen. Hier willen we zien hoe de server zich gedraagt wanneer hij meerdere gelijktijdige verzoeken ontvangt. Hiervoor hebben we threads nodig;
- regel 21: de gegenereerde threads worden in een lijst geplaatst. Het is belangrijk te begrijpen dat het script [main] zelf ook wordt uitgevoerd door een thread met de naam [MainThread]. Deze hoofdthread zal andere threads aanmaken die belast worden met het berekenen van de belasting van een of meer belastingplichtigen;
- regel 26: er wordt een logboekfunctie aangemaakt. Deze wordt door alle threads gedeeld;
- regel 32: alle belastingplichtigen worden opgehaald waarvan de belasting moet worden berekend;
- regels 39-51: deze belastingplichtigen worden over meerdere threads verdeeld;
- regels 40-41: elke thread verwerkt 1 tot 4 belastingplichtigen. Dit aantal wordt willekeurig bepaald;
- [random.randint(1, 4)] genereert willekeurig een getal uit de lijst [1, 2, 3, 4];
- de thread mag niet meer dan [l-i] belastingplichtigen bevatten, waarbij [l-i] het aantal belastingplichtigen vertegenwoordigt waaraan nog geen thread is toegewezen;
- er wordt dus de kleinste van de twee waarden genomen;
- regel 43: zodra [nb_taxpayers], het aantal belastingplichtigen dat door de thread wordt verwerkt, bekend is, worden deze uit de lijst met belastingplichtigen gehaald:
- [slice(10,12)] is de verzameling van de indices [10, 11, 12];
- [taxpayers[slice(10,12)]] is de lijst [taxpayers[10], taxpayers[11], taxpayers[12];
- regel 45: de waarde van i, die de lus van regel 39 aanstuurt, wordt verhoogd;
- regel 47: er wordt een thread aangemaakt:
- [target=thread_function] bepaalt de functie die de thread zal uitvoeren. Dit is de functie uit de regels 16-17. Deze functie verwacht drie parameters;
- [ags] is de lijst met de drie parameters die de functie [thread_function] verwacht;
Het aanmaken van een thread voert deze nog niet uit. Er wordt alleen een object aangemaakt;
- regels 48-49: de zojuist aangemaakte thread wordt toegevoegd aan de lijst met threads die door de hoofdthread zijn aangemaakt;
- regel 51: de thread wordt gestart. Deze wordt vervolgens parallel aan de andere actieve threads uitgevoerd. Hier voert hij de functie [thread_function] uit met de argumenten die eraan zijn doorgegeven;
- regels 53-54: de hoofdthread wacht op elk van de threads die hij heeft gestart. Laten we een voorbeeld nemen:
- de hoofdthread heeft drie [th1, th2, th3]-threads gestart;
- de hoofdthread wacht op elk van de threads (regels 53-54) in de volgorde van de for-lus: [th1, th2, th3];
- laten we aannemen dat de threads in de volgorde [th2, th1, th3] worden voltooid;
- de hoofdthread wacht op het einde van th1. Wanneer th2 eindigt, gebeurt er niets;
- wanneer th1 is voltooid, gaat de hoofdthread in de wachtstand voor th2. Deze is echter al voltooid. De hoofdthread gaat dan over naar de volgende thread en wacht op th3;
- wanneer th3 is voltooid, is de hoofdthread klaar met wachten en gaat hij verder met de uitvoering van regel 57;
- regel 57 schrijft de verkregen resultaten naar het resultatenbestand. Dit is een goed voorbeeld van objectreferenties:
- regel 43: de lijst [thread_payers] die aan een thread is gekoppeld, bevat kopieën van de objectreferenties uit de lijst [taxpayers];
- we weten dat de belastingberekening de objecten zal wijzigen waarnaar de verwijzingen in de lijst [thread_payers] verwijzen. Deze objecten zullen worden aangevuld met de resultaten van de belastingberekening. De verwijzingen zelf worden echter niet gewijzigd. De verwijzingen in de oorspronkelijke lijst [taxpayers] ‘zien’ of ‘verwijzen dus naar’ de gewijzigde objecten;
De functie [thread_function] die door de threads wordt uitgevoerd, is de volgende:
# uitvoering van de laag [dao] in een thread
# taxpayers is een lijst met belastingplichtigen
def thread_function(dao, logger, taxpayers: list):
# logboek: start van de thread
thread_name = threading.current_thread().name
logger.write(f"début du thread [{thread_name}] avec {len(taxpayers)} contribuable(s)\n")
# de belasting van de belastingplichtigen wordt berekend
for taxpayer in taxpayers:
# log
logger.write(f"début du calcul de l'impôt de {taxpayer}\n")
# synchrone berekening van de belasting
dao.calculate_tax(taxpayer)
# log
logger.write(f"fin du calcul de l'impôt de {taxpayer}\n")
# log einde van de thread
logger.write(f"fin du thread [{thread_name}]\n")
- functies die gelijktijdig door meerdere threads worden uitgevoerd, zijn vaak lastig te schrijven: men moet altijd controleren of de code niet probeert gegevens te wijzigen die door meerdere threads worden gedeeld. Wanneer dit laatste het geval is, moet er gesynchroniseerde toegang worden ingesteld tot de gedeelde gegevens die zullen worden gewijzigd;
- regel 3: de functie ontvangt drie parameters:
- [dao]: een verwijzing naar de laag [dao]. Deze gegevens worden gedeeld;
- [logger]: een verwijzing naar de logger. Deze gegevens worden gedeeld;
- [taxpayers]: een lijst met belastingplichtigen. Deze gegevens worden niet gedeeld: elke thread beheert een andere lijst;
- laten we de twee referenties [dao, logger] eens bekijken:
- we hebben gezien dat het object waarnaar de referentie [dao] verwijst, een referentie [self.__logger] had die door de threads werd gewijzigd, maar dan om er een waarde in te zetten die voor alle threads gelijk is;
- de referentie [logger] verwijst naar een bestandsdescriptor. We hebben gezien dat er een probleem kon ontstaan bij het schrijven van de logbestanden. Om deze reden is het schrijven naar het bestand gesynchroniseerd;
- regels 5-6: de naam van de thread en het aantal belastingplichtigen dat deze moet beheren, worden gelogd;
- regels 8-14: berekening van de belasting van de belastingplichtigen;
- regel 16: het einde van de thread wordt gelogd;
24.4.4. Uitvoering
Laten we de webserver starten zoals in de vorige paragraaf (webserver, SGBD, hMailServer, Thunderbird), en vervolgens het clientscript [main] uitvoeren. In de bestanden [data/output/errors.txt, data/output/résultats.json] zien we dezelfde resultaten als in de vorige versie. In het bestand [data/logs/logs.txt] zien we de volgende logberichten:
2020-07-24 10:05:20.942404, Thread-1 : début du thread [Thread-1] avec 1 contribuable(s)
2020-07-24 10:05:20.943458, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-24 10:05:20.943458, Thread-2 : début du thread [Thread-2] avec 3 contribuable(s)
2020-07-24 10:05:20.946502, Thread-3 : début du thread [Thread-3] avec 1 contribuable(s)
2020-07-24 10:05:20.946502, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-24 10:05:20.947003, Thread-3 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000}
2020-07-24 10:05:20.947003, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
2020-07-24 10:05:20.950324, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000}
2020-07-24 10:05:20.948449, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
2020-07-24 10:05:20.953645, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000}
2020-07-24 10:05:20.976143, Thread-1 : {"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}}}
2020-07-24 10:05:20.976695, Thread-1 : fin du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-24 10:05:20.976695, Thread-1 : fin du thread [Thread-1]
2020-07-24 10:05:21.973914, Thread-2 : {"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}}}
2020-07-24 10:05:21.973914, Thread-2 : fin du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}
2020-07-24 10:05:21.973914, Thread-2 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-24 10:05:21.977130, Thread-4 : {"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}}}
2020-07-24 10:05:21.977130, Thread-4 : fin du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}
2020-07-24 10:05:21.977130, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000}
2020-07-24 10:05:21.982634, Thread-3 : {"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}}}
2020-07-24 10:05:21.982634, Thread-5 : {"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}}}
2020-07-24 10:05:21.983134, Thread-3 : fin du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-24 10:05:21.983134, Thread-5 : fin du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}
2020-07-24 10:05:21.983134, Thread-3 : fin du thread [Thread-3]
2020-07-24 10:05:21.983763, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000}
2020-07-24 10:05:22.008562, Thread-5 : {"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}}}
2020-07-24 10:05:22.008562, Thread-5 : fin du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}
2020-07-24 10:05:22.009062, Thread-5 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-24 10:05:22.016848, Thread-5 : {"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}}}
2020-07-24 10:05:22.017349, Thread-5 : fin du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-24 10:05:22.017349, Thread-5 : fin du thread [Thread-5]
2020-07-24 10:05:23.008486, Thread-2 : {"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}}}
2020-07-24 10:05:23.008486, Thread-2 : fin du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}
2020-07-24 10:05:23.009749, Thread-2 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000}
2020-07-24 10:05:23.011722, Thread-4 : {"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}}}
2020-07-24 10:05:23.013723, Thread-4 : fin du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-24 10:05:23.013723, Thread-4 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000}
2020-07-24 10:05:23.024135, Thread-2 : {"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}}}
2020-07-24 10:05:23.024135, Thread-2 : fin du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-24 10:05:23.025178, Thread-2 : fin du thread [Thread-2]
2020-07-24 10:05:23.025178, Thread-4 : {"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}}}
2020-07-24 10:05:23.026191, Thread-4 : fin du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-24 10:05:23.026191, Thread-4 : fin du thread [Thread-4]
- uit deze logbestanden blijkt dat er vijf threads zijn gestart om de belasting van 11 belastingplichtigen te berekenen. Deze vijf threads hebben gelijktijdig verzoeken naar de server voor belastingberekening gestuurd. Het is belangrijk om te begrijpen hoe dit werkt:
- de thread [Thread-1] wordt als eerste gestart. Zodra deze de processor heeft, doorloopt hij de code totdat hij zijn verzoek HTTP verstuurt. Omdat hij op het resultaat daarvan moet wachten, wordt hij automatisch in de wachtrij geplaatst. Hij verliest dan de processor en een andere thread krijgt deze toegewezen;
- regels 1-10: hetzelfde proces herhaalt zich voor elk van de 5 threads. Zo worden de 5 threads gestart nog voordat de thread [Thread-1] zijn antwoord heeft ontvangen (regel 11);
- de threads worden niet in de volgorde beëindigd waarin ze zijn gestart. Zo is het de thread [Thread-3] die als eerste wordt beëindigd, regel 23;
Aan de serverzijde zien de logs in het bestand [data/logs/logs.txt] er als volgt uit:
2020-07-24 10:05:01.692980, MainThread : [serveur] démarrage du serveur
2020-07-24 10:05:01.877251, MainThread : [serveur] connexion à la base de données réussie
2020-07-24 10:05:03.596162, MainThread : [serveur] démarrage du serveur
2020-07-24 10:05:03.661160, MainThread : [serveur] connexion à la base de données réussie
2020-07-24 10:05:20.968053, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=ja&kinderen=2&salaris=50000' [GET]>
2020-07-24 10:05:20.969132, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.970316, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=ja&kinderen=3&salaris=100000' [GET]>
2020-07-24 10:05:20.970316, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.971335, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=ja&kinderen=2&salaris=55555' [GET]>
2020-07-24 10:05:20.972563, Thread-4 : [index] {'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}}}
2020-07-24 10:05:20.974796, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=nee&kinderen=3&salaris=100000' [GET]>
2020-07-24 10:05:20.974796, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:20.976143, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=ja&kinderen=2&salaris=30000' [GET]>
2020-07-24 10:05:20.976143, Thread-6 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:21.970615, Thread-2 : [index] {'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}}}
2020-07-24 10:05:21.973914, Thread-3 : [index] {'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}}}
2020-07-24 10:05:21.977130, Thread-6 : [index] {'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}}}
2020-07-24 10:05:21.977130, Thread-5 : [index] {'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}}}
2020-07-24 10:05:22.001693, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=ja&kinderen=3&salaris=50000' [GET]>
2020-07-24 10:05:22.003013, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:22.003013, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=ja&kinderen=5&salaris=100000' [GET]>
2020-07-24 10:05:22.003013, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-24 10:05:22.005871, Thread-9 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=nee&kinderen=0&salaris=200000' [GET]>
2020-07-24 10:05:22.006370, Thread-9 : [index] {'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}}}
2020-07-24 10:05:22.014170, Thread-10 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=ja&kinderen=3&salaris=200000' [GET]>
2020-07-24 10:05:22.014170, Thread-10 : [index] {'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}}}
2020-07-24 10:05:23.003533, Thread-7 : [index] {'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}}}
2020-07-24 10:05:23.006434, Thread-8 : [index] {'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}}}
2020-07-24 10:05:23.018026, Thread-11 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=nee&kinderen=2&salaris=100000' [GET]>
2020-07-24 10:05:23.019074, Thread-11 : [index] {'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}}}
2020-07-24 10:05:23.021447, Thread-12 : [index] requête : <Request 'http://127.0.0.1:5000/?getrouwd=nee&kinderen=0&salaris=100000' [GET]>
2020-07-24 10:05:23.022447, Thread-12 : [index] {'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}}}
- we zien dat 11 threads de 11 belastingplichtigen hebben verwerkt;
- sommige threads zijn in de wachtrij geplaatst (regels 6, 8, 12, 14, 20, 22) en andere niet (regels 9, 23, 25, 29, 31);
24.5. Tests van de laag [dao]
Net als in de |vorige versie| testen we de laag [dao] van de client. Het principe is precies hetzelfde:

De testklasse wordt uitgevoerd in de volgende omgeving:

- de configuratie [2] is identiek aan de configuratie [1] die we zojuist hebben bekeken;
De testklasse [TestHttpClientDao] is als volgt:
import unittest
from Logger import Logger
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)
…
if __name__ == '__main__':
# de applicatie wordt geconfigureerd
import config
config = config.configure({})
# logger
logger = Logger(config["logsFilename"])
# wordt opgeslagen in de configuratie
config["logger"] = logger
# de laag [dao] wordt opgehaald
dao = config["layers"]["dao"]
# we voeren de testmethoden uit
print("tests en cours...")
unittest.main()
- we maken een |uitvoeringsconfiguratie| aan voor deze test;
- we starten de webserver met de volledige omgeving;
- we voeren de test uit;
De resultaten zijn als volgt:
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/02/tests/TestHttpClientDao.py
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 6.128s
OK
Process finished with exit code 0