Flask em 40 Minutos ou Menos

Me
Eu faço perguntas em reuniões que eu não sei nada e reunião explode; não é de propósito
Eu falo rárpido

Motivação

  • Flask é super simples, mas poderoso.
  • Python é super simples, mas poderoso.
  • Ninguém fala como é colocar isso em produção.

A aplicação

Um sistema de atas de reunião onde as atas são arquivos MarkDown, com a data no nome.

Estrutura de diretórios

├── ata
│   ├── defaults.py
│   ├── __init__.py
│   ├── main.py
│   ├── static
│   │   └── style.css
│   └── templates
│       ├── entry.html
│       ├── entry_not_found.html
│       ├── index.html
│       ├── layout.html
│       └── page_not_found.html
├── contents
├── setup.py
├── wsgi.py
├── MANIFEST.in
└── requirements.txt

├── ata

Módulo/fontes.

│   ├── defaults.py

Valores default da aplicação.

│   ├── __init__.py

__init__.py é necessário para indicar ao Python que o diretório pode se lido.

│   ├── main.py

O arquivo inicial da aplicação.

│   ├── static

Arquivos estáticos.

│   └── templates

Arquivos de template.

├── contents

Onde o conteúdo a ser apresentado está.

└── requirements.txt

Dependências do projeto.

├── wsgi.py

Execução do projeto em modo wsgi.

requirements.txt

flask~=0.12.2
Markdown~=2.6.9

main.py

Imports

import os
import os.path

import markdown

from flask import Flask
from flask import render_template

main.py

A App

app = Flask(__name__)

main.py

Configuração

app.config.from_object('ata.defaults')
app.config.from_envvar('ATA_SETTINGS', silent=True)

main.py

A primeira rota.

@app.route('/')
def index():

main.py

O índice

    content = os.listdir(app.config['STORAGE'])
    data = []
    for entry in sorted(content, reverse=True):
        app.logger.debug('Found %s', entry)
        if not entry.endswith('.md'):
            continue

        with open(os.path.join(app.config['STORAGE'], entry)) as origin:
            content = origin.read()
            output = markdown.markdown(content)
            try:
                first_paragraph_end = output.index('

') paragraph = output[:first_paragraph_end] except ValuError: paragraph = output entry_name = entry[:entry.find('.md')] data.append((entry_name, output)) return render_template('index.html', data=data)

main.py

Entradas específicas

@app.route('/<entry_name>')
def show_entry(entry_name):
    filename = os.path.join(app.config['STORAGE'], entry_name + '.md')
    with open(filename) as origin:
        content = origin.read()
        output = markdown.markdown(content)

    return render_template('entry.html', output=output,
                           entry=entry_name)

main.py

Tratamento de erros.

@app.errorhandler(404)
def page_not_found():
    return render_template('page_not_found.html')

main.py

Tratamento de erros

@app.errorhandler(FileNotFoundError)
def entry_not_found(_):
    return render_template('entry_not_found.html')

defaults.py

DEBUG=True
STORAGE='/home/jbiason/src/ata/contents'

style.css

h1#header {
    background-color: black;
    color: white;
    padding: 10px;
}

.entry {
    margin-left: 30px;
    margin-right: 30px;
    margin-bottom: 15px;
}

templates

layout.html

<!doctype html>
<html>
    <head>
        <title>Atas de Reunião</title>
        <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
    </head>

    <body>
        <h1 id='header'>
            Atas de reuniao
        </h1>
        {% block maincontent %}{% endblock %}
    </body>
</html>

templates

index.html

{% extends "layout.html" %}
    {% block maincontent %}
        <div class='entry'>
            {% for entry, text in data %}
            <a href="{{ url_for('show_entry', entry_name=entry) }}">{{ entry }}</a>
                {{ text|safe }}
            {% endfor %}
        </div>
    {% endblock %}

templates

entry.html

{% extends "layout.html" %}
    {% block maincontent %}
        <div class="entry">
            <div class="title">{{ entry }}</div>
            {{ output|safe }}
        </div>
    {% endblock %}

templates

page_not_found.html

{% extends "layout.html" %}
    {% block maincontent %}
        <h2>Page not found</h2>
    {% endblock %}

templates

entry_not_found.html

{% extends "layout.html" %}
    {% block maincontent %}
        <h2>Entry not found</h2>
    {% endblock %}

setup.py

from setuptools import setup

with open('requirements.txt') as origin:
    requirements = origin.readlines()

setup(name='ata',
      version='0.1',
      long_description='A Flask app',
      packages=['ata'],
      zip_safe=False,
      include_package_data=True,
      install_requires=requirements)

wsgi.py

from ata.main import app as application

MANIFEST.in

recursive-include ata/templates *
recursive-include ata/static *
include requirements.txt

Rodando

(Dev server)


FLASK_APP=ata/main.py flask run
                        

export FLASK_APP=ata/main
flask run
                        

Rodando de verdade

Dependências de sistema

sudo dnf install nginx uwsgi uwsgi-plugin-python3
sudo yum install nginx uwsgi uwsgi-plugin-python3

Rodando de verdade

Virtualenv (primeira vez)

mkdir -p /usr/local/venv
mkdir -p /usr/local/apps
python -m venv /usr/local/venv/ata

Rodando de verdade

Instalando novas versões

tar xzf Ata-0.1.tar.gz --one-top-level=/usr/local/apps/
ln -sf /usr/local/apps/Ata-0.1 /usr/local/apps/ata
source /usr/local/venv/ata/bin/activate
python3 -m pip install -U -r /usrlocal/apps/ata/requirements.txt

Rodando de verdade

Primeira configuração uwsgi

/etc/uwsgi.d/ata.ini

[uwsgi]
env=ATA_SETTINGS=/etc/ata.cfg
chdir=/usr/local/apps/ata
module=wsgi
plugins=python3
virtualenv=/usr/local/venv/ata
uid=uwsgi
gid=uwsgi
processes=4
socket=/var/run/uwsgi/ata.sock

Rodando de verdade

Primeira configuração da app

/etc/ata.cfg

DEBUG=False
STORAGE=/var/db/ata/

Rodando de verdade

Primeira configuração do nginx

/etc/nginx/conf.d/ata.conf

pstream ata-wsgi {
    server  unix:///var/run/uwsgi/ata.sock;
}

server {
    include /etc/nginx/uwsgi_params;

    access_log /var/log/nginx/ata.log main;

    location /static/ {
        root /usr/local/apps/ata/static
    }

    location / {
        uwsgi_read_timeout  3000;
        uwsgi_pass azionmanager-uwsgi;
    }
}

Rodando de verdade

Depois da primeira execução

(Ou seja, updates)

tar xzf Ata-0.1.tar.gz --one-top-level=/usr/local/apps/
ln -sf /usr/local/apps/Ata-0.1 /usr/local/apps/ata
source /usr/local/venv/ata/bin/activate
python3 -m pip install -U -r /usrlocal/apps/ata/requirements.txt
service uwsgi restart
service nginx restart
                        

Perguntas?