Skip to content

25. Ćwiczenie praktyczne: wersja 8

25.1. Wprowadzenie

Napiszemy nową aplikację klient-serwer. Nowością w serwerze jest to, że będzie on zarządzał sesją. Zamiast umieszczać dane administracji podatkowej w obiekcie o zakresie [application], umieścimy je w obiekcie o zakresie [session]. W ten sposób obniżamy wydajność kodu. Gdy obiekt może być udostępniony wszystkim użytkownikom wyłącznie do odczytu, lepiej jest utworzyć obiekt o zakresie [application] zamiast o zakresie [session]. Zyskujemy przynajmniej na przepustowości, ponieważ w ten sposób zmniejszamy rozmiar pliku cookie sesji. Chcemy jednak przedstawić aplikację klient-serwer, w której klient i serwer wymieniają się plikiem cookie sesji.

Architektura aplikacji pozostaje bez zmian:

Image

25.2. Serwer WWW

Struktura katalogów skryptów serwera wygląda następująco:

Image

Folder [http-servers/03] powstaje początkowo poprzez skopiowanie folderu [http-servers/02]. Następnie wprowadza się w nim zmiany.

25.2.1. Konfiguracja

Jest taka sama jak w |version précédente|, z kilkoma zmianami w skrypcie [config]:


# zależności bezwzględne
    absolute_dependencies = [
        # foldery projektu
        # 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",
        # Stałe, przedziały
        f"{root_dir}/impots/v05/entities",
        # index_controller
        f"{script_dir}/../controllers",
        # skrypty [config_database, config_layers]
        script_dir,
        # Logger, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
    ]
  • wiersz 17: przepiszemy kontroler dla funkcji [index], która przetwarza plik URL / ;
  • wiersz 21: wykorzystujemy narzędzia z |version précédente|;

25.2.2. Główny skrypt [main]

Nowy skrypt [main] wprowadza kilka zmian w głównym skrypcie [main] z poprzedniej wersji:


# aplikacja Flask może się uruchomić
app = Flask(__name__)
# tajny klucz sesji
app.secret_key = os.urandom(12).hex()
  • wiersz 4: tworzy się tajny klucz dla aplikacji. Wiadomo, że jest on niezbędny do zarządzania sesjami;

Ponadto w kodzie skryptu [main] nie są już wymagane dane podatkowe. Usunięto następujące wiersze:


# pobieranie danych z urzędu skarbowego
erreur = False
try:
    # admindata będzie danymi o zasięgu aplikacji, dostępnymi tylko do odczytu
    config["admindata"] = config["layers"]["dao"].get_admindata()
    # log pomyślnego wykonania
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    # odnotowano błąd
    erreur = True
    # log błędów
    log = f"L'erreur suivante s'est produite : {ex}"
    # konsola
    print(log)
    # plik logów
    logger.write(f"{log}\n")
    # wiadomość e-mail do administratora
    send_adminmail(config, log)

Ponadto kontroler [index_controller] akceptuje dodatkowy parametr – sesję Flask:


from flask import request, Flask, session
….        
         # zlecamy wykonanie zapytania przez kontroler
        résultat, status_code = index_controller.execute(request, session, config)

25.2.3. Kontroler [index_controller]

Kontroler [index_controller] obsługuje teraz sesję:


# import zależności
import os
import re
import threading

from flask_api import status
from werkzeug.local import LocalProxy

# URL z ustawionymi parametrami: /?marié=xx&enfants=yy&salaire=zz
from AdminData import AdminData
from ImpôtsError import ImpôtsError


