Skip to content

31. Client web per i servizi JSON e XML della versione 12

Scriveremo tre applicazioni client da console per i servizi JSON e XML del server web che abbiamo appena scritto. Riutilizzeremo l'architettura client/server della versione 11:

Image

Scriveremo tre script da console:

  • gli script [main] e [main3] useranno il livello [business] del server;
  • lo script [main2] utilizzerà il livello [business] del client;

31.1. La struttura delle directory degli script client

La cartella [http-clients/07] viene inizialmente creata copiando la cartella [http-clients/06]. Viene poi modificata.

Image

  • in [1]: dati utilizzati o creati dal client;
  • in [2], gli script di configurazione e della console del client;
  • in [3], il livello [dao] del client;
  • in [4], la cartella di test per il livello [dao] del client;

31.2. Il livello [dao] dei client

Image

Image

31.2.1. Interfaccia

Il livello [dao] implementerà la seguente interfaccia [InterfaceImpôtsDaoWithHttpSession]:

from abc import abstractmethod

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from TaxPayer import TaxPayer

class InterfaceImpôtsDaoWithHttpSession(AbstractImpôtsDao):

    #  tax calculation per unit
    @abstractmethod
    def calculate_tax(self, taxpayer: TaxPayer):
        pass

    #  batch tax calculation
    @abstractmethod
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        pass

    #  session initialization
    @abstractmethod
    def init_session(self, type_session: str):
        pass

    #  end of session
    @abstractmethod
    def end_session(self):
        pass

    #  authentication
    @abstractmethod
    def authenticate_user(self, user: str, password: str):
        pass

    #  list of simulations
    @abstractmethod
    def get_simulations(self) -> list:
        pass

    #  delete a simulation
    @abstractmethod
    def delete_simulation(self, id: int) -> list:
        pass

    #  obtain data for tax calculations
    @abstractmethod
    def get_admindata(self) -> AdminData:
        pass

Ogni metodo dell'interfaccia corrisponde a un URL di servizio sul server di calcolo delle imposte.

  • riga 7: l'interfaccia estende la classe [AbstractDao], che gestisce l'accesso al file system;

La mappatura tra metodi e URL di servizio è definita nel file di configurazione [config]:


        # le serveur de calcul de l'impôt
        "server": {
            "urlServer""http://127.0.0.1:5000",
            "user": {
                "login""admin",
                "password""admin"
            },
            "url_services": {
                "calculate-tax""/calculer-impot",
                "get-admindata""/get-admindata",
                "calculate-tax-in-bulk-mode""/calculer-impots",
                "init-session""/init-session",
                "end-session""/fin-session",
                "authenticate-user""/authentifier-utilisateur",
                "get-simulations""/lister-simulations",
                "delete-simulation""/supprimer-simulation"
            }

31.2.2. Implementazione

L'interfaccia [InterfaceImpôtsDaoWithHttpSession] è implementata dalla seguente classe [ImpôtsDaoWithHttpSession]:

#  imports
import json

import requests
import xmltodict
from flask_api import status

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsDaoWithHttpSession import InterfaceImpôtsDaoWithHttpSession
from TaxPayer import TaxPayer

class ImpôtsDaoWithHttpSession(InterfaceImpôtsDaoWithHttpSession):

    #  manufacturer
    def __init__(self, config: dict):
        #  parent initialization
        AbstractImpôtsDao.__init__(self, config)
        #  saving configuration items
        #  general configuration
        self.__config = config
        #  server
        self.__config_server = config["server"]
        #  services
        self.__config_services = config["server"]['url_services']
        #  debug mode
        self.__debug = config["debug"]
        #  logger
        self.__logger = None
        #  cookies
        self.__cookies = None
        #  session type (json, xml)
        self.__session_type = None

        # étape request / response
     def get_response(self, method: str, url_service: str, data_value: dict = None, json_value=None):
        #  [method]: HTTP GET or POST method
        #  [url_service] : URL of service
        #  [data]: POST parameters in x-www-form-urlencoded
        #  [json]: POST parameters in json
        #  [cookies]: cookies to include in the request

        #  you must have a XML or JSON session, otherwise you won't be able to handle the response
        if self.__session_type not in ['json', 'xml']:
            raise ImpôtsError(73, "il n'y a pas de session valide en cours")

        #  connection
        if method == "GET":
            #  GET
            response = requests.get(url_service, cookies=self.__cookies)
        else:
            #  POST
            response = requests.post(url_service, data=data_value, json=json_value, cookies=self.__cookies)

        #  debug mode?
        if self.__debug:
            #  logger
            if not self.__logger:
                self.__logger = self.__config['logger']
            #  log on
            self.__logger.write(f"{response.text}\n")

        #  result
        if self.__session_type == "json":
            résultat = json.loads(response.text)
        else:  #  xml
            résultat = xmltodict.parse(response.text[39:])['root']

        #  retrieve response cookies, if any
        if response.cookies:
            self.__cookies = response.cookies

        #  status code
        status_code = response.status_code

        #  if status code other than 200 OK
        if status_code != status.HTTP_200_OK:
            raise ImpôtsError(35, résultat['réponse'])

        #  we return the result
        return résultat['réponse']

    def init_session(self, session_type: str):
        #  note the session type
        self.__session_type = session_type

        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['init-session']}/{session_type}"

        #  request execution
        self.get_response("GET", url_service)

  • righe 16–34: il costruttore della classe;
  • riga 19: la classe padre viene inizializzata;
  • righe 21–28: vengono memorizzati alcuni dati di configurazione;
  • righe 29–34: vengono create tre proprietà utilizzate nei metodi della classe;
  • righe 36–82: il metodo [get_response] estrae ciò che è comune a tutti i metodi nel livello [dao]: l'invio di una richiesta HTTP e il recupero della risposta HTTP dal server;
  • righe 38–42: definizione dei 5 parametri del metodo [get_response];
  • riga 42: si noti che, poiché il server mantiene una sessione, il client deve leggere/inviare i cookie;
  • righe 44–46: verifichiamo che esista effettivamente una sessione attiva valida;
  • riga 51: caso GET. I cookie ricevuti vengono rispediti;
  • riga 54: caso POST. Questo può avere due tipi di parametri:
    • il tipo [x-www-form-urlencoded]. È il caso degli URL [/calculate-tax] e [/authenticate-user]. Utilizziamo quindi il parametro [data_value] ricevuto dal metodo;
    • il tipo [json]. È il caso dell'URL [/calculate-taxes]. Utilizziamo quindi il parametro [json_value] ricevuto dal metodo;

