Skip to content

24. Практичне завдання: версія 7

24.1. Вступ

Версія 7 програми для розрахунку податку ідентична версії 6, за винятком таких деталей:

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

Архітектура додатка не змінюється:

Структура скриптів така:

Image

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

24.2. Утиліти

Image

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] дозволяє звільнити ресурси, виділені для дескриптора файлу журналу;

Журнали, записані у файл журналів, матимуть такий вигляд:

2020-07-22 20:03:52.992152, Thread-2 : …

24.2.2. Клас [SendAdminMail]

Клас [SendAminMail] дозволяє надіслати повідомлення адміністратору додатка, коли той «вилітає».

Image

Клас [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. Веб-сервер

Image

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

Image

Конфігурація сервера дуже схожа на конфігурацію сервера, розглянутого раніше. Лише файл [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], який виконує запити, є тим самим, що й у попередній версії:

Image

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

Image

Потім запускаємо 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. Веб-клієнт

Image

Image

Файл [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] клієнта. Принцип роботи точно такий самий:

Image

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

Image

  • конфігурація [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