Skip to content

30. Exercício prático: Versão 12

Neste capítulo, iremos escrever uma aplicação web seguindo a arquitetura MVC (Model-View-Controller). A aplicação será capaz de devolver respostas em três formatos: JSON, XML e HTML. Existe um aumento significativo na complexidade entre o que estamos prestes a fazer e o que fizemos anteriormente. Iremos reutilizar a maioria dos conceitos abordados até agora e detalharemos todos os passos que conduzem à aplicação final.

30.1. Arquitetura MVC

Iremos implementar o padrão arquitetónico MVC (Model–View–Controller) da seguinte forma:

Image

O processamento de um pedido do cliente decorrerá da seguinte forma:

  • 1 - Pedido

Os URLs solicitados terão o formato http://machine:port/action/param1/param2/… O [Controlador Principal] utilizará um ficheiro de configuração para «encaminhar» a solicitação para o controlador correto. Para tal, utilizará o campo [ação] do URL. O restante do URL [param1/param2/…] consiste em parâmetros opcionais que serão passados para a ação. O «C» em MVC refere-se aqui à cadeia [Controlador Principal, Controlador / Ação]. Se nenhum controlador puder lidar com a ação solicitada, o servidor web responderá que a URL solicitada não foi encontrada.

  • 2 - Processamento
  • A ação selecionada [2a] pode utilizar os parâmetros que o [Controlador Principal] lhe passou. Estes podem provir de duas fontes:
      • o caminho [/param1/param2/…] da URL,
      • dos parâmetros enviados no corpo do pedido do cliente;
    • Ao processar a solicitação do utilizador, a ação pode requerer a camada [de negócios] [2b]. Uma vez processada a solicitação do cliente, ela pode desencadear várias respostas. Um exemplo clássico é:
      • uma resposta de erro, se a solicitação não puder ser processada corretamente;
      • uma resposta de confirmação, caso contrário;
    • o [Controlador / Ação] devolverá a sua resposta [2c] ao controlador principal juntamente com um código de estado. Estes códigos de estado representarão de forma única o estado atual da aplicação. Será um código de sucesso ou um código de erro;
  • 3 - Resposta
    • Dependendo de o cliente ter solicitado uma resposta JSON, XML ou HTML, o [Controlador Principal] instanciará [3a] o tipo de resposta apropriado e instruirá este a enviar a resposta ao cliente. O [Controlador Principal] passará tanto a resposta como o código de estado fornecido pelo [Controlador/Ação] que foi executado;
    • se a resposta pretendida for do tipo JSON ou XML, a resposta selecionada formatará a resposta do [Controlador/Ação] que lhe foi fornecida e enviá-la-á [3c]. O cliente capaz de processar esta resposta pode ser um script de consola Python ou um script JavaScript incorporado numa página HTML;
    • se a resposta desejada for do tipo HTML, a resposta selecionada irá selecionar [3b] uma das vistas HTML [Vuei] utilizando o código de estado que lhe foi fornecido. Este é o V em MVC. Uma única vista corresponde a um único código de estado. Esta vista V irá apresentar a resposta do [Controlador / Ação] que foi executada. Ela envolve os dados desta resposta em HTML, CSS e JavaScript. Estes dados são chamados de modelo de vista. Este é o M em MVC. O cliente é, na maioria das vezes, um navegador;

Agora, vamos esclarecer a relação entre a arquitetura web MVC e a arquitetura em camadas. Dependendo de como o modelo é definido, estes dois conceitos podem ou não estar relacionados. Consideremos uma aplicação web MVC de camada única:

Image

No exemplo acima, o [Controlador / Ação] incorpora partes das camadas [negócio] e [DAO]. Na camada [web], temos uma arquitetura MVC, mas a aplicação como um todo não possui uma arquitetura em camadas. Aqui, existe apenas uma camada — a camada web — que lida com tudo.

Agora, vamos considerar uma arquitetura web multicamadas:

Image

A camada [Web] pode ser implementada sem seguir o modelo MVC. Temos então uma arquitetura multicamadas, mas a camada Web não implementa o modelo MVC.

Por exemplo, no mundo .NET, a camada [Web] acima pode ser implementada com ASP.NET MVC, resultando numa arquitetura em camadas com uma camada [Web] no estilo MVC. Feito isso, podemos substituir essa camada ASP.NET MVC por uma camada ASP.NET clássica (WebForms), mantendo o resto (lógica de negócio, DAO, driver) inalterado. Temos então uma arquitetura em camadas com uma camada [web] que já não é baseada em MVC.

No MVC, dissemos que o modelo M era o da vista V, ou seja, o conjunto de dados exibidos pela vista V. É dada outra definição do modelo M no MVC:

Image

Muitos autores consideram que o que se encontra à direita da camada [web] constitui o modelo M do MVC. Para evitar ambiguidades, podemos referir-nos a:

  • o modelo de domínio quando nos referimos a tudo à direita da camada [web];
  • o modelo de vista quando nos referimos aos dados apresentados por uma vista V;

No que se segue, quando nos referirmos ao modelo, estaremos sempre a referir-nos ao modelo de visualização.

30.2. Arquitetura de Aplicações Cliente/Servidor

A aplicação web terá a seguinte arquitetura:

Image

  • Em [1], o servidor web terá dois tipos de clientes:
    • em [2], um cliente de consola que irá trocar JSON e XML com o servidor;
    • em [3], um navegador que receberá HTML do servidor e o exibirá;
  • O servidor web [1] mantém as camadas [business] e [DAO] das versões anteriores;
  • o cliente web [2] será atualizado para ter em conta os novos URLs de serviço da aplicação web;
  • A aplicação HTML apresentada pelo navegador deve ser escrita de raiz;

Iremos desenvolver a aplicação em várias fases:

  • Iremos desenvolver a versão JSON do servidor. Iremos testar as URLs de serviço do servidor uma a uma utilizando um cliente Postman. Este método permite-nos construir a estrutura do servidor web sem nos preocuparmos com as vistas da aplicação (=HTML);
  • Após testar o servidor JSON com o Postman, iremos testá-lo com um cliente de consola;
  • depois, passaremos à versão XML do servidor. Vimos que a transição de JSON para XML é simples;
  • por fim, passaremos para a versão HTML do servidor. Iremos construir uma arquitetura MVC e definir as vistas a serem apresentadas. A aplicação HTML será testada utilizando tanto o cliente Postman como um navegador padrão;

30.3. A estrutura de diretórios do código do servidor