Anche in questo caso viene restituito il cookie di sessione.

  • righe 56–62: se in modalità [debug], la risposta del server viene registrata nel log. Questo log è importante perché ci permette di sapere esattamente cosa ha restituito il server;
  • righe 64–68: a seconda che ci si trovi in modalità JSON o XML, la risposta testuale del server viene convertita in un dizionario. Prendiamo l'esempio dell'URL [/init-session]:

La risposta JSON è la seguente:


2020-08-03 11:45:21.218116, MainThread : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}

La risposta XML è la seguente:


2020-08-03 11:45:54.671871, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>

Il codice nelle righe 64–68 garantisce che, in entrambi i casi, [result] contenga un dizionario con le chiavi [action, status, response];

  • righe 70–72: se la risposta contiene cookie, questi vengono recuperati. Devono essere rinviati con la richiesta successiva;
  • righe 74–79: se lo stato HTTP della risposta non è 200, viene generata un'eccezione con il messaggio di errore contenuto in result[‘response’]. Può trattarsi di un singolo errore o di un elenco di errori;
  • righe 81–82: restituiscono la risposta del server al codice chiamante;

[init_session]

  • riga 84: il metodo [init_session] viene utilizzato per impostare il tipo di sessione (JSON o XML) che il client desidera avviare con il server;
  • riga 86: il tipo di sessione desiderato viene memorizzato all'interno della classe. Infatti, tutti i metodi richiedono questa informazione per decodificare correttamente la risposta del server;
  • righe 88-90: utilizzando la configurazione dell'applicazione, viene determinato l'URL del servizio da interrogare;
  • riga 93: viene interrogato l'URL del servizio. Il risultato del metodo [get_response] non viene recuperato:
    • se genera un'eccezione, l'operazione è fallita. L'eccezione non viene gestita qui e verrà propagata direttamente al codice chiamante, che terminerà quindi il client con un messaggio di errore;
    • se non genera un'eccezione, l'inizializzazione della sessione ha avuto esito positivo;

[authenticate_user]

    def authenticate_user(self, user: str, password: str):
        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['authenticate-user']}"
        post_params = {
            "user": user,
            "password": password
        }

        #  request execution
        self.get_response("POST", url_service, post_params)
  • Il metodo [authenticate_user] viene utilizzato per l'autenticazione con il server. A tal fine, riceve le credenziali di accesso [user, password] alla riga 1;
  • righe 2–4: si determina l'URL del servizio da interrogare;
  • righe 5–8: i parametri POST, poiché l'URL [/authenticate-user] si aspetta una richiesta POST con i parametri [user, password];
  • riga 11: la richiesta viene eseguita. Anche in questo caso, non recuperiamo la risposta del server. È l'eccezione generata da [get_response] che indica se l'operazione ha avuto esito positivo o meno;

[calculate_tax]

    def calculate_tax(self, taxpayer: TaxPayer):
        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['calculate-tax']}"
        #  POST parameters
        post_params = {
            "marié": taxpayer.marié,
            "enfants": taxpayer.enfants,
            "salaire": taxpayer.salaire
        }

        #  request execution
        response = self.get_response("POST", url_service, post_params)
        #  update the TaxPayer with the response
        taxpayer.fromdict(response)
  • Il metodo [calculate_tax] calcola l'imposta per un contribuente [taxpayer] passato come parametro. Questo parametro viene modificato dal metodo (riga 15) e costituisce quindi il risultato del metodo;
  • righe 2–4: definiamo l'URL del servizio da interrogare;
  • righe 6–10: i parametri per la richiesta POST da inviare. L'URL del servizio [/calculate-tax] si aspetta una richiesta POST con i parametri [married, children, salary];
  • righe 12–13: la richiesta viene eseguita e viene recuperata la risposta del server. L'URL del servizio [/calculate-tax] restituisce un dizionario con le chiavi relative alle imposte [tax, discount, surcharge, reduction, rate];
  • riga 15: il dizionario ottenuto [response] viene utilizzato per aggiornare il contribuente [taxpayer];

