Skip to content

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

25.1. Вступ

Ми напишемо нову клієнт-серверну програму. Новизна сервера полягає в тому, що він буде керувати сесією. Замість того, щоб розміщувати дані податкової адміністрації в об’єкті з областю дії [application], ми розмістимо їх в об’єкті з областю дії [session]. Таким чином, ми знижуємо продуктивність коду. Коли об’єкт може бути доступний у режимі «тільки для читання» для всіх користувачів, краще зробити його об’єктом області дії [application], а не [session]. Це дозволяє заощадити принаймні пропускну здатність, оскільки таким чином зменшується розмір сесійного файлу cookie. Але ми хочемо продемонструвати клієнт-серверну програму, в якій клієнт і сервер обмінюються сесійним файлом cookie.

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

Image

25.2. Веб-сервер

Структура каталогів скриптів сервера така:

Image

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

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

Вона така сама, як і в |попередній версії|, з деякими змінами у скрипті [config]:


# абсолютні залежності
    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",
        # index_controller
        f"{script_dir}/../controllers",
        # скрипти [config_database, config_layers]
        script_dir,
        # Logger, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
    ]
  • рядок 17: перепишемо контролер для функції [index], яка обробляє файл URL / ;
  • рядок 21: використовуємо утиліти з |попередньої версії|;

25.2.2. Головний скрипт [main]

Новий скрипт [main] вносить деякі зміни до головного скрипта [main] попередньої версії:


# додаток Flask може запуститися
app = Flask(__name__)
# секретний ключ сеансу
app.secret_key = os.urandom(12).hex()
  • рядок 4: створюється секретний ключ для програми. Відомо, що він необхідний для управління сесіями;

Далі у коді [main] більше не запитуються податкові дані. Видаляються такі рядки:


# отримання даних від податкової служби
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)

Крім того, контролер [index_controller] підтримує додатковий параметр — сесію Flask:


from flask import request, Flask, session
….        
         # запит виконується контролером
        résultat, status_code = index_controller.execute(request, session, config)

25.2.3. Контролер [index_controller]

Контролер [index_controller] тепер підтримує сесію:


# імпорт залежностей
import os
import re
import threading

from flask_api import status
from werkzeug.local import LocalProxy

# URL з параметрами: /?одружений=xx&діти=yy&зарплата=zz
from AdminData import AdminData
from ImpôtsError import ImpôtsError


