Una pequeña introducción a una gran microframework
por Javier Luna Molina
Aprenderemos a...
- Flask 101: Programar un pequeño servidor
- Hacer uso de templates con Jinja 2
- Hacer uso de SQLAlchemy como ORM para almacenar nuestros datos
- Gestionar sesiones de usuario de una forma segura
- Estructurar bien nuestra aplicación. ¿Es Flask escalable?
- Diseñar e implementar una API REST
Aprenderemos a...
Hacer un blog!
Situación actual
Situación actual
Situación actual (real)
¿Por qué Flask?
¿Por qué Flask?
- Rápido, ligero y directo
- Se adapta mejor a tu manera de pensar
- Funciona muy bien con módulos de terceros
- Módulos muy fáciles de programar (y comunidad muy activa)
Flask 101
Hola mundo
Flask 101
Hola mundo!
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return "Hello world!"
if __name__ == '__main__':
app.run()
Hola mundo!
...
@app.route('/')
def hello_world():
return "Hello world!"
...
Rutas
Flask 101
Rutas
...
@app.route('/') #http://localhost:5000/ -> Hello world!
def hello_world():
return "Hello world!"
...
Rutas
@app.route('/') #http://localhost:5000/ -> Hello world!
@app.route('/index') #http://localhost:5000/index -> Hello world!
@app.route('/hello/world') #http://localhost:5000/hello/world -> Hello world!
def hello_world():
return "Hello world!"
print(app.url_map)
Map([<Rule '/hello/world' (OPTIONS, GET, HEAD) -> hello_world>,
<Rule '/index' (OPTIONS, GET, HEAD) -> hello_world>,
<Rule '/' (OPTIONS, GET, HEAD) -> hello_world>,
<Rule '/static/<filename>' (OPTIONS, GET, HEAD) -> static>])
Rutas: Wildcards
Flask 101
Rutas: Wildcards
Estructura:
@app.route('/<variable>/<variable2>')
def vista(variable, variable2):
return ""
Wildcards "tipadas":
@app.route('/<int:variable>/<variable2>')
def vista(variable, variable2):
return ""
# /1/hola -> 200 OK
# /hola/paco -> 404 Not found
Rutas: Wildcards
@app.route('/') #http://localhost:5000/ -> Hello world!
def hello_world():
return "Hello world!"
@app.route('/<name>') #http://localhost:5000/paco -> Hello paco
def hello_name(name):
return "Hello " + name
@app.route('/<name>/<int:times>') #http://localhost/paco/2 -> Hello paco paco
def hello_name2(name, times): #times es de tipo int
return "Hello ".join([name+" " for i in range(times)].strip()
Respuestas
Flask 101
Respuestas
@app.route('/')
def hello_world():
return "Hello world!"
<html>
<head></head>
<body>
Hello world!
</body>
<html>
Respuestas
@app.route('/')
def hello_world():
return """
<html>
<head>
<title>HELLO WORLD</title>
</head>
<body>
<h1>HELLO WORLD!!!!</h1>
<a href="http://www.disneylandparis.es/">Go to disneyland now</a>
</body>
</html>
"""
Respuestas: JSON
from flask import Flask, jsonify
...
@app.route('/json')
def hello_json():
return jsonify({'hello':'json!'})
#http://localhost:5000/json -> {'hello':'json!'} 200 OK
Respuestas: 301(redirect)
from flask import Flask, redirect
...
@app.route('/disney')
def goto_disney():
return redirect('http://www.disneylandparis.es/')
#http://localhost:5000/disney -> 301 Moved permanently
# -> http://www.disneylandparis.es/
Respuestas: 301(redirect)
from flask import Flask, redirect
...
@app.route('/')
def index():
return "<h1> I'm a cool ass index </h1>"
@app.route('/index')
def redirect_to_index():
return redirect('/')
@app.route('/indice')
def redirect_to_index2():
return redirect('http://localhost:5000/')
@app.route('/illo')
def redirect_to_index3():
return redirect('/')
Métodos HTTP
Flask 101
Métodos HTTP
- GET: Leer
- POST: Crear
- PUT: Actualizar
- PATCH: Actualizar parcialmente
- DELETE: Borrar
Métodos HTTP
from flask import Flask, request, redirect
...
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if username == 'admin' and password == 'hunter02':
return redirect('/admin')
else:
return """<p>Ha habido un error con tus datos de login</p>
<a href='/login'> Vuelve a intentarlo</a>"""
return """
<form action="/login" method='POST'>
Username:<br>
<input type="text" name="username"><br>
Password:<br>
<input type="password" name="password">
<input type="submit" value="Login">
</form>
"""
Métodos HTTP
GET /login
200 OK
POST /login
username: admin
password: hunter02
¿Datos correctos?
Sí
301 Redirect a /admin
No
GET /admin
200 OK
Error handling
Flask 101
Error handling: 404, 500...
http://localhost:5000/hola-mundo-mio
Not Found
The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
Error handling: 404, 500...
@app.errorhandler(404)
def page_not_found(e):
return "<h1>La página no está, sorry :(</h1>", 404
La página no está, sorry :(
Error handling: 404, 500...
@app.errorhandler(500)
def page_not_found(e):
return """
<html>
<head><title>Illo :(</title></head>
<body>
<h1>El servidor ha petado cosa mala</h1>
<p>Gracias</p>
</body>
</html>
""", 500
Error handling: Excepciones
@app.route('/<item>')
def yield_item(item):
valor = {'hola': 'mundo'}
return valor[item]
@app.errorhandler(KeyError)
def key_error(e):
return "<h1>Key error</h1>", 500
# /hola -> "mundo" 200 OK
# /mundo -> <h1>Key error </h1> 500 Internal Server Error
Generar URLs
Flask 101
from flask import Flask, redirect
...
@app.route('/')
def index():
return "<h1> I'm a cool ass index </h1>"
@app.route('/index')
def redirect_to_index():
return redirect('/')
@app.route('/indice')
def redirect_to_index2():
return redirect('http://localhost:5000/')
@app.route('/illo')
def redirect_to_index3():
return redirect('/')
Ejemplo de antes
from flask import Flask, redirect
...
@app.route('/coolassindex')
def index():
return "<h1> I'm a cool ass index </h1>"
@app.route('/index')
def redirect_to_index():
return redirect('/')
@app.route('/indice')
def redirect_to_index2():
return redirect('http://localhost:5000/')
@app.route('/illo')
def redirect_to_index3():
return redirect('/')
Si cambiamos la ruta...
Solución: Generar las URLs
url_for(endpoint)
from flask import Flask, redirect, url_for
...
@app.route('/coolassindex')
def index():
return "<h1> I'm a cool ass index </h1>"
@app.route('/holaa')
def redirect_to_index():
return redirect(url_for('index'))
# url_for('index') -> /coolassindex
url_for() con wildcards
from flask import Flask, redirect, url_for
...
@app.route('/hello/<name>')
def hola_illo(name):
return "Hello "+name
@app.route('/')
return redirect(url_for('hola_illo', name="World!"))
#url_for('hola_illo', name="Paco") -> /hello/paco
#url_for('hola_illo', name="Paco", _external=True)
#-> http://localhost:5000/hello/paco
¡Felicidades!
Ya sabes un 80% de Flask
¿
?
Ejercicio
Lista de comentarios
Programar un servidor que te muestre una serie de comentarios y te permita añadir un comentario usando un formulario
Ejercicio
Lista de comentarios
<html>
<head><title>Comentarios cool</title></head>
<body>
<form action="/comment" method="POST">
<h3>Comenta:</h3>
<input type="text" name="comment">
<input type="submit" name="submit">
</form>
<br>
<h3>Comentarios</h3>
<ul>
<!-- Aquí es donde tenéis que poner vuestros comentarios -->
<li>Comentario1</li>
<li>Comentario2</li>
</ul>
</body>
</html>
Solución
Lista de comentarios
from flask import Flask, request, redirect, url_for
app = Flask(__name__)
comments = []
plantilla = """
<html>
<head><title>Comentarios cool</title></head>
<body>
<form action="RUTA" method="POST">
<h3>Comenta:</h3>
<input type="text" name="comment">
<input type="submit" name="submit">
</form>
<br>
<h3>Comentarios</h3>
<ul>
COMENTARIOS
</ul>
</body>
</html>
"""
...
Solución (parte I)
...
@app.route('/')
def index():
html_comments = "".join(["<li>"+comment+"</li>\n" for comment in comments])
return plantilla.replace("COMENTARIOS", html_comments).replace('RUTA', url_for('create_comment'))
@app.route('/comment', methods=['POST'])
def create_comment():
comment = request.form['comment']
global comments
comments.append(comment)
return redirect(url_for('index'))
app.run(debug=True)
Solución (parte II)
Jinja2 Templates
@app.route('/')
def hello_world():
return """
<html>
<head>
<title>HELLO WORLD</title>
</head>
<body>
<h1>HELLO WORLD!!!!</h1>
<a href="http://www.disneylandparis.es/">Go to disneyland now</a>
</body>
</html>
"""
La chapuza del siglo
@app.route('/')
def hello_world():
content = ""
with open('helloworld.html') as file:
content = file.read()
return content
Una posible solución...
Jinja2 saves the day!
Qué nos ofrece Jinja...
- Una forma rápida, flexible y segura de generar páginas mediante plantillas
- Cierta lógica
- Herencia e inclusión
- Macros
- Filtros
Cargar plantillas desde Flask
from flask import Flask, render_template
...
@app.route('/')
def hello_world():
return render_template('helloworld.html')
Sintáxis
Jinja2
Sintáxis
- {{ expresión }} - Representar el resultado en el contenido final
- {% código %} - Ejecución de código
- {# comentario #} - Comentarios no incluidos en el contenido final
{{ expresión }}
<html>
<head>
<title>Hello {{ name }}</title>
</head>
<body>
Hello {{ name }}!!
</body>
</html>
hello_world.html
{{ expresión }}
app.py
from flask import Flask, render_template
...
@app.route('/<someones_name>')
def hello_name(someones_name):
return render_template('hello_world.html',
name=someones_name)
{% código %}
{% if <condición> %}
<html>
<head>
<title> Hello {{ name }} </title>
</head>
<body>
<p>Hello {{ name }}!</p>
{% if yearsold > 90 %}
<p> Damn, you're old! </p>
{% elif yearsold < 4 %}
<p> You're too young!!</p>
{% else %}
<p> Glad you're here </p>
{% endif %}
</body>
</html>
{% for element in collection %}
<html>
<head>
<title> Hello {{ name }} </title>
</head>
<body>
<p>Hello {{ name }}!</p>
<p>These are my friends:</p>
<ul>
{% for friend in friends %}
<li>{{ friend }}</li>
{% endfor %}
</ul>
</body>
</html>
hello_friends.html
{% for element in collection %}
app.py
from flask import Flask, render_template
...
@app.route('/')
def hello_name(someones_name):
return render_template('hello_friends.html',
name="Pablo",
friends=["Gustavo", "Poison", "Blackie"])
{% for trick in cool_tricks %}
<html>
<head>
<title> Hello {{ name }} </title>
</head>
<body>
<p>Hello {{ name }}!</p>
<p>My friends are:</p>
{% for friend in friends %}
{{ friend }}
{% if not loop.last %}
,
{% else %}
and
{% endif %}
{% endfor %}
</body>
</html>
Última iteración
{% for trick in cool_tricks %}
Par o impar
<html>
<head>
<title> Hello {{ name }} </title>
</head>
<body>
<p>Hello {{ name }}!</p>
<p>These are my friends:</p>
<ul>
{% for friend in friends %}
<li class="{{ loop.cycle('odd', 'even') }}">{{ friend }}</li>
{% endfor %}
</ul>
</body>
</html>
{% for trick in cool_tricks %}
Colección vacía
<html>
<head>
<title> Hello {{ name }} </title>
</head>
<body>
<p>Hello {{ name }}!</p>
<p>These are my friends:</p>
<ul>
{% for friend in friends %}
<li>{{ friend }}</li>
{% else %}
<li> Ol' Darkness! </li> {# Hello darkness my old friend... #}
{% endfor %}
</ul>
</body>
</html>
{% for trick in cool_tricks %}
Número de iteración (empezando en 0)
<html>
<head>
<title> Hello {{ name }} </title>
</head>
<body>
<p>Hello {{ name }}!</p>
<p>These are my friends:</p>
<ul>
{% for friend in friends %}
<li>{{ friend }} is my number {{ loop.index0 }} friend</li>
{% endfor %}
</ul>
</body>
</html>
{% for trick in cool_tricks %}
Número de iteración (empezando en 1)
<html>
<head>
<title> Hello {{ name }} </title>
</head>
<body>
<p>Hello {{ name }}!</p>
<p>These are my friends:</p>
<ul>
{% for friend in friends %}
<li>{{ friend }} is my number {{ loop.index }} friend</li>
{% endfor %}
</ul>
</body>
</html>
{% macro funcion() %}
{% macro input(name, value='', type='text', size=20) %}
<input type="{{ type }}" name="{{ name }}" value="{{
value}}" size="{{ size }}">
{% endmacro %}
<html>
<head>
<title> Hello {{ name }} </title>
</head>
<body>
<p>Hello {{ name }}!</p>
<p>Wanna hang out later?</p>
{{ input("yes", type="button", value="yes") }}
{{ input("no", type="button", value="no") }}
</body>
</html>
{% Herencia %}
{% block nombrebloque %}
<html>
<head>
<title> {%block title%}{% endblock %} </title>
{% block styles %}
{% endblock styles %}
{% block scripts %}
{% endblock %}
</head>
<body>
{% block body %}
{% endblock body %}
</body>
</html>
base.html
{% extends plantilla_padre %}
{% extends "base.html %}
{% block title %} Bootstrap {% endblock title %}
{% block styles %}
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
{% endblock styles %}
{% block scripts %}
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
{% endblock scripts %}
{% block body %}
<div class="page-header">
<h1>Bootstrap is<small>neat</small></h1>
</div>
{% endblock body %}
bootstrap.html
{% include plantilla %} y {{ super() }}
<footer> This is my template you guys ©</footer>
footer.html
b_with_footer.html
{% extends "bootstrap.html" %}
{% block body %}
{{ super() }} {# carga el bloque body del padre #}
{% include "footer.html" %}
{% endblock body %}
Ejercicio
Lista de comentarios II
Programar un servidor que te muestre una serie de comentarios y te permita añadir un comentario usando un formulario.
Ejercicio
Lista de comentarios II
El plus:
- Hacer uso de Jinja2
- Hacer uso de url_for
- Poder borrar comentarios
Solución
Lista de comentarios
Solución (parte I)
comments_template.html
<html>
<head><title>Comentarios cool</title></head>
<body>
<form action="{{ url_for('create_comment') }}" method="POST">
<h3>Comenta:</h3>
<input type="text" name="comment">
<input type="submit" name="submit">
</form>
<br>
<h3>Comentarios</h3>
<ul>
{% for comment in comments %}
<li>{{ comment }}
<a href="{{url_for('delete_comment', comment_id=loop.index0)}}">Delete</a>
</li>
{% endfor %}
</ul>
</body>
</html>
Solución (parte II)
app.py
from flask import Flask, request, redirect, url_for, render_template
app = Flask(__name__)
comments = []
@app.route('/')
def index():
return render_template('comments_template.html', comments=comments)
@app.route('/comment', methods=['POST'])
def create_comment():
comment = request.form['comment']
global comments
comments.append(comment)
return redirect(url_for('index'))
@app.route('/delete/<int:comment_id>')
def delete_comment(comment_id):
global comments
comments.pop(comment_id)
return redirect(url_for('index'))
app.run()
Hagamos un blog!
Hagamos un blog!
Extensiones que usaremos
Extensiones
Flask-SQLAlchemy
Extensiones
Flask-Bootstrap
Extensiones
Flask-WTF
Extensiones
Flask-Login
Extensiones
Flask-Migrate
Extensiones
Flask-Script
Extensiones
python-slugify
Extensiones
ForgeryPy
1. Estructura básica del proyecto
01
Cómo lo estábamos haciendo...
templates
hello_world.html
app.py
proyecto
Una posible estructura...
app
env
config.py
manage.py
proyecto
tests
Directorio: app
main
static
models.py
__init__.py
app
templates
Directorio: app/main
__init__.py
errors.py
app/main
forms.py
views.py
Demo: Pycharm
2. Configuraciones
02
Configuraciones
Development:
- Base de datos de desarrollo
- Debug activado
Testing:
- Base de datos de testeo
- Modo testing de Flask activado
Production:
- Base de datos de producción
Configuración base
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') \
or 'hunter02'
SQLALCHEMY_COMMIT_ON_TEARDOWN = True
SQLALCHEMY_TRACK_MODIFICATIONS = True
@staticmethod
def init_app(app):
pass
Desarrollo
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI =
os.environ.get('DEV_DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir,
'data-dev.sqlite')
Testing
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI =
os.environ.get('TEST_DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')
Producción
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI =
os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
Formatos de URI SQLAlchemy
MySQL mysql://username:password@hostname/database
Postgres postgresql://username:password@hostname/database
SQLite
sqlite:///absolute/path/to/db
sqlite:///c:/absolute/path/to/db
config.py
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
...
class DevelopmentConfig(Config):
...
class TestingConfig(Config):
...
class ProductionConfig(Config):
...
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
3. App Factory
03
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return "Hello world!"
if __name__ == '__main__':
app.run()
Cómo creamos apps ahora
Problemas
- Fallos y comportamiento extraño al usar servidores con varios workers
- Los test no se pueden evaluar en paralelo
- No podemos cambiar la configuración de la app dinámicamente (entra en el global scope y ya es demasiado tarde)
Solución! App Factory
from flask import Flask
from config import config
#Declarar plugins aquí
def create_app(config_name='default'):
app = Flask(__name__, static_folder='./static')
app.config.from_object(config[config_name])
config[config_name].init_app(app)
# Inicializar plugins aquí
# Registrar rutas aquí
return app
app/__init__.py
...¿registrar rutas aquí?
def create_app(config_name='default'):
app = Flask(__name__, static_folder='./static')
app.config.from_object(config[config_name])
config[config_name].init_app(app)
# Inicializar plugins aquí
# Registrar rutas aquí
@app.route('index')
def index():
return 'Index'
return app
¿
?
...¿registrar rutas aquí?
¿
?
Blueprints al rescate!
Ejemplo de uso
from flask import Blueprint
main = Blueprint('main', __name__)
from . import views, errors, forms
app/main/__init__.py
Ejemplo de uso
from . import main
@main.route('/')
def index():
return 'Index'
app/main/views.py
Ejemplo de uso
app/__init__.py
def create_app(config_name='default'):
app = Flask(__name__, static_folder='./static')
app.config.from_object(config[config_name])
config[config_name].init_app(app)
# Inicializar plugins aquí
# Registrar blueprints aquí
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
4. Flask-SQLAlchemy
04
Inicializamos el plugin
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from config import config
#Declarar plugins aquí
db = SQLAlchemy()
def create_app(config_name='default'):
app = Flask(__name__, static_folder='./static')
app.config.from_object(config[config_name])
config[config_name].init_app(app)
# Inicializar plugins aquí
db.init_app(app)
# Registrar rutas aquí
return app
app/__init__.py
Definimos el Modelo Entidad-Relación
Definimos modelos en nuestra app
app/models.py
from . import db
post_has_tags = db.Table('post_has_tags',
db.Column('post_id', db.Integer, db.ForeignKey('posts.id')),
db.Column('tag_id', db.Integer, db.ForeignKey('tags.id'))
)
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), unique=True, nullable=False)
photo = db.Column(db.String(200))
email = db.Column(db.String(255), unique=True, nullable=False)
password_hash = db.Column(db.Text(), nullable=False)
# Relaciones
posts = db.relationship('Post', backref='user', lazy='dynamic')
comments = db.relationship('Comment', backref='user', lazy='dynamic')
def __repr__(self):
return "<User " + self.username + " >"
Definimos modelos en nuestra app
app/models.py
class Post(db.Model):
__tablename__ = 'posts'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False, unique=True)
body = db.Column(db.Text(), nullable=False)
slug = db.Column(db.String(200))
created_on = db.Column(db.DateTime)
edited_on = db.Column(db.DateTime)
# Relaciones
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
comments = db.relationship('Comment', backref='post', lazy='dynamic')
tags = db.relationship('Tag', secondary=post_has_tags,
backref=db.backref('posts', lazy='dynamic'),
lazy='dynamic')
def __repr__(self):
return "<Post " + self.title + " >"
Definimos modelos en nuestra app
app/models.py
class Comment(db.Model):
__tablename__ = 'comments'
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text(), nullable=False)
posted_on = db.Column(db.DateTime)
banned = db.Column(db.Boolean, default=False)
# Relaciones
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
def __repr__(self):
return "<Comment " + self.body + " >"
Definimos modelos en nuestra app
app/models.py
class Tag(db.Model):
__tablename__ = 'tags'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
slug = db.Column(db.String(200))
def __repr__(self):
return "<Tag " + self.name + ">"
Hashing
¿Qué es hashing?
contraseñaxd
8e1310a0e786aa93c7953c5a5901953f
Función hash
Comprobar si una contraseña es correcta
Contraseña correcta: serresietemakina
Hash: 13c71b459d68adbe6e5ef5b1526e6f49
cr7makina -> 7317c893ba679d06ca0d189c615dc195
¿Son los hashes iguales?
No
Comprobar si una contraseña es correcta
Contraseña correcta: serresietemakina
Hash: 13c71b459d68adbe6e5ef5b1526e6f49
serresietemakina -> 13c71b459d68adbe6e5ef5b1526e6f49
¿Son los hashes iguales?
Si!
Funciones hash NO SEGURAS
- Las programadas por ti (La has cagao seguro)
- MD5
- SHA-1
- SHA-1(MD5)
- MD5+SHA-1(MD5(SHA-1))
Contraseñas de usuario
app/models.py
from werkzeug.security import generate_password_hash,
check_password_hash
class User(db.Model):
...
@property
def password(self):
raise AttributeError('password is not
a readable attribute')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
Generando el slug
app/models.py
from slugify import slugify
# slugify("talentum flask tutorial") -> talentum-flask-tutorial
class Post(db.Model):
...
def generate_slug(self):
self.slug = slugify(self.title)
...
class Tag(db.Model):
...
def generate_slug(self):
self.slug = slugify(self.name)
...
Auto-timestamps
app/models.py
import datetime
class Post(db.Model):
...
created_on = db.Column(db.DateTime, default=datetime.datetime.utcnow)
edited_on = db.Column(db.DateTime, default=datetime.datetime.utcnow,
onupdate=datetime.datetime.utcnow)
...
class Comment(db.Model):
...
posted_on = db.Column(db.DateTime, default=datetime.datetime.utcnow)
...
Gravatar como foto de perfil
app/models.py
import hashlib
class User(db.Model):
...
def generate_gravatar(self):
self.photo = "https://www.gravatar.com/avatar/" + hashlib.md5(
self.email.lower().encode('utf-8')).hexdigest() + "?size=60"
...
5. Falseando datos
05
Usuarios falsos
class User(db.Model):
...
@staticmethod
def generate_fake(count=100):
from sqlalchemy.exc import IntegrityError
from random import seed
import forgery_py
seed()
for _ in range(count):
u = User(username=forgery_py.internet.user_name(with_num=True),
email=forgery_py.internet.email_address(),
password=forgery_py.lorem_ipsum.word())
u.generate_gravatar()
db.session.add(u)
try:
db.session.commit()
except IntegrityError:
db.session.rollback()
...
Posts falsos
class Post(db.Model):
...
@staticmethod
def generate_fake(count=100):
from random import seed, randint
from sqlalchemy.exc import IntegrityError
import forgery_py
seed()
user_count = User.query.count()
for _ in range(count):
u = User.query.offset(randint(0, user_count - 1)).first()
p = Post(title=forgery_py.lorem_ipsum.words(3),
body=forgery_py.lorem_ipsum.sentences(randint(1, 3)),
user=u)
p.generate_slug()
db.session.add(p)
try:
db.session.commit()
except IntegrityError:
db.session.rollback()
...
Tags falsas
class Tag(db.Model):
...
@staticmethod
def generate_fake(count=6):
from random import seed, randint
from sqlalchemy.exc import IntegrityError
import forgery_py
seed()
post_count = Post.query.count()
for _ in range(count):
p = Post.query.offset(randint(0, post_count - 1)).first()
t = Tag(name=forgery_py.lorem_ipsum.word())
t.generate_slug()
t.posts.append(p)
db.session.add(t)
try:
db.session.commit()
except IntegrityError:
db.session.rollback()
...
Comments falsos
class Comment(db.Model):
...
@staticmethod
def generate_fake(count=100):
from random import seed, randint
from sqlalchemy.exc import IntegrityError
import forgery_py
seed()
post_count = Post.query.count()
user_count = User.query.count()
for _ in range(count):
p = Post.query.offset(randint(0, post_count - 1)).first()
u = User.query.offset(randint(0, user_count - 1)).first()
c = Comment(body=forgery_py.lorem_ipsum.words(), user=u, post=p)
db.session.add(c)
try:
db.session.commit()
except IntegrityError:
db.session.rollback()
...
6. manage.py
06
manage.py
Funciones interesantes:
- Migraciones de la base de datos
- Aplicar las migraciones
- Shell interactivo del proyecto
- Correr el servidor
manage.py
import os
from flask_script import Manager
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
manager = Manager(app)
if __name__ == '__main__':
manager.run()
# -----------Comandos------------
# Uso python manage.py <comando>
# runserver - Corre el servidor
manage.py: Base de datos
import os
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
manager = Manager(app)
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
manager.run()
# -----------Comandos------------
# Uso python manage.py <comando>
# runserver - Corre el servidor
# db init - Inicia el repositorio de migraciones
# db migrate -m "mensaje" - Crea una migración
# db upgrade - Aplica la migración antes creada
manage.py: Shell
import os
from flask_script import Manager, Shell
from flask_migrate import Migrate, MigrateCommand
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
manager = Manager(app)
migrate = Migrate(app, db)
manager.add_command('db', MigrateCommand)
manager.add_command('shell', Shell())
if __name__ == '__main__':
manager.run()
# -----------Comandos------------
# Uso python manage.py <comando>
# ...
# shell - Lanza un shell interactivo
manage.py: Shell con contexto
import os
from flask_script import Manager, Shell
from flask_migrate import Migrate, MigrateCommand
from app import create_app, db
from app.models import User, Post, Comment, Tag
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
manager = Manager(app)
migrate = Migrate(app, db)
def make_shell_context():
return dict(app=app, db=db, User=User, Post=Post,
Comment=Comment, Tag=Tag)
manager.add_command('db', MigrateCommand)
manager.add_command('shell', Shell(make_context=make_shell_context))
if __name__ == '__main__':
manager.run()
# -----------Comandos------------
# Uso python manage.py <comando>
# ...
# shell - Lanza un shell interactivo con todo precargado!!
manage.py: Funciones de vagos
def make_shell_context():
def clear():
[print() for _ in range(20)]
def db_add(o):
db.session.add(o)
db.session.commit()
def generate_fake():
User.generate_fake()
Post.generate_fake()
Comment.generate_fake()
Tag.generate_fake()
return dict(app=app, db=db, User=User, Post=Post, Comment=Comment,
Tag=Tag, clear=clear, db_add=db_add, generate_fake=generate_fake)
7. Vistas
07
Flask-Bootstrap
app/__init__.py
...
from flask_bootstrap import Bootstrap
...
#Declarar plugins aquí
db = SQLAlchemy()
bootstrap = Bootstrap()
def create_app(config_name='default'):
...
# Inicializar plugins aquí
db.init_app(app)
bootstrap.init_app(app)
...
En nuestras plantillas...
app/templates/base.html
{% extends "bootstrap/base.html" %}
Declara los siguientes bloques (entre otros):
- title: Contenido de <title>
- content: (Casi) Contenido de <body>
- navbar: Bloque vacío encima de content
- scripts: Contiene todas las etiquetas <script> al final del doc
Estructura de templates
main
widgets
templates
_macros.html
base.html
La base de nuestro blog
app/templates/base.html
{% extends "bootstrap/base.html" %}
{% block title %} Talentum's blog {% endblock title %}
{% block styles %}
{{ super() }}
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css"
rel="stylesheet">
<style type="text/css">
body {
padding-top: 70px;
}
footer {
margin: 50px 0;
}
</style>
{% endblock styles %}
La base de nuestro blog
app/templates/base.html
{% block navbar %}
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse"
data-target="#bs-example-navbar-collapse-1">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ url_for('main.frontpage') }}">Talentum's blog</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li>
<a href="{{ url_for('main.frontpage') }}">Home</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li><a href="#">Login</a></li>
<li><a href="#">Register</a></li>
</ul>
</div>
</div>
</nav>
{% endblock navbar %}
La base de nuestro blog
app/templates/base.html
{% block content %}
<div class="container">
<h1>{% if blog_title is defined %} {{ blog_title }} {% else %} Talentum's blog {% endif %}</h1>
<p class="lead">
{% if blog_subtitle is defined %} {{ blog_subtitle }} {% else %}written entirely in
<a href="#">Flask</a>{% endif %}
</p>
<hr>
<div class="row">
<div class="col-lg-8">
<!-- GENERAR LOS POSTS AQUI -->
{% block main_content %}{% endblock main_content %}
</div>
<div class="col-md-4">
{% block widgets %}
{% include "widgets/description.html" %}
{% include "widgets/tags.html" %}
{% endblock widgets %}
</div>
</div>
<hr>
{% include "widgets/footer.html" %}
</div>
{% endblock content %}
Camarero! Una de widgets
app/templates/widgets/description.html
<div class="well">
<h4>Welcome!</h4>
<p>
Welcome to the best blog ever.
Made by Talentums for Talentums and written entirely in Flask.
Isn't that awesome??
</p>
</div>
Camarero! Una de widgets
app/templates/widgets/tags.html
<div class="well">
<h4>Tags</h4>
<div class="row">
{% if tags %}
<ul class="list-group">
{% for tag in tags %}
<li class="list-group-item">
<span class="badge">{{ tag.posts.count() }}</span>
<a href="#">{{ tag.name }}</a>
</li>
{% endfor %}
</ul>
{% else %}
<p>No tags yet :(</p>
{% endif %}
</div>
</div>
Camarero! Una de widgets
app/templates/widgets/footer.html
<footer>
<div class="row">
<div class="col-lg-12">
<p>Plantilla gitaneada de
<a href="https://startbootstrap.com/template-categories/all/">aquí</a>
</p>
</div>
</div>
</footer>
Resultado final
Portada
Portada
app/templates/main/frontpage.html
{% extends "base.html" %} {# la base que definimos antes #}
{% import "_macros.html" as macros %}
{% block main_content %}
{% for post in posts %}
{{ macros.generate_post_summary(post) }}
{% endfor %}
{% endblock %}
Macros
Macros: Resumen Post I
app/templates/_macros.html
{% macro generate_post_summary(post) %}
<div class="panel panel-default">
<div class="panel-body">
<div class="page-header">
<h1><a href="{{ url_for('main.post_detail', post_slug=post.slug) }}">
{{ post.title }}</a></h1>
<p><span class="glyphicon glyphicon-time"></span> Posted
on {{ post.created_on.strftime("%B %d, %Y at %-I:%M%p") }} by <a
href="#">{{ post.user.username }}</a></p>
</div>
<p>{{ post.body | striptags }}</p>
</div>
...
Macros: Resumen Post II
app/templates/_macros.html
<div class="panel-footer">
<i class="fa fa-tags" aria-hidden="true"></i>
{% if post.tags.count() %}
{% for tag in post.tags.all() %}
<a href="{{ url_for('main.tagged_posts', tag_slug=tag.slug) }}">
{{ tag.name }}</a>
{% if not loop.last %}, {% endif %}
{% endfor %}
{% else %}
No tags
{% endif %}
<i class="fa fa-comments" aria-hidden="true"></i>
{% if post.comments.count() %}
{{ post.comments.count() }}
{% else %}
No comments
{% endif %}
</div>
</div>
{% endmacro %}
Vista detallada de Post
Detalle de un Post
app/templates/main/postdetail.html
{% extends "base.html" %}
{% import "_macros.html" as macros %}
{% block main_content %}
<div class="col-lg-8">
<h1>{{ post.title }}</h1>
<p class="lead">
by <a href="#">{{ post.user.username }}</a>
</p>
<hr>
<p><span class="glyphicon glyphicon-time"></span>
Posted on {{ post.created_on.strftime("%B %d, %Y at %-I:%M%p")}}</p>
<hr>
{{ post.body | safe }}
<hr>
<div class="well">
<h4> WIP</h4>
</div>
<hr>
{% for comment in post.comments.all() %}
{{ macros.generate_comment(comment) }}
{% endfor %}
</div>
{% endblock main_content %}
Blueprint principal
Blueprint principal
app/main/views.py
...
from ..models import Post, Tag, Comment, User
...
@main.route('/')
def frontpage():
return render_template("main/frontpage.html", posts=Post.query.all(), tags=Tag.query.all())
@main.route('/post/<post_slug>')
def post_detail(post_slug):
post = Post.query.filter_by(slug=post_slug).first_or_404()
return render_template('main/postdetail.html', post=post, tags=Tag.query.all())
@main.route('/tag/<tag_slug>')
def tagged_posts(tag_slug):
tag = Tag.query.filter_by(slug=tag_slug).first_or_404()
return render_template("main/frontpage.html", posts=tag.posts.all(), tags=Tag.query.all(),
blog_title="Tagged as " + tag.name, blog_subtitle="",
title="Tagged as " + tag.name)
Para probarlo...
python manage.py db init
python manage.py db migrate -m "primera migración"
python manage.py db upgrade
python manage.py shell
>>> generate_fake()
>>> exit()
python manage.py runserver
8. Creando posts
08
Cómo lo hacíamos antes...
<form action="{{ url_for('create_comment') }}" method="POST">
<h3>Comenta:</h3>
<input type="text" name="comment">
<input type="submit" name="submit">
</form>
Problema: CSRF
Ejemplo CSRF
dominio1.com
dominio1.com/usuario/borrar/21
Inicia sesión un admin
dominio2.com
dominio2.com/index
<img
src="dominio1.com/usuario/borrar/21"/>
admin navega a
admin borra sin querer usuario 21
Problema: Validación
Solución! Flask-WTF
app/main/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField, SelectMultipleField
from wtforms.validators import DataRequired, Length
class PostCreationForm(FlaskForm):
title = StringField('Title', validators=[DataRequired(), Length(1, 200)])
body = TextAreaField('Body', validators=[DataRequired(), Length(1, 999)])
tags = SelectMultipleField('Tags')
submit = SubmitField('Post')
Formulario para crear Posts
app/main/forms.py
...
class TagCreationForm(FlaskForm):
name = StringField('Name', validators=[DataRequired(), Length(1, 100)])
submit = SubmitField('Create tag')
class CommentCreationForm(FlaskForm):
content = TextAreaField('', validators=[DataRequired(), Length(1, 900)])
submit = SubmitField('Submit')
Formulario para crear Tags y Comentarios
Modificamos las vistas
app/templates/postcreate.html
Formulario para crear Posts
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %} Create! {% endblock title %}
{% block main_content %}
<div class="panel panel-default">
<div class="panel-body">
{{ wtf.quick_form(post_form) }}
</div>
</div>
{% endblock %}
{% block widgets %}
<div class="well">
<h4>Create a tag</h4>
<div class="row">
{{ wtf.quick_form(tag_form) }}
</div>
</div>
{% endblock widgets %}
Y los controladores...
app/main/views.py
Crear post: Controlador
from .forms import PostCreationForm
...
@main.route('/create/post', methods=['GET', 'POST'])
def create_post():
post_form = PostCreationForm()
if post_form.validate_on_submit():
post = Post.query.filter_by(title=post_form.title.data).first()
if post is None:
p = Post(title=post_form.title.data, body=post_form.body.data)
p.generate_slug()
for tag_name in post_form.tags.data:
tag = Tag.query.filter_by(name=tag_name).first()
if tag is not None:
p.tags.append(tag)
db.session.add(p)
try:
db.session.commit()
except IntegrityError:
db.session.rollback() #ERROR EL POST YA EXISTÍA
else:
#ERROR POR ALGUNA RAZÓN
return redirect(url_for('main.frontpage'))
...
return render_template("main/postcreate.html", post_form=post_form,
blog_title="Post something!",blog_subtitle="Hope you're inspired")
¿Cómo notificamos errores al usuario?
flash("mensaje")
Configuramos nuestra base
app/templates/base.html
...
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">×</button>
{{ message }}
</div>
{% endfor %}
<!-- GENERAR LOS POSTS AQUI -->
{% block main_content %}{% endblock main_content %}
...
Ahora podemos avisarles!!
app/main/views.py
from flask import flash
...
except IntegrityError:
flash("Error: Ya existía un post con el mismo título")
db.session.rollback()
...
else:
flash("Error: No se pudo guardar el post")
return redirect(url_for('main.frontpage'))
...
9. Flask-Login
09
Solo queremos que creen posts los usuarios registrados
Configuremos Flask-Login
app/__init__.py
from flask_login import LoginManager
...
login_manager = LoginManager()
login_manager.session_protection = 'strong'
def create_app(config_name='default'):
...
login_manager.init_app(app)
...
Configuremos Flask-Login
app/models.py
from flask_login import UserMixin
from . import db, login_manager
...
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
class User(UserMixin, db.Model):
...
¿Donde ponemos las rutas de autenticación?
Otro blueprint!
Creamos el blueprint "auth"
auth
app
__init__.py
views.py
forms.py
errors.py
...
Creamos el blueprint "auth"
app/auth/__init__.py
from flask import Blueprint
auth = Blueprint('auth', __name__)
from . import views, errors, forms
Lo registramos
app/__init__.py
def create_app():
...
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix='/auth')
...
Formulario para login
app/auth/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo
class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Length(1, 64), Email()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Keep me logged in')
submit = SubmitField('Log in')
Formulario para registro
app/auth/forms.py
class RegistrationForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(1, 54),
Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,'Usernames must contain only letters,
numbers, dots or underscores')])
email = StringField('Email', validators=[DataRequired(), Length(1, 64), Email()])
password = PasswordField('Password', validators=[DataRequired(), EqualTo('password2',
message="Passwords must match")])
password2 = PasswordField('Confirm password', validators=[DataRequired()])
submit = SubmitField('Register')
Formulario para registro II
app/auth/forms.py
class RegistrationForm(FlaskForm):
...
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError('Email already registered')
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('Username already in use')
Vistas /auth
app/auth/views.py
@auth.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is not None and user.verify_password(form.password.data):
login_user(user, form.remember_me.data)
return redirect(request.args.get('next') or url_for('main.frontpage'))
flash('Invalid username or password')
return render_template('auth/login.html', form=form, blog_title="Login", blog_subtitle="")
@auth.route('/logout')
@login_required
def logout():
logout_user()
flash('You have been logged out')
return redirect(url_for('main.frontpage'))
@auth.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(email=form.email.data, username=form.username.data, password=form.password.data)
user.generate_gravatar()
db.session.add(user)
flash("You can now login")
return redirect(url_for('auth.login'))
return render_template('auth/registration.html', form=form, blog_title="Register")
Protegemos crear post
app/main/views.py
...
@main.route('/create/post', methods=['GET', 'POST'])
@login_required
def create_post():
...
...
Último toque
app/__init__.py
...
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'
...
10. Actualizamos vistas
10
Actualizamos navbar
app/templates/base.html
<ul class="nav navbar-nav navbar-right">
{% if not current_user.is_authenticated %}
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
<li><a href="{{ url_for('auth.register') }}">Register</a></li>
{% else %}
<li><a href="{{ url_for('main.my_user') }}">{{ current_user.email }}</a></li>
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
{% endif %}
</ul>
Actualizamos navbar
app/templates/base.html
...
<ul class="nav navbar-nav">
<li>
<a href="{{ url_for('main.frontpage') }}">Home</a>
</li>
{% if current_user.is_authenticated %}
<li>
<a href="{{ url_for('main.create_post') }}">Post something!</a>
</li>
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-right">
{% if not current_user.is_authenticated %}
<li><a href="{{ url_for('auth.login') }}">Login</a></li>
<li><a href="{{ url_for('auth.register') }}">Register</a></li>
{% else %}
<li><a href="{{ url_for('main.my_user') }}">{{ current_user.email }}</a></li>
<li><a href="{{ url_for('auth.logout') }}">Logout</a></li>
{% endif %}
</ul>
...
Habilitamos comentarios
app/templates/postdetail.html
...
<div class="well">
{% if current_user.is_authenticated %}
<h4>Leave a Comment:</h4>
{{ wtf.quick_form(form) }}
{% else %}
<h4>You need to be logged in to comment</h4>
{% endif %}
</div>
...
11. Paginación
11
¡Muchos posts!
Solución: Paginar
Añadimos un parámetro
config.py
class Config:
...
POSTS_PER_PAGE = 4
...
Paginar con Flask-SQLAlchemy
app/main/views.py
from flask import current_app
...
@main.route('/')
def frontpage():
page = request.args.get('page', 1, type=int)
pagination = Post.query.order_by(Post.created_on.desc()).paginate(
page, per_page=current_app.config['POSTS_PER_PAGE'], error_out=False
)
return render_template("main/frontpage.html", posts=pagination.items, tags=Tag.query.all(),
pagination=pagination,
prevpage=url_for('main.frontpage', page=pagination.prev_num),
nextpage=url_for('main.frontpage', page=pagination.next_num))
...
Widget al canto!
app/templates/widgets/pagination.html
<ul class="pager">
<li class="previous">
{% if pagination.has_prev %}
<a href="{{ url_for(endpoint, page=pagination.prev_num) }}">← Newer</a>
{% endif %}
</li>
<li class="next">
{% if pagination.has_next %}
<a href="{{ url_for(endpoint, page=pagination.next_num) }}">Older →</a>
{% endif %}
</li>
</ul>
Incluimos el widget...
app/templates/main/frontpage.html
{% extends "base.html" %}
{% import "_macros.html" as macros %}
{% block main_content %}
{% for post in posts %}
{{ macros.generate_post_summary(post) }}
{% endfor %}
{% include "widgets/pagination.html" %}
{% endblock %}
12. REST API
12
¿Qué es una API REST?
Una API que cumple los siguientes principios:
- Interfaz uniforme entre cliente y servidor
- Sin estado
- Respuestas cacheables
- Cliente-Servidor bien marcado
- Arquitectura por capas
- (Opcional) On-demand
Acciones API REST
Verbo | Ruta | Código respuesta |
---|---|---|
Get (List) | /users | 200 y JSON |
Get (Detail) | /users/{id} | 200 y JSON |
Post (Create) | /users | 201 y Location |
Put (Update) | /users/{id} | 204 No response |
Delete (Duh) | /users/{id} | 204 No response |
¿Cómo podríamos implementarlo en Flask?
Con una blueprint, claro
Creamos la blueprint "api"
api
app
__init__.py
...
v1.0
__init__.py
posts.py
Creamos la blueprint "api"
app/api/__init__.py
from .v1_0 import *
Creamos la blueprint "api"
app/api/v1.0/__init__.py
from flask import Blueprint
api1_0 = Blueprint('api1_0', __name__)
from . import posts
Registramos la "api"
app/__init__.py
...
def create_app():
...
from .api import api1_0 as api1_0_blueprint
app.register_blueprint(api1_0_blueprint, url_prefix='/api/1.0v')
...
Nuestro "boilerplate"
app/api/v1.0/posts.py
from flask import jsonify
from . import api1_0
@api1_0.route("/posts", methods=['GET']) #List
def list_posts():
return jsonify({})
@api1_0.route('/posts/<int:postid>', methods=['GET']) #Detail
def detail_post(postid):
return jsonify({})
@api1_0.route("/posts", methods=['POST']) #Create
def create_post():
return jsonify({})
@api1_0.route('/posts/<int:postid>', methods=['PUT']) #Update
def update_post(postid):
return jsonify({})
@api1_0.route('/posts/<int:postid>', methods=['DELETE']) #Tu qué crees
def delete_post(postid):
return jsonify({})
Modificamos nuestro modelo
app/models.py
class Post(db.Model):
...
def to_json(self):
return {
'id': self.id,
'title': self.title,
'body': self.body,
'slug': self.slug,
'created_on': str(self.created_on),
'edited_on': str(self.edited_on),
'user': self.user.username,
'tags': [tag.to_json() for tag in self.tags.all()],
'comments': [comment.to_json() for comment in self.comments.all()]
}
...
GET (Detail)
app/api/v1_0
...
@api1_0.route('/posts/<int:postid>', methods=['GET'])
def detail_post(postid):
post = Post.query.get_or_404(postid)
return jsonify(post.to_json())
...
GET (List)
app/api/v1_0
...
@api1_0.route("/posts", methods=['GET'])
def list_posts():
return jsonify({'items':[post.to_json() for post in Post.query.all()])
...
GET (List) con paginación
app/api/v1_0
...
@api1_0.route("/posts", methods=['GET'])
def list_posts():
page = request.args.get('page', 1, type=int)
pagination = Post.query.order_by(Post.created_on.desc()).paginate(
page, per_page=current_app.config['RESULTS_PER_API_CALL'], error_out=False
)
response = {
'next' : url_for('api1_0.list_posts', page=pagination.next_num, _external=True)
if pagination.has_next else "",
'prev' : url_for('api1_0.list_posts', page=pagination.prev_num, _external=True)
if pagination.has_prev else "",
'items': [post.to_json() for post in pagination.items]
}
return jsonify(response)
...
RESULTS_PER_API_CALL
config.py
class Config:
...
RESULTS_PER_API_CALL = 25
...
POST (Create)
app/api/v1_0/posts.py
@api1_0.route("/posts", methods=['POST'])
def create_post():
post = Post(title=request.form['title'], body=request.form['body'], user=None) #User?
post.generate_slug()
try:
db.session.add(post)
db.session.commit()
except IntegrityError:
db.session.rollback()
abort(409)
response = jsonify({'id': post.id})
response.status_code = 201
response.headers['Location'] = url_for('api1_0.detail_post', postid=post.id)
return response
PUT (Update)
app/api/v1_0/posts.py
@api1_0.route('/posts/<int:postid>', methods=['PUT'])
def update_post(postid):
post = Post.query.get_or_404(postid)
post.title = request.form['title']
post.body = request.form['body']
post.generate_slug()
try:
db.session.add(post)
db.session.commit()
except IntegrityError:
db.session.rollback()
abort(409)
return jsonify(post.to_json())
DELETE (xd)
app/api/v1_0/posts.py
@api1_0.route('/posts/<int:postid>', methods=['DELETE'])
def delete_post(postid):
post = Post.query.get_or_404(postid)
db.session.delete(post)
db.session.commit()
return '', 204
Problema: Escribimos mucho jsonify :(
Solución: Un decorador!
@json
app/api/v1_0/__init__.py
def json(f):
@wraps(f)
def wrapped(*args, **kwargs):
rv = f(*args, **kwargs)
if not isinstance(rv, dict):
rv = rv.to_json()
return jsonify(rv)
return wrapped
GET (Detail)
app/api/v1_0/posts.py
@api1_0.route('/posts/<int:postid>', methods=['GET'])
@json
def detail_post(postid):
post = Post.query.get_or_404(postid)
return post
Problema: Los errores están en HTML :(
Solución: Error handlers!
Errors handlers
app/api/v1_0/__init__.py
@api1_0.errorhandler(409)
def error_409(e):
return jsonify({'error':
'Aborted, a conflict may had happened'}), 409
@api1_0.errorhandler(400)
def error_keyerror(e):
return jsonify({'error':
'Expected one or more parameters to the call'}), 400
@api1_0.errorhandler(401)
def error_keyerror(e):
return jsonify({'error': 'Unauthorized access'}), 401
Problema: Quiero que algunas rutas necesiten autenticación
Solución: Flask-HTTPAuth
Configuramos Flask-HTTPAuth
app/api/__init__.py
from flask_httpauth import HTTPBasicAuth
from ..models import User
api_auth = HTTPBasicAuth()
@api_auth.verify_password
def verify_pw(username, password):
user = User.query.filter_by(username=username).first()
if user is not None:
return user.verify_password(password)
return False
Protegemos las vistas
app/api/v1_0/posts.py
...
@api1_0.route('/posts/<int:postid>', methods=['PUT'])
@json
@api_auth.login_required
def update_post(postid):
...
...
¡Ahora ya sabemos el user!
app/api/v1_0/posts.py
...
@api1_0.route("/posts", methods=['POST'])
@api_auth.login_required
def create_post():
post = Post(title=request.form['title'], body=request.form['body'],
user=User.query.filter_by(username=api_auth.username()).first())
...
Probamos el API REST con requests
pip install requests
python
>> import requests
>>url = "http://localhost:5000/api/1.0v"
>> respuesta = requests.get(url+"/posts")
>> respuesta
<Response [200]>
>> respuesta.json()
{ 'next': 'http://localhost:5000/api/1.0v/posts?page=2',
'prev': '',
'items': [
{'title': 'blabla',
'body': 'ajoaisdnfoisndf',
...
]
}
Probamos rutas protegidas
>> post = {'title': 'Juan', 'body': 'Es un buen tio'}
>> respuesta = requests.post(url+"/posts")
>> respuesta
<Response [401]>
>> respuesta.json()
{'error': 'Unauthorized access'}
>> respuesta = requests.post(url+"/post", data=post,
auth=('user', 'password'))
>> respuesta
<Response [201]>
>> respuesta.headers['Location']
'http://localhost:5000/api/1.0v/posts/103'
¡Terminado!
¿Qué hacer ahora?
Ejercicio:
Haced lo que os plazca (con Flask). Divertíos
Muchas gracias :)
talentum-flask-tutorial
By Javier Luna Molina
talentum-flask-tutorial
Tutorial de flask para el curso Talentum de Python Avanzado. Además, iremos construyendo un blog desde cero mientras aprendemos con esta grandiosa microframework
- 871