Skip to content

25. Exercício prático: versão 8

25.1. Introdução

Vamos escrever um novo aplicativo cliente/servidor. A novidade do servidor é que ele irá gerenciar uma sessão. Em vez de colocar os dados da administração fiscal em um objeto de escopo [application], vamos colocá-los em um objeto de escopo [session]. Ao fazer isso, há uma redução no desempenho do código. Quando um objeto pode ser compartilhado em modo somente leitura por todos os usuários, é preferível defini-lo como um objeto de escopo [application], em vez de um de escopo [session]. Ganha-se, no mínimo, em largura de banda, já que, assim, diminui-se o tamanho do cookie de sessão. Mas queremos mostrar uma aplicação cliente/servidor em que o cliente e o servidor trocam um cookie de sessão.

A arquitetura da aplicação não muda:

Image

25.2. O servidor web

A estrutura de diretórios dos scripts do servidor é a seguinte:

Image

A pasta [http-servers/03] é obtida inicialmente por meio da cópia da pasta [http-servers/02]. Em seguida, são feitas as modificações.

25.2.1. A configuração

É a mesma da |versão anterior|, com algumas alterações no script [config]:


# dependências absolutas
    absolute_dependencies = [
        # pastas do projeto
        # 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",
        # Constantes, Faixas
        f"{root_dir}/impots/v05/entities",
        # index_controller
        f"{script_dir}/../controllers",
        # scripts [config_database, config_layers]
        script_dir,
        # Logger, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
    ]
  • linha 17: vamos reescrever um controlador para a função [index], que processa o URL / ;
  • linha 21: utilizamos os utilitários da |versão anterior|;

25.2.2. O script principal [main]

O novo script [main] traz algumas alterações ao script principal [main] da versão anterior:


# o aplicativo Flask pode ser iniciado
app = Flask(__name__)
# chave secreta da sessão
app.secret_key = os.urandom(12).hex()
  • linha 4: cria-se uma chave secreta para o aplicativo. Sabe-se que ela é necessária para gerenciar as sessões;

Além disso, os dados fiscais não são mais solicitados no código do [main]. As seguintes linhas foram removidas:


# recuperação de dados da administração fiscal
erreur = False
try:
    # admindata será um dado de escopo da aplicação, somente para leitura
    config["admindata"] = config["layers"]["dao"].get_admindata()
    # registro de sucesso
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    # registro do erro
    erreur = True
    # registro de erro
    log = f"L'erreur suivante s'est produite : {ex}"
    # console
    print(log)
    # arquivo de logs
    logger.write(f"{log}\n")
    # e-mail para o administrador
    send_adminmail(config, log)

Além disso, o controlador [index_controller] aceita um parâmetro adicional, a sessão do Flask:


from flask import request, Flask, session
….        
         # a consulta é executada por um controlador
        résultat, status_code = index_controller.execute(request, session, config)

25.2.3. O controlador [index_controller]

O controlador [index_controller] agora gerencia uma sessão:


# importação de dependências
import os
import re
import threading

from flask_api import status
from werkzeug.local import LocalProxy

# URL configurado: /?casado=xx&filhos=yy&salário=zz
from AdminData import AdminData
from ImpôtsError import ImpôtsError


