22. Веб-сервіси на базі фреймворку Flask
Під веб-сервісом тут мається на увазі будь-який веб-додаток, що надає необроблені дані, які використовуються клієнтом; у наведених нижче прикладах це часто консольний скрипт. Ми не розглядаємо конкретні технології, наприклад REST (REpresentational State Transfer) або SOAP (Simple Object Access Protocol), які надають більш-менш необроблені дані у чітко визначеному форматі. REST надає jSON, тоді як для SOAP — це XML. Кожна з цих технологій чітко описує, як саме клієнт повинен надсилати запит до сервера та який вигляд має мати відповідь сервера. У цьому курсі ми будемо набагато гнучкішими щодо характеру запиту клієнта та відповіді сервера. Однак написані скрипти та використовувані інструменти близькі до тих, що застосовуються в технології REST.
22.1. Вступ
Скрипти на Python можуть виконуватися веб-сервером. Такий скрипт стає серверною програмою, здатною обслуговувати декількох клієнтів. З точки зору клієнта виклик веб-сервісу зводиться до запиту URL цього сервісу. Клієнт може бути написаний будь-якою мовою, зокрема на Python. У цьому випадку використовуються інтернет-функції, які ми щойно розглянули. Крім того, нам потрібно вміти «спілкуватися» з веб-сервісом, тобто розуміти протокол HTTP, що забезпечує зв’язок між веб-сервером та його клієнтами. Саме цьому був присвячений розділ |протокол HTTP|. Веб-клієнти, описані в цій частині курсу, дали нам змогу ознайомитися з частиною протоколу HTTP.

У найпростішому варіанті обмін даними між клієнтом і сервером відбувається наступним чином:
- клієнт встановлює з’єднання з портом 80 веб-сервера;
- він надсилає запит щодо документа;
- веб-сервер надсилає запитуваний документ і закриває з’єднання;
- у свою чергу, клієнт закриває з’єднання;
Документ може бути різного типу: текст у форматі HTML, зображення, відео тощо. Це може бути існуючий документ (статичний документ) або документ, що генерується на льоту за допомогою скрипта (динамічний документ). В останньому випадку йдеться про веб-програмування. Скрипт для динамічного формування документів може бути написаний на різних мовах: PHP, Python, Perl, Java, Ruby, C#, VB.net тощо.
Далі ми будемо використовувати скрипти Python для динамічного формування текстових документів.

- у [1] клієнт встановлює з’єднання з сервером, запитує скрипт Python, надсилає або не надсилає параметри для цього скрипта;
- у [3] веб-сервер запускає виконання скрипта Python за допомогою інтерпретатора Python. Скрипт генерує документ, який надсилається клієнту [2];
- сервер закриває з’єднання. Клієнт робить те саме;
Веб-сервер може обслуговувати декількох клієнтів одночасно.
Далі ми будемо використовувати два веб-сервери:
- легкий сервер Werkzeug [https://werkzeug.palletsprojects.com/en/1.0.x/]. Цей сервер використовується веб-фреймворком Flask [https://flask.palletsprojects.com/en/1.1.x/]. Ми будемо частіше називати його сервером Flask;
- сервер Apache 2 [https://httpd.apache.org/];
Сервер Flask використовуватиметься у всіх прикладах. Сервер Apache використовуватиметься для розміщення веб-додатку, який ми будемо розробляти.
Фреймворк Flask розроблено на Python. Це модуль, який встановлюється в терміналі PyCharm:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>pip install flask
Collecting flask
Downloading Flask-1.1.2-py2.py3-none-any.whl (94 kB)
|| 94 kB 1.1 MB/s
Collecting click>=5.1
Downloading click-7.1.2-py2.py3-none-any.whl (82 kB)
|| 82 kB 5.8 MB/s
Collecting itsdangerous>=0.24
Downloading itsdangerous-1.1.0-py2.py3-none-any.whl (16 kB)
Collecting Jinja2>=2.10.1
Downloading Jinja2-2.11.2-py2.py3-none-any.whl (125 kB)
|| 125 kB 6.4 MB/s
Collecting Werkzeug>=0.15
Downloading Werkzeug-1.0.1-py2.py3-none-any.whl (298 kB)
|| 298 kB 6.4 MB/s
Collecting MarkupSafe>=0.23
Downloading MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl (16 kB)
Installing collected packages: click, itsdangerous, MarkupSafe, Jinja2, Werkzeug, flask
Successfully installed Jinja2-2.11.2 MarkupSafe-1.1.1 Werkzeug-1.0.1 click-7.1.2 flask-1.1.2 itsdangerous-1.1.0
- рядок 1: виконана команда;
- рядок 19: елементи, що були встановлені:
- [flask-1.1.2]: фреймворк для веб-розробки на Python;
- [Werkzeug-1.0.1] — це веб-сервер, який відповідатиме на запити клієнтів;
- [Jinja2-2.11.2] — це інструмент, що дозволяє вставляти динамічні елементи у сторінки, які інакше були б статичними;
22.2. скрипти [flask/01]: перші елементи веб-програмування

Наші приклади будуть виконуватися в такій архітектурі:

- у [1] скрипт Python виконуватиметься так само, як і звичайний консольний скрипт;
- у [2] прозоро запускається веб-сервер, який очікує на запити. Насправді він прийме лише один запит URL;
- у [3] браузер запитає у сервера його єдиний URL;
- у [4] сервер запустить скрипт Python, вказаний консоллю [1];
- у [5] скрипт поверне свої результати веб-серверу у вигляді текстового документа;
- у [6] веб-сервер надішле цей текстовий документ до браузера;
22.2.1. скрипт [exemple_01]: основи мови HTML
Веб-браузер може відображати різні документи, найпоширенішим з яких є документ HTML (HyperText Markup Language). Це текст, відформатований за допомогою тегів у формі <balise>texte</balise>. Так, тег <b>important</b> відображатиме важливий текст жирним шрифтом. Існують окремі теги, такі як тег <hr/>, який відображає горизонтальну лінію. Ми не будемо розглядати теги, які можна знайти в тексті HTML. Існує безліч програм WYSIWYG, що дозволяють створити сторінку WEB без написання жодного рядка коду HTML. Ці інструменти автоматично генерують код HTML на основі макета, створеного за допомогою миші та попередньо визначених елементів управління. Таким чином, можна (за допомогою миші) вставити на сторінку таблицю, а потім переглянути код HTML, згенерований програмним забезпеченням, щоб дізнатися, які теги слід використовувати для визначення таблиці на сторінці WEB. Все дуже просто. Крім того, знання мови HTML є необхідним, оскільки динамічні веб-додатки повинні самостійно генерувати код HTML для надсилання веб-клієнтам. Цей код генерується програмно, і, звісно, потрібно знати, що саме слід згенерувати, щоб клієнт отримав бажану веб-сторінку.
Підсумовуючи, для початку веб-програмування зовсім не обов’язково знати мову HTML у повному обсязі. Однак ці знання є необхідними і їх можна здобути завдяки використанню програмного забезпечення WYSIWYG для створення веб-сторінок WEB, таких як DreamWeaver та десятки інших. Інший спосіб ознайомитися з тонкощами мови HTML — це переглядати веб-сторінки та вивчати вихідний код тих сторінок, які мають цікаві та ще невідомі вам особливості.
Розглянемо наступний приклад, який містить деякі елементи, що можуть зустрічатися у веб-документі, такі як:
- таблиця;
- зображення;
- посилання;

Документ HTML обмежений тегами <html>…</html>. Він складається з двох частин:
- <head>…</head>: це невидима частина документа. Вона надає інформацію браузеру, який буде відображати документ. У ній часто зустрічається тег <title>…</title>, який визначає текст, що відображатиметься у рядку заголовка браузера. Також тут можуть бути інші теги, зокрема ті, що визначають ключові слова документа, які згодом використовуються пошуковими системами. У цій частині також можуть міститися скрипти, найчастіше написані на JavaScript або VBScript, які будуть виконуватися браузером;
- <body атрибути>…</body>: це частина, яка відображатиметься браузером. Теги HTML, що містяться в цій частині, вказують браузеру «бажаний» візуальний вигляд документа. Кожен браузер інтерпретує ці теги по-своєму. Тому два браузери можуть по-різному відображати один і той самий веб-документ. Зазвичай це є однією з головних проблем веб-дизайнерів;
Код HTML нашого прикладу документа виглядає так:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Quelques balises HTML</title>
</head>
<body style="background-image: url(/static/images/standard.jpg)">
<h1 style="text-align: left">Quelques balises HTML</h1>
<hr />
<table border="1">
<thead>
<tr>
<th>Colonne 1</th>
<th>Colonne 2</th>
<th>Colonne 3</th>
</tr>
</thead>
<tbody>
<tr>
<td>cellule(1,1)</td>
<td style="text-align: center;">cellule(1,2)</td>
<td>cellule(1,3)</td>
</tr>
<tr>
<td>cellule(2,1)</td>
<td>cellule(2,2)</td>
<td>cellule(2,3</td>
</tr>
</tbody>
</table>
<br /><br />
<table border="0">
<tr>
<td>Une image</td>
<td>
<img border="0" src="/static/images/cerisier.jpg" />
</td>
</tr>
<tr>
<td>Le site de Polytech'Angers</td>
<td><a href="http://www.polytech-angers.fr/fr/index.html">ici</a></td>
</tr>
</table>
</body>
</html>
Елементи | теги та приклади HTML |
<title>Деякі теги HTML</title> (рядок 5) текст [Quelques balises HTML] з’явиться у рядку заголовка браузера, який відображатиме документ | |
<hr />: відображає горизонтальну лінію (рядок 10) | |
<атрибути таблиці>….</table>: для визначення таблиці (рядки 12, 32) <thead>…</thead>: для визначення заголовків стовпців (рядки 13, 19) <tbody>…</tbody>: для визначення вмісту таблиці (рядки 20, 31) <tr атрибути>…</tr>: для визначення рядка (рядки 21, 25) <td атрибути>…</td>: для визначення комірки (рядок 22) приклади: <table border="1">…</table>: атрибут border визначає товщину межі таблиці <td style="text-align: center;">комірка(1,2)</td> (рядок 23): визначає комірку, вміст якої буде комірка(1,2). Цей вміст буде вирівняно по горизонталі (text-align: center). | |
<img border="0" src="/static/images/cerisier.jpg"/> (рядок 38): визначає зображення без рамки (border=0"), вихідний файл якого — [/static/images/cerisier.jpg] на веб-сервері (src="/static/images/cerisier.jpg"). Якщо це посилання міститься у веб-документі, отриманому за допомогою URL [http://server/chemin/balises.html], то браузер завантажить URL [http://server/ static/images/cerisier.jpg], щоб отримати зображення, на яке тут посилається. | |
<a href="http://www.polytech-angers.fr/fr/index.html">тут</a> (рядок 43): робить текст ici посиланням на URL http://www.polytech-angers.fr/fr/index.html. | |
<body style="background-image: url(/static/images/standard.jpg)"> (рядок 8): вказує, що зображення, яке має слугувати фоном сторінки, знаходиться за адресою URL [/static/images/standard.jpg] на веб-сервері. У контексті нашого прикладу браузер надішле запит на URL [http://server/static/images/standard.jpg], щоб отримати це фонове зображення. |
З цього простого прикладу видно, що для побудови всього документа браузер повинен надіслати три запити на сервер:
- [http://server/chemin/balises.html] — щоб отримати вихідний код HTML документа;
- [http://server/static/images/cerisier.jpg] — щоб отримати зображення cerisier.jpg;
- [http://server/static/images/standard.jpg] — для отримання фонового зображення standard.jpg;
Скрипт [exemple_01] дозволить нам відобразити попередню статичну сторінку [balises.html]:

- у [1] — скрипт [exemple_01], який буде виконано;
- у [3] — документ HTML, який буде відображено за допомогою скрипта;
- у [2] — зображення з документа HTML;
Скрипт [exemple_01] має такий вигляд:
import os
from flask import Flask, make_response, render_template
# додаток Flask
script_dir = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, template_folder=f"{script_dir}/../templates", static_folder=f"{script_dir}/../static")
# Головна URL
@app.route('/')
def index():
# відображення сторінки
return make_response(render_template("balises.html"))
# main
if __name__ == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run()
- рядок 7: створюється екземпляр додатка Flask. Додаток Flask — це веб-додаток;
- перший параметр — це ім’я, яке надається додатку. Можна задати будь-яке ім’я. Тут використано попередньо визначений атрибут [__name__], значення якого дорівнює [__main__] (рядок 18);
- другий параметр є іменованим, тобто його позиція в порядку параметрів не має значення. Іменований параметр [template_folder] вказує на папку, де знаходяться статичні сторінки веб-додатку. Статичні сторінки передаються браузеру без змін. У даному випадку статичні сторінки будуть знаходитися в папці [templates] у дереві папок проекту. У рядку 7 ми вказали відносний шлях до папки [script_dir], що містить скрипт [exemple_01], який виконується;
- третій параметр також є іменованим. [static_folder] позначає папку, в якій знаходяться ресурси документа HTML (зображення, відео тощо). Тут також вказано відносний шлях до папки [script_dir], що містить виконаний скрипт [exemple_01];
- рядки 10–14: тут визначаються URL, які підтримує веб-додаток. Кожен URL пов’язаний із функцією, яка виконується, коли веб-браузер запитує URL;
- рядок 11: єдиним URL у додатку є URL [/]. Зверніть увагу, що в [@app.route('/')] [app] — це змінна, ініціалізована у рядку 7. Отже, визначення маршрутів (різних URL, що обробляються додатком) обов’язково йде після визначення самого додатка [app]. Остання назва є довільною;
- рядки 12–14: функція, яка виконується при запиті URL [/] до веб-додатку [exemple_01];
- рядок 12: функція, пов’язана з URL, може мати будь-яку назву. Іноді вона може мати параметри для отримання елементів з пов’язаного з нею URL. У цьому випадку вона їх не має;
- рядок 14:
- функція [render_template] повертає рядок символів, який є текстовим документом, згенерованим її параметром. У даному випадку це [balises.html]. Через [template_folder] у рядку 7 цей документ буде шукатися у папці [f"{script_dir}/../templates"]. Він дійсно там знаходиться;
- функція [make_response] генерує відповідь HTTP для браузера, який запитав у неї URL [/]. У розділі |протокол HTTP| ми бачили, що відповідь HTTP складається з двох елементів:
- заголовки HTTP;
- документ, запитуваний браузером, у даному випадку документ HTML;
У рядку 14 функції [make_response] для генерації заголовків HTTP не було передано жодних параметрів. Тому вона згенерує їх за замовчуванням. Пізніше ми розглянемо, як задати ці заголовки HTTP.
- нарешті, коли браузер запитує URL у додатку Flask, він отримує сторінку [balises.html];
- рядки 17–20: ці рядки призначені для запуску веб-сервера, який буде виконувати веб-додаток [exemple_01];
- рядок 18: ця умова справджується лише тоді, коли скрипт [exemple_01] запускається в консолі;
- рядок 19: веб-додаток [app] із рядка 7 налаштовується:
- параметр із назвою [ENV="development"] переводить веб-сервер у режим розробки: щойно розробник змінює якийсь елемент додатка, той перегенерується та передається на веб-сервер. Розробнику не потрібно запускати додаток заново;
- параметр із назвою [DEBUG=True] дозволить розробнику встановлювати точки зупинки в коді додатка;
- рядок 20: запускається веб-додаток: створюється екземпляр веб-сервера, і на ньому розгортається веб-додаток для обробки запитів веб-клієнтів;
Ось приклад виконання:

У консолі виконання з’являються такі записи:
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/flask/01/main/exemple_01.py
* Serving Flask app "exemple_01" (lazy loading)
* Environment: development
* Debug mode: on
* Restarting with stat
* Debugger is active!
* Debugger PIN: 334-263-283
* Running on http://127.0.0.1:5000/ (Натисніть CTRL+C, щоб вийти)
- рядок 2: сервер відображає виконаний скрипт;
- рядок 3: система перебуває в режимі розробки;
- рядки 4–5: сервер бачить, що його запущено в режимі [debug]. Тож він перезапускається (рядок 5). Отже, режим [debug] дещо уповільнює запуск;
- рядок 8: URL, де доступний розгорнутий веб-додаток [exemple_01];
За допомогою веб-браузера звернемося до URL [http://127.0.0.1:5000/]:

Ми отримуємо очікуваний документ [balises.html].
22.2.2. Скрипт [exemple_02]: динамічне створення документа HTML

Скрипт [exemple_02] [1] згенерує такий документ [exemple_02.html] [2]:
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>{{page.title}}</title>
</head>
<body>
<b>{{page.contents}}</b>
</body>
</html>
Цей документ є динамічним, оскільки його вміст стає повністю відомим лише в момент, коли веб-сервер його видає. У рядках 5 та 8 містяться два елементи, які на момент написання сторінки були невідомі. Вони стають відомими лише в момент надсилання сторінки клієнту. Тоді вони замінюються своїми значеннями, які є символьними рядками.
- рядки 5, 8: синтаксис {{expression}} є синтаксисом мови шаблонів Jinja2 [https://jinja.palletsprojects.com/en/2.11.x/]. Перш ніж сторінка буде надіслана клієнту, динамічні елементи сторінки (рядки 5 та 8) обчислюються та замінюються на їхні значення;
- рядок 5: використано синтаксис [page.title]. Отже, припускається, що під час формування сторінки перед її відправкою відома змінна [page] — про це ми поговоримо пізніше. У синтаксисі {{expression}} можна використовувати будь-які імена змінних. У рядках 5 та 8, наприклад, можуть бути {{title}} та {{contents}}. Тоді можна сказати, що [title] та [contents] є параметрами сторінки. Далі ми завжди використовуватимемо ту саму техніку:
- єдиним параметром сторінки буде словник [page];
- атрибути цього словника будуть використовуватися на сторінці. У даному випадку це [page.title] у рядку 5 та [page.contents] у рядку 8;
Веб-додаток [exemple_02.py] виглядає так:
from flask import Flask, make_response, render_template
# додаток Flask
script_dir = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, template_folder=f"{script_dir}/../templates", static_folder=f"{script_dir}/../static")
# Головна URL
@app.route('/')
def index():
# вміст сторінки у вигляді словника
page = {"title": "un titre", "contents": "un contenu"}
# відображення сторінки
return make_response(render_template("exemple_02.html", page=page))
# main
if __name__ == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run()
- ми вже пояснювали у попередньому прикладі, рядки 4–5 та 18–20. Ми завжди використовуватимемо цю схему у наших прикладах;
- рядок 9: єдиним URL, що надається веб-додатком, є URL /;
- рядок 14: документ, що подається на URL /, — це документ [exemple_02.html], який ми щойно прокоментували. Ми знаємо, що він має один параметр — словник із назвою [page];
- рядок 12: ми визначаємо словник, який буде передано як параметр на сторінку [exemple_02.html]. Він може мати будь-яку назву. Однак він повинен мати атрибути [title, contents], що використовуються в документі HTML;
- рядок 14: функція [render_template] призначена для формування символьного рядка з документа [exemple_02.html]. Оскільки це документ із параметрами, ми передаємо функції [render_template] очікуваний параметр або параметри. Ми робимо це, присвоюючи значення параметру з іменем [page]. В операції [page=page]:
- ліворуч від знака = знаходиться параметр [page], який використовується в документі [exemple_02.html];
- праворуч від знака = — значення [page], визначене в рядку 12;
- загалом, якщо документ HTML має параметри [param1, param2, …, paramn], їхні значення передаватимуться до функції [render_template] у формі [render_template(document, param1=valeur1, param2=valeur2, …];
Перед запуском [exemple_02] необхідно зупинити виконання [exemple_01]:

Якщо під час виконання скрипта 1 у вас складається враження, що виконується скрипт 2, це, ймовірно, тому, що останній все ще виконується. Щоб повернутися до відомого стану, ви можете зупинити всі процеси, що виконуються в PyCharm (у верхньому правому куті вікна PyCharm):

Запустимо скрипт [exemple_02]:

Журнали консолі виглядають наступним чином:
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/flask/01/main/exemple_02.py
* Serving Flask app "exemple_02" (lazy loading)
* Environment: development
* Debug mode: on
* Restarting with stat
* Debugger is active!
* Debugger PIN: 334-263-283
* Running on http://127.0.0.1:5000/ (Натисніть CTRL+C, щоб вийти)
У рядку 8 вказано порт розгортання (5000) додатка [exemple_02] (рядок 1) на машині [localhost]. Оскільки попередні рядки завжди однакові, ми більше їх не наводитимемо.
За допомогою браузера ми запитуємо URL [http://localhost:5000/]:

- вираз {{page.title}} дав результат [1];
- вираз {{page.contents}} дав результат [2];
22.2.3. скрипт [exemple_03]: використання фрагментів сторінки

- у [1], скрипт [exemple_03.py] згенерує динамічний документ [exemple_03.html] [2]. Цей документ буде побудовано на основі фрагментів сторінки [fragment_01.html, fragment_02.html] [3];
Документ [exemple_03.html] матиме такий вигляд:
<!DOCTYPE html>
<html lang="fr">
{% include "fragments/fragment_01.html" %}
<body>
{% include "fragments/fragment_02.html" %}
</body>
</html>
- у рядках 3 та 5 використовується директива Jinja2 [include] для включення в документ елементів, що знаходяться поза ним;
- синтаксис — {% include … %}. Параметром директиви [include] є шлях до документа, який потрібно включити. Цей шлях є відносним до параметра [template_folder] додатка Flask:
app = Flask(__name__, template_folder="../templates", static_folder="../static")
Отже, у цьому випадку шляхи до документів вимірюються відносно папки [templates].
Фрагмент [fragment_01.html] (назви, звісно, можна вибирати довільно) має такий вигляд:
<meta charset="UTF-8">
<title>{{page.title}}</title>
Фрагмент [fragment_02.html] виглядає так:
<b>{{page.contents}}</b>
Якщо відновити документ [exemple_03.html] за допомогою цих фрагментів, отримаємо такий код:
<!DOCTYPE html>
<html lang="fr">
<meta charset="UTF-8">
<title>{{page.title}}</title>
<body>
<b>{{page.contents}}</b>
</body>
</html>
Отже, ми маємо документ, ідентичний [exemple_02.html], але побудований із фрагментів.
Веб-скрипт [exemple_03.py] має такий вигляд:
import os
from flask import Flask, make_response, render_template
# додаток Flask
script_dir = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, template_folder=f"{script_dir}/../templates", static_folder=f"{script_dir}/../static")
# Головна URL
@app.route('/')
def index():
# вміст сторінки
page = {"title": "un autre titre", "contents": "un autre contenu"}
# відображення сторінки
return make_response(render_template("views/exemple_03.html", page=page))
# main
if __name__ == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run()
Код аналогічний коду [exemple_02.py]. У рядку 16 показано, як можна посилатися на документи, що знаходяться в підпапках [template_folder] із рядка 7.
Виконання скрипта [exemple_03.py] дає такі результати у браузері:

22.3. скрипт [flask/02]: веб-сервіс дати та часу

Документ [date_time_server.html] має такий вигляд:
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Date et heure du moment</title>
</head>
<body>
<b>Date et heure du moment : {{page.date_heure}}</b>
</body>
</html>
- рядок 8: сторінка підтримує параметр [page.date_heure];
Веб-сервіс [date_time_server.py] має такий вигляд:
# імпорт
import os
import time
from flask import Flask, make_response, render_template
# додаток Flask
script_dir = os.path.dirname(os.path.abspath(__file__))
app = Flask(__name__, template_folder=f"{script_dir}")
# Головна URL
@app.route('/')
def index():
# відправлення часу клієнту
# time.localtime: кількість мілісекунд з 01.01.1970
# time.strftime дозволяє форматувати час і дату
# формат відображення дати та часу
# d: день у форматі з 2 цифр
# m: місяць (2 цифри)
# y: рік у форматі 2 цифри
# H: година 0,23
# M: хвилини
# S: секунди
# дата/час поточного моменту
time_of_day = time.strftime('%d/%m/%y %H:%M:%S', time.localtime())
# генерується документ для надсилання клієнту
page = {"date_heure": time_of_day}
document = render_template("date_time_server.html", page=page)
print("document", type(document), document)
# відповідь HTTP клієнту
response = make_response(document)
print("response", type(response), response)
return response
# тільки для внутрішнього використання
if __name__ == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run()
- рядок 13: веб-додаток підтримує лише URL /;
- рядки 15–24: пояснюють, як отримати дату та час і як їх відобразити;
- рядок 27: рядок символів, що представляє поточну дату та час;
- рядки 28–30: генерується динамічний документ [date_time_server.html] шляхом передачі йому словника [page] з рядка 29;
- рядок 31: виводиться тип [document] та сам документ. Ми хочемо показати, що це рядок символів;
- рядок 33: генерується відповідь HTTP, яка буде надіслана клієнту (вона ще не надіслана);
- рядок 34: виводиться її тип та значення;
- рядок 35: відповідь HTTP надсилається клієнту;
Виконання скрипта дає такий результат у браузері:

Журнали в консолі мають такий вигляд:
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\flask\02\date_time_server.py
* Serving Flask app "date_time_server" (lazy loading)
* Environment: development
* Debug mode: on
* Restarting with stat
* Debugger is active!
* Debugger PIN: 334-263-283
* Running on http://127.0.0.1:5000/ (Натисніть CTRL+C, щоб вийти)
127.0.0.1 - - [10/Jul/2020 09:32:09] "GET / HTTP/1.1" 200 -
document <class 'str'> <!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Date et heure du moment</title>
</head>
<body>
<b>Date et heure du moment : 10/07/20 09:42:33</b>
</body>
</html>
response <class 'flask.wrappers.Response'> <Response 195 bytes [200 OK]>
- рядок 10: бачимо, що тип значення, яке повертає [render_template], — це [str]. Цей рядок символів — це ніщо інше, як документ [date_time_server.html] після інтерпретації (рядки 10–19);
- рядок 20: бачимо, що тип значення, яке повертає [make_response], — це [flask.wrappers.Response]. Функція [Response.__str__] була неявно викликана для відображення об’єкта [Response]. Рядок, повернений цією функцією, містить дві відомості про відповідь HTTP, яка буде сформована:
- розмір надісланого документа становить 195 байт;
- статус відповіді HTTP — [200 OK]. Пізніше ми побачимо, що маємо доступ до цього коду статусу;
22.4. скрипти [flask/03]: веб-сервіси, що генерують текст у вигляді простого тексту
У попередньому прикладі ми бачили, що веб-сервіс повертав такий документ:
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Date et heure du moment</title>
</head>
<body>
<b>Date et heure du moment : {{page.date_heure}}</b>
</body>
</html>
Веб-клієнт може бути зацікавлений лише в інформації [page.date_heure] у рядку 8, а не в оформленні HTML, яке її оточує. Веб-сервіс може надавати цю інформацію у вигляді простого рядка символів. Тут ми наведемо приклади такого типу веб-сервісів.
22.4.1. скрипт [main_01]

- [main_01] — це веб-сервіс;
- [config] — це скрипт конфігурації веб-додатку;
- веб-сервіс використовує деякі з сутностей, визначених у [2];
Скрипт [config] має такий вигляд:
def configure():
# абсолютний шлях, на який посилаються відносні шляхи конфігурації
rootDir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# залежності додатка
absolute_dependencies = [
# Особи, Утиліти, MyException
f"{rootDir}/classes/02/entities",
]
# встановлюємо syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# виконується конфігурація
return {}
Головне завдання цієї конфігурації — визначити шлях до Python для веб-сервісу. Потрібно, щоб система могла знайти об’єкти [2] (рядок 8).
Веб-скрипт [main_01] має такий вигляд:
# налаштовуємо додаток
import config
config=config.configure()
# імпортування
from flask import Flask, make_response
from flask_api import status
# залежності
from Personne import Personne
# додаток Flask (тут немає статичних документів)
app = Flask(__name__)
# Головна URL
@app.route('/')
def index():
# особа
personne = Personne().fromdict({"prénom": "Aglaë", "nom": "de la Hûche", "âge": 87})
# відповідь HTTP
response = make_response(str(personne))
# заголовки HTTP
response.headers.set("Content-type", "application/json; charser=utf8")
# відправляємо відповідь HTTP
return response, status.HTTP_200_OK
# тільки main
if __name__ == '__main__':
# запускаємо сервер
app.config.update(ENV="development", DEBUG=True)
app.run()
- рядки 1–3: встановлюється шлях Python для додатка;
- рядки 5–10: імпортуються елементи, необхідні скрипту;
- рядок 17: веб-сервіс обслуговує лише URL /;
- рядок 20: створюється об’єкт [Personne];
- рядок 22: створюється відповідь HTTP із символьним рядком, що представляє особу. Буде викликано функцію [Personne.__str__]. Вона повертає рядок jSON зі словника [asdict] особи (див. |клас BaseEntity|). Параметром функції [make_response] є текстовий документ, що надсилається клієнту, тобто в даному випадку рядок jSON, що відповідає особі;
- рядок 24: у заголовки HTTP відповіді вставляється заголовок [Content-type], який вказує клієнту, який тип документа він отримає, у даному випадку документ jSON, закодований у форматі UTF-8;
- рядок 26: повертається кортеж із двох елементів:
- відповідь клієнту, заголовки HTTP та документ;
- код статусу відповіді. У цьому випадку потрібно задати код статусу [200 OK]. Різні коди статусу визначаються константами в модулі [flask_api], який імпортується у 7-му рядку;
Модуль [flask_api] не доступний у стандартній комплектації. Його потрібно встановити. Це робиться в терміналі PyCharm:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>pip install flask_api
Collecting flask_api
Downloading Flask_API-2.0-py3-none-any.whl (119 kB)
|| 119 kB 544 kB/s
Requirement already satisfied: Flask>=1.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from flask_api) (1.1.2)
Requirement already satisfied: Jinja2>=2.10.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask>=1.1->flask_api) (2.11.2)
Requirement already satisfied: Werkzeug>=0.15 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask>=1.1->flask_api) (1.0.1)
Requirement already satisfied: click>=5.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask>=1.1->flask_api) (7.1.2)
Requirement already satisfied: itsdangerous>=0.24 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask>=1.1->flask_api) (1.1.0)
Requirement already satisfied: MarkupSafe>=0.23 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Jinja2>=2.10.1->Flask>=1.1->flask_api) (1.1.1
)
Installing collected packages: flask-api
Successfully installed flask-api-2.0
При виконанні веб-скрипта [main_01] у браузері отримуємо такі результати:

- у [2] — отриманий рядок jSON;
- у [3-4] відображається вміст отриманого документа. Бачимо, що немає жодного оформлення HTML, лише рядок jSON;
Тепер розглянемо роль заголовка [Content-Type], надісланого клієнту веб-сервісом. Переводимо браузер у режим розробника (зазвичай F12) і повторно запитуємо той самий URL. Нижче наведено знімок екрана браузера Chrome:

- у [1] виберіть вкладку [Network];
- у [2, 4]: URL, запитаний браузером;
- у [3] виберіть вкладку [Headers] (заголовки HTTP);
- у [5] — код статусу отриманої відповіді HTTP;
- у [6] — заголовок, що повідомляє клієнту про те, що він отримає текст jSON. Це дозволяє клієнту підлаштуватися під відповідь. Таким чином, шрифт, який використовує Chrome для відображення відповіді jSON або звичайного текстового повідомлення, відрізняється;

- у [8] слід вибрати вкладку [Response], щоб отримати доступ до документа, надісланого веб-сервісом, у даному випадку — простого рядка jSON;
22.4.2. Postman
[Postman] — це інструмент, який дозволить нам запитувати різні URL веб-додатку. Він дозволяє нам:
- використовувати будь-які URL: вони створюються вручну;
- надсилати запити до веб-сервера за допомогою GET, POST, PUT, OPTIONS…;
- вказати параметри GET або POST;
- встановити заголовки HTTP запиту;
- отримати відповідь у форматі jSON, XML, HTML,
- отримати доступ до заголовків HTTP відповіді. Таким чином, ми отримуємо доступ до повної відповіді HTTP від сервера;
[Postman] — це чудовий навчальний інструмент для розуміння взаємодії «клієнт-сервер» у протоколі HTTP.
[Postman] доступний на URL [https://www.getpostman.com/downloads/]. Проведіть встановлення вашої версії [Postman]. Під час встановлення вас попросять створити обліковий запис: він тут не знадобиться. Обліковий запис [Postman] призначений для синхронізації різних пристроїв, щоб налаштування одного з них були скопійовані на інший. У даному випадку це не потрібно.
Після встановлення [Postman] відображає такий інтерфейс:

- у [2-3] можна отримати доступ до налаштувань продукту;

- у [6] — версія, що використовується в цьому документі;
Тут ми будемо використовувати [Postman] для тестування попереднього веб-сервісу jSON:
- виконуємо скрипт [flask/03/main_01];
- потім за допомогою Postman надсилаємо запит на URL [http://localhost:5000/];
- у [1] створюємо запит;
- у [2] це буде запит HTTP GET;
- у [3] — відповідь URL від запитуваного веб-сервісу;
- у [4] запит надсилається до веб-сервісу;
- у [5] вибираємо вкладку [Body], на якій відображається отриманий документ;
- у [6] вибираємо вкладку [Pretty], яка відображає отриманий документ із відповідним форматуванням, у даному випадку форматуванням, що відповідає рядку jSON;
- у [7] — отриманий документ jSON;
- у [8-9] — отриманий документ без форматування;
- у [10] відображаються заголовки HTTP, отримані через Postman;
- у [11] — статус HTTP отриманої відповіді;
- у [12] — отримані заголовки HTTP;
- у [13] — заголовок [Content-type], завдяки якому Postman дізнався, що отримає рядок jSON. Postman використав цю інформацію, щоб певним чином оформити отриманий документ;
Існує ще один спосіб використання Postman. Він полягає у використанні консолі Postman (Ctrl-Alt-C). Вона дозволяє переглядати діалог між клієнтом і сервером. Окрім комбінації клавіш Ctrl-Alt-C, консоль Postman доступна через іконку в нижньому лівому куті головного вікна Postman:

Консоль Postman зберігає обмін даними між клієнтом і сервером, що відбувається під час виконання запиту Postman:

- у [3] — список запитів, зроблених Postman з моменту його запуску. Найсвіжіші запити знаходяться внизу списку;
- у [4] — запит HTTP, зроблений Postman;
- у [5-6] — відповідь HTTP, надана веб-сервером;
- у [7] можна переглянути журнали у режимі [raw], тобто без будь-яких візуальних ефектів;
У режимі [raw] вікно консолі виглядає так:

- у [8] — запит HTTP, зроблений Postman до веб-сервера;
- у [9] — відповідь HTTP, надана веб-сервером;
- у [10] можна повернутися до режиму [pretty logs];
Для зручності пояснень ми пронумеруємо рядки, отримані з консолі Postman.
Для клієнта:
Для сервера:
Відтепер ми будемо використовувати переважно:
- [Postman] як веб-клієнт;
- консоль [Postman] та [raw mode] для пояснення діалогу між клієнтом та сервером;
22.4.3. скрипт [main_02]

Веб-скрипт [main_02] має такий вигляд:
# налаштовуємо додаток
import config
config=config.configure()
# імпортування
from flask import Flask, make_response
from flask_api import status
# залежності
from Personne import Personne
# додаток Flask
app = Flask(__name__)
# Головна URL
@app.route('/')
def index():
# особа
personne = Personne().fromdict({"prénom": "Aglaë", "nom": "de la Hûche", "âge": 87})
# вміст
response = make_response(f"personne[{personne.prénom}, {personne.nom}, {personne.âge}]")
# заголовки HTTP
response.headers.set("Content-Type", "text/plain; charset=utf8")
# відповідь HTTP
return response, status.HTTP_200_OK
# тільки основна частина
if __name__ == '__main__':
# запускаємо сервер
app.config.update(ENV="development", DEBUG=True)
app.run()
- скрипт [main_02] аналогічний скрипту [main_01]. Він відрізняється від нього у двох моментах:
- рядок 22: документ, що надсилається клієнту, є необробленим рядком символів, а не рядком jSON;
- рядок 24: це відображається в заголовку HTTP [Content-Type], який вказує тип [text/plain] для документа;
Ми запускаємо веб-скрипт [main_02], а потім використовуємо [Postman] для його запиту:

- у [1-3] надсилається запит до веб-сервісу;
- у [5] — статус відповіді OK;
- у [4, 6] — заголовки HTTP відповіді;
- у [7] — заголовок [Content-Type];
- у [8-10] — документ, надісланий веб-сервісом, — рядок символів;
Консоль Postman видає такі логи:
Запит клієнта:
Відповідь сервера:
HTTP/1.0 200 OK
Content-Type: text/plain; charset=utf8
Content-Length: 34
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Mon, 13 Jul 2020 17:34:22 GMT
personne[Aglaë, de la Hûche, 87]
22.4.4. скрипт [main_03]

Веб-скрипт [main_03] має такий вигляд:
# налаштування програми
import config
config = config.configure()
# імпорт
from flask import Flask, make_response
from flask_api import status
# залежності
from MyException import MyException
from Personne import Personne
# додаток Flask
app = Flask(__name__)
# Головна URL
@app.route('/')
def index():
# неправильна особа
msg_erreur = None
try:
personne = Personne().fromdict({"prénom": "", "nom": "", "âge": 87})
except MyException as erreur:
msg_erreur = f"{erreur}"
# помилка?
if msg_erreur:
response = make_response(msg_erreur)
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
else:
response = make_response(f"personne[{personne.prénom}, {personne.nom}, {personne.âge}]")
status_code = status.HTTP_200_OK
# заголовки HTTP
response.headers.set("Content-Type", "text/plain; charset=utf8")
# відповідь HTTP
return response, status_code
# тільки основний
if __name__ == '__main__':
# запускаємо сервер
app.config.update(ENV="development", DEBUG=True)
app.run()
- рядок 23: виникає помилка через створення екземпляра неправильної особи;
- рядки 27–29: через цю помилку:
- рядок 28: готується відповідь HTTP, що містить повідомлення про помилку;
- рядок 29: коду статусу HTTP присвоюється значення помилки [500 Internal Server Error];
- рядок 34: клієнту повідомляється, що йому надсилається текст у вигляді простого тексту;
- рядок 36: клієнту надсилається відповідь HTTP;
Ми запускаємо веб-сервіс [main_03] і використовуємо Postman для його запиту:

- у [1-3] ми надсилаємо запит;
- у [4] отримуємо відповідь із кодом статусу [500 INTERNAL SERVER ERROR];
- у [5-7]: відповідь — це текст, що описує помилку, яка сталася;

- при запиті [8-10] — заголовки HTTP у відповіді веб-сервісу;
У консолі Postman результати в режимі [raw] такі:
Запит клієнта:
Відповідь сервера:
HTTP/1.0 500 INTERNAL SERVER ERROR
Content-Type: text/plain; charset=utf8
Content-Length: 74
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Mon, 13 Jul 2020 17:39:24 GMT
MyException[11, Le prénom doit être une chaîne de caractères non vide]
22.5. Скрипти [flask/04]: інформація, вкладена у запит

Скрипт [request_parameters.py] покликаний продемонструвати, що веб-сервіс має доступ до різноманітної інформації, вбудованої у запит веб-клієнта. Код виглядає так:
# імпорт
from flask import Flask, make_response, request
from flask_api import status
# додаток Flask
app = Flask(__name__)
# Головна URL
@app.route('/', methods=['GET', 'POST'])
def index():
# параметри запиту
request_data = {}
request_data["environ"] = f"{request.environ}"
request_data["path"] = request.path
request_data["full_path"] = request.full_path
request_data["script_root"] = request.script_root
request_data["url"] = request.url
request_data["base_url"] = request.base_url
request_data["url_root"] = request.url_root
request_data["accept_charsets"] = request.accept_charsets
request_data["accept_encodings"] = request.accept_encodings
request_data["accept_languages"] = request.accept_languages
request_data["accept_mimetypes"] = request.accept_mimetypes
request_data["args"] = request.args
request_data["content_encoding"] = request.content_encoding
request_data["content_length"] = request.content_length
request_data["content_type"] = request.content_type
request_data["endpoint"] = request.endpoint
request_data["files"] = request.files
request_data["form"] = request.form
request_data["host"] = request.host
request_data["method"] = request.method
request_data["query_string"] = request.query_string.decode()
request_data["referrer"] = request.referrer
request_data["remote_addr"] = request.remote_addr
request_data["remote_user"] = request.remote_user
request_data["scheme"] = request.scheme
request_data["script_root"] = request.script_root
request_data["user_agent"] = f"{request.user_agent}"
request_data["values"] = request.values
# відповідь HTTP
response = make_response(request_data)
# заголовки HTTP
response.headers["Content-Type"] = "application/json; charset=utf-8"
# відправлення відповіді HTTP
return response, status.HTTP_200_OK
# головна
if __name__ == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run()
- рядок 9: ми вносимо зміну. Ми вказуємо, які дієслова дозволено використовувати у запиті клієнта. Postman надає їхній перелік:

Перші два — [GET, POST] — використовуються найчастіше і будуть єдиними, що застосовуватимуться в цьому документі. Повертаючись до рядка 9 коду, параметр [methods] містить перелік методів із наведеного вище списку, дозволених URL. За відсутності цього параметра дозволено лише метод [GET]. Саме так було дотепер;
- рядок 12: ми створимо словник [request_data];
- рядок 13: запит клієнта доступний у попередньо визначеному об’єкті [request], імпортованому в рядку 2, типу [werkzeug.local.LocalProxy]. Наступні рядки отримують різні атрибути цього об’єкта;
- замість того, щоб детально описувати кожен атрибут об’єкта [request], ми виконаємо цей код і подивимося на результати. Тоді ми краще зрозуміємо значення різних атрибутів, що відображаються;
- рядок 42: словник [request_data] буде вмістом відповіді HTTP. Нагадаємо, що це має бути текст. Flask автоматично перетворює словники на рядки jSON;
- рядок 44: клієнту повідомляється, що він отримає jSON;
- рядок 46: клієнту надсилається відповідь;
За допомогою клієнта Postman ми надсилаємо такий запит до попереднього веб-сервісу:

- у [1-2] — надісланий запит;
- у [2] запит налаштований. Параметри додаються до URL у вигляді [ ?param1=valeur1¶m2=valeur2]. Існує два способи введення цих параметрів у Postman:
- ввести їх безпосередньо в URL;
- ввести їх у [3-4];
Обидва способи є рівнозначними;
Ми додаємо до запиту інші параметри:

- у [5-7] ми додаємо параметри в тіло (=body) запиту. Тоді як параметри URL видимі для користувача веб-браузера, ті, що є частиною тіла запиту, не відображаються. Браузер (або, у даному випадку, Postman) надсилає їх на сервер після заголовків HTTP. Запит веб-клієнта тоді має таку саму структуру, як і відповідь веб-сервера: заголовки HTTP, за якими йде документ. Це призведе до появи двох нових заголовків HTTP у запиті клієнта:
- [Content-Type]: клієнт повідомляє серверу, який тип документа він надсилає;
- [Content-Length]: розмір документа в байтах;
- у [6] — кодування, яке слід використовувати для параметрів, заявлених у [7]. Вони можуть кодуватися різними способами. [x-www-form-urlencoded] — це метод, який часто використовують веб-браузери;
Можна побачити запит, який буде згенеровано:

Відповідь на цей запит така:

- у [1-5] отримано рядок jSON [3];
- зазвичай веб-сервіс цікавлять параметри URL [ ?param1=valeur1¶m2=valeur2] та ті, що були передані в тілі запиту (документі). Саме так, як правило, клієнт передає йому інформацію. У [5] видно, що параметри URL доступні в [request.args];
Решта відповіді виглядає так:

- у [9] — атрибути параметрів, вказаних у тілі запиту:
- [content_type] — це тип документа, що супроводжує запит. Ми бачили, що цей документ містив інформацію типу [param=valeur], закодовану у формі [x-www-form-urlencoded]. Отже, Postman згенерував заголовок HTTP [Content-Type], що вказує на характер документа;
- [content_length] — це розмір цього документа в байтах;
- у [10] атрибут [request.environ] містить велику кількість інформації про середовище, в якому обробляється запит клієнта. Більшість цієї інформації міститься в інших атрибутах об’єкта [request];
- у [11] параметри, що містяться в тілі запиту, доступні в атрибуті [request.form];
- в [12] — метод, використаний для надсилання запиту, у даному випадку метод [GET];
- у [13] атрибут [request.values] є словником усіх параметрів — як тих, що містяться в URL, так і тих, що містяться в тілі документа. Щоб отримати параметри запиту, слід використати атрибут:
- [request.args] — для отримання параметрів, що містяться в URL;
- [request.form] — для отримання параметрів, що містяться в тілі документа;
У консолі Postman журнали мають такий вигляд:
Запит клієнта:
- рядок 9: тип документа, надісланого на сервер у рядку 12;
- рядок 11: заголовки HTTP запиту відокремлені від надісланого документа порожнім рядком. Саме так сервер визначає кінець заголовків HTTP клієнта;
- рядок 12: «URL-кодований» документ. Усі символи з діакритичними знаками були піддані кодуванню;
Відповідь клієнта така:
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 2433
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:09:09 GMT
{
"accept_charsets": [],
"accept_encodings": [
[
"gzip",
1
],
[
"deflate",
1
],
[
"br",
1
]
],
"accept_languages": [],
"accept_mimetypes": [
[
"*/*",
1
]
],
"args": {
"param1": "valeur1",
"param2": "valeur2"
},
"base_url": "http://localhost:5000/",
"content_encoding": null,
"content_length": 60,
"content_type": "application/x-www-form-urlencoded",
"endpoint": "index",
"environ": "{'wsgi.version': (1, 0), 'wsgi.url_scheme': 'http', 'wsgi.input': <_io.BufferedReader name=908>, 'wsgi.errors': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='utf-8'>, 'wsgi.multithread': True, 'wsgi.multiprocess': False, 'wsgi.run_once': False, 'werkzeug.server.shutdown': <function WSGIRequestHandler.make_environ.<locals>.shutdown_server at 0x00000173CA6E5160>, 'SERVER_SOFTWARE': 'Werkzeug/1.0.1', 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'PATH_INFO': '/', 'QUERY_STRING': 'param1=valeur1¶m2=valeur2', 'REQUEST_URI': '/?param1=valeur1¶m2=valeur2', 'RAW_URI': '/?param1=valeur1¶m2=valeur2', 'REMOTE_ADDR': '127.0.0.1', 'REMOTE_PORT': 50592, 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '5000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'HTTP_USER_AGENT': 'PostmanRuntime/7.26.1', 'HTTP_ACCEPT': '*/*', 'HTTP_CACHE_CONTROL': 'no-cache', 'HTTP_POSTMAN_TOKEN': 'cbfac6aa-71a0-4076-a0c3-91d36d74a4c0', 'HTTP_HOST': 'localhost:5000', 'HTTP_ACCEPT_ENCODING': 'gzip, deflate, br', 'HTTP_CONNECTION': 'keep-alive', 'CONTENT_TYPE': 'application/x-www-form-urlencoded', 'CONTENT_LENGTH': '60', 'werkzeug.request': <Request 'http://localhost:5000/?param1=valeur1¶m2=valeur2' [GET]>}",
"files": {},
"form": {
"nom": "s\u00e9l\u00e9n\u00e9",
"pr\u00e9nom": "agla\u00eb",
"\u00e2ge": "77"
},
"full_path": "/?param1=valeur1¶m2=valeur2",
"host": "localhost:5000",
"method": "GET",
"path": "/",
"query_string": "param1=valeur1¶m2=valeur2",
"referrer": null,
"remote_addr": "127.0.0.1",
"remote_user": null,
"scheme": "http",
"script_root": "",
"url": "http://localhost:5000/?param1=valeur1¶m2=valeur2",
"url_root": "http://localhost:5000/",
"user_agent": "PostmanRuntime/7.26.1",
"values": {
"nom": "s\u00e9l\u00e9n\u00e9",
"param1": "valeur1",
"param2": "valeur2",
"pr\u00e9nom": "agla\u00eb",
"\u00e2ge": "77"
}
}
- рядки 1–5: заголовки HTTP відповіді, що закінчуються порожнім рядком;
- рядки 41–45: символи з діакритичними знаками пройшли кодування UTF-8;
Якщо тепер використати метод [POST] для надсилання того самого запиту з тими самими параметрами, ми отримаємо ту саму відповідь, за винятком того, що в [12] отримаємо [‘method’ : ‘POST’].
Тож у чому полягає різниця між методами GET та POST? Різниця незначна і зумовлена історичним використанням цих методів браузерами:
- параметри в URL є зручними, оскільки URL із такими параметрами може слугувати посиланням у документі HTML. Користувач також може самостійно змінювати параметри, щоб отримати від сервера інші відповіді. У цьому випадку веб-браузери зазвичай використовують метод [GET], і в запиті, що надсилається до веб-сервера, відсутнє тіло (content_length=0) (немає прихованих параметрів);
- іноді небажано, щоб параметри відображалися в URL. Це стосується, зокрема, паролів, що надсилаються на сервер. Крім того, розмір, який займають параметри в URL, обмежений (URL не може перевищувати певного розміру). Параметри в тілі запиту не мають такого обмеження. Крім того, велика кількість параметрів у URL робить його нечитабельним. Розглянемо типовий випадок форми реєстрації на веб-сайті. Історично, коли сторінки HTML ще не містили JavaScript, браузери надсилали введену інформацію за допомогою POST. Тоді це називали «відправленими значеннями»;
Отже, на початку розвитку веб-програмування:
- методи GET, як правило, асоціювалися із запитом інформації, що надавалася веб-сервером;
- методи POST, як правило, асоціювалися з надсиланням інформації з браузера на сервер. Сервер тоді «збагачувався» цією інформацією;
Згодом з’явився JavaScript. Якщо в попередніх прикладах розробник не мав контролю над процесом (натискання на посилання обов’язково запускало GET, а підтвердження форми обов’язково проходило через POST), то JavaScript повернув їм цей контроль. У цій моделі сторінка HTML пов’язана з кодом JavaScript, який може обійти браузер. Таким чином, клік на посилання може бути перехоплений кодом JavaScript, який потім може виконати код, що надсилає запит на сервер. Цей запит буде непомітним для користувача. Користувач його не побачить. Цей код є веб-клієнтом, і, як ми це робили з Postman, розробник може створити будь-який запит, який забажає. Повертаючись до кліка на посилання, він може виконати POST, тоді як за замовчуванням браузер виконав би GET. Ці зміни зробили відмінності між GET та POST менш суттєвими.
Проте розробники часто дотримуються таких правил:
- GET не повинен змінювати стан сервера. Послідовні запити GET, виконані з тими самими параметрами, що й у URL, повинні повертати той самий документ. Крім того, GET найчастіше не має тіла (не пов’язаний з документом), а лише параметри в URL;
- POST може змінювати стан сервера. Параметри найчастіше надсилаються у тілі запиту. У цьому випадку йдеться про «послані» значення. Найкраще це ілюструє приклад форми: значення, введені користувачем, будуть розміщені в тілі запиту POST, а сервер збереже їх десь, найчастіше в базі даних;
У подальшій частині документа ми не дотримуємося жодних конкретних правил.
22.6. Скрипти [flask-05]: управління пам’яттю користувача
22.6.1. Вступ
У попередніх прикладах «клієнт/сервер» ми мали таку схему роботи:
- клієнт відкриває з’єднання з портом 80 машини веб-сервісу;
- він надсилає текстову послідовність: заголовки HTTP, порожній рядок, [document];
- у відповідь сервер надсилає послідовність того самого типу;
- сервер закриває з’єднання з клієнтом;
- клієнт закриває з’єднання з сервером;
Якщо той самий клієнт незабаром надсилає новий запит до веб-сервера, між клієнтом і сервером встановлюється нове з’єднання. Сервер не може знати, чи клієнт, який підключається, вже відвідував його раніше, чи це перший запит. Між двома з’єднаннями сервер «забуває» свого клієнта. З цієї причини кажуть, що протокол HTTP є безстаномним протоколом. Проте серверу корисно запам’ятовувати своїх клієнтів. Так, якщо додаток є захищеним, клієнт надсилає серверу логін та пароль для автентифікації. Якщо сервер «забуває» свого клієнта між двома з’єднаннями, клієнт муситиме автентифікуватися під час кожного нового з’єднання, що є неприйнятним.
Щоб відстежувати клієнта, сервер може діяти різними способами:
- під час першого запиту клієнта він включає у свою відповідь ідентифікатор, який клієнт повинен потім надсилати йому з кожним новим запитом. Завдяки цьому ідентифікатору, який є унікальним для кожного клієнта, сервер може розпізнати клієнта. Тоді він може вести облік даних для цього клієнта у вигляді пам’яті, однозначно пов’язаної з ідентифікатором клієнта. Саме так, наприклад, працюють сервіси PHP;
- під час першого запиту клієнта сервер включає у свою відповідь не ідентифікатор, а саму пам’ять користувача. На стороні сервера нічого не зберігається. Щоб зберегти свою пам’ять, веб-клієнт повинен надсилати її з кожним новим запитом. Ця пам’ять змінюється (або ні) з кожним новим запитом і повертається (або ні) клієнту. Цей метод використовує фреймворк Flask;
Відмінності між цими двома методами такі:
- метод 1 менше споживає пропускну здатність. Між клієнтом і сервером обмінюється лише ідентифікатор. Коли обсяг пам’яті користувача збільшується, це ніяк не впливає на ідентифікатор, який залишається незмінним. Це не стосується методу 2, де пам’ять користувача обмінюється з кожним запитом і може збільшуватися з кожним новим запитом;
- метод 1 вимагає більше пам’яті. Адже сервер зберігає пам’ять користувача у своїх файлових системах. Якщо користувачів мільйон, це може стати проблемою. Метод 2 нічого не зберігає на сервері;
Технічно в обох методах це відбувається так:
- у відповіді новому клієнту сервер включає заголовок HTTP, [Set-Cookie : MotClé=Identifiant] або [Set-Cookie : mémoire]. У методі 1 це відбувається лише під час першого запиту. У методі 2 — щоразу, коли змінюється пам’ять користувача;
- у своїх запитах клієнт систематично повертає те, що отримав, — ідентифікатор або пам’ять. Він робить це за допомогою заголовка HTTP [Cookie : MotClé=Valeur];
Можна запитати, як сервер дізнається, що має справу з новим клієнтом, а не з тим, що вже відвідував сайт раніше. Про це свідчить наявність заголовка HTTP Cookie у заголовках HTTP клієнта. У нового клієнта цей заголовок відсутній.
Сукупність з’єднань певного клієнта називається сеансом.
Сервер може підтримувати й інші типи пам’яті:

- У [1] пам’ять запиту має особливу структуру. Вона використовується, коли запит веб-клієнта обробляється не одним сервісом (або додатком), а кількома. Щоб передати інформацію сервісу i+1, сервіс i може доповнити оброблюваний запит (request) цією інформацією. Це називається пам’яттю рівня запиту. У цьому документі ми не будемо використовувати цей тип пам’яті;
- у [2, 4] — це пам’ять користувача, яку ми щойно описали. Вона може бути реалізована локально ([2]) або підтримуватися за допомогою клієнта ([4]);
- у [3] пам’ять рівня «додатка» зазвичай є пам’яттю, доступною лише для читання. Вона є спільною для всіх користувачів. У ній часто містяться елементи конфігурації веб-додатка, які є спільними для всіх користувачів додатка. З цим типом пам’яті слід бути обережними: запис у неї повинен відбуватися в той момент, коли користувачі ще не надіслали запитів, найчастіше під час запуску додатка. Потім, коли надходять запити, записати дані в цю пам’ять стає складно. Коли веб-сервер одночасно обслуговує декількох користувачів і двоє з них хочуть записати дані в пам’ять рівня «додатка», існує ризик пошкодження цієї пам’яті. Адже коли користувач 1 почав записувати дані в пам’ять рівня «додатка», його операція може бути перервана ще до того, як він її завершить. У результаті ми отримуємо неповну пам’ять додатка. Оскільки вона є спільною, користувач 2 може прочитати її та отримати некоректний стан;
22.6.2. скрипт [session_scope_01]

Скрипти [session_scope_xx] ілюструють управління пам’яттю користувачів.
Скрипт [session_scope_01] має такий вигляд:
# налаштування програми
import config
config = config.configure()
# залежності
import json
from flask import Flask, make_response, session
from flask_api import status
# додаток Flask
app = Flask(__name__)
# секретний ключ сесії
app.secret_key = config["SECRET_KEY"]
@app.route('/set-session', methods=['GET'])
def set_session():
# додаємо щось у сесію
session['nom'] = 'séléné'
# надсилаємо порожню відповідь
response = make_response()
response.headers['Content-Length'] = 0
return response, status.HTTP_200_OK
@app.route('/get-session', methods=['GET'])
def get_session():
# отримуємо сесію та надсилаємо відповідь
response = make_response(json.dumps({"nom": session['nom']}, ensure_ascii=False))
response.headers['Content-Type'] = 'application/json; charset=utf-8'
return response, status.HTTP_200_OK
# тільки main
if __name__ == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run()
- рядок 11: створюється екземпляр додатка Flask;
- рядок 14: атрибут [secret_key] цього додатка отримує значення, взяте з конфігураційного файлу, що використовується в рядках 1–3. Сесія Flask можлива лише за умови ініціалізації цього атрибута. У нього можна ввести що завгодно. Він слугує для шифрування частини «пам’яті користувача», яка буде надіслана клієнту. Зазвичай туди вказують щось, що важко вгадати. У файлі [config] секретний ключ визначено таким чином:
# повертаємо конфігурацію
config = {
# конфігурація Flask
"SECRET_KEY": "vibnFfrdWYUp?*LQ"
}
- вперше ми визначаємо веб-додаток, який служить для чогось іншого, ніж URL /
- рядок 17: URL [/set-session] слугує для ініціалізації сесії користувача;
- рядок 27: URL [/get-session] слугує для відновлення пам’яті користувача (або сесії користувача);
- рядок 20: ми записуємо щось у пам’ять (= сесію) користувача, в даному випадку — ім’я. Сесія працює приблизно як словник. У сесію не можна додавати що завгодно. Значення, які туди додаються, мають бути перетворені у jSON. Для попередньо визначених типів Python це відбувається без втручання розробника. Для власних об’єктів, яких Python не розпізнає, перетворення jSON потрібно виконати самостійно;
- рядок 22: створюється відповідь HTTP без вмісту (відсутність параметра в make_response);
- рядок 23: повідомляємо клієнту, що він отримає порожній документ (розмір 0 байт);
- рядок 24: клієнту надсилається відповідь HTTP. Отже, URL та [/set-session] лише ініціалізують сесію користувача;
- рядок 27: URL та [/get-session] дозволяють користувачеві дізнатися, що міститься в його сесії;
- рядок 30: створюється відповідь HTTP, що містить рядок jSON із сесії користувача. Тут ми створили рядок jSON самостійно, замість того щоб дозволити Flask згенерувати його. Адже ми не хочемо, щоб символи з діакритичними знаками піддавалися екрануванню (ensure_ascii=False);
- рядок 31: повідомляємо клієнту, що надсилаємо йому jSON;
- рядок 32: надсилаємо відповідь HTTP клієнту;
Мета цього скрипта — продемонструвати, що сесія користувача дозволяє пов’язати його послідовні запити:
- запит 1 вимагатиме URL [/set-session];
- запит 2 вимагатиме URL [/get-session] і отримає ім’я, яке було ініціалізовано запитом 1;
Скрипт [config], який налаштовує скрипти з папки [flask/05], має такий вигляд:
def configure():
# абсолютний шлях, що є відправним для відносних шляхів у конфігурації
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# залежності додатка
absolute_dependencies = [
# Personne, Utils, MyException
f"{root_dir}/classes/02/entities",
]
# встановлюємо syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# виконується конфігурація
config = {
# конфігурація Flask
"SECRET_KEY": "vibnFfrdWYUp?*LQ"
}
return config
Ми запускаємо скрипт [session_scope_01], а потім за допомогою Postman надсилаємо запит на URL та [/set-session]. Перед цим ми перевіримо деякі елементи запиту, який буде зроблено:
![]()
- у [1] переходимо до файлів cookie Postman;
- [2-4] — перевіряємо відомі файли cookie Postman і видаляємо їх усі ([4-5]);
Тепер перевіримо запит HTTP, який буде згенеровано:

- у [9]: частина заголовків HTTP, які Postman додасть до запиту на основі налаштувань, які ми для нього встановили. Ця перевірка дозволяє переконатися, що ви не пропустили жодних параметрів або, навпаки, не залишили зайвих параметрів;
Після цього можна виконати запит:
![]()
Існують різні способи перевірки результату. Можна просто поглянути на головне вікно:

- у [1-2] — запит, надісланий до веб-сервісу;
- у [3-6] — заголовки відповіді HTTP;
- у [4], оскільки в коді не вказано тип відповіді, Flask за замовчуванням використав тип [text/html];
- у [5] клієнт знає, що у відповіді немає документа;
- рядок 6: заголовок [Set-Cookie] був надісланий сервером Flask. Його значення називається сесійним кукі. Воно складається з трьох елементів:
- [session=valeur]: значення представляє дані користувача у зашифрованому вигляді. Ці дані можна розшифрувати (див. |https://blog.miguelgrinberg.com/post/how-secure-is-the-flask-user-session|). Проте через секретний ключ, який використовує сервер, користувач не може змінити отримані дані, щоб потім надіслати їх назад на сервер. Таким чином, коли сервер отримує сесію, він має гарантію, що отримає неушкоджену сесію;
- [HttpOnly]: наявність цього елемента вказує браузеру, який його отримує, що файл cookie не повинен бути доступним для JavaScript, який може міститися на сторінці, що відображається;
- [Path=/] — це шлях, за яким слід повернути сесійний файл cookie, тобто в даному випадку будь-який шлях у веб-додатку. Кожного разу, коли користувач за допомогою клавіатури явно (вводить URL) або неявно (натискає на посилання) запитує URL з цього домену, браузер автоматично поверне сесійний файл cookie, який він отримав;
Недоліком головного вікна є те, що ми не маємо доступу до повного запиту, який призвів до цієї відповіді. Те, що відображається в цьому вікні, може ввести в оману:

- у заголовках HTTP [3-4] представлено [5] — сесійний файл cookie. Можна подумати, що Postman вставив у запит сесійний файл cookie, хоча це не так. Заголовки [3] насправді представляють заголовки HTTP, які будуть надіслані під час наступного запиту, як це наразі налаштовано. Postman щойно отримав сесійний файл cookie, який він надішле під час наступного запиту. Ось чому ми маємо [5];
Можна відкрити діалог «клієнт/сервер» у консолі Postman, натиснувши Ctrl-Alt-C:
GET /set-session HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 3673b73f-7600-4df4-8c4b-c37973e50df8
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 0
Vary: Cookie
Set-Cookie: session=eyJub20iOiJzXHUwMGU5bFx1MDBlOW5cdTAwZTkifQ.Xw6jGQ.y5Icu70wTIN-B0o_hwx0xDH247I; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:32:57 GMT
- рядок 14: сесійний файл cookie, надісланий сервером;
Тепер надішлімо запит із URL [/get-session]:
- рядок 9: клієнт Postman відправив серверу сесійний кукі, який він отримав;
- рядок 18: рядок jSON, надісланий сервером;
Цей приклад ілюструє кілька моментів:
- клієнт Postman повертає сесійний файл cookie, який він отримує від сервера Flask. Веб-браузери завжди діють саме так;
- ми бачимо, що запит 2 [/get-session] дозволив отримати інформацію, створену під час запиту 1 [/set-session]. Отже, тут маємо пам’ять користувача;
- рядки 11–16: сервер Flask не повернув сесійний файл cookie. Це відбувається не завжди. Сервер Flask повертає сесійний файл cookie лише в тому випадку, якщо останній запит змінив пам’ять користувача;
22.6.3. скрипт [session_scope_02]

Скрипт [session_02] має такий вигляд:
# залежності
import os
from flask import Flask, make_response, session
from flask_api import status
# додаток Flask
app = Flask(__name__)
# секретний ключ сеансу
app.secret_key = os.urandom(12).hex()
# Головна URL
@app.route('/', methods=['GET'])
def index():
# ми керуємо трьома лічильниками
if session.get('n1') is None:
session['n1'] = 0
else:
session['n1'] = session['n1'] + 1
if session.get('n2') is None:
session['n2'] = 10
else:
session['n2'] = session['n2'] + 1
if session.get('n3') is None:
session['n3'] = 100
else:
session['n3'] = session['n3'] + 1
# словник лічильників
compteurs = {"n1": session['n1'], "n2": session['n2'], "n3": session['n3']}
# надсилаємо відповідь
response = make_response(compteurs)
response.headers['Content-Type'] = 'application/json; charset=utf-8'
return response, status.HTTP_200_OK
# головна
if __name__ == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run()
- рядок 11: тут секретний ключ генерується за допомогою функції. Перевага цієї функції полягає в тому, що вона генерує складний випадковий рядок символів. Нагадаємо, що змінна [app] є екземпляром класу Flask, створеним у рядку 8;
- рядок 15: цього разу буде лише один маршрут — маршрут «/»;
- рядки 17–29: ми керуємо сесією, що містить три лічильники [n1, n2, n3]. Під час першого виклику користувача [n1, n2, n3]=[0, 10, 100], а потім під час кожного наступного виклику ці лічильники збільшуються на 1;
- рядок 18: під час першого запиту сесія додатка порожня. Вираз [session.get(‘clé’)] повертає значення [None]. Для наступних запитів цей вираз повертатиме значення, пов’язане з ключем;
- рядок 31: ці лічильники поміщаються у словник;
- рядок 33: цей словник є документом відповіді HTTP. Нагадаємо, що Flask автоматично перетворює словники на рядок jSON;
- рядок 34: веб-клієнту повідомляється, що він отримає jSON;
- рядок 35: відповідь HTTP надсилається клієнту;
Запустимо цей скрипт і звернемося до створеного таким чином веб-додатку за допомогою Postman після видалення всіх файлів cookie з клієнта Postman [1-3]:

У консолі Postman обмін даними між клієнтом і сервером відбувається наступним чином:
GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: c7db536d-9352-4aa6-9877-04560e03d935
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 41
Vary: Cookie
Set-Cookie: session=eyJuMSI6MCwibjIiOjEwLCJuMyI6MTAwfQ.Xw6nLg.v49CeDWwqP-6Dp9Qt330GAe-dNA; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:50:22 GMT
{
"n1": 0,
"n2": 10,
"n3": 100
}
- [14] — сесійний файл cookie, надісланий сервером;
- у [18-22] — відповідь сервера у вигляді рядка jSON;
Повторімо той самий запит ще раз. Журнали змінюються наступним чином:
GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 8205ad85-37b3-41f2-a171-70dd3b3a1679
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: session=eyJuMSI6MCwibjIiOjEwLCJuMyI6MTAwfQ.Xw6nLg.v49CeDWwqP-6Dp9Qt330GAe-dNA
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 41
Vary: Cookie
Set-Cookie: session=eyJuMSI6MSwibjIiOjExLCJuMyI6MTAxfQ.Xw6nsw.OuxIQnGhmhSsan5Qu_FL3Iyu-9k; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 06:52:35 GMT
{
"n1": 1,
"n2": 11,
"n3": 101
}
- рядок 9: клієнт Postman повертає отриманий сесійний файл cookie;
- рядок 15: у своїй відповіді сервер надсилає новий сесійний файл cookie, оскільки запит клієнта змінив пам’ять користувача (= сесію);
- рядки 19–23: нові значення лічильників;
22.6.4. скрипт [session_scope_03]
Цей новий скрипт покликаний продемонструвати, що в сесію можна поміщати різні типи даних Python: списки, словники, об’єкти. Єдина умова — об’єкти, що додаються до сесії, мають бути серіалізованими у jSON. Якщо вони не є такими за замовчуванням (списки, словники), то потрібно самостійно виконати перетворення у jSON.
# налаштування програми
import config
config = config.configure()
# залежності
import json
import os
from flask import Flask, make_response, session
from flask_api import status
from Personne import Personne
# додаток Flask
app = Flask(__name__)
# секретний ключ сеансу
app.secret_key = os.urandom(12).hex()
# Головна URL
@app.route('/', methods=['GET'])
def index():
# управління списком
liste = session.get('liste')
if liste is None:
# перший запит
liste = [0, 10, 100]
else:
# наступні запити
for i in range(len(liste)):
liste[i] += 1
# повернення списку в сесію
session['liste'] = liste
# обробка словника
dico = session.get('dico')
if not dico:
# перший запит
dico = {"un": 0, "deux": 10, "trois": 100}
else:
# наступні запити
dico = session['dico']
for key in dico.keys():
dico[key] += 1
# словник повертається в сесію
session['dico'] = dico
# управління особою
personne_json = session.get('personne')
if personne_json is None:
# перший запит
personne = Personne().fromdict({"prénom": "aglaë", "nom": "séléné", "âge": 70})
else:
# наступні запити
personne = Personne().fromjson(personne_json)
personne.âge += 1
# особу повертають у сесію
session['personne'] = personne.asjson()
# словник результатів
résultats = {"liste": liste, "dict": dico, "personne": personne.asdict()}
# надсилання відповіді jSON
response = make_response(json.dumps(résultats, ensure_ascii=False))
response.headers['Content-Type'] = 'application/json; charset=utf-8'
return response, status.HTTP_200_OK
# головна
if __name__ == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run()
- рядки 1–3: налаштовується веб-додаток;
- рядки 5–11: імпортуються залежності;
- рядок 14: створюється екземпляр додатка Flask;
- рядок 17: ініціалізується атрибут [secret_key]. Це дозволяє використовувати сесії;
- рядок 21: єдиний маршрут додатка;
- рядки 23–33: управління списком у сесії. У нього додано елементи, які за замовчуванням можна серіалізувати за допомогою jSON;
- рядки 35–46: управління словником у сесії. У нього додано елементи, які за замовчуванням можна серіалізувати у jSON;
- рядки 48–58: управління записом про особу. Об’єкт [Personne] за замовчуванням не підлягає серіалізації у формат jSON. Тому слід вжити запобіжних заходів;
- рядок 58: використовується метод [BaseEntity.asjson] для збереження в сесії рядка jSON, що відповідає особі. Зауважте, що можна було б використати [personne.asdict], оскільки [personne.asdict] — це словник, що містить значення, які за замовчуванням можна серіалізувати у jSON;
- рядок 55: оскільки ми зберегли рядок jSON у сесії, ми отримуємо особу з неї, використовуючи метод [BaseEntity.fromjson];
- рядок 61: створюється словник [résultats], який буде надіслано клієнту у відповіді. Ми знаємо, що в цьому випадку Flask надсилає рядок jSON із словника. Тому словник повинен містити лише значення, які за замовчуванням можна серіалізувати у формат jSON;
- рядок 64: явно вставляємо рядок jSON зі словника [résultats] у відповідь HTTP. Flask зробив би це за замовчуванням. Проте, знову ж таки за замовчуванням, він використовує параметр [ensure_ascii=True], що нас не влаштовувало;
- рядок 65: клієнту повідомляється, що він отримає jSON;
- рядок 66: ми надсилаємо йому відповідь;
Запускаємо веб-додаток. Видаляємо всі файли cookie з клієнта Postman. Потім клієнт запитує URL [http://localhost:5000]. Діалог клієнт/сервер у консолі Postman виглядає так:
GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 5f8b7c63-aa8a-4429-a2fa-62141423d933
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 135
Vary: Cookie
Set-Cookie: session=.eJw9isEKwyAQRH-lzHkPm15K91dqD2mzBMFq0AgF8d-jsRQG9u3MK1jsO0AKFs1fyMSEPQabOjbOHsKV4GzaFfJgmnr4Sdg0puB9a1EMtmgys959-BjIxWBe3XxWLwNq_39IQ3Q_f5zhnHxdtYs3rqgH4gQvMg.Xw6yGw.Bwpt3q-sH03gFLmg2FIPXV_ZNt8; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 07:36:59 GMT
{"liste": [0, 10, 100], "dict": {"un": 0, "deux": 10, "trois": 100}, "personne": {"prénom": "aglaë", "nom": "séléné", "âge": 70}}
Ми надсилаємо запит вдруге:
GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 40fd00ea-d45c-46b7-a51e-d4d433a37b5c
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: session=.eJw9isEKwyAQRH-lzHkPm15K91dqD2mzBMFq0AgF8d-jsRQG9u3MK1jsO0AKFs1fyMSEPQabOjbOHsKV4GzaFfJgmnr4Sdg0puB9a1EMtmgys959-BjIxWBe3XxWLwNq_39IQ3Q_f5zhnHxdtYs3rqgH4gQvMg.Xw6yGw.Bwpt3q-sH03gFLmg2FIPXV_ZNt8
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 135
Vary: Cookie
Set-Cookie: session=.eJw9isEKwyAQRH-lzHkP2kupv9LtIW2WIBgNGqEg_nu3seQ0b2Zew-zfCa5hlvqBs5aw5-SLolGuUaETgi-7wD0sqaHPk7BJLilGXdEYW-ZqjNxjWhnuwpiWMB3Ti0Haz6MMMfz9EcM5-LrIT7zZjv4F5NYvOQ.Xw6ydQ.PMWRCqKx9HNnb_DyK-ha-9pCF7M; HttpOnly; Path=/
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Wed, 15 Jul 2020 07:38:29 GMT
{"liste": [1, 11, 101], "dict": {"deux": 11, "trois": 101, "un": 1}, "personne": {"prénom": "aglaë", "nom": "séléné", "âge": 71}}
- рядок 9: клієнт відправляє отриманий сесійний файл cookie;
- рядок 15: сервер надсилає йому інший, оскільки вміст сесії змінився (рядок 19). Нагадаємо, що цей вміст міститься у сесійному файлі cookie у зашифрованому вигляді;
22.7. скрипти [flask/06]: інформація, спільна для всіх користувачів
22.7.1. Вступ
Цей розділ присвячений тому, як керувати інформацією на рівні додатка, тобто тією, що є спільною для всіх користувачів. Зазвичай це дані конфігурації додатка. Ми вже бачили, що веб-додаток може підтримувати різні типи пам’яті:

Тут нас цікавить пам’ять додатка [3].
22.7.2. скрипт [application_scope_01]

Скрипт [application_scope_01] демонструє спосіб управління даними з областю дії «додаток»:
# налаштування програми
import config
config = config.configure()
# залежності
from flask import Flask, make_response
from flask_api import status
# додаток Flask
app = Flask(__name__)
# Головна URL
@app.route('/', methods=['GET'])
def index():
# мета — продемонструвати, що додаток залишається в пам'яті між запитами різних клієнтів
# кожен клієнт взаємодіє з одним і тим самим додатком
# app_infos представляє інформацію на рівні додатка, а не на рівні сеансу
# тобто вона стосується всіх користувачів, а не якогось конкретного
# ця інформація зберігається тут у [config] (не обов’язково)
# словник результатів
résultats = {"config": config}
# відправляється відповідь
response = make_response(résultats)
response.headers['Content-Type'] = 'application/json; charset=utf-8'
return response, status.HTTP_200_OK
# головна
if __name__ == '__main__':
# перевіряється, чи цей код виконується кілька разів
print("application app lancée")
# запускаємо веб-додаток
app.config.update(ENV="development", DEBUG=True)
app.run()
- рядки 1–3: отримуємо словник з конфігурації. Ми покажемо, що код, розташований поза функціями маршрутизації, виконується лише один раз. Додаток Flask залишається в пам’яті. Уся інформація, ініціалізована поза маршрутами, є глобальною для них і, отже, їм відома. Таким чином, словник [config] із рядка 3 буде повернений маршрутом / (рядок 24). Ми покажемо, що всі веб-клієнти отримають один і той самий словник, а отже, він є спільним для всіх клієнтів. Отже, це інформація з областю дії «додаток»;
- рядок 35: додаємо запис у журнал, щоб перевірити, чи код рядків поза функцією маршрутизації (рядки 1–10, 32–38) виконується кілька разів;
Конфігурація [config] має такий вигляд:
def configure():
# повертаємо конфігурацію
config = {
# конфігурація Flask
"SECRET_KEY": "vibnFfrdWYUp?*LQ"
}
return config
Ми запускаємо цей додаток. У консолі PyCharm з’являються такі записи:

- у [1] — початковий запуск програми;
- у [2], оскільки було запрошено режим [Debug], додаток перезапускається в режимі [Debug];
Тепер у браузері (нижче — Chrome) запитуємо URL [http://127.0.0.1:5000/]:

Тепер у браузері Firefox:

Тепер за допомогою клієнта Postman:
Тепер повертаємося до консолі Pycharm: [Run]:

- обидва записи в журналі [1, 2] все ще там, але інших немає, хоча ми бачимо три запити, отримані веб-сервером;
Щоб повністю переконатися, що додаток не перезавантажується з кожним новим запитом, можна встановити лічильник у налаштуваннях і збільшувати його значення з кожним новим запитом. Тоді ми побачимо, що кожен клієнт бачить лічильник у тому стані, в якому його залишив попередній клієнт. Однак нагадаємо, що клієнти не повинні змінювати дані, що належать до області дії додатка, оскільки вони є спільними для всіх клієнтів, і в ситуації, коли сервер одночасно обслуговує декількох клієнтів без гарантії, що запит одного клієнта буде виконано повністю без переривання, клієнт 1, який надіслав запит 1, що був перерваний до його завершення, може залишити спільні дані в пошкодженому стані для наступних клієнтів.
22.7.3. скрипт [application_scope_02]

Скрипт [application_scope_02] зробить те, чого робити не слід: дозволить клієнтам змінювати інформацію, спільну для інших користувачів. Ми поділимо лічильник між користувачами, які будуть його збільшувати. Ми побачимо, що кожен користувач бачить зміни, внесені іншими користувачами до лічильника.
Скрипт має такий вигляд:
# залежності
from flask import Flask, make_response
from flask_api import status
# додаток Flask
app = Flask(__name__)
# дані області дії додатка
config = {
"counter": 0
}
# Головна сторінка URL
@app.route('/', methods=['GET'])
def index():
# мета полягає в тому, щоб показати, що словник [config] є спільним для всіх клієнтів
# веб-додатку
# збільшуємо лічильник
config["counter"] += 1
# надсилається відповідь
response = make_response(config)
response.headers['Content-Type'] = 'application/json; charset=utf-8'
return response, status.HTTP_200_OK
# main
if __name__ == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run()
- рядки 10–12: словник [config], спільний для користувачів. Він містить лічильник;
- рядок 22: щоразу, коли користувач запитує URL /, лічильник у конфігурації збільшується;
- рядки 23–26: рядок jSON зі словника надсилається кожному клієнту;
Запускаємо цей скрипт. Потім запитуємо URL [http://127.0.0.1:5000/] у першому браузері:

Потім те саме робимо за допомогою другого браузера:

Потім втретє — за допомогою Postman:

Бачимо, що кожен клієнт отримує лічильник у тому стані, в якому його залишив попередній клієнт. Отже, вони дійсно мають доступ до однієї й тієї ж інформації.
22.7.4. скрипт [application_scope_03]
Скрипт [application_scope_03] демонструє, чому інформація, якою обмінюються користувачі, має бути доступною лише для читання.

Скрипт має такий вигляд:
# залежності
import threading
from time import sleep
from flask import Flask, make_response
from flask_api import status
# додаток Flask
app = Flask(__name__)
# дані області дії додатка
config = {
"counter": 0
}
# Головна URL
@app.route('/', methods=['GET'])
def index():
# мета полягає в тому, щоб показати, що словник [config] є спільним для всіх клієнтів
# веб-додатку і що він має бути доступний лише для читання
# ім'я потоку
thread_name = threading.current_thread().name
# зчитується лічильник
counter = config["counter"]
print(f"compteur lu : {counter}, par le thread {thread_name}")
# зупинка на 5 секунд — таким чином будуть обслужені інші клієнти
sleep(5)
# збільшуємо лічильник у конфігурації
config["counter"] = counter + 1
# журнал
print(f"compteur écrit : {config['counter']}, par le thread {thread_name}")
# відправляємо відповідь
response = make_response(config)
response.headers['Content-Type'] = 'application/json; charset=utf-8'
return response, status.HTTP_200_OK
# main
if __name__ == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run(threaded=True)
- рядок 43: ми змінили режим виконання веб-додатка. Ми написали [threaded=True], щоб вказати, що додаток має обслуговувати користувачів одночасно. Це здійснюється за допомогою потоків виконання:
- може існувати кілька одночасних потоків виконання, кожен з яких обслуговує одного користувача;
- процесор комп’ютера використовується цими потоками спільно;
- потік може бути перерваний до завершення своєї роботи. Його виконання буде відновлено пізніше;
- рядок 19: функція [index] може виконуватися одночасно кількома потоками;
- рядок 24: отримуємо ім’я потоку, який виконує функцію [index];
- рядок 26: зчитується значення лічильника. Для цілей нашої демонстрації ми розбиваємо інкремент лічильника наступним чином:
- етап 1: зчитування лічильника (наприклад, 1) потоком 1;
- крок 2: позування потоку 1 на 5 секунд (рядок 29). Оскільки потік 1 запросив паузу, процесор передається іншому потоку — потоку 2. Мета полягає в тому, щоб цей новий потік зчитав те саме значення лічильника (=1). Потім він також робить паузу на 5 секунд і втрачає доступ до процесора;
- етап 3: інкрементування лічильника, рядок 31, виходячи зі значення, зчитаного на етапі 1 (=1). Потік 1 робить це першим: він змінює значення лічильника на 2, а потім завершує виконання функції [index]. Потім настає черга потоку 2 прокинутися і також перевести лічильник на 2, виходячи зі значення, зчитаного на етапі 1 (=1). У підсумку, після виконання обох потоків, лічильник дорівнює 2, хоча мав би дорівнювати 3;
- рядок 33: для перевірки виводимо значення лічильника;
Ми запускаємо скрипт, а потім звертаємося до URL-адреси [http://loaclhost :5000/] у двох браузерах, а потім за допомогою Postman. Журнали в консолі PyCharm виглядають наступним чином:
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/flask/06/application_scope_03.py
* Serving Flask app "application_scope_03" (lazy loading)
* Environment: development
* Debug mode: on
* Restarting with stat
* Debugger is active!
* Debugger PIN: 334-263-283
* Running on http://127.0.0.1:5000/ (Натисніть CTRL+C, щоб вийти)
compteur lu : 0, par le thread Thread-2
compteur lu : 0, par le thread Thread-4
compteur écrit : 1, par le thread Thread-2
127.0.0.1 - - [16/Jul/2020 08:55:37] "GET / HTTP/1.1" 200 -
compteur écrit : 1, par le thread Thread-4
127.0.0.1 - - [16/Jul/2020 08:55:40] "GET / HTTP/1.1" 200 -
compteur lu : 1, par le thread Thread-5
compteur écrit : 2, par le thread Thread-5
127.0.0.1 - - [16/Jul/2020 08:55:46] "GET / HTTP/1.1" 200 -
- рядки 9–10: перші два потоки (2 і 4) зчитують однакове значення лічильника — 0;
- рядок 11: потік 2 змінює значення лічильника на 1;
- рядок 13: потік 4 змінює значення лічильника на 1. Відтепер значення лічильника є неправильним;
- рядки 15–16: потік 5 не переривається і правильно обробляє значення лічильника;
З цього прикладу слід винести, що код веб-додатку не повинен змінювати значення інформації, якою діляться користувачі.
22.8. скрипти [flask/07]: управління маршрутами

Тут ми розглядаємо управління маршрутами додатка, тобто URL, що обслуговуються веб-додатком.
22.8.1. Скрипт [main_01]: налаштовані маршрути
Скрипт [main_01] надає можливість налаштовувати маршрути:
from flask import Flask, make_response
from flask_api import status
# додаток Flask
app = Flask(__name__)
# відправлення відповіді
def send_plain_response(réponse: str):
# відправляємо відповідь
response = make_response(réponse)
response.headers['Content-Type'] = 'text/plain; charset=utf-8'
return response, status.HTTP_200_OK
# /прізвище/ім’я
@app.route('/<string:nom>/<string:prenom>', methods=['GET'])
def index(nom, prenom):
# відповідь
return send_plain_response(f"{prenom} {nom}")
# ініціалізація сесії
@app.route('/init-session/<string:type>', methods=['GET'])
def init_session(type: str):
# відповідь
return send_plain_response(f"/init-session/{type}")
# аутентифікація-користувача
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur():
# відповідь
return send_plain_response("/authentifier-utilisateur")
# розрахунок податку
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot():
# відповідь
return send_plain_response("/calculer-impot")
# перелік-симуляцій
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations():
# відповідь
return send_plain_response("/lister-simulations")
# видалити-симуляцію
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int):
# відповідь
return send_plain_response(f"/supprimer-simulation/{numero}")
# завершення-сесії
@app.route('/fin-session', methods=['GET'])
def fin_session():
# відповідь
return send_plain_response(f"/fin-session")
# головна
if __name__ == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run()
- рядок 17: вказується тип параметрів URL. Це дозволяє Flask виконувати перевірки. Якщо параметр не відповідає очікуваному типу, запит клієнта буде відхилено (помилка 400 Bad Request). Таким чином, Flask виконує частину роботи, яку ми мали б зробити самі;
- рядок 18: для параметрів потрібно вказати точні назви параметрів із рядка 17, але не обов’язково дотримуватися їхнього порядку;
- рядок 20: використовуємо функцію [send_plain_response] для відправлення відповіді веб-клієнту;
- рядок 9: функція [send_plain_response] отримує рядок символів, який потрібно надіслати клієнту;
- рядок 11: формується тіло відповіді HTTP;
- рядок 12: клієнту повідомляється, що йому надсилається текст у вигляді простого тексту;
- рядок 13: надсилається відповідь HTTP;
- рядки 23–62: інші налаштовані маршрути, які будуть використані пізніше в практичному завданні;
Запускаємо скрипт і надсилаємо запит за допомогою клієнта Postman:

22.8.2. скрипт [main_02]: винесення маршрутів
У попередньому скрипті [main_01] обсяг коду може стати значним, якщо маршрутів багато. Скрипт [main_02] демонструє, як винести маршрути в окремий файл.

Скрипт [routes_02] об’єднує функції, пов’язані з маршрутами, з попереднього скрипта:
from flask import make_response
from flask_api import status
def send_response(réponse: str):
# відправляємо відповідь
response = make_response(réponse)
response.headers['Content-Type'] = 'text/plain; charset=utf-8'
return response, status.HTTP_200_OK
# Головна URL
def index(nom, prenom):
# відповідь
return send_response(f"{prenom} {nom}")
# ініціалізація сесії
def init_session(type: str):
# відповідь
return send_response(f"/init-session/{type}")
# аутентифікація-користувача
def authentifier_utilisateur():
# відповідь
return send_response("/authentifier-utilisateur")
# розрахувати-податок
def calculer_impot():
# відповідь
return send_response("/calculer-impot")
# перелік-симуляцій
def lister_simulations():
# відповідь
return send_response("/lister-simulations")
# видалити-симуляцію
def supprimer_simulation(numero: int):
# відповідь
return send_response(f"/supprimer-simulation/{numero}")
# завершення сеансу
def fin_session():
# відповідь
return send_response(f"/fin-session")
Слід зауважити, що скрипт [routes_02] не є скриптом маршрутів. Це перелік функцій. Саме головний скрипт [main_02] встановлює зв’язок між маршрутами та функціями:
from flask import Flask
# переносимо функції маршрутів у власний скрипт
import routes_02
# додаток Flask
app = Flask(__name__)
# зв'язки «маршрути» / «функції»
app.add_url_rule('/<string:nom>/<string:prenom>', methods=['GET'], view_func=routes_02.index)
app.add_url_rule('/init-session/<string:type>', methods=['GET'], view_func=routes_02.init_session)
app.add_url_rule('/authentifier-utilisateur', methods=['POST'], view_func=routes_02.authentifier_utilisateur)
app.add_url_rule('/calculer-impot', methods=['POST'], view_func=routes_02.calculer_impot)
app.add_url_rule('/lister-simulations', methods=['GET'], view_func=routes_02.lister_simulations)
app.add_url_rule('/supprimer-simulation/<int:numero>', methods=['GET'], view_func=routes_02.supprimer_simulation)
app.add_url_rule('/fin-session', methods=['GET'], view_func=routes_02.fin_session)
# main
if __name__ == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run()
- рядок 4: імпортується скрипт функцій, пов’язаних із маршрутами;
- рядки 9–16: прив’язка маршрутів до функцій;
Завдяки цьому методу кожна функція, пов’язана з маршрутом, може, за необхідності, бути предметом окремого скрипта.
Результати такі самі, як і в попередньому скрипті [main_01].