Image

  • em [1: o servidor web como um todo;
  • em [2]: por enquanto, ignoraremos as pastas [static, templates, tests_views], que pertencem à versão HTML do servidor. Fora desta pasta, encontraremos o script principal [main] e a sua configuração;
  • em [3], os controladores do servidor web. Estes serão instâncias de classe;
 
  • em [4], a resposta HTTP do servidor será tratada por classes;
  • em [5], mantemos o ficheiro de registo dos servidores anteriores;

Quando criamos a versão HTML do servidor, outras pastas entram em ação:

 
  • em [6], os elementos estáticos da aplicação HTML;
  • em [7], os modelos da aplicação HTML divididos em vistas [9] e fragmentos de vista [8];
  • em [9], as classes que implementam os modelos de vista;

30.4. Os URLs de serviço da aplicação

Para construir o servidor web, procederemos da seguinte forma:

  • Com base nas vistas da aplicação HTML, definiremos as ações que a aplicação web deve implementar. Utilizaremos aqui as vistas reais, mas estas poderiam ser simplesmente vistas no papel;
  • Com base nessas ações, definiremos as URLs de serviço da aplicação HTML;
  • vamos implementar estas URLs de serviço utilizando um servidor que devolve JSON. Isto permite-nos definir a estrutura do servidor web sem nos preocuparmos com as páginas HTML a serem servidas. Vamos testar estas URLs de serviço utilizando o Postman;
  • Em seguida, testaremos o nosso servidor JSON com um cliente de consola;
  • Assim que o servidor JSON tiver sido validado, passaremos à escrita da aplicação HTML;

A primeira vista será a vista de autenticação:

Image

  • a ação que conduz a esta primeira vista será denominada [init-session] [1];
  • Clicar no botão [Validate] irá acionar a ação [authenticate-user] com dois parâmetros enviados [2-3];

A vista de cálculo de impostos:

Image

  • Em [1], a ação [authenticate-user] que conduziu a esta vista;
  • em [2], clicar no botão [Validate] aciona a execução da ação [calculate-tax] com três parâmetros enviados [2-5];
  • Clicar na ligação [6] aciona a ação [list-simulations] sem parâmetros;
  • Clicar no link [7] aciona a ação [end-session] sem parâmetros;

A terceira vista apresenta as simulações realizadas pelo utilizador autenticado:

Image

  • em [3], a ação [list-simulations] que conduziu a esta visualização;
  • em [2], clicar na ligação [Eliminar] aciona a ação [delete-simulation] com um parâmetro: o número da simulação a ser eliminada da lista;
  • clicar no link [3] aciona a ação [display-tax-calculation] sem parâmetros, o que volta a exibir a vista de cálculo de impostos;
  • Clicar no link [4] aciona a ação [end-session] sem parâmetros;

Com esta informação inicial, podemos definir os vários URLs de serviço do servidor:

Ação
Função
Contexto de execução
/init-session
Utilizado para definir o tipo (json, xml, html) das respostas pretendidas
Pedido GET
Pode ser enviada a qualquer momento
/authenticate-user
Autoriza ou recusa o login de um utilizador
Pedido POST.
A solicitação deve conter dois parâmetros enviados [user, password]
Só pode ser emitida se o tipo de sessão (json, xml, html) for conhecido
/calculate-tax
Realiza uma simulação de cálculo de impostos
Pedido POST.
A solicitação deve conter três parâmetros enviados [casado, filhos, salário]
Só pode ser emitida se o tipo de sessão (json, xml, html) for conhecido e o utilizador estiver autenticado
/list-simulations
Pedido para visualizar a lista de simulações realizadas desde o início da sessão
Pedido GET.
Só pode ser emitida se o tipo de sessão (json, xml, html) for conhecido e o utilizador estiver autenticado
/delete-simulation/number
Elimina uma simulação da lista de simulações
Pedido GET.
Só pode ser emitida se o tipo de sessão (json, xml, html) for conhecido e o utilizador estiver autenticado
/display-tax-calculation
Exibe a página HTML para o cálculo de impostos
Pedido GET.
Só pode ser emitida se o tipo de sessão (json, xml, html) for conhecido e o utilizador estiver autenticado
/end-session
Encerra a sessão de simulação.
Tecnicamente, a sessão web antiga é eliminada e é criada uma nova sessão
Só pode ser emitida se o tipo de sessão (json, xml, html) for conhecido e o utilizador estiver autenticado

Estas várias URLs de serviço serão utilizadas tanto para o servidor HTML como para os servidores JSON ou XML. Duas URLs serão utilizadas exclusivamente para os dois últimos servidores: estas são as URLs da versão anterior do cliente/servidor web que estamos a reutilizar aqui:

Ação
Função
Contexto de execução
/get-admindata
Retorna os dados fiscais utilizados para calcular o imposto
.
Utilizado apenas se o tipo de sessão for json ou xml. O utilizador deve estar autenticado
/calculate-taxes
Calcula o imposto para uma lista de contribuintes enviada na solicitação GET em JSON
.
Utilizado apenas se o tipo de sessão for json ou xml. O utilizador deve estar autenticado

Todos os controladores associados a estas ações procederão da mesma forma:

  • eles verificarão os seus parâmetros. Estes encontram-se no objeto:
    • [request.path] para parâmetros presentes na URL na forma [/action/param1/param2/…];
    • no objeto [request.form] para os que são transmitidos como [x-www-form-urlencoded] no corpo da solicitação;
    • no objeto [request.data] para os dados transmitidos como JSON no corpo da solicitação;
  • Um controlador é semelhante a uma função ou método que verifica a validade dos seus parâmetros. No caso do controlador, porém, é um pouco mais complicado:
    • os parâmetros esperados podem estar em falta;
    • Os parâmetros recuperados pelo controlador são cadeias de caracteres. Se o parâmetro esperado for um número, então o controlador deve verificar se a cadeia de caracteres do parâmetro representa efetivamente um número;
    • Uma vez verificado que os parâmetros esperados estão presentes e são sintaticamente corretos, deve verificar-se se são válidos no contexto de execução atual. Este contexto está presente na sessão. O exemplo de autenticação é um exemplo de um contexto de execução. Certas ações só devem ser processadas depois de o cliente ter sido autenticado. Geralmente, uma chave na sessão indica se esta autenticação ocorreu ou não;
    • assim que as verificações anteriores tenham sido concluídas, o controlador secundário pode prosseguir. Este processo de verificação de parâmetros é muito importante. Não podemos aceitar que um cliente nos envie dados arbitrários em qualquer momento durante o ciclo de vida da aplicação. Temos de manter controlo total sobre o ciclo de vida da aplicação;
    • Assim que o seu trabalho estiver concluído, o controlador secundário devolve um dicionário com as chaves [action, state, response] ao controlador principal que o chamou:
      • [action] é a ação que acabou de ser executada;
      • [state] é um número de três dígitos que indica o resultado do processamento da ação:
    • [x00] indica processamento bem-sucedido;
    • [x01] indica uma falha no processamento;
  • [response] é o dicionário de resultados na forma {‘response’:object}. O objeto terá estruturas diferentes dependendo da ação que está a ser processada;

Vamos agora rever os vários controladores — ou, por outras palavras, as diferentes ações que estes controladores tratam — que conduzem o fluxo de trabalho da aplicação web.

30.5. Configuração do servidor

Image

A configuração da base de dados [config_database] e a configuração da camada do servidor [config_layers] são idênticas às das versões anteriores. O ficheiro [config] inclui agora novas informações:

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"

    #  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",
        #  Logger, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
        #  scripts [config_database, config_layers]
        script_dir,
        #  controllers
        f"{script_dir}/../controllers",
        #  answers HTTP
        f"{script_dir}/../responses",
        #  view models
        f"{script_dir}/../models_for_views",
    ]

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

    #  web server dependencies

    #  controllers
    from AfficherCalculImpotController import AfficherCalculImpotController
    from AuthentifierUtilisateurController import AuthentifierUtilisateurController
    from CalculerImpotController import CalculerImpotController
    from CalculerImpotsController import CalculerImpotsController
    from FinSessionController import FinSessionController
    from GetAdminDataController import GetAdminDataController
    from InitSessionController import InitSessionController
    from ListerSimulationsController import ListerSimulationsController
    from MainController import MainController
    from SupprimerSimulationController import SupprimerSimulationController

    #  answers HTTP
    from HtmlResponse import HtmlResponse
    from JsonResponse import JsonResponse
    from XmlResponse import XmlResponse

    #  view models
    from ModelForAuthentificationView import ModelForAuthentificationView
    from ModelForCalculImpotView import ModelForCalculImpotView
    from ModelForErreursView import ModelForErreursView
    from ModelForListeSimulationsView import ModelForListeSimulationsView

    #  step 2 ------
    #  application configuration
    config.update({
        #  users authorized to use the application
        "users": [
            {
                "login": "admin",
                "password": "admin"
            }
        ],

        #  log file
        "logsFilename": f"{script_dir}/../data/logs/logs.txt",

        #  server config SMTP
        "adminMail": {
            #  server SMTP
            "smtp-server": "localhost",
            #  server port SMTP
            "smtp-port": "25",
            #  director
            "from": "guest@localhost.com",
            "to": "guest@localhost.com",
            #  mail subject
            "subject": "plantage du serveur de calcul d'impôts",
            #  tls to True if server SMTP requires authorization, False otherwise
            "tls": False
        },

        #  thread pause time in seconds
        "sleep_time": 0,

        #  authorized shares and their auditors
        "controllers": {
            #  initialization of a calculation session
            "init-session": InitSessionController(),
            #  user authentication
            "authentifier-utilisateur": AuthentifierUtilisateurController(),
            #  tax calculation in individual mode
            "calculer-impot": CalculerImpotController(),
            #  batch mode tax calculation
            "calculer-impots": CalculerImpotsController(),
            #  list of simulations
            "lister-simulations": ListerSimulationsController(),
            #  deleting a simulation
            "supprimer-simulation": SupprimerSimulationController(),
            #  end of calculation session
            "fin-session": FinSessionController(),
            #  display tax calculation view
            "afficher-calcul-impot": AfficherCalculImpotController(),
            #  obtaining data from tax authorities
            "get-admindata": GetAdminDataController(),
            #  main controller
            "main-controller": MainController()
        },

        #  different response types (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

        #  HTML views and their models depend on the state rendered by the controller
        "views": [
            {
                #  authentication view
                "états": [
                    #  /init-session success
                    700,
                    #  /authentifier-user failure
                    201
                ],
                "view_name": "views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                #  tax calculation
                "états": [
                    #  /authentifier-user success
                    200,
                    #  /calculate-tax-success
                    300,
                    #  /calculate-tax failure
                    301,
                    #  /show-tax-calculation
                    800
                ],
                "view_name": "views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                #  view of simulation list
                "états": [
                    #  /lister-simulations
                    500,
                    #  /suppress-simulation
                    600
                ],
                "view_name": "views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],

        #  view of unexpected errors
        "view-erreurs": {
            "view_name": "views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },

        #  redirections
        "redirections": [
            {
                "états": [
                    400,  #  /end-session successful
                ],
                #  redirection to
                "to": "/init-session/html",
            }
        ],
    }
    )

    #  step 3 ------
    #  database configuration
    import config_database
    config["database"] = config_database.configure(config)

    #  step 4 ------
    #  instantiation of application layers
    import config_layers
    config['layers'] = config_layers.configure(config)

    #  we return the configuration
    return config
  • Até à linha 41, vemos elementos padrão;
  • linhas 43–66: na linha 43, o Python Path do servidor é definido. Podemos então importar as dependências do projeto:
    • linhas 45–55: a lista de controladores;
    • linhas 57–60: a lista de respostas HTTP;
    • linhas 62–66: a lista de modelos de visualização;
  • linhas 68–189: a configuração da aplicação com uma série de constantes;
    • linhas 71–98: já estamos familiarizados com estas linhas das versões anteriores;
    • linhas 101–122: o dicionário de controladores:
      • as chaves são os nomes das ações;
      • os valores são uma instância do controlador responsável por lidar com essa ação. Cada controlador é instanciado como uma única instância (singleton). A mesma instância será executada por diferentes threads do servidor. Portanto, é necessário ter cuidado com os dados partilhados que cada controlador possa querer modificar;
    • linhas 125–129: o dicionário das três respostas HTTP possíveis:
      • as chaves são o tipo de resposta solicitada pelo cliente (JSON, XML, HTML);
      • os valores são uma instância da resposta HTTP. Cada gerador de resposta é instanciado como uma única instância (singleton). O mesmo gerador será executado por diferentes threads do servidor. Por isso, é necessário ter cuidado com os dados partilhados que cada gerador possa querer modificar;
    • linhas 132–186: configuração das visualizações HTML. Por enquanto, vamos ignorar estas linhas;
  • linhas 191–202: já nos deparámos com estas linhas em versões anteriores;

30.6. O caminho de um pedido do cliente dentro do servidor

Image

Iremos acompanhar o percurso de um pedido do cliente que chega ao servidor até à resposta HTTP enviada de volta. Segue o fluxo do servidor MVC.

30.6.1. O script [main]

O script [main] é idêntico em muitos aspetos ao das versões anteriores. No entanto, apresentamo-lo na íntegra para garantir que começamos com o pé direito:

#  a mysql or pgres parameter is expected
import sys

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

#  configure the application
import config
config = config.configure({'sgbd': sgbd})

#  dependencies
from flask import request, Flask, session, url_for, redirect
from flask_api import status
from SendAdminMail import SendAdminMail
from myutils import json_response
from Logger import Logger
import threading
import time
from random import randint
from ImpôtsError import ImpôtsError
import os

#  send an e-mail to the administrator
def send_adminmail(config: dict, message: str):
    #  send an e-mail to the application administrator
    config_mail = config["adminMail"]
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)

#  check log file
logger = None
erreur = False
message_erreur = None
try:
    #  logger
    logger = Logger(config["logsFilename"])
except BaseException as exception:
    #  log console
    print(f"L'erreur suivante s'est produite : {exception}")
    #  we note the error
    erreur = True
    message_erreur = f"{exception}"
#  store the logger in the config
config['logger'] = logger
#  error handling
if erreur:
    #  mail to administrator
    send_adminmail(config, message_erreur)
    #  end of application
    sys.exit(1)

#  start-up log
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)

#  data recovery from tax authorities
erreur = False
try:
    #  admindata will be read-only application data
    config["admindata"] = config["layers"]["dao"].get_admindata().asdict()
    #  success log
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    #  we note the error
    erreur = True
    #  error log
    log = f"L'erreur suivante s'est produite : {ex}"
    #  console
    print(log)
    #  log file
    logger.write(f"{log}\n")
    #  mail to administrator
    send_adminmail(config, log)

#  the main thread no longer needs the logger
logger.close()

#  if there has been an error, we stop
if erreur:
    sys.exit(2)

#  flask application
app = Flask(__name__, template_folder="templates", static_folder="static")
#  session secret key
app.secret_key = os.urandom(12).hex()

#  the front controller
def front_controller() -> tuple:
    #  the request is processed
    logger = None
    

@app.route('/', methods=['GET'])
def index() -> tuple:
    #  redirect to /init-session/html
    return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)

