20. Esercizio pratico: versione 5

Svilupperemo tre applicazioni:
- L'applicazione 1 inizializzerà il database che sostituirà il file [admindata.json] della versione 4;
- L'applicazione 2 calcolerà le imposte in modalità batch;
- L'applicazione 3 calcolerà le imposte in modalità interattiva;
20.1. Applicazione 1: Inizializzazione del database
L'applicazione 1 avrà la seguente architettura:

Si tratta di un'evoluzione dell'architettura della versione 4 (vedere la sezione |Versione 4|): i dati fiscali saranno memorizzati in un database anziché in un file JSON. Il livello [DAO] sarà aggiornato per implementare questa modifica.
20.1.1. Il file [admindata.json]

Il file [admindata.json] è lo stesso della versione 4:
{
"limites": [9964, 27519, 73779, 156244, 0],
"coeffr": [0, 0.14, 0.3, 0.41, 0.45],
"coeffn": [0, 1394.96, 5798, 13913.69, 20163.45],
"plafond_qf_demi_part": 1551,
"plafond_revenus_celibataire_pour_reduction": 21037,
"plafond_revenus_couple_pour_reduction": 42074,
"valeur_reduc_demi_part": 3797,
"plafond_decote_celibataire": 1196,
"plafond_decote_couple": 1970,
"plafond_impot_couple_pour_decote": 2627,
"plafond_impot_celibataire_pour_decote": 1595,
"abattement_dixpourcent_max": 12502,
"abattement_dixpourcent_min": 437
}
Useremo le chiavi di questo dizionario come colonne nel database.
20.1.2. Creazione dei database
Come mostrato nella sezione |creazione di un database MySQL|, creiamo un database MySQL denominato [dbimpots-2019] di proprietà dell'utente [admimpots] con password [mdpimpots]. In [phpMyAdmin], appare come segue:

Allo stesso modo, come illustrato nella sezione |Creazione di un database PostgreSQL|, creiamo un database PostgreSQL denominato [dbimpots-2019] di proprietà dell'utente [admimpots] con la password [mdpimpots]. In [pgAdmin], la situazione si presenta come segue:

I database sono stati creati, ma per ora non contengono tabelle. Queste saranno generate dall'ORM [sqlalchemy].
20.1.3. Entità mappate da [sqlalchemy]
Creeremo due tabelle per incapsulare i dati da [admindata.json]:
Definita da [sqlalchemy], la tabella [tbtranches] raccoglierà i dati dagli array [limites, coeffr, coeffn] nel dizionario [admindata.json]:
# la table des tranches de l'impôt
tranches_table = Table("tbtranches", metadata,
Column('id', Integer, primary_key=True),
Column('limite', Float, nullable=False),
Column('coeffr', Float, nullable=False),
Column('coeffn', Float, nullable=False)
)
Definita da [sqlalchemy], la tabella [tbconstantes] conterrà le costanti del dizionario [admindata.json]:
# la table des constantes
constantes_table = Table("tbconstantes", metadata,
Column('id', Integer, primary_key=True),
Column('plafond_qf_demi_part', Float, nullable=False),
Column('plafond_revenus_celibataire_pour_reduction', Float, nullable=False),
Column('plafond_revenus_couple_pour_reduction', Float, nullable=False),
Column('valeur_reduc_demi_part', Float, nullable=False),
Column('plafond_decote_celibataire', Float, nullable=False),
Column('plafond_decote_couple', Float, nullable=False),
Column('plafond_impot_celibataire_pour_decote', Float, nullable=False),
Column('plafond_impot_couple_pour_decote', Float, nullable=False),
Column('abattement_dixpourcent_max', Float, nullable=False),
Column('abattement_dixpourcent_min', Float, nullable=False)
)
Le entità che verranno mappate su queste due tabelle sono le seguenti:

L'entità [Constants] incapsula le costanti del dizionario [admindata.json]:
- riga 5: la classe [Constants] estende la classe [BaseEntity];
- riga 7: tramite il mapping [sqlalchemy], la classe [Constante] riceverà la proprietà [_sa_instance_state]. La escludiamo dal dizionario [asdict] dell’entità;
- righe 11–23: le proprietà dell'entità. Abbiamo riutilizzato i nomi utilizzati nel dizionario [admindata.json] per facilitare la scrittura del codice;
L'entità [Tranche] incapsula una riga dei tre array [limites, coeffr, coeffn] presenti nel dizionario [admindata.json]:
- riga 5: la classe [Tranche] estende la classe [BaseEntity];
- riga 7: la proprietà [_sa_instance_state] aggiunta da [sqlalchemy] è esclusa dal dizionario [asdict] dell'entità;
- righe 10–12: le proprietà della classe;
La mappatura tra le entità [Constants, Slice] e le tabelle [constants, slices] sarà la seguente:

