Skip to content

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

23.1. Вступ

Тепер повернемося до нашого додатка для розрахунку податку. На його основі ми створимо різні веб-додатки.

У версії 5 нашого практичного завдання дані податкової адміністрації зберігалися в базі даних. Ця версія 5 складалася з двох окремих додатків, які, однак, мали спільні рівні:

  • додаток, що розраховував податок у режимі |batch| для платників податків, зареєстрованих у текстовому файлі;
  • додаток, що розраховував податок в |інтерактивному| режимі для платників податків, інформацію про яких вводили з клавіатури;

Версія 5 програми для розрахунку податку у пакетному режимі (batch) мала таку архітектуру:

Image

Зрештою, веб-версія цієї програми матиме таку архітектуру:

Image

  • веб-клієнт [1] звертається до веб-сервера [2], який, у свою чергу, взаємодіє з SGBD та [3];
  • веб-сервер [2] зберігає рівні [métier], [8] та [dao], [9] початкового додатка;
  • Початковий додаток зберігає свій головний скрипт [4] та свої шари [métier] і [15]. Рівні [métier], [8] та [15] є ідентичними;
  • для взаємодії клієнт/сервер потрібні два додаткові рівні:
    • шар [web] [7], який реалізує веб-додаток;
    • рівень [dao] [5], що є клієнтом веб-додатку [7];

У остаточній версії розрахунок податку пакетним способом може здійснюватися двома способами:

  • бізнес-розрахунок податку виконується на серверному рівні [métier]. Скрипт [main] використовуватиме цей метод;
  • бізнес-розрахунок податку здійснюється на рівні [métier] на стороні клієнта. Цей метод використовуватиме скрипт [main2];

Відтепер ми будемо розробляти кілька клієнт-серверних додатків вищезазначеного типу, кожен з яких ілюструватиме одну або кілька нових технологій веб-розробки.

23.2. Веб-сервер для розрахунку податку

23.2.1. Версія 1

Image

Скрипт [server_01] є наступним веб-додатком:

Image

  • у [1] використовується налаштований URL, у який передаються три значення:
    • [marié] (так / ні) для вказівки, чи перебуває платник податків у шлюбі;
    • [enfants]: кількість дітей платника податків;
    • [salaire] — річний дохід платника податків;
  • у [2] веб-сервер повертає рядок jSON, який вказує суму податку до сплати з розбивкою на окремі складові;

Архітектура додатка така:

Image

  • браузер [1] надсилає запит до сервера [2]. Скрипт [server_01] реалізує рівні [web] та [2] сервера;
  • шари [3-8] — це ті, що вже використовувалися у |версії 5| програми для розрахунку податку. Ми використовуємо їх без змін;
    • шар [métier] [3] визначено |тут|;
    • шар [dao] [4] визначено |тут|;

Веб-додаток [server_01] налаштовується за допомогою трьох скриптів:

  • [config], який налаштовує весь додаток;
  • [config_database], який налаштовує доступ до бази даних. Ми будемо працювати зі скриптами SGBD, MySQL та PostgreSQL;
  • [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",
        # IndexController
        f"{script_dir}/../controllers",
        # скрипти [config_database, config_layers]
        script_dir,
    ]
    # встановлюємо syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # крок 2 ------
    # налаштування програми
    # список користувачів, яким дозволено користуватися додатком
    config['users'] = [
        {
            "login""admin",
            "password""admin"
        }
    ]

    # крок 3 ------
    # налаштування бази даних
    import config_database
    config["database"] = config_database.configure(config)

    # крок 4 ------
    # інстанціювання шарів додатка
    import config_layers
    config['layers'] = config_layers.configure(config)

    # виконується конфігурація
    return config
  • Функція [configure] отримує словник [config] як аргумент (рядок 1) і повертає його як результат (рядок 54) після розширення його вмісту. Вже давно можна було б сказати, що немає необхідності повертати результат [config]. Адже [config] — це посилання на словник, яке викличний код ділить із викликаним кодом. Отже, викликаючий код уже має цю посилання (рядок 1), і немає сенсу повертати її йому ще раз (рядок 54). Тому слід написати:

config=[module].configure(config) (1)

є зайвим. Достатньо написати:


[module].configure(config) (2)

Проте я залишив перший варіант запису, оскільки вважав, що він, можливо, краще ілюструє те, що викликаний код змінює словник [config].

  • рядок 1: словник [config], отриманий функцією [configure], має ключ «sgbd», значення якого береться зі списку [‘mysql’, ‘pgres’]. [mysql] означає, що використовувана база даних управляється MySQL, тоді як «pgres» означає, що використовувана база даних управляється PostgreSQL;
  • рядки 4–27: перелічено всі папки, що містять елементи, необхідні для веб-додатка. Вони стануть частиною Python Path додатка (рядки 30–31);
  • рядки 33–40: доступ до додатка буде надано лише певним користувачам. Тут наведено список з одним користувачем;
  • рядки 43–46: саме скрипт [config_database] формує конфігурацію використовуваної бази даних;
  • рядок 46: конфігурація, створена скриптом [config_database], є словником, який ми зберігаємо в загальній конфігурації під ключем «database»;
  • рядки 48–51: скрипт [config_layers] створює екземпляри шарів веб-додатку. Він повертає словник, який зберігається в загальній конфігурації, пов’язаній із ключем «layers»;

