Skip to content

Pra refazer o tutorial do Flask do início, com calma, comentando e estudando cada conceito.

Notifications You must be signed in to change notification settings

JordyAraujo/flask-tutorial

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Tutorial Flask

Este repositório tem como objetivo reproduzir o tutorial da documentação do Flask, comentando, pesquisando, estudando e principalmente traduzindo para o português, na intenção de trazer conhecimento de forma didática e acessível.

O tutorial passa pela criação de um blog chamado Flaskr. A ideia é que usuários possam se registrar, logar e criar, editar ou deletar posts. Também será possível empacotar e instalar a aplicação em outros computadores. Alguns comentários são feitos na primeira página e eu resolvi traduzí-los diretamente aqui:

  • Assumimos que você já é familiar com Python. O tutorial oficial na documentação do Python é uma boa forma de aprender ou revisar primeiro;
  • Apesar de ser pensado para dar um bom ponto de partida, este tutorial não cobre todas as características e ferramentas do Flask. Dá uma olhada no Quickstart para ver mais do que o Flask pode fazer, e então mergulhe na documentação para encontrar ainda mais. O tutorial também só usa o que é dado pelo Python e pelo Flask em si. Em outros projetos, você pode decidir usar as Extensões do Flask ou outras bibliotecas para facilitar algumas atividades;
  • O Flask é flexível. Ele não requer que você use nenhum projeto ou estrutura particular. Porém, quando estamos começando, ajuda se formos mais estruturados e organizados. Portanto, o tutorial vai seguir uma certa estrutura, para evitar certas "armadilhas" ou "vícios" que podem ser encontrados por iniciantes, além de criar um projeto fácil de expandir. À medida que ficar mais confortável com o Flask, você poderá sair dessa estrutura e tirar maior proveito da flexibilidade do Flask;
  • O projeto completo do tutorial está disponível como exemplo no repositório do Flask, caso queira comparar com o nosso, à medida que proseguimos.

Estrutura do Projeto

Crie um repositório e entre nele:

$ mkdir flask-tutorial
$ cd flask-tutorial

Siga as instruções de instalação para configurar um ambiente virtual Python e instalar o Flask pro seu projeto.

Após instalar, rode o seguinte comando:

$ pip freeze > requirements.txt

Isso vai criar o arquivo requirements.txt, contendo as bibliotecas instaladas até então no seu ambiente virtual, com suas respectivas versões. Se você instalou apenas o Flask, note que outras bibliotecas foram adicionadas, pois são auxiliares ao Flask. Você pode deixar apenas o Flask na lista, mas as outras serão instaladas com ele da mesma forma.

Para instalar bibliotecas salvas dessa maneira, use o comando $ pip install -r requirements.txt

A partir de agora o tutorial vai considerar que você está trabalhando do diretório flask-tutorial. Os nomes dos arquivos acima de cada bloco de código serão relativosa este local.

Uma aplicação Flask pode ser tão simples quanto um arquivo.

hello.py

from flask import Flask

app = Flask(__name__)


@app.route('/')
def hello():
    return 'Hello, World!'

Porém, conforme o projeto vai crescendo, fica quase impossível manter o controle de muito código em um só arquivo. Projetos em Python usam pacotes para organizar código em múltiplos módulos que podem ser importados onde neessários, e nós não faremos diferente.

O diretório do projeto irá conter:

  • flaskr/, um pacote contendo nossa aplicação e seus arquivos;
  • tests/, um diretório com os módulos de teste;
  • .venv/, nosso ambiente virtual;
  • requirements.txt, com as bibliotecas que usaremos (basicamente o Flask nesse caso);
  • Arquivos de instalação, dizendo para o Python como instalar seu projeto;
  • Configurações de controle de versão (como o Git).

No final, a estrutura do projeto será mais ou menos assim:

/home/user/Projects/flask-tutorial
├── flaskr/
│   ├── __init__.py
│   ├── db.py
│   ├── schema.sql
│   ├── auth.py
│   ├── blog.py
│   ├── templates/
│   │   ├── base.html
│   │   ├── auth/
│   │   │   ├── login.html
│   │   │   └── register.html
│   │   └── blog/
│   │       ├── create.html
│   │       ├── index.html
│   │       └── update.html
│   └── static/
│       └── style.css
├── tests/
│   ├── conftest.py
│   ├── data.sql
│   ├── test_factory.py
│   ├── test_db.py
│   ├── test_auth.py
│   └── test_blog.py
├── .venv/
├── setup.py
└── MANIFEST.in

Configurando a aplicação

Uma aplicação Flask nada mais é do que uma instância da classe Flask. Todas as informações da aplicação, bem como configurações e URLs, serão registradas nesta classe.

A forma mais direta de se criar uma aplicação em Flask é criando uma instância global no topo do código, assim como fizemos no arquivo hello.py. Isso pode ser simples e útil, mas pode causar complicações conforme o projeto evolui.

Ao invés de criar uma instância global, vamos criá-la dentro de uma função. Essa função é chamada de Application Factory, ou função construtora. Quaisquer configurações, registros ou outras definições devem ser feitos dentro desta função, e então a aplicação será retornada.

A tal da Application Factory

Então vamos codar! Comece deletando o arquivo hello.py. Ele não nos será útil. Depois crie o diretório flaskr e o arquivo __init__.py. Este arquivo tem duas funções: conter a Factory e dizer pro Python que o diretório flaskr deve ser tratado como um pacote.

$ mkdir flaskr

flaskr/__init__.py

import os

from flask import Flask


def create_app(test_config=None):
    # Cria e configura a aplicação
    app = Flask(__name__, instance_relative_config=True)
    app.config.from_mapping(
        SECRET_KEY='dev',
        DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'),
    )

    if test_config is None:
        # Carrega as configurações da instância, quando existem e quando não testando
        app.config.from_pyfile('config.py', silent=True)
    else:
        # Carrega as configurações de teste
        app.config.from_mapping(test_config)

    # Garante que o diretório da instância existe
    try:
        os.makedirs(app.instance_path)
    except OSError:
        pass

    # Uma página simples que diz Olá
    @app.route('/hello')
    def hello():
        return 'Hello, World!'

    return app

Iremos adicionar mais coisas à função create_app, mas por hora ela já faz bastante coisa:

  1. A instância do Flask é criada no comando app = Flask(__name__, instance_relative_config=True).

    • __name__ é o nome do módulo Python atual. A aplicação precisa saber onde ele está localizado, para configurar alguns caminhos, e __name__ é uma forma interessante de dizer isso.

    • instance_relative_config=True diz à aplicação que os arquivos de configuração são relativos ao diretório de instância. O diretório de instância fica localizado fora do pacote flaskr e pode guardar dados que não devem ser commitados, como segredos de configuração e o arquivo de banco de dados.

  2. app.config.from_mapping() define algumas configurações padrão que a aplicação irá usar:

    • SECRET_KEY é usada pelo Flask e suas extensões para manter dados seguros. Aqui setamos para 'dev' para usar um valor conveniente durante o desenvolvimento, mas deve ser substituída por um valor aleatório ao subir para produção.
    • DATABASE é o caminho onde o arquivo de banco SQLite será salvo. Está localizado em app.instance_path, que é o caminho que o Flask escolheu para seu diretório de instância. Veremos mais sobre banco de dados na próxima sessão.
  3. app.config.from_pyfile() substitui as configurações padrão com os valores no arquivo config.py, localizado no diretório de instância, se ele existir. Por exemplo, isso pode ser usado ao fazer deploy, para definir um valor real para SECRET_KEY.

    • test_config também pode ser passado à factory e será usado no lugar da configuração de instância. Assim, os testes que vamos escrever mais pra frente no tutorial, podem ser configurados independente dos valores escolhidos durante o desenvolvimento.
  4. os.makedirs() garante que app.instance_path() existe. O Flask não cria o diretório de instância automaticamente, mas precisamos criar para que o arquivo do banco SQLite seja criado lá dentro.

  5. @app.route() cria uma rota simples para que você possa ver a aplicação rodando antes de ver o resto do tutorial. Aqui criamos uma conexão entre a URL /hello e a função que retorna uma resposta, neste caso, a string 'Hello,World!'.

