Skip to content

23. Ćwiczenie praktyczne: wersja 6

23.1. Wprowadzenie

Wracamy teraz do naszej aplikacji do obliczania podatku. Będziemy tworzyć na jej podstawie różne aplikacje internetowe.

W wersji 5 naszego ćwiczenia praktycznego dane urzędu skarbowego były przechowywane w bazie danych. Wersja ta składała się z dwóch odrębnych aplikacji, które miały jednak wspólne warstwy:

  • aplikację obliczającą podatek w trybie |batch| dla podatników zarejestrowanych w pliku tekstowym;
  • aplikacja obliczająca podatek w trybie |interactif| dla podatników, których dane wprowadzano za pomocą klawiatury;

Wersja 5 aplikacji do obliczania podatku w trybie wsadowym (batch) miała następującą architekturę:

Image

Ostatecznie wersja internetowa tej aplikacji będzie miała następującą architekturę:

Image

  • klient internetowy [1] łączy się z serwerem internetowym [2], który komunikuje się z serwerami SGBD i [3];
  • serwer internetowy [2] zachowuje warstwy [métier], [8] oraz [dao] i [9] z pierwotnej aplikacji;
  • pierwotna aplikacja zachowuje swój główny skrypt [4] oraz warstwy [métier] i [15]. Warstwy [métier], [8] i [15] są identyczne;
  • komunikacja klient–serwer wymaga dwóch dodatkowych warstw:
    • warstwa [web] [7], która implementuje aplikację internetową;
    • warstwa [dao] [5] będąca klientem aplikacji internetowej [7];

W ostatecznej wersji obliczanie podatku w trybie wsadowym będzie mogło przebiegać na dwa sposoby:

  • biznesowe obliczenie podatku odbywa się za pośrednictwem warstwy serwerowej [métier]. Skrypt [main] będzie korzystał z tej metody;
  • biznesowe obliczenie podatku odbywa się w warstwie klienckiej [métier]. Skrypt [main2] będzie korzystał z tej metody;

Od tej pory będziemy tworzyć kilka aplikacji typu klient-serwer, z których każda będzie ilustrować jedną lub kilka nowych technologii tworzenia stron internetowych.

23.2. Serwer internetowy do obliczania podatku

23.2.1. Wersja 1

Image

Skrypt [server_01] stanowi kolejną aplikację internetową:

Image

  • W przypadku kodu [1] stosuje się skonfigurowany kod URL, do którego przekazuje się trzy wartości:
    • [marié] (tak / nie) w celu wskazania, czy podatnik pozostaje w związku małżeńskim;
    • [enfants]: liczba dzieci podatnika;
    • [salaire]: roczne wynagrodzenie podatnika;
  • w [2] serwer internetowy zwraca ciąg znaków jSON, który podaje kwotę podatku do zapłaty wraz z jej poszczególnymi składnikami;

Architektura aplikacji wygląda następująco:

Image

  • przeglądarka [1] wysyła zapytanie do serwera [2]. Skrypt [server_01] implementuje warstwę serwera [web] [2];
  • warstwy [3-8] to te, które zostały już wykorzystane w warstwie |version 5| aplikacji do obliczania podatku. Przenosimy je bez zmian;
    • warstwa [métier] [3] jest zdefiniowana jako |ici|;
    • warstwa [dao] [4] jest zdefiniowana jako |ici|;

Aplikacja internetowa [server_01] jest konfigurowana za pomocą trzech skryptów:

  • [config], który konfiguruje całą aplikację;
  • [config_database], który konfiguruje dostęp do bazy danych. Będziemy pracować ze skryptami SGBD, MySQL i PostgreSQL;
  • [config_layers], który konfiguruje warstwy aplikacji;

Skrypt [config] ma następującą treść:


def configure(config: dict) -> dict:
    import os

    # krok 1 ------
    # katalog tego pliku
    script_dir = os.path.dirname(os.path.abspath(__file__))
    # ścieżka główna
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
    # 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",
        # IndexController
        f"{script_dir}/../controllers",
        # skrypty [config_database, config_layers]
        script_dir,
    ]
    # ustawiamy ścieżkę systemową
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # krok 2 ------
    # konfiguracja aplikacji
    # lista użytkowników uprawnionych do korzystania z aplikacji
    config['users'] = [
        {
            "login""admin",
            "password""admin"
        }
    ]

    # krok 3 ------
    # konfiguracja bazy danych
    import config_database
    config["database"] = config_database.configure(config)

    # krok 4 ------
    # instancjonowanie warstw aplikacji
    import config_layers
    config['layers'] = config_layers.configure(config)

    # przekazujemy konfigurację
    return config
  • funkcja [configure] otrzymuje jako parametr słownik [config] (wiersz 1) i zwraca go jako wynik (wiersz 54) po wzbogaceniu jego zawartości. Już dawno można było stwierdzić, że nie ma potrzeby zwracania wyniku [config]. W rzeczywistości [config] jest odwołaniem do słownika, które kod wywołujący dzieli z kodem wywoływanym. Kod wywołujący posiada więc już tę referencję (wiersz 1) i nie ma sensu przekazywać jej ponownie (wiersz 54). Należy zatem zapisać:

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

jest zbędne. Wystarczy wpisać:


[module].configure(config) (2)