Скрипт [config_database] — це той самий, що вже використовувався у |версії 5|. Наводимо його ще раз для нагадування:


def configure(config: dict) -> dict:
    # конфігурація SQLAlchemy
    from sqlalchemy import create_engine, Table, Column, Integer, MetaData, Float
    from sqlalchemy.orm import mapper, sessionmaker

    # рядки підключення до використовуваних баз даних
    connection_strings = {
        'mysql': "mysql+mysqlconnector://admimpots:mdpimpots@localhost/dbimpots-2019",
        'pgres': "postgresql+psycopg2://admimpots:mdpimpots@localhost/dbimpots-2019"
    }
    # рядок підключення до використовуваної бази даних
    engine = create_engine(connection_strings[config['sgbd']])

    # метадані
    metadata = MetaData()

    # таблиця констант
    constantes_table = Table("tbconstantes", metadata,
                             Column('id', Integer, primary_key=True),
                             Column('plafond_qf_demi_part', Float, nullable=False),
                             Column('plafond_revenus_celibataire_pour_reduction', Float, nullable=False),
                             Column('plafond_revenus_couple_pour_reduction', Float, nullable=False),
                             Column('valeur_reduc_demi_part', Float, nullable=False),
                             Column('plafond_decote_celibataire', Float, nullable=False),
                             Column('plafond_decote_couple', Float, nullable=False),
                             Column('plafond_impot_celibataire_pour_decote', Float, nullable=False),
                             Column('plafond_impot_couple_pour_decote', Float, nullable=False),
                             Column('abattement_dixpourcent_max', Float, nullable=False),
                             Column('abattement_dixpourcent_min', Float, nullable=False)
                             )

    # таблиця податкових шкал
    tranches_table = Table("tbtranches", metadata,
                           Column('id', Integer, primary_key=True),
                           Column('limite', Float, nullable=False),
                           Column('coeffr', Float, nullable=False),
                           Column('coeffn', Float, nullable=False)
                           )
    # маппінги
    from Tranche import Tranche
    mapper(Tranche, tranches_table)

    from Constantes import Constantes
    mapper(Constantes, constantes_table)

    # фабрика сесій
    session_factory = sessionmaker()
    session_factory.configure(bind=engine)

    # сесія
    session = session_factory()

    # зберігаємо певну інформацію та повертаємо її у вигляді словника
    return {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
            "constantes_table": constantes_table, "session": session}

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


def configure(config: dict) -> dict:
    # інстанціювання рівнів додатка
    
    # DAO
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    dao = ImpotsDaoWithAdminDataInDatabase(config)
    
    # бізнес-логіка
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    # інстанції шарів поміщаються у словник, який повертається коду, що викликає
    return {
        "dao": dao,
        "métier": métier
    }
  • рядок 6: рівень [dao] реалізовано з використанням бази даних;
  • [ImpotsDaoWithAdminDataInDatabase] було визначено |тут|;
  • [ImpôtsMétier] було визначено |тут|;

Основний скрипт [server_01] має такий вигляд:


# очікується параметр 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 ImpôtsError import ImpôtsError
from TaxPayer import TaxPayer
import re
from flask import request
from myutils import json_response
from flask import Flask
from flask_api import status

# отримання даних від податкової служби
try:
    # admindata буде даними на рівні додатка, доступними лише для читання
    admindata = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# додаток Flask
app = Flask(__name__)


# Головна сторінка URL: /?marié=xx&enfants=yy&salaire=zz
@app.route('/', methods=['GET'])
def index():
    # спочатку помилок немає
    erreurs = []
    # запит повинен містити три параметри в URL
    if len(request.args) != 3:
        erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")

    # отримуємо сімейний стан у URL
    marié = request.args.get('marié')
    if marié is None:
        erreurs.append("paramètre [marié] manquant")
    else:
        marié = marié.strip().lower()
        erreur = marié != "oui" and marié != "non"
        if erreur:
            erreurs.append(f"paramétre marié [{marié}] invalide")

    # кількість дітей отримується з URL
    enfants = request.args.get('enfants')
    if enfants is None:
        erreurs.append("paramètre [enfants] manquant")
    else:
        enfants = enfants.strip()
        match = re.match(r"^\d+", enfants)
        if not match:
            erreurs.append(f"paramétre enfants [{enfants}] invalide")
        else:
            enfants = int(enfants)

    # отримуємо інформацію про заробітну плату з файлу URL
    salaire = request.args.get('salaire')
    if salaire is None:
        erreurs.append("paramètre [salaire] manquant")
    else:
        salaire = salaire.strip()
        match = re.match(r"^\d+", salaire)
        if not match:
            erreurs.append(f"paramétre salaire [{salaire}] invalide")
        else:
            salaire = int(salaire)

    # неправильні параметри у URL?
    for key in request.args.keys():
        if key not in ['marié', 'enfants', 'salaire']:
            erreurs.append(f"paramètre [{key}] invalide")

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

    # помилок немає, можна працювати
    # розрахунок податку
    taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, admindata)
    # відправляється відповідь клієнту
    return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)