def execute(request: LocalProxy, session: LocalProxy, config: dict) -> tuple:
    # zależności
    from TaxPayer import TaxPayer

    # na początku brak błędów
    erreurs = []
    

    # czy są błędy?
    if erreurs:
        # zwracamy klientowi komunikat o błędzie
        return {"réponse": {"erreurs": erreurs}}, status.HTTP_400_BAD_REQUEST

    # brak błędów, można kontynuować pracę
    # pobieramy konfigurację powiązaną z wątkiem
    thread_name = threading.current_thread().name
    logger = config[thread_name]["config"]["logger"]
    # wykonuje się zapytanie
    réponse = None
    try:
        # najprostszy przypadek – admindata jest już zalogowany
        if session.get('client_id') is not None:
            # pobieramy informacje o sesji
            client_id = session.get('client_id')
            admindata = AdminData().fromdict(session.get('admindata'))
            # log
            logger.write(f"[index_controller] client [{client_id}], données fiscales prises en session\n")
        else:
            # pobieranie danych z urzędu skarbowego
            admindata = config["layers"]["dao"].get_admindata()
            # zalogowanie użytkownika admindata
            session['admindata'] = admindata.asdict()
            # przypisujemy numer klientowi i zapisujemy ten numer w sesji
            # umożliwi nam to śledzenie go w logach serwera
            client_id = os.urandom(12).hex()
            session['client_id'] = client_id
            # log
            logger.write(f"[index_controller] client [{client_id}], données fiscales prises dans la couche dao\n")
        # obliczenie podatku
        taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
        config["layers"]["métier"].calculate_tax(taxpayer, admindata)
        # przekazujemy odpowiedź klientowi
        return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
    except (BaseException, ImpôtsError) as erreur:
        # przesyłamy odpowiedź do klienta
        return {"réponse": {"erreurs": [f"{erreur}"]}}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • wiersz 14: kontroler odbiera bieżącą sesję od klienta internetowego;
  • wiersze 35–38: jeśli klient ma sesję, to zawiera ona dwa klucze:
    • [client_id]: numer klienta (wiersz 37);
    • [admindata]: dane administracji podatkowej w postaci słownika (wiersz 38);
  • wiersz 35: sprawdzamy, czy sesja zawiera jeden z dwóch oczekiwanych kluczy;
  • wiersze 42–51: sytuacja, w której sesja klienta nie została jeszcze zainicjowana;
    • wiersz 43: pobieramy dane z urzędu skarbowego z warstwy [dao];
    • wiersz 45: dane te są umieszczane w sesji w postaci słownika;
    • wiersz 48: przypisuje się klientowi losowy numer. Numer ten będzie inny dla każdego klienta;
    • wiersz 49: numer ten jest zapisywany w sesji;
    • wiersz 51: rejestruje się fakt, że dane z urzędu skarbowego zostały pobrane z warstwy [dao]. Dostęp do warstwy [dao] jest zazwyczaj kosztowny. Dlatego należy go ograniczać. Chodzi tu o to, aby raz pobrać dane podatkowe z warstwy [dao], umieścić je w sesji i pobierać je stamtąd podczas kolejnych żądań tego samego klienta. Należy pamiętać, że nie jest to najlepsze rozwiązanie. Ponieważ dane podatkowe urzędu skarbowego są takie same dla wszystkich klientów, ich miejsce jest w obiekcie o zasięgu aplikacji;
  • wiersze 35–40: przypadek, w którym sesja klienta została zainicjowana podczas poprzedniego żądania;
    • wiersz 37: pobieramy numer klienta z sesji;
    • wiersz 38: pobieramy dane podatkowe z administracji z sesji;
    • wiersz 40: rejestruje się fakt, że klient uzyskał dane podatkowe administracji w sesji;

25.3. Klient internetowy

Image

25.3.1. Warstwa [dao]

25.3.1.1. Klasa [ImpôtsDaoWithHttpSession]

Warstwa [dao] jest zaimplementowana przez następującą klasę [ImpôtsDaoWithHttpSession]:


# importy