Rodando a aplicação

Agora podemos rodar nossa aplicação com o comando flask. Do terminal, diremos ao Flask onde achar nossa aplicação e rodá-la em modo de debug. Lembre-se, você ainda deve estar no diretório flask-tutorial, não no pacote flaskr.

O modo de debug nos mostra um debugador interativo sempre que a página levanta uma excessão, e reinicia o servidor sempre que fazemos alterações no código. Você pode deixar ele rodando e é só recarregar as páginas conforme avança no código pelo tutorial.

$ flask --app flaskr --debug run

O retorno deve ser parecido com isso:

* Serving Flask app "flaskr"
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: nnn-nnn-nnn

Visite http://127.0.0.1:5000/hello num navegador que você verá a mensagem "Hello, World!". Parabéns, você está rodando sua aplicação Flask!

Se outra aplicação já estiver utilizando a porta 5000, você verá um erro OSError: [Errno 98] ou OSError: [WinError 10013] quando o servidor tenta iniciar. Veja a documentação sobre endereço em uso para ver como lidar com isso (ou apenas para a outra aplicação, se não for gerar problema).

Definindo e acessando o Banco de Dados

A aplicação vai usar um banco SQLite pra guardar usuários e posts. O próprio Python já tem suporte nativo pro SQLite com o módulo sqlite3.

O SQLite é conveniente por não precisar de um banco rodando separado em outro servidor. Porém, se requisições concorrentes tentarem escrever ao mesmo tempo no banco, elas serão mais demoradas, pois cada escrita acontece individualmente. Pequenas aplicações não vão notar diferença. Conforme seu uso cresce, será melhor trocar para outro banco.

O tutorial não entra em detalhes sobre SQL. Se você não é familiar, a documentação do SQLite descreve bem a linguagem.

Conectando ao Banco de Dados

A primeira coisa a se fazer quando se trabalha com SQLite (e várias outras bibliotecas de banco em Python) é criar uma conexão com ele. Quaisquer consultas e operações acontecem usando a conexão, que é fechada epois que o trabalho termina.

Em aplicações web essas conexões são tipicamente amarradas à requisição. Ela é criada em algum ponto durante o processamento da requisição, e fechada antes da resposta ser enviada.

flaskr/db.py

import sqlite3

import click
from flask import current_app, g


def get_db():
    if 'db' not in g:
        g.db = sqlite3.connect(
            current_app.config['DATABASE'],
            detect_types=sqlite3.PARSE_DECLTYPES
        )
        g.db.row_factory = sqlite3.Row

    return g.db


def close_db(e=None):
    db = g.pop('db', None)

    if db is not None:
        db.close()

g é um objeto especial único para cda requisição. Ele é utilizado para guardar dados que poderão ser acessados por múltiplas funções durante a requisição. A conexão é criada e reutilizada, ao invés de de criar uma nova conexão sempre que get_db for chamada na mesma requisição.

current_app é outro objeto especial que aponta para a aplicação Flask que está lidando com a requisição. Uma vez que utilizamos uma função construtora, o objeto da aplicação não existe quando estamos escrevendo o resto do código. get_db será chamada depois que a aplicação já foi criada e está lidando com uma requisição, portanto current_app pode ser usado.

sqlite3.connect() estabelece uma conexão com o arquivo apontado pela chave de configuração DATABASE. Este arquivo não precisa existir agora, e não irá, até que inicializemos o banco de dados mais à frente.

sqlite3.Row diz à conexão para retornar linhas que funcionam como dicionários. Isso nos permite acessar colunas por nome.

close_db checa se uma conexão foi criada conferindo se existe um g.db. Se a conexão existe, é fechada. Mais à frente iremos mencionar close_db na função construtora, para que seja chamada após cada requisição.

Criando as tabelas