# тільки для тестування
if __name__ == '__main__':
    # запускаємо сервер Flask
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • рядки 1–10: отримуємо параметр, який вказує, який SGBD використовувати;
  • рядки 12–14: маючи цю інформацію, можна налаштувати додаток. Зокрема, формується Python Path;
  • рядки 16–23: маючи новий Python Path, імпортуємо необхідні елементи;
  • рядки 25–31: отримуємо дані від податкової служби, які дозволяють розрахувати податок;
  • рядки 33–34: створюється екземпляр додатка Flask;
  • рядок 38: додаток Flask обслуговує лише URL [/]. Він очікує на URL, налаштований наступним чином [/ ?marié=xx&enfants=yy&salaire=zz] із:
    • xx: так / ні;
    • yy: кількість дітей;
    • zz: річний оклад;
  • рядки 40–89: перевіряється правильність параметрів URL;
  • рядок 41: помилки накопичуються у списку [erreurs];
  • рядок 43: можливо, варто нагадати, що параметри налаштованого об’єкта URL містяться в [request.args] (див. |тут|):
    • об’єкт [request] — це об’єкт Flask, імпортований у рядку 20;
    • об’єкт [request.args] поводиться як словник;
  • рядки 43–44: перевіряється, чи є саме три параметри (ні менше, ні більше);
  • рядки 46–49: перевіряється, чи параметр [marié] присутній у URL;
  • рядки 50–54: якщо він присутній, перевіряється, чи його значення, очищене від пробілів на початку та в кінці, є «yes» або «no»;
  • рядки 56–59: перевіряється, чи параметр [enfants] міститься у URL;
  • рядки 60–66: якщо він присутній, перевіряється, чи його значення є додатним цілим числом;
  • рядок 66: не слід забувати, що параметри URL та їхні значення є символьними рядками. Значення параметра [enfants] перетворюється на «int»;
  • рядки 68–78: для параметра [salaire] виконують ті самі перевірки, що й для параметра [enfants];
  • рядки 81–83: перевіряється, чи в URL немає інших параметрів, крім [‘marié, ‘enfants’, ‘salaire’];
  • рядки 85–89: якщо після всіх цих перевірок список [erreurs] не є порожнім, то цей список помилок надсилається клієнту у вигляді рядка jSON та коду статусу [400 Bad Request];

Оскільки надалі нам часто доведеться надсилати клієнту у відповідь рядок jSON, кілька рядків, необхідних для цього, було винесено в модуль [myutils.py], який ми вже використовували:

Image

Скрипт [myutils.py] набуває такого вигляду:


# імпорт
import json
import os
import sys

from flask import make_response


def set_syspath(absolute_dependencies: list):
    # absolute_dependencies: список абсолютних імен папок

    ….


# генерація відповіді HTTP jSON
def json_response(réponse: dict, status_code: int) -> tuple:
    # тіло відповіді HTTP
    response = make_response(json.dumps(réponse, ensure_ascii=False))
    # тіло відповіді HTTP є частиною jSON
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    # надсилається відповідь HTTP
    return response, status_code
  • рядок 16: функція [json_response] очікує два параметри:
    • [réponse]: словник, з якого потрібно надіслати рядок jSON веб-клієнту;
    • [status_code] — код статусу HTTP відповіді;
  • рядок 18: встановлюємо тіло відповіді jSON;
  • рядок 20: додається заголовок HTTP, який повідомляє веб-клієнту, що він отримає jSON;
  • рядок 22: відправляємо відповідь HTTP коду-викликувачу. Він повинен надіслати її веб-клієнту;

Файл [__init__.py] змінюється наступним чином:


from .myutils import set_syspath, json_response

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


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

Код скрипта [server_01] продовжується наступним чином:



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

    # помилок немає, можна працювати
    # розрахунок податку
    taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, admindata)
    # відправляється відповідь клієнту
    return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)

  • рядок 10: коли ви опинилися тут, очікувані параметри у файлі URL присутні та є правильними;
  • рядок 10: створюється об’єкт [TaxPayer], який моделює платника податків;
  • рядок 11: шару [métier] надається запит на обчислення податку. Нагадаємо, що елементи, обчислені шаром [métier], вставляються в об’єкт [taxpayer], переданий як параметр;
  • рядок 13: відповідь надсилається веб-клієнту у вигляді рядка jSON. Це рядок jSON зі словника. У зв’язку з ключем [result] туди поміщається словник об’єкта [taxpayer]. Сам об’єкт [taxpayer] розмістити не вдалося, оскільки його неможливо серіалізувати у jSON;

