Skip to content

31. Веб-клієнти для служб jSON та XML версії 12

Ми напишемо три консольні клієнтські програми для служб jSON та XML веб-сервера, який ми щойно написали. Ми використовуємо архітектуру «клієнт/сервер» з версії 11:

Image

Ми напишемо три консольні скрипти:

  • скрипти [main] та [main3] використовуватимуть серверний рівень [métier];
  • скрипт [main2] використовуватиме рівень [métier] клієнта;

31.1. Структура каталогів скриптів клієнтів

Папка [http-clients/07] спочатку створюється шляхом копіювання папки [http-clients/06]. Потім вона модифікується.

Image

  • у [1]: дані, що використовуються або створюються клієнтом;
  • у файл [2] — конфігурація та консольні скрипти клієнта;
  • у [3] — рівень [dao] клієнта;
  • у [4] — папка з тестами шару [dao] клієнта;

31.2. Рівень [dao] клієнтів

Image

Image

31.2.1. Інтерфейс

Рівень [dao] реалізує наступний інтерфейс [InterfaceImpôtsDaoWithHttpSession]:


from abc import abstractmethod

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from TaxPayer import TaxPayer

class InterfaceImpôtsDaoWithHttpSession(AbstractImpôtsDao):

    # розрахунок податку за одиницю
    @abstractmethod
    def calculate_tax(self, taxpayer: TaxPayer):
        pass

    # розрахунок податку за партіями
    @abstractmethod
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        pass

    # ініціалізація сеансу
    @abstractmethod
    def init_session(self, type_session: str):
        pass

    # завершення сеансу
    @abstractmethod
    def end_session(self):
        pass

    # аутентифікація
    @abstractmethod
    def authenticate_user(self, user: str, password: str):
        pass

    # перелік моделювань
    @abstractmethod
    def get_simulations(self) -> list:
        pass

    # видалити моделювання
    @abstractmethod
    def delete_simulation(self, id: int) -> list:
        pass

    # отримати дані для розрахунку податку
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass

Кожен метод інтерфейсу відповідає сервісу URL сервера розрахунку податку.

  • рядок 7: інтерфейс розширює клас [AbstractDao], який керує доступом до файлової системи;

Відповідність між методами та службовими URL встановлюється у файлі конфігурації [config]:


        # сервер розрахунку податку
        "server": {
            "urlServer""http://127.0.0.1:5000",
            "user": {
                "login""admin",
                "password""admin"
            },
            "url_services": {
                "calculate-tax""/calculer-impot",
                "get-admindata""/get-admindata",
                "calculate-tax-in-bulk-mode""/calculer-impots",
                "init-session""/init-session",
                "end-session""/fin-session",
                "authenticate-user""/authentifier-utilisateur",
                "get-simulations""/lister-simulations",
                "delete-simulation""/supprimer-simulation"
            }

31.2.2. Реалізація

Інтерфейс [InterfaceImpôtsDaoWithHttpSession] реалізовано за допомогою наступного класу [ImpôtsDaoWithHttpSession]:


# імпорт
import json

import requests
import xmltodict
from flask_api import status

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDaoWithHttpSession import InterfaceImpôtsDaoWithHttpSession
from TaxPayer import TaxPayer

class ImpôtsDaoWithHttpSession(InterfaceImpôtsDaoWithHttpSession):

    # конструктор
    def __init__(self, config: dict):
        # ініціалізація батьківського об’єкта
        AbstractImpôtsDao.__init__(self, config)
        # збереження елементів конфігурації
        # загальна конфігурація
        self.__config = config
        # сервер
        self.__config_server = config["server"]
        # служби
        self.__config_services = config["server"]['url_services']
        # режим налагодження
        self.__debug = config["debug"]
        # реєстратор
        self.__logger = None
        # файли cookie
        self.__cookies = None
        # тип сеансу (json, xml)
        self.__session_type = None

        # етап запиту/відповіді
     def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
        # [method]: метод HTTP, GET або POST
        # [url_service]: сервісний URL
        # [data]: параметри POST у форматі x-www-form-urlencoded
        # [json]: параметри POST у форматі json
        # [cookies]: файли cookie, які слід включити в запит

        # потрібно мати сесію XML або JSON, інакше не вдасться обробити відповідь
        if self.__session_type not in ['json''xml']:
            raise ImpôtsError(73"il n'y a pas de session valide en cours")

        # вхід
        if method == "GET":
            # GET
            response = requests.get(url_service, cookies=self.__cookies)
        else:
            # POST
            response = requests.post(url_service, data=data_value, json=json_value, cookies=self.__cookies)

        # режим налагодження?
        if self.__debug:
            # реєстратор
            if not self.__logger:
                self.__logger = self.__config['logger']
            # запис у журнал
            self.__logger.write(f"{response.text}\n")

        # результат
        if self.__session_type == "json":
            résultat = json.loads(response.text)
        else:  # xml
            résultat = xmltodict.parse(response.text[39:])['root']

        # отримуємо файли cookie з відповіді, якщо вони є
        if response.cookies:
            self.__cookies = response.cookies

        # код статусу
        status_code = response.status_code

        # якщо код статусу відмінний від 200 OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(35, résultat['réponse'])

        # повертаємо результат
        return résultat['réponse']

    def init_session(self, session_type: str):
        # фіксуємо тип сеансу
        self.__session_type = session_type

        # URL-адреса сервісу
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        # виконання запиту
        self.get_response("GET", url_service)

  • рядки 16–34: конструктор класу;
  • рядок 19: ініціалізується батьківський клас;
  • рядки 21–28: зберігаються певні дані конфігурації;
  • рядки 29–34: створюються три властивості, що використовуються в методах класу;
  • рядки 36–82: метод [get_response] виокремлює те, що є спільним для всіх методів шару [dao]: відправлення запиту HTTP та отримання відповіді HTTP від сервера;
  • рядки 38–42: визначення 5 параметрів методу [get_response];
  • рядок 42: слід зауважити, що оскільки сервер підтримує сесію, клієнт повинен зчитувати та відправляти файли cookie;
  • рядки 44–46: перевіряється наявність дійсного поточного сеансу;
  • рядок 51: випадок GET. Відправляються отримані файли cookie;
  • рядок 54: випадок POST. Він може мати два типи параметрів:
    • тип [x-www-form-urlencoded]. Це стосується URL, [/calculer-impot] та [/authentifier-utilisateur]. У цьому випадку використовується параметр [data_value], отриманий методом;
    • тип [json]. Це стосується URL та [/calculer-impots]. У цьому випадку використовується параметр [json_value], отриманий методом;

Тут також повертається сесійний файл cookie.

  • рядки 56–62: якщо ми перебуваємо в режимі [debug], відповідь сервера записується в журнал. Цей запис важливий, оскільки дозволяє точно дізнатися, що саме повернув сервер;
  • рядки 64–68: залежно від того, чи перебуваємо ми в режимі json чи xml, текстова відповідь сервера перетворюється на словник. Розглянемо приклад URL [/init-session]:

Відповідь jSON виглядає так:


2020-08-03 11:45:21.218116, MainThread : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}

