Structuring flask apps, a how-to for those coming from Django

The other day a friend of mine was trying out flask-peewee and he had some questions about the best way to structure his app to avoid triggering circular imports. For someone new to flask, this can be a bit of a puzzler, especially if you're coming from django which automatically imports your modules. In this post I'll walk through how I like to structure my flask apps to avoid circular imports. In my examples I'll be showing how to use "flask-peewee", but the same technique should be applicable for other flask plugins.

I'll walk through the modules I commonly use in my apps, then show how to tie them all together and provide a single entrypoint into your app.

Project layout

I use a structure that may look familiar to users of the django framework:

In a little bit I'll get to the reason "main.py" is the secret sauce, for now though I'll focus on the other bits. Rather than go alphabetically, as I did in the previous list, I will go through these models in order of their precedence in the "import chain" we'll be setting up:

  1. app.py
  2. models.py
  3. auth.py
  4. admin.py / api.py
  5. views.py
  6. main.py

app.py

Every flask application needs an "app.py", whether you call it that or not. It's the place where your Flask instance lives. I like to keep my app.py very thin, so that it only contains things that are central to the entire project. I do this because, as you'll see, we will import from app pretty much everywhere.

In addition to my Flask object, I also set up a Database, a cache (if I'm using one), logging handlers, and any global template filters in app.py.

"""
I keep app.py very thin.
"""
from flask import Flask

# flask-peewee database, but could be SQLAlchemy instead.
from flask_peewee.db import Database


app = Flask(__name__)
app.config.from_object('config.Configuration')

db = Database(app)

# Here I would set up the cache, a task queue, etc.

That's it! This may seem odd if you've seen the flask "hello world", which contains URL routing, view functions, etc. As you'll see, we will be putting those in a different module.

models.py

Now that we've got a Database object, we can create our model classes. This will introduce the first "import dependency", because we need to import the database we created in app.py. Here is what a small models file might look like:

"""
models imports app, but app does not import models so we haven't created
any loops.
"""
import datetime

from flask_peewee.auth import BaseUser  # provides password helpers..
from peewee import *

from app import db


class User(db.Model, BaseUser):
    username = CharField()
    password = CharField()
    email = CharField()
    join_date = DateTimeField(default=datetime.datetime.now)
    active = BooleanField(default=True)
    admin = BooleanField(default=False)

    def __unicode__(self):
        return self.username

auth.py

After creating a User model, we can set up authentication for the app. Flask-peewee Auth takes an app, a database and a User model as its parameters. It provides free login/logout functionality, a way to get the logged-in user, and a decorator to mark a view as "login required".

"""
auth imports app and models, but none of those import auth
so we're OK
"""
from flask_peewee.auth import Auth  # Login/logout views, etc.

from app import app, db
from models import User


auth = Auth(app, db, user_model=User)

admin.py / api.py

Most of the sites I use will have an "Admin area", where dynamic content can be created, edited and deleted. Less often I might add a REST-ful API to expose models, but for completeness I'll show how they both work since they're pretty similar.

Here is admin.py:

"""
admin imports app, auth and models, but none of these import admin
so we're OK
"""
from flask_peewee.admin import Admin, ModelAdmin

from app import app, db
from auth import auth
from models import User

admin = Admin(app, auth)
auth.register_admin(admin)
# or you could admin.register(User, ModelAdmin) -- you would also register
# any other models here.

And here is api.py:

"""
api imports app, auth and models, but none of these import api.
"""
from flask_peewee.rest import RestAPI, RestResource, UserAuthentication

from app import app
from auth import auth
from models import User

user_auth = UserAuthentication(auth)

# instantiate our api wrapper and tell it to use HTTP basic auth using
# the same credentials as our auth system.  If you prefer this could
# instead be a key-based auth, or god forbid some open auth protocol.
api = RestAPI(app, default_auth=user_auth)

class UserResource(RestResource):
    exclude = ('password', 'email',)

# register our models so they are exposed via /api/<model>/
api.register(User, UserResource, auth=user_auth)

views.py

Lastly, the views. The views are responsible for mapping urls to functions, and so they generally need to reference the app, authentication and models.

"""
views imports app, auth, and models, but none of these import views
"""
from flask import render_template  # ...etc , redirect, request, url_for

from app import app
from auth import auth
from models import User

@app.route('/')
def homepage():
    return render_template('foo.html')

@app.route('/private/')
@auth.login_required
def private_view():
    # ...
    user = auth.get_logged_in_user()
    return render_tempate(...)

Tying it all together with "main.py"

These modules are all fairly self-contained, but we have no mechanism to ensure that all are imported when we run our application. We need to import all the modules, though, to capture all the great module-level side-effects. For that purpose I tie everything together with a module named "main.py".

"""
this is the "secret sauce" -- a single entry-point that resolves the
import dependencies.  If you're using blueprints, you can import your
blueprints here too.

then when you want to run your app, you point to main.py or `main.app`
"""
from app import app, db

from auth import *
from admin import admin
from api import api
from models import *
from views import *

admin.setup()
api.setup()

def create_tables():
    # Create table for each model if it does not exist.
    # Use the underlying peewee database object instead of the
    # flask-peewee database wrapper:
    db.database.create_tables([User], safe=True)

if __name__ == '__main__':
    create_tables()
    app.run()

main.py should be treated as the entry-point into your application from here on out. If you are running a WSGI server, you would therefore want to point it at main.app as opposed to app.app, if that makes sense.

Thanks

Thanks for reading this post, I hope you found it useful. Please feel free to leave any questions or comments.

For a more complete example, check out the flask-peewee example project -- it's a very small twitter clone.

I also wrote a series of posts about building a note-taking app with Flask. Here are links to the posts:

Or simply look at all of the saturday-morning hack posts.

Comments (0)


Commenting has been closed.