[calculate_tax_in_bulk_mode]

    #  bulk tax calculation
    def calculate_tax_in_bulk_mode(self, taxpayers: list):
        #  we let the exceptions rise

        #  transform taxpayers into a list of dictionaries
        #  we keep only the properties [married, children, salary]
        list_dict_taxpayers = list(
            map(lambda taxpayer:
                taxpayer.asdict(included_keys=[
                    '_TaxPayer__marié',
                    '_TaxPayer__enfants',
                    '_TaxPayer__salaire']),
                taxpayers))

        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['calculate-tax-in-bulk-mode']}"

        #  request execution
        list_dict_taxpayers2 = self.get_response("POST", url_service, data_value=None, json_value=list_dict_taxpayers)
        #  when there's only one taxpayer and you're in an xml session, [list_dict_taxpayers2] isn't a list
        #  in this case we make a list
        if not isinstance(list_dict_taxpayers2, list):
            list_dict_taxpayers2 = [list_dict_taxpayers2]
        #  the initial taxpayer list is updated with the results received
        for i in range(len(taxpayers)):
            #  taxpayers[i] update
            taxpayers[i].fromdict(list_dict_taxpayers2[i])
        #  here the [taxpayers] parameter has been updated with the server results
  • Riga 2: Il metodo riceve un elenco di contribuenti di tipo TaxPayer;
  • righe 7–13: questo elenco di elementi [TaxPayer] viene convertito in un elenco di dizionari [spouse, children, salary];
  • righe 15–17: viene impostato l'URL del servizio;
  • righe 19–20: viene eseguita una richiesta POST, con un corpo JSON costituito dall'elenco di dizionari creato alla riga 7. Viene recuperata la risposta del server;
  • righe 23–24: i test hanno rivelato un problema quando la sessione è di tipo XML:
    • se l'elenco iniziale dei contribuenti ha N elementi (N>1), il risultato è un elenco di N dizionari di tipo [OrderedDict];
    • se l'elenco iniziale ha un solo elemento, il risultato non è un elenco ma un singolo elemento di tipo [OrderedDict];
  • righe 23–24: se questo è il caso (1 elemento), convertiamo il risultato in un elenco di 1 elemento;
  • righe 25–28: questo elenco di dizionari ricevuti contiene l'importo dell'imposta per ciascun contribuente nell'elenco iniziale. Quindi aggiorniamo ciascuno di essi con i risultati ricevuti;

[get_simulations]

1
2
3
4
5
6
7
    def get_simulations(self) -> list:
        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['get-simulations']}"

        #  request execution
        return self.get_response("GET", url_service)
  • riga 1: il metodo richiede l'elenco delle simulazioni eseguite nella sessione corrente;
  • riga 2: il metodo restituisce la risposta del server;

[delete_simulation]

1
2
3
4
5
6
7
    def delete_simulation(self, id: int) -> list:
        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['delete-simulation']}/{id}"

        #  request execution
        return self.get_response("GET", url_service)
  • riga 1: il metodo elimina la simulazione il cui ID viene passato;
  • riga 7: restituisce la risposta del server, ovvero l'elenco delle simulazioni rimaste dopo l'eliminazione richiesta;

[get-admindata]

    def get_admindata(self) -> AdminData:
        #  we let the exceptions rise

        #  service url
        config_server = self.__config_server
        url_service = f"{config_server['urlServer']}{self.__config_services['get-admindata']}"

        #  request execution
        résultat = self.get_response("GET", url_service)

        #  result is a dictionary of str values if xml session
        if self.__session_type == 'xml':
            #  new dictionary
            résultat2 = {}
            #  we take everything digital
            for key, value in résultat.items():
                #  some dictionary elements are lists
                if isinstance(value, list):
                    values = []
                    for value2 in value:
                        values.append(float(value2))
                    résultat2[key] = values
                else:
                    #  others simple elements
                    résultat2[key] = float(value)
        else:
            résultat2 = résultat
        #  result type AdminData
        return AdminData().fromdict(résultat2)
  • riga 1: il metodo richiede le costanti fiscali dal server per calcolare l'imposta;
  • riga 29: restituisce un tipo [AdminData];
  • riga 9: recuperiamo la risposta del server sotto forma di dizionario. I test mostrano che c'è un problema quando la sessione è una sessione XML: invece di essere valori numerici, i valori nel dizionario sono stringhe. Avevamo segnalato questo problema durante lo studio del modulo [xmltodict] e avevamo riscontrato che si trattava di un comportamento normale. [xmltodict] non dispone di informazioni sul tipo nel flusso XML che gli viene fornito. Detto questo, in questo caso specifico, tutti i valori nel dizionario ricevuto devono essere convertiti in valori numerici. Questo dizionario contiene tre elenchi [limites, coeffr, coeffn] e una serie di proprietà numeriche;
  • righe 13–25: creazione di un dizionario [result2] con valori numerici dal dizionario [result] con valori di tipo stringa;
  • riga 29: il dizionario [result2] viene utilizzato per inizializzare un tipo [AdminData];

31.2.3. La factory del livello [dao]

I nostri client saranno multithread. Poiché il livello [dao] è implementato da una classe con stato di lettura/scrittura (= proprietà di lettura/scrittura), ogni thread deve avere il proprio livello [dao], altrimenti l'accesso ai dati condivisi tra i thread deve essere sincronizzato. Qui scegliamo la prima soluzione. Utilizziamo una classe [ImpôtsDaoWithHttpSessionFactory] in grado di creare istanze del livello [dao]:

from ImpôtsDaoWithHttpSession import ImpôtsDaoWithHttpSession

class ImpôtsDaoWithHttpSessionFactory:

    def __init__(self, config: dict):
        #  the parameter
        self.__config = config

    def new_instance(self):
        #  render an instance of the [dao] layer
        return ImpôtsDaoWithHttpSession(self.__config)

31.3. Configurazione del client

Image

I client vengono configurati utilizzando i file [config] e [config_layers]. Il file [config] è il seguente:

def configure(config: dict) -> dict:
    import os

    #  step 1 ------

    #  folder of this file
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #  root path
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    #  absolute dependencies
    absolute_dependencies = [
        #  project files
        #  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",
        #  Constants, slices
        f"{root_dir}/impots/v05/entities",
        #  ImpôtsDaoWithHttpSession, ImpôtsDaoWithHttpSessionFactory, InterfaceImpôtsDaoWithHttpSession
        f"{script_dir}/../services",
        #  configuration scripts
        script_dir,
        #  Logger
        f"{root_dir}/impots/http-servers/02/utilities",
    ]

    #  set the syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #  step 2 ------
    #  application configuration with constants
    config.update({
        #  taxpayer file
        "taxpayersFilename": f"{script_dir}/../data/input/taxpayersdata.txt",
        #  results file
        "resultsFilename": f"{script_dir}/../data/output/résultats.json",
        #  error file
        "errorsFilename": f"{script_dir}/../data/output/errors.txt",
        #  log file
        "logsFilename": f"{script_dir}/../data/logs/logs.txt",
        #  tax calculation server
        "server": {
            "urlServer": "http://127.0.0.1:5000",
            "user": {
                "login": "admin",
                "password": "admin"
            },
            "url_services": {
                "calculate-tax": "/calculer-impot",
                "get-admindata": "/get-admindata",
                "calculate-tax-in-bulk-mode": "/calculer-impots",
                "init-session": "/init-session",
                "end-session": "/fin-session",
                "authenticate-user": "/authentifier-utilisateur",
                "get-simulations": "/lister-simulations",
                "delete-simulation": "/supprimer-simulation"
            }
        },
        #  debug mode
        "debug": True
    }
    )

    #  step 3 ------
    #  layer instantiation
    import config_layers
    config['layers'] = config_layers.configure(config)

    #  we return the configuration
    return config

Il file [config_layers] è il seguente:

def configure(config: dict) -> dict:
    #  instantiation of applicatuon layers

    #  layer [profession]
    from ImpôtsMétier import ImpôtsMétier
    métier = ImpôtsMétier()

    #  factory dao layer
    from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
    dao_factory = ImpôtsDaoWithHttpSessionFactory(config)

    #  make the layer configuration
    return {
        "dao_factory": dao_factory,
        "métier": métier
    }
  • I client non avranno accesso diretto al livello [dao]. Per ottenerlo, devono passare attraverso la factory del livello [dao];

31.4. Il client [main]

Il client [main] consente di testare gli URL [/init-session, /authenticate-user, /calculate-taxes, /end-session]:

#  a json or xml parameter is expected
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

#  configure the application
import config
config = config.configure({"session_type": session_type})

#  dependencies
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger

#  executing the [dao] layer in a thread
#  taxpayers is a list of taxpayers
def thread_function(config: dict, taxpayers: list):
    #  retrieve the [dao] layer factory
    dao_factory = config['layers']['dao_factory']
    #  create a [dao] layer instance
    dao = dao_factory.new_instance()
    #  session type
    session_type = config['session_type']
    #  number of taxpayers
    nb_taxpayers = len(taxpayers)
    #  log
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    #  initialize the session
    dao.init_session(session_type)
    #  authenticate
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    #  taxpayers' taxes are calculated
    dao.calculate_tax_in_bulk_mode(taxpayers)
    #  end of session
    dao.end_session()
    #  log
    logger.write(f"fin du calcul de l'impôt des {nb_taxpayers} contribuables\n")

#  list of client threads
threads = []
logger = None
#  code
try:
    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger
    #  start log
    logger.write("début du calcul de l'impôt des contribuables\n")
    #  retrieve the [dao] layer factory
    dao_factory = config["layers"]["dao_factory"]
    #  create an instance of the [dao] layer
    dao = dao_factory.new_instance()
    #  reading taxpayer data
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #  taxpayers?
    if not taxpayers:
        raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #  multi-threaded tax calculation for taxpayers
    i = 0
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        #  each thread will process from 1 to 4 contributors
        nb_taxpayers = min(l_taxpayers - i, random.randint(1, 4))
        #  the list of taxpayers processed by the thread
        thread_taxpayers = taxpayers[slice(i, i + nb_taxpayers)]
        #  i is incremented for the next thread
        i += nb_taxpayers
        #  create the thread
        thread = threading.Thread(target=thread_function, args=(config, thread_taxpayers))
        #  we add it to the list of threads in the main script
        threads.append(thread)
        #  we launch the thread - this operation is asynchronous - we don't wait for the thread's result
        thread.start()
    #  the main thread waits for all threads it has launched to finish
    for thread in threads:
        thread.join()
    #  here all threads have finished their work - each has modified one or more objects [taxpayer]
    #  save the results in the jSON file
    dao.write_taxpayers_results(taxpayers)
    #  end