#  init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  authenticate-user
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  calculate-tax
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  lister-simulations
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  delete-simulation
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  end of session
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  display-calculation-tax
@app.route('/afficher-calcul-impot', methods=['GET'])
def afficher_calcul_impot() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  get-admindata
@app.route('/get-admindata/<int:numero>', methods=['GET'])
def get_admindata() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

#  hand only
if __name__ == '__main__':
    #  start the server
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • linhas 1–92: todas estas linhas já foram abordadas e explicadas;
  • linha 92: o servidor irá gerir uma sessão. Por isso, precisamos de uma chave secreta. Para cada utilizador, iremos armazenar duas informações na sessão:
    • se o utilizador se autenticou com sucesso;
    • Sempre que realizar um cálculo de impostos, os resultados desse cálculo serão colocados numa lista chamada lista de simulação do utilizador. Esta lista será armazenada na sessão;
  • Linhas 100–151: a lista de URLs de serviço do servidor. As funções associadas atuam como um filtro: quaisquer URLs que não estejam presentes nesta lista serão rejeitadas pelo servidor Flask com um erro [404 NOT FOUND]. Assim que esta filtragem estiver concluída, o pedido é sistematicamente encaminhado para um «Front Controller» implementado pela função [front_controller] nas linhas 94–98, que iremos discutir em breve;
  • Linhas 100–103: tratamento da rota [/]. O ponto de entrada para a aplicação web será a URL na linha 107. Portanto, na linha 103, redirecionamos o cliente para esta URL:
  • A função [url_for] é importada na linha 18. Aqui, ela tem dois parâmetros:
      • o primeiro parâmetro é o nome de uma das funções de roteamento, neste caso a da linha 107. Podemos ver que esta função espera um parâmetro [type_response], que é o tipo de resposta (json, xml, html) solicitado pelo cliente;
      • o segundo parâmetro recebe o nome do parâmetro da linha 107, [type_response], e atribui-lhe um valor. Se houvesse outros parâmetros, repetiríamos a operação para cada um deles;
      • ela retorna a URL associada à função designada pelos dois parâmetros que lhe foram fornecidos. Aqui, isto irá retornar a URL da linha 106, onde o parâmetro é substituído pelo seu valor [/init-session/html];
    • A função [redirect] foi importada na linha 18. A sua função é enviar um cabeçalho de redirecionamento HTTP ao cliente:
      • o primeiro parâmetro é a URL para a qual o cliente deve ser redirecionado;
      • o segundo parâmetro é o código de estado da resposta HTTP enviada ao cliente. O código [status.HTTP_302_FOUND] corresponde a um redirecionamento HTTP;

