Using Flask blueprints to build a bookmarking app, a pastebin and a wiki

Flask blueprints for small projects

For a change, I've been doing all of my new app development using flask, a python web framework built atop the werkzeug WSGI toolkit. Having used django for the last two years it's been fun to do something different, but at the same time stick with python.

In this post I'd like to show a couple of the small projects I've written using flask over the past few weeks. I used flask-peewee to do persistence, but you could just as easily sub in something like flask-sqlalchemy or roll your own.

I elected to write all these projects using flask blueprints -- coming from django I like to think of a blueprint as a "reusable app", or in other words a self-contained component that lives at a certain URL. Blueprints are a neat idea and good for encapsulating code, templates and static media.

I'll show 3 small projects:

Bookmarking app with bookmarklet

Bookmark app in playground

I used to use a popular bookmarking site, but after rumors the site was shutting down, have since opted to store and manage my own. The goals for the project were dead simple -- store URLs and make it really easy to add new ones. I wanted to use a bookmarklet to create new bookmarks, so the views needed to support this.

The model is pretty straightforward:

class Bookmark(db.Model):
    url = TextField()
    created_date = DateTimeField(default=datetime.datetime.now)

    class Meta:
        ordering = (('created_date', 'desc'),)

    def __unicode__(self):
        return self.url

I only needed three views -- list, add, and delete. In the rare case I needed to edit a bookmark, I could just do it through the admin.

bookmarks = Blueprint('bookmarks', __name__, template_folder='templates')

@bookmarks.route('/')
@auth.login_required
def list():
    qr = Bookmark.select()
    return object_list('bookmarks/index.html', qr)

@bookmarks.route('/add/')
@auth.login_required
def add():
    url = request.args.get('url')
    if url:
        Bookmark.get_or_create(url=url)
    return redirect(url or url_for('bookmarks.list'))

@bookmarks.route('/<pk>/delete/')
@auth.login_required
def delete(pk):
    bookmark = get_object_or_404(Bookmark, id=pk)
    bookmark.delete_instance()
    return redirect(url_for('bookmarks.list'))

You may be wondering what the auth.login_required thing is all about -- this is functionality supplied by the flask-peewee extension and allows you to mark areas of your site as requiring login. Feel free to ignore it if you're not using flask-peewee.

Other helpers in use are object_list and get_object_or_404. These functions may sound familiar to users of the django framework. The former is responsible for rendering a paginated list of objects in a template, the latter for retrieving an object from the database and raising a 404 in the event a matching object does not exist. These helpers are included with flask-peewee -- for more info, check the docs.

Lastly, I needed a way to easily create bookmarks when I'm surfing. I use a bookmarklet, which is a little piece of javascript you can invoke while browsing any site. The javascript in this case simply determines the current location and sends it to the bookmark/add url, which saves a new bookmark and redirects the user to the page they were on originally:

javascript:location.href='http://<your domain>.com/bookmarks/add/?url='+location.href;

Get the blueprint and templates https://gist.github.com/d55324a3406a11661c50

If you want to see a more fleshed-out example that take screenshots of the bookmarked URLs, check out Building a Bookmarking Service with Python and PhantomJS.

A simple pastebin with a command-line tool

Playground pastebin blueprint

I use gist for pretty much everything...work, personal projects, saving configuration files, etc. There's also a nice command-line script for easily creating new gists. For fun I made a tiny pastebin that comes with a command-line script that functions similarly to the one written by defunkt.

Here's the paste model:

class Paste(db.Model):
    filename = CharField()
    content = TextField()
    highlighted = TextField()
    created_date = DateTimeField(default=datetime.datetime.now)
    sha = CharField()

    class Meta:
        ordering = (('created_date', 'desc'),)

    def __unicode__(self):
        return self.filename

    def save(self):
        self.highlighted = self.highlight()
        self.sha = hashlib.sha1(self.content).hexdigest()
        super(Paste, self).save()

    def highlight(self):
        lexer = None
        try:
            lexer = lexers.get_lexer_for_filename(self.filename)
        except lexers.ClassNotFound:
            lexer = lexers.guess_lexer(self.content)
        formatter = formatters.HtmlFormatter(linenos=True)
        return highlight(self.content, lexer, formatter)