def execute(request: LocalProxy, session: LocalProxy, config: dict) -> tuple:
    # dependências
    from TaxPayer import TaxPayer

    # inicialmente, sem erros
    erreurs = []
    

    # erros?
    if erreurs:
        # envia-se uma resposta de erro ao cliente
        return {"réponse": {"erreurs": erreurs}}, status.HTTP_400_BAD_REQUEST

    # sem erros, é possível trabalhar
    # recuperamos a configuração associada ao thread
    thread_name = threading.current_thread().name
    logger = config[thread_name]["config"]["logger"]
    # executando a consulta
    réponse = None
    try:
        # o caso mais simples — o admindata já está na sessão
        if session.get('client_id') is not None:
            # recuperamos as informações da sessão
            client_id = session.get('client_id')
            admindata = AdminData().fromdict(session.get('admindata'))
            # registro
            logger.write(f"[index_controller] client [{client_id}], données fiscales prises en session\n")
        else:
            # recuperação dos dados da administração fiscal
            admindata = config["layers"]["dao"].get_admindata()
            # inicialização da sessão do admindata
            session['admindata'] = admindata.asdict()
            # atribui-se um número ao cliente e esse número é registrado na sessão
            # isso nos permitirá acompanhá-lo nos logs do servidor
            client_id = os.urandom(12).hex()
            session['client_id'] = client_id
            # log
            logger.write(f"[index_controller] client [{client_id}], données fiscales prises dans la couche dao\n")
        # cálculo do imposto
        taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
        config["layers"]["métier"].calculate_tax(taxpayer, admindata)
        # enviamos a resposta ao cliente
        return {"réponse": {"result": taxpayer.asdict()}}, status.HTTP_200_OK
    except (BaseException, ImpôtsError) as erreur:
        # enviamos a resposta ao cliente
        return {"réponse": {"erreurs": [f"{erreur}"]}}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • linha 14: o controlador recebe a sessão atual do cliente web;
  • linhas 35-38: se o cliente tiver uma sessão, ela conterá duas chaves:
    • [client_id]: um número de cliente (linha 37);
    • [admindata]: os dados da administração fiscal na forma de um dicionário (linha 38);
  • linha 35: verifica-se se a sessão contém uma das duas chaves esperadas;
  • linhas 42-51: caso em que a sessão do cliente ainda não tenha sido inicializada;
    • linha 43: recuperam-se os dados da administração fiscal da camada [dao];
    • linha 45: esses dados são inseridos na sessão na forma de um dicionário;
    • linha 48: atribui-se um número aleatório ao cliente. Esse número será diferente para cada cliente;
    • linha 49: esse número é inserido na sessão;
    • linha 51: registra-se que os dados da administração fiscal foram obtidos pela camada [dao]. Os acessos à camada [dao] costumam ser onerosos. Por isso, é preciso limitá-los. A ideia aqui é obter os dados fiscais uma única vez da camada [dao], colocá-los na sessão e buscá-los lá nas consultas posteriores do mesmo cliente. Vale lembrar que essa não é a melhor solução. Como os dados fiscais da administração são os mesmos para todos os clientes, eles devem ser armazenados em um objeto de escopo de aplicação;
  • linhas 35-40: caso em que a sessão do cliente tenha sido inicializada em uma solicitação anterior;
    • linha 37: recupera-se o número do cliente na sessão;
    • linha 38: recuperam-se os dados fiscais da administração na sessão;
    • linha 40: registra-se o fato de que o cliente obteve os dados fiscais da administração na sessão;

25.3. O cliente web

Image

25.3.1. A camada [dao]

25.3.1.1. A classe [ImpôtsDaoWithHttpSession]

A camada [dao] é implementada pela seguinte classe [ImpôtsDaoWithHttpSession]:


# importações

import requests
from flask_api import status
from myutils import decode_flask_session

from AbstractImpôtsDao import AbstractImpôtsDao
from AdminData import AdminData
from ImpôtsError import ImpôtsError
from InterfaceImpôtsMétier import InterfaceImpôtsMétier
from TaxPayer import TaxPayer


