28. Esercizio pratico: versione 10
28.1. Introduzione
Negli esempi relativi ai client del server di calcolo delle imposte, i thread inviavano N richieste in sequenza se dovevano elaborare N contribuenti. L’idea qui è quella di inviare un’unica richiesta che racchiuda i N contribuenti. Per ciascuno di essi, è necessario inviare le informazioni [marié, enfants, salaire]. È possibile inviarle come parametri:
- come parametri di URL. Si otterrà così un lungo URL poco significativo;
- nel corpo (body) della richiesta HTTP. Sappiamo che questo corpo è nascosto all’utente che utilizza un browser;
In entrambi i casi, è possibile utilizzare una richiesta [GET] o [POST]. Utilizzeremo una richiesta POST con i parametri incapsulati nel corpo della richiesta HTTP.
L’architettura client/server non è cambiata:

28.2. Il server web

La cartella [http-servers/05] viene inizialmente ottenuta tramite copia della cartella [http-servers/02]. Torniamo agli scambi jSON tra client e server. Abbiamo visto che passare da jSON a XML è molto semplice.
28.2.1. Configurazione
La configurazione di [config, config_database, config_layers] rimane analoga a quella delle versioni precedenti. Non ci soffermeremo nuovamente su questo aspetto.
28.2.2. Lo script principale [main]
Lo script [main] è identico a quello della cartella [http-servers/02] che abbiamo copiato. C'è solo una differenza:
# Home URL
@app.route('/', methods=['POST'])
@auth.login_required
def index():
…
- riga 2: ora l’URL si ottiene tramite un POST;
28.2.3. Il controller [index_controller]
Il controller [index_controller] si evolve come segue:
# importazione delle dipendenze
import json
from flask_api import status
from werkzeug.local import LocalProxy
def execute(request: LocalProxy, config: dict) -> tuple:
# dipendenze
from ImpôtsError import ImpôtsError
from TaxPayer import TaxPayer
# si recupera il corpo del post - si attende un elenco di dizionari
msg_erreur = None
list_dict_taxpayers = None
# il corpo jSON del POST
request_text = request.data
try:
# che viene trasformato in un elenco di dizionari
list_dict_taxpayers = json.loads(request_text)
except BaseException as erreur:
# si rileva l'errore
msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
# si ha un elenco non vuoto?
if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
# si rileva l'errore
msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
# si ha una lista di dizionari?
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
# errore?
if erreur:
msg_erreur = "le corps du POST doit être une liste de dictionnaires"
# errore?
if msg_erreur:
# si invia una risposta di errore al client
résultats = {"réponse": {"erreurs": [msg_erreur]}}
return résultats, status.HTTP_400_BAD_REQUEST
# si controllano i TaxPayers uno per uno
# all'inizio nessun errore
list_erreurs = []
for dict_taxpayer in list_dict_taxpayers:
# si crea un TaxPayer a partire da dict_taxpayer
msg_erreur = None
try:
# l'operazione successiva eliminerà i casi in cui i parametri non sono
# delle proprietà della classe TaxPayer, nonché i casi in cui i loro valori
# non sono corretti
TaxPayer().fromdict(dict_taxpayer)
except BaseException as erreur:
msg_erreur = f"{erreur}"
# alcune chiavi devono essere presenti nel dizionario
if not msg_erreur:
# le chiavi [marié, enfants, salaire] devono essere presenti nel dizionario
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]"
# errori?
if msg_erreur:
# si rileva l'errore proprio nel TaxPayer
dict_taxpayer['erreur'] = msg_erreur
# si aggiunge TaxPayer all’elenco degli errori
list_erreurs.append(dict_taxpayer)
# Sono stati elaborati tutti i contribuenti - ci sono errori?
if list_erreurs:
# si invia una risposta di errore al cliente
résultats = {"réponse": {"erreurs": list_erreurs}}
return résultats, status.HTTP_400_BAD_REQUEST
# nessun errore, si può procedere
# recupero dei dati dall'amministrazione fiscale
admindata = config["admindata"]
métier = config["layers"]["métier"]
try:
# si elaborano i TaxPayer uno per uno
list_taxpayers = []
for dict_taxpayer in list_dict_taxpayers:
# calcolo dell'imposta
taxpayer = TaxPayer().fromdict(
{'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
'stipendio': dict_taxpayer['salaire']})
métier.calculate_tax(taxpayer, admindata)
# si memorizza il risultato come dizionario
list_taxpayers.append(taxpayer.asdict())
# si invia la risposta al cliente
return {"réponse": {"results": list_taxpayers}}, status.HTTP_200_OK
except ImpôtsError as erreur:
# si invia una risposta di errore al cliente
return {"réponse": {"erreurs": f"[{erreur}]"}}, status.HTTP_500_INTERNAL_SERVER_ERROR
- riga 9: il controller riceve:
- la richiesta [request] dal client;
- la configurazione [config] dal server;
- righe 14-18: si recupera il corpo di POST. I parametri incapsulati nel corpo della richiesta HTTP possono essere codificati in diversi modi. Ne abbiamo già incontrato uno: [x-www-form-urlencoded]. Qui useremo un'altra codifica: jSON;
- riga 18: [request.data] consente di recuperare il corpo (body) della richiesta HTTP. Qui si recupera del testo e sappiamo che tale testo proviene da jSON, che rappresenta un elenco di dizionari [marié, enfants, salaire];
- righe 19-24: si recupera questo elenco di dizionari;
- righe 22-24: se il recupero di jSON non è andato a buon fine, si registra l’errore;
- righe 26-28: se si rileva che l'oggetto recuperato non è un elenco o che si tratta di un elenco vuoto, si registra l'errore;
- righe 29-38: se è stato recuperato correttamente un elenco, si verifica che si tratti effettivamente di un elenco di dizionari;
- righe 40-43: se si è verificato un errore, ci si ferma qui e si invia una risposta di errore al cliente;
- righe 45-69: ora si verifica ciascuno dei dizionari:
- devono contenere le chiavi [marié, enfants, salaire];
- devono consentire di costruire un oggetto [TaxPayer] valido;
- righe 65-69: se è stato rilevato un errore in un dizionario, questo viene inserito nello stesso dizionario associato alla chiave ‘errore’;
- righe 72-75: i dizionari errati sono stati raccolti nell’elenco [list_erreurs]. Se tale elenco non è vuoto, viene inviato in una risposta di errore al cliente;
- riga 77: a questo punto, si sa che è possibile creare un elenco di oggetti di tipo [TaxPayer] a partire dal corpo della richiesta inviata dal cliente;
- righe 84-91: si elabora l’elenco dei dizionari ricevuti;
- riga 86: a partire da un dizionario, si crea un oggetto [TaxPayer];
- riga 89: si calcola l’imposta relativa a questo [TaxPayer];
- riga 91: si sa che [taxpayer] è stato modificato dal calcolo dell’imposta. Lo si trasforma in un dizionario e lo si aggiunge a un elenco di risultati;
- riga 93: si invia questo elenco di risultati al cliente;
28.2.4. Test del server
Testeremo il server con un client Postman:
- avviamo il server web, il SGBD, e il server di posta [hMailServer];
- avviamo il client Postman e la sua console (Ctrl-Alt-C);