Відповідь XML виглядає так:


2020-08-03 11:45:54.671871, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>

Код у рядках 64–68 гарантує, що в обох випадках у [résultat] отримуємо словник із ключами [action, état, réponse];

  • рядки 70–72: якщо відповідь містить файли cookie, їх витягують. Їх потрібно буде надіслати під час наступного запиту;
  • рядки 74–79: якщо статус HTTP відповіді відрізняється від 200, генерується виняток із повідомленням про помилку, що міститься в результаті [’réponse’]. Це може бути одна помилка або список помилок;
  • рядки 81–82: відповідь сервера повертається коду, що викликає;

[init_session]

  • рядок 84: метод [init_session] слугує для визначення типу сесії (json або xml), яку клієнт хоче розпочати з сервером;
  • рядок 86: бажаний тип сесії записується у класі. Адже всі методи потребують цієї інформації для правильного декодування відповіді сервера;
  • рядки 88–90: за допомогою конфігурації додатка визначається сервісний метод URL, до якого слід звернутися;
  • рядок 93: здійснюється запит до сервісу URL. Результат методу [get_response] не отримується:
    • якщо вона генерує виняток, то операція завершилася невдало. Виняток тут не обробляється і передається безпосередньо до коду, що викликає, який потім зупинить клієнт із повідомленням про помилку;
    • якщо виняток не виникає, значить ініціалізація сеансу пройшла успішно;

[authenticate_user]


    def authenticate_user(self, user: str, password: str):
        # URL-адреса сервісу
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['authenticate-user']}"
        post_params = {
            "user": user,
            "password": password
        }

        # виконання запиту
        self.get_response("POST", url_service, post_params)
  • метод [authenticate_user] служить для аутентифікації на сервері. Для цього він отримує облікові дані [user, password] у рядку 1;
  • рядки 2–4: визначається служба URL, до якої слід звернутися;
  • рядки 5–8: параметри POST, оскільки URL [/authentifier-utilisateur] очікує на POST з параметрами [user, password];
  • рядок 11: запит виконується. І тут знову відповідь від сервера не отримується. Саме виняток, згенерований [get_response], вказує, чи операція була успішною чи ні;

