33. Exercício prático: versão 13
A versão 13 altera a versão 12 nos seguintes pontos:
- reestrutura-se certas partes do código (refatoração);
- a sessão é gerenciada de maneira diferente por meio do módulo [flask_session];
- utilizam-se senhas criptografadas no arquivo de configuração;
A versão 13 é obtida inicialmente por meio da cópia da versão 12:


- no [1], a configuração será refatorada. Ela será removida, notadamente, da pasta [flask];
- em [2], o script principal será refatorado. Ele também será removido da pasta [flask];
- em [3], a configuração será dividida em vários arquivos;
- no [4]: vamos simplificar o script principal [main], transferindo parte do código para outros arquivos;
- em [5], o controlador de autenticação será modificado, já que, a partir de agora, as senhas dos usuários serão criptografadas;
- no [6], o controlador principal incorporará o código anteriormente presente no script principal [main];
33.1. Reestruturação da configuração do aplicativo

Aparecem três novos arquivos de configuração:
- [mvc]: para configurar a arquitetura MVC do aplicativo;
- [parameters]: que reunirá todas as constantes do aplicativo;
- [syspath]: que configura o Python Path do aplicativo;
O arquivo [syspath.py] é o seguinte:
def configure(config: dict) -> dict:
import os
# 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",
# pasta do script principal
script_dir,
# configurações [database, layers, parameters, controllers, views]
f"{script_dir}/../configs",
# controladores
f"{script_dir}/../controllers",
# respostas HTTP
f"{script_dir}/../responses",
# modelos de visualizações
f"{script_dir}/../models_for_views",
]
# definimos o syspath
from myutils import set_syspath
set_syspath(absolute_dependencies)
# fazemos a configuração
return {
"root_dir": root_dir,
"script_dir": script_dir
}
- o script [syspath] serve para configurar o Python Path do aplicativo (linhas 40-41);
- ele fornece duas informações úteis para os outros scripts de configuração (linhas 45-46);
O script [mvc] configura a arquitetura MVC do aplicativo web jSON / XML / HTML:
def configure(config: dict) -> dict:
# configuração do aplicativo MVC
# 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
# as respostas HTTP
from HtmlResponse import HtmlResponse
from JsonResponse import JsonResponse
from XmlResponse import XmlResponse
# modelos das visualizações
from ModelForAuthentificationView import ModelForAuthentificationView
from ModelForCalculImpotView import ModelForCalculImpotView
from ModelForErreursView import ModelForErreursView
from ModelForListeSimulationsView import ModelForListeSimulationsView
# ações autorizadas 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": [
700, # /init-session – sucesso
201, # /autenticar-usuário – falha
],
"view_name": "views/vue-authentification.html",
"model_for_view": ModelForAuthentificationView()
},
{
# visualização do cálculo do imposto
"états": [
200, # /autenticar-usuário bem-sucedido
300, # /calcular-imposto bem-sucedido
301, # /calcular-imposto falha
800, # /exibir-cálculo-do-imposto bem-sucedido
],
"view_name": "views/vue-calcul-impot.html",
"model_for_view": ModelForCalculImpotView()
},
{
# visualização da lista de simulações
"états": [
500, # /listar-simulações (sucesso)
600, # /excluir-simulação bem-sucedido
],
"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, # /encerrar-sessão bem-sucedido
],
# redirecionamento para o URL
"to": "/init-session/html",
}
]
# configuração sendo restaurada para MVC
return {
# controladores
"controllers": controllers,
# respostas HTTP
"responses": responses,
# visualizações e modelos
"views": views,
# lista de redirecionamentos
"redirections": redirections,
# visualização de erros inesperados
"view_erreurs": view_erreurs
}
- linhas 1-101: esse código é conhecido;
- linhas 105-116: é gerada a configuração MVC do aplicativo;
O script [parameters] reúne as constantes do aplicativo:
def configure(config: dict) -> dict:
# configuração do aplicativo
# script_dir
script_dir = config['syspath']['script_dir']
# configuração do aplicativo
parameters = {
# usuários autorizados a usar o aplicativo
"users": [
{
"login": "admin",
"password": "$pbkdf2-sha256$29000$mPM.h3COkTIGYOzde68VIg$7LH5Q7rN/1hW.Xa.6rcmR6h52PntvVqd5.na7EtgQNw"
}
],
# 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,
# servidor Redis
"redis": {
"host": "127.0.0.1",
"port": 6379
},
}
# fazemos a configuração do aplicativo
return parameters
- linha 13: a partir de agora, as senhas dos usuários serão criptografadas;
- linhas 34-38: a configuração de um servidor [Redis], sobre o qual voltaremos a falar;
Com esses novos arquivos de configuração, o script [config] fica da seguinte forma:
def configure(config: dict) -> dict:
# configuração do syspath
import syspath
config['syspath'] = syspath.configure(config)
# configuração do aplicativo
import parameters
config['parameters'] = parameters.configure(config)
# configuração do banco de dados
import database
config["database"] = database.configure(config)
# instanciação das camadas do aplicativo
import layers
config['layers'] = layers.configure(config)
# configuração MVC da camada [web]
import mvc
config['mvc'] = mvc.configure(config)
# retornamos a configuração
return config
33.2. Reestruturação do script principal [main]