No SQLite, os dados são guardados em tabelas e colunas. Estes precisam ser criados antes que possamos guardar ou acessar dados. Flaskr irá salvar dados de usuários na tabela user, e posts na tabela posts. Criaremos um arquivo com os comandos SQL necessários para criar as tabelas vazias:

flaskr/schema.sql

DROP TABLE IF EXISTS user;
DROP TABLE IF EXISTS post;

CREATE TABLE user (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  username TEXT UNIQUE NOT NULL,
  password TEXT NOT NULL
);

CREATE TABLE post (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  author_id INTEGER NOT NULL,
  created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  title TEXT NOT NULL,
  body TEXT NOT NULL,
  FOREIGN KEY (author_id) REFERENCES user (id)
);

Agora vamos adicionar as funções que vão executar esses comandos SQL ao arquivo db.py:

flaskr/db.py

def init_db():
    db = get_db()

    with current_app.open_resource('schema.sql') as f:
        db.executescript(f.read().decode('utf8'))


@click.command('init-db')
def init_db_command():
    """Clear the existing data and create new tables."""
    init_db()
    click.echo('Initialized the database.')

open_resource() abre um arquivo relativo ao pacote flaskr, o que é útil já que nós não necessariamente saberemos onde fica isso quando fizermos deploy da aplicação mais tarde. get_db retorna uma conexão com o banco de dados, que é usada para executar os comandos lidos no arquivo.

click.command() define um comando de linha de comando chamado init-db, que chama a função init_db e mostra uma mensagem de sucesso para o usuário. Você pode consultar Interface de Linha de Comando para aprender mais sobre escrever seus próprios comandos.

Registrando com a aplicação

As funções close_db e init_db_command precisam ser registradas na instância da aplicação; caso contrário, elas não serão usadas pela aplicação. Porém, como estamos usando uma função construtora, esta instãncia não está disponível quando estamos escrevendo as funções. Ao invés disso, criaremos uma função que recebe a aplicação como parâmetro e faz o registro.

flaskr/db.py

def init_app(app):
    app.teardown_appcontext(close_db)
    app.cli.add_command(init_db_command)

app.teardown_appcontext() diz ao Flask para chamar esta função depois que a requisição termina de ser processada, após retornar a resposta.

app.cli.add_command() adiciona um novo comando que pode ser chamado no terminal com o comando flask.

Importe e chame essa função dentro da construtora. Coloque o novo código na função construtora.

flaskr/__init__.py

import os

from flask import Flask
from . import db


def create_app(test_config=None):
    # Cria e configura a aplicação
    app = Flask(__name__, instance_relative_config=True)
    app.config.from_mapping(
        SECRET_KEY='dev',
        DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'),
    )

    db.init_app(app)

    # Código existente omitido


    return app

Inicie o arquivo de Banco de Dados

Agora que init-db foi registrado no app, ele pode ser chamado usando o comando flask, assim como o comando run usado anteriormente.

Se você ainda está rodando o servidor, você pode pará-lo agora, ou rodar esse comando em um novo terminal. Se você vai usar um novo terminal, não esqueça de entrar no diretório e ativar seu ambiente virtual, como descrito aqui.

Rode o comando init-db:

$ flask --app flaskr init-db
Initialized the database.

Um arquivo flaskr.sqlite será criado no diretório de instância do seu projeto.

Blueprints e Views

Uma view é o código que você escreve para responder a requisições feitas à sua aplicação. O Flask usa padrões para comparar a URL da requisição com a view que irá processá-la. A view retorna dados que o Flask converte numa resposta. O Flask também pode ir em outra direção e gerar uma URL para a view baseado em seu nome e argumentos.

Criando uma Blueprint

Uma Blueprint é uma forma de organizar um grupo de views relacionadas e outros códigos. Ao invés de registrar views e outros códigos direto em uma aplicação, eles são registrados com uma blueprint. Então a blueprint é registrada com a aplicação quando está disponível na função construtora.

Flaskr terá duas blueprints, uma para funções de autenticação e uma para as funções sobre os posts. O código para cada blueprint irá num módulo diferente. Uma vez que o blog precisa fazer autenticação, vamos escrever suas funções primeiro.

