Diving into the code

For simplicity all example code is contained within a single module,examples/twitter/app.py. For a guide on structuring larger Flask apps withpeewee, check out Structuring Flask Apps.

Models

In the spirit of the popular web framework Django, peewee uses declarativemodel definitions. If you’re not familiar with Django, the idea is that youdeclare a model class for each table. The model class then defines one or morefield attributes which correspond to the table’s columns. For the twitterclone, there are just three models:

  • User:
  • Represents a user account and stores the username and password, an emailaddress for generating avatars using gravatar, and a datetime fieldindicating when that account was created.
  • Relationship:
  • This is a utility model that contains two foreign-keys tothe User model and stores which users follow one another.
  • Message:
  • Analogous to a tweet. The Message model stores the text content ofthe tweet, when it was created, and who posted it (foreign key to User).

If you like UML, these are the tables and relationships:../_images/schema.jpgIn order to create these models we need to instantiate aSqliteDatabase object. Then we define our model classes, specifyingthe columns as Field instances on the class.

  1. # create a peewee database instance -- our models will use this database to
  2. # persist information
  3. database = SqliteDatabase(DATABASE)
  4.  
  5. # model definitions -- the standard "pattern" is to define a base model class
  6. # that specifies which database to use. then, any subclasses will automatically
  7. # use the correct storage.
  8. class BaseModel(Model):
  9. class Meta:
  10. database = database
  11.  
  12. # the user model specifies its fields (or columns) declaratively, like django
  13. class User(BaseModel):
  14. username = CharField(unique=True)
  15. password = CharField()
  16. email = CharField()
  17. join_date = DateTimeField()
  18.  
  19. # this model contains two foreign keys to user -- it essentially allows us to
  20. # model a "many-to-many" relationship between users. by querying and joining
  21. # on different columns we can expose who a user is "related to" and who is
  22. # "related to" a given user
  23. class Relationship(BaseModel):
  24. from_user = ForeignKeyField(User, backref='relationships')
  25. to_user = ForeignKeyField(User, backref='related_to')
  26.  
  27. class Meta:
  28. # `indexes` is a tuple of 2-tuples, where the 2-tuples are
  29. # a tuple of column names to index and a boolean indicating
  30. # whether the index is unique or not.
  31. indexes = (
  32. # Specify a unique multi-column index on from/to-user.
  33. (('from_user', 'to_user'), True),
  34. )
  35.  
  36. # a dead simple one-to-many relationship: one user has 0..n messages, exposed by
  37. # the foreign key. because we didn't specify, a users messages will be accessible
  38. # as a special attribute, User.messages
  39. class Message(BaseModel):
  40. user = ForeignKeyField(User, backref='messages')
  41. content = TextField()
  42. pub_date = DateTimeField()

Note

Note that we create a BaseModel class that simply defines what databasewe would like to use. All other models then extend this class and will alsouse the correct database connection.

Peewee supports many different field types which map todifferent column types commonly supported by database engines. Conversionbetween python types and those used in the database is handled transparently,allowing you to use the following in your application:

  • Strings (unicode or otherwise)
  • Integers, floats, and Decimal numbers.
  • Boolean values
  • Dates, times and datetimes
  • None (NULL)
  • Binary data

Creating tables

In order to start using the models, its necessary to create the tables. This isa one-time operation and can be done quickly using the interactive interpreter.We can create a small helper function to accomplish this:

  1. def create_tables():
  2. with database:
  3. database.create_tables([User, Relationship, Message])

Open a python shell in the directory alongside the example app and execute thefollowing:

  1. >>> from app import *
  2. >>> create_tables()

Note

If you encounter an ImportError it means that either flask or _peewee_was not found and may not be installed correctly. Check the Installing and Testingdocument for instructions on installing peewee.

Every model has a create_table() classmethod which runs a SQLCREATE TABLE statement in the database. This method will create the table,including all columns, foreign-key constraints, indexes, and sequences. Usuallythis is something you’ll only do once, whenever a new model is added.

Peewee provides a helper method Database.create_tables() which willresolve inter-model dependencies and call create_table() oneach model, ensuring the tables are created in order.

Note

Adding fields after the table has been created will require you toeither drop the table and re-create it or manually add the columnsusing an ALTER TABLE query.

Alternatively, you can use the schema migrations extensionto alter your database schema using Python.

Establishing a database connection

You may have noticed in the above model code that there is a class defined onthe base model named Meta that sets the database attribute. Peewee allowsevery model to specify which database it uses. There are many Metaoptions you can specify which control the behavior of yourmodel.

This is a peewee idiom:

  1. DATABASE = 'tweepee.db'
  2.  
  3. # Create a database instance that will manage the connection and
  4. # execute queries
  5. database = SqliteDatabase(DATABASE)
  6.  
  7. # Create a base-class all our models will inherit, which defines
  8. # the database we'll be using.
  9. class BaseModel(Model):
  10. class Meta:
  11. database = database