A função [ front_controller] nas linhas 94–98 realiza o processamento inicial do pedido do cliente:

#  the front controller
def front_controller() -> tuple:
    #  we process the request
    logger = None
    try:
        #  logger
        logger = Logger(config["logsFilename"])
        #  we store it in a config associated with the thread
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        #  log the request
        logger.write(f"[ front_controller] requête : {request}\n")
        #  the thread is interrupted if requested
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            #  pause is randomized so that some threads are interrupted and others not
            aléa = randint(0, 1)
            if aléa == 1:
                #  log before break
                logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                #  break
                time.sleep(sleep_time)
        #  forward the request to the main controller
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        #  we log the result sent to the customer
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        #  was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            #  send an e-mail to the application administrator
            send_adminmail(config, log)
        #  determine the desired type of response
        if session.get('typeResponse') is None:
            #  the session type has not yet been set - it will be jSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        #  build the response to be sent
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        #  we send the answer
        return response, status_code
    except BaseException as erreur:
        #  it's an unexpected error - log the error if possible
        if logger:
            logger.write(f"[ front_controller] {erreur}")
        #  we prepare the response to the customer
        résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
        #  we send a response in jSON
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        #  close the log file if it has been opened
        if logger:
            logger.close()
  • Linhas 1–57: Estamos familiarizados com este código. Por exemplo, este era o código da função denominada [main] no script [main] da versão anterior. Uma coisa a notar é o controlador utilizado nas linhas 25–26:
  • linha 25: recuperamos a instância do controlador associada ao nome [main-controller] da configuração. Estas são as seguintes linhas:
    #  web server dependencies
    #  controllers
    
    from MainController import MainController

    #  authorized shares and their controllers
        "controllers": {
            ,
            #  main controller
            "main-controller": MainController()
        },
  • (continuação)
    • na linha 10 acima, repare que estamos a recuperar uma instância da classe;
  • linha 26: solicitamos ao controlador [MainController] que processe a solicitação;
  • linhas 30–45: a resposta devolvida pelo [MainController] é enviada ao cliente. Voltaremos a estas linhas um pouco mais tarde;

A função da função [front_controller] e, em seguida, da classe [MainController] é tratar das tarefas comuns a todos os pedidos:

No diagrama acima, ainda estamos na fase 1 do processamento da solicitação. O controlador principal [MainController] continuará com a etapa 1.

Image

30.6.2. O controlador principal [MainController]

O controlador principal [MainController] dá continuidade ao trabalho iniciado pela função [front_controller]:

Todos os controladores implementam a seguinte interface [InterfaceController] [2]:

Image


from abc import ABC, abstractmethod
 
from werkzeug.local import LocalProxy
 
class InterfaceController(ABC):
 
    @abstractmethod
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        pass
  • A interface [InterfaceController] define apenas o método único [execute] na linha 8. Este método recebe três parâmetros:
    • [request]: o pedido do cliente;
    • [session]: a sessão do cliente;
    • [config]: a configuração da aplicação;

O método [execute] retorna uma tupla de dois elementos:

  • o primeiro é o dicionário de resultados na forma {‘action’: action, ‘status’: status, ‘response’: results};
  • o segundo é o código de estado HTTP a devolver ao cliente;

O controlador principal [MainController] [1] implementa a interface [InterfaceController] da seguinte forma:

#  import dependencies

from flask_api import status
from werkzeug.local import LocalProxy

#  web application controllers
from InterfaceController import InterfaceController

class MainController(InterfaceController):
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  retrieve path elements
        params = request.path.split('/')
        action = params[1]

        #  errors
        erreur = False
        #  session type must be known prior to certain actions
        type_response = session.get('typeResponse')
        if type_response is None and action != "init-session":
            #  we note the error
            résultat = {"action": action, "état": 101,
                        "réponse": ["pas de session en cours. Commencer par action [init-session]"]}
            erreur = True
        #  some actions require authentication
        user = session.get('user')
        if not erreur and user is None and action not in ["init-session", "authentifier-utilisateur"]:
            #  we note the error
            résultat = {"action": action, "état": 101,
                        "réponse": [f"action [{action}] demandée par utilisateur non authentifié"]}
            erreur = True
        #  are there any mistakes?
        if erreur:
            #  an error msg is returned
            return résultat, status.HTTP_400_BAD_REQUEST
        else:
            #  execute the controller associated with the action
            controller = config["controllers"][action]
            résultat, status_code = controller.execute(request, session, config)
            return résultat, status_code

O [MainController] realiza as verificações iniciais para validar o pedido.

  • linhas 11–13: O controlador começa por recuperar a ação solicitada pelo cliente. Recorde-se que os URLs dos serviços têm o formato [/action/param1/param2/…] e que este URL se encontra em [request.path];
  • linhas 17–23: a ação [init-session] é utilizada para inicializar o tipo de resposta (json, xml, html) solicitado pelo cliente. Esta informação é armazenada na sessão sob a chave [responseType]. Portanto, se a ação não for [init-session], a sessão deve conter a chave [responseType]; caso contrário, a solicitação é inválida;
  • linhas 21-22: a estrutura do resultado devolvido por cada controlador, neste caso um resultado de erro:
    • [action]: é o nome da ação atual. Isto permitir-nos-á recuperar o seu nome ao registar o resultado da solicitação;
    • [status]: é um código de estado de três dígitos:
        • [x00] para sucesso;
        • [x01] para uma falha;
  • [response]: é a resposta à solicitação. A sua natureza é específica para cada solicitação;
  • linhas 24–30: a ação [authenticate-user] é utilizada para autenticar o utilizador. Se for bem-sucedida, é adicionada uma chave [user=True] à sessão do utilizador. Certos URLs de serviço são acessíveis apenas a um utilizador autenticado. É isso que é verificado aqui;
  • linha 26: apenas as ações [init-session] e [authenticate-user] podem ser executadas por um utilizador que ainda não tenha sido autenticado;
  • linhas 28–29: a resposta a enviar em caso de erro;
  • linhas 32–34: se ocorrer qualquer um dos dois erros anteriores, a resposta de erro é enviada ao cliente com o estado HTTP 400 BAD REQUEST;
  • linhas 35–39: se não ocorreu nenhum erro, o controlo é passado para o controlador responsável por lidar com a ação atual. A sua instância encontra-se na configuração da aplicação;

A classe [MainController] dá continuidade ao trabalho da função [front_controller]: juntas, elas tratam de tudo o que pode ser separado do processamento da solicitação, esperando até o último momento para passar a solicitação a um controlador específico. A divisão do código entre a função [front_controller] e a classe [MainController] é inteiramente subjetiva. Aqui, quis preservar a estrutura da versão anterior: a função [front_controller] já existia com o nome [main]. Na prática, seria possível:

  • colocar tudo na função [front_controller] e eliminar a classe [MainController];
  • colocar tudo na classe [MainController] e eliminar a função [front_controller]. Eu tenderia a escolher esta solução porque tem a vantagem de simplificar o código do script principal [main];

30.7. Processamento específico da ação

Voltemos à arquitetura MVC da aplicação:

Image

Ainda estamos na etapa 1 acima. Se não houver erros, a etapa 2 terá início. O pedido foi encaminhado para o controlador específico da ação solicitada pelo pedido. Vamos supor que essa ação seja [/init-session], definida pela rota:

1
2
3
4
5
#  init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

Esta ação está ligada a um controlador na configuração [config]:


        # actions autorisées et leurs contrôleurs
        "controllers": {
            # initialisation d'une session de calcul
            "init-session": InitSessionController(),
            
        },

