Pushrod adds an extra layer between the Template and the View layers (from the Model-Template-View/MTV pattern, more commonly known as Model-View-Controller/MVC outside of the Python world), the renderer.
As an example, we’re going to adapt this simple Flask app (slightly adapted from the Flask hello world example) to Pushrod:
from flask import Flask, render_template
import random
app = Flask(__name__)
@app.route("/")
def hello():
return render_template("hello.html", greeting=random.choice(("Hi", "Heya")))
if __name__ == "__main__":
app.run()
Where hello.html might look like this:
<html>
<head>
</head>
<body>
Hello, {{ greeting }}.
</body>
</html>
With Pushrod you don’t do the rendering yourself in the View. Instead, you return a context which is passed to a renderer which is inferred from the request. This means that the same code-base can power both the UI and the API with minimal boilerplate. By default Pushrod is set up with a Jinja2 renderer (for HTML) and a JSON renderer.
The first step is to add a Pushrod resolver and decorate the view with the pushrod_view() decorator, which will pass it through the Pushrod rendering pipeline. The code will still work because strings and Response objects are passed through unrendered. The code should now look like this:
from flask import Flask, render_template
from flask.ext.pushrod import Pushrod, pushrod_view
import random
app = Flask(__name__)
Pushrod(app)
@app.route("/")
@pushrod_view()
def hello():
return render_template("hello.html", greeting=random.choice(("Hi", "Heya")))
if __name__ == "__main__":
app.run()
Warning
Remember to add the pushrod_view() decorator closer to the function definition than the route() decorator.
While this works and all, we get absolutely no benefit from using Pushrod right now. So let’s let Pushrod handle the rendering:
from flask import Flask, render_template
from flask.ext.pushrod import Pushrod, pushrod_view
import random
app = Flask(__name__)
Pushrod(app)
@app.route("/")
@pushrod_view(jinja_template="hello.html")
def hello():
return {
'greeting': random.choice(("Hi", "Heya"))
}
if __name__ == "__main__":
app.run()
That’s it. While it might seem a bit longer than the regular non-Pushrod code, you now get JSON rendering (and any other renderers you decide to enable) for free!
Sometimes the available Pushrod renderers might not meet your requirements. Fortunately, making your own renderer is very easy. Let’s say you want a renderer that passes the response through repr(), it would look like this:
from flask.ext.pushrod.renderers import renderer
from repr import repr
@renderer(name='repr', mime_type='text/plain')
def repr_renderer(unrendered, **kwargs):
return unrendered.rendered(
repr(unrendered.response),
'text/plain')
Warning
Always take a **kwargs in your renderer, since other renderers might take arguments that don’t matter to your renderer.
Warning
Of course, you should never use repr() like this in production code, it is just an example to demostrate the syntax without having to go through the regular boilerplate code of creating the response ourselves.
And you would register it to your Pushrod instance using register_renderer().
Note
Functions not decorated using renderer() may not be registered as renderers.
Note
This guide assumes that you’re familiar with Flask and Flask-SQLAlchemy.
While the shown examples might look neat for simple data, it can quickly get out of hand. For example, let’s take a simple blog application (let’s call it pushrodr just to be original):
from flask import Flask
from flask.ext.pushrod import Pushrod, pushrod_view
from flask.ext.sqlalchemy import SQLAlchemy
from sqlalchemy.sql.functions import now
app = Flask(__name__)
Pushrod(app)
db = SQLAlchemy(app)
class Author(db.Model):
__tablename__ = "authors"
id = db.Column(db.Integer, primary_key=True, nullable=False)
name = db.Column(db.String(80), unique=True, nullable=False)
description = db.Column(db.Text(), nullable=False)
class Post(db.Model):
__tablename__ = "posts"
id = db.Column(db.Integer, primary_key=True, nullable=False)
timestamp = db.Column(db.DateTime, nullable=False, default=now())
title = db.Column(db.String(255), unique=True, nullable=False)
content = db.Column(db.Text, nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey(Author.id), nullable=False)
author = db.relationship(Author, backref='posts')
class Comment(db.Model):
__tablename__ = "comments"
id = db.Column(db.Integer, primary_key=True, nullable=False)
author = db.Column(db.String(80), nullable=False)
timestamp = db.Column(db.DateTime, nullable=False, default=now())
content = db.Column(db.Text, nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey(Post.id), nullable=False)
post = db.relationship(Post, backref='comments')
@app.route("/")
@app.route("/<int:page>")
@pushrod_view()
def list_posts(page=1):
posts = Post.query.paginate(page)
return {
'page': page,
'pages': posts.pages,
'total': posts.total,
'items': [{
'id': post.id,
'title': post.title,
'timestamp': unicode(post.timestamp),
'content': post.content,
'author': {
'name': post.author.name,
'description': post.author.description,
},
} for post in posts.items]
}
@app.route("/posts/<int:id>")
@pushrod_view()
def blog_post(id):
post = Post.query.get_or_404(id)
return {
'item': {
'id': post.id,
'title': post.title,
'timestamp': unicode(post.timestamp),
'content': post.content,
'author': {
'name': post.author.name,
'description': post.author.description,
},
'comments': [{
'author': comment.author,
'timestamp': unicode(comment.timestamp),
'content': comment.content,
} for comment in post.comments]
}
}
if __name__ == '__main__': # pragma: no cover
app.run()
As you can see that quickly starts looking redundant, and stupid. It’s also going to cause problems if you’re going to do any form validation using, say, Flask-WTF, or anything else that, while working perfectly for an API too, has special helpers for the GUI rendering. To help with these cases Flask-Pushrod has something called “normalizers”. Normalizers are callables that take two arguments (the object and the Pushrod instance) and prepare the data for serialization (see normalize()). If a normalizer returns NotImplemented then the value is passed through to the next normalizer.
Warning
Renderers can opt out of the normalization process, so that they are passed the un-normalized data (an example is the Jinja2 renderer). Because of this, the normalizer shouldn’t add or rename data.
An example normalizer could look like this:
def my_normalizer(x, pushrod):
if x:
return NotImplemented
else:
return 0
This would return 0 for all “falsy” values, but let all other values normalize as usual. It could then be registered like this:
app = Flask(__name__)
pushrod = Pushrod(app)
pushrod.normalizers[object] = my_normalizer
Note
There is also normalizer_overrides. The difference is that normalizers is a dict of callables that should be used for the “standard” case, while normalizer_overrides is a dict of lists of callables should be used when you need to override the behaviour for a subset of cases.
Note
Both normalizer_overrides and normalizers are resolved in the regular “MRO” (method resolution order). Normalization is resolved in the same order as method calls.
Now that you should have a basic grasp on normalizers, let’s try to use some! Below is how the previous code would look using normalizers:
from flask import Flask, g
from flask.ext.pushrod import Pushrod, pushrod_view
from flask.ext.sqlalchemy import SQLAlchemy, Pagination
from sqlalchemy.sql.functions import now
app = Flask(__name__)
pushrod = Pushrod(app)
db = SQLAlchemy(app)
class Author(db.Model):
__tablename__ = "authors"
id = db.Column(db.Integer, primary_key=True, nullable=False)
name = db.Column(db.String(80), unique=True, nullable=False)
description = db.Column(db.Text(), nullable=False)
class Post(db.Model):
__tablename__ = "posts"
id = db.Column(db.Integer, primary_key=True, nullable=False)
timestamp = db.Column(db.DateTime, nullable=False, default=now())
title = db.Column(db.String(255), unique=True, nullable=False)
content = db.Column(db.Text, nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey(Author.id), nullable=False)
author = db.relationship(Author, backref='posts')
class Comment(db.Model):
__tablename__ = "comments"
id = db.Column(db.Integer, primary_key=True, nullable=False)
author = db.Column(db.String(80), nullable=False)
timestamp = db.Column(db.DateTime, nullable=False, default=now())
content = db.Column(db.Text, nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey(Post.id), nullable=False)
post = db.relationship(Post, backref='comments')
def normalize_author(x, pushrod):
return pushrod.normalize({
'name': x.name,
'description': x.description
})
def normalize_post(x, pushrod):
data = {
'id': x.id,
'title': x.title,
'timestamp': x.timestamp,
'content': x.content,
'author': x.author,
}
if not getattr(g, 'list_view', False):
data['comments'] = x.comments
return pushrod.normalize(data)
def normalize_comment(x, pushrod):
return pushrod.normalize({
'author': x.author,
'timestamp': x.timestamp,
'content': x.content,
})
def normalize_pagination(x, pushrod):
return pushrod.normalize({
'page': x.page,
'pages': x.pages,
'total': x.total,
'items': x.items,
})
pushrod.normalizers.update({
Author: normalize_author,
Post: normalize_post,
Comment: normalize_comment,
Pagination: normalize_pagination,
})
@app.route("/")
@app.route("/<int:page>")
@pushrod_view()
def list_posts(page=1):
g.list_view = True
return Post.query.paginate(page)
@app.route("/posts/<int:id>")
@pushrod_view()
def blog_post(id):
post = Post.query.get_or_404(id)
return {'item': post}
if __name__ == '__main__': # pragma: no cover
app.run()
In the spirit of converters like __bool__(), the normalizers can also be defined inline in the classes, like this:
from flask import Flask, g
from flask.ext.pushrod import Pushrod, pushrod_view
from flask.ext.sqlalchemy import SQLAlchemy, Pagination
from sqlalchemy.sql.functions import now
app = Flask(__name__)
pushrod = Pushrod(app)
db = SQLAlchemy(app)
class Author(db.Model):
__tablename__ = "authors"
id = db.Column(db.Integer, primary_key=True, nullable=False)
name = db.Column(db.String(80), unique=True, nullable=False)
description = db.Column(db.Text(), nullable=False)
def __pushrod_normalize__(self, pushrod):
return pushrod.normalize({
'name': self.name,
'description': self.description
})
class Post(db.Model):
__tablename__ = "posts"
id = db.Column(db.Integer, primary_key=True, nullable=False)
timestamp = db.Column(db.DateTime, nullable=False, default=now())
title = db.Column(db.String(255), unique=True, nullable=False)
content = db.Column(db.Text, nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey(Author.id), nullable=False)
author = db.relationship(Author, backref='posts')
def __pushrod_normalize__(self, pushrod):
data = {
'id': self.id,
'title': self.title,
'timestamp': self.timestamp,
'content': self.content,
'author': self.author,
}
if not getattr(g, 'list_view', False):
data['comments'] = self.comments
return pushrod.normalize(data)
class Comment(db.Model):
__tablename__ = "comments"
id = db.Column(db.Integer, primary_key=True, nullable=False)
author = db.Column(db.String(80), nullable=False)
timestamp = db.Column(db.DateTime, nullable=False, default=now())
content = db.Column(db.Text, nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey(Post.id), nullable=False)
post = db.relationship(Post, backref='comments')
def __pushrod_normalize__(self, pushrod):
return pushrod.normalize({
'author': self.author,
'timestamp': self.timestamp,
'content': self.content,
})
def normalize_pagination(x, pushrod):
return pushrod.normalize({
'page': x.page,
'pages': x.pages,
'total': x.total,
'items': x.items,
})
pushrod.normalizers[Pagination] = normalize_pagination
@app.route("/")
@app.route("/<int:page>")
@pushrod_view()
def list_posts(page=1):
g.list_view = True
return Post.query.paginate(page)
@app.route("/posts/<int:id>")
@pushrod_view()
def blog_post(id):
post = Post.query.get_or_404(id)
return {'item': post}
if __name__ == '__main__': # pragma: no cover
app.run()
While it’s much better than the original, as you can see the normalizers are all of the kind {'y': x.y}. However, the inline normalizer syntax, also has a shortcut for defining fields to include, like this:
from flask import Flask, g
from flask.ext.pushrod import Pushrod, pushrod_view
from flask.ext.sqlalchemy import SQLAlchemy, Pagination
from sqlalchemy.sql.functions import now
app = Flask(__name__)
pushrod = Pushrod(app)
db = SQLAlchemy(app)
class Author(db.Model):
__tablename__ = "authors"
__pushrod_fields__ = ("name", "description")
id = db.Column(db.Integer, primary_key=True, nullable=False)
name = db.Column(db.String(80), unique=True, nullable=False)
description = db.Column(db.Text(), nullable=False)
class Post(db.Model):
__tablename__ = "posts"
id = db.Column(db.Integer, primary_key=True, nullable=False)
timestamp = db.Column(db.DateTime, nullable=False, default=now())
title = db.Column(db.String(255), unique=True, nullable=False)
content = db.Column(db.Text, nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey(Author.id), nullable=False)
author = db.relationship(Author, backref='posts')
def __pushrod_fields__(self):
fields = ["id", "timestamp", "title", "content", "author"]
if not getattr(g, 'list_view', False):
fields.append("comments")
return fields
class Comment(db.Model):
__tablename__ = "comments"
__pushrod_fields__ = ("author", "timestamp", "content")
id = db.Column(db.Integer, primary_key=True, nullable=False)
author = db.Column(db.String(80), nullable=False)
timestamp = db.Column(db.DateTime, nullable=False, default=now())
content = db.Column(db.Text, nullable=False)
post_id = db.Column(db.Integer, db.ForeignKey(Post.id), nullable=False)
post = db.relationship(Post, backref='comments')
def normalize_pagination(x, pushrod):
return pushrod.normalize({
'page': x.page,
'pages': x.pages,
'total': x.total,
'items': x.items,
})
pushrod.normalizers[Pagination] = normalize_pagination
@app.route("/")
@app.route("/<int:page>")
@pushrod_view()
def list_posts(page=1):
g.list_view = True
return Post.query.paginate(page)
@app.route("/posts/<int:id>")
@pushrod_view()
def blog_post(id):
post = Post.query.get_or_404(id)
return {'item': post}
if __name__ == '__main__': # pragma: no cover
app.run()
Note
There is also another shortcut for inline definition, for delegation. It’s used like this, and it simply uses that field instead of itself (in this case, x.that_field:
__pushrod_field__ = "that_field"