except BaseException as erreur:
    #  error display
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  close the logger
    if logger:
        #  end log
        logger.write("fin du calcul de l'impôt des contribuables\n")
        #  closing the logger
        logger.close()
    #  we're done
    print("Travail terminé...")
    #  end of threads that might still exist if we stopped on error
    sys.exit()
  • righe 4-11: il client si aspetta un parametro che specifichi il tipo di sessione, JSON o XML, da utilizzare con il server;
  • righe 13-15: il client viene configurato;
  • righe 48–104: questo codice è familiare. È stato utilizzato molte volte. Distribuisce i contribuenti per i quali vogliamo calcolare l'imposta su più thread;
  • riga 26: il metodo [thread_function] è il metodo eseguito da ciascun thread per calcolare l'imposta per i contribuenti ad esso assegnati;
  • righe 27–30: ogni thread ha il proprio livello [dao];
  • Il calcolo dell'imposta viene eseguito in quattro fasi:
    • righe 37–38: inizializzazione di una sessione (JSON o XML) con il server;
    • righe 39–40: autenticazione con il server;
    • righe 41–42: calcolo dell'imposta;
    • righe 43–44: chiusura della sessione con il server;

Quando questo codice viene eseguito in modalità [json], vengono generati i seguenti log:


2020-08-03 14:28:34.320751, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:28:34.328749, Thread-1 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.328749, Thread-2 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.333592, Thread-3 : début du calcul de l'impôt des 3 contribuables
2020-08-03 14:28:34.368651, Thread-2 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.375699, Thread-1 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.377432, Thread-3 : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 14:28:34.385653, Thread-2 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.392656, Thread-1 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.396377, Thread-3 : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 14:28:34.406528, Thread-2 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}]}
2020-08-03 14:28:34.413837, Thread-1 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}]}
2020-08-03 14:28:34.416695, Thread-3 : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 3}]}
2020-08-03 14:28:34.425747, Thread-2 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.425747, Thread-2 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.428956, Thread-1 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.428956, Thread-1 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:28:34.428956, Thread-3 : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 14:28:34.428956, Thread-3 : fin du calcul de l'impôt des 3 contribuables
2020-08-03 14:28:34.428956, MainThread : fin du calcul de l'impôt des contribuables

Quanto sopra mostra il percorso di esecuzione del thread [Thread-2].

Se eseguiamo [main] in modalità XML, i log sono i seguenti:


2020-08-03 14:32:48.495316, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:32:48.496452, Thread-1 : début du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.498992, Thread-2 : début du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.498992, Thread-3 : début du calcul de l'impôt des 4 contribuables
2020-08-03 14:32:48.498992, Thread-4 : début du calcul de l'impôt des 3 contribuables
2020-08-03 14:32:48.538637, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.540783, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.547811, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.547811, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:32:48.555184, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.564793, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.564793, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.568333, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:32:48.568333, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>2</enfants><salaire>55555</salaire><impôt>2814</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>oui</marié><enfants>2</enfants><salaire>50000</salaire><impôt>1384</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>384</décôte><réduction>347</réduction><id>2</id></réponse></root>
2020-08-03 14:32:48.579205, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>3</enfants><salaire>50000</salaire><impôt>0</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>720</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>non</marié><enfants>2</enfants><salaire>100000</salaire><impôt>19884</impôt><surcôte>4480</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse></root>
2020-08-03 14:32:48.579205, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>non</marié><enfants>3</enfants><salaire>100000</salaire><impôt>16782</impôt><surcôte>7176</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>oui</marié><enfants>3</enfants><salaire>100000</salaire><impôt>9200</impôt><surcôte>2180</surcôte><taux>0.3</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse><réponse><marié>oui</marié><enfants>5</enfants><salaire>100000</salaire><impôt>4230</impôt><surcôte>0</surcôte><taux>0.14</taux><décôte>0</décôte><réduction>0</réduction><id>3</id></réponse><réponse><marié>non</marié><enfants>0</enfants><salaire>100000</salaire><impôt>22986</impôt><surcôte>0</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>4</id></réponse></root>
2020-08-03 14:32:48.588051, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>calculer-impots</action><état>1500</état><réponse><marié>oui</marié><enfants>2</enfants><salaire>30000</salaire><impôt>0</impôt><surcôte>0</surcôte><taux>0.0</taux><décôte>0</décôte><réduction>0</réduction><id>1</id></réponse><réponse><marié>non</marié><enfants>0</enfants><salaire>200000</salaire><impôt>64210</impôt><surcôte>7498</surcôte><taux>0.45</taux><décôte>0</décôte><réduction>0</réduction><id>2</id></réponse><réponse><marié>oui</marié><enfants>3</enfants><salaire>200000</salaire><impôt>42842</impôt><surcôte>17283</surcôte><taux>0.41</taux><décôte>0</décôte><réduction>0</réduction><id>3</id></réponse></root>
2020-08-03 14:32:48.594058, Thread-1 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-1 : fin du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.595198, Thread-2 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-2 : fin du calcul de l'impôt des 2 contribuables
2020-08-03 14:32:48.595198, Thread-3 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.595198, Thread-3 : fin du calcul de l'impôt des 4 contribuables
2020-08-03 14:32:48.603351, Thread-4 : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:32:48.603351, Thread-4 : fin du calcul de l'impôt des 3 contribuables
2020-08-03 14:32:48.603351, MainThread : fin du calcul de l'impôt des contribuables