O [InitSessionController] (linha 4) assume então o controlo. O seu código é o seguinte:

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class InitSessionController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action, type_response = request.path.split('/')

        #  initially no error
        erreur = False
        #  check response type
        if type_response not in config['responses'].keys():
            erreur = True
            résultat = {"action": action, "état": 701,
                        "réponse": [f"paramètre [type={type_response}] invalide"]}
        #  if no error
        if not erreur:
            #  set the session type in the flask session
            session['typeResponse'] = type_response
            résultat = {"action": action, "état": 700,
                        "réponse": [f"session démarrée avec le type de réponse {type_response}"]}
            return résultat, status.HTTP_200_OK
        else:
            return résultat, status.HTTP_400_BAD_REQUEST
  • linha 6: tal como os outros controladores, o [InitSessionController] implementa a interface [InterfaceController];
  • linha 10: a URL é do tipo [/init-session/type_response]. Recuperamos a ação [init-session] e o tipo de resposta desejado;
  • linha 15: o tipo de resposta desejado só pode ser um dos presentes na configuração de resposta:

        # les différents types de réponse (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },
  • se não for esse o caso, é preparada uma resposta de erro 701 (linha 17);
  • linhas 20–25: caso em que o tipo de resposta pretendido é válido;
  • linha 22: o tipo de resposta desejado é armazenado na sessão. Isto porque precisaremos de o recordar para pedidos subsequentes;
  • linhas 23–24: preparar uma resposta de sucesso 700;
  • linha 25: a resposta de sucesso é devolvida ao chamador;
  • linha 27: se ocorreu um erro, a resposta de erro é devolvida ao chamador;

30.8. Gerar a resposta HTTP do servidor

Voltemos à arquitetura MVC da aplicação:

Image

Acabámos de abordar os passos 1 e 2. Encontrámos três códigos de estado:

  • 700: /init-session bem-sucedida;
  • 701: /init-session falhou;
  • 101: pedido inválido, quer porque a sessão não foi inicializada, quer porque o utilizador não está autenticado;

Vamos examinar como a resposta do servidor será enviada ao cliente durante o passo 3 acima. Isto acontece na função [front_controller] do script [main]:

#  the front controller
def front_controller() -> tuple:
    #  the request is processed
    logger = None
    try:
        #  logger
        logger = Logger(config["logsFilename"])
        #  we store it in a config associated with the thread
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        #  log the request
        logger.write(f"[ front_controller] requête : {request}\n")
        #  the thread is interrupted if requested
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            #  pause is randomized so that some threads are interrupted and others not
            aléa = randint(0, 1)
            if aléa == 1:
                #  log before break
                logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                #  break
                time.sleep(sleep_time)
        #  forward the request to the main controller
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        #  we log the result sent to the customer
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        #  was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            #  send an e-mail to the application administrator
            send_adminmail(config, log)
        #  determine the desired type of response
        if session.get('typeResponse') is None:
            #  the session type has not yet been set - it will be jSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        #  build the response to be sent
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        #  we send the answer
        return response, status_code
    except BaseException as erreur:
        #  it's an unexpected error - log the error if possible
        if logger:
            logger.write(f"[ front_controller] {erreur}")
        #  we prepare the response to the customer
        résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
        #  we send a response in jSON
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        #  close the log file if it has been opened
        if logger:
            logger.close()
  • Estamos agora na linha 26: o controlador principal devolveu a sua resposta de erro;
  • linhas 27–29: independentemente da resposta do controlador principal (sucesso ou falha), esta resposta é registada no ficheiro de registo;
  • linhas 30–33: tal como nas versões anteriores, se o estado HTTP for [500 INTERNAL SERVER ERROR], enviamos um e-mail ao administrador da aplicação com o registo de erros;
  • linhas 34–39: enviamos a resposta HTTP, e o resultado devolvido pelo controlador é colocado no corpo desta resposta. Precisamos de saber em que formato (JSON, XML, HTML) o cliente deseja esta resposta. Procuramos o tipo de resposta desejado na sessão. Se não estiver lá, definimos arbitrariamente este tipo como JSON;
  • linhas 40–43: a resposta HTTP é construída;

No ficheiro de configuração, cada tipo de resposta (json, xml, html) foi associado a uma instância de classe:


        # les différents types de réponse (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

As classes de resposta estão localizadas na pasta [responses] da árvore de diretórios do servidor:

Image

Cada classe de resposta implementa a seguinte interface [InterfaceResponse]:


from abc import ABC, abstractmethod
 
from flask.wrappers import Response
from werkzeug.local import LocalProxy
 
class InterfaceResponse(ABC):
 
    @abstractmethod
    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        pass
  • linhas 8–11: a interface [InterfaceResponse] define um único método [build_http_response] com os seguintes parâmetros:
    • [request, session, config]: estes são os parâmetros recebidos pelo controlador de ação;
    • [result, status_code]: estes são os resultados produzidos pelo controlador de ação;

Vamos agora apresentar a resposta JSON. Esta é gerada pela seguinte classe [JsonResponse]:

import json

from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy

from InterfaceResponse import InterfaceResponse

class JsonResponse(InterfaceResponse):

    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        #  results: the results dictionary
        #  status_code: status code of the HTTP response

        #  we return the answer HTTP
        response = make_response(json.dumps(résultat, ensure_ascii=False))
        response.headers['Content-Type'] = 'application/json; charset=utf-8'
        return response, status_code

Estamos familiarizados com este código, que já encontrámos muitas vezes. É o código da função [json_response] no módulo [myutils].

30.9. Testes iniciais

No código que analisámos, deparámo-nos com três códigos de estado:

  • 700: /init-session bem-sucedida;
  • 701: /init-session falhou;
  • 101: pedido inválido, quer porque a sessão não foi inicializada, quer porque o utilizador não está autenticado;

Vamos tentar acionar estes erros com uma sessão JSON.

  • Iniciamos o servidor web, o SGBD e o servidor de e-mail;
  • Iniciamos um cliente Postman;

Teste 1

Primeiro, vamos demonstrar um pedido inválido, uma vez que a sessão ainda não foi inicializada:

Image

#  authenticate-user
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

mas só é aceite se a sessão tiver sido inicializada previamente com a ação [/init-session].

Vamos executar a solicitação e ver o resultado enviado pelo servidor:

Image

  • [1-2]: recebemos uma resposta JSON. Quando o tipo de resposta ainda não foi especificado pelo cliente, o servidor utiliza JSON para responder;
  • [3-5]: o dicionário JSON da resposta;
    • [action]: a ação que foi executada;
    • [status]: o código de estado da resposta. Um código [x01] indica um erro;
    • [response]: é adaptado a cada ação. Aqui, contém uma mensagem de erro;

Agora vamos iniciar uma sessão com um tipo de resposta incorreto:

Image

  • [1-2] é uma rota válida:
#  init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

Entrará, portanto, no pipeline de processamento de pedidos do servidor MVC. No entanto, deverá ser rejeitado durante este processamento, uma vez que o tipo de sessão solicitado está incorreto.

A resposta é a seguinte:

Image

  • em [4], um código de erro [x01];
  • em [5], a explicação do erro;

Agora, vamos inicializar uma sessão JSON:

Image

A resposta é a seguinte:

Image

Agora, vamos inicializar uma sessão XML. A resposta JSON será substituída por uma resposta XML gerada pela seguinte classe [XmlResponse]:

import xmltodict
from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy

from InterfaceResponse import InterfaceResponse
from Logger import Logger

class XmlResponse(InterfaceResponse):

    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        #  results: the results dictionary
        #  status_code: status code of the HTTP response

        #  result: the dictionary to be transformed into the XML string
        xml_string = xmltodict.unparse({"root": résultat})
        #  we return the answer HTTP
        response = make_response(xml_string)
        response.headers['Content-Type'] = 'application/xml; charset=utf-8'
        return response, status_code

Este é um código com o qual estamos familiarizados — provém da função [xml_response] no módulo partilhado [myutils].

Inicializamos uma sessão XML:

Image

A resposta do servidor é então a seguinte:

Image

Recebemos a mesma resposta que em JSON, mas desta vez a resposta está formatada como XML.

30.10. A ação [authenticate-user]

A ação [authenticate-user] permite autenticar um utilizador que pretenda utilizar a aplicação de cálculo de impostos. A sua rota é definida da seguinte forma no script [main]:

1
2
3
4
5
#  authenticate-user
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

O servidor espera dois parâmetros POST:

  • [user]: o ID do utilizador;
  • [password]: a sua palavra-passe;

A lista de utilizadores autorizados é definida na configuração [config]:


        # utilisateurs autorisés à utiliser l'application
        "users"[
            {
                "login""admin",
                "password""admin"
            }
        ],

