30. Exercício prático: versão 12
Neste capítulo, vamos criar um aplicativo web que siga a arquitetura MVC (Modelo-Visão-Controlador). A aplicação poderá fornecer suas respostas em três formatos: jSON, XML, HTML. Há um salto de complexidade entre o que faremos agora e o que foi feito anteriormente. Reutilizaremos a maioria dos conceitos abordados até agora e detalharemos todas as etapas que levam à aplicação final.
30.1. Arquitetura MVC
Vamos implementar o modelo de arquitetura conhecido como MVC (Modelo – Visão – Controlador) da seguinte maneira:
O processamento de uma solicitação de um cliente ocorrerá da seguinte maneira:
- 1 - solicitação
Os URL solicitados terão o formato http://machine:port/action/param1/param2/… O [Contrôleur principal] utilizará um arquivo de configuração para “rotear” a solicitação para o controlador correto. Para isso, ele utilizará o campo [action] do URL. O restante do URL e do [param1/param2/…] é composto por parâmetros opcionais que serão transmitidos à ação. O C de MVC é, neste caso, a string [Contrôleur principal, Contrôleur / Action]. Se nenhum controlador puder processar a ação solicitada, o servidor web responderá que a ação URL solicitada não foi encontrada.
- 2 - processamento
- A ação selecionada [2a] pode utilizar os parâmetros parami que a ação [Contrôleur principal] lhe transmitiu. Esses parâmetros podem provir de duas fontes:
- do caminho [/param1/param2/…] do URL,
- de parâmetros enviados no corpo da solicitação do cliente;
- no processamento da solicitação do usuário, a ação pode precisar da camada [métier] [2b]. Uma vez processada a solicitação do cliente, ela pode gerar diversas respostas. Um exemplo clássico é:
- uma resposta de erro, caso a solicitação não tenha sido processada corretamente;
- uma resposta de confirmação, caso contrário;
- o [Contrôleur / Action] enviará sua resposta [2c] ao controlador principal, juntamente com um código de estado. Esses códigos de estado representarão de forma exclusiva o estado em que a aplicação se encontra. Será um código de sucesso ou um código de erro;
- 3 - resposta
- dependendo se o cliente solicitou uma resposta jSON, XML ou HTML, o [Contrôleur principal] instanciará o [3a] com o tipo de resposta apropriado e solicitará que este envie a resposta ao cliente. O [Contrôleur principal] transmitirá a ele tanto a resposta quanto o código de status fornecidos pelo [Contrôleur / Action] que foi executado;
- se a resposta desejada for do tipo jSON ou XML, a resposta selecionada formatará a resposta do [Contrôleur / Action] que lhe foi fornecida e a enviará ao [3c]. O cliente capaz de processar essa resposta pode ser um script de console em Python ou um script em JavaScript hospedado em uma página HTML;
- se a resposta desejada for do tipo HTML, a resposta selecionada selecionará uma das visualizações HTML ou [Vuei] por meio do código de estado que lhe foi fornecido. Esse é o V de MVC. A cada código de estado corresponde uma única visualização. Essa visualização V exibirá a resposta do [Contrôleur / Action] que foi executado. Ela formata os dados dessa resposta usando HTML, CSS e JavaScript. Esses dados são chamados de modelo da visualização. É o M de 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 da definição que se dá ao modelo, esses dois conceitos podem estar ou não relacionados. Consideremos uma aplicação web MVC de camada única:

No exemplo acima, cada [Contrôleur / Action] integra uma parte das camadas [métier] e [dao]. Na camada [web], existe de fato uma arquitetura MVC, mas o conjunto da aplicação não possui uma arquitetura em camadas. Aqui, há apenas uma camada, a camada web, que realiza todas as tarefas.
Agora, vamos considerar uma arquitetura web multicamadas:

A camada [web] pode ser implementada sem seguir o modelo MVC. Nesse caso, temos, de fato, uma arquitetura multicamadas, mas a camada web não implementa o modelo MVC.
Por exemplo, no mundo .NET, a camada [web] acimaacima pode ser implementada com ASP.NET e MVC, e temos, então, uma arquitetura em camadas com uma camada [web] do tipo MVC. Feito isso, é possível substituir essa camada ASP.NET MVC por uma camada ASP.NET clássica (WebForms), mantendo o restante (negócio, DAO, Piloto) inalterado. Temos, então, uma arquitetura em camadas com uma camada [web] que não é mais do tipo MVC.
Em MVC, afirmamos que o modelo M era o da vista V, c.a.d, ou seja, o conjunto de dados exibidos pela vista V. É fornecida outra definição do modelo M de MVC:

Muitos autores consideram que o que está à direita da camada [web] forma o modelo M do MVC. Para evitar ambiguidades, pode-se referir-se:
- do modelo do domínio, ao se referir a tudo o que está à direita da camada [web];
- do modelo da visualização, quando nos referimos aos dados exibidos por uma visualização V;
Daqui em diante, quando falarmos de modelo, estaremos sempre nos referindo ao modelo da visualização.
30.2. Arquitetura da aplicação cliente/servidor
A aplicação web terá a seguinte arquitetura:
- em [1], o servidor web terá dois tipos de clientes:
- em [2], um cliente de console que 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 [métier] e [dao] das versões anteriores;
- o cliente web [2] será atualizado para incorporar as novas URL de serviço do aplicativo web;
- a aplicação HTML exibida pelo navegador precisa ser totalmente reescrita;
Vamos desenvolver a aplicação em várias etapas:
- vamos desenvolver a versão jSON do servidor. Testaremos as interfaces de serviço do servidor, uma após a outra, com o cliente Postman. Esse método nos permite construir a estrutura do servidor web sem nos preocuparmos com as visualizações (=HTML) do aplicativo;
- depois de testar o servidor jSON com o Postman, vamos testá-lo com um cliente de console;
- em seguida, passaremos para a versão XML do servidor. Vimos que a transição do jSON para o XML foi simples;
- por fim, passaremos para a versão HTML do servidor. Construiremos uma arquitetura MVC e definiremos as visualizações a serem exibidas. O aplicativo HTML será testado tanto com o cliente Postman quanto com um navegador convencional;
30.3. A estrutura do código do servidor