Створюємо дві конфігурації виконання: одну для MySQL, іншу — для PostgreSQL:

Image

Ось кілька прикладів виконання (ви запустили програму [server_01] та використану програму SGBD, а потім завантажуєте URL http://localhost:5000/ у браузері):

Image

Image

Ось приклад виконання в консолі Postman:

Image


GET /?mari%C3%A9=xx&enfants=yy&salaire=zz HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: e4c5df8c-4bd6-4250-b789-b7b164db4eff
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 134
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 06:15:44 GMT

{"réponse": {"erreurs": ["paramètre marié [xx] invalide", "paramètre enfants [yy] invalide", "paramètre salaire [zz] invalide"]}}
  • рядок 1: запитується неправильний URL;
  • рядок 10: сервер відповідає зі статусом 400 BAD REQUEST;

23.2.2. Версія 2

Image

У версії 2 сервера обробка URL виділена в окремий модуль [index_controller] [5]:


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

from flask_api import status
from werkzeug.local import LocalProxy


# URL з параметрами: /?одружений=xx&діти=yy&зарплата=zz
def execute(request: LocalProxy, config: dict) -> tuple:
    # додаткові дані
    from TaxPayer import TaxPayer

    # спочатку помилок немає
    erreurs = []
    # запит повинен містити три параметри
    if len(request.args) != 3:
        erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")

    # отримуємо сімейний стан з URL
    marié = request.args.get('marié')
    if marié is None:
        erreurs.append("paramètre [marié] manquant")
    else:
        marié = marié.strip().lower()
        erreur = marié != "oui" and marié != "non"
        if erreur:
            erreurs.append(f"paramétre marié [{marié}] invalide")

    # отримуємо кількість дітей у URL
    enfants = request.args.get('enfants')
    if enfants is None:
        erreurs.append("paramètre [enfants] manquant")
    else:
        enfants = enfants.strip()
        match = re.match(r"^\d+", enfants)
        if not match:
            erreurs.append(f"paramétre enfants {enfants} invalide")
        else:
            enfants = int(enfants)

    # отримуємо заробітну плату з URL
    salaire = request.args.get('salaire')
    if salaire is None:
        erreurs.append("paramètre [salaire] manquant")
    else:
        salaire = salaire.strip()
        match = re.match(r"^\d+", salaire)
        if not match:
            erreurs.append(f"paramétre salaire {salaire} invalide")
        else:
            salaire = int(salaire)

    # інші параметри для URL?
    for key in request.args.keys():
        if not key in ['marié', 'enfants', 'salaire']:
            erreurs.append(f"paramètre [{key}] invalide")

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

    # помилок немає, можна працювати
    # розрахунок податку
    taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, config["admindata"])
    # відправляється відповідь клієнту
    return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
  • рядок 9: функція [execute] отримує два параметри:
    • [request]: запит HTTP від клієнта;
    • [config]: словник конфігурації додатка;

Скрипт [server_02] має такий вигляд:


# очікується параметр 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 ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
import index_controller

# отримання даних від податкової служби
try:
    # admindata буде даними на рівні додатка, доступними лише для читання
    config['admindata'] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# додаток Flask
app = Flask(__name__)


# Головна сторінка URL: /?marié=xx&enfant=yy&salaire=zz
@app.route('/', methods=['GET'])
def index():
    # виконується запит
    résultat, statusCode = index_controller.execute(request, config)
    # надсилається відповідь
    return json_response(résultat, statusCode)


# лише main
if __name__ == '__main__':
    # запускається сервер
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • рядки 36–41: обробка символу «/»;
  • рядок 39: використання функції [IndexController.execute];

Відтепер ми будемо використовувати таку техніку: кожна дорога оброблятиметься власним модулем.

Результати виконання такі самі, як і у версії 1.

23.2.3. Версія 3

Image

У версії 3 вводиться поняття автентифікації.

Скрипт [server_03] виглядає так:


# очікуємо параметр 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 ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
from flask_httpauth import HTTPBasicAuth
import index_controller

# отримання даних від податкової служби
try:
    # config[‘admindata’] буде даними на рівні додатка, доступними лише для читання
    config["admindata"] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# менеджер автентифікації
auth = HTTPBasicAuth()


# метод автентифікації
@auth.verify_password
def verify_credentials(login: str, password: str) -> bool:
    # список користувачів
    users = config['users']
    # перегляд цього списку
    for user in users:
        if user['login'] == login and user['password'] == password:
            return True
    # не знайдено
    return False


# додаток Flask
app = Flask(__name__)


# Головна сторінка URL: /?marié=xx&enfant=yy&salaire=zz
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    # виконуємо запит
    résultat, statusCode = index_controller.execute(request, config)
    # надсилається відповідь
    return json_response(résultat, statusCode)