Aqui, temos uma lista com um único elemento.

A ação [authenticate-user] é tratada pelo seguinte controlador [AuthentifierUtilisateurController]:

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController
from Logger import Logger

class AuthentifierUtilisateurController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  POST parameters
        post_params = request.form
        #  response status code HTTP
        status_code = None
        #  initially no errors
        erreur = False
        erreurs = []
        #  you need a POST with two parameters
        if len(post_params) != 2:
            erreur = True
            status_code = status.HTTP_400_BAD_REQUEST
            erreurs.append("méthode POST requise, paramètre [action] dans l'URL, paramètres postés [user, password]")
        if not erreur:
            #  retrieve POST parameters
            #  parameter [user]
            user = post_params.get("user")
            if user is None:
                erreur = True
                erreurs.append("paramètre [user] manquant")
            #  parameter [password]
            password = post_params.get("password")
            if password is None:
                erreur = True
                erreurs.append("paramètre [password] manquant")
            #  mistake?
            if erreur:
                status_code = status.HTTP_400_BAD_REQUEST
        #  mistake?
        if not erreur:
            #  check the validity of the (user, password) pair
            users = config['users']
            i = 0
            nbusers = len(users)
            trouvé = False
            while not trouvé and i < nbusers:
                trouvé = user == users[i]["login"] and password == users[i]["password"]
                i += 1
            #  found?
            if not trouvé:
                #  we note the error
                erreur = True
                status_code = status.HTTP_401_UNAUTHORIZED
                erreurs.append(f"Echec de l'authentification")
            else:
                #  note in the session that the user has been found
                session["user"] = True
        #  it's over
        if not erreur:
            #  error-free return
            résultat = {"action": action, "état": 200, "réponse": f"Authentification réussie"}
            return résultat, status.HTTP_200_OK
        else:
            #  return with error
            return {"action": action, "état": 201, "réponse": erreurs}, status_code
  • linha 14: recuperar os parâmetros POST;
  • linha 19: a lista de erros encontrados na solicitação;
  • linhas 20–24: verificamos se existem efetivamente dois parâmetros enviados;
  • linhas 27–31: verificar se existe um parâmetro [users];
  • linhas 32–36: verificar a presença de um parâmetro [password];
  • linhas 38–39: se os parâmetros enviados estiverem incorretos, prepare uma resposta HTTP 400 BAD REQUEST;
  • linhas 40–58: verificar se as credenciais [user, password] pertencem a um utilizador autorizado a utilizar a aplicação;
  • linhas 51–55: se o utilizador (user, password) não estiver autorizado a utilizar a aplicação, preparar uma resposta HTTP 401 UNAUTHORIZED;
  • linhas 56–58: se o utilizador estiver autorizado, registamos na sessão, utilizando a chave [user], que ele se autenticou;

Note que se o utilizador foi autenticado com as credenciais [credenciais1] e não consegue autenticar-se com as credenciais [credenciais2], permanece autenticado com as credenciais [credenciais1].

Vamos executar alguns testes no Postman:

  • Iniciamos o servidor web, o SGBD e o servidor de e-mail;
  • Usando o cliente Postman:
    • iniciamos uma sessão JSON;
    • depois, autenticamos;

Aqui estão diferentes cenários.

Caso 1: POST sem parâmetros enviados

Image

  • Em [3-5], o POST não tem corpo;

O resultado da solicitação é o seguinte:

Image

  • Em [2], recebemos uma resposta HTTP 400 BAD REQUEST;
  • Em [5], recebemos um código de erro [201];

Caso 2: POST com credenciais incorretas

Image

  • Em [6], as credenciais estão incorretas;

O servidor envia a seguinte resposta:

Image

  • em [2], a resposta HTTP 401 UNAUTHORIZED;
  • Em [5], a resposta de erro;

Caso 2: POST com credenciais corretas

Image

  • Em [6], as credenciais estão corretas;

A resposta do servidor é a seguinte:

  • em [2], uma resposta HTTP 200 OK; Image
  • em [5], a resposta de sucesso;

30.11. A ação [calculate_tax]

A ação [calculate_tax] calcula o imposto de um contribuinte. A sua rota é definida da seguinte forma no script [main]:

1
2
3
4
5
#  calculate-tax
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

O servidor espera três parâmetros POST:

  • [married]: sim / não;
  • [children]: número de filhos do contribuinte;
  • [salário]: salário anual do contribuinte;

O controlador [CalculateTaxController] trata da ação [calculate_tax]:

import re

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController
from TaxPayer import TaxPayer

class CalculerImpotController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  no error at start
        erreur = False
        erreurs = []
        #  POST parameters
        post_params = request.form
        #  you need a POST with three parameters
        if len(post_params) != 3:
            erreur = True
            erreurs.append(
                "méthode POST requise avec les paramètres postés [marié, enfants, salaire]")
        #  analyze posted parameters
        if not erreur:
            #  married parameter
            marié = post_params.get("marié")
            if marié is None:
                erreurs.append("paramètre [marié] manquant")
            else:
                #  is the parameter valid?
                marié = marié.lower()
                if marié != "oui" and marié != "non":
                    erreur = True
                    erreurs.append(f"valeur [{marié}] invalide pour le paramètre [marié (oui/non)]")
            #  children] parameter
            enfants = post_params.get("enfants")
            if enfants is None:
                erreur = True
                erreurs.append("paramètre [enfants] manquant")
            else:
                #  is the parameter valid?
                enfants = enfants.strip()
                match = re.match(r"\d+", enfants)
                if not match:
                    erreur = True
                    erreurs.append(f"valeur [{enfants}] invalide pour le paramètre [enfants (entier>=0)]")
            #  salary parameter
            salaire = post_params.get("salaire")
            if salaire is None:
                erreur = True
                erreurs.append("paramètre [salaire] manquant")
            else:
                #  is the parameter valid?
                salaire = salaire.strip()
                match = re.match(r"\d+", salaire)
                if not match:
                    erreur = True
                    erreurs.append(f"valeur [{salaire}] invalide pour le paramètre [salaire (entier>=0)]")
        #  mistake?
        if erreur:
            status_code = status.HTTP_400_BAD_REQUEST
            résultat = {"action": action, "état": 301, "réponse": erreurs}
            #  we return the result
            return résultat, status_code

        #  tAX CALCULATION
        #  retrieve the [business] layer and the [adminData] dictionary
        métier = config["layers"]["métier"]
        admin_data = config["admindata"]
        #  tAX CALCULATION
        taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
        métier.calculate_tax(taxpayer, admin_data)
        #  simulation no
        id_simulation = session.get('id_simulation', 0)
        id_simulation += 1
        session['id_simulation'] = id_simulation
        #  we put the result in session in the form of a TaxPayer dictionary
        simulation = taxpayer.fromdict({'id': id_simulation}).asdict()
        #  we add the result to the list of simulations already carried out and put it in session
        simulations = session.get("simulations", [])
        simulations.append(simulation)
        session["simulations"] = simulations
        #  result
        résultat = {"action": action, "état": 300, "réponse": simulation}
        status_code = status.HTTP_200_OK

        #  we return the result
        return résultat, status_code
  • linha 13: recuperamos o nome da ação atual;
  • linha 17: recolhemos os erros numa lista;
  • linha 19: recuperamos os parâmetros enviados. Estes são enviados no formato [x-www-form-urlencoded], razão pela qual os recuperamos de [request.form]. Se tivessem sido enviados como JSON, teríamos de os recuperar de [request.data];
  • linhas 21–24: verificamos se existem efetivamente três parâmetros enviados;
  • linhas 27–36: verificamos a presença e a validade do parâmetro enviado [married];
  • linhas 37–48: verificamos a presença e a validade do parâmetro enviado [children];
  • linhas 49–60: verificamos a presença e a validade do parâmetro enviado [salary];
  • linhas 62–66: se houver um erro, é enviada uma resposta de erro 400 BAD REQUEST com um código de estado [301];
  • linhas 69–71: se não houve erro, preparar o cálculo do imposto. Para tal,
    • linha 70: recuperar uma referência da camada [business];
    • linha 71: recuperar dados da autoridade fiscal na configuração do servidor;
  • linhas 72–74: o imposto do contribuinte é calculado;
  • linhas 75–77: contamos o número de cálculos de imposto realizados pelo utilizador;
    • linha 76: recuperar o número do último cálculo realizado da sessão. Aqui, referimo-nos ao resultado de um cálculo como [simulação];
    • linha 77: o número da última simulação é incrementado;
    • linha 78: este número é guardado na sessão;
  • linhas 79–84: para acompanhar os cálculos realizados pelo utilizador, iremos armazenar a lista de simulações que realizou na sua sessão;
  • linha 80: uma simulação será o dicionário de um objeto TaxPayer cuja propriedade [id] terá o valor do número da simulação;
  • linhas 82–84: a simulação atual é adicionada à lista de simulações na sessão;
  • linhas 86-87: preparamos uma resposta HTTP de sucesso;
  • linha 90: devolvemos o resultado;

