33. Практичне завдання: версія 13
Версія 13 відрізняється від версії 12 у таких аспектах:
- переструктуруються деякі частини коду (рефакторинг);
- сесія обробляється інакше за допомогою модуля [flask_session];
- у файлі конфігурації використовуються зашифровані паролі;
Версію 13 спочатку отримують шляхом копіювання версії 12:


- у [1] конфігурація буде рефакторована. Зокрема, її вилучають із папки [flask];
- у [2] головний скрипт буде рефакторований. Його також вилучають із папки [flask];
- у [3] конфігурація буде розділена на кілька файлів;
- у [4]: ми спростимо головний скрипт [main], перемістивши частину коду в інші файли;
- у [5] контролер автентифікації буде змінено, оскільки відтепер паролі користувачів будуть зашифровані;
- у [6] головний контролер інтегрує код, який раніше містився в головному скрипті [main];
33.1. Рефакторизація конфігурації додатка

З’являються три нові файли конфігурації:
- [mvc]: для налаштування архітектури MVC додатка;
- [parameters]: для об’єднання всіх констант додатка;
- [syspath]: для налаштування Python Path додатка;
Файл [syspath.py] має такий вигляд:
def configure(config: dict) -> dict:
import os
# папка цього файлу
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",
# Логер, SendAdminMail
f"{root_dir}/impots/http-servers/02/utilities",
# папка головного скрипта
script_dir,
# конфігурації [database, layers, parameters, controllers, views]
f"{script_dir}/../configs",
# контролери
f"{script_dir}/../controllers",
# відповіді HTTP
f"{script_dir}/../responses",
# шаблони переглядів
f"{script_dir}/../models_for_views",
]
# встановлюємо syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# виконується конфігурація
return {
"root_dir": root_dir,
"script_dir": script_dir
}
- скрипт [syspath] служить для налаштування Python Path додатка (рядки 40–41);
- він надає дві корисні інформації для інших скриптів налаштування (рядки 45–46);
Скрипт [mvc] налаштовує архітектуру MVC веб-додатку jSON / XML / HTML:
def configure(config: dict) -> dict:
# конфігурація додатка MVC
# контролери
from AfficherCalculImpotController import AfficherCalculImpotController
from AuthentifierUtilisateurController import AuthentifierUtilisateurController
from CalculerImpotController import CalculerImpotController
from CalculerImpotsController import CalculerImpotsController
from FinSessionController import FinSessionController
from GetAdminDataController import GetAdminDataController
from InitSessionController import InitSessionController
from ListerSimulationsController import ListerSimulationsController
from MainController import MainController
from SupprimerSimulationController import SupprimerSimulationController
# відповіді HTTP
from HtmlResponse import HtmlResponse
from JsonResponse import JsonResponse
from XmlResponse import XmlResponse
# шаблони переглядів
from ModelForAuthentificationView import ModelForAuthentificationView
from ModelForCalculImpotView import ModelForCalculImpotView
from ModelForErreursView import ModelForErreursView
from ModelForListeSimulationsView import ModelForListeSimulationsView
# дозволені дії та їхні контролери
controllers = {
# ініціалізація обчислювальної сесії
"init-session": InitSessionController(),
# аутентифікація користувача
"authentifier-utilisateur": AuthentifierUtilisateurController(),
# розрахунок податку в індивідуальному режимі
"calculer-impot": CalculerImpotController(),
# розрахунок податку в пакетному режимі
"calculer-impots": CalculerImpotsController(),
# перелік моделювань
"lister-simulations": ListerSimulationsController(),
# видалення моделювання
"supprimer-simulation": SupprimerSimulationController(),
# завершення сеансу розрахунку
"fin-session": FinSessionController(),
# відображення вікна розрахунку податку
"afficher-calcul-impot": AfficherCalculImpotController(),
# отримання даних від податкової служби
"get-admindata": GetAdminDataController(),
# головний контролер
"main-controller": MainController()
}
# різні типи відповідей (json, xml, html)
responses = {
"json": JsonResponse(),
"html": HtmlResponse(),
"xml": XmlResponse()
}
# представлення HTML та їхні шаблони залежать від стану, що повертається контролером
views = [
{
# вигляд автентифікації
"états": [
700, # /init-session — успішно
201, # /authentifier-utilisateur — невдача
],
"view_name": "views/vue-authentification.html",
"model_for_view": ModelForAuthentificationView()
},
{
# сторінка розрахунку податку
"états": [
200, # /автентифікація-користувача успішно
300, # /розрахувати-податок успішно
301, # /розрахувати-податок невдача
800, # /перегляд-розрахунку-податку успішно
],
"view_name": "views/vue-calcul-impot.html",
"model_for_view": ModelForCalculImpotView()
},
{
# перегляд списку моделювань
"états": [
500, # /перелік-симуляцій успішно
600, # /видалити-симуляцію успішно
],
"view_name": "views/vue-liste-simulations.html",
"model_for_view": ModelForListeSimulationsView()
},
]
# перегляд несподіваних помилок
view_erreurs = {
"view_name": "views/vue-erreurs.html",
"model_for_view": ModelForErreursView()
}
# перенаправлення
redirections = [
{
"états": [
400, # /завершення-сесії успішно
],
# перенаправлення на URL
"to": "/init-session/html",
}
]
# віддаємо конфігурацію MVC
return {
# контролери
"controllers": controllers,
# відповіді HTTP
"responses": responses,
# перегляди та шаблони
"views": views,
# список перенаправлень
"redirections": redirections,
# перегляд несподіваних помилок
"view_erreurs": view_erreurs
}
- рядки 1–101: цей код відомий;
- рядки 105–116: відтворюється конфігурація MVC додатка;
Скрипт [parameters] збирає константи додатка:
def configure(config: dict) -> dict:
# налаштування програми
# script_dir
script_dir = config['syspath']['script_dir']
# конфігурація додатка
parameters = {
# користувачі, яким дозволено користуватися додатком
"users": [
{
"login": "admin",
"password": "$pbkdf2-sha256$29000$mPM.h3COkTIGYOzde68VIg$7LH5Q7rN/1hW.Xa.6rcmR6h52PntvVqd5.na7EtgQNw"
}
],
# файл журналу
"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,
# сервер Redis
"redis": {
"host": "127.0.0.1",
"port": 6379
},
}
# задаємо параметри додатка
return parameters
- рядок 13: відтепер паролі користувачів будуть шифруватися;
- рядки 34–38: конфігурація сервера [Redis], до якої ми ще повернемося;
З цими новими конфігураційними файлами скрипт [config] набуває такого вигляду:
def configure(config: dict) -> dict:
# конфігурація syspath
import syspath
config['syspath'] = syspath.configure(config)
# параметри додатка
import parameters
config['parameters'] = parameters.configure(config)
# конфігурація бази даних
import database
config["database"] = database.configure(config)
# інстанціювання рівнів додатка
import layers
config['layers'] = layers.configure(config)
# налаштування MVC шару [web]
import mvc
config['mvc'] = mvc.configure(config)
# повертаємо конфігурацію
return config
33.2. Рефакторизація головного скрипта [main]

