28. Esercizio pratico: versione 10
28.1. Introduzione
Negli esempi di client per il 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 incapsuli i N contribuenti. Per ciascuno di essi, devono essere inviate le informazioni [coniugato, figli, stipendio]. Queste possono essere inviate come parametri:
- nell'URL. Ciò comporta un URL lungo e privo di significato;
- nel corpo 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]. Useremo una richiesta POST con i parametri incorporati nel corpo della richiesta HTTP.
L'architettura client/server non è cambiata:

28.2. Il server web

La cartella [http-servers/05] viene inizialmente creata copiando la cartella [http-servers/02]. Torniamo agli scambi JSON tra il client e il server. Abbiamo visto che passare da JSON a XML è molto semplice.
28.2.1. Configurazione
La configurazione [config, config_database, config_layers] rimane la stessa delle versioni precedenti. Non la ripasseremo di nuovo.
28.2.2. Lo script principale [main]
Lo script [main] è identico a quello nella cartella [http-servers/02] che abbiamo copiato. C'è solo una differenza:
- riga 2: l'URL / è ora accessibile tramite una richiesta POST;
28.2.3. Il [index_controller]
Il [index_controller] si evolve come segue:
- Riga 9: Il controller riceve:
- la richiesta del client;
- la configurazione del server [config];
- righe 14–18: Recuperiamo il corpo della richiesta POST. I parametri incapsulati nel corpo della richiesta HTTP possono essere codificati in vari modi. Ne abbiamo già incontrato uno: [x-www-form-urlencoded]. Qui useremo un'altra codifica: JSON;
- riga 18: [request.data] recupera il corpo della richiesta HTTP. Qui recuperiamo del testo e sappiamo che questo testo è JSON che rappresenta un elenco di dizionari [married, children, salary];
- righe 19–24: recuperiamo questo elenco di dizionari;
- righe 22–24: se il recupero del JSON non è andato a buon fine, registriamo l'errore;
- righe 26–28: se scopriamo che l'oggetto recuperato non è un elenco o è un elenco vuoto, registriamo l'errore;
- righe 29–38: se un elenco è stato recuperato con successo, verifichiamo che si tratti effettivamente di un elenco di dizionari;
- righe 40–43: se si è verificato un errore, ci fermiamo qui e inviamo una risposta di errore al client;
- righe 45–69: ora controlliamo ciascuno dei dizionari:
- devono contenere le chiavi [married, children, salary];
- devono consentirci di creare un oggetto [TaxPayer] valido;
- righe 65–69: se viene rilevato un errore in un dizionario, questo viene aggiunto allo stesso dizionario sotto la chiave ‘error’;
- righe 72–75: i dizionari contenenti errori sono stati raccolti nella lista [list_errors]. Se questa lista non è vuota, viene inviata in una risposta di errore al client;
- riga 77: a questo punto, sappiamo di poter creare un elenco di oggetti di tipo [TaxPayer] dal corpo della richiesta inviata dal cliente;
- righe 84–91: elaboriamo l'elenco dei dizionari ricevuti;
- riga 86: da un dizionario, creiamo un oggetto [TaxPayer];
- riga 89: calcoliamo l'imposta per questo [TaxPayer];
- riga 91: sappiamo che [taxpayer] è stato modificato dal calcolo dell'imposta. Lo convertiamo in un dizionario e lo aggiungiamo a un elenco di risultati;
- riga 93: questo elenco di risultati viene inviato al client;
28.2.4. Test del server
Testeremo il server utilizzando un client Postman:
- Avviamo il server web, il DBMS e il server di posta [hMailServer];
- Avviamo il client Postman e la sua console (Ctrl-Alt-C);

- in [1]: inviamo una richiesta [POST];
- in [2]: l'URL del server;
- in [3]: il corpo della richiesta HTTP;
- in [5]: specifichiamo che questo corpo deve essere inviato come stringa JSON;
- in [4]: passiamo alla modalità [raw] per poter copiare e incollare una stringa JSON;
- in [6]: incolliamo la stringa JSON presa da uno dei file [results.json] per le diverse versioni. Quindi, per ogni contribuente, conserviamo solo le proprietà [married, salary, children];

- in [7], esaminiamo le intestazioni HTTP che il client Postman invierà al server;
- in [8], vediamo che invierà un'intestazione [Content-Type] che indica che la richiesta contiene un corpo codificato in JSON. Ciò è dovuto alla scelta effettuata in precedenza in [5];

- in [9-12]: includiamo nella richiesta le credenziali richieste dal server;
Inviamo questa richiesta. La risposta del server è la seguente:

- in [3], abbiamo ricevuto JSON;
- in [4], l'imposta dei contribuenti;
Esaminiamo il dialogo client/server che ha avuto luogo nella console di Postman (Ctrl-Alt-C):
Il client Postman ha inviato il seguente testo:
- riga 1: la richiesta POST al server;
- riga 2: l'intestazione di autenticazione HTTP;
- riga 3: il client comunica al server che sta inviando una stringa JSON e che questa stringa è lunga 824 byte (riga 11);
- righe 13–69: il corpo JSON della richiesta;
Il server 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. È lunga 1461 byte (riga 3);
- riga 7: la risposta JSON del server;
Ora proviamo alcuni casi di errore.
Caso 1: inviamo qualsiasi cosa
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 JSON valida 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 è un elenco i cui elementi non sono 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 un elenco di dizionari con un dizionario che non ha 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 un elenco di dizionari con un dizionario contenente chiavi mancanti:
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 in cui uno contiene le chiavi corrette ma alcuni 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

Il file [http-clients/05] (versione 10) viene inizialmente ottenuto copiando il file [http-clients/02] (versione 7). Viene poi modificato.
28.3.1. Il livello [dao]
Il livello [dao] è implementato dalla seguente classe [ImpôtsDaoWithHttpClient]:
- righe 1–26: il codice rimane lo stesso della versione 7 e delle altre versioni;
- righe 27–70: viene introdotto un nuovo metodo [calculate_tax_in_bulk_mode], il cui scopo è calcolare l'imposta per un elenco di contribuenti;
- riga 28: [taxpayers] è questo elenco di contribuenti;
- righe 31–39: convertiamo un elenco di oggetti [TaxPayer] in un elenco di dizionari utilizzando la funzione [map];
- righe 34–38: la funzione lambda utilizzata trasforma un oggetto di tipo [TaxPayer] in un dizionario di tipo [dict] con solo le chiavi [married, children, salary]. Per farlo, utilizziamo il parametro denominato [included_keys] del metodo [BaseEntity.asdict]. Si noti che per determinare i nomi esatti delle proprietà da includere nei parametri [excluded_keys, included_keys], è necessario utilizzare il dizionario predefinito [taxpayer.__dict__];
- righe 41–48: connessione al server e recupero della sua risposta HTTP;
- righe 44, 48:
- utilizziamo il metodo statico [requests.post] per inviare una richiesta POST al server;
- Il parametro denominato [json] viene utilizzato per indicare che il corpo della richiesta POST è una stringa JSON. Ciò comporta due conseguenze:
- l'oggetto assegnato al parametro denominato [json], in questo caso un elenco di dizionari, verrà convertito in una stringa JSON;
- l'intestazione
sarà inclusa nelle intestazioni HTTP del POST;
- riga 59: la risposta JSON del server viene deserializzata nel dizionario [result];
- righe 61–63: viene gestito qualsiasi errore inviato dal server;
- riga 65: i risultati del calcolo delle imposte sono in un elenco di dizionari;
- righe 67–69: questi risultati vengono utilizzati per aggiornare l'elenco iniziale dei contribuenti [taxpayers] originariamente ricevuto dal metodo alla riga 28;
- riga 70: qui, l'elenco iniziale dei contribuenti è stato aggiornato con i risultati del calcolo delle imposte;
28.3.2. Lo script principale [main]
Lo script principale [main] si evolve come segue: viene modificata solo la funzione [thread_function] eseguita dai thread creati dal client. Il resto del codice rimane invariato.
- righe 9–10: mentre in precedenza avevamo un ciclo che passava ogni contribuente a turno al metodo [dao.calculate_tax], qui effettuiamo una singola chiamata al metodo [dao.calculate_tax_in_bulk_mode], passando ad esso tutti i contribuenti;
28.3.3. Esecuzione client
Confronteremo i tempi di esecuzione delle versioni:
- 7, in cui ogni contribuente è oggetto di una richiesta HTTP;
- 10 (questa), in cui i contribuenti sono raggruppati in un'unica richiesta HTTP;
In primo luogo, la versione 6. Per confrontare le due versioni, impostiamo la proprietà [sleep_time] del server a zero in modo che non vi sia alcuna attesa forzata per i 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 per 11 contribuenti è quindi [913065-811347= 101718], ovvero circa 102 millisecondi.
Facciamo lo stesso con la versione 10 (timeout del server impostato 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 per 11 contribuenti è quindi [935958-871428= 64530 ns] (riga 8 – riga 1), ovvero circa 65 millisecondi. Questa nuova versione 10 offre quindi un guadagno in termini di prestazioni di circa il 57% rispetto alla versione 7.
28.3.4. Test del livello [dao] del client

Il test [TestHttpClientDao] per il client nella versione 10 è molto simile a quello della versione 7:
- riga 14: invece di chiamare il metodo [dao.calculate_tax], chiamiamo il metodo [dao.calculate_tax_in_bulk_mode], passando un elenco (indicato dalle parentesi quadre) di contribuenti;
Tutti i test sono stati superati.