Quella che segue è la traccia del thread [Thread-2].

31.5. Il client [main2]

Image

Il client [main2] consente di testare gli URL [/init-session, /authenticate-user, /get-admindata, /end-session]:

#  a json or xml parameter is expected
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

#  configure the application
import config
config = config.configure({"session_type": session_type})

#  dependencies
from ImpôtsError import ImpôtsError
from Logger import Logger

logger = None
#  code
try:
    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger
    #  start log
    logger.write("début du calcul de l'impôt des contribuables\n")
    #  retrieve the [dao] layer factory
    dao_factory = config['layers']['dao_factory']
    #  create an instance of the [dao] layer
    dao = dao_factory.new_instance()
    #  we get the taxpayers back
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #  taxpayers?
    if not taxpayers:
        raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #  session type
    session_type = config['session_type']
    #  initialize the session
    dao.init_session(session_type)
    #  authenticate
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    #  we retrieve data from the tax authorities
    admindata = dao.get_admindata()
    #  end of session
    dao.end_session()
    #  calculation of taxpayers' taxes by the [business] layer
    métier = config['layers']['métier']
    for taxpayer in taxpayers:
        métier.calculate_tax(taxpayer, admindata)
    #  save the results in the jSON file
    dao.write_taxpayers_results(taxpayers)
 except BaseException as erreur:
     #  error display
     print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  close the logger
    if logger:
        #  end log
        logger.write("fin du calcul de l'impôt des contribuables\n")
        #  closing the logger
        logger.close()
    #  we're done
    print("Travail terminé...")
  • righe 1-11: recuperiamo il parametro [json, xml] che imposta il tipo di sessione da stabilire con il server;
  • righe 13-15: configuriamo il client;
  • righe 30-33: creiamo un livello [dao];
  • righe 34-35: utilizzandolo, recuperiamo l'elenco dei contribuenti per i quali deve essere calcolata l'imposta;
  • passiamo quindi attraverso le quattro fasi del dialogo con il server;
    • righe 41–42: viene avviata una sessione con il server;
    • righe 43–44: ci autentichiamo con il server;
    • righe 45-46: richiediamo al server le costanti fiscali per calcolare l'imposta;
    • righe 47–48: la sessione con il server viene chiusa;
  • righe 49–52: utilizzando queste costanti, siamo in grado di calcolare l'imposta dei contribuenti utilizzando il livello locale [aziendale] sul client;
  • righe 53–54: i risultati vengono salvati;

Per una sessione XML, i risultati sono i seguenti:


2020-08-03 14:44:43.194294, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 14:44:43.231633, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>init-session</action><état>700</état><réponse>session démarrée avec le type de réponse xml</réponse></root>
2020-08-03 14:44:43.240872, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>authentifier-utilisateur</action><état>200</état><réponse>Authentification réussie</réponse></root>
2020-08-03 14:44:43.250061, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>get-admindata</action><état>1000</état><réponse><limites>9964.0</limites><limites>27519.0</limites><limites>73779.0</limites><limites>156244.0</limites><limites>93749.0</limites><coeffr>0.0</coeffr><coeffr>0.14</coeffr><coeffr>0.3</coeffr><coeffr>0.41</coeffr><coeffr>0.45</coeffr><coeffn>0.0</coeffn><coeffn>1394.96</coeffn><coeffn>5798.0</coeffn><coeffn>13913.7</coeffn><coeffn>20163.4</coeffn><abattement_dixpourcent_min>437.0</abattement_dixpourcent_min><plafond_impot_couple_pour_decote>2627.0</plafond_impot_couple_pour_decote><plafond_decote_couple>1970.0</plafond_decote_couple><valeur_reduc_demi_part>3797.0</valeur_reduc_demi_part><plafond_revenus_celibataire_pour_reduction>21037.0</plafond_revenus_celibataire_pour_reduction><id>1</id><abattement_dixpourcent_max>12502.0</abattement_dixpourcent_max><plafond_impot_celibataire_pour_decote>1595.0</plafond_impot_celibataire_pour_decote><plafond_decote_celibataire>1196.0</plafond_decote_celibataire><plafond_revenus_couple_pour_reduction>42074.0</plafond_revenus_couple_pour_reduction><plafond_qf_demi_part>1551.0</plafond_qf_demi_part></réponse></root>
2020-08-03 14:44:43.269850, MainThread : <?xml version="1.0" encoding="utf-8"?>
<root><action>fin-session</action><état>400</état><réponse>session réinitialisée</réponse></root>
2020-08-03 14:44:43.269850, MainThread : fin du calcul de l'impôt des contribuables

31.6. Il client [main3]

Il client [main3] consente di testare gli URL [/init-session, /calculate-taxes, /get-simulations, /delete-simulation, /end-session]:

Image

#  a json or xml parameter is expected
import sys

syntaxe = f"{sys.argv[0]} json / xml"
erreur = len(sys.argv) != 2
if not erreur:
    session_type = sys.argv[1].lower()
    erreur = session_type != "json" and session_type != "xml"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

#  configure the application
import config
config = config.configure({"session_type": session_type})

#  dependencies
from ImpôtsError import ImpôtsError
import sys
from Logger import Logger

