30. Практичне завдання: версія 12
У цьому розділі ми створимо веб-додаток, що відповідає архітектурі MVC (Модель-Вигляд-Контролер). Додаток зможе видавати відповіді у трьох форматах: jSON, XML, HTML. Існує значний стрибок у складності між тим, що ми будемо робити зараз, і тим, що робили раніше. Ми будемо використовувати більшість концепцій, розглянутих до цього моменту, і детально розглянемо всі етапи, що ведуть до створення кінцевого додатка.
30.1. Архітектура MVC
Ми реалізуємо архітектурну модель, відому як MVC (Модель – Вигляд – Контролер), таким чином:
Обробка запиту клієнта відбуватиметься таким чином:
- 1 — запит
Запитувані URL матимуть вигляд http://machine:port/action/param1/param2/… [Contrôleur principal] використовуватиме файл конфігурації для «маршрутизації» запиту до відповідного контролера. Для цього він використовуватиме поле [action] з URL. Решта URL та [param1/param2/…] складається з необов’язкових параметрів, які будуть передані до дії. Значенням параметра C у MVC тут є рядок [Contrôleur principal, Contrôleur / Action]. Якщо жоден контролер не може обробити запитувану дію, веб-сервер відповість, що запитувана дія URL не знайдена.
- 2 — обробка
- обрана дія [2a] може використовувати параметри parami, які їй передала дія [Contrôleur principal]. Вони можуть походити з двох джерел:
- з шляху [/param1/param2/…], що належить URL,
- з параметрів, розміщених у тілі запиту клієнта;
- під час обробки запиту користувача дії може знадобитися рівень [métier] [2b]. Після обробки запиту клієнта ця дія може викликати різні відповіді. Типовим прикладом є:
- відповідь про помилку, якщо запит не вдалося обробити належним чином;
- у іншому випадку — підтверджувальний відповідь;
- [Contrôleur / Action] надішле свою відповідь [2c] головному контролеру разом із кодом стану. Ці коди стану однозначно відображатимуть стан, у якому перебуває додаток. Це буде або код успіху, або код помилки;
- 3 — відповідь
- залежно від того, чи клієнт запросив відповідь jSON, XML чи HTML, [Contrôleur principal] створить екземпляр відповідного типу відповіді [3a] і попросить його надіслати відповідь клієнту. [Contrôleur principal] передасть йому як відповідь, так і код стану, надані [Contrôleur / Action], який було виконано;
- якщо бажана відповідь має тип jSON або XML, вибрана відповідь відформатує відповідь від [Contrôleur / Action], яку їй передали, і надішле її [3c]. Клієнтом, здатним обробити цю відповідь, може бути консольний скрипт на Python або скрипт на JavaScript, розміщений на сторінці HTML;
- якщо бажана відповідь має тип HTML, то вибрана відповідь [3b] вибере один із переглядів HTML або [Vuei] за допомогою наданого їй коду стану. Це V з MVC. Кожному коду стану відповідає лише один вигляд. Цей вигляд V відобразить відповідь [Contrôleur / Action], який було виконано. Вона оформлює дані цієї відповіді за допомогою HTML, CSS та JavaScript. Ці дані називаються моделлю представлення. Це «M» у MVC. Клієнтом, як правило, є браузер;
Тепер уточнимо зв’язок між веб-архітектурою MVC та багаторівневою архітектурою. Залежно від того, як ми визначаємо модель, ці два поняття можуть бути пов’язані або ні. Візьмемо однорівневий веб-додаток MVC:

У наведеному вище прикладі кожен з [Contrôleur / Action] включає частину шарів [métier] та [dao]. У шарі [web] дійсно присутня архітектура MVC, але загалом додаток не має багатошарової архітектури. Тут є лише один шар — веб-шар, який виконує всі функції.
Тепер розглянемо багатошарову веб-архітектуру:

Рівень [web] можна реалізувати, не дотримуючись моделі MVC. У цьому випадку ми маємо багаторівневу архітектуру, але веб-рівень не реалізує модель MVC.
Наприклад, у середовищі .NET цей шар [web]може бути реалізований за допомогою ASP.NET та MVC, і тоді ми отримуємо багатошарову архітектуру з шаром [web] типу MVC. Зробивши це, можна замінити цей шар ASP.NET MVC на класичний шар ASP.NET (WebForms), зберігши решту (бізнес-логіка, DAO, драйвер) без змін. У результаті ми отримуємо багаторівневу архітектуру з шаром [web], який більше не належить до типу MVC.
У MVC ми зазначили, що модель M відповідає представленню V, c.a.d, тобто сукупності даних, що відображаються представленням V. Наводимо ще одне визначення моделі M для MVC:

Багато авторів вважають, що те, що знаходиться праворуч від шару [web], утворює модель M для MVC. Щоб уникнути двозначностей, можна говорити про:
- про модель домену, коли мається на увазі все, що знаходиться праворуч від шару [web];
- про модель подання, коли йдеться про дані, що відображаються поданням V;
У подальшому, коли ми говоритимемо про модель, завжди матимемо на увазі модель подання.
30.2. Архітектура клієнт-серверного додатка
Веб-додаток матиме таку архітектуру:
- у [1] веб-сервер матиме два типи клієнтів:
- у [2] — консольний клієнт, який обмінюватиметься даними jSON та XML із сервером;
- у [3] — браузер, який отримуватиме HTML від сервера та відображатиме його;
- веб-сервер [1] зберігає рівні [métier] та [dao] з попередніх версій;
- веб-клієнт [2] буде вдосконалено з урахуванням нових URL сервісів веб-додатку;
- додаток HTML, що відображається у браузері, потрібно повністю переписати;
Ми будемо розробляти додаток у кілька етапів:
- ми розробимо версію сервера jSON. Ми будемо по черзі тестувати сервісні URL сервера за допомогою клієнта Postman. Цей метод дозволяє нам побудувати каркас веб-сервера, не переймаючись видами (=HTML) додатка;
- після тестування сервера jSON за допомогою Postman ми протестуємо його за допомогою консольного клієнта;
- потім перейдемо до версії сервера XML. Ми побачили, що перехід від jSON до XML був тривіальним;
- нарешті ми перейдемо до версії сервера HTML. Ми побудуємо архітектуру MVC і визначимо, які подання потрібно відображати. Додаток HTML буде протестовано як за допомогою клієнта Postman, так і за допомогою звичайного браузера;
30.3. Структура коду сервера