[calculate_tax]


    def calculate_tax(self, taxpayer: TaxPayer):
        # URL-адреса сервісу
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['calculate-tax']}"
        # параметри POST
        post_params = {
            "marié": taxpayer.marié,
            "enfants": taxpayer.enfants,
            "salaire": taxpayer.salaire
        }

        # виконання запиту
        response = self.get_response("POST", url_service, post_params)
        # оновлення TaxPayer з відповіддю
        taxpayer.fromdict(response)
  • метод [calculate_tax] дозволяє обчислити податок платника податків [taxpayer], переданого як параметр. Цей параметр змінюється методом (рядок 15) і, отже, є результатом методу;
  • рядки 2–4: визначається службовий URL, до якого слід звернутися;
  • рядки 6–10: параметри запиту, що надсилається. Дійсно, сервіс URL сервісу [/calculer-impot] очікує на POST з параметрами [marié, enfants, salaire];
  • рядки 12–13: запит виконано, отримано відповідь від сервера. Сервіс URL, пов’язаний із [/calculer-impot], повертає словник із ключами [impôt, décôte, surcôte, réduction, taux] щодо податку;
  • рядок 15: отриманий словник [response] використовується для оновлення даних платника податків [taxpayer];

[calculate_tax_in_bulk_mode]


    # розрахунок податку в масовому режимі
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        # дозволяється передавати винятки

        # перетворення платників податків у список словників
        # зберігаються лише властивості [marié, enfants, salaire]
        list_dict_taxpayers = list(
            map(lambda taxpayer:
                taxpayer.asdict(included_keys=[
                    '_TaxPayer__marié',
                    '_TaxPayer__enfants',
                    '_TaxPayer__salaire']),
                taxpayers))

        # URL-адреса сервісу
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['calculate-tax-in-bulk-mode']}"

        # виконання запиту
        list_dict_taxpayers2 = self.get_response("POST", url_service, data_value=None, json_value=list_dict_taxpayers)
        # коли є лише один платник податків і ми перебуваємо в сесії xml, [list_dict_taxpayers2] не є списком
        # у цьому випадку з нього формується список
        if not isinstance(list_dict_taxpayers2, list):
            list_dict_taxpayers2 = [list_dict_taxpayers2]
        # оновлюємо початковий список платників податків отриманими результатами
        for i in range(len(taxpayers)):
            # оновлення списку платників податків [i]
            taxpayers[i].fromdict(list_dict_taxpayers2[i])
        # тут параметр [taxpayers] було оновлено з урахуванням результатів, отриманих від сервера
  • рядок 2: метод отримує список платників податків типу TaxPayer;
  • рядки 7–13: цей список елементів типу [TaxPayer] перетворюється на список словників типу [marié, enfants, salaire];
  • рядки 15–17: встановлюється службовий URL;
  • рядки 19–20: виконується запит POST, тіло якого jSON складається зі списку словників, створеного в рядку 7. Отримується відповідь від сервера;
  • рядки 23–24: тестування виявило проблему, коли сесія має тип XML:
    • якщо початковий список платників податків містить N елементів (N>1), то в результаті отримуємо список із N словників типу [OrderedDict];
    • якщо початковий список містить лише один елемент, то отримуємо не список, а один елемент типу [OrderedDict];
  • рядки 23–24: якщо маємо останній випадок (1 елемент), результат перетворюється на список з 1 елемента;
  • рядки 25–28: цей список отриманих словників містить суму податку кожного платника податків із початкового списку. Тоді кожен із них оновлюється отриманими результатами;

[get_simulations]


    def get_simulations(self) -> list:
        # URL-адреса сервісу
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['get-simulations']}"

        # виконання запиту
        return self.get_response("GET", url_service)
  • рядок 1: метод запитує список симуляцій, виконаних у поточній сесії;
  • рядок 2: метод повертає відповідь сервера;

[delete_simulation]


    def delete_simulation(self, id: int) -> list:
        # URL-адреса сервісу
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['delete-simulation']}/{id}"

        # виконання запиту
        return self.get_response("GET", url_service)
  • рядок 1: метод видаляє симуляцію, ідентифікатор якої передається;
  • рядок 7: метод повертає відповідь сервера — список симуляцій, що залишилися після запитуваного видалення;