# лише main
if __name__ == '__main__':
    # запускається сервер
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • рядок 21: імпортується менеджер автентифікації. Існують різні типи автентифікації на веб-сервері. Той, який ми використовуємо тут, називається [HTTP Basic]. Кожен тип автентифікації дотримується певного діалогу «клієнт-сервер»;
  • рядок 33: створюється екземпляр модуля автентифікації;
  • рядок 37: анотація [@auth.verify_password] позначає функцію, яку слід виконати, коли менеджер автентифікації хоче перевірити логін та пароль, надіслані клієнтом, згідно з протоколом [HTTP Basic];
  • рядок 55: анотація [@auth.login_required] позначає маршрут, для якого веб-клієнт повинен пройти автентифікацію. Якщо веб-клієнт ще не надіслав свої облікові дані, веб-сервер автоматично запросить їх у нього згідно з протоколом HTTP basic;

Необхідно встановити модуль [flask_httpauth]:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\01\flask>pip install flask_httpauth
Collecting flask_httpauth
  Downloading Flask_HTTPAuth-4.1.0-py2.py3-none-any.whl (5.8 kB)
Requirement already satisfied: Flask in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from flask_httpauth) (1.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->flask_httpauth) (1.1.0)
Requirement already satisfied: click>=5.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (7.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->flask_httpauth) (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->flask_httpauth) (1.0.1)
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->flask_httpauth) (1.1.1
)
Installing collected packages: flask-httpauth
Successfully installed flask-httpauth-4.1.0

Давайте подивимося, що відбувається в консолі Postman. Вам потрібно:

  • створюєте конфігурацію виконання;
  • запускаєте веб-додаток;
  • запускаєте обраний модуль SGBD;
  • надсилаєте запит на URL [/] за допомогою Postman;

Діалог «клієнт/сервер» у консолі Postman виглядає так:

GET / HTTP/1.1
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: e65e2a28-4fe3-423b-88b3-b3e5a83092b1
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 401 UNAUTHORIZED
Content-Type: text/html; charset=utf-8
Content-Length: 19
WWW-Authenticate: Basic realm="Authentication Required"
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 07:05:37 GMT

Unauthorized Access
  • рядок 10: сервер відповідає, що ми не маємо дозволу на доступ до URL [/];
  • рядок 13: сервер вказує, який протокол автентифікації слід використовувати, в даному випадку — протокол «Basic»;

Можна налаштувати Postman так, щоб він надсилав облікові дані користувача за протоколом Auth Basic:

Image

  • у [6-7] ми вказуємо облікові дані, що містяться у скрипті [config]: Image

    config['users'] = [
        {
            "login": "admin",
            "password": "admin"
        }
    ]

Діалог «клієнт/сервер» у консолі Postman виглядає наступним чином:


GET / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
User-Agent: PostmanRuntime/7.26.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 5ce20822-e87c-4eef-a2f4-b9eaec38d881
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 203
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Fri, 17 Jul 2020 07:20:01 GMT

{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]", "paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
  • рядок 2: клієнт Postman надсилає у зашифрованому вигляді ідентифікатори користувача [admin / admin];
  • рядок 17: сервер відповідає правильно. Він повідомляє про помилки, оскільки не були надіслані параметри [marié, enfants, salaire] (рядок 1), але не повідомляє про помилку автентифікації;

Тепер спробуємо отримати URL за допомогою браузера (нижче — Firefox):

Image

  • як і в Postman, Firefox отримав від сервера відповідь HTTP із заголовками HTTP:
1
2
3
4
HTTP/1.0 401 UNAUTHORIZED
WWW-Authenticate: Basic realm="Authentication Required"

Firefox, як і інші браузери, не припиняє діалог при отриманні цих заголовків. Він запитує у користувача облікові дані, які вимагає сервер. Достатньо ввести вище admin / admin, щоб отримати відповідь від сервера:

Image

23.3. Веб-клієнт сервера розрахунку податків

23.3.1. Вступ

У попередньому розділі веб-клієнтом сервера розрахунку податків був браузер. У цій частині веб-клієнтом буде консольний скрипт. Архітектура виглядає наступним чином:

Image

  • веб-клієнт складається з шарів [1-2];
  • веб-сервер складається з шарів [3-9]. Про це йшлося в попередньому розділі;

Отже, нам потрібно написати шари [1-2].

Рівень [dao] [2] повинен вміти взаємодіяти з веб-сервером [3]. Тепер ми знаємо протокол HTTP і могли б, наприклад, за допомогою вже розглянутого модуля [pycurl], написати скрипт, що взаємодіє з веб-сервером [3]. Однак існують модулі, спеціалізовані на клієнт-серверних діалогах HTTP. Ми використаємо один із них — модуль [requests]:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\01\flask>pip install requests
Collecting requests
  Downloading requests-2.24.0-py2.py3-none-any.whl (61 kB)
     || 61 kB 137 kB/s
Collecting idna<3,>=2.5
  Downloading idna-2.10-py2.py3-none-any.whl (58 kB)
     || 58 kB 692 kB/s
Collecting chardet<4,>=3.0.2
  Downloading chardet-3.0.4-py2.py3-none-any.whl (133 kB)
     || 133 kB 1.3 MB/s
