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:
- admin.py - Where you register models with the site admin interface
- api.py - Where you register models to be exposed via a REST-ful API
- app.py - Your "Flask" application, configuration, and database.
- auth.py - The authentication system used to protect access to the admin.
- main.py - this is the secret sauce
- models.py - Database models for use with your ORM, business logic, etc.
- views.py - Views that handle requests.
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:
- app.py
- models.py
- auth.py
- admin.py / api.py
- views.py
- 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:
- Part 1: Building a little note-taking app with Flask and Peewee
- Part 2: Revisiting the note-taking app (adding todo lists, reminders, search, and a REST API)
- Part 3: Adding full-text search using SQLite's search engine extension
Or simply look at all of the saturday-morning hack posts.
Comments (0)
Commenting has been closed.