- у [1: веб-сервер у цілому;
- у [2]: поки що ми проігноруємо папки [static, templates, tests_views], які стосуються версії сервера HTML. Поза цією папкою ми знайдемо головний скрипт [main] та його конфігурацію;
- у [3] — контролери веб-сервера. Це будуть екземпляри класів;
![]() | ![]() |
- у файлі [4] відповідь сервера HTTP оброблятиметься класами;
- у [5] ми зберігаємо файл логів попередніх серверів;
Коли ми будемо створювати версію сервера HTML, будуть задіяні інші папки:
![]() | ![]() |
- у [6] — статичні елементи додатка HTML;
- у [7] — шаблони додатка HTML, розбиті на види [9] та фрагменти видів [8];
- у [9] — класи, що реалізують моделі переглядів;
30.4. URL — службовий компонент додатка
Щоб створити веб-сервер, ми будемо діяти наступним чином:
- на основі представлень додатка HTML ми визначимо дії, які має реалізувати веб-додаток. Тут ми будемо використовувати реальні представлення, але це можуть бути й просто представлення на папері;
- на основі цих дій ми визначимо сервісні URL додатка HTML;
- ми реалізуємо ці сервісні URL за допомогою сервера, що видає jSON. Це дозволяє визначити каркас веб-сервера, не переймаючись сторінками HTML, які потрібно видавати. Ми протестуємо ці сервісні URL за допомогою Postman;
- потім протестуємо наш сервер jSON за допомогою консольного клієнта;
- як тільки сервер jSON буде перевірено, ми перейдемо до написання додатка HTML;
Першим екраном буде екран автентифікації:

- дія, яка веде до цієї першої сторінки, називатиметься [init-session] [1];
- натискання на кнопку [Valider] запустить дію [authentifier-utilisateur] з двома переданими параметрами [2-3];
Вигляд розрахунку податку:

- у [1] — дія [authentifier-utilisateur], яка привела до цього перегляду;
- у [2] натискання кнопки [Valider] запускає виконання дії [calculer-impot] з трьома переданими параметрами [2-5];
- натискання на посилання [6] запускає дію [lister-simulations] без параметрів;
- натискання на посилання [7] запускає дію [fin-session] без параметрів;
Третій вигляд — це симуляції, виконані автентифікованим користувачем:

- у [3] — дія [lister-simulations], яка привела до цього перегляду;
- у [2] клік на посилання [Supprimer] запускає дію [supprimer-simulation] з одним параметром — номером симуляції, яку потрібно видалити зі списку;
- натискання на посилання [3] запускає дію [afficher-calcul-impot] без параметрів, яка знову відображає вікно розрахунку податку;
- натискання на посилання [4] запускає дію [fin-session] без параметрів;
Маючи цю початкову інформацію, ми можемо визначити різні службові URL сервера:
Дія | Роль | Контекст виконання |
/init-session | Служить для визначення типу (json, xml, html) бажаних відповідей | Запит GET Може бути відправлений у будь-який час |
/authentifier-utilisateur | Надає або відмовляє користувачеві у вході | Запит POST. Запит повинен містити два параметри, надіслані методом POST [user, password] Може бути відправлений лише у тому випадку, якщо тип сесії (json, xml, html) відомий |
/calculer-impot | Виконує симуляцію розрахунку податку | Запит POST. Запит повинен містити три параметри, що передаються методом POST: [marié, enfants, salaire] Може бути відправлений лише у разі, якщо тип сесії (json, xml, html) відомий, а користувач пройшов автентифікацію |
/lister-simulations | Запит на перегляд списку симуляцій, виконаних з початку сеансу | Запит GET. Може бути відправлений лише за умови, що тип сесії (json, xml, html) відомий, а користувач пройшов автентифікацію |
/supprimer-simulation/numéro | Видаляє симуляцію зі списку симуляцій | Запит GET. Може бути відправлений лише в тому випадку, якщо тип сесії (json, xml, html) відомий, а користувач пройшов автентифікацію |
/показати-розрахунок-податку | Відображає сторінку HTML з розрахунком податку | Запит GET. Може бути відправлений лише за умови, що тип сесії (json, xml, html) відомий, а користувач пройшов автентифікацію |
/fin-session | Завершує сесію моделювання. | Технічно старий веб-сеанс видаляється, а створюється новий Може бути відправлений лише у тому випадку, якщо тип сесії (json, xml, html) відомий, а користувач пройшов автентифікацію |
Ці різні сервісні URL використовуватимуться як для сервера HTML, так і для серверів jSON або XML. Дві URL використовуватимуться лише для цих двох останніх серверів: це URL з попередньої версії клієнта/веб-сервера, які ми тут повторюємо:
Дія | Роль | Контекст виконання |
/get-admindata | Повертає податкові дані, необхідні для розрахунку податку | Запит GET. Використовується лише у разі, якщо тип сеансу — json або xml. Користувач має бути автентифікований |
/calculer-impots | Виконує розрахунок податку для списку платників податків, переданих у запиті jSON | Запит GET. Використовується лише у разі, якщо тип сесії — json або xml. Користувач має бути автентифікований |
Усі контролери, пов’язані з цими діями, діятимуть однаково:
- вони перевірять свої параметри. Їх можна знайти в об’єкті:
- [request.path] — для параметрів, що містяться в URL у формі [/action/param1/param2/…];
- в об’єкті [request.form] — для тих, що передаються в [x-www-form-urlencoded] у тілі запиту;
- в об’єкті [request.data] — для тих, що передаються в jSON у тілі запиту;
- контролер схожий на функцію або метод, який перевіряє правильність своїх параметрів. Однак у випадку контролера все дещо складніше:
- очікувані параметри можуть бути відсутніми;
- параметри, отримані контролером, є символьними рядками. Якщо очікуваний параметр є числом, то контролер повинен перевірити, чи символьний рядок параметра дійсно відповідає числу;
- після перевірки наявності та синтаксичної правильності очікуваних параметрів необхідно переконатися, що вони є дійсними в поточному контексті виконання. Цей контекст міститься в сесії. Прикладом контексту виконання є аутентифікація. Деякі дії повинні оброблятися лише після того, як клієнт пройшов аутентифікацію. Зазвичай ключ у сесії вказує, чи відбулася ця аутентифікація чи ні;
- після виконання попередніх перевірок вторинний контролер може приступити до роботи. Ця робота з перевірки параметрів є дуже важливою. Не можна допустити, щоб клієнт надсилав нам що завгодно в будь-який момент життєвого циклу додатка. Ми повинні повністю контролювати його життєвий цикл;
- завершивши свою роботу, вторинний контролер повертає словник із ключами [action, état, réponse] головному контролеру, який його викликав:
- [action] — це дія, яка щойно була виконана;
- [état] — це трицифрове число, яке вказує на результат обробки дії:
- [x00] вказує на успішне виконання обробки;
- [x01] сигналізує про невдачу обробки;
- [réponse] — це словник результатів у форматі {‘відповідь’:об’єкт}. Структура об’єкта буде різною залежно від обробленої дії;
Тепер ми розглянемо різні контролери, або, що те саме, різні дії, які обробляють ці контролери та які визначають ритм роботи веб-додатку.
30.5. Налаштування сервера

Конфігурація бази даних [config_database], а також конфігурація рівнів сервера [config_layers] є ідентичними до тих, що були у попередніх версіях. У файлі [config] з’являється нова інформація:
def configure(config: dict) -> dict:
import os
# крок 1 ------
# папка цього файлу
script_dir = os.path.dirname(os.path.abspath(__file__))
# кореневий шлях
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# залежності
absolute_dependencies = [
# папки проєкту
# BaseEntity, MyException
f"{root_dir}/classes/02/entities",
# InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
f"{root_dir}/impots/v04/interfaces",
# AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
f"{root_dir}/impots/v04/services",
# ImpotsDaoWithAdminDataInDatabase
f"{root_dir}/impots/v05/services",
# AdminData, ImpôtsError, TaxPayer
f"{root_dir}/impots/v04/entities",
# Константи, діапазони
f"{root_dir}/impots/v05/entities",
# Logger, SendAdminMail
f"{root_dir}/impots/http-servers/02/utilities",
# скрипти [config_database, config_layers]
script_dir,
# контролери
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)
# залежності веб-сервера
# контролери
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
# етап 2 ------
# конфігурація додатка
config.update({
# користувачі, яким дозволено користуватися додатком
"users": [
{
"login": "admin",
"password": "admin"
}
],
# файл журналів
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# конфігурація сервера SMTP
"adminMail": {
# сервер SMTP
"smtp-server": "localhost",
# порт сервера SMTP
"smtp-port": "25",
# адміністратор
"from": "guest@localhost.com",
"to": "guest@localhost.com",
# тема листа
"subject": "plantage du serveur de calcul d'impôts",
# tls — «True», якщо сервер SMTP вимагає авторизації, «False» — в іншому випадку
"tls": False
},
# тривалість паузи потоку в секундах
"sleep_time": 0,
# дозволені дії та їхні контролери
"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": [
# /init-session успішно
700,
# /автентифікація-користувача — невдача
201
],
"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, # /успішне завершення сеансу
],
# перенаправлення на
"to": "/init-session/html",
}
],
}
)
# етап 3 ------
# налаштування бази даних
import config_database
config["database"] = config_database.configure(config)
# етап 4 ------
# інстанціювання шарів додатка
import config_layers
config['layers'] = config_layers.configure(config)
# зберігаємо конфігурацію
return config
- до рядка 41 містяться стандартні елементи;
- рядки 43–66: починаючи з рядка 43, визначається шлях Python Path сервера. Після цього можна імпортувати залежності проекту:
- рядки 45–55: список контролерів;
- рядки 57–60: список відповідей HTTP;
- рядки 62–66: список шаблонів переглядів;
- рядки 68–189: конфігурація додатка з набором констант;
- рядки 71–98: ці рядки нам уже знайомі з попередніх версій;
- рядки 101–122: словник контролерів:
- ключі — це назви дій;
- значення — це екземпляр контролера, який має обробляти цю дію. Кожен контролер створюється лише в одному екземплярі (синглтон). Один і той самий екземпляр виконуватиметься різними потоками сервера. Тому слід звертати увагу на спільні дані, які кожен контролер може намагатися змінити;
- рядки 125–129: словник трьох можливих відповідей HTTP:
- ключі — це тип відповіді, який бажає отримати клієнт (jSON, xml, html);
- значення — це екземпляр відповіді HTTP. Кожен генератор відповіді створюється лише в одному екземплярі (синглтон). Один і той самий генератор буде виконуватися різними потоками сервера. Тому слід звернути увагу на спільні дані, які кожен генератор може захотіти змінити;
- рядки 132–186: конфігурація представлень HTML. Наразі ці рядки ігноруються;
- рядки 191–202: ми вже зустрічали ці рядки в попередніх версіях;
30.6. Шлях клієнтського запиту всередині сервера