- Le mappature sono definite alle righe 24–29. Abbiamo omesso le mappature tra le proprietà delle entità mappate e le tabelle del database. Ciò è possibile quando i nomi delle colonne delle tabelle coincidono con quelli delle proprietà a cui devono essere associate. Per questo motivo, abbiamo incluso i nomi delle proprietà delle entità mappate nelle tabelle. Ciò rende il codice più facile da scrivere e da comprendere;
20.1.4. Il file di configurazione [sqlalchemy]

Abbiamo appena descritto in dettaglio una parte della configurazione [sqlalchemy]. Il file [config_database] completo è il seguente:
- riga 1: la funzione [configure] riceve come parametro un dizionario, la cui chiave [dbms] indica quale DBMS utilizzare: MySQL (mysql) o PostgreSQL (pgres);
- righe 6–12: viene selezionato il database specificato dalla configurazione;
- righe 14–44: mappature entità/tabella. Queste mappature sono semplici perché non c'è alcuna relazione tra le tabelle [tranches] e [constantes]. Sono indipendenti. Pertanto, non ci sono chiavi esterne tra loro da gestire;
- righe 46–51: creazione della sessione di lavoro dell'applicazione [session];
- righe 53–58: le informazioni rilevanti vengono inserite nel dizionario di configurazione, che viene poi restituito;
20.1.5. Il livello [dao]
Torniamo all'architettura dell'Applicazione 1 da realizzare:

Il livello [dao] [1] deve leggere il file [admindata.json] [2] e trasferirne il contenuto a uno dei database [3, 4];

Il livello [dao] fornisce l'interfaccia [1] ed è implementato dalla classe [2].
L'interfaccia [InterfaceDao4TransferAdminData2Database] è la seguente:
- righe 8–10: l'interfaccia definisce un solo metodo [transfer_admindata_in_database] senza parametri. Poiché questo metodo richiede dei parametri (quale file?, quale database?), ciò significa che tali parametri saranno passati al costruttore delle classi che implementano questa interfaccia;
La classe [DaoTransferAdminDataFromJsonFile2Database] implementa l'interfaccia [InterfaceDao4TransferAdminData2Database] come segue:
- riga 13: la classe [DaoTransferAdminDataFromJsonFile2Database] implementa l'interfaccia [InterfaceDao4TransferAdminData2Database];
- righe 15–17: il costruttore della classe accetta il dizionario di configurazione come parametro. Verranno utilizzate le seguenti chiavi:
- [admindataFilename] (riga 27): il nome del file JSON contenente i dati dell'amministrazione fiscale da trasferire al database;
- [database] riga 32: la configurazione [sqlalchemy] dell’applicazione;
- righe 34–37: eliminazione delle tabelle [constants] e [brackets] se presenti;
- righe 39–40: ricreazione delle due tabelle;
- riga 43: recupero della sessione [sqlalchemy] dalla configurazione;
- righe 45–51: gli array [limits, coeffr, coeffn] del dizionario [admindata] vengono aggiunti alla sessione. A tal fine, vengono aggiunte alla sessione delle istanze dell'entità [Tranche];
- righe 52–64: un'istanza dell'entità [Constantes] viene aggiunta alla sessione;
- righe 66–67: la sessione viene convalidata. Se i dati della sessione non erano ancora presenti nel database, vengono inseriti a questo punto;
- righe 68–70: gestione degli errori;
- righe 71–74: la sessione viene chiusa. Ciò è possibile perché il livello [dao] viene utilizzato una sola volta;
20.1.6. Configurazione dell'applicazione

