Flask é um microframework para desenvolvimento web.
Para inciarmos um projeto usando o Flask:
mkdir eventos-api
cd eventos-api
python -m venv venv
source venv/bin/activete
pip install FlaskFeito isso, criamos um arquivo app.py:
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "<h1>Flask successfully installed!</h1>"Quando criamos uma instância do Flask precisamo informar um nome para essa aplicação. Para isso podemos usar a variável global do python __name__, que se refere ao nome do próprio módulo que está sendo executado no momento.
para executar a aplicação:
flask run --port=5500
É importante ressaltar que o Flask depende de duas variáveis de ambiente, a FLASK_APP e FLASK_ENV:
FLASK_APPserve para informar qual a nossa aplicação python a ser executada dentro da framework do Flask. No exemplo anterior executamos nossa aplicação sem precisar informar essa variável de ambiente, pois o arquivoapp.pyé interpretado como um ponto de entra para a aplicação que o Flask deve executar, caso o nome do arquivo fossemain.pypor exemplo, teriamos que executar informando para esse variável justamente esse arquivo, da seguinte maneira:FLASK_APP=main.py flask run.FLASK_ENVserve para informar se nossa aplicação está sendo executada em produção ou desenvolvimento. Repare que ao executar o comando acima, o Flask informa que estamos em ambiente de produção, e toda alteração que fazemos o arquivo, temos que parar o Flask e executar novamente, passando para essa variável o ambiente como desenvolvimento, temos o live reload, assim o Flask identifica as alterações automaticamente.FLASK_APP=main.py FLASK_ENV=development flask run.
Para retornar dados em JSON, vamos usar o exemplo anterios dos eventos. Podemos criar uma rota para retornar os dados da seguinte forma:
@app.route("/api/eventos/")
def listar_eventos():
eventos_dict = []
for ev in eventos:
eventos_dict.append({
"id": ev.id,
"nome": ev.nome,
"local": ev.local
})
return json.dumps(eventos_dict)E nessa url teremos o retorno com um Content-Type: text/html; charset=utf-8, mas na verdade quereremos que nossa API retorne no formato JSON. Para isso podemos usar o jsonify do Flask e alterar o nosso return para:
@app.route("/api/eventos/")
def listar_eventos():
eventos_dict = []
for ev in eventos:
eventos_dict.append({
"id": ev.id,
"nome": ev.nome,
"local": ev.local
})
return jsonify(eventos_dict)Assim temos um Content-Type: application/json no Response Headers.
Agora note que no nosso for estamos montando um dicionário declarando cada uma das propriedades. Podemos alterar isso, pois o Python tem uma representação de dicionário padrão para objetos usando o __dict__. Dessa forma podemos refatorar nosso for para:
@app.route("/api/eventos/")
def listar_eventos():
eventos_dict = []
for ev in eventos:
eventos_dict.append(ev.__dict__)
return jsonify(eventos_dict)Vamos criar uma rota para acessar os detalher de um evento, para isso definos uma rota:
@app.route("/api/eventos/<int:id>/")
def detalhar_evento(id):
for ev in eventos:
if ev.id == id:
return jsonify(ev.__dict__)Note que nessa rota temos essa sintaxe do Flask <int:id>, que é um argumento que será informado na URL do tipo inteiro e um nome, nesse caso id. E como temos esse parâmetro que é recebido pela URL, definimos uma função que irá receber esse parâmetro como argumento para assim detalhar os dados do evento com esse id.
Para esse exemplo refarotei para usar uma expressão lambda apenas para aprendizem:
find = lambda func, elements: next((element for element in elements if func(element)), None)
findall = lambda func, elements: [element for element in elements if func(element)]
@app.route("/api/eventos/<int:id>/")
def detalhar_evento(id):
evento = find(lambda ev: ev.id == id, eventos)
return jsonify(evento.__dict__)Note que ao tentar usar o nossa rota para detalhar um evento, se passarmos como argumento um id que não existe, causa um erro na nossa aplicação. Para isso precisamos tratar essa situação. Para isso podemos usar o abort da biblioteca Flask.
@app.route("/api/eventos/<int:id>/")
def detalhar_evento(id):
try:
evento = find(lambda ev: ev.id == id, eventos)
return jsonify(evento.__dict__)
except AttributeError:
abort(404, "Event Not Found!")Nesse caso usei o try/except do Python para que quando houver o error que a página no retornou antes do tratamento de erros, retornar o status 404 com uma mensagem mais amigável.
Também poderimos fazer da sequinte forma se usar o try/except:
@app.route("/api/eventos/<int:id>/")
def detalhar_evento(id):
for ev in eventos:
if ev.id == id:
return jsonify(ev.__dict__)
abort(404, "Event Not Found!")Agora para retornar esse erro como JSON usamos o make_response do Flask:
@app.route("/api/eventos/<int:id>/")
def detalhar_evento(id):
try:
evento = find(lambda ev: ev.id == id, eventos)
return jsonify(evento.__dict__)
except AttributeError:
data = { "error": f"Event id {id} not found!" }
return make_response(jsonify(data), HTTPStatus.NOT_FOUND)Mas para facilitar a manipulação de erros temos @app.errorhandler, pois da forma como foi feito acima teriamos de que tratar cadas uma das rotas caso não contrassemos um recurso.
Para utilizar o @app.errorhandler, informamos o qual o status code e definimos uma função que será executada quando esse status code ocorrer em algum dos endpoints:
@app.errorhandler(404)
def not_found(error_msg):
return (jsonify(error=str(error_msg)), HTTPStatus.NOT_FOUND)Como definimos o @app.errorhandler(404) podemos deixas apenas o abort, pois o Flask cuida dessas execeções e quando for um erro 404 irá passar pela função que definimos em @app.errorhandler(404):
@app.route("/api/eventos/<int:id>/")
def detalhar_evento(id):
try:
evento = find(lambda ev: ev.id == id, eventos)
return jsonify(evento.__dict__)
except AttributeError:
abort(404, "Event Not Found!")Para saber mais sobre manipulação de erros no Flask
Para crirar novos recursos, no nosso caso, evento em nossa API precisamos criar uma rota que permita o metódo POST, para isso fazemos:
@app.route("/api/eventos/", methods=["POST"])
def criar_evento():
return jsonify(teste="teste")Mas precisamos enviar dados para que nossa API possa criar o novo evento. Usando o Postman podemos fazer isso conforme a imagem abaixo:
E para que o Flask possa pegar esses dados na requisição, vamos importar do Flask o request. Usando o request.data podemos acessar os dados enviados pela rquisição e salvar em uma variável. E para retornar esses mesmos dados, apenas para teste, vamos usar o json.loads para retornar esses dados como JOSN. E podemos testar nosso endpoint do POST:
@app.route("/api/eventos/", methods=["POST"])
def criar_evento():
data = json.loads(request.data)
return dataAgora podemos extrair os valores passados pela requisição para criarmos o nosso novo evento.
Nesse caso vamos usar o data.get("key"), onde key é o nome da propriedade que quermos acessar. Também poderiamos fazer data["key"], mas dessa forma se a requisição não enviar a key que estamos tentando acessar, nossa aplicação quebra. Utilizando o data.get("key"), caso tenha valor, será retornado o valor dessa key, caso contrário retornará None.
@app.route("/api/eventos/", methods=["POST"])
def criar_evento():
data = json.loads(request.data)
nome = data.get("nome")
local = data.get("local")
if local:
evento = Evento(nome=nome, local=local)
else:
evento = EventoOnline(nome=nome)
eventos.append(evento)
return dataAo fazer essa requisição POST iremos adicionar o novo evento na lista de eventos. Para conferir basta fazer uma requisição para a rota GET para listar os eventos.
Como a regra de negocio da nossa aplicação diz que o nome do evento não pode ser nulo, devemos fazer essa validação, para isso pode fazer:
@app.route("/api/eventos/", methods=["POST"])
def criar_evento():
data = json.loads(request.data)
nome = data.get("nome")
local = data.get("local")
if not nome:
abort(HTTPStatus.BAD_REQUEST, "Property 'nome' is required!")
if local:
evento = Evento(nome=nome, local=local)
else:
evento = EventoOnline(nome=nome)
eventos.append(evento)
return {
"id": evento.id,
"url": f"/api/eventos/{evento.id}"
}E também um novo @app.errorhandler para Bad Request, assim teremos dois manipulaores de erro:
@app.errorhandler(HTTPStatus.NOT_FOUND)
def not_found(error_msg):
return (jsonify(error=str(error_msg)), HTTPStatus.NOT_FOUND)
@app.errorhandler(HTTPStatus.BAD_REQUEST)
def not_found(error_msg):
return (jsonify(error=str(error_msg)), HTTPStatus.BAD_REQUEST)Para deletar um evento, precisamos definir uma rota para o método DELETE, que assim como o GET recebe o id do recuso que desejamos excluir:
@app.route("/api/eventos/<int:id>", methods=["DELETE"])
def deletar_evento(id):
try:
evento = find(lambda ev: ev.id == id, eventos)
if not evento:
abort(HTTPStatus.NOT_FOUND, f"Event id {id} not found!")
eventos.remove(evento)
return jsonify({
"id":id,
"message": "Evento successfully deleted"
})
except AttributeError:
abort(HTTPStatus.NOT_FOUND, f"Event id {id} not found!")Para editar um evento/recurso precisamos de uma rota para o método PUT, de forma muito parecida com o POST, pois iremos enviar os dados que serão atualizados na requisição, mas precisamos informar o id assim como no DELETE:
@app.route("/api/eventos/<int:id>", methods=["PUT"])
def editar_evento(id):
data = request.get_json()
nome = data.get("nome")
local = data.get("local")
if not nome:
abort(HTTPStatus.BAD_REQUEST, "Property 'nome' is required!")
if not local:
abort(HTTPStatus.BAD_REQUEST, "Property 'local' is required!")
evento = find(lambda ev: ev.id == id, eventos)
if not evento:
abort(HTTPStatus.NOT_FOUND, f"Event id {id} not found!")
evento.nome = nome
evento.local = local
return jsonify(evento.__dict__)O verbos/métodos PUT e PATCH tem a mesma finalidade de editar/atualizar um recuso. Mas a diferença entre eles é que enquanto o PUT precisa de todas os atributos do recurso que será atualizado, o PATCH precisa apenas dos atributo(s) que irão ser atualizados nessa requisição, por isso uma atualização parcial.
@app.route("/api/eventos/<int:id>", methods=["PATCH"])
def editar_evento_parcial(id):
data = request.get_json()
# {} => não atualiza nada
# { "nome":"" } => não pode por conta da regra de negócio
# { "nome": "Novo nome" } => atualizar o nome
if not "nome" in data.keys():
abort(HTTPStatus.BAD_REQUEST, "Property 'nome' is required!")
nome = data.get("nome")
if not nome:
abort(HTTPStatus.BAD_REQUEST, "Property 'nome' is required!")
evento = find(lambda ev: ev.id == id, eventos)
if not evento:
abort(HTTPStatus.NOT_FOUND, f"Event id {id} not found!")
evento.nome = nome
return jsonify(evento.__dict__) 
