Skip to content

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

28.1. Вступ

У прикладах клієнтів сервера розрахунку податків потоки послідовно надсилали N запитів, якщо їм потрібно було обробити N платників податків. Ідея полягає в тому, щоб надіслати один запит, що охоплює N платників податків. Для кожного з них потрібно надіслати інформацію [marié, enfants, salaire]. Її можна надіслати як параметри:

  • у запиті URL. У цьому випадку ми отримаємо довгий запит URL, який не несе особливої інформації;
  • у тілі (body) запиту HTTP. Відомо, що це тіло приховане від користувача, який використовує браузер;

У обох випадках можна використовувати запит [GET] або [POST]. Ми будемо використовувати запит POST з параметрами, вкладеними в тіло запиту HTTP.

Архітектура «клієнт/сервер» не змінилася:

Image

28.2. Веб-сервер

Image

Папка [http-servers/05] спочатку створюється шляхом копіювання папки [http-servers/02]. Повернемося до обміну даними jSON між клієнтом і сервером. Ми бачили, що перехід від jSON до XML є дуже простим.

28.2.1. Налаштування

Конфігурація [config, config_database, config_layers] залишається аналогічною до конфігурації попередніх версій. Ми не будемо на ній зупинятися.

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

Скрипт [main] ідентичний тому, що міститься у папці [http-servers/02], яку ми скопіювали. Відрізняється лише одне:


# Головна URL
@app.route('/', methods=['POST'])
@auth.login_required
def index():

  • рядок 2: тепер URL отримується через POST;

28.2.3. Контролер [index_controller]

Контролер [index_controller] змінюється наступним чином:


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

import json

from flask_api import status
from werkzeug.local import LocalProxy