flaskr/auth.py

import functools

from flask import (
    Blueprint, flash, g, redirect, render_template, request, session, url_for
)
from werkzeug.security import check_password_hash, generate_password_hash

from flaskr.db import get_db

bp = Blueprint('auth', __name__, url_prefix='/auth')

Isso cria uma Blueprint chamada auth. Assim como o objeto da aplicação, a blueprint precisa saber onde é definida, então __name__ é passado como segundo argumento. O prefixo url_prefix será adicionado ao início de todas as URLs associadas com a blueprint.

Importamos e registramos a blueprint na construtora usando a função app.register_blueprint(). Colocaremos o novo código na função construtora:

flaskr/__init__.py

import os

from flask import Flask
from . import db
from . import auth


def create_app(test_config=None):
    # Cria e configura a aplicação
    app = Flask(__name__, instance_relative_config=True)
    app.config.from_mapping(
        SECRET_KEY='dev',
        DATABASE=os.path.join(app.instance_path, 'flaskr.sqlite'),
    )

    db.init_app(app)
    app.register_blueprint(auth.bp)
    # Código existente omitido


    return app

A blueprint de autenticação terá views para registrar novos usuários, bem como login e logout.

Nossa primeira view: register

Quando usuários visitam a URL /auth/register, a view register vai retornar um HTML com um formulário a ser preenchido. Ao submeter o formulário, as entradas serão validadas e dependendo da validação, o usuário será criado e seremos redirecionados à pagina de login, ou o formulário será recarregado com uma mensagem de erro, se for o caso.

Por enquanto vamos escrever o código da view. Mais na frente iremos escrever os templates para gerar o HTML do formulário.

flaskr/auth.py

@bp.route('/register', methods=('GET', 'POST'))
def register():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        db = get_db()
        error = None

        if not username:
            error = 'Username is required.'
        elif not password:
            error = 'Password is required.'

        if error is None:
            try:
                db.execute(
                    "INSERT INTO user (username, password) VALUES (?, ?)",
                    (username, generate_password_hash(password)),
                )
                db.commit()
            except db.IntegrityError:
                error = f"User {username} is already registered."
            else:
                return redirect(url_for("auth.login"))

        flash(error)

    return render_template('auth/register.html')

Vamos ver o que a função register faz:

  1. @bp.route associa a URL /register à função view register. Quando o Flask recebe uma requisição para /auth/register, ele chama a view register e usa seu retorno como resposta.

  2. Se o usuário submeteu o formulário, o método da requisição (request.method) será 'POST'. Neste caso, começamos a validar a entrada.

  3. request.form é um tipo especial de dicionário que mapeia as chaves e valores do formulário submetido. O usuário vai preencher seus username e password.

  4. Validamos que username e password não estão vazios.

  5. Se a validação suceder, inserimos o novo usuário no banco de dados.

    • db.execute recebe uma instrução SQL com parâmetros representados por ? para entradas do usuário, e uma tupla de valores que os preencherão na ordem fornecida. A biblioteca de banco irá cuidar de converter as strings para que não fiquemos vulneráveis a ataques de injeção de SQL
    • Por segurança, senhas nunca devem ser salvas diretamente no banco de dados. Ao invés disso, a função generate_password_hash() é usada para criptografar seguramente a senha. Essa senha criptografada é que será salva. Como esta instrução modifica dados, a função db.commit() precisa ser chamada para salvar as alterações.
    • Um erro sqlite3.IntegrityError irá ocorrer se o username já existe, o que deve ser mostrado para o usuário como um erro de validação.
  6. Depois de salvar o usuário, somos redirecionados para a página de login. url_for() gera a URL para a view de login baseado em seu nome. É preferível usar esse método ao invés de escrever a URL diretamente, pois assim podemos mudar a URL depois sem precisar mudar todo o código relacionado. redirect() gera uma resposta de redirecionamento para a URL gerada.

  7. Se a validação falhar, o erro é mostrado para o usuário. flash() salva informações que podem ser recuperadas quando renderizando o template.

  8. Quando o usuário navega inicialmente para auth/register, ou acontece um erro de validação, uma página HTML com o formulário de registro deve ser mostrada. render_template() irá renderizar um template contendo o HTML que iremos escrever nas próximas etapas.

