24. Практичне завдання: версія 7
24.1. Вступ
Версія 7 програми для розрахунку податку ідентична версії 6, за винятком таких деталей:
- веб-клієнт одночасно запускатиме кілька запитів HTTP. У попередній версії ці запити запускалися послідовно. Тоді сервер обробляв у будь-який момент лише один запит;
- сервер буде багатопотоковим: він зможе обробляти кілька запитів одночасно;
- щоб відстежувати виконання цих запитів, веб-сервер буде оснащено засобом ведення журналу, за допомогою якого в текстовий файл будуть записуватися важливі моменти обробки запитів;
- сервер надсилатиме електронного листа адміністратору додатка, коли зіткнеться з проблемою, яка завадить його запуску, як правило, це проблема з базою даних, пов’язаною з веб-сервером;
Архітектура додатка не змінюється:
Структура скриптів така:

Папку [http-servers/02] спочатку створюють шляхом копіювання папки [http-servers/01]. Потім у ній вносять зміни.
24.2. Утиліти

24.2.1. Клас [Logger]
Клас [Logger] дозволить записувати в текстовий файл певні дії веб-сервера:
import codecs
import threading
from datetime import date, datetime
from threading import current_thread
from ImpôtsError import ImpôtsError
class Logger:
# атрибут класу
verrou = threading.RLock()
# конструктор
def __init__(self, logs_filename: str):
try:
# відкриття файлу в режимі додавання (a)
self.__resource = codecs.open(logs_filename, "a", "utf-8")
except BaseException as erreur:
raise ImpôtsError(18, f"{erreur}")
# запис у журнал
def write(self, message: str):
# дата / час поточного моменту
today = date.today()
now = datetime.time(datetime.now())
# ім'я потоку
thread_name = current_thread().name
# не хочемо, щоб нас переривали під час запису в файл журналу
# запитується об’єкт синхронізації (= блокування) класу — його отримає лише один потік
Logger.verrou.acquire()
try:
# запис у журнал
self.__resource.write(f"{today} {now}, {thread_name} : {message}")
# негайний запис — інакше текст буде записано лише під час закриття потоку запису
# або ж потрібно відстежувати журнали з плином часу
self.__resource.flush()
finally:
# звільняємо об’єкт синхронізації (= блокування), щоб інший потік міг його отримати
Logger.verrou.release()
# звільнення ресурсів
def close(self):
# закриття файлу
if self.__resource:
self.__resource.close()
- рядки 10–11: визначається атрибут класу. Атрибут класу — це властивість, спільна для всіх екземплярів класу. На нього посилаються за допомогою нотації [Classe.attribut_de_classe] (рядки 30, 39). Атрибут класу [verrou] буде об’єктом синхронізації для всіх потоків, що виконують код рядків 31–36;
- рядки 14–19: конструктор отримує абсолютний шлях до файлу журналу. Цей файл відкривається, а отриманий дескриптор файлу зберігається у класі;
- рядок 17: файл журналу відкривається в режимі «додавання» (a). Кожен записаний рядок буде додаватися в кінець файлу;
- рядки 22–39: метод [write] дозволяє записати у файл журналу повідомлення, передане як параметр. До нього додаються дві інформації:
- рядок 24: поточна дата;
- рядок 25: поточний час;
- рядок 27: ім’я потоку, що записує журнал. Тут не слід забувати, що веб-додаток обслуговує одразу декількох користувачів. Кожному запиту призначається потік для його виконання. Якщо цей потік призупиняється, як правило, для операції вводу/виводу (мережа, файли, база даних), то процесор буде передано іншому потоку. Через ці можливі переривання не можна бути впевненим, що потік зможе записати рядок у файл журналу без переривання. Тоді існує ризик, що записи двох різних потоків змішаються. Ризик невеликий, можливо, навіть нульовий, але ми все ж вирішили показати, як синхронізувати доступ двох потоків до спільного ресурсу, в даному випадку — файлу журналу;
- рядок 30: перед записом потік запитує ключ від вхідних дверей. Запитуваний ключ — це той, що був створений у рядку 11. Він є дійсно унікальним: атрибут класу є унікальним для всіх екземплярів класу;
- у момент часу T1 потік Thread1 отримує ключ. Після цього він може виконати рядок 33;
- у момент часу T2 потік Thread1 призупиняється ще до того, як завершив запис у журнал;
- у момент часу T3 потік Thread2, який отримав доступ до процесора, також повинен записати запис у журнал. Він доходить до рядка 30, де запитує ключ від вхідних дверей. Йому відповідають, що інший потік уже має цей ключ. Тоді його автоматично призупиняють. Так само відбуватиметься з усіма потоками, які запитуватимуть цей ключ;
- у момент часу T4 потік Thread1, який був призупинений, знову отримує доступ до процесора. Після цього він завершує запис у журнал;
- рядки 32–36: запис у файл журналу відбувається у два етапи:
- рядок 33: дескриптор файлу, отриманий у рядку 17, працює з буфером. Операція [write] у рядку 33 записує дані в цей буфер, а не безпосередньо у файл. Потім буфер вивантажується у файл за певних умов:
- буфер заповнений;
- дескриптор файлу піддається операції [close] або [flush];
- рядок 36: примусово записується рядок журналу у файл. Це робиться для того, щоб побачити, як лог-записи різних потоків чергуються між собою. Якщо цього не зробити, лог-записи одного потоку будуть записані всі одночасно під час закриття дескриптора (рядок 45). Тоді буде набагато складніше визначити, що деякі потоки були зупинені: доведеться перевіряти час у лог-записах;
- рядок 39: потік Thread1 повертає ключ, який йому було надано. Тепер його можна передати іншому потоку;
- рядок 22: отже, метод [write] є синхронізованим: у файл логів записує лише один потік за раз. Ключовим моментом є рядок 30: що б не сталося, лише один потік отримує ключ для переходу до наступного рядка. Він зберігає його, доки не поверне (рядок 39);
- рядки 41–45: метод [close] дозволяє звільнити ресурси, виділені для дескриптора файлу журналу;
Журнали, записані у файл журналів, матимуть такий вигляд:
24.2.2. Клас [SendAdminMail]
Клас [SendAminMail] дозволяє надіслати повідомлення адміністратору додатка, коли той «вилітає».