Vamos executar alguns testes: o servidor web, o SGBD, o servidor de e-mail e um cliente Postman são iniciados.

Caso 1: realizar um cálculo de impostos enquanto a sessão não está inicializada

Image

A resposta é a seguinte:

Image

Caso 2: realizar um cálculo de impostos sem estar autenticado

Primeiro, iniciamos uma sessão JSON com [/init-session/json]. Em seguida, fazemos o mesmo pedido que anteriormente. A resposta é a seguinte:

Image

Caso 3: Realização de um cálculo de impostos com parâmetros em falta

Inicializamos uma sessão JSON, autenticamo-nos e, em seguida, fazemos a seguinte solicitação:

Image

  • em [5], falta o parâmetro [married];

A resposta é a seguinte:

Caso 4: Cálculo do imposto com parâmetros incorretos

Image

Image

A resposta do servidor é a seguinte:

Image

Caso 4: Realização de um cálculo de imposto com parâmetros corretos

Image

A resposta do servidor é a seguinte:

Image

30.12. A ação [list-simulations]

A ação [list-simulations] permite ao utilizador visualizar a lista de simulações que realizou desde o início da sessão. A sua rota é definida da seguinte forma no script [main]:

1
2
3
4
5
#  lister-simulations
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

O servidor não espera quaisquer parâmetros. A ação [lister-simulations] é tratada pelo seguinte [ListerSimulationsController]:

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class ListerSimulationsController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  retrieve the list of simulations in the session
        simulations = session.get("simulations", [])
        #  we return the result
        return {"action": action, "état": 500,
                "réponse": simulations}, status.HTTP_200_OK
  • linha 13: a lista de simulações é recuperada da sessão;
  • linhas 15-16: é devolvida uma resposta de sucesso;

Vamos executar o seguinte teste no Postman:

  • Iniciamos uma sessão JSON;
  • Efetua a autenticação;
  • Realizamos dois cálculos de impostos;
  • Solicitamos a lista de simulações;

O pedido é o seguinte:

  • em [3], não há parâmetros; Image

A resposta do servidor é a seguinte:

Image

  • em [4], a lista de simulações do utilizador;

30.13. A ação [delete-simulation]

A ação [delete-simulation] permite que um utilizador elimine uma das simulações da sua lista de simulações. A sua rota é definida da seguinte forma no script [main]:

1
2
3
4
5
#  delete-simulation
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
    #  execute the controller associated with the action
    return front_controller()

O servidor espera um único parâmetro: o número da simulação a ser eliminada. A ação [delete-simulation] é tratada pelo seguinte [DeleteSimulationController]:

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class SupprimerSimulationController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action, numéro = request.path.split('/')

        #  parameter [number] is a positive integer or zero according to its route
        numéro = int(numéro)
        #  the simulation id=number must exist in the simulation list
        simulations = session.get("simulations", [])
        liste_simulations = list(filter(lambda simulation: simulation['id'] == numéro, simulations))
        if not liste_simulations:
            msg_erreur = f"la simulation n° [{numéro}] n'existe pas"
            #  we return the error
            return {"action": action, "état": 601, "réponse": [msg_erreur]}, status.HTTP_400_BAD_REQUEST
        #  delete simulation id=number
        simulation = liste_simulations.pop(0)
        simulations.remove(simulation)
        #  put the simulations back in the session
        session["simulations"] = simulations
        #  we return the result
        return {"action": action, "état": 600, "réponse": simulations}, status.HTTP_200_OK
  • linha 10: recuperamos os dois elementos do caminho da solicitação. Eles são recuperados como strings;
  • linha 13: o parâmetro [number] é convertido num inteiro. Sabemos que isto é possível devido à assinatura da rota,

@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])

Sabemos também que se trata de um número inteiro >=0. De facto, não podemos ter um URL como [/delete-simulation/-4]. Este é rejeitado pelo servidor Flask;

  • linha 15: recuperamos a lista de simulações da sessão;
  • linha 16: utilizando a função [filter], procuramos a simulação com id==número. Obtemos um objeto [filter] que convertemos numa [list];
  • linhas 17–20: se o filtro não devolver nada, então a simulação a ser eliminada não existe. Devolvemos uma resposta de erro a indicar isso;
  • linhas 21–23: eliminamos a simulação devolvida pelo filtro;
  • linha 25: restauramos a nova lista de simulações na sessão;
  • linha 27: devolvemos a nova lista de simulações na resposta;

Realizamos um teste de sucesso e um teste de falha. Executamos simulações e, em seguida, solicitamos a lista de simulações:

Image

  • As simulações aqui têm os números 2 e 3;

Solicitamos que a simulação com o número 3 seja removida.

Image

A resposta é a seguinte:

Agora, vamos repetir a mesma operação (eliminar a simulação com id=3). A resposta é então a seguinte:

Image

Image

30.14. A ação [end-session]

A ação [end-session] permite que um utilizador termine a sua sessão de simulação. O seu caminho é definido da seguinte forma no script [main]:

1
2
3
4
5
#  end of session
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

O servidor não espera parâmetros. A ação é tratada pelo seguinte [FinSessionController]:

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class FinSessionController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  delete all keys in the current session
        session.clear()
        #  we return the result
        return {"action": action, "état": 400, "réponse": "session réinitialisée"}, status.HTTP_200_OK
  • Linha 13: Apaga todas as chaves da sessão. Isto apaga:
    • [typeResponse]: o tipo de respostas HTTP (json, xml, html);
    • [simulation_id]: o ID da última simulação realizada;
    • [simulations]: a lista das simulações do utilizador;
    • [user]: o indicador de que o utilizador foi autenticado;
  • retornar a resposta;

Poder-se-á questionar como é que a resposta HTTP da linha 15 será devolvida, agora que o tipo de resposta já não se encontra na sessão. Para descobrir, precisamos de voltar à função |front_controller| no script principal [main] e modificá-la da seguinte forma:


…        
         # on not# note the type of response required if this information is in the session
        type_response1 = session.get('typeResponse'None)
        # forward the request to the main controller
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        # we log the result sent to the customer
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        # was there a fatal error?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # send an e-mail to the application administrator
            send_adminmail(config, log)
        # determine the desired type of response
        type_response2=session.get('typeResponse')
        if  type_response2 is None and type_response1 is None:
            # the session type has not yet been set - it will be jSON
            type_response = 'json'
        elif type_response2 is not None:
            # the type of response is known and in the session
            type_response = type_response2
        else:
            type_response=type_response1
        # build the response to be sent
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        # we send the answer
        return response, status_code
  • linha 3: o tipo da resposta atualmente na sessão é armazenado;
  • linha 6: a ação é executada. Se for:
    • [end-session], a chave [typeResponse] já não se encontra na sessão;
    • [init-session], a chave [typeResponse] na sessão pode ter mudado de valor;;
  • linhas 14–20: a resposta HTTP deve ser enviada. Precisamos de saber de que forma:
    • linhas 16–18: se o tipo de resposta não estiver definido nem por [type_response1] na linha 3 nem por [type_response2] na linha 15, então o tipo de resposta não foi definido nem antes nem depois da ação. Nesse caso, usamos JSON (linha 18);
    • linhas 19–21: se [type_response2] existir — o tipo de resposta na sessão após a ação — então esse é o tipo a utilizar;
    • linhas 22–23: caso contrário, [type_response1], o tipo de resposta antes da ação (que deve ser [end-session]), é o que deve ser utilizado;