Ми простежимо шлях клієнтського запиту, що надходить на сервер, аж до відповіді HTTP, яка надсилається у відповідь. Він проходить через сервер MVC.
30.6.1. Скрипт [main]

Скрипт [main] у багатьох аспектах ідентичний скрипту попередніх версій. Проте ми наводимо його повністю, щоб закласти міцну основу:
# очікуємо параметр mysql або pgres
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
sgbd = sys.argv[1].lower()
erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# налаштування додатка
import config
config = config.configure({'sgbd': sgbd})
# залежності
from flask import request, Flask, session, url_for, redirect
from flask_api import status
from SendAdminMail import SendAdminMail
from myutils import json_response
from Logger import Logger
import threading
import time
from random import randint
from ImpôtsError import ImpôtsError
import os
# надсилання електронного листа адміністратору
def send_adminmail(config: dict, message: str):
# надсилається електронний лист адміністратору програми
config_mail = config["adminMail"]
config_mail["logger"] = config['logger']
SendAdminMail.send(config_mail, message)
# перевірка файлу журналу
logger = None
erreur = False
message_erreur = None
try:
# реєстратор
logger = Logger(config["logsFilename"])
except BaseException as exception:
# журнал консолі
print(f"L'erreur suivante s'est produite : {exception}")
# записування помилки
erreur = True
message_erreur = f"{exception}"
# запис логгера в конфігурацію
config['logger'] = logger
# обробка помилки
if erreur:
# лист адміністратору
send_adminmail(config, message_erreur)
# завершення роботи програми
sys.exit(1)
# журнал запуску
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)
# отримання даних від податкової служби
erreur = False
try:
# admindata буде даними на рівні додатка, доступними лише для читання
config["admindata"] = config["layers"]["dao"].get_admindata().asdict()
# журнал успішного виконання
logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
# зафіксовано помилку
erreur = True
# журнал помилок
log = f"L'erreur suivante s'est produite : {ex}"
# консоль
print(log)
# файл журналів
logger.write(f"{log}\n")
# лист адміністратору
send_adminmail(config, log)
# головний потік більше не потребує логгера
logger.close()
# якщо сталася помилка, процес зупиняється
if erreur:
sys.exit(2)
# додаток Flask
app = Flask(__name__, template_folder="templates", static_folder="static")
# секретний ключ сеансу
app.secret_key = os.urandom(12).hex()
# фронт-контролер
def front_controller() -> tuple:
# запит обробляється
logger = None
…
@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()
# lister-simulations
@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/<int:numero>', methods=['GET'])
def get_admindata() -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
# тільки main
if __name__ == '__main__':
# запускається сервер
app.config.update(ENV="development", DEBUG=True)
app.run(threaded=True)
- рядки 1–92: усі ці рядки ми вже розглядали та пояснювали;
- рядок 92: сервер буде керувати сесією. Тому нам потрібен секретний ключ. Для кожного користувача ми будемо зберігати у сесії дві відомості:
- чи правильно користувач пройшов аутентифікацію;
- щоразу, коли він буде розраховувати податок, результати цього розрахунку будуть заноситися до списку, який ми назвемо списком симуляцій користувача. Цей список буде зберігатися в сесії;
- рядки 100–151: список службових URL сервера. Відповідні функції слугують фільтром: усі URL, яких немає в цьому списку, будуть відхилені сервером Flask із помилкою [404 NOT FOUND]. Після проходження цього фільтрування запит систематично передається до «Front Controller», реалізованого функцією [front_controller] у рядках 94–98, яку ми незабаром розглянемо;
- рядки 100–103: обробка маршруту [/]. Точкою входу веб-додатку буде URL у рядку 107. Також у рядку 103 ми перенаправляємо клієнта до цього URL:
- функція [url_for] імпортується у рядку 18. Вона має тут два параметри:
- перший параметр — це ім’я однієї з функцій маршрутизації, у даному випадку функції з рядка 107. Бачимо, що ця функція очікує на параметр [type_response], який визначає тип (json, xml, html) відповіді, що її бажає отримати клієнт;
- другий параметр повторює ім’я параметра з рядка 107, [type_response], і присвоює йому значення. Якби були інші параметри, цю операцію повторювали б для кожного з них;
- вона повертає URL, пов’язаний із функцією, визначеною двома параметрами, що були їй надані. У цьому випадку це дасть URL із рядка 106, де параметр замінюється на його значення [/init-session/html];
- функція [redirect] була імпортована у рядку 18. Її завданням є надсилання клієнту заголовка перенаправлення HTTP:
- перший параметр — це URL, на який має бути перенаправлено клієнта;
- другий параметр — це код статусу відповіді HTTP, наданої клієнту. Код [status.HTTP_302_FOUND] відповідає перенаправленню HTTP;
Функція [front_controller] у рядках 94–98 виконує початкову обробку запиту клієнта:
# фронт-контролер
def front_controller() -> tuple:
# обробляється запит
logger = None
try:
# реєстратор
logger = Logger(config["logsFilename"])
# записується у конфігурацію, пов’язану з потоком
thread_config = {"logger": logger}
thread_name = threading.current_thread().name
config[thread_name] = {"config": thread_config}
# запис запиту в журнал
logger.write(f"[ front_controller] requête : {request}\n")
# потоку припиняється, якщо це було запропоновано
sleep_time = config["sleep_time"]
if sleep_time != 0:
# пауза є випадковою, щоб одні потоки були перервані, а інші — ні
aléa = randint(0, 1)
if aléa == 1:
# запис у журнал перед паузою
logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
# пауза
time.sleep(sleep_time)
# запит перенаправляється до головного контролера
main_controller = config['controllers']["main-controller"]
résultat, status_code = main_controller.execute(request, session, config)
# результат, надісланий клієнту, записується в журнал
log = f"[front_controller] {résultat}\n"
logger.write(log)
# чи сталася фатальна помилка?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# надсилається електронний лист адміністратору додатка
send_adminmail(config, log)
# визначається бажаний тип відповіді
if session.get('typeResponse') is None:
# тип сеансу ще не встановлено — це буде jSON
type_response = 'json'
else:
type_response = session['typeResponse']
# формується відповідь для відправлення
response_builder = config["responses"][type_response]
response, status_code = response_builder \
.build_http_response(request, session, config, status_code, résultat)
# відправляється відповідь
return response, status_code
except BaseException as erreur:
# сталася несподівана помилка — якщо можливо, помилку реєструють у журналі
if logger:
logger.write(f"[ front_controller] {erreur}")
# готується відповідь клієнту
résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
# відправляється відповідь у форматі jSON
return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
finally:
# закрити файл журналу, якщо він був відкритий
if logger:
logger.close()
- рядки 1–57: цей код нам відомий. Наприклад, це був код функції з назвою [main] у скрипті [main] попередньої версії. Варто зазначити лише одне — контролер, що використовується в рядках 25–26:
- рядок 25: з конфігурації отримуємо екземпляр контролера, пов’язаний з іменем [main-controller]. Йдеться про такі рядки:
# залежності веб-сервера
# контролери
…
from MainController import MainController
# дозволені дії та їхні контролери
"controllers": {
…,
# головний контролер
"main-controller": MainController()
},
- (продовження)
- у рядку 10 вище можна помітити, що отримується екземпляр класу;
- рядок 26: контролер [MainController] отримує запит на обробку;
- рядки 30–45: відповідь, повернута контролером [MainController], надсилається клієнту. Ми повернемося до цих рядків трохи пізніше;
Завдання функції [front_controller], а потім класу [MainController] полягає у виконанні операцій, спільних для всіх запитів:
На схемі вище ми все ще перебуваємо на етапі 1 обробки запиту. Головний контролер [MainController] продовжить виконання етапу 1.
30.6.2. Головний контролер [MainController]
Головний контролер [MainController] продовжує роботу, розпочату функцією [front_controller]:
Усі контролери реалізують наступний інтерфейс [InterfaceController] [2]:

from abc import ABC, abstractmethod
from werkzeug.local import LocalProxy
class InterfaceController(ABC):
@abstractmethod
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
pass
- інтерфейс [InterfaceController] визначає лише єдиний метод [execute] у рядку 8. Цей метод приймає три параметри:
- [request]: запит клієнта;
- [session]: сесія клієнта;
- [config]: конфігурація додатка;
Метод [execute] повертає кортеж із двох елементів:
- перший — це словник результатів у форматі {‘action’: action, ‘état’:état, ‘réponse’:résultats};
- другий — код статусу HTTP, який потрібно повернути клієнту;
Головний контролер [MainController] [1] реалізує інтерфейс [InterfaceController] наступним чином:
# імпорт залежностей
from flask_api import status
from werkzeug.local import LocalProxy
# контролери веб-додатку
from InterfaceController import InterfaceController
class MainController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# отримання елементів шляху
params = request.path.split('/')
action = params[1]
# помилки
erreur = False
# тип сесії має бути відомий перед виконанням певних дій
type_response = session.get('typeResponse')
if type_response 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
# для виконання певних дій необхідно пройти автентифікацію
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:
# повертається повідомлення про помилку
return résultat, status.HTTP_400_BAD_REQUEST
else:
# виконується контролер, пов'язаний з цією дією
controller = config["controllers"][action]
résultat, status_code = controller.execute(request, session, config)
return résultat, status_code
Контролер [MainController] виконує первинну перевірку правильності запиту.
- рядки 11–13: контролер спочатку отримує дію, яку запитує клієнт. Нагадаємо, що сервісні URL мають вигляд [/action/param1/param2/…], а цей URL міститься в [request.path];
- рядки 17–23: дія [init-session] слугує для ініціалізації типу відповіді (json, xml, html), якого бажає клієнт. Ця інформація зберігається в сесії, пов’язаній із ключем [typeRéponse]. Отже, якщо дія не є [init-session], то сесія повинна містити ключ [typeRéponse], інакше запит є помилковим;
- рядки 21–22: структура результату, що повертається кожним контролером, у даному випадку — результат помилки:
- [action]: це назва поточної дії. Це дозволить отримати її назву під час запису результату запиту в журнал;
- [état] — це тризначний код стану:
- [x00] — успіх;
- [x01] — у разі невдачі;
- [réponse] — це відповідь на запит. Її характер залежить від конкретного запиту;
- рядки 24–30: дія [authentifier-utilisateur] слугує для автентифікації користувача. У разі успіху ключ [user=True] додається до сеансу користувача. Деякі сервісні дії URL доступні лише для автентифікованого користувача. Саме це тут і перевіряється;
- рядок 26: лише дії [init-session] та [authentifier-utilisateur] можуть виконуватися користувачем, який ще не пройшов автентифікацію;
- рядки 28–29: результат, який слід надіслати у разі помилки;
- рядки 32–34: якщо сталася одна з двох попередніх помилок, то клієнту надсилається відповідь про помилку зі статусом HTTP 400 BAD REQUEST;
- рядки 35–39: якщо помилки не сталося, то управління передається контролеру, відповідальному за обробку поточної дії. Його екземпляр знайдено в конфігурації додатка;
Клас [MainController] продовжує роботу функції [front_controller]: разом вони об’єднують усе, що можна винести в окремий блок при обробці запитів, і лише в останній момент передають запит конкретному контролеру. Розподіл коду між функцією [front_controller] та класом [MainController] є цілком суб’єктивним. Тут я хотів зберегти надбання попередньої версії: функція [front_controller] вже існувала під назвою [main]. На практиці можна було б:
- помістити все у функцію [front_controller] і видалити клас [MainController];
- перенести все до класу [MainController] і видалити функцію [front_controller]. Я б скоріше обрав саме це рішення, оскільки воно має ту перевагу, що полегшує код головного скрипта [main];
30.7. Обробка, пов’язана з конкретною дією
Повернемося до архітектури MVC додатка:

Ми все ще перебуваємо на етапі 1, описаному вище. Якщо помилок не сталося, розпочнеться етап 2. Запит було передано контролеру, що відповідає дії, яку вимагає запит. Припустимо, що ця дія — [/init-session], визначена маршрутом:
# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
Ця дія пов’язана з контролером у конфігурації [config]:
# дозволені дії та відповідні контролери
"controllers": {
# ініціалізація обчислювальної сесії
"init-session": InitSessionController(),
…
},
Отже, контрольер [InitSessionController] (рядок 4) бере на себе управління. Його код такий:
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class InitSessionController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# отримання елементів шляху
dummy, action, type_response = request.path.split('/')
# на початку помилок немає
erreur = False
# перевірка типу відповіді
if type_response not in config['responses'].keys():
erreur = True
résultat = {"action": action, "état": 701,
"réponse": [f"paramètre [type={type_response}] invalide"]}
# якщо помилок немає
if not erreur:
# заносимо тип сесії до сесії Flask
session['typeResponse'] = type_response
résultat = {"action": action, "état": 700,
"réponse": [f"session démarrée avec le type de réponse {type_response}"]}
return résultat, status.HTTP_200_OK
else:
return résultat, status.HTTP_400_BAD_REQUEST
- рядок 6: як і інші контролери, контролер [InitSessionController] реалізує інтерфейс [InterfaceController];
- рядок 10: контролер URL належить до типу [/init-session/type_response]. Отримуємо дію [init-session] та бажаний тип відповіді;
- рядок 15: бажаний тип відповіді може бути лише одним із тих, що присутні в конфігурації відповідей:
# різні типи відповіді (json, xml, html)
"responses": {
"json": JsonResponse(),
"html": HtmlResponse(),
"xml": XmlResponse()
},
- якщо це не так, готується відповідь про помилку 701 (рядок 17);
- рядки 20–25: випадок, коли бажаний тип відповіді є дійсним;
- рядок 22: бажаний тип відповіді зберігається в сесії. Адже його потрібно буде запам’ятати для наступних запитів;
- рядки 23–24: готується відповідь про успішне виконання 700;
- рядок 25: відповідь про успішне виконання повертається коду, що викликає;
- рядок 27: якщо сталася помилка, код помилки повертається викликуючому коду;
30.8. Формування відповіді HTTP від сервера
Повернемося до архітектури MVC додатка:

Ми щойно розглянули етапи 1 і 2. Ми зустріли три коди стану:
- 700: /init-session виконано успішно;
- 701: /init-session завершився невдало;
- 101: запит недійсний, оскільки сесія не була ініціалізована або користувач не пройшов автентифікацію;
Давайте розглянемо, як відповідь сервера буде надіслана клієнту на етапі 3, описаному вище. Це відбувається у функції [front_controller] скрипта [main]:
# фронт-контролер
def front_controller() -> tuple:
# обробка запиту
logger = None
try:
# реєстратор
logger = Logger(config["logsFilename"])
# зберігання у конфігурації, пов'язаній із потоком
thread_config = {"logger": logger}
thread_name = threading.current_thread().name
config[thread_name] = {"config": thread_config}
# запис запиту в журнал
logger.write(f"[ front_controller] requête : {request}\n")
# потоку припиняється, якщо це було запропоновано
sleep_time = config["sleep_time"]
if sleep_time != 0:
# пауза є випадковою, щоб одні потоки були перервані, а інші — ні
aléa = randint(0, 1)
if aléa == 1:
# запис у журнал перед паузою
logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
# пауза
time.sleep(sleep_time)
# запит перенаправляється до головного контролера
main_controller = config['controllers']["main-controller"]
résultat, status_code = main_controller.execute(request, session, config)
# результат, надісланий клієнту, записується в журнал
log = f"[front_controller] {résultat}\n"
logger.write(log)
# чи сталася фатальна помилка?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# надсилається електронний лист адміністратору додатка
send_adminmail(config, log)
# визначається бажаний тип відповіді
if session.get('typeResponse') is None:
# тип сеансу ще не встановлено — це буде jSON
type_response = 'json'
else:
type_response = session['typeResponse']
# формується відповідь для відправлення
response_builder = config["responses"][type_response]
response, status_code = response_builder \
.build_http_response(request, session, config, status_code, résultat)
# відправляється відповідь
return response, status_code
except BaseException as erreur:
# це несподівана помилка — якщо можливо, помилку реєструють у журналі
if logger:
logger.write(f"[ front_controller] {erreur}")
# готується відповідь для клієнта
résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
# відправляється відповідь у форматі jSON
return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
finally:
# закривається файл журналу, якщо він був відкритий
if logger:
logger.close()
- ми знаходимося на рядку 26: головний контролер повернув свою відповідь про помилку;
- рядки 27–29: незалежно від відповіді головного контролера (успіх чи невдача) ця відповідь записується у файл журналу;
- рядки 30–33: як і в попередніх версіях, якщо статус HTTP дорівнює [500 INTERNAL SERVER ERROR], адміністратору додатка надсилається електронний лист із журналом помилки;
- рядки 34–39: ми надішлемо відповідь HTTP, а результат, повернутий контролером, буде розміщений у тілі цієї відповіді. Нам потрібно знати, у якому форматі (json, xml, html) клієнт бажає отримати цю відповідь. Шукаємо бажаний тип відповіді в сесії. Якщо його там немає, то довільно встановлюємо цей тип як jSON;
- рядки 40–43: формується відповідь HTTP;
У файлі конфігурації кожен тип відповіді (json, xml, html) пов’язано з екземпляром класу:
# різні типи відповідей (json, xml, html)
"responses": {
"json": JsonResponse(),
"html": HtmlResponse(),
"xml": XmlResponse()
},
Класи відповідей знаходяться у папці [responses] у дереві каталогів сервера:

Кожен клас відповіді реалізує наступний інтерфейс [InterfaceResponse]:
from abc import ABC, abstractmethod
from flask.wrappers import Response
from werkzeug.local import LocalProxy
class InterfaceResponse(ABC):
@abstractmethod
def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
résultat: dict) -> (Response, int):
pass
- рядки 8–11: інтерфейс [InterfaceResponse] визначає єдиний метод [build_http_response] із такими параметрами:
- [request, session, config]: це параметри, які отримує контролер дії;
- [résultat, status_code] — це результати, що генеруються контролером дії;
Ми розглянемо відповідь jSON. Вона генерується наступним класом [JsonResponse]:
import json
from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy
from InterfaceResponse import InterfaceResponse
class JsonResponse(InterfaceResponse):
def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
résultat: dict) -> (Response, int):
# результати: словник результатів
# status_code: код статусу відповіді HTTP
# повертається відповідь HTTP
response = make_response(json.dumps(résultat, ensure_ascii=False))
response.headers['Content-Type'] = 'application/json; charset=utf-8'
return response, status_code
Ми знаємо цей код, з яким стикалися вже багато разів. Це код функції [json_response] з модуля [myutils].
30.9. Перші тести
У досліджуваному коді ми зустріли три коди стану:
- 700: /init-session завершився успішно;
- 701: /init-session завершилася невдало;
- 101: запит недійсний, оскільки сесія не була ініціалізована або користувач не пройшов автентифікацію;
Спробуємо отримати їх, використовуючи сесію jSON.
- запускаємо веб-сервер, SGBD, поштовий сервер;
- запускаємо клієнт Postman;
Тест 1
Спочатку продемонструємо недійсний запит, оскільки сесія не була ініціалізована:

- [1-2]: запит [POST http://localhost:5000/authentifier-utilisateur] є дійсним маршрутом:
# аутентифікація користувача
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
але вона приймається лише в тому випадку, якщо сесія була попередньо ініціалізована за допомогою дії [/init-session].
Виконаємо запит і подивимося на результат, надісланий сервером:

- [1-2]: отримано відповідь jSON. Коли тип відповіді ще не визначений клієнтом, сервер використовує jSON для відповіді;
- [3-5]: словник jSON у відповіді;
- [action]: дія, яка була виконана;
- [état]: код стану відповіді. Код [x01] вказує на помилку;
- [réponse]: відповідає кожній дії. У цьому випадку він містить повідомлення про помилку;
Тепер ініціюємо сесію з неправильним типом відповіді:

- [1-2] — це правильний маршрут:
# ініціалізація сесії
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
Отже, вона потрапить у тунель обробки запитів сервера MVC. Проте під час цієї обробки вона має бути відхилена, оскільки тип сеансу, що запитується, є неправильним.
Відповідь така:

- у [4] — код помилки [x01];
- у [5] — пояснення помилки;
Тепер ініціюємо сесію jSON:

Відповідь така:

Тепер ініціюємо сесію XML. Відповідь jSON буде замінена на відповідь XML, згенеровану наступним класом [XmlResponse]:
import xmltodict
from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy
from InterfaceResponse import InterfaceResponse
from Logger import Logger
class XmlResponse(InterfaceResponse):
def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
résultat: dict) -> (Response, int):
# результати: словник результатів
# status_code: код статусу відповіді HTTP
# результат: словник, який потрібно перетворити на рядок XML
xml_string = xmltodict.unparse({"root": résultat})
# повертається відповідь HTTP
response = make_response(xml_string)
response.headers['Content-Type'] = 'application/xml; charset=utf-8'
return response, status_code
Це код, який нам відомий — це код функції [xml_response] із спільного модуля [myutils].
Ми ініціалізуємо сесію XML:

Результат, отриманий від сервера, виглядає так:

Ми отримуємо ту саму відповідь, що й у jSON, але цього разу відповідь оформлена як XML.
30.10. Дія [authentifier-utilisateur]
Дія [authentifier-utilisateur] дозволяє автентифікувати користувача, який бажає скористатися додатком для розрахунку податку. Її маршрут визначено таким чином у скрипті [main]:
# аутентифікація користувача
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
Сервер очікує на два параметри, надіслані методом POST:
- [user]: ідентифікатор користувача;
- [password]: його пароль;
Список авторизованих користувачів визначено у конфігурації [config]:
# користувачі, які мають право користуватися додатком
"users": [
{
"login": "admin",
"password": "admin"
}
],
Тут ми маємо список, що складається з одного елемента.
Дія [authentifier-utilisateur] обробляється наступним контролером [AuthentifierUtilisateurController]:
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
from Logger import Logger
class AuthentifierUtilisateurController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# отримуються елементи шляху
dummy, action = request.path.split('/')
# параметри POST
post_params = request.form
# код статусу відповіді HTTP
status_code = None
# спочатку помилок немає
erreur = False
erreurs = []
# потрібен POST з двома параметрами
if len(post_params) != 2:
erreur = True
status_code = status.HTTP_400_BAD_REQUEST
erreurs.append("méthode POST requise, paramètre [action] dans l'URL, paramètres postés [user, password]")
if not erreur:
# отримуємо параметри з POST
# параметр [user]
user = post_params.get("user")
if user is None:
erreur = True
erreurs.append("paramètre [user] manquant")
# параметр [password]
password = post_params.get("password")
if password is None:
erreur = True
erreurs.append("paramètre [password] manquant")
# помилка?
if erreur:
status_code = status.HTTP_400_BAD_REQUEST
# помилка?
if not erreur:
# перевіряється правильність пари (ім'я користувача, пароль)
users = config['users']
i = 0
nbusers = len(users)
trouvé = False
while not trouvé and i < nbusers:
trouvé = user == users[i]["login"] and password == users[i]["password"]
i += 1
# знайдено?
if not trouvé:
# помилка зафіксована
erreur = True
status_code = status.HTTP_401_UNAUTHORIZED
erreurs.append(f"Echec de l'authentification")
else:
# у сесії фіксується, що користувач знайдено
session["user"] = True
# все закінчено
if not erreur:
# повернення без помилки
résultat = {"action": action, "état": 200, "réponse": f"Authentification réussie"}
return résultat, status.HTTP_200_OK
else:
# повернення з помилкою
return {"action": action, "état": 201, "réponse": erreurs}, status_code
- рядок 14: отримуються параметри POST;
- рядок 19: список помилок, виявлених у запиті;
- рядки 20–24: перевіряється, чи дійсно надіслано два параметри;
- рядки 27–31: перевіряється наявність параметра [users];
- рядки 32–36: перевіряється наявність параметра [password];
- рядки 38–39: якщо надіслані параметри є помилковими, готується відповідь HTTP 400 BAD REQUEST;
- рядки 40–58: перевіряється, чи ідентифікатори [user, password] належать користувачеві, який має право користуватися додатком;
- рядки 51–55: якщо користувач (user, password) не має дозволу на використання додатка, готується відповідь HTTP 401 UNAUTHORIZED;
- рядки 56–58: якщо він має дозвіл, то у сесії за допомогою ключа [user] фіксується, що він пройшов автентифікацію;
Зауважимо, що якщо користувач пройшов аутентифікацію з ідентифікаторами [identifiants1] і не зміг пройти аутентифікацію з ідентифікаторами [identifiants2], він все одно залишається аутентифікованим з ідентифікаторами [identifiants1].
Проведемо тести за допомогою Postman:
- запустимо веб-сервер, SGBD та поштовий сервер;
- за допомогою клієнта Postman:
- запускаємо сесію jSON;
- потім пройдемо автентифікацію;
Ось різні випадки.
Випадок 1: POST без переданих параметрів

- у [3-5], POST не має тіла;
Результат запиту такий:

- у [2] отримано відповідь HTTP 400 BAD REQUEST;
- на [5] отримали код помилки [201];
Випадок 2: POST з неправильними обліковими даними

- у [6] ідентифікатори є неправильними;
Сервер надсилає таку відповідь:

- у [2] — відповідь HTTP 401 UNAUTHORIZED;
- на [5] — відповідь про помилку;
Випадок 2: POST з правильними ідентифікаторами

- у [6], ідентифікатори правильні;
Відповідь сервера така:
- у [2] — відповідь HTTP 200 OK;
- у [5] — відповідь про успішне виконання;
30.11. Дія [calculer_impot]
Дія [calculer_impot] дозволяє розрахувати податок платника податків. Її маршрут визначено наступним чином у скрипті [main]:
# розрахувати-податок
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
Сервер очікує на три параметри, надіслані методом POST:
- [marié]: так / ні;
- [enfants]: кількість дітей платника податків;
- [salaire]: річний дохід платника податків;
Контролер [CalculerImpotController] обробляє дію [calculer_impot]:
import re
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
from TaxPayer import TaxPayer
class CalculerImpotController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# отримуємо елементи шляху
dummy, action = request.path.split('/')
# початково помилок немає
erreur = False
erreurs = []
# параметри POST
post_params = request.form
# потрібен POST із трьома параметрами
if len(post_params) != 3:
erreur = True
erreurs.append(
"méthode POST requise avec les paramètres postés [marié, enfants, salaire]")
# аналізуємо надіслані параметри
if not erreur:
# параметр відповідає
marié = post_params.get("marié")
if marié is None:
erreurs.append("paramètre [marié] manquant")
else:
# чи є параметр дійсним?
marié = marié.lower()
if marié != "oui" and marié != "non":
erreur = True
erreurs.append(f"valeur [{marié}] invalide pour le paramètre [marié (oui/non)]")
# параметр [enfants]
enfants = post_params.get("enfants")
if enfants is None:
erreur = True
erreurs.append("paramètre [enfants] manquant")
else:
# чи є параметр дійсним?
enfants = enfants.strip()
match = re.match(r"\d+", enfants)
if not match:
erreur = True
erreurs.append(f"valeur [{enfants}] invalide pour le paramètre [enfants (entier>=0)]")
# параметр «зарплата»
salaire = post_params.get("salaire")
if salaire is None:
erreur = True
erreurs.append("paramètre [salaire] manquant")
else:
# чи є параметр дійсним?
salaire = salaire.strip()
match = re.match(r"\d+", salaire)
if not match:
erreur = True
erreurs.append(f"valeur [{salaire}] invalide pour le paramètre [salaire (entier>=0)]")
# помилка?
if erreur:
status_code = status.HTTP_400_BAD_REQUEST
résultat = {"action": action, "état": 301, "réponse": erreurs}
# повертаємо результат
return résultat, status_code
# розрахунок податку
# отримано шар [métier] та словник [adminData]
métier = config["layers"]["métier"]
admin_data = config["admindata"]
# розрахунок податку
taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
métier.calculate_tax(taxpayer, admin_data)
# номер моделювання
id_simulation = session.get('id_simulation', 0)
id_simulation += 1
session['id_simulation'] = id_simulation
# результат записується в сесію у вигляді словника TaxPayer
simulation = taxpayer.fromdict({'id': id_simulation}).asdict()
# результат додається до списку вже виконаних симуляцій, і цей список записується в сесію
simulations = session.get("simulations", [])
simulations.append(simulation)
session["simulations"] = simulations
# результат
résultat = {"action": action, "état": 300, "réponse": simulation}
status_code = status.HTTP_200_OK
# виводимо результат
return résultat, status_code
- рядок 13: отримуємо назву поточної дії;
- рядок 17: помилки накопичуються у списку;
- рядок 19: отримуємо передані параметри. Вони передаються у форматі [x-www-form-urlencoded], тому ми отримуємо їх у [request.form]. Якби вони були передані у форматі jSON, ми б отримали їх у форматі [request.data];
- рядки 21–24: перевіряється, чи дійсно надіслано три параметри;
- рядки 27–36: перевірка наявності та правильності переданого параметра [marié];
- рядки 37–48: перевірка наявності та правильності переданого параметра [enfants];
- рядки 49–60: перевірка наявності та правильності переданого параметра [salaire];
- рядки 62–66: якщо сталася помилка, надсилається відповідь про помилку 400 BAD REQUEST із кодом стану [301];
- рядки 69–71: якщо помилки не сталося, готуємося до розрахунку податку. Для цього
- рядок 70: отримуємо посилання на шар [métier];
- рядок 71: отримуємо дані податкової адміністрації з конфігурації сервера;
- рядки 72–74: розраховується податок платника податків;
- рядки 75–77: підраховується кількість розрахунків податку, виконаних користувачем;
- рядок 76: отримуємо в сесії номер останнього виконаного розрахунку. Результат розрахунку тут позначаємо як [simulation];
- рядок 77: збільшується номер останнього моделювання;
- рядок 78: цей номер записується у сесію;
- рядки 79–84: щоб відстежувати розрахунки, виконані користувачем, ми помістимо в його сесію список виконаних ним симуляцій;
- рядок 80: симуляція буде словником об’єкта TaxPayer, властивість якого [id] матиме значення номера симуляції;
- рядки 82–84: поточне моделювання додається до списку моделювань, що міститься в сесії;
- рядки 86–87: готується відповідь HTTP про успішне виконання;
- рядок 90: повертається результат;
Проведемо кілька тестів: запускаються веб-сервер, SGBD, поштовий сервер та клієнт Postman.
Випадок 1: розрахунок податку, коли сесія не ініціалізована