Collecting urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1
  Downloading urllib3-1.25.9-py2.py3-none-any.whl (126 kB)
     || 126 kB 1.1 MB/s
Collecting certifi>=2017.4.17
  Downloading certifi-2020.6.20-py2.py3-none-any.whl (156 kB)
     || 156 kB 1.1 MB/s
Installing collected packages: idna, chardet, urllib3, certifi, requests
Successfully installed certifi-2020.6.20 chardet-3.0.4 idna-2.10 requests-2.24.0 urllib3-1.25.9

Структура скриптів веб-клієнта така:

Image

Скрипт реалізує додаток для розрахунку податку в пакетному режимі, описаний у |версії 1|. Остання версія цього додатка — |версія 5|. Нагадаємо, як він працює:

  • платники податків, для яких буде розраховано податок, зібрані у текстовому файлі [taxpayersdata.txt]:
# дійсні дані: id, одружений, діти, зарплата
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# створюються помилкові рядки
# недостатньо значень
11,12
# неправильні значення
x,x,x,x
  • результати записуються у два файли:
  • текстовий файл [errors.txt] містить помилки, виявлені у файлі платників податків:

Analyse du fichier C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-clients\01\main/../data/input/taxpayersdata.txt

Ligne 15, not enough values to unpack (expected 4, got 2)
Ligne 17, MyException[1, L'identifiant d'une entité <class 'TaxPayer.TaxPayer'> doit être un entier >=0]
  • (продовження)
    • Файл jSON [résultats.json] містить результати розрахунків податку для різних платників податків:

[
  {
    "id": 0,
    "marié": "oui",
    "enfants": 2,
    "salaire": 55555,
    "impôt": 2814,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 0,
    "réduction": 0
  },
  {
    "id": 1,
    "marié": "oui",
    "enfants": 2,
    "salaire": 50000,
    "impôt": 1384,
    "surcôte": 0,
    "taux": 0.14,
    "décôte": 384,
    "réduction": 347
  },

]

23.3.2. Налаштування веб-клієнта

Image

Налаштування здійснюється за допомогою двох скриптів:

  • [config], який забезпечує повну конфігурацію поза рівнями архітектури;
  • [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",
        # ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        # скрипти конфігурації
        script_dir,
    ]

    # встановлюємо syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # крок 2 ------
    # налаштування програми за допомогою констант
    config.update({
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        "server": {
            "urlServer""http://127.0.0.1:5000/",
            "authBasic"True,
            "user": {
                "login""admin",
                "password""admin"
            }
        }
    }
    )

    # крок 3 ------
    # інстанціювання шарів
    import config_layers
    config['layers'] = config_layers.configure(config)

    # застосовуємо конфігурацію
    return config
  • рядок 1: функція [configure] отримує як параметр словник, який потрібно заповнити інформацією про конфігурацію. Він може бути вже заповненим або порожнім. У даному випадку він буде порожнім;
  • рядки 40–42: абсолютні імена трьох текстових файлів, що обробляються шаром [dao];
  • рядки 43–50: пов’язані з ключем [server], інформація, яку повинен знати рівень [dao] про веб-сервер, з яким він має взаємодіяти:
    • рядок 44: URL веб-сервісу;
    • рядок 45: ключ [authBasic] має значення True, якщо доступ до URL вимагає автентифікації типу Basic;
    • рядки 46–49: ідентифікатори користувача, який буде проходити автентифікацію, якщо вона буде потрібна;
  • рядки 56–57: створюються екземпляри шарів, у даному випадку єдиного шару [dao], і посилання на шари вводяться в [config], пов’язаний із ключем [layers];

Скрипт [config_layers] має такий вигляд:


def configure(config: dict) -> dict:
    # інстанціювання шарів додатка

    # шар DAO
    from ImpôtsDaoWithHttpClient import ImpôtsDaoWithHttpClient
    dao = ImpôtsDaoWithHttpClient(config)

    # виконується конфігурація шарів
    return {
        "dao": dao
    }
  • рядок 1: функція [configure] отримує словник, який налаштовує додаток;
  • рядки 4–6: створюється екземпляр шару [dao]. У рядку 6 йому передається конфігурація додатка, в якій він знайде необхідну інформацію;
  • рядки 8–11: повертається словник, до якого додано посилання на шар [dao];

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

Головний скрипт [main] є варіантом скрипта з |версії 5|:


# конфігуруємо додаток
import config
config = config.configure({})

# залежності
from ImpôtsError import ImpôtsError

