|
|
|
|
<!doctype html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="utf-8">
|
|
|
|
|
|
|
|
|
|
<title>Flask em 40 Minutos ou Menos</title>
|
|
|
|
|
|
|
|
|
|
<meta name="description" content="Colocando uma aplicação Flask em produção em 40 minutos (ou menos)">
|
|
|
|
|
<meta name="author" content="Julio Biason">
|
|
|
|
|
|
|
|
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
|
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
|
|
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, minimal-ui">
|
|
|
|
|
|
|
|
|
|
<link rel="stylesheet" href="reveal.js/css/reveal.css">
|
|
|
|
|
<link rel="stylesheet" href="reveal.js/css/theme/night.css" id="theme">
|
|
|
|
|
|
|
|
|
|
<!-- Code syntax highlighting -->
|
|
|
|
|
<link rel="stylesheet" href="reveal.js/lib/css/zenburn.css">
|
|
|
|
|
|
|
|
|
|
<!-- Printing and PDF exports -->
|
|
|
|
|
<script>
|
|
|
|
|
var link = document.createElement( 'link' );
|
|
|
|
|
link.rel = 'stylesheet';
|
|
|
|
|
link.type = 'text/css';
|
|
|
|
|
link.href = window.location.search.match( /print-pdf/gi ) ? 'css/print/pdf.css' : 'css/print/paper.css';
|
|
|
|
|
document.getElementsByTagName( 'head' )[0].appendChild( link );
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<!--[if lt IE 9]>
|
|
|
|
|
<script src="lib/js/html5shiv.js"></script>
|
|
|
|
|
<![endif]-->
|
|
|
|
|
|
|
|
|
|
<style type="text/css" media="screen">
|
|
|
|
|
.happy {
|
|
|
|
|
color: yellow;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.reveal section img {
|
|
|
|
|
border: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.reveal ul.empty {
|
|
|
|
|
list-style: none outside;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
li {
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.cursor {
|
|
|
|
|
background-color: #666;
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
img {
|
|
|
|
|
max-height: 90%;
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
|
|
|
|
|
<body>
|
|
|
|
|
<div class="reveal">
|
|
|
|
|
<div class="slides">
|
|
|
|
|
<section>
|
|
|
|
|
<section data-background="_images/flask.png" data-header>
|
|
|
|
|
<h1 class="semi-opaque">Flask em 40 Minutos ou Menos</h1>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<section>
|
|
|
|
|
<img src="_images/avatar-20170726.png" alt="Me" style="float:left;width:200px;" class="no-border">
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<ul class="empty">
|
|
|
|
|
<li>Júlio Biason</li>
|
|
|
|
|
<li>@juliobiason</li>
|
|
|
|
|
<li>julio.biason@gmail.com</li>
|
|
|
|
|
<li><a href="http://presentations.juliobiason.net">http://presentations.juliobiason.net</a></li>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<img src="_images/start-a-fight.jpg" alt="Eu faço perguntas em reuniões que eu não sei nada e reunião explode; não é de propósito" class='stretch'/>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Eu sou famoso (ou era) por entrar eu reuniões, fazer uma
|
|
|
|
|
pergunta e a reunião explodir em discussões (úteis, diga-se
|
|
|
|
|
de passagem). Mas eu não fazia isso de propósito.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<img src="_images/fast-speaker.jpg" alt="Eu falo rárpido">
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Eu também tenho costume de falar rápido.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<section>
|
|
|
|
|
<h2>Motivação</h2>
|
|
|
|
|
|
|
|
|
|
<ul>
|
|
|
|
|
<li class="fragment">Flask é super simples, mas poderoso.</li>
|
|
|
|
|
<li class="fragment">Python é super simples, mas poderoso.</li>
|
|
|
|
|
<li class="fragment">Ninguém fala como é colocar isso em produção.</li>
|
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Uma das coisas que o pessoal sempre comenta é
|
|
|
|
|
como é fácil programar em Python, como Flask é
|
|
|
|
|
simples, como Django consegue fazer um monte de
|
|
|
|
|
coisa (e é fácil), mas sempre falta aquela parte
|
|
|
|
|
de "e como eu coloco isso em um servidor de
|
|
|
|
|
verdade ao invés de ficar executando no development
|
|
|
|
|
server?"
|
|
|
|
|
|
|
|
|
|
É isso que eu vou tentar responder nessa
|
|
|
|
|
apresentação.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<section>
|
|
|
|
|
<h2>A aplicação</h2>
|
|
|
|
|
|
|
|
|
|
<p>
|
|
|
|
|
Um sistema de atas de reunião onde as atas
|
|
|
|
|
são arquivos MarkDown, com a data no nome.
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Não que seja um sistema super complexo (nem
|
|
|
|
|
banco de dados tem!) mas serve pra mostrar
|
|
|
|
|
Flask o suficiente e ser complicado o
|
|
|
|
|
suficiente para ter as "pegadinhas" de se
|
|
|
|
|
colocar algo desse tipo em produção.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2>Estrutura de diretórios</h2>
|
|
|
|
|
|
|
|
|
|
<pre>├── 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</pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Essa é a estrutura básica de uma aplicação Flask
|
|
|
|
|
(ou basicamente qualquer aplicação Python). Na
|
|
|
|
|
sequencia veremos o que cada diretório contém e
|
|
|
|
|
sua funcionalidade.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h3><code>├── ata</code></h3>
|
|
|
|
|
|
|
|
|
|
<p>Módulo/fontes.</p>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Normalmente se recomenda que os
|
|
|
|
|
fontes fiquem num diretório com o nome
|
|
|
|
|
do projeto. Qualquer coisa fora desse
|
|
|
|
|
diretório não é código.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h3><code>│ ├── defaults.py</code></h3>
|
|
|
|
|
|
|
|
|
|
<p>Valores default da aplicação.</p>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
O Flask trabalha com arquivos de configuração
|
|
|
|
|
em Python mesmo (carregando objetos Python).
|
|
|
|
|
Vamos ver esse arquivo mais pra frente.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h3><code>│ ├── __init__.py</code></h3>
|
|
|
|
|
|
|
|
|
|
<p>
|
|
|
|
|
<code>__init__.py</code> é necessário
|
|
|
|
|
para indicar ao Python que o diretório
|
|
|
|
|
pode se lido.
|
|
|
|
|
</p>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h3><code>│ ├── main.py</code></h3>
|
|
|
|
|
|
|
|
|
|
<p>O arquivo inicial da aplicação.</p>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Qualquer nome (desde que seja importável
|
|
|
|
|
pelo interpretador Python) é valido. Outros
|
|
|
|
|
nomes podem ser "server.py" ou "ata.py".
|
|
|
|
|
|
|
|
|
|
A comunidade Node tem o costume de marcar
|
|
|
|
|
o ponto inicial de uma aplicação com "index.js".
|
|
|
|
|
Embora "index.py" funcione, eu não sugiro porque
|
|
|
|
|
o arquivo pode conter mais do que simplesmente
|
|
|
|
|
o o index da aplicação (e é o que vamos fazer
|
|
|
|
|
nesse aplicação).
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h3><code>│ ├── static</code></h3>
|
|
|
|
|
|
|
|
|
|
<p>
|
|
|
|
|
Arquivos estáticos.
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Qualquer coisa que não precise ser processada
|
|
|
|
|
pelo Flask. Por exemplo, arquivos CSS, JS e
|
|
|
|
|
imagens.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h3><code>│ └── templates</code></h3>
|
|
|
|
|
|
|
|
|
|
<p>
|
|
|
|
|
Arquivos de template.
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
O formato utilizado é o Jinja, que é
|
|
|
|
|
praticamente a mesma coisa que os templates
|
|
|
|
|
do Django (com algumas coisas a mais, que
|
|
|
|
|
infelizmente não iremos ver).
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h3><code>├── contents</code></h3>
|
|
|
|
|
|
|
|
|
|
<p>
|
|
|
|
|
Onde o conteúdo a ser apresentado está.
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Isso é usado apenas para testes em desenvolvimento.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h3><code>└── requirements.txt</code></h3>
|
|
|
|
|
|
|
|
|
|
<p>
|
|
|
|
|
Dependências do projeto.
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
... assim como todo bom e qualquer
|
|
|
|
|
projeto Python.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h3><code>├── wsgi.py</code></h3>
|
|
|
|
|
|
|
|
|
|
<p>
|
|
|
|
|
Execução do projeto em modo wsgi.
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Pela forma como WSGI deve ser executado,
|
|
|
|
|
normalmente se cria um arquivo chamado
|
|
|
|
|
wsgi.py que executa o projeto em modo
|
|
|
|
|
WSGI.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>requirements.txt</code></h2>
|
|
|
|
|
|
|
|
|
|
<pre><code>flask~=0.12.2
|
|
|
|
|
Markdown~=2.6.9</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Nossas duas dependências: Flask e
|
|
|
|
|
Markdown.
|
|
|
|
|
|
|
|
|
|
"~=" é um validador de versões com
|
|
|
|
|
a definição de "compatível". Ou seja
|
|
|
|
|
estamos buscando o Flask compatível com
|
|
|
|
|
a versão 0.12.2 e o Markdown compatível
|
|
|
|
|
com a versão 2.6.9.
|
|
|
|
|
|
|
|
|
|
Como o PIP sabe disso? Por vesionamento
|
|
|
|
|
semantico.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>main.py</code></h2>
|
|
|
|
|
<h3>Imports</h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>import os
|
|
|
|
|
import os.path
|
|
|
|
|
|
|
|
|
|
import markdown
|
|
|
|
|
|
|
|
|
|
from flask import Flask
|
|
|
|
|
from flask import render_template</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Dos imports, a única coisa "diferente"
|
|
|
|
|
aqui é o import do markdown -- uma biblioteca
|
|
|
|
|
externa que vai estar no nosso requirements.txt
|
|
|
|
|
-- e os imports do Flask. Aqui só vamos usar dois
|
|
|
|
|
elementos: o Flask, que é o objeto principal
|
|
|
|
|
e o render_template, para renderizar templates
|
|
|
|
|
Jinja.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>main.py</code></h2>
|
|
|
|
|
<h3>A App</h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>app = Flask(__name__)</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Flask é o objeto principal da aplicação.
|
|
|
|
|
Ele precisa de um namespace e o nome
|
|
|
|
|
da própria classe serve.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>main.py</code></h2>
|
|
|
|
|
<h3>Configuração</h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>app.config.from_object('ata.defaults')
|
|
|
|
|
app.config.from_envvar('ATA_SETTINGS', silent=True)</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
O objeto da aplicação (ou simplesmente app)
|
|
|
|
|
tem um objeto "config" com as configurações.
|
|
|
|
|
|
|
|
|
|
As configurações podem ser carregadas de vários
|
|
|
|
|
lugares e aqui temos duas:
|
|
|
|
|
|
|
|
|
|
from_object vai tentar importar o objeto e usar
|
|
|
|
|
as variáveis definidas no mesmo.
|
|
|
|
|
|
|
|
|
|
from_envvar vai procurar o arquivo apontando
|
|
|
|
|
pela variável de ambiente indicada (por exemplo,
|
|
|
|
|
se alguém tivesse feito `export ATA_SETTINGS=/etc/ata.cfg`
|
|
|
|
|
esse arquivo seria carregado). Normalmente, quando
|
|
|
|
|
uma das configs indicadas não for encontrada (por
|
|
|
|
|
exemplo, o arquivo não existe ou a variável de
|
|
|
|
|
ambiente não foi definida) o Flask vai abortar
|
|
|
|
|
a execução. Para não abortar, precisamos do
|
|
|
|
|
`silent=True` -- fail silently.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>main.py</code></h2>
|
|
|
|
|
<h3>A primeira rota.</h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>@app.route('/')
|
|
|
|
|
def index():</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Nossa primeira iteração com o sistema de
|
|
|
|
|
rotas do Flask. Vale notar que, diferente do
|
|
|
|
|
Django, que tem um arquivo com todas as rotas,
|
|
|
|
|
Flask utiliza decorators para indicar qual
|
|
|
|
|
função será chamada. É possível também indicar
|
|
|
|
|
quais métodos serão aceitos (e assim é possível
|
|
|
|
|
ter uma função para GET e outra para POST, na
|
|
|
|
|
mesma rota).
|
|
|
|
|
|
|
|
|
|
Assim como o Django, Flask trabalha com endpoints.
|
|
|
|
|
O nome do endpoint, nesse caso, é o nome da
|
|
|
|
|
própria função (então, neste caso, o endpoint
|
|
|
|
|
é "index"). Mais pra frente veremos porque isso
|
|
|
|
|
é importante.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>main.py</code></h2>
|
|
|
|
|
<h3>O índice</h3>
|
|
|
|
|
|
|
|
|
|
<pre><code> 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('</p>')
|
|
|
|
|
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)</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Infelizmente, não tem como diminuir a quantidade
|
|
|
|
|
de código a ser apresentado aqui. Mas basicamente
|
|
|
|
|
temos o seguinte:
|
|
|
|
|
|
|
|
|
|
Estamos usando o app.config para buscar uma
|
|
|
|
|
configuração, "STORAGE". Nesse exemplo, é o
|
|
|
|
|
diretório com o conteúdo e ele é configurado
|
|
|
|
|
em "defaults.py" (que vamos ver na sequencia).
|
|
|
|
|
|
|
|
|
|
Segundo, estamos usando a biblioteca Markdown para
|
|
|
|
|
converter o arquivo MarkDown para HTML. De forma
|
|
|
|
|
"boba", estamos pegando o primeiro parágrafo.
|
|
|
|
|
|
|
|
|
|
Terceiro, estamos usando a função "render_template"
|
|
|
|
|
para renderizar o arquivo "index.html", com uma
|
|
|
|
|
variável chamada "data". Essa variável vai aparecer
|
|
|
|
|
nos templates -- e a única forma de passar variáveis
|
|
|
|
|
é através da passagem pelos parametros da função
|
|
|
|
|
render_template.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>main.py</code></h2>
|
|
|
|
|
<h3>Entradas específicas</h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>@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)</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Mais uma rota, com uma diferença: ela aceita
|
|
|
|
|
um parametro a mais, "entry_name". Esse mesmo
|
|
|
|
|
nome deve aparecer na função.
|
|
|
|
|
|
|
|
|
|
Assim como visto anteriormente, render_template
|
|
|
|
|
recebe um arquivo template a ser carregado e
|
|
|
|
|
duas variáveis: "output" com o conteúdo renderizado
|
|
|
|
|
do Markdown e "entry" com o nome da entrada.
|
|
|
|
|
|
|
|
|
|
Ponto para quem achar o erro aqui (não é erro: é
|
|
|
|
|
pra mostrar umas das facilidades do Flask).
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>main.py</code></h2>
|
|
|
|
|
<h3>Tratamento de erros.</h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>@app.errorhandler(404)
|
|
|
|
|
def page_not_found():
|
|
|
|
|
return render_template('page_not_found.html')</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Com "errorhandler" é possível indicar o que
|
|
|
|
|
deve ser apresentado quando ocorre um erro.
|
|
|
|
|
|
|
|
|
|
No caso, estamos definindo que quando ocorrer
|
|
|
|
|
um 404, deve ser feito o render_template do
|
|
|
|
|
arquivo "page_not_found.html"
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>main.py</code></h2>
|
|
|
|
|
<h3>Tratamento de erros</h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>@app.errorhandler(FileNotFoundError)
|
|
|
|
|
def entry_not_found(_):
|
|
|
|
|
return render_template('entry_not_found.html')</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Além de tratar com códigos de erro,
|
|
|
|
|
errorhandler também sabe lidar com
|
|
|
|
|
exceções. No caso, se em qualquer
|
|
|
|
|
lugar da aplicação ocorrer um
|
|
|
|
|
"FileNotFoundError", esse será tratado
|
|
|
|
|
aqui.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>defaults.py</code></h2>
|
|
|
|
|
|
|
|
|
|
<pre><code>DEBUG=True
|
|
|
|
|
STORAGE='/home/jbiason/src/ata/contents'</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Contém as configurações do sistema.
|
|
|
|
|
DEBUG é uma variável controlada pelo
|
|
|
|
|
próprio Flask (assim como o DEBUG do Django).
|
|
|
|
|
Existem várias variáveis que podem ser
|
|
|
|
|
configuradas e todas estão listadas
|
|
|
|
|
na documentação do Flask.
|
|
|
|
|
|
|
|
|
|
Entre essas variáveis está o DEBUG, o diretório
|
|
|
|
|
onde se encontram os templates, o diretório
|
|
|
|
|
dos arquivos estáticos e assim por diante.
|
|
|
|
|
|
|
|
|
|
No nosso caso, como não estamos substiuindo
|
|
|
|
|
essas variáveis, elas estão com o valor padrão
|
|
|
|
|
de estarem junto com os fontes (assim como
|
|
|
|
|
criamos nossa estrutura de diretórios).
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<section>
|
|
|
|
|
<h3><code>style.css</code></h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>h1#header {
|
|
|
|
|
background-color: black;
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.entry {
|
|
|
|
|
margin-left: 30px;
|
|
|
|
|
margin-right: 30px;
|
|
|
|
|
margin-bottom: 15px;
|
|
|
|
|
}</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
O arquivo de estilos é bem bobo, mas
|
|
|
|
|
serve para vermos onde ele deve ficar
|
|
|
|
|
quando colocarmos o Flask em produção.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>templates</code></h2>
|
|
|
|
|
<h3><code>layout.html</code></h3>
|
|
|
|
|
|
|
|
|
|
<pre><code><!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></code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Para templates Jinja, assim como Django, é
|
|
|
|
|
possível ter um arquivo base do qual todos
|
|
|
|
|
os demais decendem.
|
|
|
|
|
|
|
|
|
|
Importante notar aqui o url_for(), que faz
|
|
|
|
|
a conversão do endpoint para uma URL (no nosso
|
|
|
|
|
caso, do static) e quando endpoint requer um
|
|
|
|
|
parametro (como o nosso "entry_name") ele *tem*
|
|
|
|
|
que estar no comando. static requer filename
|
|
|
|
|
para saber qual arquivo estático deve ser
|
|
|
|
|
carregado.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>templates</code></h2>
|
|
|
|
|
<h3><code>index.html</code></h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>{% 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 %}</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
O arquivo apresentado na raíz da aplicação.
|
|
|
|
|
|
|
|
|
|
Variáveis com {{ }}, url_for() usando a função
|
|
|
|
|
"show_entry" com um parametro
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>templates</code></h2>
|
|
|
|
|
<h3><code>entry.html</code></h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>{% extends "layout.html" %}
|
|
|
|
|
{% block maincontent %}
|
|
|
|
|
<div class="entry">
|
|
|
|
|
<div class="title">{{ entry }}</div>
|
|
|
|
|
{{ output|safe }}
|
|
|
|
|
</div>
|
|
|
|
|
{% endblock %}</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Como apresentar somente uma entrada.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>templates</code></h2>
|
|
|
|
|
<h3><code>page_not_found.html</code></h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>{% extends "layout.html" %}
|
|
|
|
|
{% block maincontent %}
|
|
|
|
|
<h2>Page not found</h2>
|
|
|
|
|
{% endblock %}</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
A página quando ocorrer um 404...
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>templates</code></h2>
|
|
|
|
|
<h3><code>entry_not_found.html</code></h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>{% extends "layout.html" %}
|
|
|
|
|
{% block maincontent %}
|
|
|
|
|
<h2>Entry not found</h2>
|
|
|
|
|
{% endblock %}</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
... e quando a entrada não existir
|
|
|
|
|
(que é a captura do nosso FileNotFoundError)
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>setup.py</code></h2>
|
|
|
|
|
|
|
|
|
|
<pre><code>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)</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Um arquivo de setup basicamente padrão.
|
|
|
|
|
|
|
|
|
|
Duas coisas aqui são importantes:
|
|
|
|
|
|
|
|
|
|
`zip_safe` deve ser `False`. Quando True,
|
|
|
|
|
ele indica que o conteúdo pode ser mantido
|
|
|
|
|
em um zip/egg sem problemas. Como teremos
|
|
|
|
|
arquivos que tem que ser lidos pelo sistema
|
|
|
|
|
operacional, não podemos manter os mesmos
|
|
|
|
|
dentro de um zip -- precisamos que o
|
|
|
|
|
conteúdo do pacote seja descompactado.
|
|
|
|
|
|
|
|
|
|
`include_package_data` serve para indicar
|
|
|
|
|
ao setup que devem ser considerados também
|
|
|
|
|
os arquivos indicados em MANIFEST.in; sem
|
|
|
|
|
isso, somente arquivos fontes serão colocados
|
|
|
|
|
no pacote.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<section>
|
|
|
|
|
<h3><code>wsgi.py</code></h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>from ata.main import app as application</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Sim, "tudo" isso.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<section>
|
|
|
|
|
<h2><code>MANIFEST.in</code></h2>
|
|
|
|
|
|
|
|
|
|
<pre><code>recursive-include ata/templates *
|
|
|
|
|
recursive-include ata/static *
|
|
|
|
|
include requirements.txt</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
MANIFEST.in indica diretórios com conteúdo
|
|
|
|
|
que deve ser adicionado ao pacote que não
|
|
|
|
|
são arquivos fonte.
|
|
|
|
|
|
|
|
|
|
Documentação, imagens, templates, CSS,
|
|
|
|
|
JS, qualquer arquivo dessa natureza que
|
|
|
|
|
deva ir para o pacote deve ser listado
|
|
|
|
|
aqui.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<section>
|
|
|
|
|
<h2>Rodando</h2>
|
|
|
|
|
<h3>(Dev server)</h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>
|
|
|
|
|
FLASK_APP=ata/main.py flask run
|
|
|
|
|
</code></pre>
|
|
|
|
|
|
|
|
|
|
<pre class="fragment"><code>
|
|
|
|
|
export FLASK_APP=ata/main
|
|
|
|
|
flask run
|
|
|
|
|
</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
A partir do Flask 0.12, para executar
|
|
|
|
|
a aplicação, é preciso exportar a
|
|
|
|
|
variável FLASK_APP com o nome do arquivo
|
|
|
|
|
principal.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<section>
|
|
|
|
|
<h2>Rodando de verdade</h2>
|
|
|
|
|
<h3>Dependências de sistema</h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>sudo dnf install nginx uwsgi uwsgi-plugin-python3</code></pre>
|
|
|
|
|
|
|
|
|
|
<pre><code>sudo yum install nginx uwsgi uwsgi-plugin-python3</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Vamos precisar de dois pacotes do sistema
|
|
|
|
|
operacional: nginx e uwsgi. O uwsgi vai
|
|
|
|
|
servir para "envelopar" a aplicação Flask
|
|
|
|
|
e expor o WSGI; nginx nós vamos usar para
|
|
|
|
|
expor a aplicação ao mundo.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2>Rodando de verdade</h2>
|
|
|
|
|
<h3>Virtualenv (primeira vez)</h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>mkdir -p /usr/local/venv
|
|
|
|
|
mkdir -p /usr/local/apps
|
|
|
|
|
python -m venv /usr/local/venv/ata</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Esses passos são necessários apenas se for
|
|
|
|
|
a primeira instalação: precisamos de um
|
|
|
|
|
diretório para o aplicativo e um diretório
|
|
|
|
|
para os virtualenvs -- e inicializar o
|
|
|
|
|
virtualenv da aplicação.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2>Rodando de verdade</h2>
|
|
|
|
|
<h3>Instalando novas versões</h3>
|
|
|
|
|
|
|
|
|
|
<pre><code>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</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
O que fazemos aqui é ter um diretório para as
|
|
|
|
|
aplicações (porque uma pessoa não pára simplesmente
|
|
|
|
|
na sua primeira aplicação); como o diretório
|
|
|
|
|
vem versionado, vamos aproveitar isso para termos
|
|
|
|
|
várias versões disponíveis ao mesmo tempo;
|
|
|
|
|
para mudar a versão, mudamos o link simbólico.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2>Rodando de verdade</h2>
|
|
|
|
|
<h3>Primeira configuração uwsgi</h3>
|
|
|
|
|
|
|
|
|
|
<p><code>/etc/uwsgi.d/ata.ini</code></p>
|
|
|
|
|
|
|
|
|
|
<pre><code>[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</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
`env` serve para configurar uma variável de
|
|
|
|
|
ambiente; no caso, estamos configurando
|
|
|
|
|
a variável ATA_SETTINGS para ter o valor
|
|
|
|
|
/etc/ata.cfg -- que vai ser nosso arquivo
|
|
|
|
|
de configuração local.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2>Rodando de verdade</h2>
|
|
|
|
|
<h3>Primeira configuração da app</h3>
|
|
|
|
|
|
|
|
|
|
<p><code>/etc/ata.cfg</code></p>
|
|
|
|
|
|
|
|
|
|
<pre><code>DEBUG=False
|
|
|
|
|
STORAGE=/var/db/ata/</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
Basicamente, o mesmo que o nosso arquivo
|
|
|
|
|
de exemplo, mas agora com valores de
|
|
|
|
|
verdade.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2>Rodando de verdade</h2>
|
|
|
|
|
<h3>Primeira configuração do nginx</h3>
|
|
|
|
|
|
|
|
|
|
<p><code>/etc/nginx/conf.d/ata.conf</code></p>
|
|
|
|
|
|
|
|
|
|
<pre><code>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 ata-wsgi;
|
|
|
|
|
}
|
|
|
|
|
}</code></pre>
|
|
|
|
|
|
|
|
|
|
<aside class="notes">
|
|
|
|
|
E o nginx roda apontando para o socket
|
|
|
|
|
criado pelo uwsgi.
|
|
|
|
|
</aside>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section>
|
|
|
|
|
<h2>Rodando de verdade</h2>
|
|
|
|
|
<h3>Depois da primeira execução</h3>
|
|
|
|
|
<h4>(Ou seja, updates)</h4>
|
|
|
|
|
|
|
|
|
|
<pre><code>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
|
|
|
|
|
</code></pre>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section data-background='_images/thats-all-folks.jpg'>
|
|
|
|
|
<section>
|
|
|
|
|
<h1 class="fragment semi-opaque">Perguntas?</h1>
|
|
|
|
|
</section>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script src="reveal.js/lib/js/head.min.js"></script>
|
|
|
|
|
<script src="reveal.js/js/reveal.js"></script>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
// Full list of configuration options available at:
|
|
|
|
|
// https://github.com/hakimel/reveal.js#configuration
|
|
|
|
|
Reveal.initialize({
|
|
|
|
|
controls: true,
|
|
|
|
|
progress: true,
|
|
|
|
|
history: true,
|
|
|
|
|
center: true,
|
|
|
|
|
// showNotes: true,
|
|
|
|
|
|
|
|
|
|
transition: 'slide', // none/fade/slide/convex/concave/zoom
|
|
|
|
|
|
|
|
|
|
// Optional reveal.js plugins
|
|
|
|
|
dependencies: [
|
|
|
|
|
{ src: 'reveal.js/lib/js/classList.js', condition: function() { return !document.body.classList; } },
|
|
|
|
|
{ src: 'reveal.js/plugin/markdown/marked.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } },
|
|
|
|
|
{ src: 'reveal.js/plugin/markdown/markdown.js', condition: function() { return !!document.querySelector( '[data-markdown]' ); } },
|
|
|
|
|
{ src: 'reveal.js/plugin/highlight/highlight.js', async: true, callback: function() { hljs.initHighlightingOnLoad(); } },
|
|
|
|
|
{ src: 'reveal.js/plugin/zoom-js/zoom.js', async: true },
|
|
|
|
|
{ src: 'reveal.js/plugin/notes/notes.js', async: true }
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|