Pages

Wednesday, April 29, 2009

MVC Controller for webapp using Routes

Foloowing on from my previous post my MVC home brew is coming along slowly. I decided to implement Routes (Rails style routing for Python), basing my wsgi class on one I found on the App Engine Recipe site.

I altered the wsgi class very slightly to use a constructor passing in the request and response objects.
# Initialize matched controller from given module.
__import__(module_name)
module = sys.modules[module_name]

if (module is not None and hasattr(module, class_name)):
controller = getattr(module, class_name)(request, response)
else:
raise ImportError('Controller %s could not be initialized.' % (class_name))

The Controller itself acts as a base class for the actual concrete controllers which will be used in the application itself.
import os
import urlparse
import cgi
import logging
import sys
import traceback
from google.appengine.ext.webapp import template
from SessionManager import SessionManager
from routes.util import url_for

class Controller(object):
def __init__(self, request, response):
self.request = request
self.response = response
self.sessionManager = SessionManager(self.request, self.response)
self.session = self.sessionManager.current()
self.initialize()

def initialize(self):
pass

def handle_exception(self, e, debug):
#self.error(500)
logging.exception(e)
if debug:
lines = ''.join(traceback.format_exception(*sys.exc_info()))
self.response.clear()
self.response.out.write('%s' % (cgi.escape(lines, quote=True)))

def error(self, code):
"""Clears the response output stream and sets the given HTTP error code.

Args:
code: the HTTP status error code (e.g., 501)
"""
self.response.set_status(code)
self.response.clear()

def rendertemplate(self, context, path):
if (path is None):
path = __file__ + '.html'

if (context is None):
context = dict()

context['session'] = self.session
context['request'] = self.request

path = os.path.join(os.path.dirname(__file__), path)
logging.warn(path)
self.response.out.write(template.render(path, context))

def redirect(self, uri, permanent=False):
"""Issues an HTTP redirect to the given relative URL.

Args:
uri: a relative or absolute URI (e.g., '../flowers.html')
permanent: if true, we use a 301 redirect instead of a 302 redirect
"""
if permanent:
self.response.set_status(301)
else:
self.response.set_status(302)

absolute_url = urlparse.urljoin(self.request.uri, uri)
self.response.headers['Location'] = str(absolute_url)
self.response.clear()

The Controller class has a couple of methods of note:

def rendertemplate(self, context, path): which takes a dictionary (context) and the relative path to an HTML template. The templates themselves will use the Django template markup tags.

def redirect(self, uri, permanent=False): which does an HTTP 301 or 302 redirect.

Here's an example concrete Controller class where the view method is the action as defined using Routes.

import twitter as twitter
from controllers.Controller import Controller as controllerBase

class ConcreteController(controllerBase):
def view(self, id):

data = doSomething(id)

template_values['data'] = data

self.rendertemplate(template_values, '../views/template.html')

Saturday, April 25, 2009

Session handling with Memcache API in Google AppEngine

I've been trying to teach myself Python by witting a basic Twitter client in Google AppEngine. 

One of the things which I found lacking from the Webapp and Django frameworks was anything to manage sessions well. So, realising that the Memcache API has a timeout I figured that I could use that as the basis for session management. 

I ended up creating 2 classes, a SessionManager class to create, retrieve and delete sessions ans a Session object which would act as a simple dictionary to store arbitrary session values in. 

I would then be able to use the Session and SessionManager classes like this in my Controller class: 
class Controller(object):
def __init__(self, request, response):
self.request = request
self.response = response
self.sessionManager = SessionManager(self.request, self.response)
self.session = self.sessionManager.current()
self.initialize()


The SessionManager constructor needs both Request and Response objects because it needs to read and write Cookies. 

Here's the SessionManager class: 
from google.appengine.api import memcache
import datetime
import random

class SessionManager(object):
def __init__(self, request, response, timeout=1200):
self.timeout = timeout
self.request = request
self.response = response
self.cookieName = 'SID'
def current(self):
cookievalue = self.request.cookies.get(self.cookieName)
if ((cookievalue is not None) and (memcache.get(cookievalue) is not None)):
return Session(cookievalue, self.timeout, False)
else:
return self.createSession()
def createSession(self):
newId = self.createNewId()
memcache.set(key=newId, value=True, time=self.timeout, )
now = datetime.datetime.now()
inc = datetime.timedelta(seconds=self.timeout)
now += inc
self.setCookie(key=self.cookieName,value=newId,expires=now)
return Session(newId, self.timeout, True)

def destroySession(self):
self.clearCookie(self.cookieName)
def createNewId(self):
newHash = str(hash(datetime.datetime.utcnow().strftime('%Y%m%d%H%M%S%f'))) + str(random.random())
while memcache.get(newHash) is not None:
newHash = self.CreateNewId()
return newHash
def setCookie(self,key,value,expires,path='/'):
self.response.headers.add_header('Set-Cookie', key+'='+value+ ' path='+path+'; expires '+expires.strftime('%a, %d-%b-%Y %H:%M:00 %Z'))
def clearCookie(self,key):
self.setCookie(key=key,value='',expires=datetime.datetime.now())


I'm creating a unique sessionId by concatenating the current date/time with a random number. 

Here's the Session class: 
class Session(object):
def __init__(self, id, timeout, isNew=False):
self.id = id
self.IsNew = isNew
self.keys = dict()
self.timeout = timeout
memcache.set(key=self.id+'__keys', value=self.keys, time=timeout)
def __getitem__(self, key):
return memcache.get(self.id+'_'+key)
def __setitem__(self,key,value):
memcache.set(key=self.id+'_'+key, value=value, time=self.timeout)
self.keys[key] = value
memcache.set(key=self.id+'__keys', value=self.keys, time=self.timeout)
def hasKey(self, key):
self.keys = memcache.get(self.id+'__keys')
return (key in self.keys)


That's kind of it really. It's working well at the moment but I've not used it with very large numbers of concurrent users.