- su [1]: inviamo una richiesta a [POST];
- in [2]: l’URL del server;
- in [3]: il corpo della richiesta HTTP;
- in [5]: si specifica che questo corpo dovrà essere inviato sotto forma di stringa jSON;
- in [4]: si passa alla modalità [raw] per poter copiare/incollare una stringa jSON;
- in [6]: si incolla la stringa jSON prelevata da uno dei file [résultats.json] delle diverse versioni. Quindi si mantengono per ciascun contribuente solo le proprietà [marié, salaire, enfants];

- in [7], si esaminano le intestazioni HTTP che il client Postman invierà al server;
- in [8], si vede che gli invierà un'intestazione [Content-Type] che indica che la richiesta contiene un corpo codificato in jSON. Ciò deriva dalla scelta [5] effettuata in precedenza;

- in [9-12]: inseriamo nella richiesta gli identificativi richiesti dal server;
Si invia questa richiesta. La risposta del server è la seguente:

- da [3], abbiamo ricevuto da jSON;
- in [4], l’imposta dei contribuenti;
Esaminiamo nella console di Postman (Ctrl-Alt-C) il dialogo client/server che ha avuto luogo:
Il client Postman ha inviato il seguente testo:
- riga 1: il codice POST al server;
- riga 2: l’intestazione di autenticazione HTTP;
- riga 3: il client comunica al server che gli sta inviando una stringa jSON e che tale stringa è lunga 824 byte (riga 11);
- righe 13-69: il corpo jSON della richiesta;
Il server gli ha risposto con il seguente testo:
HTTP/1.0 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 1461
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:16:34 GMT
{"réponse": {"results": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}]}}
- riga 1: la richiesta è andata a buon fine;
- riga 2: il corpo della risposta del server è una stringa jSON. Questa ha una lunghezza di 1461 byte (riga 3);
- riga 7: la risposta jSON del server;
Proviamo ora alcuni casi di errore.
Caso 1: inviamo un dato qualsiasi
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 47652706-9744-46a0-a682-de010e5406c0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 3
abc
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 125
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:43:27 GMT
{"réponse": {"erreurs": ["le corps du POST n'est pas une chaîne jSON valide : Expecting value: line 1 column 1 (char 0)"]}}
- riga 13: è stata inviata la stringa [abc], che non è una stringa jSON valida (riga 3);
- riga 15: il server risponde con un codice di errore 400;
- riga 21: la risposta jSON del server;
Caso 2: inviamo una stringa valida jSON che non sia un elenco
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 03b64735-9239-47b3-b92d-be7c9ebc7559
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 17
{"att1":"value1"}
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 97
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:50:11 GMT
{"réponse": {"erreurs": ["le corps du POST n'est pas une liste ou alors cette liste est vide"]}}
Caso 3: inviamo una stringa jSON che sia un elenco i cui elementi non siano tutti dizionari
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: a1528a5f-777c-413f-b3be-7d4e9955b12a
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 7
[0,1,2]
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 85
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:52:10 GMT
{"réponse": {"erreurs": ["le corps du POST doit être une liste de dictionnaires"]}}
Caso 4: inviamo una lista di dizionari con un dizionario che non contiene le chiavi corrette
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: ba964d81-c9d9-46ff-a521-b4c4e5639484
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 19
[{"att1":"value1"}]
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 112
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:54:33 GMT
{"réponse": {"erreurs": [{"att1": "value1", "erreur": "MyException[2, la clé [att1] n'est pas autorisée]"}]}}
Caso 5: inviamo una lista di dizionari con un dizionario in cui mancano alcune chiavi:
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 98aec51d-f37d-4c14-81cd-c7ffcbbcdc65
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 18
[{"marié":"oui"}]
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 125
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:56:40 GMT
{"réponse": {"erreurs": [{"marié": "oui", "erreur": "le dictionnaire doit inclure les clés [marié, enfants, salaire]"}]}}
Caso 6: inviamo un elenco di dizionari con un dizionario che contiene le chiavi corrette ma in cui alcune hanno valori errati:
POST / HTTP/1.1
Authorization: Basic YWRtaW46YWRtaW4=
Content-Type: application/json
User-Agent: PostmanRuntime/7.26.2
Accept: */*
Cache-Control: no-cache
Postman-Token: 3083e601-dee4-4e15-9ea4-fc0328d0fcf0
Host: localhost:5000
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 46
[{"marié":"x", "enfants":"x", "salaire":"x"}]
HTTP/1.0 400 BAD REQUEST
Content-Type: application/json; charset=utf-8
Content-Length: 167
Server: Werkzeug/1.0.1 Python/3.8.1
Date: Tue, 28 Jul 2020 07:59:32 GMT
{"réponse": {"erreurs": [{"marié": "x", "enfants": "x", "salaire": "x", "erreur": "MyException[31, l'attribut marié [x] doit avoir l'une des valeurs oui / non]"}]}}
28.3. Il client web

La cartella [http-clients/05] (versione 10) viene inizialmente ottenuta copiando la cartella [http-clients/02] (versione 7). Successivamente viene modificata.
28.3.1. Il livello [dao]
Il livello [dao] è implementato dalla seguente classe [ImpôtsDaoWithHttpClient]:
# importazioni
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):
# costruttore
def __init__(self, config: dict):
…
# metodo non utilizzato
def get_admindata(self) -> AdminData:
pass
# calcolo dell'imposta
def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
…
# calcolo dell'imposta in modalità batch
def calculate_tax_in_bulk_mode(self, taxpayers: list) -> list:
# si consentono le eccezioni
# converte i contribuenti in un elenco di dizionari
# si mantengono solo le proprietà [marié, enfants, salaire]
list_dict_taxpayers = list(
map(lambda taxpayer:
taxpayer.asdict(included_keys=[
'_TaxPayer__marié',
'_TaxPayer__enfants',
'_TaxPayer__salaire']),
taxpayers))
# connessione al server
config_server = self.__config_server
if config_server['authBasic']:
response = requests.post(config_server['urlServer'], json=list_dict_taxpayers,
auth=(config_server["user"]["login"],
config_server["user"]["password"]))
else:
response = requests.post(config_server['urlServer'], json=list_dict_taxpayers)
# modalità debug?
if self.__debug:
# logger
if not self.__logger:
self.__logger = self.__config['logger']
# si sta effettuando l'accesso
self.__logger.write(f"{response.text}\n")
# codice di stato della risposta HTTP
status_code = response.status_code
# si inserisce la risposta jSON in un dizionario
résultat = response.json()
# errore se il codice di stato è diverso da 200 OK
if status_code != status.HTTP_200_OK:
# si sa che gli errori sono stati associati alla chiave [erreurs] della risposta
raise ImpôtsError(93, résultat['réponse']['erreurs'])
# si sa che il risultato è stato associato alla chiave [results] della risposta
list_dict_taxpayers2 = résultat['réponse']['results']
# si aggiorna l'elenco iniziale dei contribuenti con i risultati ricevuti
for i in range(len(taxpayers)):
# aggiornamento dei contribuenti [i]
taxpayers[i].fromdict(list_dict_taxpayers2[i])
# qui il parametro [taxpayers] è stato aggiornato con i risultati del server
- righe 1-26: il codice rimane invariato rispetto alla versione 7 e alle altre versioni;
- righe 27-70: viene introdotto un nuovo metodo [calculate_tax_in_bulk_mode] il cui ruolo è quello di calcolare l’imposta di un elenco di contribuenti;
- riga 28: [taxpayers] è questo elenco di contribuenti;
- righe 31-39: si passa da un elenco di oggetti di tipo [TaxPayer] a un elenco di dizionari grazie alla funzione [map];
- righe 34-38: la funzione lambda utilizzata trasforma un oggetto di tipo [TaxPayer] in un dizionario di tipo [dict] avente come uniche chiavi [marié, enfants, salaire]. A tal fine si utilizza il parametro denominato [included_keys] del metodo [BaseEntity.asdict]. Si ricorda che per conoscere i nomi esatti delle proprietà da inserire nei parametri [excluded_keys, included_keys], è necessario utilizzare il dizionario predefinito [taxpayer.__dict__];
- righe 41-48: connessione al server e successivo recupero della sua risposta HTTP;
- righe 44, 48:
- si utilizza il metodo statico [requests.post] per inviare un POST al server;
- si utilizza il parametro denominato [json] per indicare che il corpo del POST è una stringa jSON. Ciò avrà due conseguenze:
- l’oggetto associato al parametro denominato [json], in questo caso un elenco di dizionari, verrà trasformato nella stringa jSON;
- l’intestazione
sarà inclusa nelle intestazioni HTTP di POST;
- riga 59: la risposta jSON del server viene deserializzata nel dizionario [résultat];
- righe 61-63: viene gestito l'eventuale errore inviato dal server;
- riga 65: i risultati del calcolo dell’imposta sono contenuti in un elenco di dizionari;
- righe 67-69: questi risultati vengono utilizzati per aggiornare l'elenco iniziale dei contribuenti [taxpayers] inizialmente ricevuto dal metodo, riga 28;
- riga 70: qui l'elenco iniziale dei contribuenti è stato aggiornato con i risultati del calcolo dell'imposta;
28.3.2. Lo script principale [main]
Lo script principale [main] viene modificato come segue: viene modificata solo la funzione [thread_function] eseguita dai thread creati dal client. Il resto del codice rimane invariato.
# esecuzione del livello [dao] in un thread
# «taxpayers» è un elenco di contribuenti
def thread_function(dao: ImpôtsDaoWithHttpClient, logger: Logger, taxpayers: list):
# log di inizio del thread
thread_name = threading.current_thread().name
nb_taxpayers = len(taxpayers)
# log
logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
# si calcola l'imposta dei contribuenti
dao.calculate_tax_in_bulk_mode(taxpayers)
# log
logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")
- righe 9-10: mentre in precedenza era presente un ciclo che passava in successione ciascuno dei contribuenti al metodo [dao.calculate_tax], qui viene effettuata una sola chiamata al metodo [dao.calculate_tax_in_bulk_mode], al quale vengono passati tutti i contribuenti;
28.3.3. Esecuzione del client
Confronteremo i tempi di esecuzione delle versioni:
- 7, in cui ogni contribuente è oggetto di una richiesta HTTP;
- 10 (questa), in cui i contribuenti vengono raggruppati in un’unica richiesta HTTP;
Innanzitutto la versione 6. Per confrontare le due versioni, impostiamo a zero la proprietà [sleep_time] del server in modo che non vi sia alcuna attesa forzata dei thread. I log del client sono i seguenti:
2020-07-28 14:20:45.811347, Thread-1 : début du thread [Thread-1] avec 4 contribuable(s)
2020-07-28 14:20:45.811347, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
…
2020-07-28 14:20:45.913065, Thread-3 : fin du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-28 14:20:45.913065, Thread-3 : fin du thread [Thread-3]
Il tempo di esecuzione del client per calcolare l’imposta di 11 contribuenti è quindi [913065-811347= 101718], ovvero circa 102 millisecondi.
Facciamo la stessa cosa con la versione 10 (sleep_time del server a zero). I log del client sono quindi i seguenti:
2020-07-28 14:25:31.871428, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-07-28 14:25:31.873594, Thread-2 : début du calcul de l'impôt des 3 contribuables
2020-07-28 14:25:31.877429, Thread-3 : début du calcul de l'impôt des 3 contribuables
2020-07-28 14:25:31.882855, Thread-4 : début du calcul de l'impôt des 1 contribuables
2020-07-28 14:25:31.930723, Thread-2 : {"réponse": {"results": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}]}}
….
2020-07-28 14:25:31.935958, Thread-4 : fin du calcul de l'impôt des 1 contribuables
2020-07-28 14:25:31.935958, Thread-1 : fin du calcul de l'impôt des 4 contribuables
Il tempo di esecuzione del client per calcolare l’imposta di 11 contribuenti è quindi [935958-871428= 64530 ns] (riga 8 – riga 1), ovvero circa 65 millisecondi. Questa nuova versione 10 comporta quindi un miglioramento di circa il 57% rispetto alla versione 7.
28.3.4. Test del livello [dao] del client

Il test [TestHttpClientDao] del client della versione 10 è molto simile a quello della versione 7:
import unittest
from Logger import Logger
class TestHttpClientDao(unittest.TestCase):
def test_1(self) -> None:
from TaxPayer import TaxPayer
# {'coniugato': 'sì', 'figli': 2, 'stipendio': 55555,
# 'imposta': 2814, 'maggiorazione': 0, 'riduzione': 0, 'sconto': 0, 'aliquota': 0,14}
taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
dao.calculate_tax_in_bulk_mode([taxpayer])
# verifica
self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
self.assertEqual(taxpayer.décôte, 0)
self.assertEqual(taxpayer.réduction, 0)
self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
self.assertEqual(taxpayer.surcôte, 0)
…
if __name__ == '__main__':
# si configura l'applicazione
import config
config = config.configure({})
# registrazione
logger = Logger(config["logsFilename"])
# si salva nella configurazione
config["logger"] = logger
# si recupera il livello [dao]
dao = config["layers"]["dao"]
# si eseguono i metodi di test
print("tests en cours...")
unittest.main()
- riga 14: invece di chiamare il metodo [dao.calculate_tax], si chiama il metodo [dao.calculate_tax_in_bulk_mode] a cui viene passato un elenco (presenza delle parentesi quadre) di un contribuente;
Tutti i test vengono superati.