Клас [SendAdminMail] налаштовується у скрипті [config] [2] наступним чином:
# конфігурація сервера SMTP
"adminMail": {
# сервер SMTP
"smtp-server": "localhost",
# порт сервера SMTP
"smtp-port": "25",
# адміністратор
"from": "guest@localhost.com",
"to": "guest@localhost.com",
# тема листа
"subject": "plantage du serveur de calcul d'impôts",
# tls — True, якщо сервер SMTP вимагає авторизації, інакше — False
"tls": False
}
Клас [SendAdminMail] отримує словник із рядків 2–13, а також налаштування надсилання електронного листа. Клас має такий вигляд:
# імпорт
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):
# надсилає повідомлення на сервер SMTP config['smtp-server'] через порт config[smtp-port]
# якщо config['tls'] має значення «true», буде використано підтримку TLS
# лист надсилається від імені config['from']
# для одержувача config['to']
# тема повідомлення — config['subject']
# у конфігурації знайдено посилання на логер ['logger']
# у конфігурації отримуємо логер — може дорівнювати None
logger = config["logger"]
# сервер SMTP
server = None
# відправляється повідомлення
try:
# сервер SMTP
server = smtplib.SMTP(config["smtp-server"])
# режим детального виведення
server.set_debuglevel(verbose)
# захищене з'єднання?
if config['tls']:
# початок діалогу щодо безпеки
server.starttls()
# аутентифікація
server.login(config["user"], config["password"])
# формування багаточастинного повідомлення — саме це повідомлення буде надіслано
msg = MIMEText(message)
msg['From'] = config["from"]
msg['To'] = config["to"]
msg['Date'] = formatdate(localtime=True)
msg['Subject'] = config["subject"]
# відправлення повідомлення
server.send_message(msg)
# журнал — логер може не існувати
if logger:
logger.write(f"[SendAdminMail] Message envoyé à [{config['to']}] : [{message}]\n")
except BaseException as erreur:
# журнал — логер може не існувати
if logger:
logger.write(
f"[SendAdminMail] Erreur [{erreur}] lors de l'envoi à [{config['to']}] du message [{message}] : \n")
finally:
# завершено — звільняються ресурси, задіяні функцією
if server:
server.quit()
- рядки 24–54: тут міститься код, який ми вже розглядали у прикладі |smtp/02|;
- рядок 20: отримується ідентифікатор логера. Він використовується в рядках 45 та 49;
24.3. Веб-сервер

24.3.1. Конфігурація