O script principal [main.py] limita-se a iniciar o servidor:
# aguarda 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()
# configura-se o aplicativo
import config
config = config.configure({'sgbd': sgbd})
# dependências
from SendAdminMail import SendAdminMail
from Logger import Logger
from ImpôtsError import ImpôtsError
import redis
# envio de um e-mail ao administrador
def send_adminmail(config: dict, message: str):
# envio de um e-mail ao administrador do aplicativo
config_mail = config['parameters']['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 de logs
logger = Logger(config['parameters']['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 logger é armazenado na configuração
config['logger'] = logger
# gestão de erros
if erreur:
# e-mail para o administrador
send_adminmail(config, message_erreur)
# encerramento do aplicativo
sys.exit(1)
# log de inicialização
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
# verificando a disponibilidade do servidor Redis
redis_client = redis.Redis(host=config["parameters"]["redis"]["host"],
port=config["parameters"]["redis"]["port"])
# fazendo ping no servidor Redis
try:
redis_client.ping()
except BaseException as exception:
# Redis indisponível
log = f"[serveur] Le serveur Redis n'est pas disponible : {exception}"
# console
print(log)
# log
logger.write(f"{log}\n")
# fim
sys.exit(1)
# o cliente [redis] é armazenado na configuração
config['redis_client'] = redis_client
# recuperação dos dados da administração fiscal
erreur = False
try:
# admindata será um dado de escopo de aplicação somente para leitura
config["admindata"] = config["layers"]["dao"].get_admindata()
# registro de sucesso
logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
# registro do erro
erreur = True
# registro de erro
log = f"L'erreur suivante s'est produite : {ex}"
# console
print(log)
# arquivo de logs
logger.write(f"{log}\n")
# e-mail para o administrador
send_adminmail(config, log)
# o thread principal não precisa mais do logger
logger.close()
# se houver erro, o processo é interrompido
if erreur:
sys.exit(2)
# importação das rotas do aplicativo web
import routes
routes.config=config
routes.execute(__name__)
- linhas 56-73: introduz-se um servidor Redis e seu cliente;
- linhas 102-104: as rotas da aplicação foram externalizadas no script [routes];
33.2.1. Os módulos [flask_session] e [redis]
O servidor [Redis] será utilizado para armazenar as sessões dos usuários. Utilizaremos o módulo [flask_session] para gerenciar essas sessões. Esse módulo pode armazenar as sessões dos usuários em vários locais. O Redis é um deles e é o que utilizaremos.
O módulo [flask_session] deve ser instalado em um terminal PyCharm:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install flask-session
Collecting flask-session
…
Para se comunicar com o servidor Redis, precisamos de um cliente Redis. Esse cliente será fornecido pelo módulo [redis], que também instalaremos:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install redis
Collecting redis
…
33.2.2. O servidor Redis
O servidor Redis servirá para armazenar as sessões dos usuários. O módulo [flask_session] funciona da seguinte maneira:
- cada usuário possui um identificador de sessão, e é exatamente isso que é enviado ao cliente — e somente isso. O cliente recebe um cookie de sessão apenas uma vez, após sua primeira solicitação. Esse cookie contém o identificador de sessão do usuário, que não mudará ao longo das solicitações do cliente. É por isso que o servidor não precisa reenviar um novo cookie de sessão;
- anteriormente, o conteúdo da sessão era enviado ao cliente. Isso não acontecerá mais. O conteúdo da sessão do usuário será armazenado no servidor Redis;
O Laragon vem com um servidor Redis desativado por padrão. Portanto, é preciso começar ativando-o:

- em [3], ative o servidor [Redis];
- em [4], mantenha a porta [6379] que os clientes Redis utilizam por padrão;
Os serviços do Laragon são reiniciados automaticamente após a ativação do Redis:

O servidor Redis pode ser consultado no modo de comando. Abra um terminal Laragon [6]:

- em [1], o comando [redis-cli] inicia o cliente no modo de comando do servidor Redis;
Em julho de 2019, o cliente Redis pode utilizar 172 comandos para se comunicar com o servidor [https://redis.io/commands#list]. Um deles, [command count] [2], exibe este número: [3].
A gravação em [Redis] é feita com o comando Redis [set attribut valeur] [4]. O valor pode então ser recuperado com o comando [get attribut] [5].
Pode ser necessário esvaziar a memória do Redis. Isso é feito com o comando [flushdb] [6]. Em seguida, se solicitarmos o valor do atributo [titre] [7], obteremos uma referência [nil] [8] indicando que o atributo não foi encontrado. Também é possível usar o comando [exists] [9-10] para verificar a existência de um atributo.
Para sair do cliente Redis, digite o comando [quit] [11].
Também é possível usar uma interface web para gerenciar as chaves presentes no servidor Redis. Para isso, é necessário que o servidor Apache do Laragon esteja em execução:

Aparece a seguinte interface:

- em [1-4], uma das sessões armazenadas no servidor Redis;
33.2.3. Gerenciamento do servidor Redis no script principal [main]
O script [main] verifica a presença do servidor Redis da seguinte maneira:
import redis
…
# verifica-se a disponibilidade do servidor Redis
redis_client = redis.Redis(host=config["parameters"]["redis"]["host"],
port=config["parameters"]["redis"]["port"])
# faz-se um ping no servidor Redis
try:
redis_client.ping()
except BaseException as exception:
# Redis indisponível
log = f"[serveur] Le serveur Redis n'est pas disponible : {exception}"
# console
print(log)
# log
logger.write(f"{log}\n")
# fim
sys.exit(1)
# o cliente [redis] é armazenado na configuração
config['redis_client'] = redis_client
- linha 4: o construtor da classe [redis.Redis] cria um cliente do servidor Redis. As características desse cliente (endereço, porta) são encontradas no script [parameters];
- linha 8: o método [ping] permite verificar se o servidor Redis está presente;
- linhas 9-17: se o ping não for bem-sucedido, o erro é registrado no log e o servidor é desligado;
- linha 20: a referência do cliente Redis é inserida na configuração;
33.2.4. Gerenciamento de rotas no script principal [main]
O gerenciamento de rotas no [main] limita-se às seguintes linhas:
# importação das rotas do aplicativo web
import routes
routes.config=config
routes.execute(__name__)
- linha 1: as rotas foram externalizadas no módulo [routes];
- linha 3: as rotas precisam conhecer a configuração da execução;
- linha 4: inicia-se a aplicação Flask, passando-lhe o nome do script executado (__main__);
O script das rotas é o seguinte:
# dependências
import os
from flask import Flask, redirect, request, session, url_for
from flask_api import status
from flask_session import Session
# aplicativo Flask
app = Flask(__name__, template_folder="../flask/templates", static_folder="../flask/static")
# configuração da aplicação
config = {}
# o controlador principal
def front_controller() -> tuple:
# a solicitação é encaminhada para o controlador principal
main_controller = config['mvc']['controllers']['main-controller']
return main_controller.execute(request, session, config)
@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:
# é executado 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()
# cálculo do imposto em lotes
@app.route('/calculer-impots', methods=['POST'])
def calculer_impots():
# 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:
# é executado 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', methods=['GET'])
def get_admindata() -> tuple:
# é executado o controlador associado à ação
return front_controller()
def execute(name: str):
# chave secreta da sessão
app.secret_key = os.urandom(12).hex()
# Flask-Session
app.config.update(SESSION_TYPE='redis',
SESSION_REDIS=config['redis_client'])
Session(app)
# caso em que a aplicação Flask seja iniciada por meio de um script de console
if name == '__main__':
app.config.update(ENV="development", DEBUG=True)
app.run(threaded=True)
- linha 9: a aplicação Flask é instanciada;
- linha 12: a configuração da aplicação ainda não é conhecida no momento da escrita do script. Ela só é conhecida no momento da execução;
- linhas 20-77: as rotas da aplicação, tal como estavam definidas na versão anterior. Isso não muda;
- linhas 14-18: todas as rotas se limitam a chamar a função [front_controller]. Removemos o código inicial dessa função. Agora, ela se limita a chamar o controlador principal do aplicativo web;
- linhas 79-89: [execute] é a função chamada pelo script [main] para iniciar o aplicativo web;
- linha 81: o módulo [flask_session] utiliza a chave secreta do Flask;
- linhas 82-84: configuração do módulo [flask_session]. Ela consiste em adicionar as chaves [SESSION_TYPE, SESSION_REDIS] à configuração [app.config] do aplicativo Flask [app]:
- [SESSION_TYPE]: o tipo da sessão. Existem vários tipos. O tipo [redis] indica que [flask_session] utiliza um servidor [redis] para armazenar as sessões dos usuários. Por isso, é necessário definir a chave [SESSION_REDIS], que deve ser a referência de um cliente Redis;
- linha 85: a sessão [Flask-Session] está associada ao aplicativo Flask;
- linhas 86-89: se o parâmetro [name] da linha 79 for a string [__main__], então a aplicação Flask é iniciada;
33.3. Reestruturação do controlador principal
O código que antes estava na função [front_controller] do script [main] foi movido para o controlador principal:

# importação de dependências
import threading
import time
from random import randint
from flask_api import status
from werkzeug.local import LocalProxy
from InterfaceController import InterfaceController
from Logger import Logger
from SendAdminMail import SendAdminMail
def send_adminmail(config: dict, message: str):
# envio de um e-mail ao administrador da aplicação
config_mail = config['parameters']['adminMail']
config_mail["logger"] = config['logger']
SendAdminMail.send(config_mail, message)
# controlador principal da aplicação
class MainController(InterfaceController):
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# processamento da solicitação
logger = None
action = None
type_response1 = None
try:
# recuperam-se os elementos do caminho
params = request.path.split('/')
# a ação é o primeiro elemento
action = params[1]
# sem erros iniciais
erreur = False
# o tipo de sessão deve ser conhecido antes de determinadas ações
type_response1 = session.get('typeResponse')
if type_response1 is None and action != "init-session":
# o erro é registrado
résultat = {"action": action, "état": 101,
"réponse": ["pas de session en cours. Commencer par action [init-session]"]}
erreur = True
# registrar no log
logger = Logger(config['parameters']['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}
# registra-se a solicitação
logger.write(f"[MainController] requête : {request}\n")
# interrompe-se o thread, caso tenha sido solicitado
sleep_time = config['parameters']['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)
# para algumas 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"]:
# observa-se o erro
résultat = {"action": action, "état": 101,
"réponse": [f"action [{action}] demandée par utilisateur non authentifié"]}
erreur = True
# há erros?
if erreur:
# a solicitação é inválida
status_code = status.HTTP_400_BAD_REQUEST
else:
# o controlador associado à ação é executado
controller = config['mvc']['controllers'][action]
résultat, status_code = controller.execute(request, session, config)
except BaseException as exception:
# outras exceções (inesperadas)
résultat = {"action": action, "état": 131, "réponse": [f"{exception}"]}
status_code = status.HTTP_400_BAD_REQUEST
finally:
pass
# o resultado enviado ao cliente está sendo registrado no log
log = f"[MainController] {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
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:
# caso contrário, continua-se a usar type_response1
type_response = type_response1
# construímos a resposta a ser enviada
response_builder = config['mvc']['responses'][type_response]
response, status_code = response_builder \
.build_http_response(request, session, config, status_code, résultat)
# fecha-se o arquivo de logs, caso tenha sido aberto
if logger:
logger.close()
# envia-se a resposta HTTP
return response, status_code
Todo esse código já foi visto em algum momento.
33.4. Gerenciamento de senhas criptografadas
Para gerenciar senhas criptografadas, utilizaremos o módulo [passlib], que é instalado a partir de um terminal do PyCharm:
(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\packages>pip install passlib
Collecting passlib
…
Aqui está um exemplo de script que criptografa a senha passada como parâmetro:

O script [create_hashed_password] é o seguinte (https://passlib.readthedocs.io/en/stable/):
import sys
# função de criptografia
from passlib.hash import pbkdf2_sha256
# aguarda a senha a ser criptografada
syntaxe = f"{sys.argv[0]} password"
erreur = len(sys.argv) != 2
if erreur:
print(f"syntaxe : {syntaxe}")
sys.exit()
else:
password = sys.argv[1]
# criptografa-se a senha
hash = pbkdf2_sha256.hash(password)
print(f"version cryptée de [{password}] = {hash}")
# verificação
correct = pbkdf2_sha256.verify(password, hash)
print(correct)
- linha 16: criptografa-se a senha passada como parâmetro;
- linha 20: compara-se a senha [password] passada como parâmetro com sua versão criptografada [hash]. A função [verify] criptografa a senha [password] e compara a sequência criptografada obtida com [hash]. Retorna True se as duas sequências forem iguais;
O script acima nos permite obter a versão criptografada da senha [admin]:
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/impots/http-servers/08/passlib/create_hashed_password.py admin
version cryptée de [admin] = $pbkdf2-sha256$29000$fU9pTendO6c0ZoyR8r5Xqg$5ZXywIUnbMfN2hPnBaefiuqWjEbmAY.Lu06i4dwcnek
True
Linha 2, o valor que inserimos no script [parameters]:
"users": [
{
"login": "admin",
"password": "$pbkdf2-sha256$29000$fU9pTendO6c0ZoyR8r5Xqg$5ZXywIUnbMfN2hPnBaefiuqWjEbmAY.Lu06i4dwcnek"
}
],
O controlador de autenticação [AuthentifierUtilisateurController] evolui da seguinte forma:
from passlib.handlers.pbkdf2 import pbkdf2_sha256
…
# verificando a validade do par (usuário, senha)
users = config['parameters']['users']
i = 0
nbusers = len(users)
trouvé = False
while not trouvé and i < nbusers:
trouvé = user == users[i]["login"] and pbkdf2_sha256.verify(password, users[i]["password"])
i += 1
# Encontrado?
if not trouvé:
…
33.5. Testes
Além dos testes com um navegador, também é possível utilizar os clientes da pasta [http-servers/07], escritos para a versão 12. Eles devem funcionar também na versão 13:

- no [1], os três clientes devem funcionar;
- no [2], os dois testes devem funcionar;