logger = None
#  code
try:
    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger
    #  start log
    logger.write("début du calcul de l'impôt des contribuables\n")
    #  retrieve the [dao] layer factory
    dao_factory = config["layers"]["dao_factory"]
    #  create an instance of the [dao] layer
    dao = dao_factory.new_instance()
    #  reading taxpayer data
    taxpayers = dao.get_taxpayers_data()["taxpayers"]
    #  taxpayers?
    if not taxpayers:
        raise ImpôtsError(36, f"Pas de contribuables valides dans le fichier {config['taxpayersFilename']}")
    #  tax calculation
    #  number of taxpayers
    nb_taxpayers = len(taxpayers)
    #  log
    logger.write(f"début du calcul de l'impôt des {nb_taxpayers} contribuables\n")
    #  initialize the session
    dao.init_session(session_type)
    #  authenticate
    dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
    #  taxpayers' taxes are calculated
    dao.calculate_tax_in_bulk_mode(taxpayers)
    #  the list of simulations is requested
    simulations = dao.get_simulations()
    #  we remove one out of two
    for i in range(len(simulations)):
        if i % 2 == 0:
            #  we delete the simulation
            dao.delete_simulation(simulations[i]['id'])
    #  end of session
    dao.end_session()
    #  consult the logs to see the various results (debug mode=True)
except BaseException as erreur:
    #  error display
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    #  close the logger
    if logger:
        #  end log
        logger.write("fin du calcul de l'impôt des contribuables\n")
        #  closing the logger
        logger.close()
    #  we're done
    print("Travail terminé...")
  • righe 1-11: recuperiamo il tipo di sessione dai parametri dello script;
  • righe 13-15: configuriamo l'applicazione;
  • righe 25-50: codice già spiegato in precedenza;
  • righe 51-52: richiediamo l'elenco delle simulazioni eseguite nella sessione corrente;
  • righe 53-57: eliminiamo una simulazione su due;
  • righe 58–59: la sessione viene terminata;

Durante una sessione jSON, i log sono i seguenti:


2020-08-03 15:01:52.702297, MainThread : début du calcul de l'impôt des contribuables
2020-08-03 15:01:52.702297, MainThread : début du calcul de l'impôt des 11 contribuables
2020-08-03 15:01:52.734806, MainThread : {"action": "init-session", "état": 700, "réponse": ["session démarrée avec le type de réponse json"]}
2020-08-03 15:01:52.747961, MainThread : {"action": "authentifier-utilisateur", "état": 200, "réponse": "Authentification réussie"}
2020-08-03 15:01:52.765721, MainThread : {"action": "calculer-impots", "état": 1500, "réponse": [{"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 1}, {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347, "id": 2}, {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0, "id": 3}, {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 4}, {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 5}, {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0, "id": 6}, {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0, "id": 7}, {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 8}, {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0, "id": 9}, {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0, "id": 10}, {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0, "id": 11}]}
2020-08-03 15:01:52.785505, MainThread : {"action": "lister-simulations", "état": 500, "réponse": [{"décôte": 0, "enfants": 2, "id": 1, "impôt": 2814, "marié": "oui", "réduction": 0, "salaire": 55555, "surcôte": 0, "taux": 0.14}, {"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 720, "enfants": 3, "id": 3, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.801475, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 720, "enfants": 3, "id": 3, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.810129, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 5, "impôt": 16782, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 7176, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.818803, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 5, "id": 7, "impôt": 4230, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.834604, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 2, "id": 9, "impôt": 0, "marié": "oui", "réduction": 0, "salaire": 30000, "surcôte": 0, "taux": 0.0}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.843803, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}, {"décôte": 0, "enfants": 3, "id": 11, "impôt": 42842, "marié": "oui", "réduction": 0, "salaire": 200000, "surcôte": 17283, "taux": 0.41}]}
2020-08-03 15:01:52.851855, MainThread : {"action": "supprimer-simulation", "état": 600, "réponse": [{"décôte": 384, "enfants": 2, "id": 2, "impôt": 1384, "marié": "oui", "réduction": 347, "salaire": 50000, "surcôte": 0, "taux": 0.14}, {"décôte": 0, "enfants": 2, "id": 4, "impôt": 19884, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 4480, "taux": 0.41}, {"décôte": 0, "enfants": 3, "id": 6, "impôt": 9200, "marié": "oui", "réduction": 0, "salaire": 100000, "surcôte": 2180, "taux": 0.3}, {"décôte": 0, "enfants": 0, "id": 8, "impôt": 22986, "marié": "non", "réduction": 0, "salaire": 100000, "surcôte": 0, "taux": 0.41}, {"décôte": 0, "enfants": 0, "id": 10, "impôt": 64210, "marié": "non", "réduction": 0, "salaire": 200000, "surcôte": 7498, "taux": 0.45}]}
2020-08-03 15:01:52.863165, MainThread : {"action": "fin-session", "état": 400, "réponse": "session réinitialisée"}
2020-08-03 15:01:52.863165, MainThread : fin du calcul de l'impôt des contribuables
  • riga 6: abbiamo 11 simulazioni;
  • riga 12: dopo le varie cancellazioni, ne rimangono solo 5;

31.7. La classe di test [Test2HttpClientDaoWithSession]

Image

La classe [Test2HttpClientDaoWithSession] verifica il livello [dao] dei client come segue:

import unittest

from ImpôtsError import ImpôtsError
from Logger import Logger
from TaxPayer import TaxPayer

class Test2HttpClientDaoWithSession(unittest.TestCase):

    def test_init_session_json(self) -> None:
        print('test_init_session_json')
        erreur = False
        try:
            dao.init_session('json')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  there must be no error
        self.assertFalse(erreur)

    def test_init_session_xml(self) -> None:
        print('test_init_session_xml')
        erreur = False
        try:
            dao.init_session('xml')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  here must be no errors
        self.assertFalse(erreur)

    def test_init_session_xxx(self) -> None:
        print('test_init_session_xxx')
        erreur = False
        try:
            dao.init_session('xxx')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  there must be an error
        self.assertTrue(erreur)

    def test_authenticate_user_success(self) -> None:
        print('test_authenticate_user_success')
        #  init session
        dao.init_session('json')
        #  test
        erreur = False
        try:
            dao.authenticate_user(config['server']['user']['login'], config['server']['user']['password'])
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  there must be no errors
        self.assertFalse(erreur)

    def test_authenticate_user_failed(self) -> None:
        print('test_authenticate_user_failed')
        #  init session
        dao.init_session('json')
        #  test
        erreur = False
        try:
            dao.authenticate_user('x', 'y')
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  there must be an error
        self.assertTrue(erreur)

    def test_get_simulations(self) -> None:
        print('test_get_simulations')
        #  init session
        dao.init_session('json')
        #  authentication
        dao.authenticate_user('admin', 'admin')
        #  tax calculation
        #  { 'married': 'yes', 'children': 2, 'salary': 55555,
        #  tax': 2814, 'surcôte': 0, 'décôte': 0, 'réduction': 0, 'taux': 0.14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        #  get_simulations
        simulations = dao.get_simulations()
        #  checks
        #  there must be 1 simulation
        self.assertEqual(1, len(simulations))
        simulation = simulations[0]
        #  verification of calculated tax
        self.assertAlmostEqual(simulation['impôt'], 2815, delta=1)
        self.assertEqual(simulation['décôte'], 0)
        self.assertEqual(simulation['réduction'], 0)
        self.assertAlmostEqual(simulation['taux'], 0.14, delta=0.01)
        self.assertEqual(simulation['surcôte'], 0)

    def test_delete_simulation(self) -> None:
        print('test_delete_simulation')
        #  init session
        dao.init_session('json')
        #  authentication
        dao.authenticate_user('admin', 'admin')
        #  tax calculation
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        #  get_simulations
        simulations = dao.get_simulations()
        #  delete_simulation
        dao.delete_simulation(simulations[0]['id'])
        #  get_simulations
        simulations = dao.get_simulations()
        #  verification - there should be no more simulations
        self.assertEqual(0, len(simulations))
        #  delete a non-existent simulation
        erreur = False
        try:
            dao.delete_simulation(100)
        except ImpôtsError as ex:
            print(ex)
            erreur = True
        #  there must be an error
        self.assertTrue(erreur)

if __name__ == '__main__':
    #  configure the application
    import config
    config = config.configure({})

    #  logger
    logger = Logger(config["logsFilename"])
    #  we save it in the config
    config["logger"] = logger

    #  layer [dao]
    dao_factory = config['layers']['dao_factory']
    dao = dao_factory.new_instance()

    #  test methods are executed
    print("tests en cours...")
    unittest.main()
  • Il livello [dao] invia una richiesta al server, riceve la sua risposta e la formatta per restituirla al codice chiamante. Quando il server invia una risposta con un codice di stato diverso da 200, il livello [dao] genera un'eccezione. Pertanto, diversi test prevedono la verifica dell'eventuale presenza di un'eccezione;
  • righe 9–18: inizializziamo una sessione JSON. Non dovrebbero esserci errori;
  • righe 20–29: inizializziamo una sessione XML. Non dovrebbero esserci errori;
  • righe 31–40: inizializziamo una sessione con un tipo errato. Deve verificarsi un errore;
  • righe 42–54: effettuiamo l'autenticazione con le credenziali corrette. Non dovrebbero verificarsi errori;
  • righe 56–68: autenticazione utilizzando credenziali errate. Deve verificarsi un errore;
  • righe 70–92: calcoliamo l'imposta e poi richiediamo l'elenco delle simulazioni. Dovremmo ottenerne una. Inoltre, verifichiamo che questa simulazione contenga l'imposta richiesta;
  • righe 94–119: viene creata una simulazione e poi eliminata. Quindi si tenta di eliminare una simulazione anche se non ne sono rimaste. Deve verificarsi un errore;
  • righe 121–137: il test viene eseguito come uno script standard da console;
  • righe 122–124: configuriamo l'applicazione;
  • righe 126–129: configuriamo il logger. Questo ci permetterà di tracciare i log;
  • righe 131–133: istanziamo il livello [DAO] che verrà testato;
  • righe 135–137: eseguiamo i test;

L'output della console è il seguente:


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/http-clients/07/tests/Test2HttpClientDaoWithSession.py
tests en cours...
test_authenticate_user_failed
..MyException[35, ["Echec de l'authentification"]]
test_authenticate_user_success
test_delete_simulation
MyException[35, ["la simulation n° [100] n'existe pas"]]
test_get_simulations
test_init_session_json
test_init_session_xml
test_init_session_xxx
MyException[73, il n'y a pas de session valide en cours]
----------------------------------------------------------------------
Ran 7 tests in 0.171s
 
OK
 
Process finished with exit code 0