[get-admindata]


    def get_admindata(self) -> AdminData:
        # дозволяє передавати винятки

        # URL-адреса сервісу
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['get-admindata']}"

        # виконання запиту
        résultat = self.get_response("GET", url_service)

        # результат — це словник значень типу str, якщо сесія XML
        if self.__session_type == 'xml':
            # новий словник
            résultat2 = {}
            # усе перетворюється на числові значення
            for key, value in résultat.items():
                # деякі елементи словника є списками
                if isinstance(value, list):
                    values = []
                    for value2 in value:
                        values.append(float(value2))
                    résultat2[key] = values
                else:
                    # інші — прості елементи
                    résultat2[key] = float(value)
        else:
            résultat2 = résultat
        # результат типу AdminData
        return AdminData().fromdict(résultat2)
  • рядок 1: метод запитує у сервера податкові константи, необхідні для розрахунку податку;
  • рядок 29: метод повертає тип [AdminData];
  • рядок 9: отримуємо відповідь сервера у вигляді словника. Тестування показує, що виникає проблема, коли сесія має тип XML: замість числових значень елементи словника є символьними рядками. Ми повідомляли про цю проблему під час вивчення модуля [xmltodict] і встановили, що це нормальна поведінка. [xmltodict] не містить інформації про типи у потоці XML, який йому передається. Однак у цьому конкретному випадку необхідно перетворити на числові всі значення отриманого словника. Він містить три списки [limites, coeffr, coeffn] та набір числових властивостей;
  • рядки 13–25: створення словника [résultat2] із числовими значеннями на основі словника [résultat] із значеннями типу «рядок символів»;
  • рядок 29: словник [resultat2] використовується для ініціалізації типу [AdminData];

31.2.3. Фабрика шару [dao]

Наші клієнти будуть багатопотоковими. Оскільки шар [dao] реалізовано класом, що має стан для читання/запису (= властивості для читання/запису), кожен потік повинен мати власний шар [dao], або ж необхідно синхронізувати доступ до спільних даних між потоками. Тут ми обираємо перший варіант. Ми використовуємо клас [ImpôtsDaoWithHttpSessionFactory], здатний створювати екземпляри шару [dao]:


from ImpôtsDaoWithHttpSession import ImpôtsDaoWithHttpSession

class ImpôtsDaoWithHttpSessionFactory:

    def __init__(self, config: dict):
        # зберігається параметр
        self.__config = config

    def new_instance(self):
        # повертається екземпляр шару [dao]
        return ImpôtsDaoWithHttpSession(self.__config)

31.3. Налаштування клієнтів

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ôtsDaoWithHttpSession, ImpôtsDaoWithHttpSessionFactory, InterfaceImpôtsDaoWithHttpSession
        f"{script_dir}/../services",
        # конфігураційні скрипти
        script_dir,
        # Logger
        f"{root_dir}/impots/http-servers/02/utilities",
    ]

    # встановлюємо 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",
        # файл журналів
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",
        # сервер розрахунку податку
        "server": {
            "urlServer""http://127.0.0.1:5000",
            "user": {
                "login""admin",
                "password""admin"
            },
            "url_services": {
                "calculate-tax""/calculer-impot",
                "get-admindata""/get-admindata",
                "calculate-tax-in-bulk-mode""/calculer-impots",
                "init-session""/init-session",
                "end-session""/fin-session",
                "authenticate-user""/authentifier-utilisateur",
                "get-simulations""/lister-simulations",
                "delete-simulation""/supprimer-simulation"
            }
        },
        # режим налагодження
        "debug"True
    }
    )

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

    # визначення конфігурації
    return config

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


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

    # шар [métier]
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    # фабрика шару DAO
    from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
    dao_factory = ImpôtsDaoWithHttpSessionFactory(config)

    # повертаємо конфігурацію шарів
    return {
        "dao_factory": dao_factory,
        "métier": métier
    }
  • клієнти не матимуть прямого доступу до шару [dao]. Щоб отримати такий доступ, їм доведеться звертатися через фабрику шару [dao];

31.4. Клієнт [main]

Image

Клієнт [main] дозволяє тестувати URL та [/init-session, /authentifier-utilisateur, /calculer-impots, /fin-session]:


# очікується параметр у форматі JSON або XML
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# налаштовуємо додаток
import config
config = config.configure({"session_type": session_type})

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

# виконання шару [dao] у потоці
# taxpayers — це список платників податків
def thread_function(config: dict, taxpayers: list):
    # отримуємо фабрику шару [dao]
    dao_factory = config['layers']['dao_factory']
    # створюється екземпляр шару [dao]
    dao = dao_factory.new_instance()
    # тип сеансу
    session_type = config['session_type']
    # кількість платників податків
    nb_taxpayers = len(taxpayers)
    # журнал
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    # ініціалізується сесія
    dao.init_session(session_type)
    # аутентифікація
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    # розраховується податок платників податків
    dao.calculate_tax_in_bulk_mode(taxpayers)
    # завершення сесії
    dao.end_session()
    # журнал
    logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")