L'applicazione è configurata da tre file [1]:
- [config] è il file di configurazione generale. Configura l'applicazione [main]. È supportato dagli altri due file:
- [config_database], che abbiamo già esaminato e che configura l'ORM [sqlalchemy];
- [config_layers], che configura i livelli dell'applicazione;
Il file [config] è il seguente:
- righe 8–36: Crea il Python Path dell'applicazione;
- righe 38–43: aggiungere il percorso del file [admindata.json] alla configurazione;
- righe 45–48: configurazione [SQLAlchemy];
- righe 50–53: istanziare i livelli dell'applicazione;
- riga 56: restituisce la configurazione generale;
Il file [config_layers] è il seguente:
- righe 3-4: istanziamento del livello [dao]. Abbiamo visto che il costruttore della classe [DaoTransferAdminDataFromJsonFile2Database] richiede come parametro il dizionario di configurazione generale dell'applicazione;
- riga 4: il riferimento al livello [dao] viene aggiunto alla configurazione;
- riga 7: restituisce la configurazione;
20.1.7. Lo script [main] dell'applicazione


Lo script principale [main] è il seguente:
- righe 1–10: attendiamo un parametro. Verifichiamo che sia presente e corretto;
- righe 12–14: configuriamo l'applicazione (general, SQLAlchemy, layers) passando il tipo di DBMS scelto come parametro;
- righe 19-20: avremo bisogno del livello [dao]. Lo recuperiamo;
- riga 25: eseguiamo il trasferimento al database. Tutte le informazioni richieste dal metodo [transfer_admindata_in_database] sono disponibili nelle proprietà del livello [dao] della riga 20. È da lì che le recupererà;
Dopo aver eseguito lo script con il database MySQL, esso contiene i seguenti elementi (phpMyAdmin):



La colonna [3] mostra i valori assegnati da MySQL alla chiave primaria [id]. La numerazione parte da 1. Lo screenshot sopra è stato acquisito dopo aver eseguito lo script diverse volte.


Con il database PostgreSQL, i risultati sono i seguenti:

- Fare clic con il tasto destro del mouse su [1], quindi su [2-3];
- In [4] vengono visualizzati chiaramente i dati relativi alle fasce di imposta;
Facciamo lo stesso per la tabella delle costanti [tbconstantes]:



20.2. Applicazione 2: Calcolo delle imposte in modalità batch

20.2.1. Architettura
L'applicazione per il calcolo delle imposte nella versione 4 utilizzava la seguente architettura:

Il livello [dao] implementa un'interfaccia [InterfaceImpôtsDao]. Abbiamo creato una classe che implementa questa interfaccia:
- [TaxDaoWithAdminDataInJsonFile], che recuperava i dati fiscali da un file JSON. Quella era la versione 3;
Implementeremo l'interfaccia [InterfaceImpôtsDao] utilizzando una nuova classe [ImpotsDaoWithTaxAdminDataInDatabase] che recupererà i dati dell'amministrazione fiscale da un database. Il livello [dao], come in precedenza, scriverà i risultati in un file JSON e recupererà i dati dei contribuenti da un file di testo. Sappiamo che se continuiamo ad aderire all'interfaccia [InterfaceImpôtsDao], il livello [business] non dovrà essere modificato.
La nuova architettura sarà la seguente:

20.2.2. Configurazione dell'applicazione

