Série API em Flask - Parte 7 - Criando usuários | Lucas Simon
Capítulo 7: Criando usuários Estamos aqui
Estamos entrando na fase de implementação dos endpoints. A idéia deste endpoint é fazer com que o qualquer cliente (SPA, REST Client, curl e etc…) possa registrar um usuário chamando a rota e o payload correto.
Nos próximos artigos os recursos para listagem, detalhes/atualização/deleção por id serão somente utilizados por quem tiver uma permissão admin setada como true no modelo de usuários.
Para detalhes do perfil logado e atualização do usuário logado serão implementados após o artigo de autenticação.
Assim vamos abordar todo o Crud de usuários da plataforma.
Instalando o pacote marshmallow e seu uso
Primeiramente o marshmallow é responsável por converter dados para tipos nativos do Python. Eu utilizo bastante para validar os payloads de entrada e saída dos meus recursos.
Existem outras alteranativas para isso como o schematics porém por questão de habito optei por usar o marshmallow.
Sendo assim, façamos, pip install marshmallow --upgrade. O ideal é cria-lo em nosso requirements/base.txt com a entrada marshmallow==2.15.6 em seguida executar o comando $ pip install -r requirements/base.txt
Após instalar vamos criar o arquivo dentro de apps/users/schemas.py. Ele irá conter nossas definições de schema.
A principio precisamos somente de schema para registrar o usuário então:
class UserRegistrationSchema(Schema):
full_name = Str(required=True, error_messages={'required': 'Campo obrigatório'})
email = Email(required=True, error_messages={'required': 'Campo obrigatório'})
password = Str(required=True, error_messages={'required': 'Campo obrigatório'})
cpf_cnpj = Str()
active = Boolean()
Dado essa classe podemos utilizá-la da seguinte forma. Desserialização de um dicionário para tipos nativos do python e realizando validações.
In [1]: from apps.users.schemas import UserRegistrationSchema
In [2]: payload = {'email': 'teste@teste.com', 'password': '123456'}
In [3]: schema = UserRegistrationSchema()
In [4]: schema.load(payload)
Out[4]: UnmarshalResult(data={'password': '123456', 'email': 'teste@teste.com'}, errors={'full_name': ['Campo obrigatório']})
In [5]: data, errors = schema.load(payload)
Out[6]: {'password': '123456', 'email': 'teste@teste.com'}
In [7]: errors
Out[7]: {'full_name': ['Campo obrigatório']}
Podem observar que na minha variável payload eu não coloquei a chave full_name e ao executar o metódo .load() ele me retorna múltiplos valores. Com isso posso retornar erros logo após o meu payload de dados serem inválidos.
Outro uso para o marshmallow é serializar objetos de um determinado modelo por exemplo:
In [1]: from apps.users.schemas import UserRegistrationSchema, UserSchema
In [2]: from apps.users.models import User
In [3]: user = User(email='teste@teste.com', password='123456', full_name='Teste OSchema')
In [4]: schema = UserSchema()
In [5]: result = schema.dump(user)
In [6]: result.data
{'cpf_cnpj': '',
'email': 'teste@teste.com',
'full_name': 'Teste OSchema',
'active': False}
Veja que eu criei uma instância do modelo User e através do método dump() consigo pegar através da variável result.data um dicionário com as informações que eu preciso e que podem ser respondidas imediatamente pelo meu recurso.
Claro que esta são uma das possibilidades do marshmallow e existem inúmeras outras possibilidades para o seu uso.
Padronizando mensagens da aplicação
Uma coisa que eu tenho costume de fazer é padronizar as mensagens da aplicação. Lembro que antigamente existia um documento de mensagens, em análise de sistemas, que detalhava todas as mensagens que a aplicação retornaria. De fato é bem usual essa padronização pois evita escrever mensagens com ou sem acentos (Campo obrigatório / Campo obrigatorio), com ou sem pontos no final da frase e etc… Outro ponto importante é que centraliza as mensagens em um único local facilitando a manutenção posteriormente.
Por isso, vamos criar um arquivo apps/messages.py que será responsável por documentar essa mensagens.
MSG_FIELD_REQUIRED = 'Campo obrigatório.'
Vamos voltar e refatorar nosso arquivo apps/users/schemas.py e alterar o nosso error_messages.
# importar nossas CONSTANTES
full_name = Str(required=True, error_messages={'required': MSG_FIELD_REQUIRED})
# Fazer isso para os demais campos
Bem simples e útil certo!? Após importar as constantes, refatore as mensagens required para receber a constante MSG_FIELD_REQUIRED. A medida que a aplicação for crescendo iremos visitar esse módulo (messages.py) várias vezes…
Instalando o Bcrypt e salvando a senha encriptada
Dando sequência, precisamos que nossa senha ao registrarmos através do endpoint seja encriptada, hoje o banco de dados não realiza essa segurança de um dado sensível por isso vamos fazer essa parte na aplicação.
Antes de tudo, devemos instalar o pacote bcrypt através do comando pip install bcrypt ou pelo pip install -r requirements/base.txt.
In [1]: from bcrypt import gensalt, hashpw, checkpw
In [2]: hashed = hashpw('123456'.encode('utf-8'), gensalt(12))
In [3]: hashed
Out[3]: b'$2b$12$E6FDm6tCBOVPiQfu6wFr1uU3Lqmyv8Ci7BR92XJ8ZAEXwQ31fi6Qe'
In [4]: checkpw('123456'.encode('utf-8'), hashed)
Esse script é bem simples. Importamos as funções do pacote e faço um hash com a string 123456. Em seguida com o método checkpw verificamos se a string do primeiro parâmetro é igual ao hashed criado acima.
Padronizando as mensagens de resposta JSON.
Mais uma forma de organizar a aplicação é padronizar as mensagens de resposta de cada recurso. Para isso eu crio vários métodos de acordo com cada tipo de status code do HTTP, com o objetivo de reaproveitamento e padronização.
Antes de seguirmos vamos criar novas mensagens para cada tipo de resposta. No arquivo apps/messages.py adicione:
MSG_INVALID_DATA = 'Ocorreu um erro nos campos informados.'
MSG_DOES_NOT_EXIST = 'Este(a) {} não existe.'
MSG_EXCEPTION = 'Ocorreu um erro no servidor. Contate o administrador.'
MSG_ALREADY_EXISTS = 'Já existe um(a) {} com estes dados.'
Agora, no arquivo apps/responses.py vamos criar os métodos.
from .messages import MSG_INVALID_DATA, MSG_DOES_NOT_EXIST, MSG_EXCEPTION
from .messages import MSG_ALREADY_EXISTS
def resp_data_invalid(resource :str, errors: dict, msg: str = MSG_INVALID_DATA):
Responses 422 Unprocessable Entity
raise ValueError('O recurso precisa ser uma string.')
resp.status_code = 422
def resp_exception(resource :str, description :str = '', msg :str = MSG_EXCEPTION):
Responses 500
def resp_does_not_exist(resource :str, description :str):
Responses 404 Not Found
'message': MSG_DOES_NOT_EXIST.format(description),
def resp_already_exists(resource :str, description :str):
Responses 400
'message': MSG_ALREADY_EXISTS.format(description),
def resp_ok(resource :str, message :str, data=None, **extras):
response = {'status': 200, 'message': message, 'resource': resource}
response.update(extras)
resp = jsonify(response)
Um novo endpoint na nossa api
Após todas essas configurações vamos codificar nosso recurso User. Crie um módulo apps/users/resources.py.
Vou deixar aqui o código comentado e abaixo farei algumas considerações:
from mongoengine.errors import NotUniqueError, ValidationError
from apps.responses import (
resp_already_exists,
resp_exception,
resp_data_invalid,
resp_ok
from apps.messages import MSG_NO_DATA, MSG_PASSWORD_WRONG, MSG_INVALID_DATA
from apps.messages import MSG_RESOURCE_CREATED
from .schemas import UserRegistrationSchema, UserSchema
from .utils import check_password_in_signup
# Inicializo todas as variaveis utilizadas
data, errors, result = None, None, None
password, confirm_password = None, None
schema = UserRegistrationSchema()
# Se meus dados postados forem Nulos retorno uma respota inválida
password = req_data.get('password', None)
confirm_password = req_data.pop('confirm_password', None)
# verifico através de uma função a senha e a confirmação da senha
# Se as senhas não são iguais retorno uma respota inválida
if not check_password_in_signup(password, confirm_password):
errors = {'password': MSG_PASSWORD_WRONG}
# Desserialização os dados postados ou melhor meu payload
data, errors = schema.load(req_data)
# Se houver erros retorno uma resposta inválida
# Crio um hash da minha senha
hashed = hashpw(password.encode('utf-8'), gensalt(12))
# Salvo meu modelo de usuário com a senha criptografada e email em lower case
# Qualquer exceção ao salvar o modelo retorno uma resposta em JSON
# ao invés de levantar uma exception no servidor
data['password'] = hashed
data['email'] = data['email'].lower()
model = User(**data)
return resp_already_exists('Users', 'fornecedor')
return resp_exception('Users', msg=MSG_INVALID_DATA, description=e)
return resp_exception('Users', description=e)
# Realizo um dump dos dados de acordo com o modelo salvo
result = schema.dump(model)
# Retorno 200 o meu endpoint
'Users', MSG_RESOURCE_CREATED.format('Usuário'),  data=result.data,
Seguem alguma considerações de desenvolvimento da aplicação:
Sempre inicializo as variáveis no inicio da método. Talvez seja mania vinda do JavaScript como também uma maneira de precaver de exceptions de variável não declarada.
Faço o possível para tratar validar os inputs de dados no incio e tratar os erros antes do processamento real.
É preciso separar o código em pequenas partes como foi feito na chamada do método check_password_in_signup. Dessa maneira fica fácil realizar teste unitário além de deixar o código mais limpo. O problema de deixar tudo numa unica função é quantidade de testes que teremos de fazer para cercar todos os casos, de cada condição if else.
Em API's Rest não deixo levantar exceptions. Sempre procuro retornar uma resposta json com o status code diferente de 200.
Criar nosso endpoint
Em nosso módulo apps/api.py faça a importação do recurso from apps.users.resources import SignUp e na função configure_api adicione nosso endpoint:
# demais rotas
# rotas para o endpoint de usuarios
api.add_resource(SignUp, '/users')
Testando nosso modelo
Algumas opções para realizar testes funcionais em nossa API são:
Exemplo de uma requisiçaõ utilizando o Httpie e a resposta do nosso servidor.
$ http -v POST 0.0.0.0:5000/users full_name="07368680629" email="teste@teste.com" password='123456' confirm_password='123456'
"confirm_password": "123456",
"full_name": "07368680629",
Date: Thu, 11 Oct 2018 02:02:58 GMT
"full_name": "07368680629"
"message": "Usuário criado(a).",
Concluindo, quis mostrar através deste artigo como padronizo minhas aplicações Flask e o modo de como gosto de desenvolver utilizando padrões para facilitar a leitura e a manutenção do código.