30. Ćwiczenie praktyczne: wersja 12
W tym rozdziale napiszemy aplikację internetową zgodną z architekturą MVC (Model-Widok-Kontroler). Aplikacja będzie mogła generować odpowiedzi w trzech formatach: jSON, XML, HTML. Istnieje znaczny skok w złożoności między tym, co zamierzamy teraz zrobić, a tym, co zostało zrobione wcześniej. Wykorzystamy większość omówionych dotychczas koncepcji i szczegółowo opiszemy wszystkie etapy prowadzące do powstania gotowej aplikacji.
30.1. Architektura MVC
Zaimplementujemy model architektury znany jako MVC (Model – Widok – Kontroler) w następujący sposób:
Przetwarzanie żądania klienta będzie przebiegało w następujący sposób:
- 1 – żądanie
Żądane obiekty URL będą miały postać http://machine:port/action/param1/param2/…. Obiekt [Contrôleur principal] wykorzysta plik konfiguracyjny do „przekierowania” żądania do właściwego kontrolera. W tym celu wykorzysta pole [action] z URL. Pozostała część URL [param1/param2/…] składa się z opcjonalnych parametrów, które zostaną przekazane do akcji. C w MVC to w tym przypadku ciąg znaków [Contrôleur principal, Contrôleur / Action]. Jeśli żaden kontroler nie będzie w stanie obsłużyć żądanej akcji, serwer WWW odpowie, że żądana akcja URL nie została znaleziona.
- 2 – przetwarzanie
- Wybrana akcja [2a] może wykorzystać parametry parami, które przekazał jej [Contrôleur principal]. Mogą one pochodzić z dwóch źródeł:
- ze ścieżki [/param1/param2/…] funkcji URL,
- z parametrów umieszczonych w treści żądania klienta;
- podczas przetwarzania żądania użytkownika akcja może wymagać warstwy [métier] [2b]. Po przetworzeniu żądania klienta może ono wywołać różne odpowiedzi. Klasycznym przykładem jest:
- odpowiedź o błędzie, jeśli żądanie nie mogło zostać poprawnie przetworzone;
- w przeciwnym razie odpowiedź potwierdzająca;
- [Contrôleur / Action] zwróci swoją odpowiedź [2c] do głównego kontrolera wraz z kodem stanu. Kody te w jednoznaczny sposób odzwierciedlają stan, w jakim znajduje się aplikacja. Będzie to albo kod sukcesu, albo kod błędu;
- 3 – odpowiedź
- w zależności od tego, czy klient poprosił o odpowiedź jSON, XML czy HTML, moduł [Contrôleur principal] zainicjuje odpowiedni typ odpowiedzi za pomocą modułu [3a] i poprosi go o wysłanie odpowiedzi do klienta. [Contrôleur principal] przekaże mu zarówno odpowiedź, jak i kod stanu dostarczone przez uruchomiony moduł [Contrôleur / Action];
- jeśli żądana odpowiedź jest typu jSON lub XML, wybrana odpowiedź sformatuje odpowiedź otrzymaną od [Contrôleur / Action] i wyśle ją do [3c]. Klientem zdolnym do wykorzystania tej odpowiedzi może być skrypt konsolowy w języku Python lub skrypt JavaScript umieszczony na stronie o nazwie HTML;
- jeśli pożądana odpowiedź jest typu HTML, wybrana odpowiedź wybierze jeden z widoków [3b] (HTML lub [Vuei]) na podstawie podanego kodu stanu. Jest to widok V dla MVC. Jednemu kodowi stanu odpowiada jeden widok. Ten widok V wyświetli odpowiedź z [Contrôleur / Action], który został wykonany. Wykorzystuje ono HTML, CSS oraz JavaScript do sformatowania danych tej odpowiedzi. Dane te nazywamy modelem widoku. To właśnie litera M w MVC. Klientem jest wówczas najczęściej przeglądarka;
Teraz wyjaśnijmy związek między architekturą internetową MVC a architekturą warstwową. W zależności od definicji modelu te dwie koncepcje mogą być ze sobą powiązane lub nie. Weźmy na przykład jednowarstwową aplikację internetową MVC:

W powyższym przykładzie każdy z elementów [Contrôleur / Action] zawiera część warstw [métier] i [dao]. W warstwie [web] rzeczywiście występuje architektura MVC, ale cała aplikacja nie ma architektury warstwowej. Tutaj istnieje tylko jedna warstwa – warstwa internetowa – która wykonuje wszystkie zadania.
Rozważmy teraz wielowarstwową architekturę internetową:

Warstwa [web] może zostać zaimplementowana bez stosowania się do modelu MVC. Mamy wówczas architekturę wielowarstwową, ale warstwa internetowa nie implementuje modelu MVC.
Na przykład w środowisku .NET warstwa [web]może być zaimplementowana przy użyciu ASP.NET i MVC, co daje architekturę warstwową z warstwą [web] typu MVC. Po wykonaniu tej czynności można zastąpić tę warstwę ASP.NET MVC klasyczną warstwą ASP.NET (WebForms), zachowując pozostałe elementy (biznesową, DAO, sterownik) bez zmian. Otrzymujemy wówczas architekturę warstwową z warstwą [web], która nie jest już typu MVC.
W MVC stwierdziliśmy, że model M odpowiada widokowi V, c.a.d, czyli zbiorowi danych wyświetlanych przez widok V. Podano kolejną definicję modelu M dla MVC:

Wielu autorów uważa, że to, co znajduje się po prawej stronie warstwy [web], tworzy model M z MVC. Aby uniknąć niejasności, można mówić o:
- o modelu domeny, gdy mamy na myśli wszystko, co znajduje się po prawej stronie warstwy [web];
- o modelu widoku, gdy mamy na myśli dane wyświetlane przez widok V;
W dalszej części tekstu, gdy będziemy mówić o modelu, zawsze będzie chodziło o model widoku.
30.2. Architektura aplikacji klient-serwer
Aplikacja internetowa będzie miała następującą architekturę:
- w [1] serwer internetowy będzie obsługiwał dwa rodzaje klientów:
- w [2] – klient konsolowy, który będzie wymieniał dane jSON i XML z serwerem;
- w [3] przeglądarka, która będzie odbierać dane HTML z serwera i wyświetlać je;
- serwer WWW [1] zachowuje warstwy [métier] i [dao] z poprzednich wersji;
- klient internetowy [2] zostanie zaktualizowany, aby uwzględnić nowe wersje usługi aplikacji internetowej URL;
- aplikacja HTML wyświetlana w przeglądarce wymaga całkowitego przepisania;
Będziemy tworzyć aplikację w kilku etapach:
- najpierw opracujemy wersję serwera o numerze jSON. Będziemy testować kolejno usługi serwera o numerach URL za pomocą klienta Postman. Metoda ta pozwala nam zbudować szkielet serwera WWW bez konieczności zajmowania się widokami (=HTML) aplikacji;
- po przetestowaniu serwera jSON za pomocą Postmana przetestujemy go za pomocą klienta konsolowego;
- następnie przejdziemy do wersji serwera XML. Widzieliśmy już, że przejście z wersji jSON do XML było bardzo proste;
- na koniec przejdziemy do wersji serwera HTML. Zbudujemy architekturę MVC i zdefiniujemy widoki do wyświetlenia. Aplikacja HTML zostanie przetestowana zarówno za pomocą klienta Postman, jak i zwykłej przeglądarki;
30.3. Struktura kodu serwera

