24. Ćwiczenie praktyczne: wersja 7
24.1. Wprowadzenie
Wersja 7 aplikacji do obliczania podatku jest identyczna z wersją 6, z wyjątkiem następujących szczegółów:
- klient internetowy będzie wysyłał jednocześnie kilka żądań HTTP. W poprzedniej wersji żądania te były wysyłane sekwencyjnie. Serwer przetwarzał wówczas w danym momencie tylko jedno żądanie;
- serwer będzie wielowątkowy: będzie mógł przetwarzać wiele zapytań jednocześnie;
- aby śledzić przebieg tych żądań, serwer internetowy zostanie wyposażony w moduł rejestrujący, za pomocą którego w pliku tekstowym będą rejestrowane kluczowe momenty przetwarzania żądań;
- serwer wyśle wiadomość e-mail do administratora aplikacji, gdy napotka problem uniemożliwiający jej uruchomienie, zazwyczaj związany z bazą danych powiązaną z serwerem WWW;
Architektura aplikacji pozostaje bez zmian:
Struktura skryptów wygląda następująco:

Folder [http-servers/02] powstaje najpierw poprzez skopiowanie folderu [http-servers/01]. Następnie wprowadza się w nim zmiany.
24.2. Narzędzia

24.2.1. Klasa [Logger]
Klasa [Logger] umożliwi rejestrowanie w pliku tekstowym niektórych działań serwera WWW:
import codecs
import threading
from datetime import date, datetime
from threading import current_thread
from ImpôtsError import ImpôtsError
class Logger:
# atrybut klasy
verrou = threading.RLock()
# konstruktor
def __init__(self, logs_filename: str):
try:
# otwieranie pliku w trybie dołączania (a)
self.__resource = codecs.open(logs_filename, "a", "utf-8")
except BaseException as erreur:
raise ImpôtsError(18, f"{erreur}")
# zapis w dzienniku
def write(self, message: str):
# data / godzina bieżąca
today = date.today()
now = datetime.time(datetime.now())
# nazwa wątku
thread_name = current_thread().name
# nie chcemy, aby nam przeszkadzano podczas zapisywania w pliku dziennika
# żądamy obiektu synchronizacji (= blokady) klasy – tylko jeden wątek go otrzyma
Logger.verrou.acquire()
try:
# zapis do dziennika
self.__resource.write(f"{today} {now}, {thread_name} : {message}")
# zapis natychmiastowy – w przeciwnym razie tekst zostanie zapisany dopiero po zamknięciu strumienia zapisu
# lub chcemy śledzić logi w czasie
self.__resource.flush()
finally:
# zwalniamy obiekt synchronizacji (= blokadę), aby inny wątek mógł go uzyskać
Logger.verrou.release()
# zwolnienie zasobów
def close(self):
# zamknięcie pliku
if self.__resource:
self.__resource.close()
- wiersze 10–11: definiujemy atrybut klasy. Atrybut klasy to właściwość wspólna dla wszystkich instancji tej klasy. Odwołujemy się do niego za pomocą notacji [Classe.attribut_de_classe] (wiersze 30, 39). Atrybut klasy [verrou] będzie obiektem synchronizacji dla wszystkich wątków wykonujących kod z wierszy 31–36;
- wiersze 14–19: konstruktor otrzymuje bezwzględną nazwę pliku logów. Plik ten jest następnie otwierany, a pobrany deskryptor pliku jest zapisywany w klasie;
- wiersz 17: plik logów jest otwierany w trybie „append” (a). Każdy zapisywany wiersz zostanie umieszczony na końcu pliku;
- wiersze 22–39: metoda [write] umożliwia zapisanie w pliku logów komunikatu przekazanego jako parametr. Do komunikatu dołączane są dwie informacje:
- wiersz 24: bieżąca data;
- wiersz 25: aktualna godzina;
- wiersz 27: nazwa wątku, który zapisuje log. Należy pamiętać, że aplikacja internetowa obsługuje wielu użytkowników jednocześnie. Każdemu żądaniu przypisywany jest wątek do jego wykonania. Jeśli wątek ten zostanie wstrzymany, zazwyczaj z powodu operacji wejścia/wyjścia (sieć, pliki, baza danych), wówczas procesor zostanie przydzielony innemu wątkowi. Ze względu na te możliwe przerwy nie ma pewności, że wątek zdoła zapisać wiersz do pliku logów bez zakłóceń. Istnieje zatem ryzyko, że logi z dwóch różnych wątków zostaną pomieszane. Ryzyko to jest niewielkie, a może nawet zerowe, jednak postanowiliśmy pokazać, jak zsynchronizować dostęp dwóch wątków do wspólnego zasobu, w tym przypadku pliku logów;
- wiersz 30: przed zapisaniem wątek żąda klucza do drzwi wejściowych. Żądany klucz to ten utworzony w wierszu 11. Jest on rzeczywiście unikalny: atrybut klasy jest unikalny dla wszystkich instancji tej klasy;
- w momencie T1 wątek Thread1 otrzymuje klucz. Może wówczas wykonać wiersz 33;
- w momencie T2 wątek Thread1 zostaje wstrzymany, zanim jeszcze zakończy zapisywanie logu;
- w momencie T3 wątek Thread2, który uzyskał dostęp do procesora, również musi zapisać wpis do dziennika. Dochodzi więc do linii 30, gdzie żąda klucza do drzwi wejściowych. Otrzymuje odpowiedź, że inny wątek już go posiada. Zostaje wówczas automatycznie wstrzymany. Tak samo będzie w przypadku wszystkich wątków, które będą żądać tego klucza;
- w momencie T4 wątek Thread1, który został wstrzymany, odzyskuje dostęp do procesora. Następnie kończy zapisywanie logu;
- wiersze 32–36: zapis do pliku dziennika odbywa się w dwóch etapach:
- wiersz 33: deskryptor pliku uzyskany w wierszu 17 pracuje z buforem. Operacja [write] z wiersza 33 zapisuje dane do tego bufora, ale nie bezpośrednio do pliku. Bufor jest następnie opróżniany do pliku pod pewnymi warunkami:
- bufor jest pełny;
- na deskryptorze pliku wykonywana jest operacja [close] lub [flush];
- wiersz 36: wymuszamy zapis wiersza dziennika do pliku. Robimy to, ponieważ chcemy zobaczyć, jak logi z różnych wątków przeplatają się między sobą. Gdybyśmy tego nie zrobili, logi jednego wątku zostałyby zapisane wszystkie naraz podczas zamykania deskryptora (linia 45). Wówczas znacznie trudniej byłoby stwierdzić, że niektóre wątki zostały zatrzymane: należałoby sprawdzać godziny w logach;
- wiersz 39: wątek Thread1 zwraca klucz, który mu przyznano. Klucz ten będzie mógł zostać przyznany innemu wątku;
- wiersz 22: metoda [write] jest zatem zsynchronizowana: w danym momencie tylko jeden wątek zapisuje dane do pliku logów. Kluczem do działania jest wiersz 30: bez względu na okoliczności tylko jeden wątek pobiera klucz umożliwiający przejście do następnego wiersza. Zachowuje go tak długo, dopóki go nie zwróci (wiersz 39);
- wiersze 41–45: metoda [close] pozwala zwolnić zasoby przydzielone do deskryptora pliku logów;
Logi zapisane w pliku logów będą miały następujący wygląd:
24.2.2. Klasa [SendAdminMail]
Klasa [SendAminMail] umożliwia wysłanie wiadomości do administratora aplikacji w przypadku jej „awarii”.