Конфігурація сервера дуже схожа на конфігурацію сервера, розглянутого раніше. Лише файл [config.py] дещо змінився:
def configure(config: dict) -> dict:
import os
# етап 1 ------
# папка цього файлу
script_dir = os.path.dirname(os.path.abspath(__file__))
# кореневий шлях
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# абсолютні залежності
absolute_dependencies = [
# папки проєкту
# 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",
# Константи, діапазони
f"{root_dir}/impots/v05/entities",
# IndexController
f"{root_dir}/impots/http-servers/01/controllers",
# скрипти [config_database, config_layers]
script_dir,
# Logger, SendAdminMail
f"{script_dir}/../utilities",
]
# встановлюємо syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# крок 2 ------
# налаштування програми
config.update({
# користувачі, яким дозволено користуватися додатком
"users": [
{
"login": "admin",
"password": "admin"
}
],
# файл журналу
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# конфігурація сервера SMTP
"adminMail": {
# сервер SMTP
"smtp-server": "localhost",
# порт сервера SMTP
"smtp-port": "25",
# адміністратор
"from": "guest@localhost.com",
"to": "guest@localhost.com",
# тема листа
"subject": "plantage du serveur de calcul d'impôts",
# tls — True, якщо сервер SMTP вимагає авторизації, інакше — False
"tls": False
},
# тривалість паузи потоку в секундах
"sleep_time": 0
})
# етап 3 ------
# конфігурація бази даних
import config_database
config["database"] = config_database.configure(config)
# етап 4 ------
# інстанціювання рівнів додатка
import config_layers
config['layers'] = config_layers.configure(config)
# зберігаємо конфігурацію
return config
- рядки 40–66: до словника конфігурації сервера додаються елементи, що стосуються логгера (рядок 49), та елементи, що стосуються надсилання попереджувального листа адміністратору додатка (рядки 51–63);
- рядок 65: щоб краще бачити роботу потоків, ми змусимо деякі з них зупинитися. [sleep_time] — це тривалість зупинки, виражена в секундах;
- рядки 27–28: зверніть увагу, що ми використовуємо контролер [index_controller] з попередньої версії 6;
24.3.2. Головний скрипт [main]
Головний скрипт [main] має такий вигляд:
# очікуємо параметр mysql або 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()
# налаштування додатка
import config
config = config.configure({'sgbd': sgbd})
# залежності
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
# менеджер автентифікації
auth = HTTPBasicAuth()
@auth.verify_password
def verify_password(login, password):
# список користувачів
users = config['users']
# проглядаємо цей список
for user in users:
if user['login'] == login and user['password'] == password:
return True
# не знайдено
return False
# надсилання електронного листа адміністратору
def send_adminmail(config: dict, message: str):
# надсилання електронного листа адміністратору додатка
config_mail = config["adminMail"]
config_mail["logger"] = config['logger']
SendAdminMail.send(config_mail, message)
# перевірка файлу журналу
logger = None
erreur = False
message_erreur = None
try:
# реєстратор
logger = Logger(config["logsFilename"])
except BaseException as exception:
# журнал консолі
print(f"L'erreur suivante s'est produite : {exception}")
# записування помилки
erreur = True
message_erreur = f"{exception}"
# заносимо логгер у конфігурацію
config['logger'] = logger
# обробка помилки
if erreur:
# лист адміністратору
send_adminmail(config, message_erreur)
# завершення роботи програми
sys.exit(1)
# журнал запуску
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)
# отримання даних від податкової служби
erreur = False
try:
# admindata буде даними на рівні додатка, доступними лише для читання
config["admindata"] = config["layers"]["dao"].get_admindata()
# журнал успішного виконання
logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
# помилка зафіксована
erreur = True
# журнал помилок
log = f"L'erreur suivante s'est produite : {ex}"
# консоль
print(log)
# файл журналу
logger.write(f"{log}\n")
# лист адміністратору
send_adminmail(config, log)
# головний потік більше не потребує логгера
logger.close()
# якщо сталася помилка, процес зупиняється
if erreur:
sys.exit(2)
# додаток Flask може запуститися
app = Flask(__name__)
# Головна URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
…
# тільки main
if __name__ == '__main__':
# запускаємо сервер
app.config.update(ENV="development", DEBUG=True)
app.run(threaded=True)
- рядки 1–10: скрипт очікує на параметр [mysql / pgres], який вказує йому, який SGBD слід використовувати;
- рядки 12–14: налаштовується додаток (Python Path, шари, база даних);
- рядки 16–28: необхідні для додатка залежності;
- рядки 30–43: управління автентифікацією;
- рядки 46–51: функція, яка надсилає електронний лист адміністратору додатка;
- функція очікує два параметри:
- config: словник із ключами [adminMail] та [logger];
- повідомлення, яке потрібно надіслати;
- рядки 49–50: готуємо налаштування відправлення;
- надсилаємо електронний лист;
- рядки 54–74: перевіряємо наявність файлу журналу;
- рядки 70–74: якщо не вдалося відкрити файл журналу, надсилається електронний лист адміністратору та процес зупиняється;
- рядки 76–79: реєструємо запуск сервера;
- рядки 81–98: здійснюється запит до бази даних податкової служби для отримання даних;
- рядки 88–98: якщо не вдалося отримати ці дані, помилка записується як у консоль, так і у файл журналу;
- рядки 100–101: головний потік більше не вестиме журнал (створені потоки не використовуватимуть той самий дескриптор файлу);
- рядки 103–105: якщо не вдалося підключитися до бази даних, програма зупиняється;
- рядок 122: запускається сервер у багатопотоковому режимі;
Функція [index] (рядок 114) має такий вигляд:
# Головна URL
@app.route('/', methods=['GET'])
@auth.login_required
def index():
logger = None
try:
# реєстратор
logger = Logger(config["logsFilename"])
# зберігається у конфігурації, пов'язаній із потоком
thread_config = {"logger": logger}
thread_name = threading.current_thread().name
config[thread_name] = {"config": thread_config}
# записуємо запит у журнал
logger.write(f"[index] requête : {request}\n")
# потік переривається, якщо це було запропоновано
sleep_time = config["sleep_time"]
if sleep_time != 0:
# пауза є випадковою, щоб одні потоки були перервані, а інші — ні
aléa = randint(0, 1)
if aléa == 1:
# запис у журнал перед паузою
logger.write(f"[index] mis en pause du thread pendant {sleep_time} seconde(s)\n")
# пауза
time.sleep(sleep_time)
# запит виконується контролером
résultat, status_code = index_controller.execute(request, config)
# чи сталася фатальна помилка?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# надсилається електронний лист адміністратору додатка
config_mail = config["adminMail"]
config_mail["logger"] = logger
SendAdminMail.send(config_mail, json.dumps(résultat, ensure_ascii=False))
# відповідь записується в журнал
logger.write(f"[index] {résultat}\n")
# відправляється відповідь
return json_response(résultat, status_code)
except BaseException as erreur:
# записуємо помилку в журнал, якщо це можливо
if logger:
logger.write(f"[index] {erreur}")
# готується відповідь клієнту
résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
# відправляється відповідь
return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
finally:
# закривається файл журналу, якщо він був відкритий
if logger:
logger.close()
- рядок 4: функція, що виконується, коли користувач запитує URL /. Оскільки сервер є багатопотоковим (рядок 112), для виконання функції буде створено потік. Цей потік у будь-який момент може бути перерваний і поставлений на паузу, щоб продовжити виконання трохи пізніше. Про це завжди слід пам’ятати, коли код звертається до ресурсу, спільного для всіх потоків. Таким ресурсом тут є файл журналів: усі потоки записують у нього;
- рядок 8: створюється екземпляр об’єкта запису в журнал. Отже, кожен потік матиме свій окремий екземпляр цього об’єкта. Проте всі ці об’єкти запису в журнал вказують на один і той самий файл журналу. Важливо зазначити, що коли потік закриває свій об’єкт запису в журнал, це не впливає на об’єкти запису в журнал інших потоків;
- рядки 9–12: об’єкт логування зберігається у словнику [config] додатка, пов’язаному з ключем, що має назву потоку. Отже, якщо одночасно виконується n потоків, у словнику [config] буде створено n записів. [config] є ресурсом, спільним для всіх потоків. Тому може виникнути потреба в синхронізації. Тут я висунув гіпотезу. Я припустив, що якщо два потоки одночасно створюють свої записи у файлі [config] і один із них переривається іншим, це не має значення. Той, що був перерваний, може згодом завершити створення запису. Якщо експеримент покаже, що ця гіпотеза є хибною, доведеться синхронізувати доступ до рядка 12;
- рядок 10: додаємо логер до словника;
- рядок 11: [threading.current_thread()] — це потік, який виконує цей рядок, отже, потік, що виконує функцію [index]. Записуємо його ім’я. Кожен потік має унікальне ім’я;
- рядок 12: запам'ятовуємо конфігурацію потоку. Відтепер ми завжди будемо діяти так: якщо є інформація, яку не можна ділити між потоками, її все одно помістимо в загальну конфігурацію, але пов'яжемо з іменем потоку;
- рядок 14: записуємо в журнал запит, який саме виконуємо;
- рядки 15–24: у випадковому порядку призупиняємо деякі потоки, щоб вони звільнили процесор для іншого потоку;
- рядок 16: з конфігурації отримуємо тривалість паузи (у секундах);
- рядок 17: пауза відбувається лише тоді, коли тривалість паузи відмінна від 0;
- рядок 19: випадкове ціле число в діапазоні [0, 1]. Отже, можливі лише значення 0 та 1;
- рядок 20: потік призупиняється лише тоді, коли випадкове число дорівнює 1;
- рядок 22: записується в журнал інформація про те, що потік буде перервано;
- рядок 24: потік призупиняється на [sleep_time] секунд;
- рядок 26: коли потік відновлює роботу, він запускає виконання запиту модулем [index_controller];
- рядки 28–32: якщо під час виконання виникає помилка типу [500 INTERNAL SERVER ERROR], надсилається електронний лист адміністратору;
- рядки 30–31: налаштовується словник [config_mail], який буде передано класу [SendAdminMail];
- рядок 32: повідомлення, що надсилається адміністратору, — це рядок jSON із результату, який буде надіслано клієнту;
- рядки 33–34: записуємо у журнал відповідь, яку будемо надсилати клієнту (рядок 36);
- рядки 37–44: обробка можливого винятку;
- рядки 39–40: якщо журнал існує, записуємо в нього помилку, що сталася;
- рядки 47–48: закриваємо логгер, якщо він існує. У підсумку потік створює логгер на початку запиту та закриває його після обробки запиту;
24.3.3. Контролер [index_controller]
Контролер [index_controller], який виконує запити, є тим самим, що й у попередній версії:

24.3.4. Виконання
Запускаємо сервер Flask, поштовий сервер |hMailServer|, а також поштовий клієнт |Thunderbird|. SGBD не запускаємо. Сервер зупиняється з такими записами в консолі:
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
Файл журналу [logs.txt] має такий вигляд:
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)]]
За допомогою Thunderbird перевіряємо листи адміністратора [guest@localhost.com]:

Потім запускаємо SGBD і запитуємо URL та [http://127.0.0.1:5000/?mari%C3%A9=oui&enfants=3&salaire=200000]. Журнали мають такий вигляд:
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}}}
- рядки 1–4: нагадаємо, що сервер запускається двічі, оскільки режим [Debug=True] викликає другий запуск;
- рядки 5–6: журнали дають уявлення про час виконання запиту, у даному випадку 2,293 мілісекунди;
24.4. Веб-клієнт


Файл [http-clients/02] отримано шляхом копіювання файлу [http-clients/01]. Потім до нього вносяться деякі зміни.
24.4.1. Конфігурація
Конфігурація [config] додатка [http-clients/02] є такою самою, як і конфігурація додатка [http-clients/01], за винятком кількох дрібних деталей:
def configure(config: dict) -> dict:
import os
# крок 1 ------
# папка цього файлу
script_dir = os.path.dirname(os.path.abspath(__file__))
# кореневий шлях
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# абсолютні залежності
absolute_dependencies = [
# папки проєкту
# 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",
# Константи, діапазони
f"{root_dir}/impots/v05/entities",
# ImpôtsDaoWithHttpClient
f"{script_dir}/../services",
# конфігураційні скрипти
script_dir,
# Реєстратор
f"{root_dir}/impots/http-servers/02/utilities",
]
# встановлюємо syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# крок 2 ------
# налаштування програми за допомогою констант
config.update({
# файл платників податків
"taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
# файл результатів
"resultsFilename": f"{script_dir}/../data/output/résultats.json",
# файл помилок
"errorsFilename": f"{script_dir}/../data/output/errors.txt",
# файл журналів
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# сервер розрахунку податку
"server": {
"urlServer": "http://127.0.0.1:5000/",
"authBasic": True,
"user": {
"login": "admin",
"password": "admin"
}
},
# режим налагодження
"debug": True
}
)
# етап 3 ------
# інстанціювання шарів
import config_layers
config['layers'] = config_layers.configure(config)
# видаємо конфігурацію
return config
- рядки 31–32: ми будемо використовувати той самий логер |Logger|, що й для сервера;
- рядок 49: абсолютний шлях до файлу логів;
- рядок 60: режим [debug=True] слугує для запису відповідей веб-сервера у файл логів;
24.4.2. Рівень [dao]
Код класу [ImpôtsDaoWithHttpClient] дещо змінюється:
# імпорт
import requests
from flask_api import status
…
class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
# конструктор
def __init__(self, config: dict):
# ініціалізація батьківського об’єкта
AbstractImpôtsDao.__init__(self, config)
# збереження елементів конфігурації
# загальна конфігурація
self.__config = config
# сервер
self.__config_server = config["server"]
# режим налагодження
self.__debug = config["debug"]
# реєстратор
self.__logger = None
# невикористаний метод
def get_admindata(self) -> AdminData:
pass
# розрахунок податку
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
# дозволяє передавати винятки
…
# режим налагодження?
if self.__debug:
# логгер
if not self.__logger:
self.__logger = self.__config['logger']
# запис у журнал
self.__logger.write(f"{response.text}\n")
# код статусу відповіді HTTP
status_code = response.status_code
…
- рядок 17: зберігається загальна конфігурація. Пізніше ми побачимо, що під час виконання конструктора класу [ImpôtsDaoWithHttpClient] словник [config] ще не містить ключа [logger], який використовується в рядку 37. Саме з цієї причини неможливо ініціалізувати [self.__logger] (рядок 23) у конструкторі;
- рядок 21: у конфігурацію додано ключ [debug], який керує журналом у рядках 33–39;
- рядок 34: якщо перебуваємо в режимі [debug];
- рядки 36–37: можлива ініціалізація властивості [self.__logger]. Коли використовується метод [calculate_tax], ключ [logger] входить до словника [config];
- рядок 39: здійснюється запис у журнал текстового документа, пов’язаного з відповіддю HTTP від сервера;
Рівень [dao] буде виконуватися одночасно кількома потоками. Однак тут створюється лише один екземпляр цього рівня (див. config_layers). Тому необхідно перевірити, чи код не передбачає доступ для запису до спільних даних, як правило, до властивостей класу [ImpôtsDaoWithHttpClient], який реалізує шар [dao]. Однак у наведеному вище коді рядок 37 змінює властивість екземпляра класу. У даному випадку це не має наслідків, оскільки всі потоки використовують один і той самий логер. Якби це було не так, доступ до рядка 37 мав би бути синхронізований.
24.4.3. Головний скрипт
Головний скрипт [main] змінюється наступним чином:
# налаштування додатка
import config
config = config.configure({})
# залежності
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger
# виконання шару [dao] у потоці
# taxpayers — це список платників податків
def thread_function(dao, logger, taxpayers: list):
…
# список потоків клієнта
threads = []
logger = None
# код
try:
# реєстратор
logger = Logger(config["logsFilename"])
# зберігається у конфігурації
config["logger"] = logger
# отримуємо шар [dao]
dao = config["layers"]["dao"]
# зчитування даних платників податків
taxpayers = dao.get_taxpayers_data()["taxpayers"]
# платників податків?
if not taxpayers:
raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
# розрахунок податку платників податків за допомогою декількох потоків
i = 0
l_taxpayers = len(taxpayers)
while i < len(taxpayers):
# кожен потік оброблятиме від 1 до 4 платників податків
nb_taxpayers = min(l_taxpayers - i, random.randint(1, 4))
# список платників податків, оброблених потоком
thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
# збільшуємо значення i для наступного потоку
i += nb_taxpayers
# створюється потік
thread = threading.Thread(target=thread_function, args=(dao, logger, thread_taxpayers))
# додаємо його до списку потоків головного скрипта
threads.append(thread)
# запускається потік — ця операція є асинхронною — результат роботи потоку не очікується
thread.start()
# головний потік очікує завершення роботи всіх потоків, які він запустив
for thread in threads:
thread.join()
# тут усі потоки завершили свою роботу — кожен з них змінив один або кілька об’єктів [taxpayer]
# результати записуються у файл jSON
dao.write_taxpayers_results(taxpayers)
except BaseException as erreur:
# виведення повідомлення про помилку
print(f"L'erreur suivante s'est produite : {erreur}")
finally:
# закриття логера
if logger:
logger.close()
# завершено
print("Travail terminé...")
# завершення потоків, які можуть ще існувати, якщо процес зупинився через помилку
sys.exit()
- Головний скрипт відрізняється від скрипта попереднього клієнта тим, що він створює кілька потоків виконання для надсилання запитів на сервер. Клієнт версії 6 надсилав усі свої запити послідовно. Запит № i надсилався лише після отримання відповіді на запит № [i-1]. Тут ми хочемо побачити, як поведеться сервер, коли отримає кілька одночасних запитів. Для цього нам потрібні потоки;
- рядок 21: створені потоки будуть додані до списку. Слід розуміти, що скрипт [main] також виконується потоком, який називається [MainThread]. Цей головний потік створить інші потоки, які будуть відповідати за розрахунок податку одного або кількох платників податків;
- рядок 26: створюється об’єкт журналу. Він буде спільним для всіх потоків;
- рядок 32: отримуємо всіх платників податків, для яких потрібно розрахувати податок;
- рядки 39–51: ці платники податків розподіляються між кількома потоками;
- рядки 40–41: кожен потік оброблятиме від 1 до 4 платників податків. Ця кількість визначається випадковим чином;
- [random.randint(1, 4)] випадковим чином вибирає число зі списку [1, 2, 3, 4];
- потік не може мати більше ніж [l-i] платників податків, де [l-i] — це кількість платників податків, яким ще не призначено потік;
- отже, береться мінімальне з цих двох значень;
- рядок 43: як тільки стає відомо [nb_taxpayers] — кількість платників податків, що обробляються потоком, — їх вибирають зі списку платників податків:
- [slice(10,12)] — це сукупність індексів [10, 11, 12];
- [response.text[39:]] — це список [taxpayers[10], taxpayers[11], taxpayers[12];
- рядок 45: збільшуємо значення i, яке керує циклом у рядку 39;
- рядок 47: створюється потік:
- [target=thread_function] визначає функцію, яку виконуватиме потік. Це функція з рядків 16–17. Вона очікує три параметри;
- [ags] — це список трьох параметрів, які очікує функція [thread_function];
Створення потоку не означає його виконання. Це лише створення об’єкта і все;
- рядки 48–49: щойно створений потік додається до списку потоків, створених головним потоком;
- рядок 51: потік запускається. Відтепер він буде виконуватися паралельно з іншими активними потоками. Тут він виконає функцію [thread_function] із заданими аргументами;
- рядки 53–54: головний потік очікує на завершення кожного з запущених ним потоків. Розглянемо приклад:
- головний потік запустив три потоки [th1, th2, th3];
- головний потік переходить у стан очікування кожного з цих потоків (рядки 53–54) у порядку циклу for: [th1, th2, th3];
- припустимо, що потоки завершуються в порядку [th2, th1, th3];
- головний потік очікує завершення th1. Коли th2 завершується, нічого не відбувається;
- коли th1 завершується, головний потік переходить у режим очікування th2. Однак останній уже завершився. Тоді головний потік переходить до наступного потоку й очікує th3;
- коли th3 завершується, головний потік закінчує очікування і переходить до виконання рядка 57;
- у рядку 57 отримані результати записуються у файл результатів. Це хороший приклад використання посилань на об’єкти:
- рядок 43: список [thread_payers], пов’язаний із потоком, містить копії посилань на об’єкти, що містяться у списку [taxpayers];
- відомо, що обчислення податку змінить об’єкти, на які вказують посилання зі списку [thread_payers]. Ці об’єкти будуть доповнені результатами обчислення податку. Проте самі посилання не змінюються. Отже, посилання початкового списку [taxpayers] «бачать» або «вказують на» змінені об’єкти;
Функція [thread_function], що виконується потоками, має такий вигляд:
# виконання рівня [dao] у потоці
# taxpayers — це список платників податків
def thread_function(dao, logger, taxpayers: list):
# запис у журналі про початок потоку
thread_name = threading.current_thread().name
logger.write(f"début du thread [{thread_name}] avec {len(taxpayers)} contribuable(s)\n")
# розраховується податок платників податків
for taxpayer in taxpayers:
# журнал
logger.write(f"début du calcul de l'impôt de {taxpayer}\n")
# синхронний розрахунок податку
dao.calculate_tax(taxpayer)
# журнал
logger.write(f"fin du calcul de l'impôt de {taxpayer}\n")
# журнал: кінець потоку
logger.write(f"fin du thread [{thread_name}]\n")
- функції, що виконуються одночасно кількома потоками, часто складно писати: завжди потрібно перевіряти, чи код не намагається змінити дані, спільні для потоків. У такому випадку необхідно забезпечити синхронізований доступ до спільних даних, які будуть змінені;
- рядок 3: функція отримує три параметри:
- [dao]: посилання на шар [dao]. Ці дані є спільними;
- [logger]: посилання на логер. Ці дані є спільними;
- [taxpayers]: список платників податків. Ці дані не є спільними: кожен потік керує окремим списком;
- розглянемо два посилання [dao, logger]:
- ми бачили, що об’єкт, на який вказує посилання [dao], мав посилання [self.__logger], яке змінювалося потоками, але для того, щоб встановити значення, спільне для всіх потоків;
- посилання [logger] вказує на дескриптор файлу. Ми бачили, що під час запису логів у файл може виникнути проблема. З цієї причини запис у файл було синхронізовано;
- рядки 5–6: записується в журнал ім’я потоку та кількість платників податків, якими він має керувати;
- рядки 8–14: обчислення податку платників податків;
- рядок 16: записується в журнал завершення роботи потоку;
24.4.4. Виконання
Запустимо веб-сервер, як описано в попередньому параграфі (веб-сервер, SGBD, hMailServer, Thunderbird), а потім запустимо скрипт [main] на стороні клієнта. У файлах [data/output/errors.txt, data/output/résultats.json] ми отримуємо ті самі результати, що й у попередній версії. У файлі [data/logs/logs.txt] ми бачимо такі записи журналу:
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]
- ці записи журналу показують, що було запущено п’ять потоків для розрахунку податку для 11 платників податків. Ці п’ять потоків одночасно відправили запити на сервер розрахунку податку. Потрібно зрозуміти, як це працює:
- потік [Thread-1] запускається першим. Коли він отримує доступ до процесора, він просувається по коду, аж доки не надішле свій запит HTTP. Оскільки йому доводиться чекати на результат цього запиту, він автоматично переводиться в режим очікування. Тоді він втрачає доступ до процесора, і його отримує інший потік;
- рядки 1–10: той самий процес повторюється для кожного з 5 потоків. Таким чином, 5 потоків запускаються ще до того, як потік [Thread-1] отримав свою відповідь (рядок 11);
- потоки завершуються не в тому порядку, в якому вони були запущені. Отже, першим завершується потік [Thread-3], рядок 23;
Що стосується сервера, то записи в файлі [data/logs/logs.txt] мають такий вигляд:
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/?одружений=так&діти=3&зарплата=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/?одружений=так&діти=2&зарплата=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/?одружений=ні&діти=3&зарплата=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/?одружений=так&діти=2&зарплата=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/?одружений=так&діти=3&зарплата=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/?одружений=так&діти=5&зарплата=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/?одружений=ні&діти=0&зарплата=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/?одружений=так&діти=3&зарплата=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/?одружений=ні&діти=2&зарплата=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/?одружений=ні&діти=0&зарплата=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}}}
- видно, що 11 потоків обробили 11 платників податків;
- деякі потоки були поставлені в режим очікування (рядки 6, 8, 12, 14, 20, 22), а інші — ні (рядки 9, 23, 25, 29, 31);
24.5. Тестування шару [dao]
Як і в |попередній версії|, ми тестуємо рівень [dao] клієнта. Принцип роботи точно такий самий:

Тестовий клас буде виконано в такому середовищі:

- конфігурація [2] ідентична конфігурації [1], яку ми щойно розглянули;
Тестовий клас [TestHttpClientDao] має такий вигляд:
import unittest
from Logger import Logger
class TestHttpClientDao(unittest.TestCase):
def test_1(self) -> None:
from TaxPayer import TaxPayer
# {'одружений': 'так', 'діти': 2, 'зарплата': 55555,
# 'податок': 2814, 'надбавка': 0, 'знижка': 0, 'зниження': 0, 'ставка': 0,14}
taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
dao.calculate_tax(taxpayer)
# перевірка
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__':
# налаштування програми
import config
config = config.configure({})
# реєстратор
logger = Logger(config["logsFilename"])
# записується в конфігурацію
config["logger"] = logger
# отримуємо шар [dao]
dao = config["layers"]["dao"]
# виконуємо методи тестування
print("tests en cours...")
unittest.main()
- створюємо |конфігурацію виконання| для цього тесту;
- запускаємо веб-сервер разом з усім його середовищем;
- виконуємо тест;
Результати такі:
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