class ImpôtsDaoWithHttpSession(AbstractImpôtsDao, InterfaceImpôtsMétier):

    # gerador
    def __init__(self, config: dict):
        # inicialização do pai
        AbstractImpôtsDao.__init__(self, config)
        # armazenamento dos elementos da configuração
        # configuração geral
        self.__config = config
        # servidor
        self.__config_server = config["server"]
        # modo de depuração
        self.__debug = config["debug"]
        # registrador
        self.__logger = None
        # cookies
        self.__cookies = None

    # método não utilizado
    def get_admindata(self) -> AdminData:
        pass

    # cálculo do imposto
    def calculate_tax(self, taxpayer: TaxPayer, admindata: AdminData = None):
        # permitir que as exceções sejam propagadas
        # parâmetros do GET
        params = {"marié": taxpayer.marié, "enfants": taxpayer.enfants, "salaire": taxpayer.salaire}
        # conexão com autenticação Auth Basic?
        if self.__config_server['authBasic']:
            response = requests.get(
                # URL do servidor consultado
                self.__config_server['urlServer'],
                # parâmetros do URL
                params=params,
                # autenticação Basic
                auth=(
                    self.__config_server["user"]["login"],
                    self.__config_server["user"]["password"]),
                cookies=self.__cookies)

        else:
            # conexão sem autenticação Auth Basic
            response = requests.get(self.__config_server['urlServer'], params=params, cookies=self.__cookies)
        # recupera-se os cookies da resposta, caso haja
        if response.cookies:
            self.__cookies = response.cookies
            # recupera-se o cookie de sessão
            session_cookie = response.cookies.get('session')
            # decodifica-se para fazer o login
            if session_cookie:
                # programa de login
                if not self.__logger:
                    self.__logger = self.__config['logger']
                # registra
                self.__logger.write(f"cookie de session={decode_flask_session(session_cookie)}\n")

        # modo de depuração?
        if self.__debug:
            # registrador
            if not self.__logger:
                self.__logger = self.__config['logger']
            # registrando
            self.__logger.write(f"{response.text}\n")
        # código de status da resposta HTTP
        status_code = response.status_code
        # a resposta jSON é inserida em um dicionário
        résultat = response.json()
        # erro se o código de status for diferente de 200 OK
        if status_code != status.HTTP_200_OK:
            # sabe-se que os erros foram associados à chave [erreurs] da resposta
            raise ImpôtsError(87, résultat['réponse']['erreurs'])
        # sabe-se que o resultado foi associado à chave [result] da resposta
        # modifica-se o parâmetro de entrada com esse resultado
        taxpayer.fromdict(résultat["réponse"]["result"])
  • linha 30: a camada [dao] irá gerenciar um dicionário de cookies;
  • linha 58: a propriedade [response.cookies] é um dicionário que reúne os cookies enviados pelo servidor nos cabeçalhos HTTP e [Set-Cookie];
  • linha 59: esses cookies são armazenados na camada [dao]. Eles serão reenviados ao servidor em solicitações posteriores do mesmo cliente;
  • linhas 60-68: embora não seja indispensável, recuperamos o cookie de sessão. No dicionário de cookies enviados pelo servidor, o cookie de sessão está associado à chave [session];
  • linhas 62-68: decodificamos o cookie de sessão para autenticar o usuário;
  • linha 68: voltaremos mais tarde à função [decode_flask_session], que decodifica o cookie de sessão;
  • linhas 52 e 57: a cada solicitação do mesmo cliente, os cookies enviados pelo servidor são devolvidos a ele. É dessa forma que a sessão do Flask é mantida entre o cliente e o servidor;

É preciso lembrar agora que a camada [dao] será executada simultaneamente por várias threads. Portanto, é necessário examinar todas as propriedades da instância da classe e verificar se o acesso simultâneo a essas propriedades representa algum problema. Aqui, adicionamos a propriedade [self.__cookies], na linha 30. Essa propriedade é modificada na linha 59. Portanto, temos um acesso de gravação a um dado compartilhado por todas as threads. No entanto, esse acesso representa um problema: cada thread, que representa um determinado cliente, possui seu próprio cookie de sessão. De fato, nele há um número de cliente (=thread) único para cada cliente. Se nada for feito, a thread T2 pode sobrescrever os cookies da thread T1.

Já vimos um método para lidar com esse problema: é possível criar, no arquivo [config] passado como parâmetro ao construtor (linha 17), chaves diferentes para cada thread. Por exemplo, pode-se usar como chave o nome do thread:

  • na linha 59, poderíamos escrever:

config[thread_name][‘cookies’]=cookies
  • na linha 52, poderíamos então escrever:

cookies=config[thread_name][‘cookies’]

Vamos utilizar aqui uma técnica diferente: cada thread (=cliente) terá sua própria camada [dao]. Assim, a linha 59 não representa mais um problema, pois os cookies utilizados são, então, os de um único cliente.

Para isso, vamos criar uma nova classe [ImpôtsDaoWithHttpSessionFactory].

25.3.1.2. A função de decodificação da sessão do Flask

A função [decode_flask_session] está definida no script [myutils]:

Image

Já estudamos o script |myutils|. Esse script é um módulo de escopo de máquina que os diversos scripts deste curso podem importar com a instrução:

import myutils

Nele, define-se a função [decode_flask_session] da seguinte maneira:


def decode_flask_session(cookie: str) -> str:
    # fonte: https://www.kirsle.net/wizards/flask-session.cgi
    compressed = False
    payload = cookie

    if payload.startswith('.'):
        compressed = True
        payload = payload[1:]

    data = payload.split(".")[0]

    data = base64_decode(data)
    if compressed:
        data = zlib.decompress(data)

    return data.decode("utf-8")
  • linha 2: o URL, onde encontrei essa função;
  • linha 1: o parâmetro [cookie] é a sequência de caracteres associada à chave [session] no dicionário de cookies recebidos por um cliente web;
  • linhas 3-16: não vou comentar esse código, pois não o domino;

Adicionamos uma nova importação no arquivo [__init__.py]:


from .myutils import set_syspath, json_response, decode_flask_session

A nova versão do [myutils] é instalada entre os módulos de escopo de máquina com o comando [pip install .] em um terminal do PyCharm:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install .
Processing c:\data\st-2020\dev\python\cours-2020\python3-flask-2020\packages
Using legacy setup.py install for myutils, since package 'wheel' is not installed.
Installing collected packages: myutils
  Attempting uninstall: myutils
    Found existing installation: myutils 0.1
    Uninstalling myutils-0.1:
      Successfully uninstalled myutils-0.1
    Running setup.py install for myutils ... done
Successfully installed myutils-0.1
  • linha 1: é necessário estar na pasta [packages] para digitar essa instrução;

25.3.1.3. A classe [ImpôtsDaoWithHttpSessionFactory]

A classe [ImpôtsDaoWithHttpSessionFactory] é a seguinte:


from ImpôtsDaoWithHttpSession import ImpôtsDaoWithHttpSession


class ImpôtsDaoWithHttpSessionFactory:

    def __init__(self, config: dict):
        # o parâmetro é armazenado
        self.__config = config

    def new_instance(self):
        # retorna-se uma instância da camada [dao]
        return ImpôtsDaoWithHttpSession(self.__config)
  • a classe [ImpôtsDaoWithHttpSessionFactory] permite criar uma nova implementação da camada [dao] com o método [new_instance] das linhas 10 a 12;

25.3.2. A configuração

O script [config_layers], que configura as camadas do cliente web, é modificado da seguinte forma:


def configure(config: dict) -> dict:
    # instanciação das camadas do aplicativo

    # camada DAO
    from ImpôtsDaoWithHttpSessionFactory import ImpôtsDaoWithHttpSessionFactory
    dao_factory = ImpôtsDaoWithHttpSessionFactory(config)

    # configuramos as camadas
    return {
        "dao_factory": dao_factory
    }
  • linhas 5-6: em vez de instanciar uma única camada [dao], como era feito anteriormente, instanciamos uma “fábrica” dessa camada (fábrica = fábrica de produção de objetos, neste caso, a camada [dao]);
  • linhas 9-11: retorna-se a configuração das camadas;

25.3.3. O script principal do cliente

O script [main] sofreu as seguintes alterações em relação à versão anterior:


# configurando o aplicativo

import config
config = config.configure({})

# dependências
from ImpôtsError import ImpôtsError
import random
import sys
import threading
from Logger import Logger


# execução da camada [dao] em um thread
# “taxpayers” é uma lista de contribuintes
def thread_function(thread_dao, logger, taxpayers: list):
    


# lista de threads do cliente
threads = []
logger = None
# código
try:
    
    l_taxpayers = len(taxpayers)
    while i < len(taxpayers):
        
        # cada thread deve ter sua própria camada [dao] para gerenciar corretamente seu cookie de sessão
        thread_dao = dao_factory.new_instance()
        # cria-se o thread
        thread = threading.Thread(target=thread_function, args=(thread_dao, logger, thread_taxpayers))
        # adiciona-se à lista de threads do script principal
        threads.append(thread)
        # inicia-se o thread — essa operação é assíncrona — não se aguarda o resultado do thread
        thread.start()
    # o thread principal aguarda a conclusão de todos os threads que iniciou
    
except BaseException as erreur:
    # exibição do erro
    print(f"L'erreur suivante s'est produite : {erreur}")