# код
try:
    # отримання шару [dao]
    dao = config["layers"]["dao"]
    # читання даних платників податків
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # платників податків?
    if not taxpayers:
        raise ImpôtsError(f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # розрахунок податку платників податків
    for taxpayer in taxpayers:
        # «taxpayer» є одночасно вхідним і вихідним параметром
        # «taxpayer» буде змінено
        dao.calculate_tax(taxpayer)
    # запис результатів у текстовий файл
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError  as erreur:
    # виведення повідомлення про помилку
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # завершено
    print("Travail terminé...")
  • рядки 2–3: налаштовано програму;
  • рядок 13: шар [dao] надає список платників податків, для яких потрібно розрахувати податок;
  • рядок 21: шар [dao] обчислює податок для кожного з них;
  • рядок 23: результати записуються у файл jSON;

23.3.4. Реалізація шару [dao]

Image

Повернемося до використовуваної архітектури «клієнт/сервер»:

Image

  • у [2, 6] бачимо, що рівень [dao] виконує дві функції:
    • вона звертається до файлової системи як для зчитування даних платників податків, так і для запису результатів розрахунків податку. У нас уже є клас |AbstractImpôtsDao|, який це вміє робити. Він використовувався ще з |версії 4|;
    • він взаємодіє з веб-сервером [3];

У |версії 5| головний скрипт [main] [1] безпосередньо взаємодіяв із рівнем [métier] [4]. Ми не хочемо змінювати цей скрипт. Для цього ми зробимо так, щоб рівень [dao] [2] реалізував інтерфейс рівня [métier] [4]. Таким чином, головний скрипт [main] буде «відчувати», що взаємодіє безпосередньо з шаром [métier] [4], і зможе повністю ігнорувати той факт, що цей шар розташований на іншій машині.

Визначення класу, що реалізує рівень [dao] [2], може виглядати так:


class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
  • клас [ImpôtsDaoWithHttpClient]:
    • успадковує клас [AbstractImpôtsDao], що дозволить йому керувати взаємодією з файловою системою [6];
    • реалізує інтерфейс [InterfaceImpôtsMétier], щоб не доводилося змінювати головний скрипт [main] |версії 5|;

Повний код класу [ImpôtsDaoWithHttpClient] виглядає наступним чином:


# імпорт
import requests
from flask_api import status

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


class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):

    # конструктор
    def __init__(self, config: dict):
        # ініціалізація батьківського об’єкта
        AbstractImpôtsDao.__init__(self, config)
        # збереження параметрів
        self.__config_server = config["server"]

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

    # розрахунок податку
    def calculate_tax(self: object, taxpayer: TaxPayer, admindata: AdminData = None):
        # винятки передаються нагору
        # параметри get
        params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
        # підключення з аутентифікацією Auth Basic?
        if self.__config_server['authBasic']:
            response = requests.get(
                # URL запитуваного сервера
                self.__config_server['urlServer'],
                # параметри URL
                params=params,
                # аутентифікація Basic
                auth=(
                    self.__config_server["user"]["login"],
                    self.__config_server["user"]["password"]))
        else:
            # підключення без аутентифікації Auth Basic
            response = requests.get(self.__config_server['urlServer'], params=params)
        # перевірка
        print(response.text)
        # код статусу відповіді HTTP
        status_code = response.status_code
        # відповідь jSON додається до словника
        résultat = response.json()
        # помилка, якщо код статусу відмінний від 200 OK
        if status_code != status.HTTP_200_OK:
            # відомо, що помилки були пов’язані з ключем [erreurs] відповіді
            raise ImpôtsError(87, résultat['réponse']['erreurs'])
        # відомо, що результат пов'язано з ключем [result] у відповіді
        # вхідний параметр змінюється з урахуванням цього результату
        taxpayer.fromdict(résultat["réponse"]["result"])
  • рядки 21–23: клас [AbstractImpôtsDao] (рядок 12) має абстрактний метод [get_admindata]. Ми змушені його реалізувати, навіть якщо ним не користуємося (admindata обробляється сервером, а не клієнтом);
  • рядок 26: метод [calculate_tax] належить до інтерфейсу [InterfaceImpôtsMétier] (рядок 12). Ми повинні його реалізувати;
  • рядок 15: конструктор отримує як єдиний параметр словник конфігурації додатка;
  • рядки 16–17: батьківський клас [AbstractImpôtsDao] ініціалізується шляхом передачі йому, також, конфігурації додатка. У ній він знайде імена трьох текстових файлів, якими йому доведеться керувати;
  • рядки 18–19: у класі локально зберігається інформація щодо веб-сервера для розрахунку податку;
  • рядок 26: метод [calculate_tax] отримує як параметр об’єкт типу |Taxpayer|. Щоб відповідати сигнатурі методу [InterfaceImpôtsMétier.calculate_tax], він також отримує параметр [admindata], який повинен інкапсулювати дані податкової адміністрації. На стороні клієнта цих даних немає. Цей параметр завжди залишатиметься рівним [None]. Ця хитрощі свідчить про те, що клас [ImpôtsMétier] спочатку був написаний неправильно:
  • сигнатура [calculate_tax] мала б бути просто:

def calculate_tax(self, taxpayer: TaxPayer)