It uses the python library pygments to do syntax highlighting, storing the highlighted content in a special field. A hash of the content will be used to uniquely identify the pastes.

Now that we've seen the model, let's look at the blueprint and the CRUD views for working with the data. These views are pretty straightforward thanks to flask-peewee, which provides helpers for retrieving objects from the database and rendering lists of objects. wtf-peewee provides integration with the wtforms form processing library.

pastebin = Blueprint('pastebin', __name__, static_folder='static', template_folder='templates')

@pastebin.route('/')
@auth.login_required
def list():
    qr = Paste.select()
    return object_list('pastebin/index.html', qr)

@pastebin.route('/add/', methods=['GET', 'POST'])
@auth.login_required
def add():
    Form = model_form(Paste, only=('filename', 'content',))
    if request.method == 'POST':
        form = Form(request.form)
        if form.validate():
            instance = Paste()
            form.populate_obj(instance)
            instance.save()
            return redirect(url_for('pastebin.list'))
    else:
        form = Form()
    return render_template('pastebin/add.html', form=form)

@pastebin.route('/<sha>/')
def detail(sha):
    paste = get_object_or_404(Paste, sha=sha)
    return render_template('pastebin/detail.html', paste=paste)

@pastebin.route('/<sha>/raw/')
def detail_raw(sha):
    paste = get_object_or_404(Paste, sha=sha)
    return Response(paste.content)

@pastebin.route('/<sha>/delete/', methods=['GET', 'POST'])
@auth.login_required
def delete(sha):
    paste = get_object_or_404(Paste, sha=sha)
    if request.method == 'POST':
        paste.delete_instance()
        return redirect(url_for('pastebin.list'))
    return render_template('pastebin/delete.html', paste=paste)

Now for the real fun part, the command-line utility. This script is designed to take a filename or read from standard in. Authentication can be provided by specifying arguments on the command line, or can be read from a global git config file. The API expects a username and "key" to be sent using HTTP basic auth.

#!/usr/bin/env python

# gitconfig
# paste.user / paste.key

# env
# PASTE_USER PASTE_KEY

import datetime
import httplib2
import json
import optparse
import os
import subprocess
import sys


ENDPOINT = 'http://<your site>/api/paste/'
URL_TEMPLATE = 'http://<your site>/pastebin/%s/'

def get_credentials(options):
    if options.user and options.key:
        return [options.user, options.key]
    elif 'PASTE_USER' in os.environ and 'PASTE_KEY' in os.environ:
        return [os.environ['PASTE_USER'], os.environ['PASTE_KEY']]
    else:
        data = []
        for piece in ('user', 'key'):
            p = subprocess.Popen(['git', 'config', '--global', 'paste.%s' % piece], stdout=subprocess.PIPE)
            data.append(p.communicate()[0].strip())
        return data

def post_file(user, key, filename, content):
    sock = httplib2.Http()
    sock.add_credentials(user, key)
    headers, resp = sock.request(ENDPOINT, 'POST', json.dumps({
        'filename': filename,
        'content': content,
    }))
    if headers['status'] == '200':
        resp_data = json.loads(resp)
        print URL_TEMPLATE % resp_data['sha']
    else:
        print 'Error: %s' % headers['status']
        print resp

def main(user, key, args):
    if args:
        for filename in args:
            fh = open(filename)
            post_file(user, key, os.path.basename(filename), fh.read())
    else:
        now = datetime.datetime.now()
        post_file(user, key, now.strftime('%Y-%m-%d %H:%M:%S'), sys.stdin.read())