- w [1: cały serwer WWW;
- w [2]: na razie pominęmy foldery [static, templates, tests_views], które dotyczą wersji serwera HTML. Poza tym folderem znajdziemy główny skrypt [main] oraz jego konfigurację;
- w [3] – kontrolery serwera WWW. Będą to instancje klas;
![]() | ![]() |
- w pliku [4] odpowiedź serwera HTTP będzie obsługiwana przez klasy;
- w [5] zachowujemy plik logów z poprzednich serwerów;
Kiedy stworzymy wersję serwera HTML, pojawią się inne foldery:
![]() | ![]() |
- w wersji [6] – statyczne elementy aplikacji HTML;
- w pliku [7] szablony aplikacji HTML, rozbite na widoki [9] i fragmenty widoków [8];
- w [9] – klasy implementujące modele widoków;
30.4. URL – usługi aplikacji
Aby zbudować serwer WWW, postąpimy w następujący sposób:
- na podstawie widoków aplikacji HTML zdefiniujemy akcje, które musi realizować aplikacja internetowa. Wykorzystamy tutaj rzeczywiste widoki, ale mogłyby to być po prostu widoki na papierze;
- na podstawie tych akcji zdefiniujemy elementy serwisu aplikacji URL w aplikacji HTML;
- zaimplementujemy te URL usług przy pomocy serwera dostarczającego jSON. Pozwala to zdefiniować szkielet serwera internetowego bez konieczności zajmowania się stronami HTML, które mają być dostarczane. Przetestujemy te usługi URL za pomocą Postmana;
- następnie przetestujemy nasz serwer jSON za pomocą klienta konsolowego;
- gdy serwer jSON zostanie zatwierdzony, przejdziemy do pisania aplikacji HTML;
Pierwszym widokiem będzie widok uwierzytelniania:

- akcja prowadząca do tego pierwszego widoku będzie nosiła nazwę [init-session] [1];
- kliknięcie przycisku [Valider] uruchomi akcję [authentifier-utilisateur] z dwoma parametrami przesłanymi metodą POST: [2-3];
Widok obliczeń podatku:

- w [1] – akcja [authentifier-utilisateur], która doprowadziła do tego widoku;
- w [2] kliknięcie przycisku [Valider] uruchamia akcję [calculer-impot] z trzema przekazanymi parametrami [2-5];
- kliknięcie linku [6] uruchamia akcję [lister-simulations] bez parametrów;
- kliknięcie linku [7] uruchamia akcję [fin-session] bez parametrów;
Trzeci widok przedstawia symulacje przeprowadzone przez uwierzytelnionego użytkownika:

- w [3] – akcja [lister-simulations], która doprowadziła do wyświetlenia tego widoku;
- w [2] kliknięcie linku [Supprimer] uruchamia akcję [supprimer-simulation] z jednym parametrem – numerem symulacji, którą należy usunąć z listy;
- kliknięcie linku [3] uruchamia akcję [afficher-calcul-impot] bez parametrów, która ponownie wyświetla widok obliczeń podatku;
- kliknięcie linku [4] uruchamia akcję [fin-session] bez parametrów;
Na podstawie tych wstępnych informacji możemy zdefiniować różne akcje serwisowe serwera URL:
Akcja | Rola | Kontekst wykonania |
/init-session | Służy do określenia typu (json, xml, html) oczekiwanych odpowiedzi | Żądanie GET Może zostać wysłane w dowolnym momencie |
/authentifier-utilisateur | Zezwala lub nie zezwala użytkownikowi na zalogowanie się | Żądanie POST. Żądanie musi zawierać dwa parametry wysyłane metodą POST [user, password] Może zostać wysłane tylko wtedy, gdy typ sesji (json, xml, html) jest znany |
/obliczyć-podatek | Przeprowadza symulację obliczenia podatku | Żądanie POST. Żądanie musi zawierać trzy parametry wysyłane metodą POST: [marié, enfants, salaire] Może zostać wysłane tylko wtedy, gdy znany jest typ sesji (json, xml, html) i użytkownik jest uwierzytelniony |
/lister-simulations | Żądanie wyświetlenia listy symulacji przeprowadzonych od początku sesji | Żądanie GET. Może zostać wysłane tylko wtedy, gdy typ sesji (json, xml, html) jest znany, a użytkownik uwierzytelniony |
/usuń-symulację/numer | Usuwa symulację z listy symulacji | Żądanie GET. Może zostać wysłane tylko wtedy, gdy znany jest typ sesji (json, xml, html) i użytkownik jest uwierzytelniony |
/wyświetl-obliczenie-podatku | Wyświetla stronę HTML z obliczeniami podatku | Żądanie GET. Może zostać wysłane tylko wtedy, gdy znany jest typ sesji (json, xml, html) i użytkownik jest uwierzytelniony |
/zakończ-sesję | Zamyka sesję symulacji. | Z technicznego punktu widzenia poprzednia sesja internetowa zostaje usunięta, a tworzona jest nowa sesja Może zostać wysłane tylko wtedy, gdy znany jest typ sesji (json, xml, html) i użytkownik jest uwierzytelniony |
Te różne kody serwisowe URL będą wykorzystywane zarówno dla serwera HTML, jak i dla serwerów jSON lub XML. Dwa elementy URL będą wykorzystywane wyłącznie dla tych dwóch ostatnich serwerów: są to elementy URL z poprzedniej wersji klienta/serwera WWW, które tutaj ponownie wykorzystujemy:
Akcja | Rola | Kontekst wykonania |
/get-admindata | Zwraca dane podatkowe umożliwiające obliczenie podatku | Zapytanie GET. Używane tylko wtedy, gdy typ sesji to json lub xml. Użytkownik musi być uwierzytelniony |
/calculer-impots | Oblicza podatek dla listy podatników przesłanych w jSON | Żądanie GET. Używane tylko wtedy, gdy typ sesji to json lub xml. Użytkownik musi być uwierzytelniony |
Wszystkie kontrolery powiązane z tymi akcjami będą działać w ten sam sposób:
- sprawdzają swoje parametry. Znajdują się one w obiekcie:
- [request.path] dla parametrów obecnych w URL w postaci [/action/param1/param2/…];
- w obiekcie [request.form] dla tych, które są przekazywane w [x-www-form-urlencoded] w treści żądania;
- w obiekcie [request.data] dla tych, które są przekazywane w obiekcie jSON w treści żądania;
- kontroler przypomina funkcję lub metodę, która sprawdza poprawność swoich parametrów. W przypadku kontrolera sprawa jest jednak nieco bardziej skomplikowana:
- oczekiwane parametry mogą nie występować;
- parametry pobierane przez kontroler to ciągi znaków. Jeśli oczekiwanym parametrem jest liczba, kontroler musi sprawdzić, czy ciąg znaków parametru rzeczywiście odpowiada liczbie;
- po sprawdzeniu, czy oczekiwane parametry są obecne i poprawne pod względem składniowym, należy sprawdzić, czy są one prawidłowe w bieżącym kontekście wykonania. Kontekst ten jest zawarty w sesji. Przykładem kontekstu wykonania jest uwierzytelnianie. Niektóre akcje mogą być przetwarzane dopiero po uwierzytelnieniu klienta. Zazwyczaj klucz w sesji wskazuje, czy uwierzytelnienie miało miejsce, czy nie;
- Po przeprowadzeniu powyższych weryfikacji kontroler pomocniczy może rozpocząć pracę. Ta weryfikacja parametrów jest niezwykle ważna. Nie można dopuścić, aby klient przesyłał nam dowolne dane w dowolnym momencie cyklu życia aplikacji. Musimy mieć pełną kontrolę nad całym cyklem życia aplikacji;
- po zakończeniu swojej pracy kontroler pomocniczy zwraca słownik z kluczami [action, état, réponse] do kontrolera głównego, który go wywołał:
- [action] to akcja, która właśnie została wykonana;
- [état] to trzycyfrowa liczba wskazująca wynik przetworzenia akcji:
- [x00] oznacza pomyślne przetworzenie;
- [x01] sygnalizuje niepowodzenie przetworzenia;
- [réponse] to słownik wyników w postaci {‘odpowiedź’:obiekt}. Obiekt będzie miał różną strukturę w zależności od przetwarzanej akcji;
Przejdziemy teraz do omówienia poszczególnych kontrolerów, czyli – co sprowadza się do tego samego – różnych akcji, które te kontrolery obsługują i które nadają rytm działaniu aplikacji internetowej.
30.5. Konfiguracja serwera

Konfiguracja bazy danych [config_database] oraz warstw serwera [config_layers] jest identyczna jak w poprzednich wersjach. W pliku [config] pojawiają się nowe informacje:
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
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",
# Rejestrator, SendAdminMail
f"{root_dir}/impots/http-servers/02/utilities",
# skrypty [config_database, config_layers]
script_dir,
# kontrolery
f"{script_dir}/../controllers",
# odpowiedzi HTTP
f"{script_dir}/../responses",
# szablony widoków
f"{script_dir}/../models_for_views",
]
# ustawiamy ścieżkę systemową
from myutils import set_syspath
set_syspath(absolute_dependencies)
# zależności serwera WWW
# kontrolery
from AfficherCalculImpotController import AfficherCalculImpotController
from AuthentifierUtilisateurController import AuthentifierUtilisateurController
from CalculerImpotController import CalculerImpotController
from CalculerImpotsController import CalculerImpotsController
from FinSessionController import FinSessionController
from GetAdminDataController import GetAdminDataController
from InitSessionController import InitSessionController
from ListerSimulationsController import ListerSimulationsController
from MainController import MainController
from SupprimerSimulationController import SupprimerSimulationController
# odpowiedzi HTTP
from HtmlResponse import HtmlResponse
from JsonResponse import JsonResponse
from XmlResponse import XmlResponse
# szablony widoków
from ModelForAuthentificationView import ModelForAuthentificationView
from ModelForCalculImpotView import ModelForCalculImpotView
from ModelForErreursView import ModelForErreursView
from ModelForListeSimulationsView import ModelForListeSimulationsView
# krok 2 ------
# konfiguracja aplikacji
config.update({
# użytkownicy uprawnieni do korzystania z aplikacji
"users": [
{
"login": "admin",
"password": "admin"
}
],
# plik logów
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# konfiguracja serwera SMTP
"adminMail": {
# serwer SMTP
"smtp-server": "localhost",
# port serwera SMTP
"smtp-port": "25",
# administrator
"from": "guest@localhost.com",
"to": "guest@localhost.com",
# temat wiadomości e-mail
"subject": "plantage du serveur de calcul d'impôts",
# tls ustawione na True, jeśli serwer SMTP wymaga uwierzytelnienia, w przeciwnym razie na False
"tls": False
},
# czas przerwy wątku w sekundach
"sleep_time": 0,
# dozwolone działania i ich kontrolery
"controllers": {
# inicjowanie sesji obliczeniowej
"init-session": InitSessionController(),
# uwierzytelnianie użytkownika
"authentifier-utilisateur": AuthentifierUtilisateurController(),
# obliczanie podatku w trybie indywidualnym
"calculer-impot": CalculerImpotController(),
# obliczanie podatku w trybie zbiorczym
"calculer-impots": CalculerImpotsController(),
# lista symulacji
"lister-simulations": ListerSimulationsController(),
# usunięcie symulacji
"supprimer-simulation": SupprimerSimulationController(),
# zakończenie sesji obliczeniowej
"fin-session": FinSessionController(),
# wyświetlenie widoku obliczeń podatku
"afficher-calcul-impot": AfficherCalculImpotController(),
# pobieranie danych z urzędu skarbowego
"get-admindata": GetAdminDataController(),
# główny kontroler
"main-controller": MainController()
},
# różne typy odpowiedzi (json, xml, html)
"responses": {
"json": JsonResponse(),
"html": HtmlResponse(),
"xml": XmlResponse()
},
# widoki HTML i ich szablony zależą od stanu zwracanego przez kontroler
"views": [
{
# widok uwierzytelniania
"états": [
# /init-session – operacja zakończona powodzeniem
700,
# /uwierzytelnianie-użytkownika – niepowodzenie
201
],
"view_name": "views/vue-authentification.html",
"model_for_view": ModelForAuthentificationView()
},
{
# widok obliczeń podatku
"états": [
# /uwierzytelnianie-użytkownika zakończone sukcesem
200,
# /obliczyć-podatek pomyślnie
300,
# /obliczyć-podatek niepowodzenie
301,
# /wyświetl-obliczenia-podatku
800
],
"view_name": "views/vue-calcul-impot.html",
"model_for_view": ModelForCalculImpotView()
},
{
# widok listy symulacji
"états": [
# /wyświetl-listę-symulacji
500,
# /usuń-symulację
600
],
"view_name": "views/vue-liste-simulations.html",
"model_for_view": ModelForListeSimulationsView()
}
],
# widok nieoczekiwanych błędów
"view-erreurs": {
"view_name": "views/vue-erreurs.html",
"model_for_view": ModelForErreursView()
},
# przekierowania
"redirections": [
{
"états": [
400, # /pomyślne zakończenie sesji
],
# przekierowanie do
"to": "/init-session/html",
}
],
}
)
# etap 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
- do wiersza 41 znajdują się standardowe elementy;
- wiersze 43–66: w wierszu 43 definiowana jest ścieżka Python Path serwera. Następnie można zaimportować zależności projektu:
- wiersze 45–55: lista kontrolerów;
- wiersze 57–60: lista odpowiedzi HTTP;
- wiersze 62–66: lista szablonów widoków;
- wiersze 68–189: konfiguracja aplikacji z wykorzystaniem szeregu stałych;
- wiersze 71–98: znamy już te wiersze z poprzednich wersji;
- wiersze 101–122: słownik kontrolerów:
- klucze to nazwy akcji;
- wartościami są instancje kontrolera, który ma obsługiwać daną akcję. Każdy kontroler jest instancjonowany tylko w jednym egzemplarzu (singleton). Ta sama instancja będzie wykonywana przez różne wątki serwera. Należy zatem zwrócić uwagę na dane współdzielone, które każdy kontroler mógłby chcieć modyfikować;
- wiersze 125–129: słownik trzech możliwych odpowiedzi HTTP:
- klucze to typ odpowiedzi oczekiwanej przez klienta (jSON, xml, html);
- wartościami są instancje odpowiedzi HTTP. Każdy generator odpowiedzi jest instancjonowany tylko w jednym egzemplarzu (singleton). Ten sam generator będzie uruchamiany przez różne wątki serwera. Należy zatem zwrócić uwagę na dane współdzielone, które każdy generator może chcieć modyfikować;
- wiersze 132–186: konfiguracja widoków HTML. Na razie pomijamy te wiersze;
- wiersze 191–202: te wiersze znamy już z poprzednich wersji;
30.6. Przebieg żądania klienta na serwerze