def execute(request: LocalProxy, session: LocalProxy, config: dict) -> tuple:
    # залежності
    from TaxPayer import TaxPayer

    # спочатку помилок немає
    erreurs = []
    

    # помилки?
    if erreurs:
        # клієнту повертається відповідь про помилку
        return {"réponse": {"erreurs": erreurs}}, status.HTTP_400_BAD_REQUEST

    # помилок немає, можна працювати
    # отримуємо конфігурацію, пов’язану з потоком
    thread_name = threading.current_thread().name
    logger = config[thread_name]["config"]["logger"]
    # виконується запит
    réponse = None
    try:
        # найпростіший випадок — admindata вже у сесії
        if session.get('client_id') is not None:
            # отримуємо інформацію про сесію
            client_id = session.get('client_id')
            admindata = AdminData().fromdict(session.get('admindata'))
            # журнал
            logger.write(f"[index_controller] client [{client_id}], données fiscales prises en session\n")
        else:
            # отримання даних податкової адміністрації
            admindata = config["layers"]["dao"].get_admindata()
            # вхід admindata в сесію
            session['admindata'] = admindata.asdict()
            # присвоюємо клієнту номер і записуємо цей номер у сесію
            # це дозволить нам відстежувати його в журналах сервера
            client_id = os.urandom(12).hex()
            session['client_id'] = client_id
            # журнал
            logger.write(f"[index_controller] client [{client_id}], données fiscales prises dans la couche dao\n")
        # розрахунок податку
        taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
        config["layers"]["métier"].calculate_tax(taxpayer, admindata)
        # надаємо відповідь клієнту
        return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
    except (BaseException, ImpôtsError) as erreur:
        # відправляємо відповідь клієнту
        return {"réponse": {"erreurs": [f"{erreur}"]}}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • рядок 14: контролер отримує поточну сесію веб-клієнта;
  • рядки 35–38: якщо у клієнта є сесія, то вона містить два ключі:
    • [client_id]: номер клієнта (рядок 37);
    • [admindata]: дані податкової адміністрації у вигляді словника (рядок 38);
  • рядок 35: перевіряється, чи сесія містить один із двох очікуваних ключів;
  • рядки 42–51: випадок, коли сесія клієнта ще не ініціалізована;
    • рядок 43: отримуємо дані податкової адміністрації з шару [dao];
    • рядок 45: ці дані заносяться в сесію у вигляді словника;
    • рядок 48: клієнту присвоюється випадковий номер. Цей номер буде різним для кожного клієнта;
    • рядок 49: цей номер зберігається в сесії;
    • рядок 51: реєструється факт отримання даних податкової служби через шар [dao]. Звернення до шару [dao], як правило, є ресурсоємними. Тому їх слід обмежувати. Ідея полягає в тому, щоб один раз отримати податкові дані з шару [dao], зберегти їх у сесії та використовувати їх звідти під час наступних запитів того самого клієнта. Нагадуємо, що це не найкраще рішення. Оскільки податкові дані податкової адміністрації однакові для всіх клієнтів, їх місце — в об’єкті з областю дії на рівні додатка;
  • рядки 35–40: випадок, коли сесія клієнта була ініціалізована під час попереднього запиту;
    • рядок 37: отримуємо номер клієнта з сесії;
    • рядок 38: отримуємо податкові дані адміністрації з сесії;
    • рядок 40: фіксуємо в журналі, що клієнт отримав податкові дані адміністрації з сесії;

25.3. Веб-клієнт

Image

25.3.1. Рівень [dao]

25.3.1.1. Клас [ImpôtsDaoWithHttpSession]

Рівень [dao] реалізовано наступним класом [ImpôtsDaoWithHttpSession]:


# імпорт

import requests
from flask_api import status
from myutils import decode_flask_session

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsMétier import InterfaceImpôtsMétier
from TaxPayer import TaxPayer