Niemniej jednak zachowałem ten sposób zapisu (1), ponieważ uznałem, że być może lepiej pokazuje on, iż kod wywoływany modyfikuje słownik [config].

  • wiersz 1: słownik [config] otrzymany przez funkcję [configure] posiada klucz „sgbd”, którego wartość pochodzi z listy [‘mysql’, ‘pgres’]. [mysql] oznacza, że używana baza danych jest zarządzana przez MySQL, natomiast „pgres” oznacza, że używana baza danych jest zarządzana przez PostgreSQL;
  • wiersze 4–27: wymieniono wszystkie foldery zawierające elementy niezbędne dla aplikacji internetowej. Będą one częścią ścieżki Python Path aplikacji (wiersze 30–31);
  • wiersze 33–40: dostęp do aplikacji zostanie przyznany tylko niektórym użytkownikom. W tym przypadku mamy listę zawierającą jednego użytkownika;
  • wiersze 43–46: skrypt [config_database] tworzy konfigurację używanej bazy danych;
  • wiersz 46: konfiguracja utworzona przez skrypt [config_database] jest słownikiem, który umieszczamy w ogólnej konfiguracji przypisanym do klucza „database”;
  • wiersze 48–51: skrypt [config_layers] instancjonuje warstwy aplikacji internetowej. Zwraca słownik, który jest zapisywany w ogólnej konfiguracji powiązanej z kluczem „layers”;

Skrypt [config_database] to ten sam, który został już użyty w skrypcie |version 5|. Przytaczamy go ponownie dla przypomnienia:


def configure(config: dict) -> dict:
    # konfiguracja SQLAlchemy
    from sqlalchemy import create_engine, Table, Column, Integer, MetaData, Float
    from sqlalchemy.orm import mapper, sessionmaker

    # ciągi połączeń z wykorzystywanymi bazami danych
    connection_strings = {
        'mysql': "mysql+mysqlconnector://admimpots:mdpimpots@localhost/dbimpots-2019",
        'pgres': „postgresql+psycopg2://admimpots:mdpimpots@localhost/dbimpots-2019”
    }
    # ciąg połączenia z wykorzystywaną bazą danych
    engine = create_engine(connection_strings[config['sgbd']])

    # metadane
    metadata = MetaData()

    # tabela stałych
    constantes_table = Table("tbconstantes", metadata,
                             Column('id', Integer, primary_key=True),
                             Column('plafond_qf_demi_part', Float, nullable=False),
                             Column('plafond_revenus_celibataire_pour_reduction', Float, nullable=False),
                             Column('plafond_revenus_couple_pour_reduction', Float, nullable=False),
                             Column('valeur_reduc_demi_part', Float, nullable=False),
                             Column('plafond_decote_celibataire', Float, nullable=False),
                             Column('plafond_decote_couple', Float, nullable=False),
                             Column('plafond_impot_celibataire_pour_decote', Float, nullable=False),
                             Column('plafond_impot_couple_pour_decote', Float, nullable=False),
                             Column('abattement_dixpourcent_max', Float, nullable=False),
                             Column('abattement_dixpourcent_min', Float, nullable=False)
                             )

    # tabela przedziałów podatkowych
    tranches_table = Table("tbtranches", metadata,
                           Column('id', Integer, primary_key=True),
                           Column('limite', Float, nullable=False),
                           Column('coeffr', Float, nullable=False),
                           Column('coeffn', Float, nullable=False)
                           )
    # mapowania
    from Tranche import Tranche
    mapper(Tranche, tranches_table)

    from Constantes import Constantes
    mapper(Constantes, constantes_table)

    # fabryka sesji
    session_factory = sessionmaker()
    session_factory.configure(bind=engine)

    # sesja
    session = session_factory()

    # zapisujemy pewne informacje i zwracamy je w słowniku
    return {"engine": engine, "metadata": metadata, "tranches_table": tranches_table,
            "constantes_table": constantes_table, "session": session}

Skrypt [config_layers] konfiguruje warstwy serwera internetowego. Wykorzystujemy skrypt |script|, który już wcześniej pojawił się:


def configure(config: dict) -> dict:
    # instancjonowanie warstw aplikacji
    
    # DAO
    from ImpotsDaoWithAdminDataInDatabase import ImpotsDaoWithAdminDataInDatabase
    dao = ImpotsDaoWithAdminDataInDatabase(config)
    
    # warstwa biznesowa
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    # instancje warstw umieszczamy w słowniku, który zwracamy do kodu wywołującego
    return {
        "dao": dao,
        "métier": métier
    }
  • wiersz 6: warstwa [dao] została zaimplementowana wraz z bazą danych;
  • [ImpotsDaoWithAdminDataInDatabase] zostało zdefiniowane jako |ici|;
  • [ImpôtsMétier] zostało zdefiniowane jako |ici|;

Główny skrypt [server_01] ma następującą treść:


# oczekuje się parametru mysql lub pgres
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# konfiguruje się aplikację
import config
config = config.configure({'sgbd': sgbd})

# zależności
from ImpôtsError import ImpôtsError
from TaxPayer import TaxPayer
import re
from flask import request
from myutils import json_response
from flask import Flask
from flask_api import status

# pobieranie danych z urzędu skarbowego
try:
    # admindata będzie danymi o zasięgu aplikacji, dostępnymi tylko do odczytu
    admindata = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# aplikacja Flask
app = Flask(__name__)