а параметр [admindata : AdminData] мав би бути переданий у конструктор класу;

  • рядок 27: код методу [calculate_tax] не був інкапсульований у блок try / catch / finally. Це означає, що можливі винятки не будуть оброблені й будуть передані до коду, що викликає, а саме до скрипта [main]. Останній успішно перехоплює всі винятки, що надходять із рівня [dao];
  • рядок 28: розрахунок податку відбувається на стороні сервера. Тому потрібно встановити зв’язок із ним. Це робиться за допомогою модуля [requests], імпортованого у рядку 2;
  • рядки 31–43: щоб надіслати запит GET на веб-сервер, використовується метод [requests.get]:
    • рядки 33–34: першим параметром методу є URL, до якого слід звернутися;
    • рядки 35–40: два інші параметри — це іменовані параметри, порядок яких не має значення;
    • рядки 35–36: значення іменованого параметра [params] має бути словником, що містить інформацію, яку потрібно внести в URL у форматі [/url ?param1=valeur1&param2=valeur2&…];
    • рядок 29: словник, що містить три параметри [marié, enfants, salaire], яких очікує веб-сервер. Не потрібно турбуватися про кодування (так зване urlencoded), якому мають піддаватися ці параметри. Цим займається [requests];
    • рядки 37–40: параметр із назвою [auth] є кортежем із двох елементів (login, password). Він представляє облікові дані для автентифікації типу Basic;
  • рядки 44–45: ці два рядки мають лише навчальну мету (їх буде вилучено у коментарі після завершення налагодження):
    • [response] представляє відповідь HTTP від сервера;
    • [response.text] — це текст документа, вкладений у цю відповідь. На етапі налагодження корисно перевірити, що саме сервер нам надіслав;
  • рядок 47: [response.status_code] — це код статусу HTTP отриманої відповіді. Наш сервер надсилає лише три таких коди:
    • 200 OK
    • 400 BAD REQUEST
    • 500 INTERNAL SERVER ERROR
  • рядок 49: наш сервер завжди надсилає jSON, навіть у разі помилки. Функція [response.json()] створює словник на основі отриманого рядка jSON. Нагадаємо про дві можливі форми рядка jSON:

{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]", "paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
{"réponse": {"result": {"id": 0, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
  • рядки 51–53: якщо код статусу не дорівнює 200, то генерується виняток із повідомленнями про помилки, вкладеними у відповідь;
  • рядок 56: отримуємо словник, сформований під час розрахунку податку, і використовуємо його для оновлення вхідного параметра [taxpayer];

23.3.5. Виконання

Щоб запустити клієнт:

  • запустіть сервер [server_03] із вибраним вами SGBD;
  • запустіть скрипт клієнта [main];

Результати можна знайти в папці [data/output]. Вони такі самі, як і для версії 5.

23.4. Тестування рівня [dao]

Повернемося до архітектури клієнт-серверного додатка:

  • У клієнтському коді нам вдалося забезпечити, щоб рівень [dao] [1] мав той самий інтерфейс, що й рівень [métier] [3]. Отже, у [4] ми будемо використовувати вже розглянутий тестовий клас |TestDaoMétier| для тестування шару [métier] [3];

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

Image

  • конфігурація [2] ідентична конфігурації [1], яку ми щойно розглянули;

Тестовий клас [TestHttpClientDao] має такий вигляд:


import unittest


class TestHttpClientDao(unittest.TestCase):

    def test_1(self) -> None:
        from TaxPayer import TaxPayer

        # {'одружений': 'так', 'діти': 2, 'зарплата': 55555,
        # 'податок': 2814, 'надбавка': 0, 'знижка': 0, 'зниження': 0, 'ставка': 0.14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        # перевірка
        self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)

    

    def test_11(self) -> None:
        from TaxPayer import TaxPayer

        # {'одружений': 'так', 'діти': 3, 'зарплата': 200000,
        # 'податок': 42842, 'надбавка': 17283, 'знижка': 0, 'зниження': 0, 'ставка': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        dao.calculate_tax(taxpayer)
        # перевірки
        self.assertAlmostEqual(taxpayer.impôt, 42842, 1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 17283, delta=1)


if __name__ == '__main__':

    # налаштування програми
    import config
    config = config.configure({})

    # рівень DAO
    dao = config['layers']['dao']

    # виконуємо тестові методи
    print("tests en cours...")
    unittest.main()

Цей клас аналогічний тому, що вже розглядався у версії 4 додатка.

  • рядки 40–41: налаштовується тестове середовище;
  • рядок 44: отримуємо посилання на шар [dao];
  • рядки 47–48: виконуємо тести;

Для виконання тестів створюється |конфігурація виконання|:

Image

  • створюється конфігурація виконання для консольного скрипта, а не для тесту UnitTest;

Під час виконання цієї конфігурації отримуємо такі результати:

C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-clients/01/tests/TestHttpClientDao.py
tests en cours...
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
....{"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}}}
...{"réponse": {"result": {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
{"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}}}
....
{"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}}}
----------------------------------------------------------------------
Ran 11 tests in 0.130s

OK

Process finished with exit code 0

Усі 11 тестів пройшли успішно.