Una pequeña introducción a una gran microframework
por Javier Luna Molina
Aprenderemos a...
Aprenderemos a...
Hacer un blog!
Situación actual
Situación actual
Situación actual (real)
¿Por qué Flask?
¿Por qué Flask?
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
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
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
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...
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 }}
<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:
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:
Testing:
Production:
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
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
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:
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):
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:
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 :)