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

25.2. Веб-сервер
Структура каталогів скриптів сервера така:

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

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

Ми вже розглядали скрипт |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]:

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

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