class ImpôtsDaoWithHttpSession(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
        # файли cookie
        self.__cookies = None

    # невикористаний метод
    def get_admindata(self) -> AdminData:
        pass

    # розрахунок податку
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        # дозволяє передавати винятки
        # параметри GET
        params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
        # підключення з автентифікацією Auth Basic?
        if self.__config_server['authBasic']:
            response = requests.get(
                # URL запитуваного сервера
                self.__config_server['urlServer'],
                # параметри URL
                params=params,
                # аутентифікація Basic
                auth=(
                    self.__config_server["user"]["login"],
                    self.__config_server["user"]["password"]),
                cookies=self.__cookies)

        else:
            # підключення без аутентифікації Auth Basic
            response = requests.get(self.__config_server['urlServer'], params=params, cookies=self.__cookies)
        # отримуємо файли cookie з відповіді, якщо вони є
        if response.cookies:
            self.__cookies = response.cookies
            # витягується сесійний файл cookie
            session_cookie = response.cookies.get('session')
            # декодується для входу
            if session_cookie:
                # модуль входу
                if not self.__logger:
                    self.__logger = self.__config['logger']
                # здійснюється вхід
                self.__logger.write(f"cookie de session={decode_flask_session(session_cookie)}\n")

        # режим налагодження?
        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
        # відповідь jSON додається до словника
        résultat = response.json()
        # помилка, якщо код статусу відмінний від 200 OK
        if status_code != status.HTTP_200_OK:
            # відомо, що помилки були пов’язані з ключем [erreurs] відповіді
            raise ImpôtsError(87, résultat['réponse']['erreurs'])
        # відомо, що результат пов'язано з ключем [result] у відповіді
        # вхідний параметр змінюється з урахуванням цього результату
        taxpayer.fromdict(résultat["réponse"]["result"])
  • рядок 30: рівень [dao] буде керувати словником файлів cookie;
  • рядок 58: властивість [response.cookies] — це словник, що містить файли cookie, надіслані сервером у заголовках HTTP та [Set-Cookie];
  • рядок 59: ці файли cookie зберігаються у шарі [dao]. Вони будуть надіслані назад на сервер під час наступних запитів від того самого клієнта;
  • рядки 60–68: хоча це й не є обов’язковим, ми отримуємо сесійний файл cookie. У словнику файлів cookie, надісланих сервером, сесійний файл cookie пов’язаний із ключем [session];
  • рядки 62–68: ми декодуємо сесійний файл cookie для входу користувача;
  • рядок 68: пізніше ми повернемося до функції [decode_flask_session], яка декодує сесійний файл cookie;
  • рядки 52 та 57: при кожному запиті від того самого клієнта файли cookie, надіслані сервером, повертаються йому. Саме таким чином підтримується сесія Flask між клієнтом і сервером;

Тепер слід пам’ятати, що рівень [dao] буде виконуватися одночасно кількома потоками. Тому потрібно перевірити всі властивості екземпляра класу та з’ясувати, чи не створює проблем одночасний доступ до цих властивостей. Тут ми додали властивість [self.__cookies] (рядок 30). Ця властивість змінюється в рядку 59. Отже, ми маємо доступ на запис до даних, спільних для всіх потоків. Однак такий доступ створює проблему: кожен потік, що представляє певного клієнта, має власний сесійний файл cookie. Адже в ньому міститься унікальний номер клієнта (=потоку) для кожного клієнта. Якщо нічого не зробити, потік T2 може перезаписати файли cookie потоку T1.

Ми вже розглядали один із способів вирішення цієї проблеми: у файлі [config], що передається як параметр конструктору (рядок 17), можна створити різні ключі для кожного потоку. Наприклад, як ключ можна використовувати ім’я потоку:

  • у рядку 59 можна написати:

config[thread_name][‘cookies’]=cookies
  • у рядку 52 можна було б написати:

cookies=config[thread_name][‘cookies’]

Тут ми використаємо іншу техніку: кожен потік (=клієнт) матиме власний шар [dao]. Таким чином, рядок 59 більше не створює проблем, оскільки використовуються файли cookie лише одного клієнта.

Для цього ми створимо новий клас [ImpôtsDaoWithHttpSessionFactory].

25.3.1.2. Функція декодування сесії Flask

Функція [decode_flask_session] визначена у скрипті [myutils]:

Image

Ми вже розглядали скрипт |myutils|. Цей скрипт є модулем на рівні машини, який різні скрипти цього курсу можуть імпортувати за допомогою оператора:

import myutils

У ньому функція [decode_flask_session] визначається наступним чином:


def decode_flask_session(cookie: str) -> str:
    # джерело: https://www.kirsle.net/wizards/flask-session.cgi
    compressed = False
    payload = cookie

    if payload.startswith('.'):
        compressed = True
        payload = payload[1:]

    data = payload.split(".")[0]

    data = base64_decode(data)
    if compressed:
        data = zlib.decompress(data)

    return data.decode("utf-8")
  • рядок 2: URL, де я знайшов цю функцію;
  • рядок 1: параметр [cookie] — це рядок символів, пов’язаний із ключем [session] у словнику файлів cookie, отриманих веб-клієнтом;
  • рядки 3–16: я не коментуватиму цей код, оскільки не володію ним;

Додаємо новий імпорт у файл [__init__.py]:


from .myutils import set_syspath, json_response, decode_flask_session

Нова версія [myutils] встановлюється серед модулів машинного рівня за допомогою команди [pip install .] у терміналі Pycharm:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install .
Processing c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\packages
Using legacy setup.py install for myutils, since package 'wheel' is not installed.
Installing collected packages: myutils
  Attempting uninstall: myutils
    Found existing installation: myutils 0.1
    Uninstalling myutils-0.1:
      Successfully uninstalled myutils-0.1
    Running setup.py install for myutils ... done
Successfully installed myutils-0.1
  • рядок 1: щоб ввести цю команду, потрібно перебувати в папці [packages];

25.3.1.3. Клас [ImpôtsDaoWithHttpSessionFactory]

Клас [ImpôtsDaoWithHttpSessionFactory] має такий вигляд:


from ImpôtsDaoWithHttpSession import ImpôtsDaoWithHttpSession


class ImpôtsDaoWithHttpSessionFactory:

    def __init__(self, config: dict):
        # зберігаємо параметр
        self.__config = config

    def new_instance(self):
        # створюється екземпляр шару [dao]
        return ImpôtsDaoWithHttpSession(self.__config)
  • клас [ImpôtsDaoWithHttpSessionFactory] дозволяє створити нову реалізацію шару [dao] з використанням методу [new_instance] з рядків 10–12;

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

Скрипт [config_layers], який налаштовує шари веб-клієнта, змінюється наступним чином:


def configure(config: dict) -> dict:
    # створення екземплярів шарів додатка

    # шар DAO
    from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
    dao_factory = ImpôtsDaoWithHttpSessionFactory(config)

    # виконується налаштування шарів
    return {
        "dao_factory": dao_factory
    }
  • рядки 5–6: замість створення екземпляра єдиного шару [dao], як це робилося раніше, створюється екземпляр «фабрики» цього шару (фабрика = об’єкт, що створює об’єкти, у даному випадку шар [dao]);
  • рядки 9–11: повертаємо конфігурацію шарів;

25.3.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(thread_dao, logger, taxpayers: list):
    


# список потоків клієнта
threads = []
logger = None
# код
try:
    
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        
        # кожен потік повинен мати власний рівень [dao] для правильного управління своїм сесійним файлом cookie
        thread_dao = dao_factory.new_instance()
        # створюємо потік
        thread = threading.Thread(target=thread_function, args=(thread_dao, logger, thread_taxpayers))
        # додаємо його до списку потоків головного скрипта
        threads.append(thread)
        # запускаємо потік — ця операція є асинхронною — не чекаємо на результат роботи потоку
        thread.start()
    # головний потік очікує завершення всіх запущених ним потоків
    
except BaseException as erreur:
    # виведення повідомлення про помилку
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # закриття логгера
    if logger:
        logger.close()
    # завершено
    print("Travail terminé...")
    # завершення потоків, які ще можуть існувати, якщо процес зупинився через помилку
    sys.exit()
  • рядки 29–30: кожен потік має свій шар [dao];

25.3.4. Запуск клієнта

Запускається веб-сервер, запускається SGBD, запускається поштовий сервер [hMailServer]. Потім запускається скрипт [main] веб-клієнта. Журнали виконання в [data/logs/logs.txt] мають такий вигляд:


2020-07-25 10:21:46.478511, Thread-1 : début du thread [Thread-1] avec 1 contribuable(s)
2020-07-25 10:21:46.479111, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-25 10:21:46.479111, Thread-2 : début du thread [Thread-2] avec 1 contribuable(s)
2020-07-25 10:21:46.480195, Thread-3 : début du thread [Thread-3] avec 2 contribuable(s)
2020-07-25 10:21:46.480195, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-25 10:21:46.481137, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
2020-07-25 10:21:46.481137, Thread-3 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-25 10:21:46.482279, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
2020-07-25 10:21:46.482622, Thread-6 : début du thread [Thread-6] avec 1 contribuable(s)
2020-07-25 10:21:46.482622, Thread-4 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-5 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-6 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-25 10:21:46.725910, Thread-4 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"fa3c83b82761c83e13217967"}
2020-07-25 10:21:46.725910, Thread-4 : {"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-25 10:21:46.725910, Thread-4 : 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-25 10:21:46.726960, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:47.514108, Thread-3 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,24999.5],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"700e3f5dc808c7c48f0c9007"}
2020-07-25 10:21:47.514610, Thread-3 : {"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-25 10:21:47.514939, Thread-3 : 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-25 10:21:47.514939, Thread-3 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000}
2020-07-25 10:21:47.527211, Thread-5 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"9e14a5d4a3057f69ab95ab2d"}
2020-07-25 10:21:47.527211, Thread-2 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,22500.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"a06e8fd70a44c9e311f4dce0"}
2020-07-25 10:21:47.527211, Thread-5 : {"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-25 10:21:47.527211, Thread-1 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"28c38df998f67685b3a482b8"}
2020-07-25 10:21:47.527211, 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-25 10:21:47.528341, Thread-5 : 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-25 10:21:47.528341, 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-25 10:21:47.528842, 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-25 10:21:47.529349, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000}
2020-07-25 10:21:47.529699, 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-25 10:21:47.529699, Thread-2 : fin du thread [Thread-2]
2020-07-25 10:21:47.531905, Thread-1 : fin du thread [Thread-1]
2020-07-25 10:21:47.536121, Thread-6 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,93749.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"38499b63076516c02f2770ec"}
2020-07-25 10:21:47.537161, Thread-3 : {"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-25 10:21:47.537161, Thread-6 : {"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-25 10:21:47.538156, Thread-3 : 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-25 10:21:47.538557, Thread-6 : 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-25 10:21:47.538828, Thread-3 : fin du thread [Thread-3]
2020-07-25 10:21:47.538828, Thread-6 : fin du thread [Thread-6]
2020-07-25 10:21:47.546198, 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-25 10:21:47.546198, 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-25 10:21:47.546198, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000}
2020-07-25 10:21:47.739643, 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-25 10:21:47.739643, 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-25 10:21:47.740668, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000}
2020-07-25 10:21:48.557469, 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-25 10:21:48.558715, 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-25 10:21:48.558715, Thread-5 : fin du thread [Thread-5]
2020-07-25 10:21:48.753025, 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-25 10:21:48.753318, 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-25 10:21:48.753540, Thread-4 : fin du thread [Thread-4]
  • всього є 6 потоків, тобто 6 клієнтів (рядки 1, 3, 4, 6, 8, 9), які одночасно звертаються до сервера розрахунку податку;
  • ми простежимо за потоком [Thread-4], який обслуговує 3 платників податків (рядок 6). Він послідовно зробить три запити до сервера розрахунку податку;
  • рядок 10: перший запит від [Thread-4];
  • рядок 13: [Thread-4] отримав відповідь на свій перший запит. У ній він знаходить сесійний файл cookie, у якому міститься номер [fa3c83b82761c83e13217967], присвоєний йому сервером;
  • рядок 14: податок першого платника податків;
  • рядок 16: [Thread-4] надсилає запит щодо другого платника податків;
  • рядок 43: [Thread-4] отримує суму податку другого платника податків;
  • рядок 45: [Thread-4] надсилає запит щодо третього платника податків;
  • рядок 49: [Thread-4] отримує податок від третього платника податків;
  • рядок 51: [Thread-4] завершив свою роботу;