Prześledzimy drogę żądania klienta przychodzącego na serwer aż do wysłanej w odpowiedzi odpowiedzi HTTP. Żądanie to podąża ścieżką serwera MVC.
30.6.1. Skrypt [main]

Skrypt [main] jest pod wieloma względami identyczny jak w poprzednich wersjach. Przedstawiamy go jednak w całości, aby zapewnić solidną podstawę:
# 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 flask import request, Flask, session, url_for, redirect
from flask_api import status
from SendAdminMail import SendAdminMail
from myutils import json_response
from Logger import Logger
import threading
import time
from random import randint
from ImpôtsError import ImpôtsError
import os
# wysyłanie wiadomości e-mail do administratora
def send_adminmail(config: dict, message: str):
# wysyłanie wiadomości e-mail do administratora aplikacji
config_mail = config["adminMail"]
config_mail["logger"] = config['logger']
SendAdminMail.send(config_mail, message)
# sprawdzanie pliku logów
logger = None
erreur = False
message_erreur = None
try:
# moduł rejestrujący
logger = Logger(config["logsFilename"])
except BaseException as exception:
# log konsoli
print(f"L'erreur suivante s'est produite : {exception}")
# odnotowuje się błąd
erreur = True
message_erreur = f"{exception}"
# zapisywanie modułu logującego w konfiguracji
config['logger'] = logger
# obsługa błędu
if erreur:
# wiadomość e-mail do administratora
send_adminmail(config, message_erreur)
# zakończenie działania aplikacji
sys.exit(1)
# log uruchomienia
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)
# pobieranie danych z urzędu skarbowego
erreur = False
try:
# admindata będzie danymi o zasięgu aplikacji, dostępnymi tylko do odczytu
config["admindata"] = config["layers"]["dao"].get_admindata().asdict()
# log pomyślnego zakończenia
logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
# odnotowano błąd
erreur = True
# log błędu
log = f"L'erreur suivante s'est produite : {ex}"
# konsola
print(log)
# plik logów
logger.write(f"{log}\n")
# wiadomość e-mail do administratora
send_adminmail(config, log)
# główny wątek nie potrzebuje już modułu logującego
logger.close()
# w razie błędu program się zatrzymuje
if erreur:
sys.exit(2)
# aplikacja Flask
app = Flask(__name__, template_folder="templates", static_folder="static")
# tajny klucz sesji
app.secret_key = os.urandom(12).hex()
# kontroler frontowy
def front_controller() -> tuple:
# przetwarzamy żądanie
logger = None
…
@app.route('/', methods=['GET'])
def index() -> tuple:
# przekierowanie do /init-session/html
return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)
# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
# uruchamianie kontrolera powiązanego z akcją
return front_controller()
# uwierzytelnianie użytkownika
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
# uruchamiany jest kontroler powiązany z akcją
return front_controller()
# oblicz-podatek
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
# uruchamia się kontroler powiązany z akcją
return front_controller()
# wyświetl-symulacje
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
# uruchamia się kontroler powiązany z akcją
return front_controller()
# usuń-symulację
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
# uruchamia się kontroler powiązany z akcją
return front_controller()
# koniec sesji
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
# uruchamia się kontroler powiązany z akcją
return front_controller()
# wyświetl-obliczenia-podatku
@app.route('/afficher-calcul-impot', methods=['GET'])
def afficher_calcul_impot() -> tuple:
# uruchamiany jest kontroler powiązany z akcją
return front_controller()
# pobierz-dane-administracyjne
@app.route('/get-admindata/<int:numero>', methods=['GET'])
def get_admindata() -> tuple:
# uruchamia się kontroler powiązany z akcją
return front_controller()
# tylko main
if __name__ == '__main__':
# uruchamia się serwer
app.config.update(ENV="development", DEBUG=True)
app.run(threaded=True)
- wiersze 1–92: wszystkie te wiersze zostały już omówione i wyjaśnione;
- wiersz 92: serwer będzie zarządzał sesją. Potrzebujemy więc tajnego klucza. Dla każdego użytkownika umieścimy w sesji dwie informacje:
- czy użytkownik poprawnie się uwierzytelnił;
- za każdym razem, gdy użytkownik wykona obliczenia podatkowe, wyniki tych obliczeń zostaną umieszczone na liście, którą nazwiemy listą symulacji użytkownika. Lista ta zostanie umieszczona w sesji;
- wiersze 100–151: lista usług serwera URL. Powiązane funkcje pełnią rolę filtra: wszystkie URL nieobecne na tej liście zostaną odrzucone przez serwer Flask z błędem [404 NOT FOUND]. Po przejściu przez to filtrowanie żądanie jest systematycznie przekazywane do „Front Controllera” zaimplementowanego przez funkcję [front_controller] w wierszach 94–98, którą wkrótce omówimy;
- wiersze 100–103: obsługa trasy [/]. Punktem wejścia do aplikacji internetowej będzie funkcja URL z linii 107. Również w linii 103 przekierowujemy klienta do tej funkcji URL:
- funkcja [url_for] jest importowana w wierszu 18. Posiada ona tutaj dwa parametry:
- pierwszy parametr to nazwa jednej z funkcji routingu, w tym przypadku tej z linii 107. Widzimy, że funkcja ta oczekuje parametru [type_response], który określa typ odpowiedzi (json, xml, html) oczekiwany przez klienta;
- drugi parametr przejmuje nazwę parametru z wiersza 107, [type_response], i nadaje mu wartość. Gdyby istniały inne parametry, operację tę powtórzono by dla każdego z nich;
- zwraca parametr URL powiązany z funkcją wskazaną przez dwa podane mu parametry. W tym przypadku będzie to parametr URL z wiersza 106, gdzie parametr został zastąpiony wartością [/init-session/html];
- funkcja [redirect] została zaimportowana w wierszu 18. Jej zadaniem jest wysłanie do klienta nagłówka przekierowania HTTP:
- pierwszym parametrem jest adres URL, na który klient ma zostać przekierowany;
- drugim parametrem jest kod statusu odpowiedzi HTTP udzielonej klientowi. Kod [status.HTTP_302_FOUND] odpowiada przekierowaniu HTTP;
Funkcja [front_controller] w wierszach 94–98 przeprowadza wstępną obsługę żądania klienta:
# kontroler front-end
def front_controller() -> tuple:
# przetwarzanie żądania
logger = None
try:
# rejestrowanie
logger = Logger(config["logsFilename"])
# zapisywanie w konfiguracji powiązanej z wątkiem
thread_config = {"logger": logger}
thread_name = threading.current_thread().name
config[thread_name] = {"config": thread_config}
# rejestruje się żądanie
logger.write(f"[ front_controller] requête : {request}\n")
# przerywamy wątek, jeśli zostało to zlecone
sleep_time = config["sleep_time"]
if sleep_time != 0:
# przerwa jest losowa, aby niektóre wątki zostały przerwane, a inne nie
aléa = randint(0, 1)
if aléa == 1:
# log przed przerwą
logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
# przerwa
time.sleep(sleep_time)
# przekazujemy żądanie do głównego kontrolera
main_controller = config['controllers']["main-controller"]
résultat, status_code = main_controller.execute(request, session, config)
# rejestruje się wynik wysłany do klienta
log = f"[front_controller] {résultat}\n"
logger.write(log)
# czy wystąpił błąd krytyczny?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# wysyłamy wiadomość e-mail do administratora aplikacji
send_adminmail(config, log)
# określa się żądany typ odpowiedzi
if session.get('typeResponse') is None:
# typ sesji nie został jeszcze ustalony – będzie to jSON
type_response = 'json'
else:
type_response = session['typeResponse']
# tworzy się odpowiedź do wysłania
response_builder = config["responses"][type_response]
response, status_code = response_builder \
.build_http_response(request, session, config, status_code, résultat)
# wysyłamy odpowiedź
return response, status_code
except BaseException as erreur:
# wystąpił nieoczekiwany błąd – jeśli to możliwe, rejestruje się błąd
if logger:
logger.write(f"[ front_controller] {erreur}")
# przygotowuje się odpowiedź dla klienta
résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
# wysyłamy odpowiedź w formacie jSON
return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
finally:
# zamykamy plik dziennika, jeśli został otwarty
if logger:
logger.close()
- wiersze 1–57: znamy ten kod. Był to na przykład kod funkcji o nazwie [main] w skrypcie [main] z poprzedniej wersji. Należy zwrócić uwagę tylko na jedną rzecz, a mianowicie na kontroler użyty w wierszach 25–26:
- wiersz 25: z konfiguracji pobieramy instancję kontrolera powiązaną z nazwą [main-controller]. Chodzi o następujące wiersze:
# zależności serwera WWW
# kontrolery
…
from MainController import MainController
# dozwolone akcje i ich kontrolery
"controllers": {
…,
# kontroler główny
"main-controller": MainController()
},
- (ciąg dalszy)
- w wierszu 10 powyżej widać, że pobieramy instancję klasy;
- wiersz 26: wysyłamy żądanie do kontrolera [MainController] o przetworzenie żądania;
- wiersze 30–45: odpowiedź zwrócona przez kontroler [MainController] jest wysyłana do klienta. Do tych wierszy wrócimy nieco później;
Zadaniem funkcji [front_controller], a następnie klasy [MainController], jest wykonanie czynności wspólnych dla wszystkich żądań:
Na powyższym schemacie nadal znajdujemy się w fazie 1 przetwarzania żądania. Główny kontroler [MainController] będzie kontynuował etap 1.
30.6.2. Główny kontroler [MainController]
Główny kontroler [MainController] kontynuuje pracę rozpoczętą przez funkcję [front_controller]:
Wszystkie kontrolery implementują następujący interfejs [InterfaceController] [2]:

from abc import ABC, abstractmethod
from werkzeug.local import LocalProxy
class InterfaceController(ABC):
@abstractmethod
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
pass
- Interfejs [InterfaceController] definiuje jedynie jedną metodę [execute] w wierszu 8. Metoda ta przyjmuje trzy parametry:
- [request]: żądanie klienta;
- [session]: sesja klienta;
- [config]: konfiguracja aplikacji;
Metoda [execute] zwraca krotkę składającą się z dwóch elementów:
- pierwszy to słownik wyników w postaci {‘action’: action, ‘état’: état, ‘réponse’: wyniki};
- drugi to kod statusu HTTP, który ma zostać zwrócony klientowi;
Główny kontroler [MainController] [1] implementuje interfejs [InterfaceController] w następujący sposób:
# import zależności
from flask_api import status
from werkzeug.local import LocalProxy
# kontrolery aplikacji internetowej
from InterfaceController import InterfaceController
class MainController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# pobieranie elementów ścieżki
params = request.path.split('/')
action = params[1]
# błędy
erreur = False
# typ sesji musi być znany przed wykonaniem niektórych działań
type_response = session.get('typeResponse')
if type_response is None and action != "init-session":
# odnotowujemy błąd
résultat = {"action": action, "état": 101,
"réponse": ["pas de session en cours. Commencer par action [init-session]"]}
erreur = True
# w przypadku niektórych działań wymagane jest uwierzytelnienie
user = session.get('user')
if not erreur and user is None and action not in ["init-session", "authentifier-utilisateur"]:
# odnotowano błąd
résultat = {"action": action, "état": 101,
"réponse": [f"action [{action}] demandée par utilisateur non authentifié"]}
erreur = True
# czy występują błędy?
if erreur:
# zwracamy komunikat o błędzie
return résultat, status.HTTP_400_BAD_REQUEST
else:
# uruchamia się kontroler powiązany z akcją
controller = config["controllers"][action]
résultat, status_code = controller.execute(request, session, config)
return résultat, status_code
Kontroler [MainController] przeprowadza wstępne sprawdzenie poprawności żądania.
- wiersze 11–13: kontroler rozpoczyna od pobrania akcji żądanej przez klienta. Przypomnijmy, że akcje serwisowe typu URL mają postać [/action/param1/param2/…], a ta akcja URL znajduje się w [request.path];
- wiersze 17–23: akcja [init-session] służy do zainicjowania typu odpowiedzi (json, xml, html) żądanego przez klienta. Informacja ta jest umieszczana w sesji powiązanej z kluczem [typeRéponse]. Jeśli więc akcja nie jest [init-session], to sesja musi zawierać klucz [typeRéponse], w przeciwnym razie żądanie jest nieprawidłowe;
- wiersze 21–22: struktura wyniku zwracanego przez każdy kontroler, w tym przypadku wynik błędu:
- [action]: to nazwa bieżącej akcji. Pozwoli to na uzyskanie jej nazwy podczas rejestrowania wyniku żądania;
- [état]: to trzycyfrowy kod stanu:
- [x00] oznacza sukces;
- [x01] oznacza niepowodzenie;
- [réponse]: to odpowiedź na zapytanie. Jej charakter jest specyficzny dla każdego zapytania;
- wiersze 24–30: akcja [authentifier-utilisateur] służy do uwierzytelnienia użytkownika. Jeśli zakończy się powodzeniem, do sesji użytkownika dodawany jest klucz [user=True]. Niektóre operacje serwisowe o kluczu URL są dostępne wyłącznie dla uwierzytelnionego użytkownika. Właśnie to jest tutaj sprawdzane;
- wiersz 26: tylko akcje [init-session] i [authentifier-utilisateur] mogą być wykonywane przez użytkownika, który nie został jeszcze uwierzytelniony;
- wiersze 28–29: wynik, który należy wysłać w przypadku błędu;
- wiersze 32–34: jeśli wystąpił którykolwiek z dwóch poprzednich błędów, wysyłamy klientowi odpowiedź o błędzie ze statusem HTTP 400 BAD REQUEST;
- wiersze 35–39: jeśli nie wystąpił żaden błąd, to przekazujemy kontrolę do kontrolera odpowiedzialnego za przetworzenie bieżącej akcji. Jego instancja znajduje się w konfiguracji aplikacji;
Klasa [MainController] kontynuuje pracę funkcji [front_controller]: obie razem skupiają wszystko, co można wyodrębnić z przetwarzania żądań, czekając do ostatniej chwili z przekazaniem żądania do konkretnego kontrolera. Podział kodu między funkcją [front_controller] a klasą [MainController] jest całkowicie subiektywny. W tym przypadku chciałem zachować dorobek poprzedniej wersji: funkcja [front_controller] istniała już wcześniej pod nazwą [main]. W praktyce można by:
- umieścić wszystko w funkcji [front_controller] i usunąć klasę [MainController];
- przenieść wszystko do klasy [MainController] i usunąć funkcję [front_controller]. Wybrałbym raczej to rozwiązanie, ponieważ ma ono tę zaletę, że odciąża kod głównego skryptu [main];
30.7. Przetwarzanie specyficzne dla danej akcji
Wróćmy do architektury aplikacji MVC:

Wciąż znajdujemy się na etapie 1 opisanym powyżej. Jeśli nie wystąpił żaden błąd, rozpocznie się etap 2. Żądanie zostało przekazane do kontrolera specyficznego dla akcji, o którą prosi żądanie. Załóżmy, że tą akcją jest [/init-session] zdefiniowana przez trasę:
# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
# uruchamiany jest kontroler powiązany z akcją
return front_controller()
Ta akcja jest powiązana z kontrolerem w konfiguracji [config]:
# dozwolone akcje i ich kontrolery
"controllers": {
# inicjowanie sesji obliczeniowej
"init-session": InitSessionController(),
…
},
W związku z tym uruchamia się kontroler [InitSessionController] (wiersz 4). Jego kod wygląda następująco:
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class InitSessionController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# pobieranie elementów ścieżki
dummy, action, type_response = request.path.split('/')
# na początku brak błędów
erreur = False
# sprawdzanie typu odpowiedzi
if type_response not in config['responses'].keys():
erreur = True
résultat = {"action": action, "état": 701,
"réponse": [f"paramètre [type={type_response}] invalide"]}
# jeśli nie ma błędu
if not erreur:
# zapisujemy typ sesji w sesji Flaska
session['typeResponse'] = type_response
résultat = {"action": action, "état": 700,
"réponse": [f"session démarrée avec le type de réponse {type_response}"]}
return résultat, status.HTTP_200_OK
else:
return résultat, status.HTTP_400_BAD_REQUEST
- wiersz 6: podobnie jak inne kontrolery, kontroler [InitSessionController] implementuje interfejs [InterfaceController];
- wiersz 10: kontroler URL jest typu [/init-session/type_response]. Pobieramy akcję [init-session] oraz żądany typ odpowiedzi;
- wiersz 15: pożądany typ odpowiedzi może być wyłącznie jednym z tych, które występują w konfiguracji odpowiedzi:
# różne typy odpowiedzi (json, xml, html)
"responses": {
"json": JsonResponse(),
"html": HtmlResponse(),
"xml": XmlResponse()
},
- jeśli tak nie jest, przygotowujemy komunikat o błędzie 701 (wiersz 17);
- wiersze 20–25: przypadek, w którym żądany typ odpowiedzi jest prawidłowy;
- wiersz 22: żądany typ odpowiedzi jest zapisywany w sesji. Trzeba bowiem o nim pamiętać przy kolejnych żądaniach;
- wiersze 23–24: przygotowujemy odpowiedź o pomyślnym zakończeniu 700;
- wiersz 25: odpowiedź o pomyślnym zakończeniu jest zwracana do kodu wywołującego;
- wiersz 27: jeśli wystąpił błąd, kod wywołujący otrzymuje odpowiedź o błędzie;
30.8. Tworzenie odpowiedzi serwera HTTP
Wróćmy do architektury aplikacji MVC:

Omówiliśmy właśnie etapy 1 i 2. Napotkaliśmy trzy kody stanu:
- 700: /init-session zakończyło się powodzeniem;
- 701: operacja /init-session zakończyła się niepowodzeniem;
- 101: nieprawidłowe żądanie, ponieważ sesja nie została zainicjowana lub użytkownik nie jest uwierzytelniony;
Przyjrzyjmy się, w jaki sposób odpowiedź serwera zostanie wysłana do klienta w powyższym etapie 3. Odbywa się to w funkcji [front_controller] skryptu [main]:
# kontroler frontowy
def front_controller() -> tuple:
# przetwarzanie żądania
logger = None
try:
# logowanie
logger = Logger(config["logsFilename"])
# zapisywanie w konfiguracji powiązanej z wątkiem
thread_config = {"logger": logger}
thread_name = threading.current_thread().name
config[thread_name] = {"config": thread_config}
# rejestruje się żądanie
logger.write(f"[ front_controller] requête : {request}\n")
# przerywamy wątek, jeśli zostało to zlecone
sleep_time = config["sleep_time"]
if sleep_time != 0:
# przerwa jest losowa, aby niektóre wątki zostały przerwane, a inne nie
aléa = randint(0, 1)
if aléa == 1:
# logowanie przed przerwą
logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
# przerwa
time.sleep(sleep_time)
# przekazujemy żądanie do głównego kontrolera
main_controller = config['controllers']["main-controller"]
résultat, status_code = main_controller.execute(request, session, config)
# rejestruje się wynik wysłany do klienta
log = f"[front_controller] {résultat}\n"
logger.write(log)
# czy wystąpił błąd krytyczny?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# wysyłamy wiadomość e-mail do administratora aplikacji
send_adminmail(config, log)
# określany jest żądany typ odpowiedzi
if session.get('typeResponse') is None:
# typ sesji nie został jeszcze ustalony – będzie to jSON
type_response = 'json'
else:
type_response = session['typeResponse']
# tworzy się odpowiedź do wysłania
response_builder = config["responses"][type_response]
response, status_code = response_builder \
.build_http_response(request, session, config, status_code, résultat)
# wysyłamy odpowiedź
return response, status_code
except BaseException as erreur:
# wystąpił nieoczekiwany błąd – jeśli to możliwe, rejestruje się błąd
if logger:
logger.write(f"[ front_controller] {erreur}")
# przygotowuje się odpowiedź dla klienta
résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
# wysyłamy odpowiedź w formacie jSON
return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
finally:
# zamykamy plik dziennika, jeśli został otwarty
if logger:
logger.close()
- jesteśmy w linii 26: główny kontroler zwrócił swoją odpowiedź o błędzie;
- wiersze 27–29: niezależnie od odpowiedzi kontrolera głównego (powodzenie lub niepowodzenie) odpowiedź ta jest rejestrowana w pliku logów;
- wiersze 30–33: podobnie jak w poprzednich wersjach, jeśli status HTTP wynosi [500 INTERNAL SERVER ERROR], wysyłamy wiadomość e-mail do administratora aplikacji wraz z logiem błędu;
- wiersze 34–39: wyślemy odpowiedź o statusie HTTP, a wynik zwrócony przez kontroler zostanie umieszczony w treści tej odpowiedzi. Musimy wiedzieć, w jakim formacie (json, xml, html) klient oczekuje tej odpowiedzi. W sesji sprawdzamy, jaki typ odpowiedzi jest wymagany. Jeśli go tam nie ma, ustalamy ten typ arbitralnie na jSON;
- wiersze 40–43: tworzona jest odpowiedź HTTP;
W pliku konfiguracyjnym każdy typ odpowiedzi (json, xml, html) został powiązany z instancją klasy:
# różne typy odpowiedzi (json, xml, html)
"responses": {
"json": JsonResponse(),
"html": HtmlResponse(),
"xml": XmlResponse()
},
Klasy odpowiedzi znajdują się w folderze [responses] w drzewie katalogów serwera:

Każda klasa odpowiedzi implementuje następujący interfejs [InterfaceResponse]:
from abc import ABC, abstractmethod
from flask.wrappers import Response
from werkzeug.local import LocalProxy
class InterfaceResponse(ABC):
@abstractmethod
def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
résultat: dict) -> (Response, int):
pass
- wiersze 8–11: interfejs [InterfaceResponse] definiuje jedną metodę [build_http_response] o następujących parametrach:
- [request, session, config]: są to parametry odbierane przez kontroler akcji;
- [résultat, status_code]: są to wyniki generowane przez kontroler akcji;
Przedstawimy odpowiedź jSON. Jest ona generowana przez następującą klasę [JsonResponse]:
import json
from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy
from InterfaceResponse import InterfaceResponse
class JsonResponse(InterfaceResponse):
def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
résultat: dict) -> (Response, int):
# wyniki: słownik wyników
# status_code: kod statusu odpowiedzi HTTP
# zwracamy odpowiedź HTTP
response = make_response(json.dumps(résultat, ensure_ascii=False))
response.headers['Content-Type'] = 'application/json; charset=utf-8'
return response, status_code
Znamy ten kod, z którym spotykaliśmy się już wielokrotnie. Jest to kod funkcji [json_response] z modułu [myutils].
30.9. Pierwsze testy
W analizowanym kodzie natrafiliśmy na trzy kody stanu:
- 700: /init-session zakończyło się powodzeniem;
- 701: /init-session zakończyło się niepowodzeniem;
- 101: nieprawidłowe żądanie, ponieważ sesja nie została zainicjowana lub użytkownik nie jest uwierzytelniony;
Spróbujemy je uzyskać przy użyciu sesji o numerze jSON.
- uruchamiamy serwer WWW, serwer SGBD oraz serwer pocztowy;
- uruchamiamy klienta Postman;
Test 1
Najpierw przedstawiamy żądanie nieprawidłowe, ponieważ sesja nie została zainicjowana:

- [1-2]: żądanie [POST http://localhost:5000/authentifier-utilisateur] jest prawidłową trasą:
# uwierzytelnianie użytkownika
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
# uruchamia się kontroler powiązany z akcją
return front_controller()
ale jest ona akceptowana tylko wtedy, gdy sesja została wcześniej zainicjowana za pomocą akcji [/init-session].
Wykonajmy zapytanie i zobaczmy wynik wysłany przez serwer:

- [1-2]: otrzymaliśmy odpowiedź jSON. Gdy typ odpowiedzi nie został jeszcze określony przez klienta, serwer używa jSON do udzielenia odpowiedzi;
- [3-5]: słownik odpowiedzi jSON;
- [action]: wykonana akcja;
- [état]: kod stanu odpowiedzi. Kod [x01] oznacza błąd;
- [réponse]: jest dostosowany do każdej akcji. W tym przypadku zawiera komunikat o błędzie;
Teraz zainicjujmy sesję z nieprawidłowym typem odpowiedzi:

- [1-2] to poprawna trasa:
# inicjowanie sesji
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
# uruchamia się kontroler powiązany z akcją
return front_controller()
Zostanie ona zatem przekazana do tunelu przetwarzania żądań serwera MVC. Niemniej jednak powinna zostać odrzucona w trakcie tego przetwarzania, ponieważ żądany typ sesji jest nieprawidłowy.
Odpowiedź brzmi następująco:

- w [4], kod błędu [x01];
- w [5] – wyjaśnienie błędu;
Teraz zainicjujmy sesję jSON:

Odpowiedź jest następująca:

Teraz zainicjujmy sesję XML. Odpowiedź jSON zostanie zastąpiona odpowiedzią XML wygenerowaną przez następującą klasę [XmlResponse]:
import xmltodict
from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy
from InterfaceResponse import InterfaceResponse
from Logger import Logger
class XmlResponse(InterfaceResponse):
def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
résultat: dict) -> (Response, int):
# wyniki: słownik wyników
# status_code: kod statusu odpowiedzi HTTP
# wynik: słownik do przekształcenia w ciąg znaków XML
xml_string = xmltodict.unparse({"root": résultat})
# zwracamy odpowiedź HTTP
response = make_response(xml_string)
response.headers['Content-Type'] = 'application/xml; charset=utf-8'
return response, status_code
Jest to kod, który znamy – pochodzi on z funkcji [xml_response] modułu współdzielonego [myutils].
Inicjujemy sesję XML:

Wynik z serwera wygląda następująco:

Otrzymujemy tę samą odpowiedź co w przypadku jSON, ale tym razem odpowiedź jest sformułowana jako XML.
30.10. Akcja [authentifier-utilisateur]
Akcja [authentifier-utilisateur] umożliwia uwierzytelnienie użytkownika, który chce skorzystać z aplikacji do obliczania podatku. Jej ścieżka jest zdefiniowana w następujący sposób w skrypcie [main]:
# uwierzytelnianie użytkownika
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
# uruchamiamy kontroler powiązany z akcją
return front_controller()
Serwer oczekuje dwóch parametrów przesłanych metodą POST:
- [user]: identyfikator użytkownika;
- [password]: hasło użytkownika;
Lista uprawnionych użytkowników jest zdefiniowana w konfiguracji [config]:
# użytkownicy uprawnieni do korzystania z aplikacji
"users": [
{
"login": "admin",
"password": "admin"
}
],
W tym przypadku mamy listę zawierającą jeden element.
Akcja [authentifier-utilisateur] jest obsługiwana przez następujący kontroler [AuthentifierUtilisateurController]:
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
from Logger import Logger
class AuthentifierUtilisateurController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# pobierane są elementy ścieżki
dummy, action = request.path.split('/')
# parametry POST
post_params = request.form
# kod statusu odpowiedzi HTTP
status_code = None
# na początku brak błędów
erreur = False
erreurs = []
# potrzebny jest POST z dwoma parametrami
if len(post_params) != 2:
erreur = True
status_code = status.HTTP_400_BAD_REQUEST
erreurs.append("méthode POST requise, paramètre [action] dans l'URL, paramètres postés [user, password]")
if not erreur:
# pobieramy parametry z POST
# parametr [user]
user = post_params.get("user")
if user is None:
erreur = True
erreurs.append("paramètre [user] manquant")
# parametr [password]
password = post_params.get("password")
if password is None:
erreur = True
erreurs.append("paramètre [password] manquant")
# błąd?
if erreur:
status_code = status.HTTP_400_BAD_REQUEST
# błąd?
if not erreur:
# sprawdzana jest poprawność pary (nazwa użytkownika, hasło)
users = config['users']
i = 0
nbusers = len(users)
trouvé = False
while not trouvé and i < nbusers:
trouvé = user == users[i]["login"] and password == users[i]["password"]
i += 1
# znaleziono?
if not trouvé:
# odnotowuje się błąd
erreur = True
status_code = status.HTTP_401_UNAUTHORIZED
erreurs.append(f"Echec de l'authentification")
else:
# w sesji odnotowuje się, że znaleziono użytkownika
session["user"] = True
# zakończono
if not erreur:
# powrót bez błędu
résultat = {"action": action, "état": 200, "réponse": f"Authentification réussie"}
return résultat, status.HTTP_200_OK
else:
# powrót z błędem
return {"action": action, "état": 201, "réponse": erreurs}, status_code
- wiersz 14: pobierane są parametry akcji POST;
- wiersz 19: lista błędów wykrytych w żądaniu;
- wiersze 20–24: sprawdzane jest, czy rzeczywiście przesłano dwa parametry;
- wiersze 27–31: sprawdzamy obecność parametru [users];
- wiersze 32–36: sprawdzamy obecność parametru [password];
- wiersze 38–39: jeśli przesłane parametry są nieprawidłowe, przygotowuje się odpowiedź HTTP 400 BAD REQUEST;
- wiersze 40–58: sprawdzane jest, czy dane uwierzytelniające [user, password] należą do użytkownika uprawnionego do korzystania z aplikacji;
- wiersze 51–55: jeśli użytkownik (nazwa użytkownika, hasło) nie ma uprawnień do korzystania z aplikacji, przygotowuje się odpowiedź HTTP 401 UNAUTHORIZED;
- wiersze 56–58: jeśli jest uprawniony, w sesji odnotowuje się za pomocą klucza [user], że użytkownik się uwierzytelnił;
Należy zauważyć, że jeśli użytkownik został uwierzytelniony przy użyciu identyfikatorów [identifiants1] i nie uda mu się uwierzytelnić przy użyciu identyfikatorów [identifiants2], to nadal pozostaje uwierzytelniony przy użyciu identyfikatorów [identifiants1].
Przeprowadźmy testy w Postmanie:
- uruchamiamy serwer WWW, serwer SGBD oraz serwer pocztowy;
- za pomocą klienta Postman:
- rozpoczynamy sesję jSON;
- następnie uwierzytelniamy się;
Oto różne przypadki.
Przypadek 1: POST bez parametrów wysyłanych w żądaniu

- w [3-5], POST nie ma treści;
Wynik zapytania jest następujący:

- w przypadku [2] otrzymano odpowiedź HTTP 400 BAD REQUEST;
- po wprowadzeniu [5] otrzymano kod błędu [201];
Przypadek 2: POST z błędnymi danymi uwierzytelniającymi

- w [6] dane uwierzytelniające są nieprawidłowe;
Serwer wysyła następującą odpowiedź:

- w przypadku [2] – odpowiedź HTTP 401 UNAUTHORIZED;
- w przypadku [5] – odpowiedź z błędem;
Przypadek 2: POST z poprawnymi danymi uwierzytelniającymi

- w [6] identyfikatory są poprawne;
Odpowiedź serwera jest następująca:
- w przypadku [2] otrzymano odpowiedź HTTP 200 OK;
- w przypadku [5] – odpowiedź potwierdzająca powodzenie;
30.11. Akcja [calculer_impot]
Akcja [calculer_impot] umożliwia obliczenie podatku podatnika. Jej ścieżka jest zdefiniowana w następujący sposób w skrypcie [main]:
# oblicz-podatek
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
# uruchamiamy kontroler powiązany z akcją
return front_controller()
Serwer oczekuje trzech parametrów przesłanych metodą POST:
- [marié]: tak / nie;
- [enfants]: liczba dzieci podatnika;
- [salaire]: roczne wynagrodzenie podatnika;
Moduł kontrolny [CalculerImpotController] przetwarza akcję [calculer_impot]:
import re
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
from TaxPayer import TaxPayer
class CalculerImpotController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# pobieramy elementy ścieżki
dummy, action = request.path.split('/')
# na początku nie ma błędów
erreur = False
erreurs = []
# parametry POST
post_params = request.form
# potrzebny jest plik POST z trzema parametrami
if len(post_params) != 3:
erreur = True
erreurs.append(
"méthode POST requise avec les paramètres postés [marié, enfants, salaire]")
# analizujemy przesłane parametry
if not erreur:
# parametr jest powiązany
marié = post_params.get("marié")
if marié is None:
erreurs.append("paramètre [marié] manquant")
else:
# czy parametr jest prawidłowy?
marié = marié.lower()
if marié != "oui" and marié != "non":
erreur = True
erreurs.append(f"valeur [{marié}] invalide pour le paramètre [marié (oui/non)]")
# parametr [enfants]
enfants = post_params.get("enfants")
if enfants is None:
erreur = True
erreurs.append("paramètre [enfants] manquant")
else:
# czy parametr jest prawidłowy?
enfants = enfants.strip()
match = re.match(r"\d+", enfants)
if not match:
erreur = True
erreurs.append(f"valeur [{enfants}] invalide pour le paramètre [enfants (entier>=0)]")
# parametr wynagrodzenia
salaire = post_params.get("salaire")
if salaire is None:
erreur = True
erreurs.append("paramètre [salaire] manquant")
else:
# czy parametr jest prawidłowy?
salaire = salaire.strip()
match = re.match(r"\d+", salaire)
if not match:
erreur = True
erreurs.append(f"valeur [{salaire}] invalide pour le paramètre [salaire (entier>=0)]")
# błąd?
if erreur:
status_code = status.HTTP_400_BAD_REQUEST
résultat = {"action": action, "état": 301, "réponse": erreurs}
# zwracamy wynik
return résultat, status_code
# obliczenie podatku
# pobieramy warstwę [métier] i słownik [adminData]
métier = config["layers"]["métier"]
admin_data = config["admindata"]
# obliczenie podatku
taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
métier.calculate_tax(taxpayer, admin_data)
# numer symulacji
id_simulation = session.get('id_simulation', 0)
id_simulation += 1
session['id_simulation'] = id_simulation
# wynik umieszcza się w sesji w postaci słownika o nazwie TaxPayer
simulation = taxpayer.fromdict({'id': id_simulation}).asdict()
# wynik dodaje się do listy już wykonanych symulacji, a listę tę zapisuje się w sesji
simulations = session.get("simulations", [])
simulations.append(simulation)
session["simulations"] = simulations
# wynik
résultat = {"action": action, "état": 300, "réponse": simulation}
status_code = status.HTTP_200_OK
# wyświetla się wynik
return résultat, status_code
- wiersz 13: pobieramy nazwę bieżącej akcji;
- wiersz 17: błędy są gromadzone na liście;
- wiersz 19: pobierane są przesłane parametry. Są one przesyłane w postaci [x-www-form-urlencoded], dlatego pobieramy je w [request.form]. Gdyby zostały przesłane w formacie jSON, pobralibyśmy je jako [request.data];
- wiersze 21–24: sprawdzamy, czy rzeczywiście przesłano trzy parametry;
- wiersze 27–36: sprawdzamy obecność i poprawność przesłanego parametru [marié];
- wiersze 37–48: sprawdzamy obecność i poprawność przesłanego parametru [enfants];
- wiersze 49–60: sprawdzanie obecności i poprawności przesłanego parametru [salaire];
- wiersze 62–66: jeśli wystąpił błąd, wysyłana jest odpowiedź o błędzie 400 BAD REQUEST z kodem stanu [301];
- wiersze 69–71: jeśli nie wystąpił błąd, przygotowujemy się do obliczenia podatku. W tym celu
- wiersz 70: pobieramy odniesienie do warstwy [métier];
- wiersz 71: pobierane są dane z urzędu skarbowego z konfiguracji serwera;
- wiersze 72–74: obliczany jest podatek podatnika;
- wiersze 75–77: zliczamy liczbę obliczeń podatku wykonanych przez użytkownika;
- wiersz 76: pobieramy z sesji numer ostatniego wykonanego obliczenia. Wynik obliczenia nazywamy tutaj [simulation];
- wiersz 77: zwiększamy numer ostatniej symulacji;
- wiersz 78: zapisujemy ten numer w sesji;
- wiersze 79–84: aby śledzić obliczenia wykonane przez użytkownika, umieścimy w jego sesji listę przeprowadzonych przez niego symulacji;
- wiersz 80: symulacja będzie słownikiem obiektu TaxPayer, którego właściwość [id] będzie miała wartość numeru symulacji;
- wiersze 82–84: bieżąca symulacja zostaje dodana do listy symulacji obecnej w sesji;
- wiersze 86–87: przygotowuje się odpowiedź HTTP potwierdzającą powodzenie;
- wiersz 90: zwracany jest wynik;
Przeprowadźmy kilka testów: uruchomiono serwer WWW, serwer SGBD, serwer pocztowy oraz klienta Postman.
Przypadek 1: wykonanie obliczenia podatku, gdy sesja nie jest zainicjowana