import requests
from flask_api import status
from myutils import decode_flask_session

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ôtsDaoWithHttpSession(AbstractImpôtsDao, InterfaceImpôtsMétier):

    # konstruktor
    def __init__(self, config: dict):
        # inicjalizacja elementu nadrzędnego
        AbstractImpôtsDao.__init__(self, config)
        # zapisywanie elementów konfiguracji
        # ogólna konfiguracja
        self.__config = config
        # serwer
        self.__config_server = config["server"]
        # tryb debugowania
        self.__debug = config["debug"]
        # rejestrator
        self.__logger = None
        # pliki cookie
        self.__cookies = None

    # metoda nieużywana
    def get_admindata(self) -> AdminData:
        pass

    # obliczanie podatku
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        # pozwalamy na przekazywanie wyjątków
        # parametry GET
        params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
        # połączenie z uwierzytelnianiem Auth Basic?
        if self.__config_server['authBasic']:
            response = requests.get(
                # URL serwera, do którego wysłano zapytanie
                self.__config_server['urlServer'],
                # parametry URL
                params=params,
                # uwierzytelnianie Basic
                auth=(
                    self.__config_server["user"]["login"],
                    self.__config_server["user"]["password"]),
                cookies=self.__cookies)

        else:
            # połączenie bez uwierzytelniania Auth Basic
            response = requests.get(self.__config_server['urlServer'], params=params, cookies=self.__cookies)
        # pobierane są pliki cookie z odpowiedzi, jeśli takie istnieją
        if response.cookies:
            self.__cookies = response.cookies
            # pobieramy plik cookie sesji
            session_cookie = response.cookies.get('session')
            # dekoduje się go w celu zalogowania
            if session_cookie:
                # moduł logowania
                if not self.__logger:
                    self.__logger = self.__config['logger']
                # rejestruje się
                self.__logger.write(f"cookie de session={decode_flask_session(session_cookie)}\n")

        # tryb debugowania?
        if self.__debug:
            # moduł logowania
            if not self.__logger:
                self.__logger = self.__config['logger']
            # rejestruje się
            self.__logger.write(f"{response.text}\n")
        # kod statusu odpowiedzi HTTP
        status_code = response.status_code
        # odpowiedź jSON jest umieszczana w słowniku
        résultat = response.json()
        # błąd, jeśli kod statusu różni się od 200 OK
        if status_code != status.HTTP_200_OK:
            # wiemy, że błędy zostały powiązane z kluczem [erreurs] odpowiedzi
            raise ImpôtsError(87, résultat['réponse']['erreurs'])
        # wiadomo, że wynik został powiązany z kluczem [result] w odpowiedzi
        # modyfikuje się parametr wejściowy na podstawie tego wyniku
        taxpayer.fromdict(résultat["réponse"]["result"])
  • wiersz 30: warstwa [dao] będzie zarządzać słownikiem plików cookie;
  • wiersz 58: właściwość [response.cookies] to słownik zawierający pliki cookie wysyłane przez serwer w nagłówkach HTTP i [Set-Cookie];
  • wiersz 59: te pliki cookie są przechowywane w warstwie [dao]. Zostaną one odesłane do serwera podczas kolejnych żądań tego samego klienta;
  • wiersze 60–68: choć nie jest to konieczne, pobieramy plik cookie sesji. W słowniku plików cookie wysłanych przez serwer plik cookie sesji jest powiązany z kluczem [session];
  • wiersze 62–68: dekodujemy plik cookie sesji w celu zalogowania użytkownika;
  • wiersz 68: do funkcji [decode_flask_session], która dekoduje plik cookie sesji, wrócimy później;
  • wiersze 52 i 57: przy każdym żądaniu od tego samego klienta pliki cookie wysłane przez serwer są mu zwracane. W ten sposób utrzymywana jest sesja Flaska między klientem a serwerem;

Należy teraz pamiętać, że warstwa [dao] będzie wykonywana jednocześnie przez wiele wątków. Należy zatem sprawdzić wszystkie właściwości instancji klasy i upewnić się, czy jednoczesny dostęp do tych właściwości nie stanowi problemu. W tym przypadku dodaliśmy właściwość [self.__cookies] w linii 30. Właściwość ta jest modyfikowana w linii 59. Mamy zatem dostęp zapisu do danych współdzielonych przez wszystkie wątki. Dostęp ten stanowi jednak problem: każdy wątek reprezentujący danego klienta posiada własny plik cookie sesji. W pliku tym znajduje się bowiem unikalny numer klienta (=wątku) dla każdego klienta. Jeśli nie podejmiemy żadnych działań, wątek T2 może nadpisać pliki cookie wątku T1.