def execute(request: LocalProxy, config: dict) -> tuple:
    # залежності
    from ImpôtsError import ImpôtsError
    from TaxPayer import TaxPayer

    # отримано тіло повідомлення — очікується список словників
    msg_erreur = None
    list_dict_taxpayers = None
    # тіло jSON з POST
    request_text = request.data
    try:
        #, який перетворюється на список словників
        list_dict_taxpayers = json.loads(request_text)
    except BaseException as erreur:
        # фіксуємо помилку
        msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
    # чи маємо ми непорожній список?
    if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
        # фіксуємо помилку
        msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
    # чи маємо ми список словників?
    if not msg_erreur:
        erreur = False
        i = 0
        while not erreur and i < len(list_dict_taxpayers):
            erreur = not isinstance(list_dict_taxpayers[i], dict)
            i += 1
        # помилка?
        if erreur:
            msg_erreur = "le corps du POST doit être une liste de dictionnaires"
    # помилка?
    if msg_erreur:
        # відправляється відповідь про помилку клієнту
        résultats = {"réponse": {"erreurs": [msg_erreur]}}
        return résultats, status.HTTP_400_BAD_REQUEST

    # перевіряємо TaxPayers по одному
    # спочатку помилок немає
    list_erreurs = []
    for dict_taxpayer in list_dict_taxpayers:
        # створюється TaxPayer на основі dict_taxpayer
        msg_erreur = None
        try:
            # наступна операція усуне випадки, коли параметри не
            # властивостей класу TaxPayer, а також випадки, коли їхні значення
            # є неправильними
            TaxPayer().fromdict(dict_taxpayer)
        except BaseException as erreur:
            msg_erreur = f"{erreur}"
        # деякі ключі мають бути присутніми у словнику
        if not msg_erreur:
            # ключі [marié, enfants, salaire] повинні бути присутніми у словнику
            keys = dict_taxpayer.keys()
            if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
                msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
        # помилки?
        if msg_erreur:
            # помилка виявлена в самому TaxPayer
            dict_taxpayer['erreur'] = msg_erreur
            # TaxPayer додано до списку помилок
            list_erreurs.append(dict_taxpayer)

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

    # помилок немає, можна продовжувати роботу
    # отримано дані від податкової служби
    admindata = config["admindata"]
    métier = config["layers"]["métier"]
    try:
        # обробляємо TaxPayer по одному
        list_taxpayers = []
        for dict_taxpayer in list_dict_taxpayers:
            # розрахунок податку
            taxpayer = TaxPayer().fromdict(
                {'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
                 'зарплата': dict_taxpayer['salaire']})
            métier.calculate_tax(taxpayer, admindata)
            # результат зберігається у вигляді словника
            list_taxpayers.append(taxpayer.asdict())
        # надсилаємо відповідь клієнту
        return {"réponse": {"results": list_taxpayers}}, status.HTTP_200_OK
    except ImpôtsError as erreur:
        # надсилаємо клієнту відповідь про помилку
        return {"réponse": {"erreurs": f"[{erreur}]"}}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • рядок 9: контролер отримує:
      • запит [request] від клієнта;
      • конфігурацію [config] від сервера;
  • рядки 14–18: витягується тіло запиту POST. Параметри, інкапсульовані в тілі запиту HTTP, можуть бути закодовані різними способами. Ми вже зустрічали один із них: [x-www-form-urlencoded]. Тут ми використаємо інше кодування: jSON;
  • рядок 18: [request.data] дозволяє отримати тіло (body) запиту HTTP. Тут ми отримуємо текст, і ми знаємо, що цей текст належить до jSON, який представляє список словників [marié, enfants, salaire];
  • рядки 19–24: витягується цей список словників;
  • рядки 22–24: якщо витяг jSON пройшов з помилкою, фіксуємо помилку;
  • рядки 26–28: якщо виявляється, що отриманий об’єкт не є списком або є порожнім списком, фіксується помилка;
  • рядки 29–38: якщо список отримано успішно, перевіряємо, чи це дійсно список словників;
  • рядки 40–43: якщо сталася помилка, зупиняємося на цьому етапі та надсилаємо клієнту відповідь про помилку;
  • рядки 45–69: тепер перевіряємо кожен із словників:
    • вони повинні містити ключі [marié, enfants, salaire];
    • вони повинні дозволяти створити дійсний об’єкт [TaxPayer];
  • рядки 65–69: якщо в словнику виявлено помилку, її додають до цього ж словника, пов’язавши з ключем «помилка»;
  • рядки 72–75: словники з помилками були зібрані у список [list_erreurs]. Якщо цей список не порожній, то його надсилають у відповіді про помилку, що надсилається клієнту;
  • рядок 77: дійшовши до цього місця, ми знаємо, що можемо створити список об’єктів типу [TaxPayer] на основі тіла запиту, надісланого клієнтом;
  • рядки 84–91: обробляємо список отриманих словників;
  • рядок 86: на основі словника створюємо об’єкт типу [TaxPayer];
  • рядок 89: обчислюється податок для цього [TaxPayer];
  • рядок 91: відомо, що об’єкт [taxpayer] було змінено в результаті розрахунку податку. Його перетворюють на словник і додають до списку результатів;
  • рядок 93: цей список результатів надсилається клієнту;

28.2.4. Тестування сервера

Ми протестуємо сервер за допомогою клієнта Postman:

  • запускаємо веб-сервер SGBD та поштовий сервер [hMailServer];
  • запускаємо клієнт Postman та його консоль (Ctrl-Alt-C);

Image

  • на [1]: надсилаємо запит [POST];
  • в [2]: відповідь URL від сервера;
  • у [3]: тіло запиту HTTP;
  • в [5]: вказується, що це тіло має бути надіслане у вигляді рядка jSON;
  • у [4]: переходимо в режим [raw], щоб мати змогу скопіювати/вставити рядок jSON;
  • у [6]: вставляємо рядок jSON, взятий з одного з файлів [résultats.json] різних версій. Потім для кожного платника податків зберігаємо лише властивості [marié, salaire, enfants];

Image

  • у [7] переглядаємо заголовки HTTP, які клієнт Postman надішле на сервер;
  • у [8] бачимо, що він надішле йому заголовок [Content-Type], який вказує, що запит містить тіло, закодоване у форматі jSON. Це пов’язано з раніше зробленим вибором [5];

Image

  • у [9-12]: у запит вносимо ідентифікатори, яких очікує сервер;

Надсилаємо цей запит. Відповідь сервера така:

Image

  • у [3] отримано з jSON;
  • у [4] — податок платників податків;

Давайте розглянемо в консолі Postman (Ctrl-Alt-C) діалог між клієнтом і сервером, що відбувся:

Клієнт Postman надіслав такий текст:

POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 03c4aa28-5a5d-4bb5-ac51-7ad51968c71d
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 824

[
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 55555
  },
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 50000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 50000
  },
  {
    "marié": "non",
    "enfants": 2,
    "salaire": 100000
  },
  {
    "marié": "non",
    "enfants": 3,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 5,
    "salaire": 100000
  },
  {
    "marié": "non",
    "enfants": 0,
    "salaire": 100000
  },
  {
    "marié": "oui",
    "enfants": 2,
    "salaire": 30000
  },
  {
    "marié": "non",
    "enfants": 0,
    "salaire": 200000
  },
  {
    "marié": "oui",
    "enfants": 3,
    "salaire": 200000
  }
]
  • рядок 1: POST на сервер;
  • рядок 2: заголовок HTTP для автентифікації;
  • рядок 3: клієнт повідомляє серверу, що надсилає йому рядок jSON, розмір якого становить 824 байти (рядок 11);
  • рядки 13–69: тіло запиту jSON;

Сервер надіслав йому такий текст:


HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 1461
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:16:34 GMT

{"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}]}}
  • рядок 1: запит виконано успішно;
  • рядок 2: тіло відповіді сервера — це рядок jSON. Його розмір становить 1461 байт (рядок 3);
  • рядок 7: відповідь сервера jSON;

Тепер перевіримо випадки помилок.

Випадок 1: надсилаємо будь-що


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 47652706-9744-46a0-a682-de010e5406c0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 3

abc

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 125
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:43:27 GMT

{"réponse": {"erreurs": ["le corps du POST n'est pas une chaîne jSON valide : Expecting value: line 1 column 1 (char 0)"]}}
  • рядок 13: надіслано рядок [abc], який не є дійсним рядком jSON (рядок 3);
  • рядок 15: сервер відповідає кодом помилки 400;
  • рядок 21: відповідь сервера jSON;

Випадок 2: надішлімо дійсний рядок jSON, який не є списком


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 03b64735-9239-47b3-b92d-be7c9ebc7559
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 17

{"att1":"value1"}

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 97
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:50:11 GMT

{"réponse": {"erreurs": ["le corps du POST n'est pas une liste ou alors cette liste est vide"]}}

Випадок 3: надішлімо рядок jSON, який є списком, елементи якого не всі є словниками


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: a1528a5f-777c-413f-b3be-7d4e9955b12a
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 7

[0,1,2]

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 85
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:52:10 GMT

{"réponse": {"erreurs": ["le corps du POST doit être une liste de dictionnaires"]}}

Випадок 4: надішлімо список словників, у якому один словник містить неправильні ключі


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: ba964d81-c9d9-46ff-a521-b4c4e5639484
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 19

[{"att1":"value1"}]

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 112
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:54:33 GMT

{"réponse": {"erreurs": [{"att1": "value1", "erreur": "MyException[2, la clé [att1] n'est pas autorisée]"}]}}

Випадок 5: надішлімо список словників, у якому один словник має відсутні ключі:


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 98aec51d-f37d-4c14-81cd-c7ffcbbcdc65
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 18

[{"marié":"oui"}]

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 125
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:56:40 GMT

{"réponse": {"erreurs": [{"marié": "oui", "erreur": "le dictionnaire doit inclure les clés [marié, enfants, salaire]"}]}}

Випадок 6: надішлімо список словників із словником, що містить правильні ключі, але деякі з них мають неправильні значення:


POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 3083e601-dee4-4e15-9ea4-fc0328d0fcf0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 46

[{"marié":"x", "enfants":"x", "salaire":"x"}]

HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 167
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:59:32 GMT

{"réponse": {"erreurs": [{"marié": "x", "enfants": "x", "salaire": "x", "erreur": "MyException[31, l'attribut marié [x] doit avoir l'une des valeurs oui / non]"}]}}

28.3. Веб-клієнт

Image

Папка [http-clients/05] (версія 10) спочатку створюється шляхом копіювання папки [http-clients/02] (версія 7). Потім вона модифікується.

28.3.1. Рівень [dao]

Рівень [dao] реалізовано за допомогою наступного класу [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):
        

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

    # розрахунок податку
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        

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

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

        # підключення до сервера
        config_server = self.__config_server
        if config_server['authBasic']:
            response = requests.post(config_server['urlServer'], json=list_dict_taxpayers,
                                     auth=(config_server["user"]["login"],
                                           config_server["user"]["password"]))
        else:
            response = requests.post(config_server['urlServer'], json=list_dict_taxpayers)
        # режим налагодження?
        if self.__debug:
            # реєстратор
            if not self.__logger:
                self.__logger = self.__config['logger']
            # запис у журнал
            self.__logger.write(f"{response.text}\n")
        # код статусу відповіді HTTP
        status_code = response.status_code
        # відповідь jSON додається до словника
        résultat = response.json()
        # помилка, якщо код статусу відмінний від 200 OK
        if status_code != status.HTTP_200_OK:
            # відомо, що помилки пов’язані з ключем [erreurs] у відповіді
            raise ImpôtsError(93, résultat['réponse']['erreurs'])
        # відомо, що цей результат пов’язано з ключем [results] у відповіді
        list_dict_taxpayers2 = résultat['réponse']['results']
        # початковий список платників податків оновлюється з урахуванням отриманих результатів
        for i in range(len(taxpayers)):
            # оновлення списку платників податків [i]
            taxpayers[i].fromdict(list_dict_taxpayers2[i])
        # тут параметр [taxpayers] було оновлено за результатами з сервера
  • рядки 1–26: код залишається таким самим, як у версії 7 та в інших версіях;
  • рядки 27–70: вводиться новий метод [calculate_tax_in_bulk_mode], завданням якого є обчислення податку за списком платників податків;
  • рядок 28: [taxpayers] — це список платників податків;
  • рядки 31–39: за допомогою функції [map] здійснюється перетворення списку об’єктів типу [TaxPayer] у список словників;
  • рядки 34–38: використовувана лямбда-функція перетворює об’єкт типу [TaxPayer] на словник типу [dict], що містить лише ключі [marié, enfants, salaire]. Для цього використовується параметр із назвою [included_keys] методу [BaseEntity.asdict]. Нагадаємо, що для визначення точних імен властивостей, які слід вказати в параметрах [excluded_keys, included_keys], необхідно використовувати попередньо визначений словник [taxpayer.__dict__];
  • рядки 41–48: підключення до сервера, а потім отримання його відповіді HTTP;
  • рядки 44, 48:
    • використовується статичний метод [requests.post] для відправки запиту POST на сервер;
    • використовується параметр з іменем [json], щоб вказати, що тіло POST є рядком jSON. Це матиме два наслідки:
      • об’єкт, прив’язаний до параметра з іменем [json] (у даному випадку — список словників), буде перетворено на рядок jSON;
      • заголовок
Content-Type: application/json

буде включено до заголовків HTTP та POST;

  • рядок 59: відповідь jSON від сервера десеріалізується у словник [résultat];
  • рядки 61–63: обробляється можлива помилка, надіслана сервером;
  • рядок 65: результати розрахунку податку містяться у списку словників;
  • рядки 67–69: ці результати використовуються для оновлення початкового списку платників податків [taxpayers], який спочатку був отриманий методом у рядку 28;
  • рядок 70: тут початковий список платників податків було оновлено результатами розрахунку податку;

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

Основний скрипт [main] змінюється наступним чином: змінам піддається лише функція [thread_function], яка виконується потоками, створеними клієнтом. Решта коду залишається без змін.


# виконання шару [dao] у потоці
# taxpayers — це список платників податків
def thread_function(dao: ImpôtsDaoWithHttpClient, logger: Logger, taxpayers: list):
    # початок журналу потоку
    thread_name = threading.current_thread().name
    nb_taxpayers = len(taxpayers)
    # журнал
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    # розраховується податок платників податків
    dao.calculate_tax_in_bulk_mode(taxpayers)
    # журнал
    logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")
  • рядки 9–10: якщо раніше був цикл, який послідовно передавав кожного платника податків до методу [dao.calculate_tax], то тут виконується лише один виклик методу [dao.calculate_tax_in_bulk_mode], якому передаються всі платники податків;

28.3.3. Виконання клієнта

Ми порівняємо час виконання версій:

  • 7, де для кожного платника податків виконується окремий запит HTTP;
  • 10 (ця версія), де платники податків об’єднуються в єдиний запит HTTP;

Спочатку розглянемо версію 6. Щоб порівняти обидві версії, ми встановлюємо властивість [sleep_time] сервера на нуль, щоб уникнути примусового очікування потоків. Журнали клієнта виглядають так:


2020-07-28 14:20:45.811347, Thread-1 : début du thread [Thread-1] avec 4 contribuable(s)
2020-07-28 14:20:45.811347, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}