Login

Esta _view_segue o mesmo padrão que a view de register acima.

flaskr/auth.py

@bp.route('/login', methods=('GET', 'POST'))
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        db = get_db()
        error = None
        user = db.execute(
            'SELECT * FROM user WHERE username = ?', (username,)
        ).fetchone()

        if user is None:
            error = 'Incorrect username.'
        elif not check_password_hash(user['password'], password):
            error = 'Incorrect password.'

        if error is None:
            session.clear()
            session['user_id'] = user['id']
            return redirect(url_for('index'))

        flash(error)

    return render_template('auth/login.html')

Aqui vemos algumas diferenças da view register:

  1. O usuário é consultado primeiro e salvo em uma variável para usarmos depois. fetchone() retorna uma linha da consulta. Se a consulta não retornar resultados, retorna None. Usaremos fetchall() depois, para retornar uma lista com todos os resultados.

  2. check_pasword_hash() criptografa a senha submetida da mesma forma que a criptografada salva no banco e as compara seguramente. Se forem iguais, a senha é válida.

  3. session é um dicionário que guarda dados entre requisições. Que a validação é bem sucedida, o id do usuário é guardado em uma nova sessão. Os dados são guardado em um cookie que é enviado para o navegador, que as manda de volta nas próximas requisições. O próprio Flask assina os dados seguramente para que não possam ser adulterados.

Agora que o id do usuário está salvo na sessão, ele ficará disponível nas próximas requisições. No início de cada requisição, se um usuário está logado, suas informações devem ser carregadas e estar disponíveis para outras views.

flaskr/auth.py

@bp.before_app_request
def load_logged_in_user():
    user_id = session.get('user_id')

    if user_id is None:
        g.user = None
    else:
        g.user = get_db().execute(
            'SELECT * FROM user WHERE id = ?', (user_id,)
        ).fetchone()

bp.before_app_request() registra uma função que roda antes da função da view, independente da URL requisitada. load_logged_in_usercheca se um id de usuário está salvo na sessão e pega as informações desse usuário no banco de dados, salvando em g.user, que tem a duração da requisição. Se não há id de usuário, ou se o id não existe, g.userserá ``Ǹone```.

Logout

Para deslogar, precisamos remover o id do usuário da sessão.Assim load_logged_in_user não irá carregar um usuário nas próximas requisições.

flaskr/auth.py

@bp.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('index'))

Solicitando autenticação em outras views

Criar, editar e deletar posts no blog só deverá ser possível com um usuário logado. Um decorador pode ser usado para checar isso em cada view em que o aplicarmos.

flaskr/auth.py

def login_required(view):
    @functools.wraps(view)
    def wrapped_view(**kwargs):
        if g.user is None:
            return redirect(url_for('auth.login'))

        return view(**kwargs)

    return wrapped_view

Este decorador retorna uma nova função view que envolve a _view_original na qual é aplicada. A nova função checa se um usuário foi carregadoe redireciona para a página de login em caso negativo. Se um usuário foi carregado, a view original é chamada e continua normalmente. Usaremos este decorador quando escrevermos as views do blog.

Endpoints e URLs

A função url_for() gera a URL para a view em um nome e argumentos. O nome associado com a view também é chamado de endpoint, e por padrão tem o mesmo nome da função da view.

Por exemplo, a view hello() que foi adicionada à função construtora mais cedo no tutorial tem o nome 'hello' e pode ser referenciada com url_for('hello'). Se ela recebe um argumento, o que veremos depois, usamos a sintaxe url_for('hello', who='World').

Quando usamos uma blueprint, o nome dela deve ser adicionado ao nome da função, assim o endpoint para a função login que escrevemos será 'auth.login' pois o adicionamos à blueprint 'auth'.

About

Pra refazer o tutorial do Flask do início, com calma, comentando e estudando cada conceito.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages