Cheatsheet Flask
Micro-framework Python leve e flexível para aplicações web
Flask
Setup
Instalar Flask
# Criar ambiente virtual: python -m venv venv source venv/bin/activate # Linux/Mac venv\Scripts\activate # Windows # Instalar: pip install flask # Verificar: flask --version # Python 3.12, Flask 3.1, Werkzeug 3.1
venv isola dependências do projeto. pip install flask instala o framework e dependências (Werkzeug, Jinja2). Sempre ativar o ambiente antes de trabalhar.
Configuração
# config.py
import os
class Config:
SECRET_KEY = os.environ.get("SECRET_KEY", "dev")
SQLALCHEMY_DATABASE_URI = os.environ.get(
"DATABASE_URL", "sqlite:///app.db"
)
SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevConfig(Config):
DEBUG = True
class ProdConfig(Config):
DEBUG = False
config = {
"development": DevConfig,
"production": ProdConfig,
"default": DevConfig,
}SECRET_KEY é obrigatório para sessões e CSRF. os.environ.get() lê variáveis de ambiente. Classes de config por ambiente (Dev, Prod). SQLALCHEMY_TRACK_MODIFICATIONS = False evita overhead.
Flask CLI
# Comandos úteis:
flask routes # listar todas as rotas
flask shell # shell interativo
flask db upgrade # aplicar migrations
flask db migrate -m "msg" # gerar migration
# Comandos custom:
import click
@app.cli.command("seed")
def seed():
"""Popular base de dados."""
db.session.add(User(nome="Admin"))
db.session.commit()
click.echo("Dados criados!")
# Executar:
flask seedflask routes lista endpoints registados. flask shell abre REPL com app carregada. @app.cli.command() cria comandos custom. click.echo() imprime no terminal. Útil para seeds, limpeza e tarefas admin.
App mínima (Hello World)
# app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def inicio():
return "Olá, Flask!"
if __name__ == "__main__":
app.run(debug=True)Flask(__name__) cria a aplicação. @app.route() define o endpoint. app.run(debug=True) inicia o servidor com auto-reload. __name__ ajuda o Flask a localizar templates e static.
Variáveis de ambiente
# .env (usar python-dotenv) FLASK_APP=run.py FLASK_DEBUG=1 SECRET_KEY=super-secreto-123 DATABASE_URL=postgresql://user:pass@localhost/db # Carregar automaticamente: pip install python-dotenv # Flask carrega .env automaticamente! # Ou manual: from dotenv import load_dotenv load_dotenv() # Aceder: import os key = os.environ["SECRET_KEY"]
FLASK_APP indica o módulo da app. FLASK_DEBUG=1 ativa debug. Flask 2.x+ carrega .env automaticamente com python-dotenv. Nunca commitar .env (adicionar ao .gitignore).
Estrutura do projeto
projeto/ ├── app/ │ ├── __init__.py # app factory │ ├── models.py # modelos SQLAlchemy │ ├── routes.py # rotas principais │ ├── templates/ # HTML Jinja2 │ │ ├── base.html │ │ └── index.html │ └── static/ # CSS, JS, imagens ├── config.py # configurações ├── requirements.txt # dependências └── run.py # ponto de entrada
templates/ e static/ são localizados automaticamente pelo Flask. __init__.py contém o factory. config.py separa configurações. requirements.txt lista dependências para reprodução.
Executar o servidor
# Modo recomendado (Flask CLI): flask run flask run --debug # com debug flask run --port 8080 # porta custom flask run --host 0.0.0.0 # acessível na rede # Alternativa (script): python run.py # Com auto-reload e debug: export FLASK_DEBUG=1 flask run # Servidor de produção (NUNCA usar flask run): gunicorn -w 4 "app:create_app()"
flask run é o comando padrão. --debug ativa debugger e reload. --host 0.0.0.0 expõe na rede local. Em produção usar gunicorn ou uwsgi — o servidor do Flask é só para desenvolvimento.
Application Factory
# app/__init__.py
from flask import Flask
def create_app(config_name="default"):
app = Flask(__name__)
app.config.from_object(config[config_name])
# Inicializar extensões
db.init_app(app)
migrate.init_app(app, db)
login.init_app(app)
# Registar blueprints
from .routes import bp
app.register_blueprint(bp)
return appcreate_app() é o padrão factory — permite múltiplas instâncias (testes, produção). Extensões são inicializadas com init_app(). Blueprints registados dentro do factory. Essencial para projetos médios/grandes.
Extensões essenciais
# Instalar extensões comuns: pip install flask-sqlalchemy # ORM pip install flask-migrate # migrations pip install flask-login # autenticação pip install flask-wtf # formulários pip install flask-cors # CORS pip install flask-caching # cache pip install flask-mail # emails pip install flask-restful # API REST # requirements.txt: flask>=3.0 flask-sqlalchemy>=3.1 flask-migrate>=4.0 flask-login>=0.6
Flask é minimalista — extensões adicionam funcionalidade. flask-sqlalchemy = ORM. flask-migrate = migrations (Alembic). flask-login = sessões de utilizador. flask-wtf = forms com CSRF. Sempre fixar versões no requirements.txt.
Rotas e Views
Rotas básicas
@app.route("/")
def inicio():
return "Página inicial"
@app.route("/sobre")
def sobre():
return render_template("sobre.html")
# Múltiplas rotas para mesma view:
@app.route("/ola")
@app.route("/ola/<nome>")
def ola(nome="Mundo"):
return f"Olá, {nome}!"@app.route() mapeia URL para função. O nome da função é o endpoint (usado no url_for). Múltiplos decoradores = múltiplas URLs. Valor default no parâmetro torna-o opcional.
url_for e redirect
from flask import url_for, redirect
# Gerar URLs (nunca hardcode!):
url_for("inicio") # "/"
url_for("perfil", username="ana") # "/user/ana"
url_for("post", id=5) # "/post/5"
# Com query string:
url_for("search", q="flask", page=2)
# "/search?q=flask&page=2"
# Redirecionar:
return redirect(url_for("login"))
return redirect(url_for("perfil", username="ana"))
# Redirect com código:
return redirect(url_for("inicio"), code=301)url_for() gera URLs a partir do nome da função — se a rota mudar, o código continua válido. redirect() retorna 302 por default. code=301 para redirect permanente. Sempre usar url_for em vez de strings hardcoded.
Subdomínios e regras
# Ativar subdomínios:
app.config["SERVER_NAME"] = "meusite.com"
@app.route("/", subdomain="<user>")
def perfil_sub(user):
return f"Perfil de {user}"
# ana.meusite.com → "Perfil de ana"
# Regras avançadas:
from werkzeug.routing import Rule
app.url_map.add(Rule(
"/api/<version>/users",
endpoint="users",
defaults={"version": "v1"}
))
# Converter custom:
from werkzeug.routing import BaseConverter
class RegexConverter(BaseConverter):
def __init__(self, map, *items):
super().__init__(map)
self.regex = items[0]subdomain="<user>" captura subdomínios (requer SERVER_NAME). Rule permite regras avançadas. Conversores custom com BaseConverter e regex. Útil para multi-tenancy e APIs versionadas.
Métodos HTTP
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
email = request.form["email"]
# autenticar...
return redirect(url_for("dashboard"))
return render_template("login.html")
# API REST:
@app.route("/api/users", methods=["GET"])
def listar(): ...
@app.route("/api/users", methods=["POST"])
def criar(): ...
@app.route("/api/users/<int:id>", methods=["PUT", "DELETE"])
def atualizar(id): ...methods=["GET", "POST"] define métodos aceites. Default é só GET. request.method indica o método usado. Para APIs: GET=ler, POST=criar, PUT=atualizar, DELETE=remover.
Error handlers
@app.errorhandler(404)
def nao_encontrado(e):
return render_template("404.html"), 404
@app.errorhandler(500)
def erro_interno(e):
return render_template("500.html"), 500
@app.errorhandler(403)
def acesso_negado(e):
return "Acesso negado!", 403
# Erro custom com abort:
from flask import abort
@app.route("/admin")
def admin():
if not current_user.is_admin:
abort(403)
return "Painel admin"@app.errorhandler(código) personaliza páginas de erro. abort(código) levanta erro HTTP manualmente. Retornar tupla (template, código) define o status. Útil para 404, 403, 500 customizados com layout da app.
Parâmetros de URL
# String (default):
@app.route("/user/<username>")
def perfil(username):
return f"Perfil de {username}"
# Conversores de tipo:
@app.route("/post/<int:id>")
def post(id): # id é int
...
@app.route("/price/<float:valor>")
def preco(valor): # valor é float
...
@app.route("/path/<path:subpath>")
def ficheiro(subpath): # aceita /
...
# Múltiplos parâmetros:
@app.route("/blog/<int:ano>/<slug>")
def artigo(ano, slug): ...Conversores: int, float, path (aceita barras), string (default). Se o tipo não corresponder → 404 automático. Múltiplos parâmetros separados por /. O nome deve corresponder ao argumento da função.
Before/After request
@app.before_request
def antes():
# Executa ANTES de cada request
g.inicio = time.time()
if request.endpoint != "static":
# log, auth, etc.
pass
@app.after_request
def depois(response):
# Executa DEPOIS (modifica response)
duracao = time.time() - g.inicio
response.headers["X-Duracao"] = f"{duracao:.3f}s"
return response
@app.teardown_request
def finalizar(exc):
# Sempre executa (mesmo com erro)
pass@app.before_request executa antes de cada request (auth, logging). @app.after_request modifica a response (headers, cache). @app.teardown_request executa sempre (cleanup). g é um objeto global por-request.
Query parameters
from flask import request
# URL: /search?q=flask&page=2&lang=pt
@app.route("/search")
def search():
q = request.args.get("q", "")
page = request.args.get("page", 1, type=int)
lang = request.args.get("lang", "pt")
# Todos os params:
todos = request.args.to_dict()
# {"q": "flask", "page": "2", "lang": "pt"}
# Múltiplos valores (lista):
# /tags?a=1&a=2
tags = request.args.getlist("a")
# ["1", "2"]
return f"Busca: {q}, página {page}"request.args é um dicionário imutável. .get(chave, default, type=) com conversão automática. .getlist() para parâmetros repetidos. .to_dict() converte tudo. Valores são sempre strings sem type=.
Class-based views
from flask.views import MethodView
class UserAPI(MethodView):
def get(self, user_id=None):
if user_id is None:
return {"users": ["ana", "joão"]}
return {"id": user_id}
def post(self):
data = request.json
return {"criado": data}, 201
def delete(self, user_id):
return "", 204
# Registar:
app.add_url_rule(
"/api/users",
view_func=UserAPI.as_view("users"),
defaults={"user_id": None}
)
app.add_url_rule(
"/api/users/<int:user_id>",
view_func=UserAPI.as_view("user_detail")
)MethodView separa métodos HTTP em métodos da classe. Cada método (get, post, delete) trata o verbo correspondente. add_url_rule() regista. Ideal para APIs REST com lógica complexa por método.
Templates Jinja2
Renderizar templates
from flask import render_template, render_template_string
# Ficheiro HTML:
@app.route("/posts")
def posts():
lista = [{"titulo": "Post 1"}, {"titulo": "Post 2"}]
return render_template(
"posts.html",
titulo="Meus Posts",
posts=lista,
total=len(lista)
)
# String inline (cuidado com XSS!):
return render_template_string(
"Olá {{ nome }}!", nome="Ana"
)render_template() procura em templates/. Passar variáveis como keyword arguments. render_template_string() para strings (evitar com input do utilizador — risco XSS). Dados ficam disponíveis como variáveis no template.
Loops
{% for post in posts %}
<li>{{ loop.index }}. {{ post.titulo }}</li>
{% endfor %}
<!-- Variáveis do loop: -->
{{ loop.index }} <!-- 1, 2, 3... -->
{{ loop.index0 }} <!-- 0, 1, 2... -->
{{ loop.first }} <!-- True no 1º -->
{{ loop.last }} <!-- True no último -->
{{ loop.length }} <!-- total -->
<!-- Else (lista vazia): -->
{% for item in itens %}
<p>{{ item }}</p>
{% else %}
<p>Sem itens.</p>
{% endfor %}
<!-- Dict: -->
{% for chave, valor in dict.items() %}
{{ chave }}: {{ valor }}
{% endfor %}{% for %} itera listas/dicts. loop.index (1-based), loop.first, loop.last são variáveis especiais. {% else %} executa se a lista estiver vazia. .items() para dicionários.
Segurança em templates
<!-- Jinja2 escapa HTML automaticamente: -->
{{ user_input }}
<!-- <script> → <script> (seguro!) -->
<!-- DESATIVAR escape (cuidado!): -->
{{ html_content | safe }}
<!-- Renderiza HTML real — só para conteúdo
de confiança (ex: rich text admin) -->
<!-- Autoescape por extensão: -->
<!-- .html → auto-escape ON -->
<!-- .txt → auto-escape OFF -->
<!-- Configurar globalmente: -->
app.jinja_env.autoescape = True
<!-- Nunca fazer: -->
<!-- {{ request.args.get("nome") | safe }} -->
<!-- ↑ XSS garantido! -->Jinja2 faz auto-escape por default em .html — previne XSS. | safe desativa escape (só para conteúdo confiável). Nunca aplicar safe a input do utilizador. Markup() marca strings como seguras no Python.
Variáveis e expressões
<!-- {{ }} para output -->
<h1>{{ titulo }}</h1>
<p>{{ user.nome }}</p>
<p>{{ lista[0] }}</p>
<p>{{ dict["chave"] }}</p>
<!-- Expressões -->
<p>{{ 2 + 3 }}</p>
<p>{{ "Olá " ~ nome }}</p>
<p>{{ lista | length }} itens</p>
<!-- Comentários -->
{# Isto não aparece no HTML #}
<!-- Whitespace control -->
{%- if ativo -%}
Ativo
{%- endif -%}{{ }} imprime variáveis. Ponto para atributos/keys: user.nome. ~ concatena strings. {# #} são comentários. {%- -%} remove whitespace extra. Expressões Python básicas são suportadas.
Herança de templates
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{% block titulo %}App{% endblock %}</title>
</head>
<body>
{% include "navbar.html" %}
<main>
{% block conteudo %}{% endblock %}
</main>
{% block scripts %}{% endblock %}
</body>
</html>
<!-- templates/index.html -->
{% extends "base.html" %}
{% block titulo %}Início{% endblock %}
{% block conteudo %}
<h1>Página inicial</h1>
{% endblock %}{% extends %} herda de um template base. {% block %} define secções sobrescrevíveis. O filho só redefine os blocks que precisa. {{ super() }} inclui conteúdo do block pai. Padrão essencial para layouts consistentes.
Filtros
<!-- Filtros com pipe | -->
{{ nome | upper }} <!-- ANA -->
{{ nome | lower }} <!-- ana -->
{{ nome | capitalize }} <!-- Ana -->
{{ nome | title }} <!-- Ana Silva -->
{{ texto | truncate(50) }} <!-- corta a 50 chars -->
{{ lista | length }} <!-- tamanho -->
{{ lista | join(", ") }} <!-- "a, b, c" -->
{{ valor | round(2) }} <!-- 3.14 -->
{{ data | datetimeformat }} <!-- custom -->
{{ html | safe }} <!-- sem escape -->
{{ preco | default("N/A") }} <!-- se None -->
<!-- Encadear: -->
{{ nome | trim | upper }}Filtros transformam output com |. safe desativa escape HTML (cuidado!). default() para valores nulos. truncate() corta texto. Encadear com múltiplos |. Filtros custom registados com @app.template_filter().
Include e macros
<!-- Include (inserir parcial): -->
{% include "partials/navbar.html" %}
{% include "partials/footer.html" %}
<!-- Com variáveis: -->
{% include "card.html" with context %}
<!-- Macros (funções reutilizáveis): -->
{% macro input(name, label, type="text") %}
<div class="field">
<label for="{{ name }}">{{ label }}</label>
<input type="{{ type }}" name="{{ name }}"
id="{{ name }}">
</div>
{% endmacro %}
<!-- Usar macro: -->
{{ input("email", "Email", type="email") }}
{{ input("senha", "Senha", type="password") }}{% include %} insere parciais (navbar, footer). {% macro %} cria componentes reutilizáveis com parâmetros. Macros aceitam defaults. Importar de outro ficheiro: {% from "forms.html" import input %}.
Condicionais
{% if user %}
<p>Olá, {{ user.nome }}</p>
{% elif user_convidado %}
<p>Bem-vindo, convidado!</p>
{% else %}
<p>Faça <a href="/login">login</a></p>
{% endif %}
<!-- Operadores: -->
{% if idade >= 18 and ativo %}
{% if role in ["admin", "mod"] %}
{% if lista is defined %}
{% if valor is none %}
{% if nome is not none %}
<!-- Expressão inline: -->
<p>{{ "Admin" if user.is_admin else "User" }}</p>{% if %} / {% elif %} / {% else %} / {% endif %}. Operadores: and, or, not, in. Testes: is defined, is none. Expressão ternária: {{ X if cond else Y }}.
Filtros e testes custom
# Filtro custom:
@app.template_filter("moeda")
def filtro_moeda(valor):
return f"€{valor:,.2f}"
# No template: {{ preco | moeda }} → €1.234,56
# Filtro com parâmetro:
@app.template_filter("truncate_words")
def truncate_words(texto, n=20):
words = texto.split()[:n]
return " ".join(words) + "..."
# Teste custom:
@app.template_test("par")
def teste_par(n):
return n % 2 == 0
# No template: {% if n is par %}@app.template_filter("nome") regista filtro custom. Usar com {{ valor | nome }}. @app.template_test() cria testes para {% if x is teste %}. Filtros recebem o valor como 1º argumento + parâmetros extras.
Request e Response
Dados do request
from flask import request
@app.route("/dados", methods=["POST"])
def dados():
# Form data (application/x-www-form-urlencoded):
email = request.form["email"]
nome = request.form.get("nome", "")
# JSON body:
data = request.json # dict
data = request.get_json() # alternativo
data = request.get_json(silent=True) # None se inválido
# Ficheiros:
foto = request.files["foto"]
# Info do request:
request.method # "POST"
request.url # URL completa
request.remote_addr # IP do cliente
request.content_type # mime typerequest.form para dados de formulário. request.json / get_json() para body JSON. request.files para uploads. request.method indica o verbo. Usar .get() com default para evitar KeyError.
Sessões
from flask import session
# Requer SECRET_KEY configurado!
app.config["SECRET_KEY"] = "super-secreto"
@app.route("/login", methods=["POST"])
def login():
if autenticar(request.form):
session["user_id"] = user.id
session["nome"] = user.nome
session.permanent = True # usa PERMANENT_SESSION_LIFETIME
return redirect(url_for("dashboard"))
return "Credenciais inválidas", 401
@app.route("/logout")
def logout():
session.clear() # limpar tudo
session.pop("user_id", None) # ou só uma chave
return redirect(url_for("inicio"))
# Verificar:
if "user_id" in session: ...session armazena dados no cookie assinado (cliente). Requer SECRET_KEY. session.permanent = True usa tempo de vida configurado. session.clear() limpa tudo. Dados são serializados — só tipos JSON-safe. Não guardar objetos complexos.
CORS e caching
# CORS (Cross-Origin Resource Sharing):
pip install flask-cors
from flask_cors import CORS
CORS(app) # todas as rotas
CORS(app, resources={
r"/api/*": {"origins": "https://meusite.com"}
})
# Cache com flask-caching:
pip install flask-caching
from flask_caching import Cache
cache = Cache(app, config={"CACHE_TYPE": "simple"})
@app.route("/dados")
@cache.cached(timeout=300) # 5 minutos
def dados():
return jsonify(dados_pesados())
# Invalidar:
cache.delete("dados")flask-cors permite requests de outros domínios. Configurar origins para restringir. flask-caching com @cache.cached(timeout=N) evita recálculos. Tipos: simple (memória), redis, memcached. Essencial para APIs públicas.
Respostas JSON (API)
from flask import jsonify
@app.route("/api/users")
def users():
return jsonify({
"users": [
{"id": 1, "nome": "Ana"},
{"id": 2, "nome": "João"},
],
"total": 2
})
# Com status code:
return jsonify({"erro": "Não encontrado"}), 404
# Lista direta (Flask 2.2+):
return [{"id": 1}, {"id": 2}]
# Dict direto (Flask 2.2+):
return {"status": "ok"}
# Headers custom:
resp = jsonify({"ok": True})
resp.headers["X-Total"] = "42"
return respjsonify() serializa dict/lista para JSON com Content-Type: application/json. Flask 2.2+ permite retornar dict/lista diretamente. Tupla (json, código) define status. Ideal para APIs REST.
Upload de ficheiros
from werkzeug.utils import secure_filename
import os
UPLOAD_DIR = "uploads"
EXTENSOES = {"png", "jpg", "pdf"}
@app.route("/upload", methods=["POST"])
def upload():
f = request.files["ficheiro"]
if not f or f.filename == "":
return "Sem ficheiro", 400
ext = f.filename.rsplit(".", 1)[1].lower()
if ext not in EXTENSOES:
return "Tipo não permitido", 400
nome = secure_filename(f.filename)
f.save(os.path.join(UPLOAD_DIR, nome))
return f"Enviado: {nome}", 201
# HTML: <form enctype="multipart/form-data">request.files["campo"] acede ao ficheiro. secure_filename() remove caracteres perigosos. Validar extensão e tamanho. f.save() guarda no disco. Formulário precisa de enctype="multipart/form-data". Configurar MAX_CONTENT_LENGTH para limite.
Headers e status code
from flask import make_response
# Status code simples:
return "Criado!", 201
return "Sem conteúdo", 204
return "Erro", 400
# Response completo:
resp = make_response("OK")
resp.status_code = 200
resp.headers["X-Custom"] = "valor"
resp.headers["Cache-Control"] = "no-cache"
resp.content_type = "text/plain"
return resp
# Tuple com headers:
return "Erro", 400, {"X-Erro": "validacao"}
# Redirect:
return "", 302, {"Location": "/login"}Retornar tupla: (body, status) ou (body, status, headers). make_response() cria objeto editável. resp.headers[] adiciona headers. Códigos: 200=OK, 201=Criado, 204=Sem conteúdo, 400=Bad request, 404=Not found.
Download e streaming
from flask import send_file, send_from_directory, Response
# Enviar ficheiro:
@app.route("/download/<nome>")
def download(nome):
return send_from_directory("uploads", nome,
as_attachment=True)
# Gerar e enviar:
@app.route("/export")
def export():
return send_file(
"relatorio.pdf",
mimetype="application/pdf",
as_attachment=True,
download_name="relatorio.pdf"
)
# Streaming (ficheiros grandes):
@app.route("/stream")
def stream():
def gerar():
for i in range(1000):
yield f"linha {i}\n"
return Response(gerar(), mimetype="text/plain")send_from_directory() envia ficheiro de forma segura (previne path traversal). as_attachment=True força download. Response(generator()) para streaming. mimetype define o tipo. Ideal para exports e ficheiros grandes.
Cookies
from flask import request, make_response
# Definir cookie:
resp = make_response("Cookie definido")
resp.set_cookie(
"tema", "escuro",
max_age=3600, # 1 hora
httponly=True, # sem acesso JS
secure=True, # só HTTPS
samesite="Lax" # proteção CSRF
)
return resp
# Ler cookie:
tema = request.cookies.get("tema", "claro")
# Remover cookie:
resp = make_response("Removido")
resp.delete_cookie("tema")
return respset_cookie() define com opções de segurança. httponly=True impede acesso via JavaScript. secure=True só envia por HTTPS. samesite protege contra CSRF. request.cookies.get() lê. Preferir sessões para dados sensíveis.
Flash messages
from flask import flash, get_flashed_messages
@app.route("/salvar", methods=["POST"])
def salvar():
flash("Dados salvos com sucesso!", "sucesso")
flash("Email já registado.", "erro")
return redirect(url_for("perfil"))
# No template (base.html):
# {% with messages = get_flashed_messages(
# with_categories=true) %}
# {% for categoria, msg in messages %}
# <div class="alert-{{ categoria }}">
# {{ msg }}
# </div>
# {% endfor %}
# {% endwith %}
# Categorias: success, error, warning, infoflash(msg, categoria) guarda mensagem para o próximo request. get_flashed_messages(with_categories=true) recupera no template. Mensagens são consumidas (aparecem uma vez). Requer SECRET_KEY. Padrão para feedback pós-redirect.
Base de Dados
Setup Flask-SQLAlchemy
pip install flask-sqlalchemy
# app/__init__.py
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = \
"sqlite:///app.db"
db.init_app(app)
return app
# Criar tabelas (dev apenas!):
with app.app_context():
db.create_all()SQLAlchemy() cria a instância. db.init_app(app) liga à aplicação (factory pattern). SQLALCHEMY_DATABASE_URI define a BD. db.create_all() cria tabelas (usar migrations em produção). Requer app_context().
Relações
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
comentarios = db.relationship(
"Comentario", backref="post",
lazy="dynamic", cascade="all, delete-orphan"
)
class Comentario(db.Model):
id = db.Column(db.Integer, primary_key=True)
texto = db.Column(db.Text)
post_id = db.Column(
db.Integer, db.ForeignKey("post.id"),
nullable=False
)
# Usar:
post.comentarios.all()
post.comentarios.filter_by(aprovado=True)
comentario.post.titulo # backrefdb.relationship() define a relação. backref="post" cria acesso inverso. db.ForeignKey() na coluna da tabela filha. lazy="dynamic" retorna query (filtros). cascade="all, delete-orphan" apaga filhos ao eliminar pai.
Raw SQL e transações
from sqlalchemy import text
# Query SQL direta:
result = db.session.execute(
text("SELECT * FROM posts WHERE views > :min"),
{"min": 100}
)
for row in result:
print(row.titulo)
# Transação manual:
try:
db.session.begin_nested() # SAVEPOINT
db.session.add(Post(titulo="A"))
db.session.add(Post(titulo="B"))
db.session.commit()
except Exception:
db.session.rollback()
raise
# Context manager (Flask 2.x):
with db.session.begin():
db.session.add(post)
# commit automático (ou rollback)db.session.execute(text(...)) para SQL raw com parâmetros seguros. begin_nested() cria SAVEPOINT. rollback() reverte em caso de erro. with db.session.begin() faz commit/rollback automático. Evitar SQL raw — preferir ORM.
Definir modelos
from datetime import datetime
class Post(db.Model):
__tablename__ = "posts"
id = db.Column(db.Integer, primary_key=True)
titulo = db.Column(db.String(200), nullable=False)
corpo = db.Column(db.Text, default="")
views = db.Column(db.Integer, default=0)
publicado = db.Column(db.Boolean, default=False)
criado = db.Column(db.DateTime, default=datetime.utcnow)
preco = db.Column(db.Float)
def __repr__(self):
return f"<Post {self.titulo}>"
# Tipos: Integer, String, Text, Boolean,
# DateTime, Float, JSON, LargeBinarydb.Model é a classe base. db.Column(tipo, opções) define campos. nullable=False = obrigatório. default= valor por omissão. __tablename__ personaliza o nome da tabela. Tipos comuns: String, Integer, Text, Boolean, DateTime.
Migrations (Flask-Migrate)
pip install flask-migrate # Setup: from flask_migrate import Migrate migrate = Migrate(app, db) # Comandos CLI: flask db init # criar pasta migrations/ flask db migrate -m "add tabela posts" flask db upgrade # aplicar flask db downgrade # reverter flask db history # histórico flask db current # versão atual # Workflow: # 1. Modificar modelo # 2. flask db migrate -m "descrição" # 3. Revisar ficheiro gerado # 4. flask db upgrade
flask-migrate usa Alembic por baixo. db init só uma vez. db migrate gera script de migração. db upgrade aplica. Sempre revisar o ficheiro gerado antes de aplicar. Essencial para evolução do esquema em produção.
CRUD básico
# CREATE: post = Post(titulo="Olá", corpo="Texto") db.session.add(post) db.session.commit() # READ: post = Post.query.get(1) # por PK post = db.session.get(Post, 1) # alternativo todos = Post.query.all() primeiro = Post.query.first() # UPDATE: post.titulo = "Novo título" db.session.commit() # DELETE: db.session.delete(post) db.session.commit() # Bulk: db.session.add_all([post1, post2, post3]) db.session.commit()
db.session.add() + commit() para criar. query.get(id) busca por PK. Modificar atributos + commit() para atualizar. db.session.delete() remove. Sempre commit() para persistir. rollback() para cancelar.
Paginação
@app.route("/posts")
def posts():
page = request.args.get("page", 1, type=int)
per_page = 20
paginacao = Post.query.order_by(
Post.criado.desc()
).paginate(
page=page, per_page=per_page,
error_out=False
)
posts = paginacao.items # lista da página
total = paginacao.total # total de registos
tem_proxima = paginacao.has_next
tem_anterior = paginacao.has_prev
return render_template("posts.html",
posts=posts, paginacao=paginacao)
# No template:
# {% for p in paginacao.iter_pages() %}.paginate(page=, per_page=) divide resultados. .items = registos da página. .has_next / .has_prev para navegação. .iter_pages() gera números de página. error_out=False retorna vazio em vez de 404.
Queries e filtros
# Filtros:
Post.query.filter_by(publicado=True).all()
Post.query.filter(Post.views > 100).all()
Post.query.filter(
Post.titulo.like("%flask%")
).all()
# Múltiplas condições:
from sqlalchemy import and_, or_
Post.query.filter(
and_(Post.views > 50, Post.publicado == True)
).all()
# Ordenação e limite:
Post.query.order_by(Post.criado.desc()).limit(10).all()
# Contagem e agregação:
Post.query.count()
Post.query.filter_by(publicado=True).count()
# Primeiro ou 404:
post = Post.query.get_or_404(id)filter_by() para igualdade simples. filter() para expressões complexas (>, like, and_, or_). order_by() + desc() para ordenar. limit() restringe resultados. get_or_404() levanta 404 se não existir.
Eventos e hooks
from sqlalchemy import event
# Antes de inserir:
@event.listens_for(Post, "before_insert")
def antes_inserir(mapper, connection, target):
target.slug = gerar_slug(target.titulo)
# Depois de atualizar:
@event.listens_for(Post, "after_update")
def depois_update(mapper, connection, target):
target.atualizado = datetime.utcnow()
# Hybrid properties:
from sqlalchemy.ext.hybrid import hybrid_property
class User(db.Model):
nome = db.Column(db.String(50))
apelido = db.Column(db.String(50))
@hybrid_property
def nome_completo(self):
return f"{self.nome} {self.apelido}"@event.listens_for(Model, "evento") regista hooks. Eventos: before_insert, after_update, before_delete. hybrid_property funciona em Python e em queries SQL. Útil para slugs, timestamps e campos calculados.
Forms e Validação
Setup Flask-WTF
pip install flask-wtf email-validator
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField
from wtforms.validators import DataRequired, Email
class LoginForm(FlaskForm):
email = StringField("Email", validators=[
DataRequired(message="Email obrigatório"),
Email(message="Email inválido")
])
senha = PasswordField("Senha", validators=[
DataRequired()
])
lembrar = BooleanField("Lembrar-me")
# Campos: StringField, TextAreaField,
# SelectField, IntegerField, DateField,
# FileField, BooleanField, RadioFieldFlaskForm é a classe base (inclui CSRF). Campos do wtforms com validators. DataRequired() = obrigatório. Email() valida formato (requer email-validator). Cada campo tem .data (valor) e .errors (lista de erros).
Proteção CSRF
# Flask-WTF ativa CSRF automaticamente!
# Requer SECRET_KEY na config.
# No template (obrigatório em todo form):
{{ form.hidden_tag() }}
<!-- ou manualmente: -->
<input type="hidden" name="csrf_token"
value="{{ csrf_token() }}">
# Para AJAX:
# <meta name="csrf-token" content="{{ csrf_token() }}">
# fetch(url, {
# headers: {"X-CSRFToken": token}
# })
# Excluir view específica:
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
@app.route("/webhook", methods=["POST"])
@csrf.exempt
def webhook(): ...CSRFProtect protege todos os POST. {{ form.hidden_tag() }} inclui o token. Para AJAX: enviar X-CSRFToken no header. @csrf.exempt exclui rotas (webhooks, APIs). Sem token → erro 400. Nunca desativar globalmente.
AJAX com forms
# View que retorna JSON:
@app.route("/api/validar", methods=["POST"])
def validar():
form = RegistoForm()
if form.validate():
return {"valido": True}
return {"valido": False, "erros": form.errors}, 400
# JavaScript:
# const token = document.querySelector(
# "[name=csrf-token]").content;
#
# fetch("/api/validar", {
# method: "POST",
# headers: {
# "Content-Type": "application/json",
# "X-CSRFToken": token
# },
# body: JSON.stringify({
# email: "ana@mail.com",
# csrf_token: token
# })
# })form.errors é um dict com erros por campo — serializável para JSON. Enviar X-CSRFToken no header para AJAX. form.validate() sem on_submit para APIs. Retornar 400 com erros para o frontend tratar.
Validadores
from wtforms.validators import (
DataRequired, Length, Email,
EqualTo, NumberRange, Regexp,
Optional, URL
)
class RegistoForm(FlaskForm):
nome = StringField(validators=[
Length(min=2, max=50,
message="2 a 50 caracteres")
])
email = StringField(validators=[
DataRequired(), Email()
])
senha = PasswordField(validators=[
Length(min=8, message="Mínimo 8 caracteres"),
Regexp(r"\d", message="Requer um número")
])
confirmar = PasswordField(validators=[
EqualTo("senha", message="Senhas não coincidem")
])
idade = IntegerField(validators=[
Optional(), NumberRange(min=18, max=120)
])Length(min, max) limita tamanho. EqualTo("campo") compara (confirmação de senha). Regexp() valida com regex. NumberRange() para números. Optional() permite vazio. message= personaliza o erro. Validadores executam em ordem.
Validador custom
from wtforms.validators import ValidationError
# Função validadora:
def email_unico(form, field):
user = User.query.filter_by(
email=field.data.lower()
).first()
if user:
raise ValidationError("Email já registado.")
# Usar no form:
class RegistoForm(FlaskForm):
email = StringField(validators=[
DataRequired(), Email(), email_unico
])
# Método validate_<campo> (automático):
class RegistoForm(FlaskForm):
username = StringField()
def validate_username(self, field):
if len(field.data) < 3:
raise ValidationError("Mínimo 3 caracteres.")
if " " in field.data:
raise ValidationError("Sem espaços.")Validador custom: função que recebe (form, field) e levanta ValidationError. Método validate_<campo> é chamado automaticamente. Ambos adicionam a field.errors. Ideal para validações que dependem da BD (unicidade).
Processar na view
@app.route("/registo", methods=["GET", "POST"])
def registo():
form = RegistoForm()
if form.validate_on_submit():
# Dados válidos:
user = User(
nome=form.nome.data,
email=form.email.data
)
user.set_password(form.senha.data)
db.session.add(user)
db.session.commit()
flash("Conta criada!", "sucesso")
return redirect(url_for("login"))
# GET ou validação falhou:
return render_template("registo.html", form=form)
# validate_on_submit() = POST + válidoform.validate_on_submit() verifica se é POST E dados válidos. form.campo.data acede ao valor limpo. Se inválido, re-renderiza com erros. Padrão: GET mostra form, POST processa. flash() + redirect() após sucesso (PRG pattern).
File upload com form
from flask_wtf.file import FileField, FileAllowed, FileRequired
class UploadForm(FlaskForm):
foto = FileField("Foto de perfil", validators=[
FileRequired(message="Selecione um ficheiro"),
FileAllowed(["jpg", "png", "webp"],
message="Apenas imagens!")
])
# Na view:
@app.route("/upload", methods=["POST"])
def upload():
form = UploadForm()
if form.validate_on_submit():
f = form.foto.data
nome = secure_filename(f.filename)
f.save(f"static/uploads/{nome}")
flash("Upload feito!")
return redirect(url_for("perfil"))
return render_template("upload.html", form=form)
# HTML: <form enctype="multipart/form-data">FileField para uploads. FileAllowed(["ext"]) valida extensão. FileRequired() torna obrigatório. form.campo.data é o objeto ficheiro. Formulário precisa de enctype="multipart/form-data". Combinar com secure_filename().
Renderizar no template
<!-- registo.html -->
<form method="POST" novalidate>
{{ form.hidden_tag() }}
<div class="field">
{{ form.nome.label }}
{{ form.nome(class="input", placeholder="Nome") }}
{% for erro in form.nome.errors %}
<span class="erro">{{ erro }}</span>
{% endfor %}
</div>
<div class="field">
{{ form.email.label }}
{{ form.email(class="input") }}
{% for erro in form.email.errors %}
<span class="erro">{{ erro }}</span>
{% endfor %}
</div>
{{ form.submit(class="btn") }}
</form>{{ form.hidden_tag() }} renderiza o token CSRF (obrigatório!). {{ form.campo() }} gera o input HTML. class= adiciona classes CSS. form.campo.errors lista erros de validação. form.campo.label gera o <label>.
Form com dados iniciais
# Editar registo existente:
@app.route("/post/<int:id>/editar", methods=["GET", "POST"])
def editar(id):
post = Post.query.get_or_404(id)
form = PostForm(obj=post) # preencher!
if form.validate_on_submit():
form.populate_obj(post) # atualizar!
db.session.commit()
flash("Post atualizado!")
return redirect(url_for("post", id=id))
return render_template("editar.html",
form=form, post=post)
# populate_obj copia form → objeto
# obj=post copia objeto → form (GET)
# Ou manualmente:
form = PostForm(titulo=post.titulo,
corpo=post.corpo)PostForm(obj=post) preenche o form com dados do objeto. form.populate_obj(post) faz o inverso (form → objeto). Ideal para edição: GET mostra dados atuais, POST atualiza. Alternativa: passar campos manualmente no construtor.
Blueprints
Criar blueprint
# app/auth/routes.py
from flask import Blueprint, render_template
bp = Blueprint(
"auth", # nome único
__name__, # módulo
url_prefix="/auth", # prefixo URL
template_folder="templates",
static_folder="static"
)
@bp.route("/login")
def login():
return render_template("auth/login.html")
@bp.route("/registo")
def registo():
return render_template("auth/registo.html")
# URLs finais: /auth/login, /auth/registoBlueprint(nome, __name__) cria um módulo independente. url_prefix prefixa todas as rotas. template_folder e static_folder são opcionais. Cada blueprint é uma mini-app. Ideal para separar auth, blog, admin, API.
Error handlers por BP
# Erro específico do blueprint:
@bp.app_errorhandler(404)
def bp_404(e):
return render_template("auth/404.html"), 404
# Before request só neste BP:
@bp.before_app_request
def verificar_auth():
if request.endpoint and \
request.endpoint.startswith("admin."):
if not current_user.is_admin:
abort(403)
# Context processor do BP:
@bp.context_processor
def inject_auth():
return {"auth_version": "2.0"}@bp.app_errorhandler() regista handlers globais. @bp.before_app_request executa antes de todos os requests (não só do BP). @bp.context_processor injeta variáveis nos templates. Útil para middleware específico de um módulo.
Testar blueprints
import pytest
from app import create_app
@pytest.fixture
def app():
app = create_app("testing")
app.config["TESTING"] = True
return app
@pytest.fixture
def client(app):
return app.test_client()
# Testar BP auth:
def test_login_page(client):
r = client.get("/auth/login")
assert r.status_code == 200
assert b"Login" in r.data
def test_login_post(client):
r = client.post("/auth/login", data={
"email": "ana@mail.com",
"senha": "123456"
}, follow_redirects=True)
assert b"Dashboard" in r.dataTestar BPs como rotas normais com test_client(). create_app("testing") usa config de teste. follow_redirects=True segue redirects. r.data é bytes — usar b"texto". Fixtures do pytest para setup. Um ficheiro de teste por BP.
Registar blueprints
# app/__init__.py
from flask import Flask
def create_app():
app = Flask(__name__)
from .auth.routes import bp as auth_bp
from .blog.routes import bp as blog_bp
from .api.routes import bp as api_bp
app.register_blueprint(auth_bp)
app.register_blueprint(blog_bp)
app.register_blueprint(api_bp, url_prefix="/api/v1")
return app
# url_prefix no registo sobrescreve
# o prefixo definido no blueprintapp.register_blueprint(bp) adiciona à app. Importar dentro do factory evita imports circulares. url_prefix no registo sobrescreve o do blueprint. Ordem de registo não importa. Cada BP pode ter o seu próprio prefixo.
API versioning
# api/v1/routes.py
bp_v1 = Blueprint("api_v1", __name__)
@bp_v1.route("/users")
def users():
return jsonify({"versao": 1, "users": []})
# api/v2/routes.py
bp_v2 = Blueprint("api_v2", __name__)
@bp_v2.route("/users")
def users():
return jsonify({"versao": 2, "data": []})
# Registar:
app.register_blueprint(bp_v1, url_prefix="/api/v1")
app.register_blueprint(bp_v2, url_prefix="/api/v2")
# /api/v1/users → versão 1
# /api/v2/users → versão 2Versionar com blueprints: um BP por versão. url_prefix="/api/v1" separa. Clientes antigos continuam a funcionar. Nova versão = novo BP sem quebrar existente. Alternativa: header Accept-Version. Padrão para APIs públicas.
Estrutura modular
app/ ├── __init__.py # create_app() ├── extensions.py # db, migrate, login ├── auth/ │ ├── __init__.py │ ├── routes.py # bp auth │ ├── forms.py # LoginForm │ └── templates/auth/ ├── blog/ │ ├── __init__.py │ ├── routes.py # bp blog │ ├── models.py # Post, Comment │ └── templates/blog/ ├── api/ │ ├── __init__.py │ └── routes.py # bp api └── templates/ # base.html global
Cada funcionalidade num pacote próprio. extensions.py centraliza db, migrate, login (evita imports circulares). Templates do BP em templates/nome_bp/. models.py por módulo. Escalável para equipas grandes.
Templates por blueprint
# Blueprint com templates próprios:
bp = Blueprint("blog", __name__,
template_folder="templates",
static_folder="static",
static_url_path="/blog/static"
)
# Estrutura:
# app/blog/
# templates/blog/
# index.html
# post.html
# static/
# blog.css
# No route do BP:
@bp.route("/")
def index():
# Procura em blog/templates/blog/index.html
return render_template("blog/index.html")
# Herdar do base global:
# {% extends "base.html" %}template_folder define pasta de templates do BP. Usar subpasta com nome do BP evita conflitos. static_folder + static_url_path para assets próprios. Templates do BP podem herdar do base.html global. Prefixar nomes para clareza.
url_for com blueprints
# Formato: "nome_bp.funcao"
url_for("auth.login") # /auth/login
url_for("blog.post", id=5) # /blog/post/5
url_for("api.users") # /api/v1/users
# No template:
# <a href="{{ url_for('auth.login') }}">Login</a>
# <a href="{{ url_for('blog.post', id=p.id) }}">
# Redirecionar entre BPs:
return redirect(url_for("auth.login"))
# Verificar endpoint atual:
request.endpoint # "auth.login"
request.blueprint # "auth"url_for("bp.funcao") referencia rotas de blueprints. O prefixo é o nome do BP (1º arg do Blueprint). request.endpoint mostra o endpoint atual. request.blueprint mostra o BP. Sempre usar url_for — nunca hardcode URLs.
Nested blueprints
# Flask 2.0+ suporta BPs aninhados:
parent = Blueprint("admin", __name__,
url_prefix="/admin")
child = Blueprint("users", __name__,
url_prefix="/users")
@child.route("/")
def listar():
return "Lista de users"
# Aninhar:
parent.register_blueprint(child)
app.register_blueprint(parent)
# URL final: /admin/users/
# Útil para:
# /admin/users/
# /admin/posts/
# /admin/settings/
# Cada sub-módulo é um BP filhoFlask 2.0+ permite parent.register_blueprint(child). Prefixos acumulam: /admin + /users = /admin/users. Ideal para painéis admin com sub-módulos. Cada nível é independente e testável. Evitar mais de 2 níveis de nesting.
Extensões e Deploy
Flask-Login (autenticação)
pip install flask-login
from flask_login import (
LoginManager, UserMixin,
login_user, logout_user,
login_required, current_user
)
login = LoginManager(app)
login.login_view = "auth.login" # redirect
class User(UserMixin, db.Model):
# UserMixin adiciona: is_authenticated,
# is_active, is_anonymous, get_id()
pass
@login.user_loader
def load_user(id):
return db.session.get(User, int(id))
# Proteger rota:
@app.route("/dashboard")
@login_required
def dashboard():
return f"Olá {current_user.nome}"flask-login gere sessões de utilizador. UserMixin adiciona métodos obrigatórios. @login_required protege rotas. current_user é o utilizador atual. login_user() / logout_user() gerem a sessão. login_view define redirect se não autenticado.
Deploy com Gunicorn
pip install gunicorn
# Executar:
gunicorn -w 4 -b 0.0.0.0:8000 "app:create_app()"
# Opções:
gunicorn \
--workers 4 \
--bind 0.0.0.0:8000 \
--timeout 120 \
--access-logfile - \
--error-logfile - \
"app:create_app()"
# Workers = (2 × CPU cores) + 1
# Nginx como reverse proxy:
# proxy_pass http://127.0.0.1:8000;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# systemd para gerir processogunicorn é o servidor WSGI de produção. -w 4 = 4 workers (processos). "app:create_app()" chama o factory. Nunca usar flask run em produção. Nginx na frente como reverse proxy. systemd para auto-restart.
Segurança e boas práticas
# 1. Headers de segurança:
from flask_talisman import Talisman
Talisman(app, force_https=True)
# 2. Rate limiting:
from flask_limiter import Limiter
limiter = Limiter(app, key_func=get_remote_address)
@app.route("/api/login")
@limiter.limit("5/minute")
def login(): ...
# 3. Nunca debug em produção:
app.config["DEBUG"] = False
# 4. Validar TODO input:
# request.json pode ser None!
data = request.get_json(silent=True) or {}
# 5. Usar parâmetros em SQL:
db.session.execute(
text("SELECT * FROM users WHERE id = :id"),
{"id": user_id}
)
# 6. HTTPS obrigatório em produçãoflask-talisman força HTTPS e headers seguros. flask-limiter limita requests (anti brute-force). Nunca DEBUG=True em produção. Validar input (get_json(silent=True)). Parâmetros em SQL (anti injection). HTTPS obrigatório. SECRET_KEY forte e em env vars.
Flask-RESTful (API)
pip install flask-restful
from flask_restful import Resource, Api, reqparse
api = Api(app)
class UserResource(Resource):
def get(self, user_id):
user = User.query.get_or_404(user_id)
return {"id": user.id, "nome": user.nome}
def put(self, user_id):
parser = reqparse.RequestParser()
parser.add_argument("nome", required=True)
args = parser.parse_args()
user.nome = args["nome"]
db.session.commit()
return {"ok": True}
def delete(self, user_id):
return "", 204
api.add_resource(UserResource, "/api/user/<int:user_id>")Resource define endpoints com métodos HTTP. Api(app) regista. reqparse valida argumentos. add_resource() mapeia URL. Retornar dict = JSON automático. Alternativa moderna: flask-smorest ou flask-restx com OpenAPI.
Docker
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", \
"app:create_app()"]
# docker-compose.yml
# services:
# web:
# build: .
# ports: ["8000:8000"]
# environment:
# - SECRET_KEY=super
# - DATABASE_URL=postgresql://...
# db:
# image: postgres:16
# volumes: [pgdata:/var/lib/postgresql/data]
# docker compose up --buildpython:3.12-slim como base (leve). Instalar dependências antes do COPY (cache Docker). gunicorn como CMD. docker-compose orquestra web + BD. Variáveis de ambiente para config. --build reconstrói a imagem.
Context processors
# Variáveis globais em todos os templates:
@app.context_processor
def inject_globals():
return {
"app_name": "Minha App",
"ano_atual": datetime.now().year,
"menu_items": ["Início", "Blog", "Contacto"]
}
# Em qualquer template:
# {{ app_name }} - {{ ano_atual }}
# Com blueprint:
@bp.context_processor
def inject_bp():
return {"bp_version": "1.0"}
# Utility functions:
@app.context_processor
def inject_utils():
return {"formatar_data": lambda d: d.strftime("%d/%m/%Y")}@app.context_processor injeta variáveis em todos os templates. Retornar dict. Executa em cada request (manter leve). Ideal para: nome da app, ano, menu, user atual. Alternativa: app.jinja_env.globals para funções.
Logging
import logging
from logging.handlers import RotatingFileHandler
def setup_logging(app):
handler = RotatingFileHandler(
"logs/app.log",
maxBytes=5_000_000, # 5MB
backupCount=5
)
handler.setFormatter(logging.Formatter(
"%(asctime)s %(levelname)s: %(message)s"
))
handler.setLevel(logging.INFO)
app.logger.addHandler(handler)
app.logger.setLevel(logging.INFO)
# Usar:
app.logger.info("Servidor iniciado")
app.logger.warning("Cache quase cheio")
app.logger.error(f"Erro: {e}")
# Em produção: log para stdout (Docker)app.logger é o logger do Flask. RotatingFileHandler limita tamanho dos logs. logging.Formatter define o formato. Níveis: DEBUG, INFO, WARNING, ERROR. Em Docker: log para stdout. Nunca logar dados sensíveis.
Testing com pytest
# conftest.py
import pytest
from app import create_app, db
@pytest.fixture
def app():
app = create_app("testing")
with app.app_context():
db.create_all()
yield app
db.drop_all()
@pytest.fixture
def client(app):
return app.test_client()
# test_routes.py
def test_home(client):
r = client.get("/")
assert r.status_code == 200
def test_create_post(client, auth_headers):
r = client.post("/posts", json={
"titulo": "Teste"
}, headers=auth_headers)
assert r.status_code == 201
# Executar: pytest -vconftest.py define fixtures partilhadas. create_app("testing") usa BD de teste. db.create_all() / drop_all() por teste. test_client() simula requests HTTP. pytest -v executa. Config testing: SQLALCHEMY_DATABASE_URI = "sqlite://" (memória).
Flask-Mail
pip install flask-mail
from flask_mail import Mail, Message
mail = Mail(app)
app.config["MAIL_SERVER"] = "smtp.gmail.com"
app.config["MAIL_PORT"] = 587
app.config["MAIL_USE_TLS"] = True
app.config["MAIL_USERNAME"] = "app@gmail.com"
app.config["MAIL_PASSWORD"] = "senha-app"
def enviar_email(destinatario, assunto, corpo):
msg = Message(
subject=assunto,
recipients=[destinatario],
body=corpo,
sender="noreply@app.com"
)
mail.send(msg)
# HTML:
msg.html = "<h1>Olá!</h1>"flask-mail envia emails via SMTP. Configurar servidor, porta, credenciais. Message() cria o email. mail.send() envia (síncrono). msg.html para conteúdo HTML. Em produção: usar fila (Celery) para não bloquear. Gmail requer senha de app.