# список потоків клієнта
threads = []
logger = None
# код
try:
    # реєстратор
    logger = Logger(config["logsFilename"])
    # зберігається у конфігурації
    config["logger"] = logger
    # початковий журнал
    logger.write("début du calcul de l'impôt des contribuables\n")
    # отримуємо фабрику з шару [dao]
    dao_factory = config["layers"]["dao_factory"]
    # створюємо екземпляр шару [dao]
    dao = dao_factory.new_instance()
    # читання даних про платників податків
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # платників податків?
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # розрахунок податку платників податків за допомогою декількох потоків
    i = 0
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        # кожен потік оброблятиме від 1 до 4 платників податків
        nb_taxpayers = min(l_taxpayers - i, random.randint(14))
        # список платників податків, оброблених потоком
        thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
        # збільшуємо значення i для наступного потоку
        i += nb_taxpayers
        # створюється потік
        thread = threading.Thread(target=thread_function, args=(config, thread_taxpayers))
        # додаємо його до списку потоків головного скрипта
        threads.append(thread)
        # запускається потік — ця операція є асинхронною — результат роботи потоку не очікується
        thread.start()
    # головний потік очікує завершення роботи всіх потоків, які він запустив
    for thread in threads:
        thread.join()
    # тут усі потоки завершили свою роботу — кожен з них змінив один або кілька об’єктів [taxpayer]
    # результати записуються у файл jSON
    dao.write_taxpayers_results(taxpayers)
    # кінець
except BaseException as erreur:
    # виведення повідомлення про помилку
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # закриття логгера
    if logger:
        # завершення запису
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # закриття логера
        logger.close()
    # завершено
    print("Travail terminé...")
    # завершення потоків, які можуть ще існувати, якщо виконання було зупинено через помилку
    sys.exit()
  • рядки 4–11: клієнт очікує на параметр, що вказує тип сеансу (json або xml), який слід використовувати з сервером;
  • рядки 13–15: клієнт налаштований;
  • рядки 48–104: цей код відомий. Він використовувався багато разів. Він розподіляє платників податків, для яких потрібно обчислити податок, між кількома потоками;
  • рядок 26: метод [thread_function] — це метод, який виконується кожним потоком для розрахунку податку платників податків, які були йому призначені;
  • рядки 27–30: кожен потік має власний рівень [dao];
  • розрахунок податку відбувається у чотири етапи:
    • рядки 37–38: ініціалізація сеансу (json або xml) із сервером;
    • рядки 39–40: автентифікація на сервері;
    • рядки 41–42: розрахунок податку;
    • рядки 43–44: закриття сесії з сервером;

При виконанні цього коду в режимі [json] отримуємо такі журнали:


2020-08-03 14:28:34.320751, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:28:34.328749, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.328749, Thread-2 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.333592, Thread-3 : début du calcul de l'impôt des 3 contribuables
2020-08-03 14:28:34.368651, Thread-2 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.375699, Thread-1 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.377432, Thread-3 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.385653, Thread-2 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.392656, Thread-1 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.396377, Thread-3 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.406528, Thread-2 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}]}
2020-08-03 14:28:34.413837, Thread-1 : {"action": "calculer-impots", "état": 1500, "réponse": [{"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, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}]}
2020-08-03 14:28:34.416695, Thread-3 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 3}]}
2020-08-03 14:28:34.425747, Thread-2 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.425747, Thread-2 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.428956, Thread-1 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.428956, Thread-1 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.428956, Thread-3 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.428956, Thread-3 : fin du calcul de l'impôt des 3 contribuables
2020-08-03 14:28:34.428956, MainThread : fin du calcul de l'impôt des contribuables

Вище показано хід виконання потоку [Thread-2].

Якщо виконати [main] у режимі XML, журнали матимуть такий вигляд:


2020-08-03 14:32:48.495316, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:32:48.496452, Thread-1 : début du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.498992, Thread-2 : début du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.498992, Thread-3 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:32:48.498992, Thread-4 : début du calcul de l'impôt des 3 contribuables
2020-08-03 14:32:48.538637, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.540783, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.547811, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.547811, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.555184, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.564793, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.564793, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.568333, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.568333, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>2</enfants><salaire>55555</salaire><impôt>2814</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>oui</marié><enfants>2</enfants><salaire>50000</salaire><impôt>1384</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>384</décôte><réduction>347</réduction><id>2</id></réponse></root>
2020-08-03 14:32:48.579205, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>3</enfants><salaire>50000</salaire><impôt>0</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>720</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>non</marié><enfants>2</enfants><salaire>100000</salaire><impôt>19884</impôt><surcôte>4480</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse></root>
2020-08-03 14:32:48.579205, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>non</marié><enfants>3</enfants><salaire>100000</salaire><impôt>16782</impôt><surcôte>7176</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>oui</marié><enfants>3</enfants><salaire>100000</salaire><impôt>9200</impôt><surcôte>2180</surcôte><taux>0.3</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse><réponse><marié>oui</marié><enfants>5</enfants><salaire>100000</salaire><impôt>4230</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>0</décôte><réduction>0</réduction><id>3</id></réponse><réponse><marié>non</marié><enfants>0</enfants><salaire>100000</salaire><impôt>22986</impôt><surcôte>0</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>4</id></réponse></root>
2020-08-03 14:32:48.588051, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>2</enfants><salaire>30000</salaire><impôt>0</impôt><surcôte>0</surcôte><taux>0.0</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>non</marié><enfants>0</enfants><salaire>200000</salaire><impôt>64210</impôt><surcôte>7498</surcôte><taux>0.45</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse><réponse><marié>oui</marié><enfants>3</enfants><salaire>200000</salaire><impôt>42842</impôt><surcôte>17283</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>3</id></réponse></root>
2020-08-03 14:32:48.594058, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-1 : fin du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.595198, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-2 : fin du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.595198, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-3 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:32:48.603351, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.603351, Thread-4 : fin du calcul de l'impôt des 3 contribuables
2020-08-03 14:32:48.603351, MainThread : fin du calcul de l'impôt des contribuables