Тепер давайте подивимося, як 3 запити від [Thread-4] були оброблені на стороні сервера. Ми зможемо відстежити це в логах сервера за допомогою його клієнтського номера [fa3c83b82761c83e13217967].

Журнали [data/logs/logs.txt] на стороні сервера виглядають так:


2020-07-25 10:21:39.187366, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:40.439093, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:46.502011, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=50000' [GET]>
2020-07-25 10:21:46.504049, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.505452, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?одружений=так&діти=2&зарплата=55555' [GET]>
2020-07-25 10:21:46.506257, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.507292, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?одружений=ні&діти=0&зарплата=100000' [GET]>
2020-07-25 10:21:46.507292, Thread-4 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.508301, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?одружений=так&діти=2&зарплата=50000' [GET]>
2020-07-25 10:21:46.509293, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.511808, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?одружений=ні&діти=3&зарплата=100000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?одружений=так&діти=3&зарплата=200000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.719504, Thread-6 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises dans la couche dao
2020-07-25 10:21:46.720003, Thread-6 : [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-25 10:21:46.736108, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?одружений=так&діти=3&зарплата=100000' [GET]>
2020-07-25 10:21:46.736108, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.506709, Thread-2 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises dans la couche dao
2020-07-25 10:21:47.507216, Thread-2 : [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-25 10:21:47.507216, Thread-3 : [index_controller] client [28c38df998f67685b3a482b8], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508442, Thread-4 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508940, Thread-3 : [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-25 10:21:47.510506, Thread-4 : [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}}}
2020-07-25 10:21:47.511513, Thread-5 : [index_controller] client [a06e8fd70a44c9e311f4dce0], données fiscales prises dans la couche dao
2020-07-25 10:21:47.514939, Thread-5 : [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-25 10:21:47.520727, Thread-7 : [index_controller] client [38499b63076516c02f2770ec], données fiscales prises dans la couche dao
2020-07-25 10:21:47.523162, Thread-7 : [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-25 10:21:47.530835, Thread-9 : [index] requête : <Request 'http://127.0.0.1:5000/?одружений=ні&діти=2&зарплата=100000' [GET]>
2020-07-25 10:21:47.531736, Thread-9 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises en session
2020-07-25 10:21:47.531905, Thread-9 : [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-25 10:21:47.541899, Thread-10 : [index] requête : <Request 'http://127.0.0.1:5000/?одружений=так&діти=2&зарплата=30000' [GET]>
2020-07-25 10:21:47.542488, Thread-10 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:47.542488, Thread-10 : [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-25 10:21:47.553628, Thread-11 : [index] requête : <Request 'http://127.0.0.1:5000/?одружений=ні&діти=0&зарплата=200000' [GET]>
2020-07-25 10:21:47.553628, Thread-11 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.736910, Thread-8 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:47.737191, Thread-8 : [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-25 10:21:47.748226, Thread-12 : [index] requête : <Request 'http://127.0.0.1:5000/?одружений=так&діти=5&зарплата=100000' [GET]>
2020-07-25 10:21:47.748226, Thread-12 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:48.554695, Thread-11 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:48.555070, Thread-11 : [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-25 10:21:48.748753, Thread-12 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:48.748753, Thread-12 : [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}}}
  • клієнт [fa3c83b82761c83e13217967] вперше з’являється у рядку 14: для розрахунку податку серверу довелося отримати з бази даних податкової адміністрації;
  • далі у рядку 36 знову зустрічається клієнт [fa3c83b82761c83e13217967]. Цього разу сервер знаходить дані податкової адміністрації в поточній сесії, що дозволяє йому уникнути, можливо, дорогого доступу до рівня [dao];
  • клієнт [fa3c83b82761c83e13217967] зустрічається втретє у рядку 42, де сервер знову використовує сесію цього клієнта;

Цей приклад добре ілюструє користь сесії для клієнта: туди поміщаються дані, спільні для всіх запитів цього клієнта, отримання яких є ресурсоємним.

З боку клієнта результати у файлі [data/output/résultats.json] такі самі, як і для попередніх версій.

25.4. Тестування рівня [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_factory = config["layers"]["dao_factory"]
    # створюється екземпляр шару [dao]
    dao = dao_factory.new_instance()

    # виконуємо тестові методи
    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/03/tests/TestHttpClientDao.py
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 3.392s

OK

Process finished with exit code 0