Odpowiedź jest następująca:

Przypadek 2: obliczenie podatku bez uwierzytelnienia
Najpierw uruchamia się sesję jSON przy użyciu [/init-session/json]. Następnie wysyła się to samo zapytanie, co poprzednio. Odpowiedź brzmi wówczas następująco:

Przypadek 3: obliczenie podatku przy brakujących parametrach
Inicjujemy sesję jSON, uwierzytelniamy się, a następnie wysyłamy następujące zapytanie:

- w [5] brakuje parametru [marié];
Odpowiedź jest następująca:
Przypadek 4: obliczenie podatku przy użyciu błędnych parametrów


Odpowiedź serwera brzmi następująco:

Przypadek 4: obliczenie podatku przy użyciu prawidłowych parametrów

Odpowiedź serwera jest następująca:

30.12. Akcja [lister-simulations]
Akcja [lister-simulations] umożliwia użytkownikowi wyświetlenie listy symulacji, które przeprowadził od początku sesji. Jej ścieżka jest zdefiniowana w następujący sposób w skrypcie [main]:
# wyświetlenie listy symulacji
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
# uruchamiamy kontroler powiązany z akcją
return front_controller()
Serwer nie oczekuje żadnych parametrów. Akcja [lister-simulations] jest przetwarzana przez następujący kontroler [ListerSimulationsController]:
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class ListerSimulationsController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# pobieramy elementy ścieżki
dummy, action = request.path.split('/')
# pobieramy listę symulacji w sesji
simulations = session.get("simulations", [])
# zwracamy wynik
return {"action": action, "état": 500,
"réponse": simulations}, status.HTTP_200_OK
- wiersz 13: lista symulacji jest pobierana z sesji;
- wiersze 15–16: zwracana jest odpowiedź potwierdzająca powodzenie operacji;
Przeprowadźmy następujący test w Postmanie:
- uruchamiamy sesję jSON;
- autoryzujemy się;
- wykonujemy dwa obliczenia podatku;
- żądamy listy symulacji;
Żądanie wygląda następująco:
- w [3] nie ma żadnych parametrów;
Odpowiedź serwera jest następująca:

- w [4] znajduje się lista symulacji użytkownika;
30.13. Akcja [supprimer-simulation]
Akcja [supprimer-simulation] umożliwia użytkownikowi usunięcie jednej z symulacji z jego listy symulacji. Jej ścieżka jest zdefiniowana w następujący sposób w skrypcie [main]:
# usuń-symulację
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
# uruchamia się kontroler powiązany z akcją
return front_controller()
Serwer oczekuje jednego parametru – numeru symulacji, która ma zostać usunięta. Akcja [supprimer-simulation] jest przetwarzana przez następujący kontroler [SupprimerSimulationController]:
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class SupprimerSimulationController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# pobierane są elementy ścieżki
dummy, action, numéro = request.path.split('/')
# parametr [numéro] jest liczbą całkowitą dodatnią lub zerową, zgodnie z jego ścieżką
numéro = int(numéro)
# symulacja o identyfikatorze id=numer musi istnieć na liście symulacji
simulations = session.get("simulations", [])
liste_simulations = list(filter(lambda simulation: simulation['id'] == numéro, simulations))
if not liste_simulations:
msg_erreur = f"la simulation n° [{numéro}] n'existe pas"
# zwracany jest błąd
return {"action": action, "état": 601, "réponse": [msg_erreur]}, status.HTTP_400_BAD_REQUEST
# usunięcie symulacji o identyfikatorze „numer”
simulation = liste_simulations.pop(0)
simulations.remove(simulation)
# symulacje są ponownie dodawane do sesji
session["simulations"] = simulations
# zwracanie wyniku
return {"action": action, "état": 600, "réponse": simulations}, status.HTTP_200_OK
- wiersz 10: pobierane są dwa elementy ścieżki żądania. Pobierane są one jako ciągi znaków;
- wiersz 13: parametr [numéro] jest przekształcany na liczbę całkowitą. Wiemy, że jest to możliwe dzięki sygnaturze jego ścieżki,
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
Wiemy ponadto, że jest to liczba całkowita >=0. Nie można bowiem uzyskać wartości URL ani [/supprimer-simulation/-4]. Takie wartości są odrzucane przez serwer Flask;
- wiersz 15: pobieramy listę symulacji z sesji;
- wiersz 16: za pomocą funkcji [filter] wyszukujemy symulację o id==numer. Otrzymujemy obiekt [filter], który konwertujemy na typ [list];
- wiersze 17–20: jeśli filtr nie zwrócił żadnych wyników, oznacza to, że symulacja do usunięcia nie istnieje. Zwracamy komunikat o błędzie informujący o tym;
- wiersze 21–23: usuwamy symulację zwróconą przez filtr;
- wiersz 25: ponownie umieszczamy nową listę symulacji w sesji;
- wiersz 27: w odpowiedzi zwracamy nową listę symulacji;
Przeprowadzamy test powodzenia i test niepowodzenia. Wykonujemy symulacje, a następnie żądamy listy symulacji:

- symulacje mają tutaj numery 2 i 3;
Prosimy o usunięcie symulacji o numerze 3.

Odpowiedź brzmi następująco:
Teraz powtórzmy tę samą operację (usunięcie symulacji o identyfikatorze id=3). Odpowiedź brzmi wówczas następująco:


30.14. Akcja [fin-session]
Akcja [fin-session] umożliwia użytkownikowi zakończenie sesji symulacji. Jej ścieżka jest zdefiniowana w następujący sposób w skrypcie [main]:
# koniec sesji
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
# uruchamiamy kontroler powiązany z akcją
return front_controller()
Serwer nie oczekuje żadnych parametrów. Akcja jest przetwarzana przez następujący kontroler [FinSessionController]:
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class FinSessionController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# pobieramy elementy ścieżki
dummy, action = request.path.split('/')
# usuwamy wszystkie klucze z bieżącej sesji
session.clear()
# zwracany jest wynik
return {"action": action, "état": 400, "réponse": "session réinitialisée"}, status.HTTP_200_OK
- wiersz 13: usuwa się wszystkie klucze sesji. Powoduje to usunięcie:
- [typeResponse]: typ odpowiedzi HTTP (json, xml, html);
- [id_simulation]: numer ostatniej przeprowadzonej symulacji;
- [simulations]: lista symulacji użytkownika;
- [user]: wskaźnik potwierdzający uwierzytelnienie użytkownika;
- zwracana jest odpowiedź;
Można się zastanawiać, w jaki sposób zostanie zwrócona odpowiedź HTTP z linii 15, skoro typ odpowiedzi nie znajduje się już w sesji. Aby to sprawdzić, należy powrócić do funkcji |front_controller| w głównym skrypcie [main] i zmodyfikować ją w następujący sposób:
…
# on not# odnotowuje się żądany typ odpowiedzi, jeśli ta informacja znajduje się w sesji
type_response1 = session.get('typeResponse', None)
# przekazujemy żądanie do głównego kontrolera
main_controller = config['controllers']["main-controller"]
résultat, status_code = main_controller.execute(request, session, config)
# rejestruje się wynik wysłany do klienta
log = f"[front_controller] {résultat}\n"
logger.write(log)
# czy wystąpił błąd krytyczny?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# wysyła się wiadomość e-mail do administratora aplikacji
send_adminmail(config, log)
# określa się żądany typ odpowiedzi
type_response2=session.get('typeResponse')
if type_response2 is None and type_response1 is None:
# typ sesji nie został jeszcze ustalony – będzie to jSON
type_response = 'json'
elif type_response2 is not None:
# typ odpowiedzi jest znany i znajduje się w sesji
type_response = type_response2
else:
type_response=type_response1
# tworzona jest odpowiedź do wysłania
response_builder = config["responses"][type_response]
response, status_code = response_builder \
.build_http_response(request, session, config, status_code, résultat)
# wysyłamy odpowiedź
return response, status_code
- wiersz 3: zapisywany jest typ odpowiedzi aktualnie obecny w sesji;
- wiersz 6: wykonywana jest akcja. Jeśli jest to:
- [fin-session], klucz [typeResponse] nie znajduje się już w sesji;
- [init-session], klucz [typeResponse] sesji mógł zmienić swoją wartość;;
- wiersze 14–20: należy wysłać odpowiedź HTTP. Musimy wiedzieć, w jakiej postaci:
- wiersze 16–18: jeśli typ odpowiedzi nie jest zdefiniowany ani przez [type_response1] z wiersza 3, ani przez [type_response2] z wiersza 15, to typ odpowiedzi nie był zdefiniowany ani przed, ani po akcji. W takim przypadku stosuje się jSON (wiersz 18);
- wiersze 19–21: jeśli istnieje [type_response2], czyli typ w sesji po akcji, to należy użyć właśnie tego typu;
- wiersze 22–23: w przeciwnym razie należy użyć typu odpowiedzi sprzed akcji, czyli [type_response1] (ten typ to z konieczności [fin-session]);
30.15. Akcja [get-admindata]
Przechodzimy teraz do dwóch akcji URL zarezerwowanych dla usług jSON i XML:
Akcja | Rola | Kontekst wykonania |
/get-admindata | Zwraca dane podatkowe umożliwiające obliczenie podatku | Zapytanie GET. Używane tylko wtedy, gdy typ sesji to json lub xml. Użytkownik musi być uwierzytelniony |
/calculer-impots | Oblicza podatek dla listy podatników przesłanych w żądaniu jSON | Żądanie GET. Używane tylko wtedy, gdy typ sesji to json lub xml. Użytkownik musi być uwierzytelniony |
URL i [/get-admindata] są zdefiniowane w trasach głównego skryptu [main] w następujący sposób:
# get-admindata
@app.route('/get-admindata', methods=['GET'])
def get_admindata() -> tuple:
# uruchamia się kontroler powiązany z akcją
return front_controller()
Trasa [/get-admindata] jest obsługiwana przez następujący kontroler [GetAdminDataController]:
# import zależności
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
class GetAdminDataController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# pobieranie elementów ścieżki
dummy, action = request.path.split('/')
# akceptowane są wyłącznie sesje w formacie JSON i XML
type_response = session.get('typeResponse')
if type_response != 'json' and type_response != 'xml':
# zwracamy odpowiedź o błędzie
return {
"action": action,
"état": 1001,
"réponse": ["cette action n'est possible que pour les sessions json ou xml"]
}, status.HTTP_400_BAD_REQUEST
else:
# zwracana jest odpowiedź o pomyślnym zakończeniu
return {"action": action, "état": 1000, "réponse": config["adminData"].asdict()}, status.HTTP_200_OK
- wiersze 13–21: sprawdzane jest, czy mamy do czynienia z sesją JSON lub XML;
- wiersz 24: zwracany jest słownik danych administracji podatkowej, które zostały umieszczone w konfiguracji już przy uruchomieniu serwera:
# admindata będzie danymi o zasięgu aplikacji, dostępnymi tylko do odczytu
config["admindata"] = config["layers"]["dao"].get_admindata()
Użyjmy klienta Postman i wyślijmy żądanie dotyczące URL [/get-admindata] po uruchomieniu sesji jSON i uwierzytelnieniu się:

Odpowiedź serwera wygląda następująco:

30.16. Akcja [calculer-impots]
Akcja [calculer-impots] oblicza podatek dla listy podatników zawartej w treści żądania w postaci ciągu znaków jSON. Znamy już tę akcję: w poprzedniej wersji nosiła nazwę [calculate_tax_in_bulk_mode].
Jej ścieżka wygląda następująco:
# obliczanie podatku w trybie wsadowym
@app.route('/calculer-impots', methods=['POST'])
def calculer_impots():
# uruchamia się kontroler powiązany z akcją
return front_controller()
Ta akcja jest obsługiwana przez następujący kontroler [CalculerImpotsController]:
import json
from flask_api import status
from werkzeug.local import LocalProxy
from ImpôtsError import ImpôtsError
from InterfaceController import InterfaceController
from TaxPayer import TaxPayer
class CalculerImpotsController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# pobierane są elementy ścieżki
dummy, action = request.path.split('/')
# akceptowane są wyłącznie sesje w formacie JSON i XML
type_response = session.get('typeResponse')
if type_response != 'json' and type_response != 'xml':
# zwracana jest odpowiedź o błędzie
return {
"action": action,
"état": 1501,
"réponse": ["cette action n'est possible que pour les sessions json ou xml"]
}, status.HTTP_400_BAD_REQUEST
# pobieramy treść żądania POST – oczekujemy listy słowników
msg_erreur = None
list_dict_taxpayers = None
# treść jSON z POST
request_text = request.data
try:
#, którą przekształcamy w listę słowników
list_dict_taxpayers = json.loads(request_text)
except BaseException as erreur:
# odnotowujemy błąd
msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
# czy lista jest niepusta?
if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
# odnotowujemy błąd
msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
# czy mamy listę słowników?
if not msg_erreur:
erreur = False
i = 0
while not erreur and i < len(list_dict_taxpayers):
erreur = not isinstance(list_dict_taxpayers[i], dict)
i += 1
# błąd?
if erreur:
msg_erreur = "le corps du POST doit être une liste de dictionnaires"
# błąd?
if msg_erreur:
# wysyłamy klientowi komunikat o błędzie
résultats = {"action": action, "état": 1501, "réponse": [msg_erreur]}
return résultats, status.HTTP_400_BAD_REQUEST
# sprawdzamy po kolei TaxPayers
# na początku brak błędów
list_erreurs = []
for dict_taxpayer in list_dict_taxpayers:
# tworzy się plik TaxPayer na podstawie pliku dict_taxpayer
msg_erreur = None
try:
# kolejna operacja wyeliminuje przypadki, w których parametry nie są
# właściwości klasy TaxPayer, a także przypadki, w których ich wartości
# są nieprawidłowe
TaxPayer().fromdict(dict_taxpayer)
except BaseException as erreur:
msg_erreur = f"{erreur}"
# niektóre klucze muszą znajdować się w słowniku
if not msg_erreur:
# klucze [marié, enfants, salaire] muszą znajdować się w słowniku
keys = dict_taxpayer.keys()
if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
# czy są jakieś błędy?
if msg_erreur:
# błąd występuje w samym TaxPayer
dict_taxpayer['erreur'] = msg_erreur
# dodajemy TaxPayer do listy błędów
list_erreurs.append(dict_taxpayer)
# przetworzono wszystkich podatników – czy występują błędy?
if list_erreurs:
# wysyłamy klientowi komunikat o błędzie
résultats = {"action": action, "état": 1501, "réponse": list_erreurs}
return résultats, status.HTTP_400_BAD_REQUEST
# brak błędów, można kontynuować pracę
# pobieranie danych z urzędu skarbowego
admindata = config["admindata"]
métier = config["layers"]["métier"]
try:
# przetwarzamy pliki TaxPayer pojedynczo
list_taxpayers = []
for dict_taxpayer in list_dict_taxpayers:
# obliczanie podatku
taxpayer = TaxPayer().fromdict(
{'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
'wynagrodzenie': dict_taxpayer['salaire']})
métier.calculate_tax(taxpayer, admindata)
# zapisujemy wynik jako słownik
list_taxpayers.append(taxpayer.asdict())
# dodajemy list_taxpayers do bieżących symulacji, przypisując każdej symulacji numer
simulations = session.get("simulations", [])
id_simulation = session.get("id_simulation", 0)
for simulation in list_taxpayers:
# przypisuje się numer każdej symulacji
id_simulation += 1
simulation['id'] = id_simulation
# dodajemy ją do aktualnej listy symulacji
simulations.append(simulation)
# całość ponownie umieszczamy w sesji
session["simulations"] = simulations
session["id_simulation"] = id_simulation
# wysyła się odpowiedź do klienta
return {"action": action, "état": 1500, "réponse": list_taxpayers}, status.HTTP_200_OK
except ImpôtsError as erreur:
# wysyłamy komunikat o błędzie do klienta
return {"action": action, "état": 1501, "réponse": [f"{erreur}"]}, status.HTTP_500_INTERNAL_SERVER_ERROR
- wiersze 16–24: sprawdzane jest, czy rzeczywiście mamy do czynienia z sesją JSON lub XML
- wiersze 26–120: ten kod jest nam ogólnie znany. Pochodzi on z funkcji |index_controller| z wersji 10 aplikacji, która została dostosowana do specyfikacji zaimplementowanego interfejsu [InterfaceController];
- wiersze 104–115: kod dodany w celu uwzględnienia nowego środowiska tego kontrolera. Właśnie przeprowadziliśmy obliczenia podatkowe. Musimy zapisać wyniki na liście symulacji przechowywanych w sesji;
- wiersz 105: pobieramy listę symulacji w bieżącej sesji;
- wiersz 106: pobieramy numer ostatniej przeprowadzonej symulacji;
- wiersze 107–112: przeglądana jest lista plików wyników obliczeń podatku; każdemu z nich przypisywany jest numer symulacji [id], a każdy plik jest dodawany do listy symulacji;
- wiersze 113–115: nowa lista symulacji oraz numer ostatniej przeprowadzonej symulacji są zapisywane w sesji;
Po zainicjowaniu sesji jSON i uwierzytelnieniu się przeprowadzamy następujący test w Postmanie:


Odpowiedź serwera jest następująca:

Jeśli teraz zażądamy listy symulacji:
Można zauważyć, że na liście wyników [/calcul-impots] podatnicy nie posiadają atrybutu [id], podczas gdy na liście symulacji każda symulacja ma numer identyfikacyjny.