if __name__ == '__main__':
    parser = optparse.OptionParser()
    parser.add_option('-u', '--user', dest='user')
    parser.add_option('-k', '--key', dest='key')

    options, args = parser.parse_args()

    user, key = get_credentials(options)
    if not user or not key:
        print 'Error: unable to determine user or api key'
        sys.exit(1)

    main(user, key, args)

The final piece is the API authentication. I created a simple REST API on top of the flask-peewee Rest API tools. Building an API with auth is probably too big a topic for this blog post, but in case you're curious, the rest resource for the Paste model looks like this:

class PasteResource(RestResource):
    exclude = ('highlighted',)

Get the blueprint, command-line tool, templates and stylesheet https://gist.github.com/62c6e55e9aa6d1e745ac

Building a tiny wiki

Editing a wiki page

The last project I'll discuss in this post is a little wiki I wrote. There's not much new here that we haven't seen, except for the addition of a template filter which will convert WikiWords into links.

Creating a template filter is as simple as decorating a function.

wikify_re = re.compile(r'\b(([A-Z]+[a-z]+){2,})\b')

@app.template_filter('wikify')
def wikify(s):
    return Markup(wikify_re.sub(r'<a href="/wiki/\1/">\1</a>', s))

The wiki uses a simple model:

class WikiArticle(db.Model):
    name = CharField()
    content = TextField()
    modified_date = DateTimeField()

    class Meta:
        ordering = (('modified_date', 'desc'),)

    def __unicode__(self):
        return self.name

    def save(self):
        self.modified_date = datetime.datetime.now()
        return super(WikiArticle, self).save()

The blueprint and views are dead simple. One somewhat interesting bit is that if a page does not exist, the view does not return a 404 but instead displays a message to the user and allows them to create the page.

wiki = Blueprint('wiki', __name__, template_folder='templates')

@wiki.route('/')
@auth.login_required
def index():
    qr = WikiArticle.select()
    return object_list('wiki/index.html', qr)

@wiki.route('/<name>/', methods=['GET', 'POST'])
@auth.login_required
def detail(name):
    WikiForm = model_form(WikiArticle, only=('name', 'content',))

    try:
        article = WikiArticle.get(name=name)
    except WikiArticle.DoesNotExist:
        article = WikiArticle(name=name)

    if request.method == 'POST':
        form = WikiForm(request.form, obj=article)
        if form.validate():
            form.populate_obj(article)
            article.save()
            flash('Your changes have been saved')
            return redirect(url_for('wiki.detail', name=article.name))
        else:
            flash('There were errors with your submission')
    else:
        form = WikiForm(obj=article)

    return render_template('wiki/detail.html', article=article, form=form)

@wiki.route('/<name>/delete/', methods=['GET', 'POST'])
@auth.login_required
def delete(name):
    article = get_object_or_404(WikiArticle, name=name)
    if request.method == 'POST':
        article.delete_instance()
        return redirect(url_for('wiki.index'))

    return render_template('wiki/delete.html', article=article)

Get the blueprint and templates https://gist.github.com/5449d29f25e0d78bc2e8

In conclusion...

I hope you enjoyed this post. I wanted to show how simple it can be to write small but useful web apps using the flask web framework. I also wanted to hilite how tools like flask blueprints and flask-peewee can greatly reduce the complexity of your code. If you're interested in reading more, here are some links that might interest you:

If you're interested in more projects like this, check out the saturday-morning hack posts.

Comments (2)

Garrett Robinson | nov 10 2011, at 02:43pm

Thanks for this post! It's very cool that you're writing your own solutions to things you feel that you need, but there's nothing already out there that quite fits the bill.

I'm playing with Flask right now, and your code is simply a gorgeous expression of its potential. Even though these are very simple webapps, I'm starting to see all of these wonderful design ideas from Django, Rails, Flask etc. merging into something great.

jim syyap | nov 02 2011, at 03:33am

Thanks for sharing these. I am a newbie programmer, teaching myself how to code in python.


Commenting has been closed.