30.15. A ação [get-admindata]

Vamos agora discutir os dois URLs reservados para os serviços JSON e XML:

Ação
Função
Contexto de execução
/get-admindata
Retorna os dados fiscais utilizados para calcular o imposto
.
Utilizado apenas se o tipo de sessão for json ou xml. O utilizador deve estar autenticado
/calculate-taxes
Calcula o imposto para uma lista de contribuintes enviada em JSON
Pedido GET.
Utilizado apenas se o tipo de sessão for json ou xml. O utilizador deve estar autenticado

A URL [/get-admindata] está definida nas rotas do script principal [main] da seguinte forma:

1
2
3
4
5
#  get-admindata
@app.route('/get-admindata', methods=['GET'])
def get_admindata() -> tuple:
    #  execute the controller associated with the action
    return front_controller()

A rota [/get-admindata] é tratada pelo seguinte [GetAdminDataController]:

#  import dependencies

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class GetAdminDataController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action = request.path.split('/')
        #  only json and xml sessions are accepted
        type_response = session.get('typeResponse')
        if type_response != 'json' and type_response != 'xml':
            #  an error response is returned
            return {
                       "action": action,
                       "état": 1001,
                       "réponse": ["cette action n'est possible que pour les sessions json ou xml"]
                   }, status.HTTP_400_BAD_REQUEST
        else:
            #  a success answer is returned
            return {"action": action, "état": 1000, "réponse": config["adminData"].asdict()}, status.HTTP_200_OK
  • linhas 13-21: verificamos se estamos numa sessão JSON ou XML;
  • linha 24: devolvemos o dicionário de dados da administração fiscal, que foi colocado na configuração quando o servidor foi iniciado:

    # admindata sera une donnée de portée application en lecture seule
    config["admindata"] = config["layers"]["dao"].get_admindata()

Vamos utilizar um cliente Postman e solicitar a URL [/get-admindata], após iniciar uma sessão JSON e autenticar-nos:

Image

A resposta do servidor é a seguinte:

Image

30.16. A ação [calculate-taxes]

A ação [calculate-taxes] calcula os impostos para uma lista de contribuintes encontrada no corpo da solicitação como uma string JSON. Já estamos familiarizados com esta ação: na versão anterior, ela era chamada de [calculate_tax_in_bulk_mode].

A sua rota é a seguinte:

1
2
3
4
5
#  batch tax calculation
@app.route('/calculer-impots', methods=['POST'])
def calculer_impots():
    #  execute the controller associated with the action
    return front_controller()

Esta ação é tratada pelo seguinte [CalculateTaxesController]:

import json

from flask_api import status
from werkzeug.local import LocalProxy

from ImpôtsError import ImpôtsError
from InterfaceController import InterfaceController
from TaxPayer import TaxPayer

class CalculerImpotsController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        #  path elements are retrieved
        dummy, action = request.path.split('/')

        #  only json and xml sessions are accepted
        type_response = session.get('typeResponse')
        if type_response != 'json' and type_response != 'xml':
            #  an error response is returned
            return {
                       "action": action,
                       "état": 1501,
                       "réponse": ["cette action n'est possible que pour les sessions json ou xml"]
                   }, status.HTTP_400_BAD_REQUEST

        #  retrieve the body of the post - wait for a list of dictionaries
        msg_erreur = None
        list_dict_taxpayers = None
        #  the jSON body of POST
        request_text = request.data
        try:
            #  which we transform into a list of dictionaries
            list_dict_taxpayers = json.loads(request_text)
        except BaseException as erreur:
            #  we note the error
            msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
        #  do we have a non-empty list?
        if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
            #  we note the error
            msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
        #  do we have a list of dictionaries?
        if not msg_erreur:
            erreur = False
            i = 0
            while not erreur and i < len(list_dict_taxpayers):
                erreur = not isinstance(list_dict_taxpayers[i], dict)
                i += 1
            #  mistake?
            if erreur:
                msg_erreur = "le corps du POST doit être une liste de dictionnaires"
        #  mistake?
        if msg_erreur:
            #  an error response is sent to the client
            résultats = {"action": action, "état": 1501, "réponse": [msg_erreur]}
            return résultats, status.HTTP_400_BAD_REQUEST

        #  check TaxPayers one by one
        #  initially no errors
        list_erreurs = []
        for dict_taxpayer in list_dict_taxpayers:
            #  we create a TaxPayer from dict_taxpayer
            msg_erreur = None
            try:
                #  the following operation will eliminate cases where the parameters are not
                #  properties of the TaxPayer class as well as the cases where their values
                #  are incorrect
                TaxPayer().fromdict(dict_taxpayer)
            except BaseException as erreur:
                msg_erreur = f"{erreur}"
            #  certain keys must be present in the dictionary
            if not msg_erreur:
                #  the keys [married, children, salary] must be present in the dictionary
                keys = dict_taxpayer.keys()
                if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
                    msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
            #  mistakes?
            if msg_erreur:
                #  we note the error in the TaxPayer itself
                dict_taxpayer['erreur'] = msg_erreur
                #  add the TaxPayer to the error list
                list_erreurs.append(dict_taxpayer)

        #  we've processed all the taxpayers - are there any mistakes?
        if list_erreurs:
            #  an error response is sent to the client
            résultats = {"action": action, "état": 1501, "réponse": list_erreurs}
            return résultats, status.HTTP_400_BAD_REQUEST

        #  no mistakes, we can work
        #  data recovery from tax authorities
        admindata = config["admindata"]
        métier = config["layers"]["métier"]
        try:
            #  process the TaxPayer one by one
            list_taxpayers = []
            for dict_taxpayer in list_dict_taxpayers:
                #  tAX CALCULATION
                taxpayer = TaxPayer().fromdict(
                    {'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
                     'salaire': dict_taxpayer['salaire']})
                métier.calculate_tax(taxpayer, admindata)
                #  the result is stored as a dictionary
                list_taxpayers.append(taxpayer.asdict())
            #  we add list_taxpayers to the current simulations, giving each simulation a number
            simulations = session.get("simulations", [])
            id_simulation = session.get("id_simulation", 0)
            for simulation in list_taxpayers:
                #  each simulation is given a number
                id_simulation += 1
                simulation['id'] = id_simulation
                #  we add it to the current list of simulations
                simulations.append(simulation)
            #  we put everything back in session
            session["simulations"] = simulations
            session["id_simulation"] = id_simulation
            #  we send the response to the client
            return {"action": action, "état": 1500, "réponse": list_taxpayers}, status.HTTP_200_OK
        except ImpôtsError as erreur:
            #  an error response is sent to the client
            return {"action": action, "état": 1501, "réponse": [f"{erreur}"]}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • linhas 16-24: verificamos se estamos efetivamente numa sessão JSON ou XML
  • linhas 26–120: este código é-nos geralmente familiar. Provém da função |index_controller| da versão 10 da aplicação, que foi adaptada para cumprir as especificações da interface [InterfaceController] implementada;
  • linhas 104–115: código adicionado para ter em conta o novo ambiente deste controlador. Acabámos de realizar cálculos fiscais. Precisamos de armazenar os resultados na lista de simulações mantida na sessão;
  • linha 105: recuperamos a lista de simulações na sessão;
  • linha 106: recuperamos o número da última simulação realizada;
  • linhas 107–112: percorremos a lista de dicionários que contêm os resultados do cálculo de impostos; atribuímos um ID de simulação a cada um, e cada dicionário é adicionado à lista de simulações;
  • linhas 113–115: a nova lista de simulações e o número da última simulação realizada são devolvidos à sessão;

Executamos o seguinte teste no Postman após inicializar uma sessão JSON e autenticar:

Image

Image

A resposta do servidor é a seguinte:

Image

Se agora solicitarmos a lista de simulações:

Note-se que na lista de resultados para [/calcul-impots], os contribuintes não têm um atributo [id], enquanto que na lista de simulações, cada simulação tem um número que a identifica.

Image