finally:
    # fecha-se o registrador de log
    if logger:
        logger.close()
    # concluído
    print("Travail terminé...")
    # fim dos threads que ainda poderiam existir caso a execução tenha sido interrompida por um erro
    sys.exit()
  • linhas 29-30: cada thread possui sua própria camada [dao];

25.3.4. Execução do cliente

O servidor web é iniciado, o SGBD é iniciado, o servidor de e-mails [hMailServer] é iniciado. Em seguida, é iniciado o script [main] do cliente web. Os logs de execução no [data/logs/logs.txt] são, então, os seguintes:


2020-07-25 10:21:46.478511, Thread-1 : début du thread [Thread-1] avec 1 contribuable(s)
2020-07-25 10:21:46.479111, Thread-1 : début du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555}
2020-07-25 10:21:46.479111, Thread-2 : début du thread [Thread-2] avec 1 contribuable(s)
2020-07-25 10:21:46.480195, Thread-3 : début du thread [Thread-3] avec 2 contribuable(s)
2020-07-25 10:21:46.480195, Thread-2 : début du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000}
2020-07-25 10:21:46.481137, Thread-4 : début du thread [Thread-4] avec 3 contribuable(s)
2020-07-25 10:21:46.481137, Thread-3 : début du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000}
2020-07-25 10:21:46.482279, Thread-5 : début du thread [Thread-5] avec 3 contribuable(s)
2020-07-25 10:21:46.482622, Thread-6 : début du thread [Thread-6] avec 1 contribuable(s)
2020-07-25 10:21:46.482622, Thread-4 : début du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-5 : début du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000}
2020-07-25 10:21:46.485923, Thread-6 : début du calcul de l'impôt de {"id": 11, "marié": "oui", "enfants": 3, "salaire": 200000}
2020-07-25 10:21:46.725910, Thread-4 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"fa3c83b82761c83e13217967"}
2020-07-25 10:21:46.725910, Thread-4 : {"réponse": {"result": {"marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:46.725910, Thread-4 : fin du calcul de l'impôt de {"id": 5, "marié": "non", "enfants": 3, "salaire": 100000, "impôt": 16782, "surcôte": 7176, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:46.726960, Thread-4 : début du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000}
2020-07-25 10:21:47.514108, Thread-3 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,24999.5],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"700e3f5dc808c7c48f0c9007"}
2020-07-25 10:21:47.514610, Thread-3 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}}}
2020-07-25 10:21:47.514939, Thread-3 : fin du calcul de l'impôt de {"id": 3, "marié": "oui", "enfants": 3, "salaire": 50000, "impôt": 0, "surcôte": 0, "taux": 0.14, "décôte": 720, "réduction": 0}
2020-07-25 10:21:47.514939, Thread-3 : début du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000}
2020-07-25 10:21:47.527211, Thread-5 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"9e14a5d4a3057f69ab95ab2d"}
2020-07-25 10:21:47.527211, Thread-2 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,22500.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"a06e8fd70a44c9e311f4dce0"}
2020-07-25 10:21:47.527211, Thread-5 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.527211, Thread-1 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,90000.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"28c38df998f67685b3a482b8"}
2020-07-25 10:21:47.527211, Thread-2 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}}}
2020-07-25 10:21:47.528341, Thread-5 : fin du calcul de l'impôt de {"id": 8, "marié": "non", "enfants": 0, "salaire": 100000, "impôt": 22986, "surcôte": 0, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.528341, Thread-1 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.528842, Thread-2 : fin du calcul de l'impôt de {"id": 2, "marié": "oui", "enfants": 2, "salaire": 50000, "impôt": 1384, "surcôte": 0, "taux": 0.14, "décôte": 384, "réduction": 347}
2020-07-25 10:21:47.529349, Thread-5 : début du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000}
2020-07-25 10:21:47.529699, Thread-1 : fin du calcul de l'impôt de {"id": 1, "marié": "oui", "enfants": 2, "salaire": 55555, "impôt": 2814, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.529699, Thread-2 : fin du thread [Thread-2]
2020-07-25 10:21:47.531905, Thread-1 : fin du thread [Thread-1]
2020-07-25 10:21:47.536121, Thread-6 : cookie de session={"admindata":{"abattement_dixpourcent_max":12502.0,"abattement_dixpourcent_min":437.0,"coeffn":[0.0,1394.96,5798.0,13913.7,20163.4],"coeffr":[0.0,0.14,0.3,0.41,0.45],"id":1,"limites":[9964.0,27519.0,73779.0,156244.0,93749.0],"plafond_decote_celibataire":1196.0,"plafond_decote_couple":1970.0,"plafond_impot_celibataire_pour_decote":1595.0,"plafond_impot_couple_pour_decote":2627.0,"plafond_qf_demi_part":1551.0,"plafond_revenus_celibataire_pour_reduction":21037.0,"plafond_revenus_couple_pour_reduction":42074.0,"valeur_reduc_demi_part":3797.0},"client_id":"38499b63076516c02f2770ec"}
2020-07-25 10:21:47.537161, Thread-3 : {"réponse": {"result": {"marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.537161, Thread-6 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 200000, "impôt": 42842, "surcôte": 17283, "taux": 0.41, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.538156, Thread-3 : fin du calcul de l'impôt de {"id": 4, "marié": "non", "enfants": 2, "salaire": 100000, "impôt": 19884, "surcôte": 4480, "taux": 0.41, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.538557, Thread-6 : 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-25 10:21:47.538828, Thread-3 : fin du thread [Thread-3]
2020-07-25 10:21:47.538828, Thread-6 : fin du thread [Thread-6]
2020-07-25 10:21:47.546198, Thread-5 : {"réponse": {"result": {"marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.546198, Thread-5 : fin du calcul de l'impôt de {"id": 9, "marié": "oui", "enfants": 2, "salaire": 30000, "impôt": 0, "surcôte": 0, "taux": 0.0, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.546198, Thread-5 : début du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000}
2020-07-25 10:21:47.739643, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:47.739643, Thread-4 : fin du calcul de l'impôt de {"id": 6, "marié": "oui", "enfants": 3, "salaire": 100000, "impôt": 9200, "surcôte": 2180, "taux": 0.3, "décôte": 0, "réduction": 0}
2020-07-25 10:21:47.740668, Thread-4 : début du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000}
2020-07-25 10:21:48.557469, Thread-5 : {"réponse": {"result": {"marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:48.558715, Thread-5 : fin du calcul de l'impôt de {"id": 10, "marié": "non", "enfants": 0, "salaire": 200000, "impôt": 64210, "surcôte": 7498, "taux": 0.45, "décôte": 0, "réduction": 0}
2020-07-25 10:21:48.558715, Thread-5 : fin du thread [Thread-5]
2020-07-25 10:21:48.753025, Thread-4 : {"réponse": {"result": {"marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}}}
2020-07-25 10:21:48.753318, Thread-4 : fin du calcul de l'impôt de {"id": 7, "marié": "oui", "enfants": 5, "salaire": 100000, "impôt": 4230, "surcôte": 0, "taux": 0.14, "décôte": 0, "réduction": 0}
2020-07-25 10:21:48.753540, Thread-4 : fin du thread [Thread-4]
  • temos, no total, 6 threads, portanto 6 clientes (linhas 1, 3, 4, 6, 8, 9) que consultam simultaneamente o servidor de cálculo de impostos;
  • vamos acompanhar o thread [Thread-4], que gerencia 3 contribuintes (linha 6). Ele fará, sequencialmente, três solicitações ao servidor de cálculo de impostos;
  • linha 10: a primeira solicitação de [Thread-4];
  • linha 13: [Thread-4] recebeu a resposta à sua primeira solicitação. Nela, encontra um cookie de sessão contendo o número [fa3c83b82761c83e13217967], atribuído pelo servidor;
  • linha 14: o imposto do primeiro contribuinte;
  • linha 16: [Thread-4] faz uma solicitação para o segundo contribuinte;
  • linha 43: [Thread-4] recebe o imposto do segundo contribuinte;
  • linha 45: [Thread-4] faz uma consulta para o terceiro contribuinte;
  • linha 49: [Thread-4] recebe o imposto do terceiro contribuinte;
  • linha 51: [Thread-4] concluiu seu trabalho;

Agora, vamos ver como as três solicitações de [Thread-4] foram processadas no servidor. Poderemos acompanhá-las nos logs do servidor graças ao seu número de cliente [fa3c83b82761c83e13217967].

Os logs de [data/logs/logs.txt] no servidor são os seguintes:


2020-07-25 10:21:39.187366, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:40.439093, MainThread : [serveur] démarrage du serveur
2020-07-25 10:21:46.502011, Thread-2 : [index] requête : <Request 'http://127.0.0.1:5000/?casado=sim&filhos=3&salário=50000' [GET]>
2020-07-25 10:21:46.504049, Thread-2 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.505452, Thread-3 : [index] requête : <Request 'http://127.0.0.1:5000/?casado=sim&filhos=2&salário=55555' [GET]>
2020-07-25 10:21:46.506257, Thread-3 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.507292, Thread-4 : [index] requête : <Request 'http://127.0.0.1:5000/?casado=não&filhos=0&salário=100000' [GET]>
2020-07-25 10:21:46.507292, Thread-4 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.508301, Thread-5 : [index] requête : <Request 'http://127.0.0.1:5000/?casado=sim&filhos=2&salário=50000' [GET]>
2020-07-25 10:21:46.509293, Thread-5 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.511808, Thread-6 : [index] requête : <Request 'http://127.0.0.1:5000/?casado=não&filhos=3&salário=100000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] requête : <Request 'http://127.0.0.1:5000/?casado=sim&filhos=3&salário=200000' [GET]>
2020-07-25 10:21:46.517604, Thread-7 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:46.719504, Thread-6 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises dans la couche dao
2020-07-25 10:21:46.720003, Thread-6 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 3, 'salaire': 100000, 'impôt': 16782, 'surcôte': 7176, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:46.736108, Thread-8 : [index] requête : <Request 'http://127.0.0.1:5000/?casado=sim&filhos=3&salário=100000' [GET]>
2020-07-25 10:21:46.736108, Thread-8 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.506709, Thread-2 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises dans la couche dao
2020-07-25 10:21:47.507216, Thread-2 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 50000, 'impôt': 0, 'surcôte': 0, 'taux': 0.14, 'décôte': 720, 'réduction': 0}}}
2020-07-25 10:21:47.507216, Thread-3 : [index_controller] client [28c38df998f67685b3a482b8], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508442, Thread-4 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises dans la couche dao
2020-07-25 10:21:47.508940, Thread-3 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 55555, 'impôt': 2814, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.510506, Thread-4 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0, 'salaire': 100000, 'impôt': 22986, 'surcôte': 0, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.511513, Thread-5 : [index_controller] client [a06e8fd70a44c9e311f4dce0], données fiscales prises dans la couche dao
2020-07-25 10:21:47.514939, Thread-5 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 50000, 'impôt': 1384, 'surcôte': 0, 'taux': 0.14, 'décôte': 384, 'réduction': 347}}}
2020-07-25 10:21:47.520727, Thread-7 : [index_controller] client [38499b63076516c02f2770ec], données fiscales prises dans la couche dao
2020-07-25 10:21:47.523162, Thread-7 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 200000, 'impôt': 42842, 'surcôte': 17283, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.530835, Thread-9 : [index] requête : <Request 'http://127.0.0.1:5000/?casado=não&filhos=2&salário=100000' [GET]>
2020-07-25 10:21:47.531736, Thread-9 : [index_controller] client [700e3f5dc808c7c48f0c9007], données fiscales prises en session
2020-07-25 10:21:47.531905, Thread-9 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 2, 'salaire': 100000, 'impôt': 19884, 'surcôte': 4480, 'taux': 0.41, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.541899, Thread-10 : [index] requête : <Request 'http://127.0.0.1:5000/?casado=sim&filhos=2&salário=30000' [GET]>
2020-07-25 10:21:47.542488, Thread-10 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:47.542488, Thread-10 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 2, 'salaire': 30000, 'impôt': 0, 'surcôte': 0, 'taux': 0.0, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.553628, Thread-11 : [index] requête : <Request 'http://127.0.0.1:5000/?casado=não&filhos=0&salário=200000' [GET]>
2020-07-25 10:21:47.553628, Thread-11 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:47.736910, Thread-8 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:47.737191, Thread-8 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 3, 'salaire': 100000, 'impôt': 9200, 'surcôte': 2180, 'taux': 0.3, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:47.748226, Thread-12 : [index] requête : <Request 'http://127.0.0.1:5000/?casado=sim&filhos=5&salário=100000' [GET]>
2020-07-25 10:21:47.748226, Thread-12 : [index] mis en pause du thread pendant 1 seconde(s)
2020-07-25 10:21:48.554695, Thread-11 : [index_controller] client [9e14a5d4a3057f69ab95ab2d], données fiscales prises en session
2020-07-25 10:21:48.555070, Thread-11 : [index] {'réponse': {'result': {'marié': 'non', 'enfants': 0, 'salaire': 200000, 'impôt': 64210, 'surcôte': 7498, 'taux': 0.45, 'décôte': 0, 'réduction': 0}}}
2020-07-25 10:21:48.748753, Thread-12 : [index_controller] client [fa3c83b82761c83e13217967], données fiscales prises en session
2020-07-25 10:21:48.748753, Thread-12 : [index] {'réponse': {'result': {'marié': 'oui', 'enfants': 5, 'salaire': 100000, 'impôt': 4230, 'surcôte': 0, 'taux': 0.14, 'décôte': 0, 'réduction': 0}}}
  • o cliente [fa3c83b82761c83e13217967] aparece pela primeira vez na linha 14: para calcular o imposto, o servidor precisou buscar na base de dados os dados da administração fiscal;
  • em seguida, encontramos o cliente [fa3c83b82761c83e13217967] na linha 36. Desta vez, o servidor encontra os dados da administração fiscal na sessão, o que evita um acesso, possivelmente oneroso, à camada [dao];
  • encontramos o cliente [fa3c83b82761c83e13217967] pela terceira vez na linha 42, onde, mais uma vez, o servidor utiliza a sessão do cliente;

Este exemplo ilustra bem a utilidade da sessão para um cliente: nela são armazenados dados compartilhados por todas as solicitações desse cliente e cuja aquisição é onerosa.

Do lado do cliente, os resultados no arquivo [data/output/résultats.json] são os mesmos das versões anteriores.

25.4. Testes da camada [dao]

Assim como fizemos nas |versões anteriores|, testamos a camada [dao] do cliente:

Image

A classe de teste será executada no seguinte ambiente:

Image

  • a configuração [2] é idêntica à configuração [1] que acabamos de analisar;

A classe de teste [TestHttpClientDao] é a seguinte:


import unittest

from Logger import Logger


class TestHttpClientDao(unittest.TestCase):

    def test_1(self) -> None:
        from TaxPayer import TaxPayer

        # {'casado': 'sim', 'filhos': 2, 'salário': 55555,
        # 'imposto': 2814, 'sobretaxa': 0, 'abatimento': 0, 'redução': 0, 'alíquota': 0,14}
        taxpayer = TaxPayer().fromdict({"marié": "oui", "enfants": 2, "salaire": 55555})
        dao.calculate_tax(taxpayer)
        # verificação
        self.assertAlmostEqual(taxpayer.impôt, 2815, delta=1)
        self.assertEqual(taxpayer.décôte, 0)
        self.assertEqual(taxpayer.réduction, 0)
        self.assertAlmostEqual(taxpayer.taux, 0.14, delta=0.01)
        self.assertEqual(taxpayer.surcôte, 0)



if __name__ == '__main__':
    # configuramos o aplicativo
    import config
    config = config.configure({})

    # registrador
    logger = Logger(config["logsFilename"])
    # armazenando na configuração
    config["logger"] = logger
    # recuperando a fábrica da camada [dao]
    dao_factory = config["layers"]["dao_factory"]
    # cria-se uma instância da camada [dao]
    dao = dao_factory.new_instance()

    # executa-se os métodos de teste
    print("tests en cours...")
    unittest.main()
  • criamos uma |configuração de execução| para este teste;
  • iniciamos o servidor web com todo o seu ambiente;
  • executa-se o teste;

Os resultados são os seguintes:


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/03/tests/TestHttpClientDao.py
tests en cours...
...........
----------------------------------------------------------------------
Ran 11 tests in 3.392s

OK

Process finished with exit code 0