- em [1: o servidor web como um todo;
- em [2]: por enquanto, ignoraremos as pastas [static, templates, tests_views], que dizem respeito à versão HTML do servidor. Fora dessa pasta, encontraremos o script principal [main] e sua configuração;
- em [3], os controladores do servidor web. Trata-se de instâncias de classes;
![]() | ![]() |
- no [4], a resposta HTTP do servidor será gerenciada por classes;
- em [5], mantemos o arquivo de logs dos servidores anteriores;
Quando criarmos a versão HTML do servidor, outras pastas serão utilizadas:
![]() | ![]() |
- na versão [6], os elementos estáticos do aplicativo HTML;
- em [7], os modelos da aplicação HTML, decompostos em visualizações [9] e em fragmentos de visualização [8];
- em [9], as classes que implementam os modelos das visualizações;
30.4. Os URL do serviço do aplicativo
Para construir o servidor web, procederemos da seguinte maneira:
- a partir das visualizações da aplicação HTML, definiremos as ações que a aplicação web deve implementar. Aqui, utilizaremos as visualizações reais, mas poderiam ser simplesmente esboços no papel;
- a partir dessas ações, definiremos os URL de serviço da aplicação HTML;
- vamos implementar essas URL de serviço com um servidor que forneça jSON. Isso permite definir a estrutura do servidor web sem nos preocuparmos com as páginas HTML a serem fornecidas. Testaremos esses serviços URL com o Postman;
- em seguida, testaremos nosso servidor jSON com um cliente de console;
- assim que o servidor jSON for validado, passaremos à programação do aplicativo HTML;
A primeira tela será a de autenticação:

- a ação que leva a essa primeira tela se chamará [init-session] [1];
- ao clicar no botão [Valider], será acionada a ação [authentifier-utilisateur] com dois parâmetros enviados: [2-3];
A visualização do cálculo do imposto:

- em [1], a ação [authentifier-utilisateur] que levou a esta visualização;
- em [2], o clique no botão [Valider] aciona a execução da ação [calculer-impot] com três parâmetros passados [2-5];
- clicar no link [6] aciona a ação [lister-simulations] sem parâmetros;
- o clique no link [7] aciona a ação [fin-session] sem parâmetros;
A terceira visualização é a das simulações realizadas pelo usuário autenticado:

- em [3], a ação [lister-simulations] que levou a esta visualização;
- em [2], um clique no link [Supprimer] aciona a ação [supprimer-simulation] com um parâmetro: o número da simulação a ser excluída da lista;
- um clique no link [3] aciona a ação [afficher-calcul-impot] sem parâmetros, que exibe novamente a tela de cálculo do imposto;
- um clique no link [4] aciona a ação [fin-session] sem parâmetros;
Com essas informações iniciais, podemos definir as diferentes ações de serviço do servidor URL:
Ação | Função | Contexto de execução |
/init-session | Serve para definir o tipo (json, xml, html) das respostas desejadas | Solicitação GET Pode ser emitida a qualquer momento |
/autenticar-usuário | Autoriza ou não um usuário a fazer login | Solicitação POST. A solicitação deve conter dois parâmetros enviados por POST [user, password] Só pode ser emitida se o tipo da sessão (json, xml, html) for conhecido |
/calcular-imposto | Realiza uma simulação de cálculo de imposto | Solicitação POST. A solicitação deve conter três parâmetros enviados via POST: [marié, enfants, salaire] Só pode ser emitida se o tipo da sessão (json, xml, html) for conhecido e o usuário estiver autenticado |
/listar-simulações | Solicita a exibição da lista de simulações realizadas desde o início da sessão | Solicitação GET. Só pode ser enviada se o tipo da sessão (json, xml, html) for conhecido e o usuário estiver autenticado |
/excluir-simulação/número | Exclui uma simulação da lista de simulações | Solicitação GET. Só pode ser emitida se o tipo da sessão (json, xml, html) for conhecido e o usuário estiver autenticado |
/exibir-cálculo-imposto | Exibe a página HTML do cálculo do imposto | Solicitação GET. Só pode ser emitida se o tipo da sessão (json, xml, html) for conhecido e o usuário estiver autenticado |
/fim-sessão | Encerra a sessão de simulações. | Tecnicamente, a sessão web anterior é excluída e uma nova sessão é criada Só pode ser emitida se o tipo da sessão (json, xml, html) for conhecido e o usuário estiver autenticado |
Esses diferentes códigos de serviço URL serão utilizados tanto para o servidor HTML quanto para os servidores jSON ou XML. Duas URL serão utilizadas apenas para esses dois últimos servidores: trata-se das URL da versão anterior do cliente/servidor web que retomamos aqui:
Ação | Função | Contexto de execução |
/get-admindata | Retorna os dados fiscais que permitem o cálculo do imposto | Consulta GET. É utilizada apenas se o tipo da sessão for json ou xml. O usuário deve estar autenticado |
/calcular-impostos | Realiza o cálculo do imposto de uma lista de contribuintes enviada na solicitação jSON | Consulta GET. Utilizada apenas se o tipo da sessão for json ou xml. O usuário deve estar autenticado |
Todos os controladores associados a essas ações procederão da mesma forma:
- verificarão seus parâmetros. Estes são encontrados no objeto:
- [request.path] para os parâmetros presentes no URL na forma [/action/param1/param2/…];
- no objeto [request.form] para aqueles que são transmitidos no [x-www-form-urlencoded] no corpo da solicitação;
- no objeto [request.data] para aqueles que são transmitidos no jSON no corpo da solicitação;
- um controlador é semelhante a uma função ou método que verifica a validade de seus parâmetros. No caso do controlador, porém, é um pouco mais complicado:
- os parâmetros esperados podem estar ausentes;
- os parâmetros recuperados pelo controlador são cadeias de caracteres. Se o parâmetro esperado for um número, o controlador deve verificar se a cadeia de caracteres do parâmetro corresponde, de fato, a um número;
- uma vez verificado que os parâmetros esperados estão presentes e são sintaticamente corretos, é preciso verificar se eles são válidos no contexto de execução atual. Esse contexto está presente na sessão. O exemplo da autenticação é um exemplo de contexto de execução. Certas ações só devem ser processadas após a autenticação do cliente. Geralmente, uma chave na sessão indica se essa autenticação ocorreu ou não;
- uma vez realizadas as verificações anteriores, o controlador secundário pode operar. Esse trabalho de verificação dos parâmetros é muito importante. Não se pode aceitar que um cliente nos envie qualquer coisa em qualquer momento da vida útil do aplicativo. É preciso controlar totalmente a vida útil do aplicativo;
- uma vez concluído seu trabalho, o controlador secundário retorna um dicionário com as chaves [action, état, réponse] ao controlador principal que o chamou:
- [action] é a ação que acaba de ser executada;
- [état] é um número de três dígitos que indica o resultado do processamento da ação:
- [x00] indicará que o processamento foi bem-sucedido;
- [x01] indicará que o processamento falhou;
- [réponse] é o dicionário de resultados na forma {‘resposta’:objeto}. O objeto terá estruturas diferentes dependendo da ação processada;
Vamos agora examinar os diferentes controladores — ou, o que dá no mesmo, as diferentes ações que esses controladores processam e que definem o ritmo de funcionamento do aplicativo web.
30.5. Configuração do servidor

A configuração do banco de dados [config_database], bem como a das camadas do servidor [config_layers], são idênticas às das versões anteriores. O arquivo [config] apresenta novas informações:
def configure(config: dict) -> dict:
import os
# etapa 1 ------
# pasta deste arquivo
script_dir = os.path.dirname(os.path.abspath(__file__))
# caminho raiz
root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"
# dependências
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",
# Logger, SendAdminMail
f"{root_dir}/impots/http-servers/02/utilities",
# scripts [config_database, config_layers]
script_dir,
# controladores
f"{script_dir}/../controllers",
# respostas HTTP
f"{script_dir}/../responses",
# modelos de visualização
f"{script_dir}/../models_for_views",
]
# definimos o syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# dependências do servidor web
# os controladores
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
# respostas HTTP
from HtmlResponse import HtmlResponse
from JsonResponse import JsonResponse
from XmlResponse import XmlResponse
# os modelos das visualizações
from ModelForAuthentificationView import ModelForAuthentificationView
from ModelForCalculImpotView import ModelForCalculImpotView
from ModelForErreursView import ModelForErreursView
from ModelForListeSimulationsView import ModelForListeSimulationsView
# etapa 2 ------
# configuração do aplicativo
config.update({
# usuários autorizados a usar o aplicativo
"users": [
{
"login": "admin",
"password": "admin"
}
],
# arquivo de logs
"logsFilename": f"{script_dir}/../data/logs/logs.txt",
# configuração do servidor SMTP
"adminMail": {
# servidor SMTP
"smtp-server": "localhost",
# porta do servidor SMTP
"smtp-port": "25",
# administrador
"from": "guest@localhost.com",
"to": "guest@localhost.com",
# assunto do e-mail
"subject": "plantage du serveur de calcul d'impôts",
# tls definido como True se o servidor SMTP exigir autenticação; caso contrário, definido como False
"tls": False
},
# duração da pausa do thread em segundos
"sleep_time": 0,
# ações permitidas e seus controladores
"controllers": {
# inicialização de uma sessão de cálculo
"init-session": InitSessionController(),
# autenticação de um usuário
"authentifier-utilisateur": AuthentifierUtilisateurController(),
# cálculo do imposto no modo individual
"calculer-impot": CalculerImpotController(),
# cálculo do imposto no modo em lote
"calculer-impots": CalculerImpotsController(),
# lista de simulações
"lister-simulations": ListerSimulationsController(),
# exclusão de uma simulação
"supprimer-simulation": SupprimerSimulationController(),
# encerramento da sessão de cálculo
"fin-session": FinSessionController(),
# exibição da tela de cálculo do imposto
"afficher-calcul-impot": AfficherCalculImpotController(),
# obtenção dos dados da administração fiscal
"get-admindata": GetAdminDataController(),
# controlador principal
"main-controller": MainController()
},
# os diferentes tipos de resposta (json, xml, html)
"responses": {
"json": JsonResponse(),
"html": HtmlResponse(),
"xml": XmlResponse()
},
# as visualizações HTML e seus modelos dependem do status retornado pelo controlador
"views": [
{
# visualização de autenticação
"états": [
# /init-session (sucesso)
700,
# falha na autenticação do usuário
201
],
"view_name": "views/vue-authentification.html",
"model_for_view": ModelForAuthentificationView()
},
{
# visualização do cálculo do imposto
"états": [
# /autenticar-usuário bem-sucedido
200,
# /calcular-imposto bem-sucedido
300,
# /calcular-imposto falha
301,
# /exibir-cálculo-de-imposto
800
],
"view_name": "views/vue-calcul-impot.html",
"model_for_view": ModelForCalculImpotView()
},
{
# visualização da lista de simulações
"états": [
# /listar-simulações
500,
# /excluir-simulação
600
],
"view_name": "views/vue-liste-simulations.html",
"model_for_view": ModelForListeSimulationsView()
}
],
# visualização de erros inesperados
"view-erreurs": {
"view_name": "views/vue-erreurs.html",
"model_for_view": ModelForErreursView()
},
# redirecionamentos
"redirections": [
{
"états": [
400, # /encerramento-da-sessão-bem-sucedido
],
# redirecionamento para
"to": "/init-session/html",
}
],
}
)
# etapa 3 ------
# configuração do banco de dados
import config_database
config["database"] = config_database.configure(config)
# etapa 4 ------
# instanciação das camadas do aplicativo
import config_layers
config['layers'] = config_layers.configure(config)
# salvando a configuração
return config
- até a linha 41, encontramos elementos clássicos;
- linhas 43-66: ao chegar à linha 43, o Python Path do servidor é definido. É possível, 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 do aplicativo com uma série de constantes;
- linhas 71-98: já conhecemos essas linhas, encontradas nas 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 gerenciar essa ação. Cada controlador é instanciado apenas uma vez (singleton). A mesma instância será executada por diferentes threads do servidor. Portanto, é preciso prestar atenção aos dados compartilhados que cada controlador possa querer modificar;
- linhas 125-129: o dicionário das três respostas possíveis HTTP:
- as chaves correspondem ao tipo de resposta desejado pelo cliente (jSON, xml, html);
- os valores são uma instância da resposta HTTP. Cada gerador de resposta é instanciado apenas uma vez (singleton). O mesmo gerador será executado por diferentes threads do servidor. Portanto, é preciso prestar atenção aos dados compartilhados que cada gerador possa querer modificar;
- linhas 132-186: configuração das visualizações HTML. Por enquanto, ignoramos essas linhas;
- linhas 191-202: já nos deparamos com essas linhas nas versões anteriores;
30.6. Percurso de uma solicitação do cliente dentro do servidor