# Strona główna URL: /?marié=xx&enfants=yy&salaire=zz
@app.route('/', methods=['GET'])
def index():
    # początkowo brak błędów
    erreurs = []
    # żądanie musi zawierać trzy parametry w URL
    if len(request.args) != 3:
        erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")

    # pobieramy stan cywilny z URL
    marié = request.args.get('marié')
    if marié is None:
        erreurs.append("paramètre [marié] manquant")
    else:
        marié = marié.strip().lower()
        erreur = marié != "oui" and marié != "non"
        if erreur:
            erreurs.append(f"paramétre marié [{marié}] invalide")

    # pobieramy liczbę dzieci z URL
    enfants = request.args.get('enfants')
    if enfants is None:
        erreurs.append("paramètre [enfants] manquant")
    else:
        enfants = enfants.strip()
        match = re.match(r"^\d+", enfants)
        if not match:
            erreurs.append(f"paramétre enfants [{enfants}] invalide")
        else:
            enfants = int(enfants)

    # pobiera się wynagrodzenie z pliku URL
    salaire = request.args.get('salaire')
    if salaire is None:
        erreurs.append("paramètre [salaire] manquant")
    else:
        salaire = salaire.strip()
        match = re.match(r"^\d+", salaire)
        if not match:
            erreurs.append(f"paramétre salaire [{salaire}] invalide")
        else:
            salaire = int(salaire)

    # czy w pliku URL występują nieprawidłowe parametry?
    for key in request.args.keys():
        if key not in ['marié', 'enfants', 'salaire']:
            erreurs.append(f"paramètre [{key}] invalide")

    # czy wystąpiły błędy?
    if erreurs:
        # wysyłamy klientowi komunikat o błędzie
        résultats = {"réponse": {"erreurs": erreurs}}
        return json_response(résultats, status.HTTP_400_BAD_REQUEST)

    # brak błędów, można kontynuować pracę
    # obliczanie podatku
    taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, admindata)
    # wysyłamy odpowiedź do klienta
    return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)


# tylko tryb ręczny
if __name__ == '__main__':
    # uruchamiamy serwer Flask
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • wiersze 1–10: pobierany jest parametr wskazujący, którego SGBD należy użyć;
  • wiersze 12–14: na podstawie tej informacji można skonfigurować aplikację. W szczególności tworzona jest ścieżka Python Path;
  • wiersze 16–23: korzystając z nowej ścieżki Python Path, importujemy potrzebne elementy;
  • wiersze 25–31: pobieramy dane z urzędu skarbowego, które pozwalają obliczyć podatek;
  • wiersze 33–34: instancja aplikacji Flask;
  • wiersz 38: aplikacja Flask obsługuje wyłącznie URL [/]. Oczekuje na URL skonfigurowane w następujący sposób [/ ?marié=xx&enfants=yy&salaire=zz] z:
    • xx: tak / nie;
    • yy: liczba dzieci;
    • zz: roczne wynagrodzenie;
  • wiersze 40–89: sprawdzamy poprawność parametrów pliku URL;
  • wiersz 41: komunikaty o błędach są gromadzone na liście [erreurs];
  • wiersz 43: warto pamiętać, że parametry obiektu URL znajdują się w [request.args] (patrz |ici|):
    • obiekt [request] to obiekt Flask zaimportowany w wierszu 20;
    • obiekt [request.args] zachowuje się jak słownik;
  • wiersze 43–44: sprawdzamy, czy mamy dokładnie trzy parametry (ani mniej, ani więcej);
  • wiersze 46–49: sprawdzamy, czy parametr [marié] występuje w URL;
  • wiersze 50–54: jeśli występuje, sprawdzamy, czy jego wartość mała, po usunięciu spacji na początku i na końcu, to „tak” lub „nie”;
  • wiersze 56–59: sprawdzamy, czy parametr [enfants] znajduje się w URL;
  • wiersze 60–66: jeśli parametr występuje, sprawdza się, czy jego wartość jest dodatnią liczbą całkowitą;
  • wiersz 66: nie należy zapominać, że parametry z zestawu URL i ich wartości są ciągami znaków. Wartość parametru [enfants] jest przekształcana na typ „int”;
  • wiersze 68–78: w przypadku parametru [salaire] przeprowadza się te same testy, co w przypadku parametru [enfants];
  • wiersze 81–83: sprawdzamy, czy w URL nie ma żadnych parametrów innych niż [‘marié, ‘enfants’, ‘salaire’];
  • wiersze 85–89: jeśli po wszystkich tych weryfikacjach lista [erreurs] nie jest pusta, wysyłamy tę listę błędów do klienta w postaci ciągu znaków jSON oraz kodu statusu [400 Bad Request];

Ponieważ w dalszej części często będziemy mieli okazję wysyłać ciąg znaków jSON w odpowiedzi do klienta, kilka wierszy niezbędnych do tej operacji zostało wyodrębnionych do modułu [myutils.py], z którego już korzystaliśmy:

Image

Skrypt [myutils.py] przyjmuje następującą postać:


# importy
import json
import os
import sys

from flask import make_response


def set_syspath(absolute_dependencies: list):
    # absolute_dependencies: lista bezwzględnych nazw folderów

    ….


# generowanie odpowiedzi HTTP jSON
def json_response(réponse: dict, status_code: int) -> tuple:
    # treść odpowiedzi HTTP
    response = make_response(json.dumps(réponse, ensure_ascii=False))
    # treść odpowiedzi HTTP pochodzi z jSON
    response.headers['Content-Type'] = 'application/json; charset=utf-8'
    # wysyłamy odpowiedź HTTP
    return response, status_code
  • wiersz 16: funkcja [json_response] oczekuje dwóch parametrów:
    • [réponse]: słownik, z którego należy wysłać ciąg znaków jSON do klienta internetowego;
    • [status_code]: kod statusu odpowiedzi HTTP;
  • wiersz 18: ustalamy treść odpowiedzi jSON;
  • wiersz 20: dodaje się nagłówek HTTP, który informuje klienta internetowego, że otrzyma jSON;
  • wiersz 22: wysyłamy odpowiedź HTTP do kodu wywołującego. Do niego należy przesłanie jej do klienta internetowego;

Plik [__init__.py] zmienia się w następujący sposób:


from .myutils import set_syspath, json_response

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];

Kod skryptu [server_01] wygląda następująco:



    # czy wystąpiły błędy?
    if erreurs:
        # wysyłamy odpowiedź o błędzie do klienta
        résultats = {"réponse": {"erreurs": erreurs}}
        return json_response(résultats, status.HTTP_400_BAD_REQUEST)

    # brak błędów, można kontynuować pracę
    # obliczanie podatku
    taxpayer = TaxPayer().fromdict({'id': 0, 'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, admindata)
    # wysyłamy odpowiedź do klienta
    return json_response({"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK)

  • wiersz 10: w tym miejscu parametry oczekiwane w skrypcie URL są obecne i poprawne;
  • wiersz 10: tworzymy obiekt [TaxPayer], który modeluje podatnika;
  • wiersz 11: zlecamy warstwie [métier] obliczenie podatku. Przypominamy, że elementy obliczone przez warstwę [métier] są wstawiane do obiektu [taxpayer] przekazanego jako parametr;
  • wiersz 13: odpowiedź jest wysyłana do klienta internetowego w postaci ciągu znaków jSON. Jest to ciąg znaków jSON z słownika. W powiązaniu z kluczem [result] umieszczamy w nim słownik obiektu [taxpayer]. Nie można było umieścić samego obiektu [taxpayer], ponieważ nie da się go zserializować do postaci jSON;

Tworzymy dwie konfiguracje wykonania, jedną dla obiektu MySQL, drugą dla obiektu PostgreSQL:

Image

Oto kilka przykładów działania (uruchomiono aplikację [server_01] i wykorzystano SGBD, a następnie za pomocą przeglądarki wywołano adres http://localhost:5000/ z parametrem URL):

Image

Image

Oto przykładowe wyniki działania w konsoli Postmana:

Image


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

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

{"réponse": {"erreurs": ["paramètre marié [xx] invalide", "paramètre enfants [yy] invalide", "paramètre salaire [zz] invalide"]}}
  • wiersz 1: wysłano nieprawidłowy kod URL;
  • wiersz 10: serwer odpowiada statusem 400 BAD REQUEST;

23.2.2. Wersja 2

Image

W wersji 2 serwera przetwarzanie URL zostało wyodrębnione do modułu [index_controller] [5]:


# import zależności
import re

from flask_api import status
from werkzeug.local import LocalProxy


# URL z parametrami: /?marié=xx&enfants=yy&salaire=zz
def execute(request: LocalProxy, config: dict) -> tuple:
    # osoby pozostające na utrzymaniu
    from TaxPayer import TaxPayer

    # na początku brak błędów
    erreurs = []
    # zapytanie musi zawierać trzy parametry
    if len(request.args) != 3:
        erreurs.append("Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]")

    # pobieramy stan cywilny z URL
    marié = request.args.get('marié')
    if marié is None:
        erreurs.append("paramètre [marié] manquant")
    else:
        marié = marié.strip().lower()
        erreur = marié != "oui" and marié != "non"
        if erreur:
            erreurs.append(f"paramétre marié [{marié}] invalide")

    # pobierana jest liczba dzieci z URL
    enfants = request.args.get('enfants')
    if enfants is None:
        erreurs.append("paramètre [enfants] manquant")
    else:
        enfants = enfants.strip()
        match = re.match(r"^\d+", enfants)
        if not match:
            erreurs.append(f"paramétre enfants {enfants} invalide")
        else:
            enfants = int(enfants)

    # pobieramy wynagrodzenie z URL
    salaire = request.args.get('salaire')
    if salaire is None:
        erreurs.append("paramètre [salaire] manquant")
    else:
        salaire = salaire.strip()
        match = re.match(r"^\d+", salaire)
        if not match:
            erreurs.append(f"paramétre salaire {salaire} invalide")
        else:
            salaire = int(salaire)

    # inne parametry dotyczące URL?
    for key in request.args.keys():
        if not key in ['marié', 'enfants', 'salaire']:
            erreurs.append(f"paramètre [{key}] invalide")

    # czy wystąpiły błędy?
    if erreurs:
        # wysyłamy klientowi komunikat o błędzie
        résultats = {"réponse": {"erreurs": erreurs}}
        return résultats, status.HTTP_400_BAD_REQUEST

    # brak błędów, można kontynuować pracę
    # obliczanie podatku
    taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
    config["layers"]["métier"].calculate_tax(taxpayer, config["admindata"])
    # wysyłamy odpowiedź do klienta
    return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
  • wiersz 9: funkcja [execute] otrzymuje dwa parametry:
    • [request]: żądanie klienta HTTP;
    • [config]: słownik konfiguracyjny aplikacji;

Skrypt [server_02] wygląda następująco:


# oczekuje się na parametr mysql lub pgres
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# konfiguracja aplikacji
import config
config = config.configure({'sgbd': sgbd})

# zależności
from ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
import index_controller

# pobieranie danych z urzędu skarbowego
try:
    # admindata będzie danymi o zasięgu aplikacji, dostępnymi tylko do odczytu
    config['admindata'] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# aplikacja Flask
app = Flask(__name__)


# Strona główna URL: /?marié=xx&enfant=yy&salaire=zz
@app.route('/', methods=['GET'])
def index():
    # wykonywane jest zapytanie
    résultat, statusCode = index_controller.execute(request, config)
    # wysyłamy odpowiedź
    return json_response(résultat, statusCode)


# tylko w trybie main
if __name__ == '__main__':
    # uruchamia się serwer
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • wiersze 36–41: przetwarzanie ścieżki /;
  • wiersz 39: użycie funkcji [IndexController.execute];

Od tej pory będziemy stosować tę technikę: każda trasa będzie przetwarzana przez własny moduł.

Wyniki wykonania są takie same jak w wersji 1.

23.2.3. Wersja 3

Image

W wersji 3 wprowadzono pojęcie uwierzytelniania.

Skrypt [server_03] przyjmuje następującą postać:


# oczekuje się na parametr mysql lub pgres
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# konfiguracja aplikacji
import config
config = config.configure({'sgbd': sgbd})

# zależności
from ImpôtsError import ImpôtsError
from flask import request
from myutils import json_response
from flask import Flask
from flask_httpauth import HTTPBasicAuth
import index_controller

# pobieranie danych z urzędu skarbowego
try:
    # config[‘admindata’] będzie danymi o zasięgu aplikacji, dostępnymi wyłącznie do odczytu
    config["admindata"] = config["layers"]["dao"].get_admindata()
except ImpôtsError as erreur:
    print(f"L'erreur suivante s'est produite : {erreur}")
    sys.exit(1)

# moduł zarządzania uwierzytelnianiem
auth = HTTPBasicAuth()


# metoda uwierzytelniania
@auth.verify_password
def verify_credentials(login: str, password: str) -> bool:
    # lista użytkowników
    users = config['users']
    # przeglądamy tę listę
    for user in users:
        if user['login'] == login and user['password'] == password:
            return True
    # nie znaleziono
    return False


# aplikacja Flask
app = Flask(__name__)


# Strona główna URL: /?marié=xx&enfant=yy&salaire=zz
@app.route('/', methods=['GET'])
@auth.login_required
def index():
    # wykonywano zapytanie
    résultat, statusCode = index_controller.execute(request, config)
    # wysyłamy odpowiedź
    return json_response(résultat, statusCode)


# tylko w trybie main
if __name__ == '__main__':
    # uruchamia się serwer
    app.config.update(ENV="development", DEBUG=True)
    app.run()
  • wiersz 21: importujemy menedżera uwierzytelniania. Istnieją różne rodzaje uwierzytelniania na serwerze WWW. Ten, którego tutaj używamy, nazywa się [HTTP Basic]. Każdy rodzaj uwierzytelniania przebiega zgodnie z określonym dialogiem klient–serwer;
  • wiersz 33: tworzymy instancję modułu uwierzytelniającego;
  • wiersz 37: adnotacja [@auth.verify_password] oznacza funkcję, która ma zostać wykonana, gdy moduł uwierzytelniający chce zweryfikować login i hasło przesłane przez klienta zgodnie z protokołem [HTTP Basic];
  • wiersz 55: adnotacja [@auth.login_required] oznacza trasę, dla której klient internetowy musi zostać uwierzytelniony. Jeśli klient internetowy nie przesłał jeszcze swoich danych uwierzytelniających, serwer internetowy automatycznie poprosi go o ich podanie zgodnie z protokołem HTTP (basic);

Należy zainstalować moduł [flask_httpauth]:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\impots\http-servers\01\flask>pip install flask_httpauth
Collecting flask_httpauth
  Downloading Flask_HTTPAuth-4.1.0-py2.py3-none-any.whl (5.8 kB)
Requirement already satisfied: Flask in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from flask_httpauth) (1.1.2)
Requirement already satisfied: itsdangerous>=0.24 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (1.1.0)
Requirement already satisfied: click>=5.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (7.1.2)
Requirement already satisfied: Jinja2>=2.10.1 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (2.11.2)
Requirement already satisfied: Werkzeug>=0.15 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Flask->flask_httpauth) (1.0.1)
Requirement already satisfied: MarkupSafe>=0.23 in c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\lib\site-packages (from Jinja2>=2.10.1->Flask->flask_httpauth) (1.1.1
)
Installing collected packages: flask-httpauth
Successfully installed flask-httpauth-4.1.0

Zobaczmy, co się dzieje w konsoli Postman. Należy:

  • tworzysz konfigurację uruchomienia;
  • uruchamiają aplikację internetową;
  • uruchamiają wybrany plik SGBD;
  • wysyłasz żądanie do URL [/] za pomocą Postmana;

Dialog klient–serwer w konsoli Postman wygląda następująco:

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

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

Unauthorized Access
  • wiersz 10: serwer odpowiada, że nie mamy uprawnień dostępu do URL [/];
  • wiersz 13: serwer wskazuje protokół uwierzytelniania, którego należy użyć, w tym przypadku jest to tzw. uwierzytelnianie podstawowe;

Można skonfigurować Postmana tak, aby wysyłał dane uwierzytelniające użytkownika zgodnie z protokołem Auth Basic:

Image

  • w [6-7] umieszczamy dane uwierzytelniające zawarte w skrypcie [config]: Image

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

Dialog klient–serwer w konsoli Postman wygląda następująco:


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

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

{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]", "paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
  • wiersz 2: klient Postman wysyła w postaci zaszyfrowanej identyfikatory użytkownika [admin / admin];
  • wiersz 17: serwer odpowiada poprawnie. Sygnalizuje błędy, ponieważ nie wysłano parametrów [marié, enfants, salaire] (wiersz 1), ale nie zgłasza błędu uwierzytelnienia;

Teraz wywołajmy URL / za pomocą przeglądarki (poniżej Firefox):

Image

  • podobnie jak w przypadku Postmana, Firefox otrzymał od serwera odpowiedź HTTP z nagłówkami HTTP:
1
2
3
4
HTTP/1.0 401 UNAUTHORIZED
WWW-Authenticate: Basic realm="Authentication Required"

Firefox, podobnie jak inne przeglądarki, nie przerywa dialogu po otrzymaniu tych nagłówków. Prosi użytkownika o podanie danych uwierzytelniających wymaganych przez serwer. Wystarczy wpisać powyżej „admin / admin”, aby otrzymać odpowiedź z serwera:

Image

23.3. Klient internetowy serwera obliczającego podatek

23.3.1. Wprowadzenie

W poprzednim akapicie klientem internetowym serwera obliczającego podatek była przeglądarka. W tej części klientem internetowym będzie skrypt konsolowy. Architektura wygląda następująco:

Image

  • klient internetowy składa się z warstw [1-2];
  • serwer internetowy składa się z warstw [3-9]. Zostało to opisane w poprzednim akapicie;

Musimy zatem napisać warstwy [1-2].

Warstwa [dao] [2] musi umieć komunikować się z serwerem internetowym [3]. Znamy już protokół HTTP i moglibyśmy na przykład, korzystając z omówionego wcześniej modułu [pycurl], napisać skrypt komunikujący się z serwerem WWW [3]. Istnieją jednak moduły specjalizujące się w komunikacji klient-serwer typu HTTP. Wykorzystamy jeden z nich, moduł [requests]:


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

Struktura skryptów klienta internetowego wygląda następująco:

Image

Skrypt ten będzie realizował aplikację do obliczania podatku w trybie wsadowym, opisaną począwszy od pliku |version 1|. Najnowsza wersja tej aplikacji to |version 5|. Przypomnijmy jej działanie:

  • podatnicy, dla których zostanie obliczony podatek, są zgromadzeni w pliku tekstowym [taxpayersdata.txt]:
# prawidłowe dane: id, stan cywilny, dzieci, wynagrodzenie
1,oui,2,55555
2,oui,2,50000
3,oui,3,50000
4,non,2,100000
5,non,3,100000
6,oui,3,100000
7,oui,5,100000
8,non,0,100000
9,oui,2,30000
10,non,0,200000
11,oui,3,200000
# tworzy się błędne wiersze
# za mało wartości
11,12
# błędne wartości
x,x,x,x
  • wyniki są zapisywane w dwóch plikach:
  • plik tekstowy [errors.txt] zawiera błędy wykryte w pliku podatników:

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

Ligne 15, not enough values to unpack (expected 4, got 2)
Ligne 17, MyException[1, L'identifiant d'une entité <class 'TaxPayer.TaxPayer'> doit être un entier >=0]
  • (ciąg dalszy)
    • Plik jSON [résultats.json] zawiera wyniki obliczeń podatku dla poszczególnych podatników:

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

]

23.3.2. Konfiguracja klienta internetowego

Image

Konfiguracja odbywa się za pomocą dwóch skryptów:

  • [config], który zapewnia całość konfiguracji poza warstwami architektury;
  • [config_layers], który odpowiada za konfigurację warstw architektury;

Skrypt [config] ma następującą treść:


def configure(config: dict) -> dict:
    import os

    # etap 1 ------

    # katalog tego pliku
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # ścieżka główna
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # 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",
        # ImpôtsDaoWithHttpClient
        f"{script_dir}/../services",
        # skrypty konfiguracyjne
        script_dir,
    ]

    # ustawiamy ścieżkę systemową
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # krok 2 ------
    # konfiguracja aplikacji za pomocą stałych
    config.update({
        "taxpayersFilename"f"{script_dir}/../data/input/taxpayersdata.txt",
        "resultsFilename"f"{script_dir}/../data/output/résultats.json",
        "errorsFilename"f"{script_dir}/../data/output/errors.txt",
        "server": {
            "urlServer""http://127.0.0.1:5000/",
            "authBasic"True,
            "user": {
                "login""admin",
                "password""admin"
            }
        }
    }
    )

    # krok 3 ------
    # instancjonowanie warstw
    import config_layers
    config['layers'] = config_layers.configure(config)

    # ustawiamy konfigurację
    return config
  • wiersz 1: funkcja [configure] otrzymuje jako parametr słownik, który należy wypełnić informacjami konfiguracyjnymi. Może on być już wstępnie wypełniony lub pusty. W tym przypadku będzie pusty;
  • wiersze 40–42: bezwzględne nazwy trzech plików tekstowych obsługiwanych przez warstwę [dao];
  • wiersze 43–50: powiązane z kluczem [server] informacje, które warstwa [dao] musi znać na temat serwera WWW, z którym ma się komunikować:
    • wiersz 44: identyfikator URL usługi internetowej;
    • wiersz 45: klucz [authBasic] ma wartość True, jeśli dostęp do URL wymaga uwierzytelnienia typu Basic;
    • wiersze 46–49: dane identyfikacyjne użytkownika, który będzie się uwierzytelniał, jeśli uwierzytelnianie będzie wymagane;
  • wiersze 56–57: tworzymy instancje warstw, w tym przypadku jedynej warstwy [dao], i umieszczamy odniesienia do warstw w [config] powiązanych z kluczem [layers];

Skrypt [config_layers] wygląda następująco:


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

    # warstwa DAO
    from ImpôtsDaoWithHttpClient import ImpôtsDaoWithHttpClient
    dao = ImpôtsDaoWithHttpClient(config)

    # konfiguracja warstw
    return {
        "dao": dao
    }
  • wiersz 1: funkcja [configure] otrzymuje słownik konfigurujący aplikację;
  • wiersze 4–6: tworzona jest instancja warstwy [dao]. W wierszu 6 przekazywana jest jej konfiguracja aplikacji, w której znajdzie potrzebne informacje;
  • wiersze 8–11: zwracany jest słownik, w którym umieszczono odwołanie do warstwy [dao];

23.3.3. Główny skrypt [main]

Główny skrypt [main] jest wariantem skryptu |version 5|:


# konfiguracja aplikacji
import config
config = config.configure({})

# zależności
from ImpôtsError import ImpôtsError

# kod
try:
    # pobieranie warstwy [dao]
    dao = config["layers"]["dao"]
    # odczyt danych podatników
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    # podatników?
    if not taxpayers:
        raise ImpôtsError(f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    # obliczanie podatku podatników
    for taxpayer in taxpayers:
        # podatnik jest zarówno parametrem wejściowym, jak i wyjściowym
        # „podatnik” zostanie zmodyfikowany
        dao.calculate_tax(taxpayer)
    # zapis wyników do pliku tekstowego
    dao.write_taxpayers_results(taxpayers)
except ImpôtsError  as erreur:
    # wyświetlenie błędu
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # zakończono
    print("Travail terminé...")
  • wiersze 2–3: aplikacja jest skonfigurowana;
  • wiersz 13: warstwa [dao] dostarcza listę podatników, dla których należy obliczyć podatek;
  • wiersz 21: warstwa [dao] oblicza podatek dla każdego z nich;
  • wiersz 23: wyniki są zapisywane w pliku jSON;

23.3.4. Implementacja warstwy [dao]

Image

Wróćmy do zastosowanej architektury klient-serwer:

Image

  • w pliku [2, 6] widać, że warstwa [dao] pełni dwie funkcje:
    • uzyskuje dostęp do systemu plików zarówno w celu odczytu danych podatników, jak i zapisu wyników obliczeń podatkowych. Mamy już klasę |AbstractImpôtsDao|, która potrafi to zrobić. Została ona wykorzystana już w |version 4|;
    • komunikuje się z serwerem WWW [3];

W |version 5| główny skrypt [main] [1] komunikował się bezpośrednio z warstwą [métier] [4]. Nie chcielibyśmy zmieniać tego skryptu. W tym celu sprawimy, aby warstwa [dao] [2] implementowała interfejs warstwy [métier] [4]. W ten sposób główny skrypt [main] będzie miał wrażenie, że komunikuje się bezpośrednio z warstwą [métier] [4] i będzie mógł całkowicie zignorować fakt, że znajduje się ona na innym komputerze.

Definicja klasy implementującej warstwę [dao] [2] mogłaby wyglądać następująco:


class ImpôtsDaoWithHttpClient(AbstractImpôtsDao, InterfaceImpôtsMétier):
  • klasa [ImpôtsDaoWithHttpClient]:
    • dziedziczy po klasie [AbstractImpôtsDao], co pozwoli jej zarządzać komunikacją z systemem plików [6];
    • implementuje interfejs [InterfaceImpôtsMétier], aby nie trzeba było zmieniać głównego skryptu [main] klasy |version 5|;

Pełny kod klasy [ImpôtsDaoWithHttpClient] wygląda następująco:


# importy
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):

    # konstruktor
    def __init__(self, config: dict):
        # inicjalizacja obiektu nadrzędnego
        AbstractImpôtsDao.__init__(self, config)
        # zapis parametrów
        self.__config_server = config["server"]

    # nieużywana metoda z [AbstractImpôtsDao]
    def get_admindata(self) -> AdminData:
        pass

    # obliczenie podatku
    def calculate_tax(self: object, taxpayer: TaxPayer, admindata: AdminData = None):
        # pozwala się na zgłaszanie wyjątków
        # parametry funkcji 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 kierowane jest zapytanie
                self.__config_server['urlServer'],
                # parametry URL
                params=params,
                # uwierzytelnianie Basic
                auth=(
                    self.__config_server["user"]["login"],
                    self.__config_server["user"]["password"]))
        else:
            # połączenie bez uwierzytelniania Auth Basic
            response = requests.get(self.__config_server['urlServer'], params=params)
        # weryfikacja
        print(response.text)
        # 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 jest inny niż 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
        # modyfikujemy parametr wejściowy tym wynikiem
        taxpayer.fromdict(résultat["réponse"]["result"])
  • wiersze 21–23: klasa [AbstractImpôtsDao] (wiersz 12) posiada metodę abstrakcyjną [get_admindata]. Jesteśmy zmuszeni ją zaimplementować, nawet jeśli z niej nie korzystamy (admindata jest obsługiwane przez serwer, a nie przez klienta);
  • wiersz 26: metoda [calculate_tax] należy do interfejsu [InterfaceImpôtsMétier] (wiersz 12). Musimy ją zaimplementować;
  • wiersz 15: konstruktor otrzymuje jako jedyny parametr słownik konfiguracji aplikacji;
  • wiersze 16–17: klasa nadrzędna [AbstractImpôtsDao] jest inicjowana poprzez przekazanie jej, również w tym miejscu, konfiguracji aplikacji. Znajdzie w niej nazwy trzech plików tekstowych, którymi ma zarządzać;
  • wiersze 18–19: w klasie zapisujemy lokalnie informacje dotyczące serwera internetowego służącego do obliczania podatku;
  • wiersz 26: metoda [calculate_tax] otrzymuje jako parametr obiekt typu |Taxpayer|. Aby zachować sygnaturę metody [InterfaceImpôtsMétier.calculate_tax], otrzymuje ona również parametr [admindata], który ma hermetyzować dane urzędu skarbowego. Po stronie klienta nie dysponujemy tymi danymi. Parametr ten zawsze będzie miał wartość [None]. To skomplikowane rozwiązanie sugeruje, że klasa [ImpôtsMétier] została początkowo nieprawidłowo napisana:
  • sygnatura klasy [calculate_tax] powinna po prostu brzmieć:

def calculate_tax(self, taxpayer: TaxPayer)

, a parametr [admindata : AdminData] powinien zostać przekazany do konstruktora klasy;

  • wiersz 27: kod metody [calculate_tax] nie został zamknięty w bloku try / catch / finally. Oznacza to, że ewentualne wyjątki nie będą obsługiwane i zostaną przekazane do kodu wywołującego, w tym przypadku do skryptu [main]. Ten ostatni skutecznie przechwytuje wszystkie wyjątki przekazywane z warstwy [dao];
  • wiersz 28: obliczenie podatku odbywa się po stronie serwera. Konieczne będzie zatem nawiązanie z nim komunikacji. Odbywa się to za pomocą modułu [requests] zaimportowanego w wierszu 2;
  • wiersze 31–43: aby wysłać żądanie GET do serwera WWW, używa się metody [requests.get]:
    • wiersze 33–34: pierwszym parametrem metody jest adres URL, z którym należy się skontaktować;
    • wiersze 35–40: pozostałe dwa parametry to parametry nazwane, których kolejność nie ma znaczenia;
    • wiersze 35–36: wartość parametru o nazwie [params] musi być słownikiem zawierającym informacje, które mają zostać umieszczone w URL w postaci [/url ?param1=valeur1&param2=valeur2&…];
    • wiersz 29: słownik zawierający trzy parametry [marié, enfants, salaire], których oczekuje serwer WWW. Nie musimy zajmować się kodowaniem (zwanym urlencoded), któremu muszą zostać poddane te parametry. Zajmuje się tym [requests];
    • wiersze 37–40: parametr o nazwie [auth] jest krotką składającą się z dwóch elementów (login, password). Reprezentuje on dane uwierzytelniające dla uwierzytelniania typu Basic;
  • wiersze 44–45: te dwa wiersze mają wyłącznie charakter poglądowy (zostaną one skomentowane po zakończeniu debugowania):
    • [response] reprezentuje odpowiedź serwera o wartości HTTP;
    • [response.text] to tekst dokumentu zawarty w tej odpowiedzi. Podczas debugowania warto sprawdzić, co serwer nam przesłał;
  • wiersz 47: [response.status_code] to kod statusu HTTP otrzymanej odpowiedzi. Nasz serwer wysyła tylko trzy z nich:
    • 200 OK
    • 400 BAD REQUEST
    • 500 INTERNAL SERVER ERROR
  • wiersz 49: nasz serwer zawsze wysyła jSON, nawet w przypadku błędu. Funkcja [response.json()] tworzy słownik na podstawie otrzymanego ciągu znaków jSON. Przypomnijmy dwie możliwe formy ciągu znaków jSON:

{"réponse": {"erreurs": ["Méthode GET requise avec les seuls paramètres [marié, enfants, salaire]", "paramètre [marié] manquant", "paramètre [enfants] manquant", "paramètre [salaire] manquant"]}}
{"réponse": {"result": {"id": 0, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
  • wiersze 51–53: jeśli kod statusu nie wynosi 200, to generowany jest wyjątek z komunikatami o błędach zawartymi w odpowiedzi;
  • wiersz 56: pobieramy słownik wygenerowany w wyniku obliczenia podatku i wykorzystujemy go do aktualizacji parametru wejściowego [taxpayer];

23.3.5. Wykonanie

Aby uruchomić klienta:

  • uruchom serwer [server_03] z wybranym SGBD;
  • uruchom skrypt klienta [main];

Wyniki znajdą się w folderze [data/output]. Są one takie same jak w wersji 5.

23.4. Testy warstwy [dao]

Wróćmy do architektury aplikacji klient-serwer:

  • w napisanym kliencie zadbaliśmy o to, aby warstwa [dao] [1] oferowała ten sam interfejs, co warstwa [métier] [3]. W związku z tym w warstwie [4] wykorzystamy omówioną już wcześniej klasę testową |TestDaoMétier| do przetestowania warstw [métier] i [3];

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


class TestHttpClientDao(unittest.TestCase):

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

        # {'żonaty': 'tak', 'dzieci': 2, 'wynagrodzenie': 55555,
        # 'podatek': 2814, 'dopłata': 0, 'odliczenie': 0, 'ulga': 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)

    

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

        # {'stan cywilny': 'żonaty', 'dzieci': 3, 'wynagrodzenie': 200000,
        # 'podatek': 42842, 'dopłata': 17283, 'ulga': 0, 'obniżka': 0, 'stawka': 0,41}
        taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
        dao.calculate_tax(taxpayer)
        # weryfikacje
        self.assertAlmostEqual(taxpayer.impôt, 42842, 1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
        self.assertAlmostEqual(taxpayer.surcôte, 17283, delta=1)


if __name__ == '__main__':

    # konfigurujemy aplikację
    import config
    config = config.configure({})

    # warstwa DAO
    dao = config['layers']['dao']

    # uruchamianie metod testowych
    print("tests en cours...")
    unittest.main()

Klasa ta jest analogiczna do klasy |celle|, którą omówiliśmy już w wersji 4 aplikacji.

  • wiersze 40–41: konfigurujemy środowisko testowe;
  • wiersz 44: pobieramy odwołanie do warstwy [dao];
  • wiersze 47–48: uruchamia się testy;

Aby uruchomić testy, tworzy się plik |configuration d’exécution|:

Image

  • tworzymy konfigurację uruchomienia dla skryptu konsolowego, a nie dla testu UnitTest;

Po uruchomieniu tej konfiguracji otrzymujemy następujące wyniki:

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

OK

Process finished with exit code 0

Wszystkie 11 testów zakończyło się powodzeniem.