Вище наведено хід виконання потоку [Thread-2].

31.5. Клієнт [main2]

Image

Клієнт [main2] дозволяє протестувати URL та [/init-session, /authentifier-utilisateur, /get-admindata, /fin-session]:


# очікується параметр у форматі JSON або XML
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# налагоджуємо додаток
import config
config = config.configure({"session_type": session_type})

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

logger = None
# код
try:
    # логгер
    logger = Logger(config["logsFilename"])
    # зберігається у конфігурації
    config["logger"] = logger
    # журнал запуску
    logger.write("début du calcul de l'impôt des contribuables\n")
    # отримуємо фабрику з шару [dao]
    dao_factory = config['layers']['dao_factory']
    # створюємо екземпляр шару [dao]
    dao = dao_factory.new_instance()
    # отримуємо платників податків
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # платників податків?
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # тип сеансу
    session_type = config['session_type']
    # ініціалізується сесія
    dao.init_session(session_type)
    # проводиться аутентифікація
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    # отримано дані податкової служби
    admindata = dao.get_admindata()
    # завершення сесії
    dao.end_session()
    # розрахунок податку платників податків за допомогою шару [métier]
    métier = config['layers']['métier']
    for taxpayer in taxpayers:
        métier.calculate_tax(taxpayer, admindata)
    # результати записуються у файл jSON
    dao.write_taxpayers_results(taxpayers)
 except BaseException as erreur:
     # виведення повідомлення про помилку
     print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # закриття програми ведення журналу
    if logger:
        # завершення запису
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # закриття програми запису
        logger.close()
    # завершено
    print("Travail terminé...")
  • рядки 1–11: отримується параметр [json, xml], який визначає тип сеансу, що має бути встановлений із сервером;
  • рядки 13–15: налаштовується клієнт;
  • рядки 30–33: створюється шар [dao];
  • рядки 34–35: за допомогою цього шару отримуємо список платників податків, для яких потрібно розрахувати податок;
  • далі йдуть чотири етапи взаємодії з сервером;
    • рядки 41–42: запускається сесія з сервером;
    • рядки 43–44: виконується аутентифікація на сервері;
    • рядки 45–46: запитуються у сервера податкові константи, необхідні для розрахунку податку;
    • рядки 47–48: закривається сесія з сервером;
  • рядки 49–52: за допомогою цих констант можна розрахувати податок платників податків за допомогою локального для клієнта шару [métier];
  • рядки 53–54: записуються отримані результати;

Для сеансу XML результати такі:


2020-08-03 14:44:43.194294, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:44:43.231633, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:44:43.240872, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:44:43.250061, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>get-admindata</action><état>1000</état><réponse><limites>9964.0</limites><limites>27519.0</limites><limites>73779.0</limites><limites>156244.0</limites><limites>93749.0</limites><coeffr>0.0</coeffr><coeffr>0.14</coeffr><coeffr>0.3</coeffr><coeffr>0.41</coeffr><coeffr>0.45</coeffr><coeffn>0.0</coeffn><coeffn>1394.96</coeffn><coeffn>5798.0</coeffn><coeffn>13913.7</coeffn><coeffn>20163.4</coeffn><abattement_dixpourcent_min>437.0</abattement_dixpourcent_min><plafond_impot_couple_pour_decote>2627.0</plafond_impot_couple_pour_decote><plafond_decote_couple>1970.0</plafond_decote_couple><valeur_reduc_demi_part>3797.0</valeur_reduc_demi_part><plafond_revenus_celibataire_pour_reduction>21037.0</plafond_revenus_celibataire_pour_reduction><id>1</id><abattement_dixpourcent_max>12502.0</abattement_dixpourcent_max><plafond_impot_celibataire_pour_decote>1595.0</plafond_impot_celibataire_pour_decote><plafond_decote_celibataire>1196.0</plafond_decote_celibataire><plafond_revenus_couple_pour_reduction>42074.0</plafond_revenus_couple_pour_reduction><plafond_qf_demi_part>1551.0</plafond_qf_demi_part></réponse></root>
2020-08-03 14:44:43.269850, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:44:43.269850, MainThread : fin du calcul de l'impôt des contribuables

