Série API em Flask - Parte 10 - Editando um usuário
Hoje vamos implementar a atualização de dados, refatorar algumas linhas de código e criar uma validação simples no campo e-mail.
Para entendimento, segue os capítulos que serão abordados.
Capítulo 1: Introdução, configuração e Hello World
Capítulo 2: Organizando as dependências e requerimentos
Capítulo 3: Configurando o pytest e nosso primeiro teste
Capítulo 4: Configurando o Makefile
Capítulo 5: Adicionando o MongoDB
Capítulo 6: Criando e testando o modelo de usuários
Capítulo 7: Criando usuários
Capítulo 8: Listando usuários
Capítulo 9: Buscando usuários
Capítulo 10: Editando um usuário Estamos aqui
Capítulo 11: Deletando um usuário
Capítulo 12: Autênticação por JWT
Capítulo 13: Criando um container Docker
Capítulo 14: Arquivos de configuração para Deploy na Digital Ocean
Capítulo 15: Automatizando o processo de deploy com Fabric
Capítulo 16: CI e CD com Jenkins, Python, Flask e Fabric
Capítulo 17: Utilizando o RabbitMQ com Flask e Sendgrid para enviar e-mails de boas vindas e ativar a conta do usuário
O repositório com todo o código fonte esta aqui. Os capítulos estão em branches
.
Alterar o nome dos endpoints
Uma mudança que tive de fazer é o nome das rotas. E para não dar conflitos altere a url /admin/users/<int:page_id>
para /admin/users/page/<int:page_id>
# rotas para os admins
api.add_resource(AdminUserPageList, '/admin/users/page/<int:page_id>')
api.add_resource(AdminUserResource, '/admin/users/<string:user_id>')
Criar uma nova mensagem
Como de praxe, precisamos padronizar as mensagens da aplicação. Para este caso vamos criar uma mensagem referenciando à um recurso atualizado. Em nosso messages.py
adicione:
MSG_RESOURCE_UPDATED = '{} atualizado(a).'
Refatorar a busca do usuário
Reparei que o código, abaixo iria se repetir nos métodos GET
, PUT
e DELETE
.
# -*- coding: utf-8 -*-
try:
# Buscando usuário por id
user = User.objects.get(id=user_id)
except FieldDoesNotExist as e:
return resp_exception('Users', description=e.__str__())
except Exception as e:
return resp_exception('Users', description=e.__str__())
Logo surgiu a necessidade de refatorar e reaproveitar esse código em uma única função. Para isso dentro de nosso apps/users/utils.py
vamos criar o meodo get_user_by_id
contendo a lógica acima.
def get_user_by_id(user_id: str):
try:
# buscamos todos os usuários da base utilizando o paginate
return User.objects.get(id=user_id)
except DoesNotExist as e:
return resp_does_not_exist('Users', 'Usuário')
except FieldDoesNotExist as e:
return resp_exception('Users', description=e.__str__())
except Exception as e:
return resp_exception('Users', description=e.__str__())
De acordo com o bloco try
, caso encontre o usuário a sua instância retornarda. Em caso de exceções elas são retornadas como resposta de requisição do flask. Outro ponto a observar, é que foi incluido mais um tratamento de exceção DoesNotExist
.
Feito isso, vamos refatorar nosso método GET
do recurso AdminUserResource
. Praticamente todo o bloco try/except
será substituido por 3 linhas.
from .utils import get_user_by_id
class AdminUserResource(Resource):
def get(self, user_id):
result = None
schema = UserSchema()
user = get_user_by_id(user_id)
if not isinstance(user, User):
return user
result = schema.dump(user)
return resp_ok(
'Users', MSG_RESOURCE_FETCHED.format('Usuários'), data=result.data
)
Novo schema para atualizar dados
Precisaremos, de um novo schema para atualizar os dados de um determinado usuário. Logo seguindo a lógica os campos devem ser não obrigatórios já que podemos atualizar qualquer campo.
Logo em nosso schema.py
, adicionamos a seguinte classe:
from marshmallow.fields import Email, Str, Boolean, Nested
# ... códigos anteriores
class UserUpdateSchema(Schema):
full_name = Str()
email = Email()
cpf_cnpj = Str()
address = Nested(AddressSchema)
Verifica se existe um email já cadastrado
Afim de verificar previamente se existe um email ja cadastrado na base de dados e lançar uma resposta antes de chamar o método save()
do modelo de User
, vamos adicionar o seguinte código em nosso utils.py
.
def exists_email_in_users(email: str, instance=None):
"""
Verifico se existe um usuário com aquele email
"""
user = None
try:
user = User.objects.get(email=email)
except DoesNotExist:
return False
except MultipleObjectsReturned:
return True
# verifico se o id retornado na pesquisa é mesmo da minha instancia
# informado no parâmetro
if instance and instance.id == user.id:
return False
return True
O método PUT
O código abaixo refere-se a implementação do método PUT
.
# -*- coding: utf-8 -*-
# Flask
from flask import request
# Third
from flask_restful import Resource
from mongoengine.errors import FieldDoesNotExist
from mongoengine.errors import NotUniqueError, ValidationError
# Apps
from apps.responses import resp_ok, resp_exception, resp_data_invalid, resp_already_exists
from apps.messages import MSG_RESOURCE_FETCHED_PAGINATED, MSG_RESOURCE_FETCHED
from apps.messages import MSG_NO_DATA, MSG_RESOURCE_UPDATED, MSG_INVALID_DATA
from apps.messages import MSG_ALREADY_EXISTS
# Local
from .models import User
from .schemas import UserSchema, UserUpdateSchema
from .utils import get_user_by_id, exists_email_in_users
class AdminUserPageList(Resource):
# ...
class AdminUserResource(Resource):
def get(self, user_id):
# ...
def put(self, user_id):
result = None
schema = UserSchema()
update_schema = UserUpdateSchema()
req_data = request.get_json() or None
email = None
# Valido se o payload está vazio
if req_data is None:
return resp_data_invalid('Users', [], msg=MSG_NO_DATA)
# Busco o usuário na coleção users pelo seu id
user = get_user_by_id(user_id)
# se não for uma instancia do modelo User retorno uma resposta
# da requisição http do flask
if not isinstance(user, User):
return user
# carrego meus dados de acordo com o schema de atualização
data, errors = update_schema.load(req_data)
# em caso de erros retorno uma resposta 422 com os erros de
# validação do schema
if errors:
return resp_data_invalid('Users', errors)
email = data.get('email', None)
# Valido se existe um email na coleção de usuários
if email and exists_email_in_users(email, user):
return resp_data_invalid(
'Users', [{'email': [MSG_ALREADY_EXISTS.format('usuário')]}]
)
try:
# para cada chave dentro do dados do update schema
# atribuimos seu valor
for i in data.keys():
user[i] = data[i]
user.save()
except NotUniqueError:
return resp_already_exists('Users', 'usuário')
except ValidationError as e:
return resp_exception('Users', msg=MSG_INVALID_DATA, description=e.__str__())
except Exception as e:
return resp_exception('Users', description=e.__str__())
result = schema.dump(user)
return resp_ok(
'Users', MSG_RESOURCE_UPDATED.format('Usuário'), data=result.data
)
Testando
$ http -v PUT 0.0.0.0:5000/admin/users/5bbeaf52fb5d1b0a32466c93 full_name="teste sobrenome"
PUT /admin/users/5bbeaf52fb5d1b0a32466c93 HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 32
Content-Type: application/json
Host: 0.0.0.0:5000
User-Agent: HTTPie/0.9.8
{
"full_name": "teste sobrenome"
}
HTTP/1.0 200 OK
Content-Length: 268
Content-Type: application/json
Date: Wed, 17 Oct 2018 17:39:17 GMT
Server: Werkzeug/0.14.1 Python/3.6.5
{
"data": {
"active": false,
"cpf_cnpj": "1234567",
"email": "teste@teste.com",
"full_name": "teste sobrenome",
"id": "5bbeaf52fb5d1b0a32466c93"
},
"message": "Usuário atualizado(a).",
"resource": "Users",
"status": 200
}
Graças a refatoração de código podemos economizar algumas linhas de código. Com isso finalizamos a atualização de dados do usuário e estamos quase terminando o nosso CRUD.
Próximo artigo: Deletando um usuário