Il file di configurazione [config_database] rimane lo stesso dell'Applicazione 1. La configurazione [config] include nuovi elementi:
# étape 2 ------
# on complète la configuration de l'application
config.update({
# chemins absolus des fichiers de données
"admindataFilename": f"{script_dir}/../../data/input/admindata.json",
"taxpayersFilename": f"{script_dir}/../../data/input/taxpayersdata.txt",
"errorsFilename": f"{script_dir}/../../data/output/errors.txt",
"resultsFilename": f"{script_dir}/../../data/output/résultats.json"
})
- righe 6–8: i percorsi assoluti dei file di testo utilizzati dall'applicazione 2;
La configurazione dei livelli [config_layers] si evolve come segue:
- righe 3-4: il livello [dao] è ora implementato dalla classe [TaxDaoWithAdminDataInDatabase]. Questa classe è nuova ma implementa la stessa interfaccia [DaoInterface] della versione 4 dell'esercizio dell'applicazione;
- righe 7-8: il livello [business] è implementato dalla classe [ImpôtsMétier]. Questa è la classe utilizzata nella versione 4 dell'esercizio dell'applicazione;
20.2.3. Il livello [DAO]
La classe di implementazione [ImpotsDaoWithAdminDataInDatabase] per l'interfaccia [InterfaceImpôtsDao] sarà la seguente:
Note
- riga 11: la classe [ImpotsDaoWithAdminDataInDatabase] eredita dalla classe [AbstractImpôtsDao] presentata nella versione 4. Sappiamo che quest'ultima implementa l'interfaccia [InterfaceDao] presentata nella stessa versione. È proprio la conformità a questa interfaccia che ci permette di mantenere invariato il livello [business];
- riga 13: il costruttore della classe riceve come parametro il dizionario di configurazione dell'applicazione;
- riga 20: la classe padre [] viene inizializzata. Essa implementa parzialmente l'interfaccia [InterfaceDao]:
- [get_taxpayers_data] legge il file [taxpayersdata.txt] contenente i dati dei contribuenti;
- [write_taxpayers_results] scrive i risultati nel file JSON [results.json];
- [get_admindata] non è implementato;
- riga 22: la configurazione passata come parametri viene memorizzata;
- riga 27: implementazione del metodo [get_admindata] dell'interfaccia [InterfaceDao]:
- righe 28–30: il metodo [get_admindata] recupera i dati dall'amministrazione fiscale in un oggetto di tipo [AdminData] e memorizza questo oggetto in [self.__admindata]. Se il metodo [get_admindata] viene chiamato più volte, il database non viene interrogato più volte. Viene interrogato solo la prima volta. Nelle chiamate successive, viene restituito l'oggetto [self.__admindata];
- righe 36–37: recuperiamo la sessione [sqlalchemy] creata durante la configurazione dell'applicazione da [config_database];
- riga 40: recuperiamo le fasce di imposta in un elenco;
- riga 43: recuperiamo le costanti per il calcolo delle imposte;
- riga 46: creiamo un'istanza della classe [AdminData]. Ricordiamo che essa deriva da [BaseEntity];
- righe 48–54: inizializziamo gli array [limites, coeffr, coeffn] dell'istanza [AdminData];
- righe 55–56: inizializziamo le altre proprietà di [AdminData] con le costanti di calcolo delle imposte. Abbiamo fatto in modo di dare gli stessi nomi alle proprietà delle classi [AdminData] e [Constantes], il che semplifica il codice;
- righe 57–58: l'istanza [AdminData] viene memorizzata nel livello [dao] per essere restituita durante le successive chiamate al metodo [get_admindata];
- riga 60: viene restituito il valore richiesto dal codice chiamante;
- righe 61–63: gestione degli errori;
- righe 64–67: il database viene interrogato una sola volta. Possiamo quindi chiudere la sessione [sqlalchemy];
20.2.4. Test del livello [dao]
Nella versione 4 di questa applicazione, abbiamo creato una classe di test per il livello [business]. Più specificamente, essa testava sia il livello [business] che quello [DAO]. Stiamo riutilizzando questo test per verificare che il livello [DAO] funzioni come previsto. Il livello [business], tuttavia, rimane invariato.


Il test [TestDaoMétier] è il seguente:
import unittest
class TestDaoMétier(unittest.TestCase):
def test_1(self) -> None:
from TaxPayer import TaxPayer
# {'marié': 'oui', 'enfants': 2, 'salaire': 55555,
# 'impôt': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
métier.calculate_tax(taxpayer, admindata)
# vérification
self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
self.assertEqual(taxpayer.décôte, 0)
self.assertEqual(taxpayer.réduction, 0)
self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
self.assertEqual(taxpayer.surcôte, 0)
…
def test_11(self) -> None:
from TaxPayer import TaxPayer
# {'marié': 'oui', 'enfants': 3, 'salaire': 200000,
# 'impôt': 42842, 'surcôte': 17283, 'décôte': 0, 'réduction': 0, 'taux': 0.41}
taxpayer = TaxPayer().fromdict({'marié': 'oui', 'enfants': 3, 'salaire': 200000})
métier.calculate_tax(taxpayer, admindata)
# vérifications
self.assertAlmostEqual(taxpayer.impôt, 42842, 1)
self.assertEqual(taxpayer.décôte, 0)
self.assertEqual(taxpayer.réduction, 0)
self.assertAlmostEqual(taxpayer.taux, 0.41, delta=0.01)
self.assertAlmostEqual(taxpayer.surcôte, 17283, delta=1)
if __name__ == '__main__':
# on attend un paramètre mysql ou 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()
# on configure l'application
import config
config = config.configure({'sgbd': sgbd})
# couche métier
métier = config['métier']
try:
# admindata
admindata = config['dao'].get_admindata()
except BaseException as ex:
# affichage
print((f"L'erreur suivante s'est produite : {ex}"))
# fin
sys.exit()
# on enève le paramètre reçu par le script
sys.argv.pop()
# on exécute les méthodes de test
print("tests en cours...")
unittest.main()
- Non torneremo sui 11 test descritti nella sezione |[business] layer test versione 4|;
- righe 37–66: eseguiremo lo script di test come una normale applicazione piuttosto che come un UnitTest. La riga 66 è quella che attiverà il framework UnitTest. Nei test precedenti, abbiamo utilizzato il metodo [setUp] per configurare l'esecuzione di ciascun test. Ripetevamo la stessa configurazione 11 volte poiché la funzione [setUp] viene eseguita prima di ogni test. Qui, eseguiamo la configurazione una sola volta. Consiste nel definire le variabili globali [business] alla riga 53 e [admindata] alla riga 56, che saranno poi utilizzate dai metodi di [TestDaoBusiness], ad esempio alla riga 12;
- righe 39–47: lo script di test si aspetta un parametro [mysql / pgres] che indichi se si sta utilizzando un database MySQL o PostgreSQL;
- righe 50–51: il test viene configurato;
- riga 53: il livello [business] viene recuperato dalla configurazione;
- riga 56: facciamo lo stesso con il livello [dao]. Recuperiamo quindi l'istanza [admindata], che incapsula i dati necessari per calcolare l'imposta;
- I test hanno dimostrato che il metodo [unittest.main()] alla riga 66 non ignorava il parametro [mysql / pgres] passato allo script, ma gli attribuiva invece un significato diverso. La riga 63 garantisce che questo metodo non abbia più alcun parametro;
Creiamo due configurazioni di esecuzione:


Se eseguiamo una di queste due configurazioni, otteniamo i seguenti risultati:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/v05/tests/TestDaoMétier.py mysql
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 0.001s
OK
Process finished with exit code 0
- Righe 5 e 7: tutti gli 11 test superati;
Si noti che questi test verificano solo 11 casi di calcolo delle imposte. Il loro esito positivo può comunque essere sufficiente a darci fiducia nel livello [dao].
20.2.5. Lo script principale


Lo script principale [main] è lo stesso della versione 4:
Note
- righe 1-10: recuperiamo il parametro [mysql / pgres], che specifica il DBMS da utilizzare;
- righe 12–14: l'applicazione viene configurata;
- righe 16-17: viene importata la classe [ImpôtsError]. Ne avremo bisogno alla riga 38;
- righe 19-21: recuperiamo i riferimenti ai livelli dell'applicazione;
- riga 25: richiediamo i dati dell'amministrazione fiscale dal livello [dao]. Il livello [business] ne ha bisogno per calcolare l'imposta;
- riga 27: recuperiamo i dati dei contribuenti (id, stato civile, figli, stipendio) in un elenco;
- righe 29–30: se questo elenco è vuoto, viene generata un'eccezione;
- righe 32–35: calcoliamo l'imposta per gli elementi nell'elenco [contribuenti];
- riga 37: si scrivono i risultati nel file JSON [results.json];
- righe 38–40: gestiamo eventuali errori;
Per eseguire lo script, creiamo due |configurazioni di esecuzione|:

I risultati ottenuti nel file [results.json] sono quelli della versione 4.

20.3. Applicazione 3: Calcolo delle imposte in modalità interattiva
Presentiamo ora l'applicazione che consente il calcolo interattivo delle imposte. Si tratta di un porting dell'Applicazione 2 dalla Versione 4.


- Lo script [main] avvia il dialogo con l'utente utilizzando il metodo [ui.run] del livello [ui];
- Il livello [ui]:
- utilizza il livello [dao] per recuperare i dati necessari al calcolo dell'imposta;
- chiede all'utente le informazioni relative al contribuente per il quale deve essere calcolata l'imposta;
- utilizza il livello [business] per eseguire questo calcolo;
Il file [config_layers] istanzia un livello aggiuntivo:
La classe [ImpôtsConsole], righe 11–12, è la stessa della |versione 4|.
Lo script principale [main] è il seguente:
- righe 1-10: lo script si aspetta un parametro [mysql / pgres] che specifichi il DBMS da utilizzare;
- righe 12-14: l'applicazione viene configurata;
- righe 19-20: il livello [ui] viene recuperato dalla configurazione;
- riga 25: viene eseguito;
I risultati sono identici a quelli della |versione 4|. Non potrebbe essere altrimenti, dato che tutte le interfacce della versione 4 sono state mantenute nella versione 5.