When developing a web application, it’s common to open a connection when arequest starts, and close it when the response is returned. You should alwaysmanage your connections explicitly. For instance, if you are using aconnection pool, connections will only be recycled correctly ifyou call connect() and close().

We will tell flask that during the request/response cycle we need to create aconnection to the database. Flask provides some handy decorators to make this asnap:

  1. @app.before_requestdef before_request(): database.connect()

  2. @app.after_requestdef after_request(response): database.close() return response

Note

Peewee uses thread local storage to manage connection state, so thispattern can be used with multi-threaded WSGI servers.

Making queries

In the User model there are a few instance methods that encapsulate someuser-specific functionality:

  • following(): who is this user following?
  • followers(): who is following this user?

These methods are similar in their implementation but with an importantdifference in the SQL JOIN and WHERE clauses:

  1. def following(self):
  2. # query other users through the "relationship" table
  3. return (User
  4. .select()
  5. .join(Relationship, on=Relationship.to_user)
  6. .where(Relationship.from_user == self)
  7. .order_by(User.username))
  8.  
  9. def followers(self):
  10. return (User
  11. .select()
  12. .join(Relationship, on=Relationship.from_user)
  13. .where(Relationship.to_user == self)
  14. .order_by(User.username))

Creating new objects

When a new user wants to join the site we need to make sure the username isavailable, and if so, create a new User record. Looking at the join() view,we can see that our application attempts to create the User usingModel.create(). We defined the User.username field with a uniqueconstraint, so if the username is taken the database will raise anIntegrityError.

  1. try:
  2. with database.atomic():
  3. # Attempt to create the user. If the username is taken, due to the
  4. # unique constraint, the database will raise an IntegrityError.
  5. user = User.create(
  6. username=request.form['username'],
  7. password=md5(request.form['password']).hexdigest(),
  8. email=request.form['email'],
  9. join_date=datetime.datetime.now())
  10.  
  11. # mark the user as being 'authenticated' by setting the session vars
  12. auth_user(user)
  13. return redirect(url_for('homepage'))
  14.  
  15. except IntegrityError:
  16. flash('That username is already taken')

We will use a similar approach when a user wishes to follow someone. Toindicate a following relationship, we create a row in the Relationship tablepointing from one user to another. Due to the unique index on from_user andto_user, we will be sure not to end up with duplicate rows:

  1. user = get_object_or_404(User, username=username)
  2. try:
  3. with database.atomic():
  4. Relationship.create(
  5. from_user=get_current_user(),
  6. to_user=user)
  7. except IntegrityError:
  8. pass

Performing subqueries

If you are logged-in and visit the twitter homepage, you will see tweets fromthe users that you follow. In order to implement this cleanly, we can use asubquery:

Note

The subquery, user.following(), by default would ordinarily select allthe columns on the User model. Because we’re using it as a subquery,peewee will only select the primary key.

  1. # python code
  2. user = get_current_user()
  3. messages = (Message
  4. .select()
  5. .where(Message.user.in_(user.following()))
  6. .order_by(Message.pub_date.desc()))

This code corresponds to the following SQL query:

  1. SELECT t1."id", t1."user_id", t1."content", t1."pub_date"
  2. FROM "message" AS t1
  3. WHERE t1."user_id" IN (
  4. SELECT t2."id"
  5. FROM "user" AS t2
  6. INNER JOIN "relationship" AS t3
  7. ON t2."id" = t3."to_user_id"
  8. WHERE t3."from_user_id" = ?
  9. )

Other topics of interest

There are a couple other neat things going on in the example app that are worthmentioning briefly.

  • Support for paginating lists of results is implemented in a simple function calledobject_list (after it’s corollary in Django). This function is used by allthe views that return lists of objects.
  1. def object_list(template_name, qr, var_name='object_list', **kwargs):
  2. kwargs.update(
  3. page=int(request.args.get('page', 1)),
  4. pages=qr.count() / 20 + 1)
  5. kwargs[var_name] = qr.paginate(kwargs['page'])
  6. return render_template(template_name, **kwargs)
  • Simple authentication system with a login_required decorator. The firstfunction simply adds user data into the current session when a user successfullylogs in. The decorator login_required can be used to wrap view functions,checking for whether the session is authenticated and if not redirecting to thelogin page.
  1. def auth_user(user):
  2. session['logged_in'] = True
  3. session['user'] = user
  4. session['username'] = user.username
  5. flash('You are logged in as %s' % (user.username))
  6.  
  7. def login_required(f):
  8. @wraps(f)
  9. def inner(*args, **kwargs):
  10. if not session.get('logged_in'):
  11. return redirect(url_for('login'))
  12. return f(*args, **kwargs)
  13. return inner
  • Return a 404 response instead of throwing exceptions when an object is notfound in the database.
  1. def get_object_or_404(model, *expressions):
  2. try:
  3. return model.get(*expressions)
  4. except model.DoesNotExist:
  5. abort(404)

Note

To avoid having to frequently copy/paste object_list() orget_object_or_404(), these functions are included as part of theplayhouse flask extension module.

  1. from playhouse.flask_utils import get_object_or_404, object_list