Vamos acompanhar o percurso de uma solicitação do cliente que chega ao servidor até a resposta HTTP enviada de volta. Ela segue o percurso do servidor MVC.
30.6.1. O script [main]

O script [main] é idêntico em muitos aspectos ao das versões anteriores. No entanto, apresentamos-o na íntegra para que você tenha uma boa base de partida:
# aguardando um parâmetro mysql ou pgres
import sys
syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
sgbd = sys.argv[1].lower()
erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
# configurando a aplicação
import config
config = config.configure({'sgbd': sgbd})
# dependências
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
# envio de um e-mail ao administrador
def send_adminmail(config: dict, message: str):
# envio de um e-mail ao administrador da aplicação
config_mail = config["adminMail"]
config_mail["logger"] = config['logger']
SendAdminMail.send(config_mail, message)
# verificação do arquivo de logs
logger = None
erreur = False
message_erreur = None
try:
# registrador
logger = Logger(config["logsFilename"])
except BaseException as exception:
# log da console
print(f"L'erreur suivante s'est produite : {exception}")
# registra-se o erro
erreur = True
message_erreur = f"{exception}"
# o registrador de logs é armazenado na configuração
config['logger'] = logger
# gerenciamento do erro
if erreur:
# e-mail para o administrador
send_adminmail(config, message_erreur)
# fim da aplicação
sys.exit(1)
# log de inicialização
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)
# 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().asdict()
# 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)
# o thread principal não precisa mais do logger
logger.close()
# se houver erro, o programa é encerrado
if erreur:
sys.exit(2)
# aplicativo Flask
app = Flask(__name__, template_folder="templates", static_folder="static")
# chave secreta da sessão
app.secret_key = os.urandom(12).hex()
# o front controller
def front_controller() -> tuple:
# processamos a solicitação
logger = None
…
@app.route('/', methods=['GET'])
def index() -> tuple:
# redirecionamento para /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:
# executando o controlador associado à ação
return front_controller()
# autenticar-usuário
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
# executa-se o controlador associado à ação
return front_controller()
# calcular-imposto
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
# executa-se o controlador associado à ação
return front_controller()
# listar-simulações
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
# executa-se o controlador associado à ação
return front_controller()
# excluir-simulação
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
# executa-se o controlador associado à ação
return front_controller()
# fim-sessão
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
# executa-se o controlador associado à ação
return front_controller()
# exibir-cálculo-imposto
@app.route('/afficher-calcul-impot', methods=['GET'])
def afficher_calcul_impot() -> tuple:
# executa-se o controlador associado à ação
return front_controller()
# obter-dados-administrativos
@app.route('/get-admindata/<int:numero>', methods=['GET'])
def get_admindata() -> tuple:
# é executado o controlador associado à ação
return front_controller()
# somente main
if __name__ == '__main__':
# inicia-se o servidor
app.config.update(ENV="development", DEBUG=True)
app.run(threaded=True)
- linhas 1-92: todas essas linhas já foram abordadas e explicadas;
- linha 92: o servidor irá gerenciar uma sessão. Portanto, precisamos de uma chave secreta. Colocaremos, para cada usuário, duas informações na sessão:
- se o usuário se autenticou corretamente;
- sempre que ele fizer um cálculo de imposto, os resultados desse cálculo serão colocados em uma lista que chamaremos de lista de simulações do usuário. Essa lista será armazenada na sessão;
- linhas 100-151: a lista de URL de serviço do servidor. As funções associadas servem como filtro: todas as URL que não estiverem presentes nessa lista serão rejeitadas pelo servidor Flask com o erro [404 NOT FOUND]. Depois de passar por essa filtragem, a solicitação é sistematicamente encaminhada a um “Front Controller” implementado pela função [front_controller] das linhas 94-98, que apresentaremos em breve;
- linhas 100 a 103: gerenciamento da rota [/]. O ponto de entrada do aplicativo web será a função URL da linha 107. Assim, na linha 103, redirecionamos o cliente para essa função URL:
- a função [url_for] é importada na linha 18. Ela possui aqui dois parâmetros:
- o primeiro parâmetro é o nome de uma das funções de roteamento, neste caso a da linha 107. Vemos que essa função espera um parâmetro [type_response], que é o tipo (json, xml, html) de resposta desejado pelo cliente;
- o segundo parâmetro retoma 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 o URL associado à função designada pelos dois parâmetros que lhe foram fornecidos. Aqui, isso resultará no URL da linha 106, onde o parâmetro é substituído por seu valor [/init-session/html];
- a função [redirect] foi importada na linha 18. Sua função é enviar um cabeçalho de redirecionamento HTTP ao cliente:
- o primeiro parâmetro é o URL para o qual o cliente deve ser redirecionado;
- o segundo parâmetro é o código de status 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 a 98, realiza os primeiros processamentos da solicitação do cliente:
# o controlador frontal
def front_controller() -> tuple:
# processa-se a solicitação
logger = None
try:
# registrador
logger = Logger(config["logsFilename"])
# armazenando-a em uma configuração associada à thread
thread_config = {"logger": logger}
thread_name = threading.current_thread().name
config[thread_name] = {"config": thread_config}
# a solicitação é registrada no log
logger.write(f"[ front_controller] requête : {request}\n")
# interrompe-se o thread, caso tenha sido solicitado
sleep_time = config["sleep_time"]
if sleep_time != 0:
# a pausa é aleatória para que alguns threads sejam interrompidos e outros não
aléa = randint(0, 1)
if aléa == 1:
# registro antes da pausa
logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
# pausa
time.sleep(sleep_time)
# a solicitação é encaminhada ao controlador principal
main_controller = config['controllers']["main-controller"]
résultat, status_code = main_controller.execute(request, session, config)
# registra-se o resultado enviado ao cliente
log = f"[front_controller] {résultat}\n"
logger.write(log)
# houve algum erro fatal?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# envia-se um e-mail ao administrador do aplicativo
send_adminmail(config, log)
# determina-se o tipo desejado para a resposta
if session.get('typeResponse') is None:
# o tipo de sessão ainda não foi definido — será jSON
type_response = 'json'
else:
type_response = session['typeResponse']
# construímos a resposta a ser enviada
response_builder = config["responses"][type_response]
response, status_code = response_builder \
.build_http_response(request, session, config, status_code, résultat)
# enviando a resposta
return response, status_code
except BaseException as erreur:
# trata-se de um erro inesperado — registra-se o erro, se possível
if logger:
logger.write(f"[ front_controller] {erreur}")
# prepara-se a resposta para o cliente
résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
# envia-se uma resposta em jSON
return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
finally:
# fecha-se o arquivo de logs caso ele tenha sido aberto
if logger:
logger.close()
- linhas 1 a 57: já conhecemos esse código. Era, por exemplo, o código da função chamada [main] no script [main] da versão anterior. Há apenas um detalhe a ser observado: o controlador utilizado nas linhas 25 e 26:
- linha 25: recuperamos na configuração a instância do controlador associada ao nome [main-controller]. Trata-se das seguintes linhas:
# dependências do servidor web
# os controladores
…
from MainController import MainController
# ações autorizadas e seus controladores
"controllers": {
…,
# controlador principal
"main-controller": MainController()
},
- (continuação)
- na linha 10 acima, observe-se que se recupera uma instância de classe;
- linha 26: solicita-se ao controlador [MainController] que processe a solicitação;
- linhas 30-45: a resposta retornada pelo controlador [MainController] é enviada ao cliente. Voltaremos a essas linhas um pouco mais adiante;
A função da função [front_controller] e, em seguida, da classe [MainController] é realizar o trabalho comum a todas as solicitações:
No esquema acima, ainda estamos na fase 1 do processamento da solicitação. O controlador principal [MainController] dará continuidade à etapa 1.
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]:

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 único método [execute] na linha 8. Esse método recebe três parâmetros:
- [request]: a solicitação do cliente;
- [session]: a sessão do cliente;
- [config]: a configuração do aplicativo;
O método [execute] retorna uma tupla de dois elementos:
- o primeiro é o dicionário de resultados na forma {‘ação’: ação, ‘estado’: estado, ‘resposta’: resultados};
- o segundo é o código de status HTTP a ser retornado ao cliente;
O controlador principal [MainController] [1] implementa a interface [InterfaceController] da seguinte maneira:
# importação de dependências
from flask_api import status
from werkzeug.local import LocalProxy
# controladores do aplicativo web
from InterfaceController import InterfaceController
class MainController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# recuperamos os elementos do caminho
params = request.path.split('/')
action = params[1]
# erros
erreur = False
# o tipo de sessão deve ser conhecido antes de determinadas ações
type_response = session.get('typeResponse')
if type_response is None and action != "init-session":
# registramos o erro
résultat = {"action": action, "état": 101,
"réponse": ["pas de session en cours. Commencer par action [init-session]"]}
erreur = True
# para determinadas ações, é necessário estar autenticado
user = session.get('user')
if not erreur and user is None and action not in ["init-session", "authentifier-utilisateur"]:
# o erro é registrado
résultat = {"action": action, "état": 101,
"réponse": [f"action [{action}] demandée par utilisateur non authentifié"]}
erreur = True
# há erros?
if erreur:
# envia-se uma mensagem de erro
return résultat, status.HTTP_400_BAD_REQUEST
else:
# é executado o controlador associado à ação
controller = config["controllers"][action]
résultat, status_code = controller.execute(request, session, config)
return résultat, status_code
O controlador [MainController] realiza as primeiras verificações da validade da solicitação.
- linhas 11-13: o controlador começa recuperando a ação solicitada pelo cliente. Vale lembrar que os URL de serviço têm o formato [/action/param1/param2/…] e que este URL está em [request.path];
- linhas 17-23: a ação [init-session] serve para inicializar o tipo de resposta (json, xml, html) desejado pelo cliente. Essa informação é armazenada na sessão associada à chave [typeRéponse]. Portanto, se a ação não for [init-session], a sessão deve conter a chave [typeRéponse]; caso contrário, a solicitação está incorreta;
- linhas 21-22: a estrutura do resultado retornado por cada controlador, neste caso, um resultado de erro:
- [action]: é o nome da ação em andamento. Isso permitirá obter seu nome ao registrar o resultado da solicitação;
- [état]: é um código de status de três dígitos:
- [x00] para sucesso;
- [x01] para falha;
- [réponse]: é a resposta à consulta. Sua natureza é específica para cada consulta;
- linhas 24-30: a ação [authentifier-utilisateur] serve para autenticar o usuário. Se for bem-sucedida, uma chave [user=True] é inserida na sessão do usuário. Algumas ações de serviço URL só são acessíveis por um usuário autenticado. É isso que se verifica aqui;
- linha 26: apenas as ações [init-session] e [authentifier-utilisateur] podem ser executadas por um usuário ainda não autenticado;
- linhas 28-29: o resultado a ser enviado em caso de erro;
- linhas 32-34: se ocorrer um dos dois erros anteriores, envia-se a resposta de erro ao cliente com o status HTTP 400 BAD REQUEST;
- linhas 35-39: se não houve erro, passa-se o controle para o controlador responsável por processar a ação em andamento. Sua instância é encontrada na configuração do aplicativo;
A classe [MainController] dá continuidade ao trabalho da função [front_controller]: juntas, elas reúnem tudo o que pode ser fatorizado no processamento das solicitações, aguardando o último momento para repassar a solicitação a um controlador específico. A divisão do código entre a função [front_controller] e a classe [MainController] é totalmente subjetiva. Aqui, quis manter o que já havia sido feito na 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 optaria por essa solução, pois ela tem a vantagem de tornar o código do script principal [main] mais leve;
30.7. Processamento específico para uma ação
Voltemos à arquitetura MVC do aplicativo:

Ainda estamos na etapa 1 acima. Se não houver erros, a etapa 2 será iniciada. A solicitação foi encaminhada ao controlador específico da ação solicitada pela solicitação. Suponhamos que essa ação seja [/init-session], definida pela rota:
# inicialização da sessão
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
# é executado o controlador associado à ação
return front_controller()
Esta ação está vinculada a um controlador na configuração [config]:
# ações autorizadas e seus controladores
"controllers": {
# inicialização de uma sessão de cálculo
"init-session": InitSessionController(),
…
},
O controlador [InitSessionController] (linha 4) assume, portanto, o controle. 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):
# recuperam-se os elementos do caminho
dummy, action, type_response = request.path.split('/')
# inicialmente, sem erros
erreur = False
# verificação do tipo de resposta
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"]}
# se não houver erro
if not erreur:
# coloca-se o tipo da sessão na sessão do Flask
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: assim como os outros controladores, o controlador [InitSessionController] implementa a interface [InterfaceController];
- linha 10: o URL é do tipo [/init-session/type_response]. Recupera-se a ação [init-session] e o tipo de resposta desejado;
- linha 15: o tipo de resposta desejado só pode ser um dos que constam na configuração das respostas:
# os diferentes tipos de resposta (json, xml, html)
"responses": {
"json": JsonResponse(),
"html": HtmlResponse(),
"xml": XmlResponse()
},
- caso contrário, prepara-se uma resposta de erro 701 (linha 17);
- linhas 20-25: caso em que o tipo de resposta desejado seja válido;
- linha 22: o tipo de resposta desejado é armazenado na sessão. De fato, será necessário lembrá-lo para as solicitações que se seguirão;
- linhas 23-24: prepara-se uma resposta de sucesso 700;
- linha 25: a resposta de sucesso é retornada ao código chamador;
- linha 27: se houver erro, a resposta de erro é retornada ao código chamador;
30.8. Elaboração da resposta HTTP do servidor
Voltemos à arquitetura MVC do aplicativo:

Acabamos de ver as etapas 1 e 2. Encontramos três códigos de status:
- 700: /init-session foi bem-sucedida;
- 701: /init-session falhou;
- 101: solicitação inválida, seja porque a sessão não foi inicializada, seja porque o usuário não está autenticado;
Vamos examinar como a resposta do servidor será enviada ao cliente durante a etapa 3 acima. Isso ocorre na função [front_controller] do script [main]:
# o front controller
def front_controller() -> tuple:
# processamos a solicitação
logger = None
try:
# registrador
logger = Logger(config["logsFilename"])
# armazenamos em uma configuração associada ao thread
thread_config = {"logger": logger}
thread_name = threading.current_thread().name
config[thread_name] = {"config": thread_config}
# a solicitação é registrada no log
logger.write(f"[ front_controller] requête : {request}\n")
# interrompe-se o thread, caso tenha sido solicitado
sleep_time = config["sleep_time"]
if sleep_time != 0:
# a pausa é aleatória para que alguns threads sejam interrompidos e outros não
aléa = randint(0, 1)
if aléa == 1:
# registro antes da pausa
logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
# pausa
time.sleep(sleep_time)
# a solicitação é encaminhada ao controlador principal
main_controller = config['controllers']["main-controller"]
résultat, status_code = main_controller.execute(request, session, config)
# registra-se o resultado enviado ao cliente
log = f"[front_controller] {résultat}\n"
logger.write(log)
# houve algum erro fatal?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# envia-se um e-mail ao administrador do aplicativo
send_adminmail(config, log)
# determina-se o tipo desejado para a resposta
if session.get('typeResponse') is None:
# o tipo de sessão ainda não foi definido — será jSON
type_response = 'json'
else:
type_response = session['typeResponse']
# construímos a resposta a ser enviada
response_builder = config["responses"][type_response]
response, status_code = response_builder \
.build_http_response(request, session, config, status_code, résultat)
# enviando a resposta
return response, status_code
except BaseException as erreur:
# trata-se de um erro inesperado — registra-se o erro, se possível
if logger:
logger.write(f"[ front_controller] {erreur}")
# prepara-se a resposta para o cliente
résultat = {"réponse": {"erreurs": [f"{erreur}"]}}
# envia-se uma resposta em jSON
return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
finally:
# fecha-se o arquivo de logs, caso tenha sido aberto
if logger:
logger.close()
- estamos na linha 26: o controlador principal retornou sua resposta de erro;
- linhas 27-29: independentemente da resposta do controlador principal (sucesso ou falha), essa resposta é registrada no arquivo de logs;
- linhas 30-33: assim como nas versões anteriores, se o status HTTP for [500 INTERNAL SERVER ERROR], enviamos um e-mail ao administrador do aplicativo com o log do erro;
- linhas 34-39: enviaremos a resposta HTTP e o resultado retornado pelo controlador será inserido no corpo dessa resposta. Precisamos saber em que formato (json, xml, html) o cliente deseja essa resposta. Procuramos o tipo de resposta desejado na sessão. Se não estiver lá, definimos arbitrariamente esse tipo como jSON;
- linhas 40-43: a resposta HTTP é construída;
No arquivo de configuração, cada tipo de resposta (json, xml, html) foi associado a uma instância de classe:
# os diferentes tipos de resposta (json, xml, html)
"responses": {
"json": JsonResponse(),
"html": HtmlResponse(),
"xml": XmlResponse()
},
As classes de respostas estão na pasta [responses] da estrutura de diretórios do servidor:

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]: são os parâmetros recebidos pelo controlador da ação;
- [résultat, status_code]: são os resultados produzidos pelo controlador da ação;
Apresentaremos a resposta jSON. Ela é 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):
# resultados: o dicionário de resultados
# status_code: o código de status da resposta HTTP
# retornamos a resposta HTTP
response = make_response(json.dumps(résultat, ensure_ascii=False))
response.headers['Content-Type'] = 'application/json; charset=utf-8'
return response, status_code
Conhecemos esse código, com o qual já nos deparamos várias vezes. Trata-se do código da função [json_response] do módulo [myutils].
30.9. Primeiros testes
No código analisado, encontramos três códigos de estado:
- 700: /init-session foi bem-sucedida;
- 701: /init-session falhou;
- 101: solicitação inválida, seja porque a sessão não foi inicializada, seja porque o usuário não está autenticado;
Vamos tentar obtê-los com uma sessão jSON.
- Iniciamos o servidor web, o SGBD e o servidor de e-mails;
- iniciamos um cliente Postman;
Teste 1
Primeiramente, mostramos uma solicitação inválida porque a sessão não foi inicializada:

- [1-2]: a consulta [POST http://localhost:5000/authentifier-utilisateur] é válida:
# autenticar usuário
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
# é executado o controlador associado à ação
return front_controller()
mas ela só é aceita se a sessão tiver sido inicializada previamente com a ação [/init-session].
Vamos executar a consulta e ver o resultado enviado pelo servidor:

- [1-2]: obtivemos uma resposta jSON. Quando o tipo de resposta ainda não foi definido pelo cliente, o servidor usa o jSON para responder;
- [3-5]: o dicionário jSON da resposta;
- [action]: a ação que foi executada;
- [état]: o código de status da resposta. Um código [x01] indica um erro;
- [réponse]: é adaptada a cada ação. Aqui, ela contém uma mensagem de erro;
Agora, vamos inicializar uma sessão com um tipo de resposta incorreto:

- [1-2] é uma rota correta:
# inicializar sessão
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
# executa-se o controlador associado à ação
return front_controller()
Portanto, ela entrará no fluxo de processamento de solicitações do servidor MVC. No entanto, ela deverá ser rejeitada durante esse processamento, pois o tipo de sessão solicitado está incorreto.
A resposta é a seguinte:

- em [4], um código de erro [x01];
- em [5], a explicação do erro;
Agora, vamos inicializar uma sessão jSON:

A resposta é a seguinte:

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):
# resultados: o dicionário de resultados
# status_code: o código de status da resposta HTTP
# resultado: o dicionário a ser transformado em string XML
xml_string = xmltodict.unparse({"root": résultat})
# geramos a resposta HTTP
response = make_response(xml_string)
response.headers['Content-Type'] = 'application/xml; charset=utf-8'
return response, status_code
Esse é um código que conhecemos, o da função [xml_response] do módulo compartilhado [myutils].
Inicializamos uma sessão XML:

O resultado do servidor é, então, o seguinte:

Obtemos a mesma resposta que em jSON, mas, desta vez, a resposta está formatada como XML.
30.10. A ação [authentifier-utilisateur]
A ação [authentifier-utilisateur] permite autenticar um usuário que deseja utilizar o aplicativo de cálculo de impostos. Sua rota é definida da seguinte forma no script [main]:
# autenticar usuário
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
# executa-se o controlador associado à ação
return front_controller()
O servidor aguarda dois parâmetros enviados via POST:
- [user]: o identificador do usuário;
- [password]: sua senha;
A lista de usuários autorizados é definida na configuração [config]:
# usuários autorizados a usar o aplicativo
"users": [
{
"login": "admin",
"password": "admin"
}
],
Aqui, temos uma lista com um único elemento.
A ação [authentifier-utilisateur] é processada pelo controlador [AuthentifierUtilisateurController] a seguir:
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):
# recuperam-se os elementos do caminho
dummy, action = request.path.split('/')
# os parâmetros do POST
post_params = request.form
# código de status da resposta HTTP
status_code = None
# inicialmente, sem erros
erreur = False
erreurs = []
# é necessário um POST com dois parâmetros
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:
# recuperam-se os parâmetros do POST
# parâmetro [user]
user = post_params.get("user")
if user is None:
erreur = True
erreurs.append("paramètre [user] manquant")
# parâmetro [password]
password = post_params.get("password")
if password is None:
erreur = True
erreurs.append("paramètre [password] manquant")
# erro?
if erreur:
status_code = status.HTTP_400_BAD_REQUEST
# erro?
if not erreur:
# verificando a validade do par (usuário, senha)
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
# encontrado?
if not trouvé:
# registrando o erro
erreur = True
status_code = status.HTTP_401_UNAUTHORIZED
erreurs.append(f"Echec de l'authentification")
else:
# registra-se na sessão que o usuário foi encontrado
session["user"] = True
# pronto
if not erreur:
# retorno sem erro
résultat = {"action": action, "état": 200, "réponse": f"Authentification réussie"}
return résultat, status.HTTP_200_OK
else:
# retorno com erro
return {"action": action, "état": 201, "réponse": erreurs}, status_code
- linha 14: recuperam-se os parâmetros do POST;
- linha 19: a lista de erros encontrados na solicitação;
- linhas 20-24: verifica-se se há de fato dois parâmetros enviados;
- linhas 27-31: verifica-se a presença de um parâmetro [users];
- linhas 32-36: verifica-se a presença do parâmetro [password];
- linhas 38-39: se os parâmetros enviados estiverem incorretos, prepara-se uma resposta HTTP 400 BAD REQUEST;
- linhas 40-58: verifica-se se as credenciais [user, password] pertencem a um usuário autorizado a utilizar o aplicativo;
- linhas 51-55: se o usuário (user, password) não estiver autorizado a usar o aplicativo, prepara-se uma resposta HTTP 401 UNAUTHORIZED;
- linhas 56-58: se ele estiver autorizado, registra-se com a chave [user] na sessão que ele se autenticou;
Observe que, se o usuário estava autenticado com as credenciais [identifiants1] e não consegue se autenticar com as credenciais [identifiants2], ele continua, no entanto, autenticado com as credenciais [identifiants1].
Vamos fazer alguns testes no Postman:
- iniciamos o servidor web, o SGBD e o servidor de e-mails;
- com o cliente Postman:
- iniciamos uma sessão com jSON;
- em seguida, fazemos a autenticação;
Veja a seguir diferentes casos.
Caso 1: POST sem parâmetros enviados

- em [3-5], o POST não possui corpo;
O resultado da consulta é o seguinte:

- em [2], obtivemos uma resposta HTTP 400 BAD REQUEST;
- ao inserir [5], obtivemos um código de erro [201];
Caso 2: POST com credenciais incorretas

- em [6], os identificadores estão incorretos;
O servidor envia a seguinte resposta:

- em [2], a resposta HTTP 401 UNAUTHORIZED;
- em [5], a resposta de erro;
Caso 2: POST com credenciais corretas

- em [6], as credenciais estão corretas;
A resposta do servidor é a seguinte:
- em [2], uma resposta HTTP 200 OK;
- em [5], a resposta de sucesso;
30.11. A ação [calculer_impot]
A ação [calculer_impot] permite calcular o imposto de um contribuinte. Seu caminho é definido da seguinte forma no script [main]:
# calcular-impostos
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
# executando o controlador associado à ação
return front_controller()
O servidor aguarda três parâmetros enviados por POST:
- [marié]: sim / não;
- [enfants]: número de filhos do contribuinte;
- [salaire]: salário anual do contribuinte;
O controlador [CalculerImpotController] processa a ação [calculer_impot]:
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):
# recuperamos os elementos do caminho
dummy, action = request.path.split('/')
# sem erros inicialmente
erreur = False
erreurs = []
# os parâmetros do POST
post_params = request.form
# é necessário um POST com três parâmetros
if len(post_params) != 3:
erreur = True
erreurs.append(
"méthode POST requise avec les paramètres postés [marié, enfants, salaire]")
# analisamos os parâmetros enviados
if not erreur:
# parâmetro casado
marié = post_params.get("marié")
if marié is None:
erreurs.append("paramètre [marié] manquant")
else:
# o parâmetro é válido?
marié = marié.lower()
if marié != "oui" and marié != "non":
erreur = True
erreurs.append(f"valeur [{marié}] invalide pour le paramètre [marié (oui/non)]")
# parâmetro [enfants]
enfants = post_params.get("enfants")
if enfants is None:
erreur = True
erreurs.append("paramètre [enfants] manquant")
else:
# o parâmetro é válido?
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)]")
# parâmetro de salário
salaire = post_params.get("salaire")
if salaire is None:
erreur = True
erreurs.append("paramètre [salaire] manquant")
else:
# o parâmetro é válido?
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)]")
# erro?
if erreur:
status_code = status.HTTP_400_BAD_REQUEST
résultat = {"action": action, "état": 301, "réponse": erreurs}
# retornando o resultado
return résultat, status_code
# cálculo do imposto
# recuperando a camada [métier] e o dicionário [adminData]
métier = config["layers"]["métier"]
admin_data = config["admindata"]
# cálculo do imposto
taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
métier.calculate_tax(taxpayer, admin_data)
# número da simulação
id_simulation = session.get('id_simulation', 0)
id_simulation += 1
session['id_simulation'] = id_simulation
# insere-se o resultado na sessão na forma do dicionário de um TaxPayer
simulation = taxpayer.fromdict({'id': id_simulation}).asdict()
# adiciona-se o resultado à lista de simulações já realizadas e insere-se essa lista na sessão
simulations = session.get("simulations", [])
simulations.append(simulation)
session["simulations"] = simulations
# resultado
résultat = {"action": action, "état": 300, "réponse": simulation}
status_code = status.HTTP_200_OK
# retornamos o resultado
return résultat, status_code
- linha 13: recupera-se o nome da ação em andamento;
- linha 17: acumula-se os erros em uma lista;
- linha 19: recuperam-se os parâmetros lançados. Estes são lançados na forma [x-www-form-urlencoded] e é por isso que são recuperados em [request.form]. Se tivessem sido enviados como jSON, teríamos os recuperado como [request.data];
- linhas 21-24: verifica-se se há de fato três parâmetros enviados;
- linhas 27-36: verificação da presença e da validade do parâmetro enviado [marié];
- linhas 37-48: verificação da presença e da validade do parâmetro enviado [enfants];
- linhas 49-60: verificação da presença e da validade do parâmetro enviado [salaire];
- linhas 62-66: se houver erro, é enviada uma resposta de erro 400 BAD REQUEST com um código de status [301];
- linhas 69-71: se não houver erro, prepara-se o cálculo do imposto. Para isso,
- linha 70: recupera-se uma referência na camada [métier];
- linha 71: recuperam-se os dados da administração fiscal na configuração do servidor;
- linhas 72-74: o imposto do contribuinte é calculado;
- linhas 75-77: conta-se o número de cálculos de imposto realizados pelo usuário;
- linha 76: recupera-se, na sessão, o número do último cálculo realizado. Denomina-se aqui [simulation] o resultado de um cálculo;
- linha 77: incrementa-se o número da última simulação;
- linha 78: esse número é gravado na sessão;
- linhas 79-84: para acompanhar os cálculos realizados pelo usuário, vamos inserir na sessão dele a lista das simulações que ele realizou;
- linha 80: uma simulação será o dicionário de um objeto TaxPayer, cuja propriedade [id] terá como valor o número da simulação;
- linhas 82-84: a simulação atual é adicionada à lista de simulações presente na sessão;
- linhas 86-87: prepara-se uma resposta HTTP indicando sucesso;
- linha 90: o resultado é retornado;
Vamos fazer alguns testes: o servidor web, o SGBD, o servidor de e-mails e um cliente Postman estão em execução.
Caso 1: fazer um cálculo de imposto quando a sessão não está inicializada

