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ę:

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

- 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

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

- 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:

- 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:

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:

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


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

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

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

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:
- 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:

- w [6-7] umieszczamy dane uwierzytelniające zawarte w skrypcie [config]:
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):

- podobnie jak w przypadku Postmana, Firefox otrzymał od serwera odpowiedź HTTP z nagłówkami HTTP:
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:

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:

- 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:

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

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]

Wróćmy do zastosowanej architektury klient-serwer:

- 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¶m2=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:

- 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|:

- tworzymy konfigurację uruchomienia dla skryptu konsolowego, a nie dla testu UnitTest;
Po uruchomieniu tej konfiguracji otrzymujemy następujące wyniki:
Wszystkie 11 testów zakończyło się powodzeniem.