2020-07-28 14:20:45.913065, Thread-3 : fin du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-28 14:20:45.913065, Thread-3 : fin du thread [Thread-3]

Отже, час виконання клієнтом розрахунку податку для 11 платників податків становить [913065-811347= 101718], тобто приблизно 102 мілісекунди.

Зробимо те саме з версією 10 (sleep_time сервера з нульовим навантаженням). Логи клієнта в цьому випадку такі:


2020-07-28 14:25:31.871428, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-07-28 14:25:31.873594, Thread-2 : début du calcul de l'impôt des 3 contribuables
2020-07-28 14:25:31.877429, Thread-3 : début du calcul de l'impôt des 3 contribuables
2020-07-28 14:25:31.882855, Thread-4 : début du calcul de l'impôt des 1 contribuables
2020-07-28 14:25:31.930723, Thread-2 : {"réponse": {"results": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}]}}
….
2020-07-28 14:25:31.935958, Thread-4 : fin du calcul de l'impôt des 1 contribuables
2020-07-28 14:25:31.935958, Thread-1 : fin du calcul de l'impôt des 4 contribuables

Отже, час виконання клієнтом розрахунку податку для 11 платників податків становить [935958-871428= 64530 ns] (рядок 8 – рядок 1), тобто приблизно 65 мілісекунд. Таким чином, ця нова версія 10 забезпечує прискорення приблизно на 57 % порівняно з версією 7.

28.3.4. Тестування рівня [dao] клієнта

Image

Результат тесту [TestHttpClientDao] клієнта версії 10 дуже близький до результату тесту версії 7:


import unittest

from Logger import Logger


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_in_bulk_mode([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)

    

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

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

    # виконуємо методи тестування
    print("tests en cours...")
    unittest.main()
  • рядок 14: замість виклику методу [dao.calculate_tax] викликається метод [dao.calculate_tax_in_bulk_mode], якому передається список (наявність дужок) платника податків;

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