A resposta é a seguinte:

Caso 2: realizar um cálculo de imposto sem estar autenticado
Primeiro, inicia-se uma sessão jSON com [/init-session/json]. Em seguida, realiza-se a mesma consulta que anteriormente. A resposta é então a seguinte:

Caso 3: realizar um cálculo de imposto com parâmetros ausentes
Inicializa-se uma sessão jSON, autentica-se e, em seguida, faz-se a seguinte consulta:

- em [5], falta o parâmetro [marié];
A resposta é a seguinte:
Caso 4: realizar um cálculo de imposto com parâmetros incorretos


A resposta do servidor é a seguinte:

Caso 4: realizar um cálculo de imposto com parâmetros corretos

A resposta do servidor é a seguinte:

30.12. A ação [lister-simulations]
A ação [lister-simulations] permite que um usuário consulte a lista de simulações que realizou desde o início da sessão. Seu caminho é definido da seguinte forma no script [main]:
# listar-simulações
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
# executa-se o controlador associado à ação
return front_controller()
O servidor não espera nenhum parâmetro. A ação [lister-simulations] é processada pelo controlador [ListerSimulationsController] a seguir:
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):
# recupera-se os elementos do caminho
dummy, action = request.path.split('/')
# recupera-se a lista de simulações na sessão
simulations = session.get("simulations", [])
# retorna-se o resultado
return {"action": action, "état": 500,
"réponse": simulations}, status.HTTP_200_OK
- linha 13: a lista de simulações é obtida da sessão;
- linhas 15-16: é retornada uma resposta de sucesso;
Vamos realizar o seguinte teste no Postman:
- iniciamos uma sessão jSON;
- fazemos a autenticação;
- fazemos dois cálculos de imposto;
- solicitamos a lista de simulações;
A solicitação é a seguinte:
- em [3], não há nenhum parâmetro;
A resposta do servidor é a seguinte:

- em [4], a lista de simulações do usuário;
30.13. A ação [supprimer-simulation]
A ação [supprimer-simulation] permite que um usuário exclua uma das simulações de sua lista de simulações. Seu caminho é definido da seguinte forma no script [main]:
# excluir-simulação
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
# executa-se o controlador associado à ação
return front_controller()
O servidor espera um único parâmetro: o número da simulação a ser excluída. A ação [supprimer-simulation] é processada pelo controlador [SupprimerSimulationController] a seguir:
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):
# recupera-se os elementos do caminho
dummy, action, numéro = request.path.split('/')
# o parâmetro [numéro] é um número inteiro positivo ou zero, de acordo com sua rota
numéro = int(numéro)
# a simulação com id=número deve existir na lista de simulações
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"
# retorna-se o erro
return {"action": action, "état": 601, "réponse": [msg_erreur]}, status.HTTP_400_BAD_REQUEST
# exclusão da simulação com id=número
simulation = liste_simulations.pop(0)
simulations.remove(simulation)
# as simulações são reinseridas na sessão
session["simulations"] = simulations
# retorna o resultado
return {"action": action, "état": 600, "réponse": simulations}, status.HTTP_200_OK
- linha 10: recuperam-se os dois elementos do caminho da solicitação. Eles são recuperados como cadeias de caracteres;
- linha 13: o parâmetro [numéro] é convertido em inteiro. Sabemos que isso é possível devido à assinatura de sua rota,
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
Além disso, sabemos que se trata de um inteiro >=0. De fato, não é possível ter um URL ou um [/supprimer-simulation/-4]. Esses valores são rejeitados pelo servidor Flask;
- linha 15: recuperamos a lista de simulações da sessão;
- linha 16: com a função [filter], busca-se a simulação cujo id==número. Obtém-se um objeto [filter], que é convertido para o tipo [list];
- linhas 17-20: se o filtro não retornou nada, significa que a simulação a ser excluída não existe. Retorna-se uma resposta de erro indicando isso;
- linhas 21-23: exclui-se a simulação retornada pelo filtro;
- linha 25: insere-se a nova lista de simulações na sessão;
- linha 27: retorna-se, na resposta, a nova lista de simulações;
Fazemos um teste de sucesso e um teste de falha. Realizamos simulações e, em seguida, solicitamos a lista de simulações:

- as simulações têm aqui os números 2 e 3;
Solicitamos a exclusão da simulação com o nº 3.

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


30.14. A ação [fin-session]
A ação [fin-session] permite que um usuário encerre sua sessão de simulações. Seu fluxo é definido da seguinte forma no script [main]:
# fim da sessão
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
# executa-se o controlador associado à ação
return front_controller()
O servidor não espera nenhum parâmetro. A ação é processada pelo controlador [FinSessionController] a seguir:
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):
# recuperam-se os elementos do caminho
dummy, action = request.path.split('/')
# excluímos todas as chaves da sessão atual
session.clear()
# retorna-se o resultado
return {"action": action, "état": 400, "réponse": "session réinitialisée"}, status.HTTP_200_OK
- linha 13: todas as chaves da sessão são removidas. Isso remove:
- [typeResponse]: o tipo das respostas HTTP (json, xml, html);
- [id_simulation]: o número da última simulação realizada;
- [simulations]: a lista de simulações do usuário;
- [user]: o indicador de que o usuário foi autenticado;
- retorna-se a resposta;
Pode-se questionar como a resposta HTTP da linha 15 será retornada, agora que o tipo de resposta não está mais na sessão. Para descobrir isso, é preciso voltar à função |front_controller| do script principal [main] e modificá-la da seguinte maneira:
…
# on not# registra-se o tipo de resposta desejado, caso essa informação esteja na sessão
type_response1 = session.get('typeResponse', None)
# encaminha-se a solicitação ao controlador principal
main_controller = config['controllers']["main-controller"]
résultat, status_code = main_controller.execute(request, session, config)
# registra-se o resultado enviado ao cliente
log = f"[front_controller] {résultat}\n"
logger.write(log)
# ocorreu algum erro fatal?
if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
# envia-se um e-mail ao administrador do aplicativo
send_adminmail(config, log)
# determina-se o tipo desejado para a resposta
type_response2=session.get('typeResponse')
if type_response2 is None and type_response1 is None:
# o tipo de sessão ainda não foi definido — será jSON
type_response = 'json'
elif type_response2 is not None:
# o tipo da resposta é conhecido e está na sessão
type_response = type_response2
else:
type_response=type_response1
# a resposta a ser enviada está sendo construída
response_builder = config["responses"][type_response]
response, status_code = response_builder \
.build_http_response(request, session, config, status_code, résultat)
# a resposta é enviada
return response, status_code
- linha 3: o tipo da resposta atualmente na sessão é armazenado;
- linha 6: a ação é executada. Se for:
- [fin-session], a chave [typeResponse] não está mais na sessão;
- [init-session], a chave [typeResponse] da sessão pode ter mudado de valor;;
- linhas 14-20: deve-se enviar a resposta HTTP. É preciso saber de que forma:
- linhas 16-18: se o tipo da resposta não estiver definido nem por [type_response1] da linha 3, nem por [type_response2] da linha 15, então o tipo de resposta não estava definido nem antes nem depois da ação. Nesse caso, utiliza-se jSON (linha 18);
- linhas 19-21: se [type_response2] existir, o tipo na sessão após a ação, então é esse tipo que deve ser utilizado;
- linhas 22-23: caso contrário, deve-se utilizar [type_response1], o tipo de resposta antes da ação (que é necessariamente [fin-session]);
30.15. A ação [get-admindata]
Abordamos agora as duas respostas URL reservadas para os serviços jSON e XML:
Ação | Função | Contexto de execução |
/get-admindata | Retorna os dados fiscais necessários para o cálculo do imposto | Consulta GET. É utilizada apenas se o tipo da sessão for json ou xml. O usuário deve estar autenticado |
/calcular-impostos | Realiza o cálculo do imposto de uma lista de contribuintes enviada na solicitação jSON | Consulta GET. É utilizada apenas se o tipo da sessão for json ou xml. O usuário deve estar autenticado |
A URL [/get-admindata] é definida nas rotas do script principal [main] da seguinte maneira:
# get-admindata
@app.route('/get-admindata', methods=['GET'])
def get_admindata() -> tuple:
# é executado o controlador associado à ação
return front_controller()
A rota [/get-admindata] é processada pelo controlador [GetAdminDataController] a seguir:
# importação das dependências
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):
# recuperam-se os elementos do caminho
dummy, action = request.path.split('/')
# somente as sessões JSON e XML são aceitas
type_response = session.get('typeResponse')
if type_response != 'json' and type_response != 'xml':
# retorna uma resposta de erro
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:
# retorna uma resposta de sucesso
return {"action": action, "état": 1000, "réponse": config["adminData"].asdict()}, status.HTTP_200_OK
- linhas 13-21: verifica-se se estamos em uma sessão JSON ou XML;
- linha 24: é retornado o dicionário de dados da administração fiscal que, desde o início do servidor, havia sido colocado na configuração:
# admindata será um dado de escopo de aplicação somente para leitura
config["admindata"] = config["layers"]["dao"].get_admindata()
Vamos usar o cliente Postman e solicitar o URL [/get-admindata], após iniciar uma sessão jSON e realizar a autenticação:

A resposta do servidor é a seguinte:

30.16. A ação [calculer-impots]
A ação [calculer-impots] calcula o imposto de uma lista de contribuintes encontrada no corpo da solicitação na forma de uma string jSON. Já conhecemos essa ação: ela se chamava [calculate_tax_in_bulk_mode] na versão anterior.
Seu caminho é o seguinte:
# cálculo do imposto em lotes
@app.route('/calculer-impots', methods=['POST'])
def calculer_impots():
# é executado o controlador associado à ação
return front_controller()
Essa ação é processada pelo controlador [CalculerImpotsController] a seguir:
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):
# os elementos do caminho são recuperados
dummy, action = request.path.split('/')
# somente as sessões JSON e XML são aceitas
type_response = session.get('typeResponse')
if type_response != 'json' and type_response != 'xml':
# retorna-se uma resposta de erro
return {
"action": action,
"état": 1501,
"réponse": ["cette action n'est possible que pour les sessions json ou xml"]
}, status.HTTP_400_BAD_REQUEST
# recupera-se o corpo da postagem — espera-se uma lista de dicionários
msg_erreur = None
list_dict_taxpayers = None
# o corpo jSON do POST
request_text = request.data
try:
# que é transformado em uma lista de dicionários
list_dict_taxpayers = json.loads(request_text)
except BaseException as erreur:
# observa-se o erro
msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
# temos uma lista não vazia?
if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
# observa-se o erro
msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
# temos uma lista de dicionários?
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
# erro?
if erreur:
msg_erreur = "le corps du POST doit être une liste de dictionnaires"
# erro?
if msg_erreur:
# envia-se uma resposta de erro ao cliente
résultats = {"action": action, "état": 1501, "réponse": [msg_erreur]}
return résultats, status.HTTP_400_BAD_REQUEST
# verificamos os TaxPayers um por um
# inicialmente, sem erros
list_erreurs = []
for dict_taxpayer in list_dict_taxpayers:
# cria-se um TaxPayer a partir de dict_taxpayer
msg_erreur = None
try:
# a operação a seguir eliminará os casos em que os parâmetros não estejam
# das propriedades da classe TaxPayer, bem como os casos em que seus valores
# estejam incorretos
TaxPayer().fromdict(dict_taxpayer)
except BaseException as erreur:
msg_erreur = f"{erreur}"
# algumas chaves devem estar presentes no dicionário
if not msg_erreur:
# as chaves [marié, enfants, salaire] devem estar presentes no dicionário
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]"
# algum erro?
if msg_erreur:
# observa-se o erro no próprio TaxPayer
dict_taxpayer['erreur'] = msg_erreur
# adiciona-se o TaxPayer à lista de erros
list_erreurs.append(dict_taxpayer)
# Todos os contribuintes foram processados — há erros?
if list_erreurs:
# envia-se uma resposta de erro ao cliente
résultats = {"action": action, "état": 1501, "réponse": list_erreurs}
return résultats, status.HTTP_400_BAD_REQUEST
# sem erros, podemos prosseguir
# recuperação dos dados da administração fiscal
admindata = config["admindata"]
métier = config["layers"]["métier"]
try:
# processamos os TaxPayer um por um
list_taxpayers = []
for dict_taxpayer in list_dict_taxpayers:
# cálculo do imposto
taxpayer = TaxPayer().fromdict(
{'marié': dict_taxpayer['marié'], 'enfants': dict_taxpayer['enfants'],
''salário': dict_taxpayer['salaire']})
métier.calculate_tax(taxpayer, admindata)
# armazenamos o resultado como um dicionário
list_taxpayers.append(taxpayer.asdict())
# adiciona-se list_taxpayers às simulações atuais, atribuindo um número a cada simulação
simulations = session.get("simulations", [])
id_simulation = session.get("id_simulation", 0)
for simulation in list_taxpayers:
# atribui-se um número a cada simulação
id_simulation += 1
simulation['id'] = id_simulation
# adiciona-se à lista atual de simulações
simulations.append(simulation)
# reinicia-se a sessão
session["simulations"] = simulations
session["id_simulation"] = id_simulation
# envia-se a resposta ao cliente
return {"action": action, "état": 1500, "réponse": list_taxpayers}, status.HTTP_200_OK
except ImpôtsError as erreur:
# envia-se uma resposta de erro ao cliente
return {"action": action, "état": 1501, "réponse": [f"{erreur}"]}, status.HTTP_500_INTERNAL_SERVER_ERROR
- linhas 16-24: verifica-se se estamos de fato em uma sessão JSON ou XML
- linhas 26-120: esse código já nos é conhecido em linhas gerais. Trata-se do código da função |index_controller| da versão 10 do aplicativo, que foi adaptado para atender às especificações da interface [InterfaceController] implementada;
- linhas 104-115: o código adicionado para levar em conta o novo ambiente desse controlador. Acabamos de realizar cálculos de impostos. Precisamos armazenar os resultados na lista de simulações mantida na sessão;
- linha 105: recuperamos a lista de simulações da sessão;
- linha 106: recuperamos o número da última simulação realizada;
- linhas 107-112: percorremos a lista de dicionários dos resultados do cálculo do imposto; a cada um deles atribuímos um número de simulação [id], e cada dicionário é adicionado à lista de simulações;
- linhas 113-115: a nova lista de simulações, bem como o número da última simulação realizada, são repostos na sessão;
Realizamos o seguinte teste no Postman, após inicializar uma sessão jSON e nos autenticarmos:


A resposta do servidor é a seguinte:

Se agora solicitarmos a lista de simulações:
Notamos que, na lista de resultados de [/calcul-impots], os contribuintes não possuem o atributo [id], enquanto que, na lista de simulações, cada simulação possui um número que a identifica.