31.6. Клієнт [main3]

Клієнт [main3] дозволяє тестувати URL та [/init-session, /calculer-impots, /get-simulations, /delete-simulation, /fin-session]:

Image


# очікується параметр у форматі JSON або XML
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# налагоджуємо додаток
import config
config = config.configure({"session_type": session_type})

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

logger = None
# код
try:
    # логгер
    logger = Logger(config["logsFilename"])
    # зберігається у конфігурації
    config["logger"] = logger
    # журнал запуску
    logger.write("début du calcul de l'impôt des contribuables\n")
    # отримуємо фабрику з шару [dao]
    dao_factory = config["layers"]["dao_factory"]
    # створюємо екземпляр шару [dao]
    dao = dao_factory.new_instance()
    # читання даних про платників податків
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # платників податків?
    if not taxpayers:
        raise ImpôtsError(36f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # розрахунок податку платників податків
    # кількість платників податків
    nb_taxpayers = len(taxpayers)
    # журнал
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    # ініціалізація сеансу
    dao.init_session(session_type)
    # аутентифікація
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    # розраховується податок платників податків
    dao.calculate_tax_in_bulk_mode(taxpayers)
    # запит списку моделювань
    simulations = dao.get_simulations()
    # видаляється кожна друга симуляція
    for i in range(len(simulations)):
        if i % 2 == 0:
            # видаляється моделювання
            dao.delete_simulation(simulations[i]['id'])
    # завершення сеансу
    dao.end_session()
    # перегляньте журнали, щоб побачити різні результати (режим debug=True)
except BaseException as erreur:
    # виведення повідомлення про помилку
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # закриття логгера
    if logger:
        # завершення запису в журнал
        logger.write("fin du calcul de l'impôt des contribuables\n")
        # закриття логгера
        logger.close()
    # завершено
    print("Travail terminé...")
  • рядки 1–11: отримуємо тип сеансу з параметрів скрипта;
  • рядки 13–15: налаштовується додаток;
  • рядки 25–50: тут міститься код, який вже пояснювався раніше;
  • рядки 51–52: запитується список симуляцій, виконаних у поточній сесії;
  • рядки 53–57: видаляємо кожну другу симуляцію;
  • рядки 58–59: завершуємо сесію;

Під час сеансу jSON журнали мають такий вигляд:


2020-08-03 15:01:52.702297, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 15:01:52.702297, MainThread : début du calcul de l'impôt des 11 contribuables
2020-08-03 15:01:52.734806, MainThread : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 15:01:52.747961, MainThread : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 15:01:52.765721, MainThread : {"action": "calculer-impots", "état": 1500, "réponse": [{"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, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 5}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 6}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 7}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 8}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 9}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 10}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 11}]}
2020-08-03 15:01:52.785505, MainThread : {"action": "lister-simulations", "état": 500, "réponse": [{"décôte": 0, "enfants": 2, "id": 1, "impôt": 2814, "marié": "oui", "réduction": 0, "salaire": 55555, "surcôte": 0, "taux": 0.14}, {"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 720, "enfants": 3, "id": 3, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.801475, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 720, "enfants": 3, "id": 3, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.810129, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.818803, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.834604, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.843803, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.851855, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}]}
2020-08-03 15:01:52.863165, MainThread : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 15:01:52.863165, MainThread : fin du calcul de l'impôt des contribuables
  • рядок 6: у нас є 11 симуляцій;
  • рядок 12: після різних видалень залишилося лише 5;

31.7. Тестовий клас [Test2HttpClientDaoWithSession]

Image

Клас [Test2HttpClientDaoWithSession] тестує рівень [dao] клієнтів наступним чином:


import unittest

from ImpôtsError import ImpôtsError
from Logger import Logger
from TaxPayer import TaxPayer

class Test2HttpClientDaoWithSession(unittest.TestCase):

    def test_init_session_json(self) -> None:
        print('test_init_session_json')
        erreur = False
        try:
            dao.init_session('json')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # помилок не повинно бути
        self.assertFalse(erreur)

    def test_init_session_xml(self) -> None:
        print('test_init_session_xml')
        erreur = False
        try:
            dao.init_session('xml')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # помилок не повинно бути
        self.assertFalse(erreur)

    def test_init_session_xxx(self) -> None:
        print('test_init_session_xxx')
        erreur = False
        try:
            dao.init_session('xxx')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # має бути помилка
        self.assertTrue(erreur)

    def test_authenticate_user_success(self) -> None:
        print('test_authenticate_user_success')
        # ініціалізація сеансу
        dao.init_session('json')
        # тест
        erreur = False
        try:
            dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # помилки бути не повинно
        self.assertFalse(erreur)

    def test_authenticate_user_failed(self) -> None:
        print('test_authenticate_user_failed')
        # ініціалізація сеансу
        dao.init_session('json')
        # тест
        erreur = False
        try:
            dao.authenticate_user('x''y')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # має бути помилка
        self.assertTrue(erreur)

    def test_get_simulations(self) -> None:
        print('test_get_simulations')
        # ініціалізація сеансу
        dao.init_session('json')
        # аутентифікація
        dao.authenticate_user('admin''admin')
        # розрахунок податку
        # {'одружений': 'так', 'діти': 2, 'зарплата': 55555,
        # 'податок': 2814, 'надбавка': 0, 'знижка': 0, 'зниження': 0, 'ставка': 0,14}
        taxpayer = TaxPayer().fromdict({"marié""oui""enfants"2"salaire"55555})
        dao.calculate_tax(taxpayer)
        # get_simulations
        simulations = dao.get_simulations()
        # перевірки
        # має бути 1 симуляція
        self.assertEqual(1, len(simulations))
        simulation = simulations[0]
        # перевірка розрахованого податку
        self.assertAlmostEqual(simulation['impôt']2815, delta=1)
        self.assertEqual(simulation['décôte']0)
        self.assertEqual(simulation['réduction']0)
        self.assertAlmostEqual(simulation['taux']0.14, delta=0.01)
        self.assertEqual(simulation['surcôte']0)

    def test_delete_simulation(self) -> None:
        print('test_delete_simulation')
        # ініціалізація сеансу
        dao.init_session('json')
        # аутентифікація
        dao.authenticate_user('admin''admin')
        # розрахунок податку
        taxpayer = TaxPayer().fromdict({"marié""oui""enfants"2"salaire"55555})
        dao.calculate_tax(taxpayer)
        # get_simulations
        simulations = dao.get_simulations()
        # delete_simulation
        dao.delete_simulation(simulations[0]['id'])
        # get_simulations
        simulations = dao.get_simulations()
        # перевірка — симуляцій більше не повинно бути
        self.assertEqual(0, len(simulations))
        # видаляємо симуляцію, якої не існує
        erreur = False
        try:
            dao.delete_simulation(100)
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        # має бути помилка
        self.assertTrue(erreur)

if __name__ == '__main__':
    # налагоджуємо додаток
    import config
    config = config.configure({})

    # реєстратор
    logger = Logger(config["logsFilename"])
    # зберігаємо його в конфігурації
    config["logger"] = logger

    # шар [dao]
    dao_factory = config['layers']['dao_factory']
    dao = dao_factory.new_instance()

    # виконуємо тестові методи
    print("tests en cours...")
    unittest.main()
  • Рівень [dao] надсилає запит на сервер, отримує відповідь і форматує її для передачі коду, що викликає. Коли сервер надсилає відповідь із кодом статусу, відмінним від 200, рівень [dao] генерує виняток. Також низка тестів полягає у перевірці, чи відбулося виключення чи ні;
  • рядки 9–18: ініціалізується сесія jSON. Помилок бути не повинно;
  • рядки 20–29: ініціалізується сесія XML. Помилок бути не повинно;
  • рядки 31–40: ініціалізується сесія з неправильним типом. Повинна виникнути помилка;
  • рядки 42–54: відбувається аутентифікація з правильними обліковими даними. Помилки бути не повинно;
  • рядки 56–68: проводиться аутентифікація з неправильними ідентифікаційними даними. Повинна виникнути помилка;
  • рядки 70–92: виконується розрахунок податку, після чого запитується список симуляцій. Повинна бути хоча б одна. Крім того, перевіряється, чи дійсно ця симуляція містить запитуваний податок;
  • рядки 94–119: створюється симуляція, яку потім видаляють. Потім робиться спроба видалити симуляцію, хоча її вже немає. Повинна з’явитися помилка;
  • рядки 121–137: тест запускається як звичайний консольний скрипт;
  • рядки 122–124: налаштовуємо додаток;
  • рядки 126–129: налаштовуємо логер. Це дозволить нам відстежувати логи;
  • рядки 131–133: створюється екземпляр шару [dao], який буде тестуватися;
  • рядки 135–137: запускаються тести;

Результати у консолі такі:


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/07/tests/Test2HttpClientDaoWithSession.py
tests en cours...
test_authenticate_user_failed
..MyException[35, ["Echec de l'authentification"]]
test_authenticate_user_success
test_delete_simulation
MyException[35, ["la simulation n° [100] n'existe pas"]]
test_get_simulations
test_init_session_json
test_init_session_xml
test_init_session_xxx
MyException[73, il n'y a pas de session valide en cours]
----------------------------------------------------------------------
Ran 7 tests in 0.171s

OK

Process finished with exit code 0