Klasa [SendAdminMail] jest skonfigurowana w skrypcie [config] [2] w następujący sposób:
# konfiguracja serwera SMTP
"adminMail": {
# serwer SMTP
"smtp-server": "localhost",
# port serwera SMTP
"smtp-port": "25",
# administrator
"from": "guest@localhost.com",
"to": "guest@localhost.com",
# temat wiadomości e-mail
"subject": "plantage du serveur de calcul d'impôts",
# tls ustawione na True, jeśli serwer SMTP wymaga uwierzytelnienia, w przeciwnym razie na False
"tls": False
}
Klasa [SendAdminMail] otrzymuje słownik z wierszy 2–13 oraz konfigurację wysyłania wiadomości e-mail. Klasa ta wygląda następująco:
# importy
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):
# wysyła wiadomość do serwera SMTP config['smtp-server'] na porcie config[smtp-port]
# jeśli config['tls'] ma wartość true, zostanie użyte wsparcie TLS
# wiadomość e-mail jest wysyłana w imieniu config['from']
# do odbiorcy config['to']
# wiadomość ma temat config['subject']
# w pliku config['logger'] znajduje się odniesienie do modułu logującego
# pobieramy moduł rejestrujący z pliku konfiguracyjnego – może mieć wartość „None”
logger = config["logger"]
# serwer SMTP
server = None
# wysyłamy komunikat
try:
# serwer SMTP
server = smtplib.SMTP(config["smtp-server"])
# tryb szczegółowy
server.set_debuglevel(verbose)
# połączenie zabezpieczone?
if config['tls']:
# rozpoczęcie dialogu zabezpieczeń
server.starttls()
# uwierzytelnianie
server.login(config["user"], config["password"])
# tworzenie komunikatu wieloczęściowego – to właśnie ten komunikat zostanie wysłany
msg = MIMEText(message)
msg['From'] = config["from"]
msg['To'] = config["to"]
msg['Date'] = formatdate(localtime=True)
msg['Subject'] = config["subject"]
# wysyłamy komunikat
server.send_message(msg)
# log – rejestrator może nie istnieć
if logger:
logger.write(f"[SendAdminMail] Message envoyé à [{config['to']}] : [{message}]\n")
except BaseException as erreur:
# log – rejestrator może nie istnieć
if logger:
logger.write(
f"[SendAdminMail] Erreur [{erreur}] lors de l'envoi à [{config['to']}] du message [{message}] : \n")
finally:
# zakończono – zwolniono zasoby wykorzystane przez funkcję
if server:
server.quit()
- wiersze 24–54: znajduje się tu kod omówiony już w przykładzie |smtp/02|;
- wiersz 20: pobierany jest identyfikator modułu rejestrującego. Jest on wykorzystywany w wierszach 45 i 49;
24.3. Serwer WWW

24.3.1. Konfiguracja

Konfiguracja serwera jest bardzo podobna do tej z poprzedniego przykładu. Jedynie plik [config.py] uległ niewielkiej zmianie:
def configure(config: dict) -> dict:
import os
# etap 1 ------
# katalog tego pliku
script_dir = os.path.dirname(os.path.abspath(__file__))
# ścieżka główna
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# zależności bezwzględne
absolute_dependencies = [
# foldery projektu
# 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",
# Stałe, przedziały
f"{root_dir}/impots/v05/entities",
# IndexController
f"{root_dir}/impots/http-servers/01/controllers",
# skrypty [config_database, config_layers]
script_dir,
# Logger, SendAdminMail
f"{script_dir}/../utilities",
]
# ustawiamy ścieżkę systemową
from myutils import set_syspath
set_syspath(absolute_dependencies)
# krok 2 ------
# konfiguracja aplikacji
config.update({
# użytkownicy uprawnieni do korzystania z aplikacji
"users": [
{
"login": "admin",
"password": "admin"
}
],
# plik logów
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# konfiguracja serwera SMTP
"adminMail": {
# serwer SMTP
"smtp-server": "localhost",
# port serwera SMTP
"smtp-port": "25",
# administrator
"from": "guest@localhost.com",
"to": "guest@localhost.com",
# temat wiadomości e-mail
"subject": "plantage du serveur de calcul d'impôts",
# tls ustawione na True, jeśli serwer SMTP wymaga uwierzytelnienia, w przeciwnym razie na False
"tls": False
},
# czas przerwy wątku w sekundach
"sleep_time": 0
})
# etap 3 ------
# konfiguracja bazy danych
import config_database
config["database"] = config_database.configure(config)
# etap 4 ------
# instancjonowanie warstw aplikacji
import config_layers
config['layers'] = config_layers.configure(config)
# przekazujemy konfigurację
return config
- wiersze 40–66: do słownika konfiguracyjnego serwera dodajemy elementy dotyczące modułu logowania (wiersz 49) oraz wysyłania wiadomości e-mail z alertem do administratora aplikacji (wiersze 51–63);
- wiersz 65: aby lepiej obserwować działanie wątków, niektóre z nich zostaną zatrzymane. [sleep_time] to czas zatrzymania wyrażony w sekundach;
- wiersze 27–28: należy zauważyć, że używamy kontrolera [index_controller] z poprzedniej wersji 6;
24.3.2. Główny skrypt [main]
Główny skrypt [main] wygląda następująco:
# oczekuje się na parametr mysql lub pgres
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()
# konfiguracja aplikacji
import config
config = config.configure({'sgbd': sgbd})
# zależności
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
# moduł uwierzytelniający
auth = HTTPBasicAuth()
@auth.verify_password
def verify_password(login, password):
# lista użytkowników
users = config['users']
# przeglądamy tę listę
for user in users:
if user['login'] == login and user['password'] == password:
return True
# nie znaleziono
return False
# wysyłanie wiadomości e-mail do administratora
def send_adminmail(config: dict, message: str):
# wysyłanie wiadomości e-mail do administratora aplikacji
config_mail = config["adminMail"]
config_mail["logger"] = config['logger']
SendAdminMail.send(config_mail, message)
# sprawdzanie pliku logów
logger = None
erreur = False
message_erreur = None
try:
# moduł rejestrujący
logger = Logger(config["logsFilename"])
except BaseException as exception:
# log konsoli
print(f"L'erreur suivante s'est produite : {exception}")
# odnotowuje się błąd
erreur = True
message_erreur = f"{exception}"
# zapisujemy moduł logujący w konfiguracji
config['logger'] = logger
# obsługa błędu
if erreur:
# wiadomość e-mail do administratora
send_adminmail(config, message_erreur)
# zakończenie działania aplikacji
sys.exit(1)
# log uruchomienia
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)
# pobieranie danych z urzędu skarbowego
erreur = False
try:
# admindata będzie danymi o zasięgu aplikacji, dostępnymi tylko do odczytu
config["admindata"] = config["layers"]["dao"].get_admindata()
# log pomyślnego zakończenia
logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
# odnotowano błąd
erreur = True
# log błędów
log = f"L'erreur suivante s'est produite : {ex}"
# konsola
print(log)
# plik logów
logger.write(f"{log}\n")
# wiadomość e-mail do administratora
send_adminmail(config, log)
# główny wątek nie potrzebuje już modułu logującego
logger.close()
# w przypadku wystąpienia błędu następuje zatrzymanie
if erreur:
sys.exit(2)
# aplikacja Flask może się uruchomić
app = Flask(__name__)
# Strona główna URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
…
# tylko main
if __name__ == '__main__':
# uruchamiamy serwer
app.config.update(ENV="development", DEBUG=True)
app.run(threaded=True)
- wiersze 1–10: skrypt oczekuje parametru [mysql / pgres], który wskazuje mu, którego SGBD ma użyć;
- wiersze 12–14: konfiguracja aplikacji (ścieżka Python, warstwy, baza danych);
- wiersze 16–28: zależności niezbędne dla działania aplikacji;
- wiersze 30–43: obsługa uwierzytelniania;
- wiersze 46–51: funkcja wysyłająca wiadomość e-mail do administratora aplikacji;
- funkcja oczekuje dwóch parametrów:
- config: słownik zawierający klucze [adminMail] i [logger];
- wiadomość do wysłania;
- wiersze 49–50: przygotowujemy konfigurację wysyłki;
- wysyłamy wiadomość e-mail;
- wiersze 54–74: sprawdzamy, czy plik logów istnieje;
- wiersze 70–74: jeśli nie udało się otworzyć pliku dziennika, wysyłamy wiadomość e-mail do administratora i kończymy działanie;
- wiersze 76–79: rejestruje się uruchomienie serwera;
- wiersze 81–98: pobieramy dane z urzędu skarbowego z bazy danych;
- wiersze 88–98: jeśli nie udało się uzyskać tych danych, rejestruje się błąd zarówno na konsoli, jak i w pliku dziennika;
- wiersze 100–101: główny wątek nie będzie już tworzył logów (utworzone wątki nie będą korzystać z tego samego deskryptora pliku);
- wiersze 103–105: jeśli nie udało się połączyć z bazą danych, program się zatrzymuje;
- wiersz 122: uruchamia się serwer w trybie wielowątkowym;
Funkcja [index] (wiersz 114) ma następującą postać:
# Strona główna URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
logger = None
try:
# rejestruje
logger = Logger(config["logsFilename"])
# zapisywanie w konfiguracji powiązanej z wątkiem
thread_config = {"logger": logger}
thread_name = threading.current_thread().name
config[thread_name] = {"config": thread_config}
# rejestruje się żądanie
logger.write(f"[index] requête : {request}\n")
# przerywa się wątek, jeśli zostało to zlecone
sleep_time = config["sleep_time"]
if sleep_time != 0:
# przerwa jest losowa, aby niektóre wątki zostały przerwane, a inne nie
aléa = randint(0, 1)
if aléa == 1:
# rejestruje się przed przerwą
logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
# przerwa
time.sleep(sleep_time)
# zlecamy wykonanie żądania kontrolerowi
résultat, status_code = index_controller.execute(request, config)
# czy wystąpił błąd krytyczny?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# wysyłamy wiadomość e-mail do administratora aplikacji
config_mail = config["adminMail"]
config_mail["logger"] = logger
SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
# rejestruje się odpowiedź
logger.write(f"[index] {résultat}\n")
# wysyłamy odpowiedź
return json_response(résultat, status_code)
except BaseException as erreur:
# rejestruje się błąd, jeśli to możliwe
if logger:
logger.write(f"[index] {erreur}")
# przygotowuje się odpowiedź dla klienta
résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
# wysyłamy odpowiedź
return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
finally:
# zamykamy plik dziennika, jeśli został otwarty
if logger:
logger.close()
- wiersz 4: funkcja wykonywana, gdy użytkownik żąda URL /. Ponieważ serwer jest wielowątkowy (wiersz 112), zostanie utworzony wątek w celu wykonania tej funkcji. Wątek ten może w dowolnym momencie zostać przerwany i wstrzymany, aby wznowić swoje działanie nieco później. Należy zawsze pamiętać o tym aspekcie, gdy kod uzyskuje dostęp do zasobu współdzielonego przez wszystkie wątki. Takim zasobem jest tutaj plik logów: wszystkie wątki zapisują w nim dane;
- wiersz 8: tworzymy instancję obiektu logującego. Zatem wszystkie wątki będą miały odrębne instancje tego obiektu. Niemniej jednak wszystkie te obiekty logujące odwołują się do tego samego pliku logów. Należy jednak zauważyć, że gdy wątek zamyka swój obiekt logujący, nie ma to wpływu na obiekty logujące pozostałych wątków;
- wiersze 9–12: rejestrator jest zapisywany w słowniku aplikacji o nazwie [config], powiązanym z kluczem o nazwie wątku. Jeśli więc jednocześnie wykonywanych jest n wątków, w słowniku [config] powstanie n wpisów. [config] jest zasobem współdzielonym przez wszystkie wątki. Może więc zaistnieć potrzeba synchronizacji. W tym miejscu przyjąłem pewne założenie. Założyłem, że gdyby dwa wątki jednocześnie tworzyły swoje wpisy w pliku [config] i jeden z nich zostałby przerwany przez drugi, nie miałoby to żadnego znaczenia. Wątek, który został przerwany, mógłby później dokończyć tworzenie wpisu. Gdyby eksperyment wykazał, że hipoteza ta jest błędna, należałoby zsynchronizować dostęp do wiersza 12;
- wiersz 10: umieszczamy moduł logujący w słowniku;
- wiersz 11: [threading.current_thread()] to wątek wykonujący ten wiersz, a więc wątek wykonujący funkcję [index]. Zapisujemy jego nazwę. Każdy wątek ma unikalną nazwę;
- wiersz 12: zapisujemy konfigurację wątku. Od tej pory będziemy zawsze postępować w ten sposób: jeśli istnieją informacje, których nie można udostępniać między wątkami, zostaną one mimo to umieszczone w ogólnej konfiguracji, ale powiązane z nazwą wątku;
- wiersz 14: rejestrujemy żądanie, które właśnie wykonujemy;
- wiersze 15–24: losowo wstrzymujemy niektóre wątki, aby ustąpiły procesora innemu wątkowi;
- wiersz 16: pobieramy czas trwania pauzy (w sekundach) z konfiguracji;
- wiersz 17: wstrzymanie następuje tylko wtedy, gdy czas wstrzymania jest różny od 0;
- wiersz 19: losowa liczba całkowita z przedziału [0, 1]. Zatem możliwe są tylko wartości 0 i 1;
- wiersz 20: wątek zostaje wstrzymany tylko wtedy, gdy liczba losowa wynosi 1;
- wiersz 22: rejestruje się fakt, że wątek zostanie przerwany;
- wiersz 24: wątek zostaje przerwany na [sleep_time] sekund;
- wiersz 26: gdy wątek się budzi, zleca wykonanie zapytania przez moduł [index_controller];
- wiersze 28–32: jeśli podczas wykonywania wystąpi błąd typu [500 INTERNAL SERVER ERROR], wysyłana jest wiadomość e-mail do administratora;
- wiersze 30–31: konfigurujemy słownik [config_mail], który zostanie przekazany do klasy [SendAdminMail];
- wiersz 32: wiadomość wysyłana do administratora to ciąg znaków jSON z wyniku, który zostanie wysłany do klienta;
- wiersze 33–34: rejestrujemy odpowiedź, którą wyślemy do klienta (wiersz 36);
- wiersze 37–44: obsługa ewentualnego wyjątku;
- wiersze 39–40: jeśli logger istnieje, rejestruje się wystąpiony błąd;
- wiersze 47–48: zamykamy logger, jeśli istnieje. Ostatecznie wątek tworzy logger na początku żądania i zamyka go po jego przetworzeniu;
24.3.3. Kontroler [index_controller]
Kontroler [index_controller], który wykonuje zapytania, pochodzi z poprzedniej wersji:

24.3.4. Uruchomienie
Uruchamiamy serwer Flask, serwer pocztowy |hMailServer| oraz moduł odczytu poczty |Thunderbird|. Nie uruchamiamy modułu SGBD. Serwer zatrzymuje się, wyświetlając następujące logi konsoli:
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
Plik dziennika [logs.txt] wygląda następująco:
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)]]
Za pomocą programu Thunderbird sprawdzamy wiadomości e-mail administratora [guest@localhost.com]:

Następnie uruchamiamy SGBD i żądamy plików URL oraz [http://127.0.0.1:5000/?mari%C3%A9=oui&enfants=3&salaire=200000]. Logi przybierają następującą postać:
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/?marié=oui&enfants=3&salaire=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}}}
- wiersze 1–4: przypominamy, że serwer uruchamia się dwukrotnie, ponieważ tryb [Debug=True] powoduje drugie uruchomienie;
- wiersze 5–6: logi dają nam wyobrażenie o czasie wykonania zapytania, w tym przypadku 2,293 milisekundy;
24.4. Klient internetowy


Folder [http-clients/02] powstaje poprzez skopiowanie folderu [http-clients/01]. Następnie wprowadza się kilka zmian.
24.4.1. Konfiguracja
Konfiguracja [config] aplikacji [http-clients/02] jest taka sama jak konfiguracja aplikacji [http-clients/01], z wyjątkiem kilku szczegółów:
def configure(config: dict) -> dict:
import os
# krok 1 ------
# katalog tego pliku
script_dir = os.path.dirname(os.path.abspath(__file__))
# ścieżka główna
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# zależności bezwzględne
absolute_dependencies = [
# foldery projektu
# 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",
# Stałe, przedziały
f"{root_dir}/impots/v05/entities",
# ImpôtsDaoWithHttpClient
f"{script_dir}/../services",
# skrypty konfiguracyjne
script_dir,
# Rejestrator
f"{root_dir}/impots/http-servers/02/utilities",
]
# ustawiamy ścieżkę systemową
from myutils import set_syspath
set_syspath(absolute_dependencies)
# krok 2 ------
# konfiguracja aplikacji za pomocą stałych
config.update({
# plik podatników
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
# plik wyników
"resultsFilename": f"{script_dir}/../data/output/résultats.json",
# plik błędów
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
# plik logów
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# serwer obliczający podatek
"server": {
"urlServer": "http://127.0.0.1:5000/",
"authBasic": True,
"user": {
"login": "admin",
"password": "admin"
}
},
# tryb debugowania
"debug": True
}
)
# etap 3 ------
# instancjonowanie warstw
import config_layers
config['layers'] = config_layers.configure(config)
# ustawiamy konfigurację
return config
- wiersze 31–32: użyjemy tego samego modułu logowania |Logger|, co w przypadku serwera;
- wiersz 49: bezwzględna ścieżka do pliku logów;
- wiersz 60: tryb [debug=True] służy do zapisywania odpowiedzi serwera WWW w pliku logów;
24.4.2. Warstwa [dao]
Kod klasy [ImpôtsDaoWithHttpClient] ulega niewielkim zmianom:
# importy
import requests
from flask_api import status
…
class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
# konstruktor
def __init__(self, config: dict):
# inicjalizacja obiektu nadrzędnego
AbstractImpôtsDao.__init__(self, config)
# zapisywanie elementów konfiguracji
# konfiguracja ogólna
self.__config = config
# serwer
self.__config_server = config["server"]
# tryb debugowania
self.__debug = config["debug"]
# rejestrator
self.__logger = None
# metoda nieużywana
def get_admindata(self) -> AdminData:
pass
# obliczanie podatku
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
# pozwala na przekazywanie wyjątków
…
# tryb debugowania?
if self.__debug:
# moduł logujący
if not self.__logger:
self.__logger = self.__config['logger']
# rejestruje się
self.__logger.write(f"{response.text}\n")
# kod statusu odpowiedzi HTTP
status_code = response.status_code
…
- wiersz 17: zapisujemy ogólną konfigurację. Później zobaczymy, że gdy uruchamia się konstruktor klasy [ImpôtsDaoWithHttpClient], słownik [config] nie zawiera jeszcze klucza [logger] użytego w wierszu 37. Z tego powodu nie można zainicjować klucza [self.__logger] (wiersz 23) w konstruktorze;
- wiersz 21: w konfiguracji dodano klucz [debug], który kontroluje logowanie w wierszach 33–39;
- wiersz 34: jeśli jesteśmy w trybie [debug];
- wiersze 36–37: ewentualna inicjalizacja właściwości [self.__logger]. Gdy używana jest metoda [calculate_tax], klucz [logger] jest częścią słownika [config];
- wiersz 39: rejestruje się dokument tekstowy powiązany z odpowiedzią serwera o numerze HTTP;
Warstwa [dao] będzie wykonywana jednocześnie przez kilka wątków. Tymczasem w tym przypadku tworzony jest tylko jeden egzemplarz tej warstwy (por. config_layers). Należy zatem sprawdzić, czy kod nie przewiduje dostępu z prawem zapisu do danych współdzielonych, zazwyczaj właściwości klasy [ImpôtsDaoWithHttpClient], która implementuje warstwę [dao]. Jednak w powyższym kodzie w wierszu 37 modyfikowana jest właściwość instancji klasy. W tym przypadku nie ma to znaczenia, ponieważ wszystkie wątki korzystają z tego samego modułu logowania. Gdyby tak nie było, dostęp do wiersza 37 musiałby zostać zsynchronizowany.
24.4.3. Skrypt główny
Główny skrypt [main] zmienia się w następujący sposób:
# konfiguracja aplikacji
import config
config = config.configure({})
# zależności
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger
# uruchomienie warstwy [dao] w wątku
# taxpayers to lista podatników
def thread_function(dao, logger, taxpayers: list):
…
# lista wątków klienta
threads = []
logger = None
# kod
try:
# logger
logger = Logger(config["logsFilename"])
# zapisujemy to w konfiguracji
config["logger"] = logger
# pobieramy warstwę [dao]
dao = config["layers"]["dao"]
# odczyt danych podatników
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# podatników?
if not taxpayers:
raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
# obliczanie podatku podatników przy użyciu wielu wątków
i = 0
l_taxpayers = len(taxpayers)
while i < len(taxpayers):
# każdy wątek przetworzy od 1 do 4 podatników
nb_taxpayers = min(l_taxpayers - i, random.randint(1, 4))
# lista podatników przetworzonych przez wątek
thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
# zwiększamy wartość i dla następnego wątku
i += nb_taxpayers
# tworzymy wątek
thread = threading.Thread(target=thread_function, args=(dao, logger, thread_taxpayers))
# dodaje się go do listy wątków skryptu głównego
threads.append(thread)
# uruchamia się wątek – operacja ta jest asynchroniczna – nie czeka się na wynik wątku
thread.start()
# wątek główny czeka na zakończenie wszystkich wątków, które uruchomił
for thread in threads:
thread.join()
# w tym momencie wszystkie wątki zakończyły swoją pracę – każdy z nich zmodyfikował jeden lub więcej obiektów [taxpayer]
# wyniki zapisuje się w pliku jSON
dao.write_taxpayers_results(taxpayers)
except BaseException as erreur:
# wyświetlenie błędu
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# zamykamy program rejestrujący
if logger:
logger.close()
# zakończono
print("Travail terminé...")
# zakończenie wątków, które mogłyby nadal istnieć, jeśli proces został zatrzymany z powodu błędu
sys.exit()
- Skrypt główny różni się od skryptu poprzedniego klienta tym, że generuje kilka wątków wykonawczych w celu wysyłania żądań do serwera. Klient w wersji 6 wysyłał wszystkie żądania sekwencyjnie. Żądanie nr i było wysyłane dopiero po otrzymaniu odpowiedzi na żądanie nr [i-1]. W tym przypadku chcemy sprawdzić, jak serwer będzie się zachowywał po otrzymaniu wielu żądań jednocześnie. W tym celu potrzebujemy wątków;
- wiersz 21: wygenerowane wątki zostaną umieszczone na liście. Należy pamiętać, że skrypt [main] jest również wykonywany przez wątek o nazwie [MainThread]. Ten główny wątek utworzy kolejne wątki, które będą odpowiedzialne za obliczenie podatku dla jednego lub kilku podatników;
- wiersz 26: tworzony jest moduł logowania. Będzie on współdzielony przez wszystkie wątki;
- wiersz 32: pobieramy wszystkich podatników, dla których należy obliczyć podatek;
- wiersze 39–51: podatnicy ci zostaną rozdzieleni między kilka wątków;
- wiersze 40–41: każdy wątek będzie obsługiwał od 1 do 4 podatników. Liczba ta jest ustalana losowo;
- [random.randint(1, 4)] losowo wybiera liczbę z listy [1, 2, 3, 4];
- wątek nie może mieć więcej niż [l-i] podatników, gdzie [l-i] oznacza liczbę podatników, którym nie przypisano jeszcze wątku;
- bierzemy zatem mniejszą z tych dwóch wartości;
- wiersz 43: gdy znana jest już liczba podatników obsługiwanych przez wątek ([nb_taxpayers]), wybiera się ich z listy podatników:
- [slice(10,12)] to zbiór indeksów [10, 11, 12];
- [response.text[39:]] to lista [taxpayers[10], taxpayers[11], taxpayers[12];
- wiersz 45: zwiększamy wartość zmiennej i, która steruje pętlą z wiersza 39;
- wiersz 47: tworzy się wątek:
- [target=thread_function] określa funkcję, którą wykona wątek. Jest to funkcja z wierszy 16–17. Oczekuje ona trzech parametrów;
- [ags] to lista trzech parametrów oczekiwanych przez funkcję [thread_function];
Utworzenie wątku nie powoduje jego uruchomienia. Tworzy jedynie obiekt i to wszystko;
- wiersze 48–49: właśnie utworzony wątek zostaje dodany do listy wątków utworzonych przez wątek główny;
- wiersz 51: wątek zostaje uruchomiony. Będzie on teraz wykonywany równolegle z innymi aktywnymi wątkami. W tym przypadku wykona on funkcję [thread_function] z podanymi argumentami;
- wiersze 53–54: wątek główny oczekuje na zakończenie pracy każdego z uruchomionych przez siebie wątków. Weźmy przykład:
- główny wątek uruchomił trzy wątki [th1, th2, th3];
- wątek główny przechodzi w stan oczekiwania na każdy z wątków (wiersze 53–54) w kolejności pętli for: [th1, th2, th3];
- załóżmy, że wątki kończą się w kolejności [th2, th1, th3];
- wątek główny czeka na zakończenie th1. Gdy th2 się kończy, nic się nie dzieje;
- gdy th1 się kończy, wątek główny przechodzi w stan oczekiwania na th2. Ten jednak już się zakończył. Wątek główny przechodzi wówczas do następnego wątku i oczekuje na th3;
- gdy th3 się kończy, wątek główny kończy oczekiwanie i przechodzi do wykonania linii 57;
- wiersz 57 zapisuje uzyskane wyniki do pliku wyników. Mamy tu dobry przykład odwołań do obiektów:
- wiersz 43: lista [thread_payers] powiązana z wątkiem zawiera kopie odwołań do obiektów zawartych w liście [taxpayers];
- wiemy, że obliczenie podatku zmieni obiekty, na które wskazują odwołania z listy [thread_payers]. Obiekty te zostaną wzbogacone o wyniki obliczenia podatku. Jednak same odwołania nie ulegają zmianie. Zatem odwołania z początkowej listy [taxpayers] „widzą” lub „wskazują na” zmodyfikowane obiekty;
Funkcja [thread_function] wykonywana przez wątki wygląda następująco:
# wykonanie warstwy [dao] w wątku
# taxpayers to lista podatników
def thread_function(dao, logger, taxpayers: list):
# log rozpoczęcia wątku
thread_name = threading.current_thread().name
logger.write(f"début du thread [{thread_name}] avec {len(taxpayers)} contribuable(s)\n")
# obliczamy podatek podatników
for taxpayer in taxpayers:
# log
logger.write(f"début du calcul de l'impôt de {taxpayer}\n")
# synchroniczne obliczanie podatku
dao.calculate_tax(taxpayer)
# log
logger.write(f"fin du calcul de l'impôt de {taxpayer}\n")
# log – koniec wątku
logger.write(f"fin du thread [{thread_name}]\n")
- Funkcje wykonywane jednocześnie przez wiele wątków są często trudne do napisania: zawsze należy sprawdzić, czy kod nie próbuje modyfikować danych współdzielonych między wątkami. Gdy tak się dzieje, należy zapewnić zsynchronizowany dostęp do współdzielonych danych, które mają zostać zmodyfikowane;
- wiersz 3: funkcja otrzymuje trzy parametry:
- [dao]: odwołanie do warstwy [dao]. Dane te są współdzielone;
- [logger]: odwołanie do modułu rejestrującego. Dane te są współdzielone;
- [taxpayers]: lista podatników. Dane te nie są współdzielone: każdy wątek zarządza inną listą;
- przyjrzyjmy się dwóm odwołaniom [dao, logger]:
- widzieliśmy, że obiekt, na który wskazuje odwołanie [dao], posiadał odwołanie [self.__logger], które było modyfikowane przez wątki, ale w celu nadania mu wartości wspólnej dla wszystkich wątków;
- odwołanie [logger] wskazuje na deskryptor pliku. Widzieliśmy, że podczas zapisywania logów do pliku może wystąpić problem. Z tego powodu zapis do pliku został zsynchronizowany;
- wiersze 5–6: rejestruje się nazwę wątku oraz liczbę podatników, którymi ma on zarządzać;
- wiersze 8–14: obliczanie podatku podatników;
- wiersz 16: rejestruje się zakończenie działania wątku;
24.4.4. Uruchomienie
Uruchommy serwer WWW tak jak w poprzednim akapicie (serwer WWW, SGBD, hMailServer, Thunderbird), a następnie uruchommy skrypt kliencki [main]. W plikach [data/output/errors.txt, data/output/résultats.json] uzyskujemy te same wyniki, co w poprzedniej wersji. W pliku [data/logs/logs.txt] znajdują się następujące wpisy dziennika:
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]
- te logi pokazują, że uruchomiono pięć wątków w celu obliczenia podatku dla 11 podatników. Te pięć wątków wysłało równoczesne żądania do serwera obliczającego podatek. Należy zrozumieć, jak to działa:
- wątek [Thread-1] jest uruchamiany jako pierwszy. Gdy uzyskuje dostęp do procesora, przechodzi przez kod aż do wysłania żądania HTTP. Ponieważ musi czekać na wynik tego żądania, zostaje automatycznie zawieszony. Traci wówczas dostęp do procesora, a inny wątek go przejmuje;
- wiersze 1–10: ten sam proces powtarza się dla każdego z 5 wątków. W ten sposób wszystkie 5 wątków zostaje uruchomionych, zanim wątek [Thread-1] otrzyma swoją odpowiedź (wiersz 11);
- wątki nie kończą się w kolejności, w jakiej zostały uruchomione. W ten sposób jako pierwszy kończy się wątek [Thread-3], wiersz 23;
Po stronie serwera logi w pliku [data/logs/logs.txt] są następujące:
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/?marié=oui&enfants=2&salaire=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/?żonaty=tak&dzieci=3&wynagrodzenie=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/?żonaty=tak&dzieci=2&wynagrodzenie=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/?żonaty=nie&dzieci=3&wynagrodzenie=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/?marié=oui&enfants=2&salaire=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/?marié=oui&enfants=3&salaire=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/?marié=oui&enfants=5&salaire=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/?żonaty=nie&dzieci=0&wynagrodzenie=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/?marié=oui&enfants=3&salaire=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/?marié=nie&enfants=2&salaire=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/?marié=non&enfants=0&salaire=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}}}
- widać, że 11 wątków przetworzyło 11 podatników;
- niektóre wątki zostały zawieszone (wiersze 6, 8, 12, 14, 20, 22), a inne nie (wiersze 9, 23, 25, 29, 31);
24.5. Testy warstwy [dao]
Podobnie jak w przypadku warstwy |version précédente|, testujemy warstwę [dao] klienta. Zasada działania jest dokładnie taka sama:

Klasa testowa zostanie uruchomiona w następującym środowisku:

- konfiguracja [2] jest identyczna z konfiguracją [1], którą właśnie przeanalizowaliśmy;
Klasa testowa [TestHttpClientDao] wygląda następująco:
import unittest
from Logger import Logger
class TestHttpClientDao(unittest.TestCase):
def test_1(self) -> None:
from TaxPayer import TaxPayer
# {'stan cywilny': 'tak', 'dzieci': 2, 'wynagrodzenie': 55555,
# 'podatek': 2814, 'dopłata': 0, 'ulga': 0, 'obniżka': 0, 'stawka': 0,14}
taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
dao.calculate_tax(taxpayer)
# weryfikacja
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__':
# konfiguracja aplikacji
import config
config = config.configure({})
# rejestrator
logger = Logger(config["logsFilename"])
# zapisujemy to w konfiguracji
config["logger"] = logger
# pobieramy warstwę [dao]
dao = config["layers"]["dao"]
# wykonujemy metody testowe
print("tests en cours...")
unittest.main()
- tworzymy |configuration d’exécution| na potrzeby tego testu;
- uruchamiamy serwer WWW wraz z całym jego środowiskiem;
- uruchamia się test;
Wyniki są następujące:
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