An Introduction to BFG part 2 of ...
In part 1, we really didn't get into any code, just a high level overview of what bfg is, and why I've taken an interest in it. From here on out, it will get pretty technical. For this article I thought it would be nice to get something significant working that demonstrates how model traversal can be used.
If you are coming from another framework this may be an odd feature, however I believe it lends itself nicely to some situations. But first we have to have an app to work with. So let's do that.
Environment Setup
I assume if you are following along you at least have python setup on your OS of choice, however what you might not have is a virtual environment setup. If not, you should. Spend a few moments reading/installing virtualenv It's a HUGE help.
We'll start by creating and activating the virtual environment for this project.
twillis:~/Documents$ cd ~/ twillis:~$ cd projects/ twillis:~/projects$ virtualenv-2.6 --no-site-packages bfgbb New python executable in bfgbb/bin/python2.6 Also creating executable in bfgbb/bin/python Installing setuptools............done. twillis:~/projects$ cd bfgbb/ twillis:~/projects/bfgbb$ source bin/activate (bfgbb)twillis:~/projects/bfgbb$
So now we have an environment that we can play in that won't impact our system python installation.
Downloading BFG
Now that we have our own environment to play in, it's time to install bfg. It can be installed via easy_install like most python libraries.
(bfgbb)twillis:~/projects/bfgbb$ easy_install repoze.bfg Searching for repoze.bfg Reading http://pypi.python.org/simple/repoze.bfg/ Reading http://bfg.repoze.org Reading http://www.repoze.org Best match: repoze.bfg 1.1b2 <lots of output>
Creating the skeleton app
If you are familiar with pylons this step should look pretty familiar. You begin a project using "paster create" command and a template. For this application I used the bfg_alchemy template, because I like sqlalchemy , bfg comes with several templates though.
(bfgbb)twillis:~/projects/bfgbb$ mkdir src (bfgbb)twillis:~/projects/bfgbb$ cd src (bfgbb)twillis:~/projects/bfgbb/src$ paster create -t bfg_alchemy bb <lots more output>
And now one final step and then we can get to looking at some code. We need to make our virtual environment aware of the application we will be creating, this is done via the standard setuptools way.
(bfgbb)twillis:~/projects/bfgbb/src$ cd bb (bfgbb)twillis:~/projects/bfgbb/src/bb$ python setup.py develop <even more output>
And finally, a bfg app can be run just like a pylons app because it is a wsgi application that runs off of a configuration file.
(bfgbb)twillis:~/projects/bfgbb/src/bb$ paster serve --reload bb.ini Starting subprocess with file monitor Starting server in PID 3992. serving on 0.0.0.0:6543 view at http://127.0.0.1:6543
And point your browser of choice at localhost port 6543 and marvel at your creation.
Model Traversal
OK so it took a while to get to what I really wanted to cover. I would recommend that you spend some time surfing through all the files that were generated, we will likely cover them all throughout the series.
The Model
I had mentioned in the previous article how bfg has a unique way of routing requests. The crux of how it works is that when a request comes in a root model object is consulted and traversed until there's no more to traverse. The last object it finds, is selected to be the context. Next, a configuration file is consulted to select a view function. The context, and the request are then used to call the configured view function which is responsible for returning a response.
So what kind of complex machinery do you need to write and configure for your object to be traversed? Get ready for it...
class Model(object):
__name__ = None
__parent__ = None
This isn't to useful but it's all that is required for a context to be given to a view. What's even more useful in a site is a set of models that correspond to a url pattern.
For example, if you wanted your site to have these urls...
All you need is a model for your root which also implements the "__getitem__" method.
class Base(object):
"""
Base object to handle setting the required __parent__ and __name__ attributes
"""
def __init__(self,parent = None,name = None):
self.__parent__ = parent
self.__name__ = self.__class__.__name__.lower()
class Foo(Base):
"""
Foo object
"""
pass
class Bar(Base):
"""
Bar Object
"""
pass
class Root(Base):
"""
Root object which has a foo attribute and a bar attribute, and implements __getitem__
"""
__parent__ = None
__name__ = None
def __init__(self):
self.foo = Foo(self)
self.bar = Bar(self)
def __getitem__(self,key):
if hasattr(self,str(key)):
return getattr(self,key,None)
else:
raise KeyError, key
root = Root()
Yep that's pretty much it. So let's run through a couple of scenarios to help it sink in.
Say for some odd reason you wanted an application that responded to any url and never returned a 404 response. I created a quick bfg application called "echo" to do just this.
class RootModel(object):
def __init__(self, name = None, parent = None):
self.__name__ = name
self.__parent__ = parent
def __getitem__(self,key):
return self.__class__(name = key, parent = self)
This is a simple enough hack. Basically for any key that is used to call "__getitem__" will result in an instance of RootModel being returned with it's "__parent__" and "__name__" attributes set. Because of this, your application can have the effect much like Wikipedia in that instead of returning "(404) page not found" it throws up a page offering the user an opportunity to create the page if they want.
The Root Model
When a request comes in to a bfg application that relies on model traversal, a root model object is consulted first. Because after all you have to start somewhere. Unless you do something fancy to your models.py, there are a few lines of plumbing for getting a root model for bfg to use.
models.py
class RootModel(object):
def __init__(self, name = None, parent = None):
self.__name__ = name
self.__parent__ = parent
def __getitem__(self,key):
return self.__class__(name = key, parent = self)
root = RootModel()
def get_root(environ):
return root
For the sake of completeness, we should take a look at the run.py file to see how everything gets bootstrapped together when the application is started up.
run.py
from repoze.bfg.router import make_app
def app(global_config, **kw):
""" This function returns a repoze.bfg.router.Router object. It
is usually called by the PasteDeploy framework during ``paster
serve``"""
# paster app config callback
from echo.models import get_root
import echo
return make_app(get_root, echo, options=kw)
As you can see, the "models.py" module is imported and the "get_root" method is used to initialize the application. And in the "models.py" module we see that "get_root" merely returns whatever is set as the variable "root" in the file.
A model being selected as context is only half the task, we still have to somehow transform that into a response. The way that's done in bfg_ is by consulting the configure.zcml file. The configure.zcml file can contain a variety of settings besides views, but the main question it answers for the app is "Given a model of this type, what do I do with it?"
configure.zcml
<configure xmlns="http://namespaces.repoze.org/bfg">
<!-- this must be included for the view declarations to work -->
<include package="repoze.bfg.includes" />
<view
for=".models.RootModel"
view=".views.my_view"
renderer="templates/mytemplate.pt"
/>
<static
name="static"
path="templates/static"
/>
</configure>
From this configuration you can see that for an object of type RootModel, .views.my_view is the selected view. What is "my_view"? It's just a callable. You'll also notice there's a renderer of "templates/mytemplate.pt".
views.py
def my_view(context, request):
return {'project':'echo'}
mytemplate.pt
<html>
<head>
<title>${project}:${context.__name__}</title>
</head>
<body>
<h1>${context.__name__}</h1>
<p>Nothing to see here carry on....</p>
<div>
Your User-Agent ${request.headers["User-Agent"]}
</div>
</body>
</html>
As you can see, all the things we passed into the view are made available to the template. Including the context, the request, and whatever items are in the dictionary that "my_view" returns. And that's how it works in a nutshell.
Stepping Back
So let's step back for a moment, it's important to note that a model is not a model in the sense we've come to know in other frameworks, it's not an ActiveRecord pattern. I haven't even motioned a database yet, but there's nothing stopping you from retrieving models from a database. If you can identify them by key, you can likely get it set as the context for a view based on some url pattern.
For example, you could have a PageManager model that can return pages by title, or id, or whatever you can think of to uniquely identify them. Your app urls could theoretically look like...
http://example.com/page/1
You could then have a PageMapper model that maps takes a "friendly" url fragment and maps it to a page id.
http://example.com/thepage.html
And to tie it all together, your root could be of type Site that takes care of the initial traversal, when given the "page" key, gives back the PageManager object, and anything else is handed off to the PageMapper for further processing.
You could potentially have the same behavior as what drupal has.
The Calendar Model
The example app relies on a CalendarContainer model so that it can have a url pattern like...
http://localhost:6543/2009/10/1
And arriving at this url would show you all the dated items that are applicable for that date. But that's not all.....
http://localhost:6543/2009/10
Would show you all the dated items applicable for that month. And finally...
http://localhost:6543/2009
Would show you all the applicable items for that year.
Database Model
The database model has 2 objects, a DatedItem which is merely information with a date. And an EventItem which is a DatedItem with an end date. For this we will use sqlalchemy the declarative way.
models/calendar/data.py
"""
Sql classes for calendar
"""
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Integer
from sqlalchemy import Unicode, UnicodeText
from sqlalchemy import DateTime
from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import or_, and_
Base = declarative_base()
DBSession = None
transaction = None
def _s():
"""
returns session instance, relies on global being set before import
"""
global DBSession
return DBSession()
def _t():
"""
returns reference to zope transaction, relies on global being set before import
"""
global transaction
return transaction
class DatedItem(Base):
"""
Basic Dated item, has date, title, and body
"""
__tablename__ = "dated_items"
id = Column(Integer, primary_key = True)
discriminator = Column("type",Unicode(50))
date = Column(DateTime, nullable = False)
title = Column(Unicode(255),nullable = False)
body = Column(UnicodeText, nullable = True)
__mapper_args__ = {"polymorphic_on":discriminator}
@classmethod
def get(cls,id):
_session = _s()
return _session.query(cls).get(id)
@classmethod
def get_for_range(cls,begin,end):
_session = _s()
query = _session.query(cls).filter(
cls.date.between(begin,end)
).order_by(cls.date)
return query
def __repr__(self):
return "%s\n==========\n%s" % (self.__class__.__name__,
"\n".join(["%s: %s" % (k,v) for k,v in self.__dict__.items()]))
class EventItem(DatedItem):
"""
Basic Dated item. Will likely have subclasses
"""
__tablename__ = 'event_items'
__mapper_args__ = {"polymorphic_identity":"event_item"}
id = Column(Integer, ForeignKey(DatedItem.id),primary_key = True)
end_date = Column(DateTime, nullable = False)
def __init__(self,**kw):
for k,v in kw.items():
setattr(self, k, v)
@classmethod
def get_for_range(cls,begin,end):
_session = _s()
query = _session.query(cls).filter(
or_(
cls.date.between(begin,end),
cls.end_date.between(begin,end),
or_(
and_(cls.date<=begin,cls.end_date>=end)
)
),).order_by(cls.date).order_by(cls.end_date)
return query
Context Model
In order to make this work I also have a year, month and day model. Which do nothing as far as the database goes except define a date range to query the database for items.
Year, Month and Day all inherit from an object called "DateContext" which implements the idea of a date range. If you look back at the Database Model, you'll notice both classes have a get_for_range which takes a begin and end date. Since the date context holds that, getting objects relevant for a given year, month or day should be relatively easy. Here's the implementation of the DateContext.
models/calendar/date_context.py
from .data import DatedItem, EventItem
class DateContext(object):
"""
Superclass for models in calendar object graph
"""
def __init__(self,date_ctx,parent):
self.__date_context = date_ctx
self.__parent__ = parent
def items(self,cls = None):
"""
returns calendar_data items filtered by cls for the current date range
"""
if not cls:
qrys = []
for c in (DatedItem,EventItem):
qrys.append(c.get_for_range(self.date_context,
self.max).all())
results = {}
for qry in qrys:
for o in qry:
results[o.id] = o
#tweak sort as generator
return results.values()
else:
return cls.get_for_range(
self.date_context,
self.max).all()
def _get_context(self):
return self.__date_context
def _get_max(self):
return getattr(self,"__max__",None)
date_context = property(_get_context)
max = property(_get_max)
The magic is in the "items" method which takes a cls param and call it's "get_for_range" using it's date_context or, run the corresponding "get_for_range" on both DatedItem, and EventItem. and combine the results into a list. Another tricky part is that DateContext needs to defer to what inherits from it for the max of the range. Kind of like defining an abstract method that you require your children to implement. Except I'm doing it the easy way and just looking for an attribute named "__max__".
So now, the year, month, and day context models only differ in 4 ways.
- the way they initialize their DateContext,
- the way they set their __name__ attribute.
- the way they set their __max__ attribute.
- the way they implement their __getitem__ method.
models/calendar/year.py
from date_context import DateContext
from datetime import datetime
from .month import Month
class Year(DateContext):
"""
Year datecontext, begin 1/1/self 00:00:00 end 12/31/self 23:59:59
"""
def __init__(self,year, parent):
DateContext.__init__(self,datetime(year,1,1),parent)
self.__max__ = datetime(self.date_context.year,12,31,23,59)
self.__name__ = str(self.date_context.year)
self.months = {}
def __getitem__(self,key):
if key not in self.months:
try:
self.months[key] = Month(int(key),self)
except (TypeError,ValueError):
raise KeyError(key)
return self.months[key]
models/calendar/month.py
from date_context import DateContext
from datetime import datetime, date, timedelta
from .day import Day
def mfd(dt, d_years=0, d_months=0):
"""
first day of month
"""
# d_years, d_months are "deltas" to apply to dt
y, m = dt.year + d_years, dt.month + d_months
a, m = divmod(m-1, 12)
return date(y+a, m+1, 1)
def mld(dt):
"""
last day of month
"""
return mfd(dt, 0, 1) + timedelta(-1)
class Month(DateContext):
"""
Month date context begin self/1/parent.year 00:00:00 end self/mld(self)/self.parent.year 23:59:59
"""
def __init__(self,m,parent):
DateContext.__init__(self,datetime(parent.date_context.year,m,1),parent)
self.__name__ = str(self.date_context.month)
self.__max__ = mld(self.date_context)
self.days = {}
def __getitem__(self,key):
if key not in self.days:
try:
self.days[key] = Day(int(key),self)
except (TypeError,ValueError):
raise KeyError(key)
return self.days[key]
models/calendar/day.py
from .date_context import DateContext
from datetime import datetime, date
from hour import Hour
class Day(DateContext):
"""
Day date context
begin self.parent.month/self/self.parent.year 00:00:00
end self.parent.month/self/self.parent.year 23:59:59
"""
def __init__(self,d,parent):
DateContext.__init__(self,date(parent.date_context.year,parent.date_context.month,d),parent)
self.__name__ = str(self.date_context.day)
self.__max__ = datetime(
self.date_context.year,
self.date_context.month,
self.date_context.day,
23,
59)
self.hours={}
def __getitem__(self,key):
if key not in self.hours:
try:
self.hours[key] = Hour(int(key),self)
except (TypeError,ValueError):
raise KeyError(key)
return self.hours[key]
models/calendar/__init__.py
And finally, the CalendarContainer starts it all off. But has a few convenience methods we'll eventually use on the view side.
"""
Calendar Container takes care of selecting date context and showing data
"""
from datetime import date, datetime, timedelta
from . import data
from .data import DatedItem, EventItem
from .date_context import DateContext
from year import Year
class CalendarContainer(object):
"""
Container to handle url pattern of year/month/day/hour
"""
def __init__(self, config, name = "calendar" ,parent = None):
self.years = dict()
self.__parent__ = parent
self.__name__ = name
def __getitem__(self,key):
try:
_k = int(key)
except ValueError:
raise KeyError(key)
if _k not in self.years:
self.years[_k] = Year(_k,self)
return self.years[_k]
def today(self):
d = datetime.today()
return self[d.year][d.month][d.day]
def now(self):
d = datetime.today()
return self[d.year][d.month][d.day][d.hour]
def save(self,item):
session = data._s()
session.add(item)
And that does it for the model side. Let's tie in a quick view to see if it works.
A basic view
We'll be adding a lot to the views side in later articles but for now it's sufficient just to get something running to prove to ourselves it works as we think it does.
Since most of our context model types have a date_context , we'll whip up a quick template to display it.
templates/model.pt
<html>
<head>
<title>${context.__name__}</title>
</head>
<body>
${getattr(context,"date_context", None)}
</body>
</html>
As in our previous example, views get tied to models through the bb/configure.zcml.
<configure xmlns="http://namespaces.repoze.org/bfg">
<!-- this must be included for the view declarations to work -->
<include package="repoze.bfg.includes" />
<view
for=".models.RootContainer"
view=".views.view_root"
renderer="templates/root.pt"
/>
<view
for=".models.calendar.CalendarContainer"
view=".views.view_root"
renderer="templates/model.pt"
/>
<view
for=".models.calendar.year.Year"
view=".views.view_root"
renderer="templates/model.pt"
/>
<view
for=".models.calendar.month.Month"
view=".views.view_root"
renderer="templates/model.pt"
/>
<view
for=".models.calendar.day.Day"
view=".views.view_root"
renderer="templates/model.pt"
/>
<static
name="static"
path="templates/static"
/>
<subscriber for="repoze.bfg.interfaces.INewRequest"
handler=".run.handle_teardown"
/>
</configure>
You'll notice that every view is configured to be "view_root", this is inconsequential at the moment because our template only relies on the context that is passed in, and the context is always passed in. Later on, we'll change this around as we implement more view functions.
Tying it together
The final piece of the puzzle is the RootContainer . We could make the CalendarContainer the root but, instead we'll make it a child of RootContainer.
models/__init__.py
"""
Root Container for traversal
"""
import transaction
from sqlalchemy.orm import scoped_session
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
from zope.sqlalchemy import ZopeTransactionExtension
from .calendar import CalendarContainer
import calendar.data
from calendar.data import Base
DBSession = scoped_session(sessionmaker(expire_on_commit=False, extension=ZopeTransactionExtension()))
#pass the session along to the model classes that need it,
#probably a more elegant way to do this
#root is made available to other parts of the app here
root = None
class RootContainer(object):
"""
We'll hang the calendar and account containers off here.
Thus: both containers can peacefully coexist without knowing
about each other, and then should be reusable
"""
__name__ = None
__parent__ = None
def __init__(self,config):
self.calendar = CalendarContainer(config,name = "calendar",parent = self)
def __getitem__(self,key):
_k = key.lower()
if _k == "calendar":
return self.calendar
else:
raise KeyError,key
def make_root(config):
"""
Construct the callable that bfg calls with environ
But, initialize the RootContainer with acl's and
set a module level reference so the rest of the app
can get to it.
"""
_root = RootContainer(config)
global root
#setting module level reference to root
root = _root
def _x(environ):
return _root
return _x
def initialize_sql(db_string, db_echo):
engine = create_engine(db_string, echo=True)
DBSession.configure(bind=engine)
Base.metadata.bind = engine
Base.metadata.create_all(engine)
if calendar.data.DBSession is None:
print "setting DBSession"
calendar.data.DBSession = DBSession
else:
print "DBSession already set"
def appmaker(db_string, db_echo, config):
"""
Initialize SQL portion and make the root callable to hand back
"""
initialize_sql(db_string, db_echo)
return make_root(config)
You'll see that the "__getitem__" method looks for "calendar" as the key, anything else will be a key error. With this setup, a url like http://example.com/calendar will result in the CalendarContainer being set as the context to be handed off later to a view function. Because the CalendarContainer has no date_context attribute, our model.pt displays nothing in the body but the title you'll see is indeed "calendar". These urls will display the corresponding date_context of the passed in context in the body of the page.
And an invalid date?
- http://localhost:6543/calendar/2009/11/31 <----causes a 404
So this is still a far cry from a complete app that you would want to show to your boss, but we're off to a good start. In the next article we'll implement the data entry portions, and some views that are a bit more pleasant to look at.
Here's the source code for this revision of bb for reference
Feel free to ask questions or leave comments.
Post new comment