Omówiliśmy już metodę rozwiązania tego problemu: w pliku [config], przekazanym jako parametr do konstruktora (wiersz 17), można utworzyć różne klucze dla każdego wątku. Jako klucz można na przykład użyć nazwy wątku:

  • w wierszu 59 można by zapisać:

config[thread_name][‘cookies’]=cookies
  • w wierszu 52 można by wówczas wpisać:

cookies=config[thread_name][‘cookies’]

W tym przypadku zastosujemy inną technikę: każdy wątek (=klient) będzie miał własną warstwę [dao]. Dzięki temu wiersz 59 nie stanowi już problemu, ponieważ używane pliki cookie należą do jednego klienta.

W tym celu utworzymy nową klasę [ImpôtsDaoWithHttpSessionFactory].

25.3.1.2. Funkcja dekodowania sesji Flask

Funkcja [decode_flask_session] jest zdefiniowana w skrypcie [myutils]:

Image

Omówiliśmy już skrypt |myutils|. Skrypt ten jest modułem o zasięgu maszynowym, który różne skrypty z tego kursu mogą importować za pomocą instrukcji:

import myutils

Zdefiniowano w nim funkcję [decode_flask_session] w następujący sposób:


def decode_flask_session(cookie: str) -> str:
    # źródło: https://www.kirsle.net/wizards/flask-session.cgi
    compressed = False
    payload = cookie

    if payload.startswith('.'):
        compressed = True
        payload = payload[1:]

    data = payload.split(".")[0]

    data = base64_decode(data)
    if compressed:
        data = zlib.decompress(data)

    return data.decode("utf-8")
  • wiersz 2: URL, w którym znalazłem tę funkcję;
  • wiersz 1: parametr [cookie] to ciąg znaków powiązany z kluczem [session] w słowniku plików cookie otrzymanych przez klienta internetowego;
  • wiersze 3–16: nie będę komentować tego kodu, ponieważ nie znam się na nim;

Dodajemy nowy import do pliku [__init__.py]:


from .myutils import set_syspath, json_response, decode_flask_session

Nowa wersja pliku [myutils] zostaje zainstalowana wśród modułów o zasięgu maszynowym za pomocą polecenia [pip install .] w terminalu 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
  • wiersz 1: aby wpisać tę instrukcję, należy znajdować się w folderze [packages];

25.3.1.3. Klasa [ImpôtsDaoWithHttpSessionFactory]

Klasa [ImpôtsDaoWithHttpSessionFactory] wygląda następująco:


from ImpôtsDaoWithHttpSession import ImpôtsDaoWithHttpSession


class ImpôtsDaoWithHttpSessionFactory:

    def __init__(self, config: dict):
        # zapisujemy parametr
        self.__config = config

    def new_instance(self):
        # zwracamy instancję warstwy [dao]
        return ImpôtsDaoWithHttpSession(self.__config)
  • klasa [ImpôtsDaoWithHttpSessionFactory] umożliwia utworzenie nowej implementacji warstwy [dao] z wykorzystaniem metody [new_instance] z wierszy 10–12;

25.3.2. Konfiguracja

Skrypt [config_layers], który konfiguruje warstwy klienta internetowego, zostaje zmodyfikowany w następujący sposób:


def configure(config: dict) -> dict:
    # tworzenie instancji warstw aplikacji

    # warstwa DAO
    from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
    dao_factory = ImpôtsDaoWithHttpSessionFactory(config)

    # konfiguracja warstw
    return {
        "dao_factory": dao_factory
    }
  • wiersze 5–6: zamiast instancjonować pojedynczą warstwę [dao], jak to miało miejsce wcześniej, instancjonuje się „fabrykę” tej warstwy (fabryka = miejsce wytwarzania obiektów, w tym przypadku warstwa [dao]);
  • wiersze 9–11: zwracamy konfigurację warstw;

25.3.3. Główny skrypt klienta

Skrypt [main] różni się od skryptu z poprzedniej wersji w następujący sposób:


# konfiguracja aplikacji

import config
config = config.configure({})

# zależności
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger


# uruchomienie warstwy [dao] w wątku
# taxpayers to lista podatników
def thread_function(thread_dao, logger, taxpayers: list):
    


# lista wątków klienta
threads = []
logger = None
# kod
try:
    
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        
        # każdy wątek musi posiadać własną warstwę [dao], aby prawidłowo zarządzać swoim plikiem cookie sesji
        thread_dao = dao_factory.new_instance()
        # tworzymy wątek
        thread = threading.Thread(target=thread_function, args=(thread_dao, logger, thread_taxpayers))
        # dodajemy go do listy wątków głównego skryptu
        threads.append(thread)
        # uruchamia się wątek – operacja ta jest asynchroniczna – nie czeka się na wynik wątku
        thread.start()
    # wątek główny czeka na zakończenie wszystkich wątków, które uruchomił
    
except BaseException as erreur:
    # wyświetlenie błędu
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # zamyka się moduł logowania
    if logger:
        logger.close()
    # zakończono
    print("Travail terminé...")
    # zakończenie wątków, które mogłyby nadal istnieć, gdyby proces został zatrzymany z powodu błędu
    sys.exit()
  • wiersze 29–30: każdy wątek ma swoją warstwę [dao];

25.3.4. Uruchomienie klienta

Uruchomiono serwer WWW, uruchomiono proces SGBD, uruchomiono serwer pocztowy [hMailServer]. Następnie uruchomiono skrypt klienta WWW [main]. Logi wykonania w [data/logs/logs.txt] mają następujący wygląd:


2020-07-25 10:21:46.478511, Thread-1 : début du thread [Thread-1] avec 1 contribuable(s)
2020-07-25 10:21:46.479111, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-25 10:21:46.479111, Thread-2 : début du thread [Thread-2] avec 1 contribuable(s)
2020-07-25 10:21:46.480195, Thread-3 : début du thread [Thread-3] avec 2 contribuable(s)
2020-07-25 10:21:46.480195, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-25 10:21:46.481137, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
2020-07-25 10:21:46.481137, Thread-3 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-25 10:21:46.482279, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
2020-07-25 10:21:46.482622, Thread-6 : début du thread [Thread-6] avec 1 contribuable(s)
2020-07-25 10:21:46.482622, Thread-4 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-5 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-6 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-25 10:21:46.725910, Thread-4 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"fa3c83b82761c83e13217967"}
2020-07-25 10:21:46.725910, Thread-4 : {"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}}}
2020-07-25 10:21:46.725910, Thread-4 : fin du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:46.726960, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:47.514108, Thread-3 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,24999.5],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"700e3f5dc808c7c48f0c9007"}
2020-07-25 10:21:47.514610, Thread-3 : {"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}}}
2020-07-25 10:21:47.514939, Thread-3 : fin du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}
2020-07-25 10:21:47.514939, Thread-3 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000}
2020-07-25 10:21:47.527211, Thread-5 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"9e14a5d4a3057f69ab95ab2d"}
2020-07-25 10:21:47.527211, Thread-2 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,22500.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"a06e8fd70a44c9e311f4dce0"}
2020-07-25 10:21:47.527211, Thread-5 : {"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}}}
2020-07-25 10:21:47.527211, Thread-1 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"28c38df998f67685b3a482b8"}
2020-07-25 10:21:47.527211, Thread-2 : {"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}}}
2020-07-25 10:21:47.528341, Thread-5 : fin du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.528341, Thread-1 : {"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}}}
2020-07-25 10:21:47.528842, Thread-2 : fin du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}
2020-07-25 10:21:47.529349, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000}
2020-07-25 10:21:47.529699, Thread-1 : fin du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.529699, Thread-2 : fin du thread [Thread-2]
2020-07-25 10:21:47.531905, Thread-1 : fin du thread [Thread-1]
2020-07-25 10:21:47.536121, Thread-6 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,93749.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"38499b63076516c02f2770ec"}
2020-07-25 10:21:47.537161, Thread-3 : {"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}}}
2020-07-25 10:21:47.537161, Thread-6 : {"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}}}
2020-07-25 10:21:47.538156, Thread-3 : fin du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.538557, Thread-6 : 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-25 10:21:47.538828, Thread-3 : fin du thread [Thread-3]
2020-07-25 10:21:47.538828, Thread-6 : fin du thread [Thread-6]
2020-07-25 10:21:47.546198, Thread-5 : {"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}}}
2020-07-25 10:21:47.546198, Thread-5 : fin du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.546198, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000}
2020-07-25 10:21:47.739643, Thread-4 : {"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}}}
2020-07-25 10:21:47.739643, Thread-4 : fin du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.740668, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000}
2020-07-25 10:21:48.557469, Thread-5 : {"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}}}
2020-07-25 10:21:48.558715, Thread-5 : fin du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}
2020-07-25 10:21:48.558715, Thread-5 : fin du thread [Thread-5]
2020-07-25 10:21:48.753025, Thread-4 : {"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}}}
2020-07-25 10:21:48.753318, Thread-4 : fin du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-25 10:21:48.753540, Thread-4 : fin du thread [Thread-4]
  • mamy łącznie 6 wątków, a więc 6 klientów (wiersze 1, 3, 4, 6, 8, 9), którzy jednocześnie wysyłają zapytania do serwera obliczającego podatek;
  • prześledzimy wątek [Thread-4], który obsługuje 3 podatników (wiersz 6). Wyśle on kolejno trzy zapytania do serwera obliczającego podatek;
  • wiersz 10: pierwsze zapytanie wątku [Thread-4];
  • wiersz 13: [Thread-4] otrzymał odpowiedź na swoje pierwsze zapytanie. Znajduje w niej plik cookie sesji, w którym znajduje się numer [fa3c83b82761c83e13217967] przypisany mu przez serwer;
  • wiersz 14: podatek pierwszego podatnika;
  • wiersz 16: [Thread-4] wysyła zapytanie dotyczące drugiego podatnika;
  • wiersz 43: [Thread-4] otrzymuje kwotę podatku drugiego podatnika;
  • wiersz 45: [Thread-4] wysyła zapytanie dotyczące trzeciego podatnika;
  • wiersz 49: [Thread-4] otrzymuje podatek od trzeciego podatnika;
  • wiersz 51: [Thread-4] zakończył swoją pracę;

Teraz przyjrzyjmy się, jak trzy żądania [Thread-4] zostały przetworzone po stronie serwera. Będziemy mogli śledzić je w logach serwera dzięki jego numerowi klienta [fa3c83b82761c83e13217967].

Logi serwerowe dla [data/logs/logs.txt] przedstawiają się następująco:


2020-07-25 10:21:39.187366, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:40.439093, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:46.502011, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=50000' [GET]>
2020-07-25 10:21:46.504049, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.505452, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=55555' [GET]>
2020-07-25 10:21:46.506257, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.507292, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=nie&enfants=0&salaire=100000' [GET]>
2020-07-25 10:21:46.507292, Thread-4 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.508301, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?stan_cywilny=tak&dzieci=2&wynagrodzenie=50000' [GET]>
2020-07-25 10:21:46.509293, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.511808, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=nie&enfants=3&salaire=100000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?stan_cywilny=tak&dzieci=3&wynagrodzenie=200000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.719504, Thread-6 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises dans la couche dao
2020-07-25 10:21:46.720003, Thread-6 : [index] {'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}}}
2020-07-25 10:21:46.736108, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=3&salaire=100000' [GET]>
2020-07-25 10:21:46.736108, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.506709, Thread-2 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises dans la couche dao
2020-07-25 10:21:47.507216, Thread-2 : [index] {'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}}}
2020-07-25 10:21:47.507216, Thread-3 : [index_controller] client [28c38df998f67685b3a482b8], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508442, Thread-4 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508940, Thread-3 : [index] {'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}}}
2020-07-25 10:21:47.510506, Thread-4 : [index] {'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}}}
2020-07-25 10:21:47.511513, Thread-5 : [index_controller] client [a06e8fd70a44c9e311f4dce0], données fiscales prises dans la couche dao
2020-07-25 10:21:47.514939, Thread-5 : [index] {'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}}}
2020-07-25 10:21:47.520727, Thread-7 : [index_controller] client [38499b63076516c02f2770ec], données fiscales prises dans la couche dao
2020-07-25 10:21:47.523162, Thread-7 : [index] {'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}}}
2020-07-25 10:21:47.530835, Thread-9 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=nie&enfants=2&salaire=100000' [GET]>
2020-07-25 10:21:47.531736, Thread-9 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises en session
2020-07-25 10:21:47.531905, Thread-9 : [index] {'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}}}
2020-07-25 10:21:47.541899, Thread-10 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=oui&enfants=2&salaire=30000' [GET]>
2020-07-25 10:21:47.542488, Thread-10 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:47.542488, Thread-10 : [index] {'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}}}
2020-07-25 10:21:47.553628, Thread-11 : [index] requête : <Request 'http://127.0.0.1:5000/?marié=nie&enfants=0&salaire=200000' [GET]>
2020-07-25 10:21:47.553628, Thread-11 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.736910, Thread-8 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:47.737191, Thread-8 : [index] {'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}}}
2020-07-25 10:21:47.748226, Thread-12 : [index] requête : <Request 'http://127.0.0.1:5000/?stan_cywilny=tak&dzieci=5&wynagrodzenie=100000' [GET]>
2020-07-25 10:21:47.748226, Thread-12 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:48.554695, Thread-11 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:48.555070, Thread-11 : [index] {'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}}}
2020-07-25 10:21:48.748753, Thread-12 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:48.748753, Thread-12 : [index] {'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}}}
  • klienta [fa3c83b82761c83e13217967] po raz pierwszy spotykamy w wierszu 14: aby obliczyć podatek, serwer musiał pobrać z bazy danych urzędu skarbowego;
  • następnie w wierszu 36 pojawia się klient [fa3c83b82761c83e13217967]. Tym razem serwer odnajduje dane z urzędu skarbowego w ramach bieżącej sesji, co pozwala mu uniknąć potencjalnie kosztownego dostępu do warstwy [dao];
  • klienta [fa3c83b82761c83e13217967] spotykamy po raz trzeci w wierszu 42, gdzie serwer ponownie korzysta z sesji klienta;

Ten przykład dobrze ilustruje korzyści płynące z sesji dla klienta: przechowuje się w niej dane wspólne dla wszystkich żądań tego klienta, których pozyskanie jest kosztowne.

Po stronie klienta wyniki w pliku [data/output/résultats.json] są takie same jak w poprzednich wersjach.

25.4. Testy warstwy [dao]

Podobnie jak w przypadku |versions précédentes|, testujemy warstwę [dao] po stronie klienta:

Image

Klasa testowa zostanie uruchomiona w następującym środowisku:

Image

  • konfiguracja [2] jest identyczna z konfiguracją [1], którą właśnie przeanalizowaliśmy;

Klasa testowa [TestHttpClientDao] ma następujący wygląd:


import unittest

from Logger import Logger


class TestHttpClientDao(unittest.TestCase):

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

        # {'stan cywilny': 'tak', 'dzieci': 2, 'wynagrodzenie': 55555,
        # 'podatek': 2814, 'dopłata': 0, 'ulga': 0, 'obniżka': 0, 'stawka': 0,14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        # weryfikacja
        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__':
    # konfiguracja aplikacji
    import config
    config = config.configure({})

    # rejestruje
    logger = Logger(config["logsFilename"])
    # zapisywanie w konfiguracji
    config["logger"] = logger
    # pobieramy fabrykę warstwy [dao]
    dao_factory = config["layers"]["dao_factory"]
    # tworzymy instancję warstwy [dao]
    dao = dao_factory.new_instance()

    # wykonujemy metody testowe
    print("tests en cours...")
    unittest.main()
  • tworzymy |configuration d’exécution| na potrzeby tego testu;
  • uruchamiamy serwer WWW wraz z całym jego środowiskiem;
  • uruchamia się test;

Wyniki są następujące:


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/03/tests/TestHttpClientDao.py
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 3.392s

OK

Process finished with exit code 0