Відповідь така:

Випадок 2: розрахунок податку без авторизації
Спочатку запускаємо сесію jSON із [/init-session/json]. Потім виконуємо той самий запит, що й раніше. Відповідь буде такою:

Випадок 3: розрахунок податку з відсутніми параметрами
Ініціюємо сесію jSON, проходимо автентифікацію, а потім надсилаємо такий запит:

- у запиті [5] відсутній параметр [marié];
Відповідь така:
Випадок 4: розрахунок податку з використанням неправильних параметрів


Відповідь сервера така:

Випадок 4: розрахунок податку з правильними параметрами

Відповідь сервера така:

30.12. Дія [lister-simulations]
Дія [lister-simulations] дозволяє користувачеві переглянути список моделювань, які він виконав з початку сеансу. Її шлях визначено у скрипті [main] наступним чином:
# перелік-моделювань
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
# виконуємо контролер, пов'язаний із цією дією
return front_controller()
Сервер не очікує жодних параметрів. Дія [lister-simulations] обробляється наступним контролером [ListerSimulationsController]:
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class ListerSimulationsController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# отримуємо елементи шляху
dummy, action = request.path.split('/')
# отримуємо список симуляцій у сесії
simulations = session.get("simulations", [])
# виводиться результат
return {"action": action, "état": 500,
"réponse": simulations}, status.HTTP_200_OK
- рядок 13: список симуляцій береться із сесії;
- рядки 15–16: повертається відповідь про успішне виконання;
Проведемо наступний тест у Postman:
- запускаємо сесію jSON;
- проводимо аутентифікацію;
- виконуємо два розрахунки податку;
- запитуємо список симуляцій;
Запит має такий вигляд:
- у [3] немає жодних параметрів;
Відповідь сервера така:

- у [4] — список симуляцій користувача;
30.13. Дія [supprimer-simulation]
Дія [supprimer-simulation] дозволяє користувачеві видалити одну з симуляцій зі свого списку симуляцій. Її маршрут визначено таким чином у скрипті [main]:
# видалити-симуляцію
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
Сервер очікує на єдиний параметр — номер симуляції, яку потрібно видалити. Дія [supprimer-simulation] обробляється наступним контролером [SupprimerSimulationController]:
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class SupprimerSimulationController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# отримуємо елементи шляху
dummy, action, numéro = request.path.split('/')
# параметр [numéro] є додатним цілим числом або нулем, виходячи з його шляху
numéro = int(numéro)
# симуляція з ідентифікатором id=номер має бути присутньою у списку симуляцій
simulations = session.get("simulations", [])
liste_simulations = list(filter(lambda simulation: simulation['id'] == numéro, simulations))
if not liste_simulations:
msg_erreur = f"la simulation n° [{numéro}] n'existe pas"
# видається помилка
return {"action": action, "état": 601, "réponse": [msg_erreur]}, status.HTTP_400_BAD_REQUEST
# видалення симуляції з ідентифікатором «номер»
simulation = liste_simulations.pop(0)
simulations.remove(simulation)
# симуляції повертаються до сесії
session["simulations"] = simulations
# повертає результат
return {"action": action, "état": 600, "réponse": simulations}, status.HTTP_200_OK
- рядок 10: отримуємо два елементи шляху запиту. Вони отримуються у вигляді символьних рядків;
- рядок 13: параметр [numéro] перетворюється на ціле число. Ми знаємо, що це можливо завдяки сигнатурі його маршруту,
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
Крім того, відомо, що це ціле число >=0. Адже не може існувати URL або [/supprimer-simulation/-4]. Такі значення відхиляються сервером Flask;
- рядок 15: отримуємо список симуляцій із сесії;
- рядок 16: за допомогою функції [filter] шукаємо симуляцію з id==номер. Отримуємо об’єкт [filter], який перетворюємо на тип [list];
- рядки 17–20: якщо фільтр нічого не повернув, це означає, що симуляція, яку потрібно видалити, не існує. Повертається відповідь про помилку, що вказує на це;
- рядки 21–23: видаляємо симуляцію, повернуту фільтром;
- рядок 25: новий список симуляцій повертається в сесію;
- рядок 27: у відповіді повертається новий список симуляцій;
Ми виконуємо тест на успіх і тест на невдачу. Проводимо симуляції, а потім запитуємо список симуляцій:

- симуляції тут мають номери 2 та 3;
Запитуємо видалити симуляцію з номером 3.

Відповідь така:
Тепер повторімо ту саму операцію (видалення симуляції з id=3). Відповідь буде такою:


30.14. Дія [fin-session]
Дія [fin-session] дозволяє користувачеві завершити сеанс симуляцій. Її маршрут визначено таким чином у скрипті [main]:
# завершення сесії
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
# виконується контролер, пов'язаний з цією дією
return front_controller()
Сервер не очікує жодних параметрів. Дія обробляється наступним контролером [FinSessionController]:
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class FinSessionController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# отримуємо елементи шляху
dummy, action = request.path.split('/')
# видаляються всі ключі поточної сесії
session.clear()
# повертається результат
return {"action": action, "état": 400, "réponse": "session réinitialisée"}, status.HTTP_200_OK
- рядок 13: видаляються всі ключі сеансу. Це видаляє:
- [typeResponse]: тип відповідей HTTP (json, xml, html);
- [id_simulation]: номер останнього виконаного моделювання;
- [simulations]: список симуляцій користувача;
- [user]: індикатор того, що користувач пройшов автентифікацію;
- повертається відповідь;
Можна задатися питанням, як буде повернуто відповідь HTTP із рядка 15, оскільки тип відповіді більше не зберігається у сесії. Щоб це з’ясувати, потрібно повернутися до функції |front_controller| головного скрипта [main] і змінити її наступним чином:
…
# on not# фіксується бажаний тип відповіді, якщо ця інформація міститься в сесії
type_response1 = session.get('typeResponse', None)
# запит передається головному контролеру
main_controller = config['controllers']["main-controller"]
résultat, status_code = main_controller.execute(request, session, config)
# результат, надісланий клієнту, записується в журнал
log = f"[front_controller] {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_response=type_response1
# формується відповідь для відправлення
response_builder = config["responses"][type_response]
response, status_code = response_builder \
.build_http_response(request, session, config, status_code, résultat)
# відправляється відповідь
return response, status_code
- рядок 3: запам'ятовується тип відповіді, що наразі знаходиться в сесії;
- рядок 6: виконується дія. Якщо це:
- [fin-session], то ключ [typeResponse] більше не перебуває в сесії;
- [init-session], значення ключа [typeResponse] сесії могло змінитися;;
- рядки 14–20: потрібно надіслати відповідь HTTP. Потрібно знати, у якому форматі:
- рядки 16–18: якщо тип відповіді не визначено ані за допомогою [type_response1] у рядку 3, ані за допомогою [type_response2] у рядку 15, то тип відповіді не був визначений ані до, ані після дії. У такому разі використовується jSON (рядок 18);
- рядки 19–21: якщо існує [type_response2] — тип у сесії після дії, то саме цей тип і слід використовувати;
- рядки 22–23: інакше слід використовувати [type_response1] — тип відповіді до дії (яка обов’язково має бути [fin-session]);
30.15. Дія [get-admindata]
Тепер розглянемо два типи URL, зарезервовані для служб jSON та XML:
Дія | Роль | Контекст виконання |
/get-admindata | Повертає податкові дані, необхідні для розрахунку податку | Запит GET. Використовується лише у разі, якщо тип сеансу — json або xml. Користувач має бути автентифікований |
/calculer-impots | Виконує розрахунок податку за списком платників податків, розміщеним у jSON | Запит GET. Використовується лише у разі, якщо тип сеансу — json або xml. Користувач має бути автентифікований |
URL та [/get-admindata] визначені в маршрутах головного скрипта [main] наступним чином:
# get-admindata
@app.route('/get-admindata', methods=['GET'])
def get_admindata() -> tuple:
# виконується контролер, пов’язаний із цією дією
return front_controller()
Маршрут [/get-admindata] обробляється наступним контролером [GetAdminDataController]:
# імпортуються залежності
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class GetAdminDataController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# отримуються елементи шляху
dummy, action = request.path.split('/')
# приймаються лише сесії у форматі json та xml
type_response = session.get('typeResponse')
if type_response != 'json' and type_response != 'xml':
# повертається відповідь про помилку
return {
"action": action,
"état": 1001,
"réponse": ["cette action n'est possible que pour les sessions json ou xml"]
}, status.HTTP_400_BAD_REQUEST
else:
# повертається відповідь про успішне виконання
return {"action": action, "état": 1000, "réponse": config["adminData"].asdict()}, status.HTTP_200_OK
- рядки 13–21: перевіряється, чи сесія є у форматі json або xml;
- рядок 24: повертається словник даних податкової адміністрації, які з моменту запуску сервера були розміщені в конфігурації:
# admindata буде даними з областю дії «додаток» у режимі «тільки для читання»
config["admindata"] = config["layers"]["dao"].get_admindata()
Скористаємося клієнтом Postman і надішлемо запит на URL [/get-admindata] після запуску сесії jSON та проходження автентифікації:

Відповідь сервера така:

30.16. Дія [calculer-impots]
Дія [calculer-impots] обчислює податок для списку платників податків, знайденого в тілі запиту у вигляді рядка jSON. Ми вже знайомі з цією дією: у попередній версії вона називалася [calculate_tax_in_bulk_mode].
Її шлях такий:
# пакетний розрахунок податку
@app.route('/calculer-impots', methods=['POST'])
def calculer_impots():
# виконується контролер, пов'язаний з цією дією
return front_controller()
Ця дія обробляється наступним контролером [CalculerImpotsController]:
import json
from flask_api import status
from werkzeug.local import LocalProxy
from ImpôtsError import ImpôtsError
from InterfaceController import InterfaceController
from TaxPayer import TaxPayer
class CalculerImpotsController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# отримуються елементи шляху
dummy, action = request.path.split('/')
# приймаються лише сесії у форматі json та xml
type_response = session.get('typeResponse')
if type_response != 'json' and type_response != 'xml':
# повертається відповідь про помилку
return {
"action": action,
"état": 1501,
"réponse": ["cette action n'est possible que pour les sessions json ou xml"]
}, status.HTTP_400_BAD_REQUEST
# отримується тіло запиту POST — очікується список словників
msg_erreur = None
list_dict_taxpayers = None
# тіло jSON з POST
request_text = request.data
try:
#, який перетворюється на список словників
list_dict_taxpayers = json.loads(request_text)
except BaseException as erreur:
# фіксуємо помилку
msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
# чи маємо ми непорожній список?
if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
# фіксуємо помилку
msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
# чи є список словників?
if not msg_erreur:
erreur = False
i = 0
while not erreur and i < len(list_dict_taxpayers):
erreur = not isinstance(list_dict_taxpayers[i], dict)
i += 1
# помилка?
if erreur:
msg_erreur = "le corps du POST doit être une liste de dictionnaires"
# помилка?
if msg_erreur:
# надсилається відповідь про помилку клієнту
résultats = {"action": action, "état": 1501, "réponse": [msg_erreur]}
return résultats, status.HTTP_400_BAD_REQUEST
# перевіряються TaxPayers по одному
# спочатку помилок немає
list_erreurs = []
for dict_taxpayer in list_dict_taxpayers:
# створюється TaxPayer на основі dict_taxpayer
msg_erreur = None
try:
# наступна операція усуне випадки, коли параметри не
# властивостей класу TaxPayer, а також випадки, коли їхні значення
# є некоректними
TaxPayer().fromdict(dict_taxpayer)
except BaseException as erreur:
msg_erreur = f"{erreur}"
# деякі ключі повинні бути присутніми у словнику
if not msg_erreur:
# ключі [marié, enfants, salaire] повинні бути присутніми у словнику
keys = dict_taxpayer.keys()
if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
# помилки?
if msg_erreur:
# помилка виявлена в самому TaxPayer
dict_taxpayer['erreur'] = msg_erreur
# TaxPayer додано до списку помилок
list_erreurs.append(dict_taxpayer)
# всі платники податків оброблені — чи є помилки?
if list_erreurs:
# надсилається відповідь про помилку клієнту
résultats = {"action": action, "état": 1501, "réponse": list_erreurs}
return résultats, status.HTTP_400_BAD_REQUEST
# помилок немає, можна продовжувати роботу
# отримано дані від податкової служби
admindata = config["admindata"]
métier = config["layers"]["métier"]
try:
# обробка TaxPayer по одному
list_taxpayers = []
for dict_taxpayer in list_dict_taxpayers:
# розрахунок податку
taxpayer = TaxPayer().fromdict(
{'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
'«заробітна плата»: dict_taxpayer['salaire']})
métier.calculate_tax(taxpayer, admindata)
# результат зберігаємо як словник
list_taxpayers.append(taxpayer.asdict())
# додаємо list_taxpayers до поточних моделювань, присвоюючи кожному моделюванню номер
simulations = session.get("simulations", [])
id_simulation = session.get("id_simulation", 0)
for simulation in list_taxpayers:
# кожному моделюванню присвоюється номер
id_simulation += 1
simulation['id'] = id_simulation
# додаємо його до поточного списку симуляцій
simulations.append(simulation)
# завантажуємо все в сесію
session["simulations"] = simulations
session["id_simulation"] = id_simulation
# відправляється відповідь клієнту
return {"action": action, "état": 1500, "réponse": list_taxpayers}, status.HTTP_200_OK
except ImpôtsError as erreur:
# відправляємо клієнту повідомлення про помилку
return {"action": action, "état": 1501, "réponse": [f"{erreur}"]}, status.HTTP_500_INTERNAL_SERVER_ERROR
- рядки 16–24: перевіряється, чи дійсно ми перебуваємо в сесії json або xml
- рядки 26–120: цей код нам загалом відомий. Це код функції |index_controller| з версії 10 додатка, який було адаптовано відповідно до специфікацій реалізованого інтерфейсу [InterfaceController];
- рядки 104–115: код, доданий для врахування нового середовища цього контролера. Ми щойно провели розрахунки податку. Нам потрібно зберегти результати у списку симуляцій, що зберігаються у сесії;
- рядок 105: отримуємо список симуляцій у сесії;
- рядок 106: отримуємо номер останнього виконаного моделювання;
- рядки 107–112: пробігаємо список словників результатів розрахунку податку, кожному з них присвоюємо номер симуляції [id], і кожен словник додаємо до списку симуляцій;
- рядки 113–115: новий список симуляцій, а також номер останньої виконаної симуляції передаються в сесію;
Після ініціалізації сесії jSON та проходження автентифікації ми виконуємо наступний тест у Postman:


Відповідь сервера така:

Якщо тепер запросити список симуляцій:
Можна помітити, що у списку результатів [/calcul-impots] платники податків не мають атрибута [id], тоді як у списку симуляцій кожна симуляція має номер, що її ідентифікує.