Головний скрипт [main.py] лише запускає сервер:
# очікується параметр 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 SendAdminMail import SendAdminMail
from Logger import Logger
from ImpôtsError import ImpôtsError
import redis
# надсилання електронного листа адміністратору
def send_adminmail(config: dict, message: str):
# надсилається електронний лист адміністратору додатка
config_mail = config['parameters']['adminMail']
config_mail["logger"] = config['logger']
SendAdminMail.send(config_mail, message)
# перевірка файлу журналу
logger = None
erreur = False
message_erreur = None
try:
# реєстратор
logger = Logger(config['parameters']['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")
# перевірка доступності сервера Redis
redis_client = redis.Redis(host=config["parameters"]["redis"]["host"],
port=config["parameters"]["redis"]["port"])
# виконується ping-перевірка сервера Redis
try:
redis_client.ping()
except BaseException as exception:
# Redis недоступний
log = f"[serveur] Le serveur Redis n'est pas disponible : {exception}"
# консоль
print(log)
# журнал
logger.write(f"{log}\n")
# кінець
sys.exit(1)
# заносимо клієнта [redis] до конфігурації
config['redis_client'] = redis_client
# отримання даних від податкової служби
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)
# імпортуємо маршрути веб-додатку
import routes
routes.config=config
routes.execute(__name__)
- рядки 56–73: додано сервер Redis та його клієнт;
- рядки 102–104: маршрутизатори додатка винесені в окремий скрипт [routes];
33.2.1. Модулі [flask_session] та [redis]
Сервер [Redis] буде використовуватися для збереження сесій користувачів. Ми будемо використовувати модуль [flask_session] для управління цими сесіями. Цей модуль може зберігати сесії користувачів у кількох місцях. Redis є одним із них, і ми будемо використовувати саме його.
Модуль [flask_session] потрібно встановити на терміналі PyCharm:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install flask-session
Collecting flask-session
…
Для взаємодії з сервером Redis нам потрібен клієнт Redis. Його нам надасть модуль [redis], який ми також встановлюємо:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install redis
Collecting redis
…
33.2.2. Сервер Redis
Сервер Redis слугуватиме для зберігання сесій користувачів. Модуль [flask_session] працює наступним чином:
- кожен користувач має ідентифікатор сесії, і саме він надсилається клієнту — і лише він. Клієнт отримує сесійний файл cookie лише один раз, після свого першого запиту. Цей файл cookie містить ідентифікатор сесії користувача, який не змінюватиметься під час подальших запитів клієнта. Саме тому серверу не потрібно надсилати новий сесійний файл cookie;
- раніше вміст сесії надсилався клієнту. Тепер цього більше не відбуватиметься. Вміст сесії користувача зберігатиметься на сервері Redis;
У Laragon сервер Redis за замовчуванням не активований. Тому спочатку його потрібно активувати:

- у [3] увімкніть сервер [Redis];
- у [4] залиште порт [6379], який клієнти Redis використовують за замовчуванням;
Служби Laragon автоматично перезапускаються після активації Redis:

Сервер Redis можна запитувати в командному режимі. Відкриваємо термінал Laragon [6]:

- у [1] команда [redis-cli] запускає клієнт у командному режимі сервера Redis;
У липні 2019 року клієнт Redis може використовувати 172 команди для взаємодії з сервером [https://redis.io/commands#list]. Одна з них, [command count] [2], виводить це число [3].
Запис у [Redis] здійснюється за допомогою команди Redis [set attribut valeur] [4]. Потім значення можна отримати за допомогою команди [get attribut] [5].
Може знадобитися очищення пам’яті Redis. Це робиться за допомогою команди [flushdb] [6]. Далі, якщо запитати значення атрибута [titre] [7], отримаємо посилання [nil] [8], яке вказує, що атрибут не знайдено. Також можна використати команду [exists] [9-10], щоб перевірити наявність атрибута.
Щоб вийти з клієнта Redis, введіть команду [quit] [11].
Також можна використовувати веб-інтерфейс для управління ключами на сервері Redis. Для цього потрібно запустити сервер Apache в Laragon:

З’явиться такий інтерфейс:

- [1-4] — одна з сесій, збережених на сервері Redis;
33.2.3. Управління сервером Redis у головному скрипті [main]
Скрипт [main] перевіряє наявність сервера Redis наступним чином:
import redis
…
# перевіряємо доступність сервера Redis
redis_client = redis.Redis(host=config["parameters"]["redis"]["host"],
port=config["parameters"]["redis"]["port"])
# виконується ping-тест сервера Redis
try:
redis_client.ping()
except BaseException as exception:
# Redis недоступний
log = f"[serveur] Le serveur Redis n'est pas disponible : {exception}"
# консоль
print(log)
# журнал
logger.write(f"{log}\n")
# кінець
sys.exit(1)
# заносимо клієнта [redis] у конфігурацію
config['redis_client'] = redis_client
- рядок 4: конструктор класу [redis.Redis] створює клієнт Redis-сервера. Його параметри (адреса, порт) знаходяться у скрипті [parameters];
- рядок 8: метод [ping] дозволяє перевірити наявність сервера Redis;
- рядки 9–17: якщо ping не вдається, то помилка записується в журнал, а сервер зупиняється;
- рядок 20: посилання на клієнт Redis вноситься до конфігурації;
33.2.4. Управління маршрутами в головному скрипті [main]
Управління маршрутами у [main] обмежується такими рядками:
# імпортуємо маршрути веб-додатку
import routes
routes.config=config
routes.execute(__name__)
- рядок 1: маршрути були винесені в модуль [routes];
- рядок 3: маршрути потребують інформації про конфігурацію виконання;
- рядок 4: запускається додаток Flask із передачею йому імені виконуваного скрипта (__main__);
Скрипт маршрутів має такий вигляд:
# залежності
import os
from flask import Flask, redirect, request, session, url_for
from flask_api import status
from flask_session import Session
# додаток Flask
app = Flask(__name__, template_folder="../flask/templates", static_folder="../flask/static")
# конфігурація додатка
config = {}
# фронт-контролер
def front_controller() -> tuple:
# запит перенаправляється до головного контролера
main_controller = config['mvc']['controllers']['main-controller']
return main_controller.execute(request, session, config)
@app.route('/', methods=['GET'])
def index() -> tuple:
# перенаправлення на /init-session/html
return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)
# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
# аутентифікація користувача
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
# розрахувати-податок
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
# пакетний розрахунок податку
@app.route('/calculer-impots', methods=['POST'])
def calculer_impots():
# виконується контролер, пов'язаний з цією дією
return front_controller()
# перелік-моделювання
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
# видалити-симуляцію
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
# завершення сеансу
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
# показати-розрахунок-податку
@app.route('/afficher-calcul-impot', methods=['GET'])
def afficher_calcul_impot() -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
# отримати-дані-адміністратора
@app.route('/get-admindata', methods=['GET'])
def get_admindata() -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
def execute(name: str):
# секретний ключ сеансу
app.secret_key = os.urandom(12).hex()
# Flask-Session
app.config.update(SESSION_TYPE='redis',
SESSION_REDIS=config['redis_client'])
Session(app)
# випадок, коли додаток Flask запускається через консольний скрипт
if name == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run(threaded=True)
- рядок 9: створюється екземпляр додатка Flask;
- рядок 12: конфігурація додатка ще не відома на момент написання скрипта. Вона стає відомою лише під час його виконання;
- рядки 20–77: маршрути додатка, як вони були визначені в попередній версії. Вони не змінюються;
- рядки 14–18: усі маршрути просто викликають функцію [front_controller]. Ми видалили з неї початковий код. Тепер вона просто викликає головний контролер веб-додатка;
- рядки 79–89: [execute] — це функція, яку викликає скрипт [main] для запуску веб-додатка;
- рядок 81: модуль [flask_session] використовує секретний ключ Flask;
- рядки 82–84: налаштування модуля [flask_session]. Воно полягає у додаванні ключів [SESSION_TYPE, SESSION_REDIS] до конфігурації [app.config] веб-додатку Flask [app]:
- [SESSION_TYPE]: тип сеансу. Існує кілька таких типів. Тип [redis] вказує на те, що [flask_session] використовує сервер [redis] для збереження сеансів користувачів. Через це необхідно визначити ключ [SESSION_REDIS], який має бути посиланням на клієнт Redis;
- рядок 85: сесія [Flask-Session] пов’язана з додатком Flask;
- рядки 86–89: якщо параметр [name] у рядку 79 є рядком [__main__], то запускається додаток Flask;
33.3. Рефакторизація головного контролера
Код, який раніше містився у функції [front_controller] скрипта [main], було переміщено до головного контролера:

# імпорт залежностей
import threading
import time
from random import randint
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
from Logger import Logger
from SendAdminMail import SendAdminMail
def send_adminmail(config: dict, message: str):
# надсилання електронного листа адміністратору додатка
config_mail = config['parameters']['adminMail']
config_mail["logger"] = config['logger']
SendAdminMail.send(config_mail, message)
# головний контролер додатка
class MainController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# обробка запиту
logger = None
action = None
type_response1 = None
try:
# отримуються елементи шляху
params = request.path.split('/')
# дія є першим елементом
action = params[1]
# початково помилок немає
erreur = False
# тип сесії має бути відомий перед виконанням певних дій
type_response1 = session.get('typeResponse')
if type_response1 is None and action != "init-session":
# помилка зафіксована
résultat = {"action": action, "état": 101,
"réponse": ["pas de session en cours. Commencer par action [init-session]"]}
erreur = True
# заносити в журнал
logger = Logger(config['parameters']['logsFilename'])
# зберігаємо її у конфігурації, пов’язаній із потоком
thread_config = {"logger": logger}
thread_name = threading.current_thread().name
config[thread_name] = {"config": thread_config}
# заносимо запит у журнал
logger.write(f"[MainController] requête : {request}\n")
# потоку припиняють роботу, якщо це було запропоновано
sleep_time = config['parameters']['sleep_time']
if sleep_time != 0:
# пауза є випадковою, щоб одні потоки були перервані, а інші — ні
aléa = randint(0, 1)
if aléa == 1:
# запис у журнал перед паузою
logger.write(f"[front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
# пауза
time.sleep(sleep_time)
# для виконання деяких дій потрібно пройти автентифікацію
user = session.get('user')
if not erreur and user is None and action not in ["init-session", "authentifier-utilisateur"]:
# помічено помилку
résultat = {"action": action, "état": 101,
"réponse": [f"action [{action}] demandée par utilisateur non authentifié"]}
erreur = True
# чи є помилки?
if erreur:
# запит недійсний
status_code = status.HTTP_400_BAD_REQUEST
else:
# виконується контролер, пов'язаний з цією дією
controller = config['mvc']['controllers'][action]
résultat, status_code = controller.execute(request, session, config)
except BaseException as exception:
# інші (несподівані) винятки
résultat = {"action": action, "état": 131, "réponse": [f"{exception}"]}
status_code = status.HTTP_400_BAD_REQUEST
finally:
pass
# результат, надісланий клієнту, записується в журнал
log = f"[MainController] {résultat}\n"
logger.write(log)
# чи сталася фатальна помилка?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# надсилається електронний лист адміністратору додатка
send_adminmail(config, log)
# визначається бажаний тип відповіді
type_response2 = session.get('typeResponse')
if type_response2 is None and type_response1 is None:
# тип сеансу ще не встановлено — це буде jSON
type_response = 'json'
elif type_response2 is not None:
# тип відповіді відомий і використовується в сесії
type_response = type_response2
else:
# інакше продовжуємо використовувати type_response1
type_response = type_response1
# формується відповідь для відправлення
response_builder = config['mvc']['responses'][type_response]
response, status_code = response_builder \
.build_http_response(request, session, config, status_code, résultat)
# закриваємо файл журналу, якщо він був відкритий
if logger:
logger.close()
# відправляється відповідь HTTP
return response, status_code
Весь цей код ми вже бачили в той чи інший момент.
33.4. Обробка зашифрованих паролів
Для управління зашифрованими паролями ми будемо використовувати модуль [passlib], який встановлюється з терміналу Pycharm:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install passlib
Collecting passlib
…
Ось приклад скрипта, що шифрує пароль, переданий йому як параметр:

Скрипт [create_hashed_password] виглядає так (https://passlib.readthedocs.io/en/stable/):
import sys
# функція шифрування
from passlib.hash import pbkdf2_sha256
# очікується пароль для шифрування
syntaxe = f"{sys.argv[0]} password"
erreur = len(sys.argv) != 2
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
else:
password = sys.argv[1]
# шифрується пароль
hash = pbkdf2_sha256.hash(password)
print(f"version cryptée de [{password}] = {hash}")
# перевірка
correct = pbkdf2_sha256.verify(password, hash)
print(correct)
- рядок 16: шифрується пароль, переданий як параметр;
- рядок 20: порівнюється пароль [password], переданий як параметр, із його зашифрованою версією [hash]. Функція [verify] шифрує пароль [password] і порівнює отриманий зашифрований рядок із [hash]. Повертає True, якщо обидва рядки збігаються;
Наведений вище скрипт дозволяє отримати зашифровану версію пароля [admin]:
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/08/passlib/create_hashed_password.py admin
version cryptée de [admin] = $pbkdf2-sha256$29000$fU9pTendO6c0ZoyR8r5Xqg$5ZXywIUnbMfN2hPnBaefiuqWjEbmAY.Lu06i4dwcnek
True
У рядку 2 ми вводимо в скрипт значення [parameters]:
"users": [
{
"login": "admin",
"password": "$pbkdf2-sha256$29000$fU9pTendO6c0ZoyR8r5Xqg$5ZXywIUnbMfN2hPnBaefiuqWjEbmAY.Lu06i4dwcnek"
}
],
Контролер автентифікації [AuthentifierUtilisateurController] змінюється наступним чином:
from passlib.handlers.pbkdf2 import pbkdf2_sha256
…
# перевіряється правильність пари (користувач, пароль)
users = config['parameters']['users']
i = 0
nbusers = len(users)
trouvé = False
while not trouvé and i < nbusers:
trouvé = user == users[i]["login"] and pbkdf2_sha256.verify(password, users[i]["password"])
i += 1
# знайдено?
if not trouvé:
…
33.5. Тестування
Окрім тестування за допомогою браузера, можна також використовувати клієнти з папки [http-servers/07], написані для версії 12. Вони повинні «працювати» також і для версії 13:

- у [1] усі три клієнти мають працювати;
- у [2] обидва тести мають працювати;