21 May 2010

Google App Engine Changes Everything

Google App Engine changes everything.

Much of the out-of-the-gate resistance inherent in kicking out a new web app has been greased by Google.

Google gives you essential software services, storage, and scalable hardware.
Apps running in App Engine scale automatically.

If boat-loads of customers start pounding on your app, App Engine allocates more resources.

You only pay for the resources you use.

Google measures resource consumption at the gigabyte level with no monthly fees or set-up charges. CPU usage, storage per month, incoming and outgoing bandwidth, and resources specific to App Engine services are potentially billable items - But ONLY if your app hits pay dirt.

Developers get a generous chunk of free resources for exploratory apps and Lean Startup hypothesis testing (approximately 5 million page views a month).

Google language support consists of Java and Python.

The following My Preferences example I cobbled together uses the Python SDK to demonstrate two key features:
  • Google Accounts (for authentication) and
  • Google Datastore (data access & persistence)
My Preferences

I wrote My Preferences to learn how out-of-the-box Google Account authentication and Datastore "cloud" persistence is done via App Engine.

My Preferences lets one sign in to select preferences for:
  • Page Theme (4 color schemes) and 
  • Time Zone (offset from GMT).
Screen capture #1 (below) shows a hyperlink to Sign in or Register. The Sign in and Register links are wired up to Google Accounts. The time displayed before Signs In is the Greenwich Mean Time (GMT) of the web server.


Screen capture #2 (below) shows the Default theme (white background) and GMT time zone after I sign in with with my Google account, but before I have specified my preferences.


Screen capture #3 (below) shows the Winter Green theme I chose. It also shows that I have changed to my time zone (GMT -5.00). These preferences are persisted and keyed to my Google account on the onChange events of the two dropdown lists.

If I Sign Out and then Sign In, my preferences will be displayed.


What You'll Need Bare Bones

Getting started requires a minimum amount of software. All free. Following is a checklist of items you'll need to copy and run My Preferences on a Windows computer (Win 7 or Windows XP SP3). You will need:
  1. The Python 2.5 programming language. There are newer versions, but I worked with the recommended version 2.5. The Windows installer for Python 2.5 from python.org is at
    http://www.python.org/ftp/python/2.5/python-2.5.msi.
  2. The Google App Engine SDK for Python. The Windows installer for version 1.3.4 from Google is at
    http://googleappengine.googlecode.com/files/GoogleAppEngine_1.3.4.msi.
Project Setup

After running the Python msi and the Google App Engine SDK for Python msi, I had to make sure the Python directory was in the Windows path.

I made a Windows folder for my source code. The source files and a sub-folder for my css stylesheets looked like the adjacent screen capture (right).

Sample Code

app.yaml (below) is a config file in the root of your app directory.

application: preferences
version: 1
runtime: python
api_version: 1

handlers:
- url: /prefs
script: prefs.py
login: required

- url: /stylesheets
static_dir: stylesheets

- url: /.*
script: main.py

models.py (below) contains my datastore objects (e.g., theme, zone, and user) that I want to persist using the Google Datastore.

from google.appengine.api import memcache
from google.appengine.api import users
from google.appengine.ext import db
import prefs

class UserPrefs(db.Model):
theme = db.StringProperty(default="0")
zone = db.StringProperty(default="0.0")
user = db.UserProperty(auto_current_user_add=True)

def cache_set(self):
memcache.set(self.key().name(), self, namespace=self.key().kind())

def put(self):
self.cache_set()
db.Model.put(self)

def get_userprefs(user_id=None):
if not user_id:
user = users.get_current_user()
if not user:
return None
user_id = user.user_id()

userprefs = memcache.get(user_id, namespace='UserPrefs')
if not userprefs:
key = db.Key.from_path('UserPrefs', user_id)
userprefs = db.get(key)
if userprefs:
userprefs.cache_set()
else:
userprefs = UserPrefs(key_name=user_id)

return userprefs

prefs.py (below) gets the page post and stores preferences. That is, the authenticated user specifies and posts their theme choice and time zone preference. The userprefs method I created is called to put( ) them into the Datastore.

from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
import models

class PrefsPage(webapp.RequestHandler):
def post(self):
userprefs = models.get_userprefs()
try:
zonePost = self.request.get('selZone', allow_multiple=False)
userprefs.zone = zonePost

themePost = self.request.get('selTheme', allow_multiple=False)
userprefs.theme = themePost

userprefs.put()

except ValueError:
# Ignore for now.
pass

self.redirect('/')

application = webapp.WSGIApplication([('/prefs', PrefsPage)],
debug=True)

def main():
run_wsgi_app(application)

if __name__ == '__main__':
main()

main.py (below) is the main page that includes code and HTML.

Python is persnickety about whitespace and column alignment, so beware. If you cut & paste the code below, you'll probably have to format it nicely so Python can digest it. I had to jury rig the HTML for this blog post, so lookout for munged HTML if you cut & paste.


from google.appengine.api import users
from google.appengine.ext import webapp
from google.appengine.ext.webapp.util import run_wsgi_app
import datetime
import models

class MainPage(webapp.RequestHandler):

def get(self):
time = datetime.datetime.now()
user = users.get_current_user()

def IsEq(a,b):
if str(a)==str(b):
return "selected=selected"
else:
return ""

if not user:
prefsForm = ''
myTheme = "0"
signInOut = ('To set and persist preferences, <a href="%s" style="text-decoration:none">Sign in or Register</a>.'
% (users.create_login_url(self.request.path)))
else:
uPrf = models.get_userprefs()
prefsForm = '''
<form action="/prefs" method="post" name="frmMyPrefs">
<label for="selTheme">
Theme<br/>
</label>
<select name="selTheme" id="selTheme" OnChange ="document.frmMyPrefs.submit()" style="font-family:Arial, sans-serif; font-size:12px">
<option %s value="0">Default</option>
<option %s value="1">Slate Blue</option>
<option %s value="2">Salmon Eggs</option>
<option %s value="3">Winter Green</option>
</select><br/><br/>

<label for="selZone">
Time Zone<br/>
</label>
<select name="selZone" id="selZone" OnChange ="document.frmMyPrefs.submit()" style="font-family:Arial, sans-serif; font-size:12px">
<option %s value="-12.0">GMT -12:00</option>
<option %s value="-11.0">GMT -11:00</option>
<option %s value="-10.0">GMT -10:00</option>
<option %s value="-9.0">GMT -9:00</option>
<option %s value="-8.0">GMT -8:00</option>
<option %s value="-7.0">GMT -7:00</option>
<option %s value="-6.0">GMT -6:00</option>
<option %s value="-5.0">GMT -5:00</option>
<option %s value="-4.0">GMT -4:00</option>
<option %s value="-3.5">GMT -3:30</option>
<option %s value="-3.0">GMT -3:00</option>
<option %s value="-2.0">GMT -2:00</option>
<option %s value="-1.0">GMT -1:00</option>
<option %s value="0.0">GMT (Greenwich Mean Time)</option>
<option %s value="1.0">GMT +1:00</option>
<option %s value="2.0">GMT +2:00</option>
<option %s value="3.0">GMT +3:00</option>
<option %s value="3.5">GMT +3:30</option>
<option %s value="4.0">GMT +4:00</option>
<option %s value="4.5">GMT +4:30</option>
<option %s value="5.0">GMT +5:00</option>
<option %s value="5.5">GMT +5:30</option>
<option %s value="5.75">GMT +5:45</option>
<option %s value="6.0">GMT +6:00</option>
<option %s value="7.0">GMT +7:00</option>
<option %s value="8.0">GMT +8:00</option>
<option %s value="9.0">GMT +9:00</option>
<option %s value="9.5">GMT +9:30</option>
<option %s value="10.0">GMT +10:00</option>
<option %s value="11.0">GMT +11:00</option>
<option %s value="12.0">GMT +12:00</option>
</select>
</form>
''' % (IsEq(uPrf.theme,"0"),IsEq(uPrf.theme,"1"),IsEq(uPrf.theme,"2"),IsEq(uPrf.theme,"3"),IsEq(uPrf.zone,"-12.0"),IsEq(uPrf.zone,"-11.0"),IsEq(uPrf.zone,"-10.0"),IsEq(uPrf.zone,"-9.0"),IsEq(uPrf.zone,"-8.0"),IsEq(uPrf.zone,"-7.0"),IsEq(uPrf.zone,"-6.0"),IsEq(uPrf.zone,"-5.0"),IsEq(uPrf.zone,"-4.0"),IsEq(uPrf.zone,"-3.5"),IsEq(uPrf.zone,"-3.0"),IsEq(uPrf.zone,"-2.0"),IsEq(uPrf.zone,"-1.0"),IsEq(uPrf.zone,"0.0"),IsEq(uPrf.zone,"1.0"),IsEq(uPrf.zone,"2.0"),IsEq(uPrf.zone,"3.0"),IsEq(uPrf.zone,"3.5"),IsEq(uPrf.zone,"4.0"),IsEq(uPrf.zone,"4.5"),IsEq(uPrf.zone,"5.0"),IsEq(uPrf.zone,"5.5"),IsEq(uPrf.zone,"5.75"),IsEq(uPrf.zone,"6.0"),IsEq(uPrf.zone,"7.0"),IsEq(uPrf.zone,"8.0"),IsEq(uPrf.zone,"9.0"),IsEq(uPrf.zone,"9.5"),IsEq(uPrf.zone,"10.0"),IsEq(uPrf.zone,"11.0"),IsEq(uPrf.zone,"12.0"))
if uPrf.zone.endswith('0'):
time += datetime.timedelta(hours=int(uPrf.zone.split('.')[0]),minutes=0)
else:
time += datetime.timedelta(hours=int(uPrf.zone.split('.')[0]),minutes=30)
signInOut = ('Welcome, %s.&nbsp;&nbsp;<a href="%s" style="text-decoration:none">Sign Out</a>'
% (user.nickname(), users.create_logout_url(self.request.path)))
myTheme = uPrf.theme

self.response.headers['Content-Type'] = 'text/html'
self.response.out.write('''
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>My Preferences</title>
<link rel="stylesheet" type="text/css" href="stylesheets/style''' + myTheme + '''.css" />
</head>
<body>
<div class="signInOut">%s</div>
<div style="clear:both;padding-left:5px;padding-top:5px;">%s</div>
<div style="clear:both;padding-left:5px;padding-top:10px;">%s</div>
</body>
</html>
''' % (signInOut, prefsForm, str(time.strftime("%A, %d %B %Y &nbsp;&nbsp;<b>%I:%M:%S</b>"))))

application = webapp.WSGIApplication([('/', MainPage)],
debug=True)

def main():
run_wsgi_app(application)

if __name__ == '__main__':
main()

index.yaml (not shown) is an auto-generated file that is updated whenever the dev_appserver detects that a new type of query has been run.

style0.css (below) contains the css stylesheet for the Default theme. There are 3 additional stylesheets (style1.css, style2.css, and style3.css) that are identical except for tweaks in colors. Here's the Default template:

body
{
background-color:white;
font-family:Arial,sans-serif;
font-size:13px;
margin-left:0px;
margin-right:0px;
margin-top:0px;
}
a:link{font-weight:normal}
a:visited{font-weight:normal}
a:active{font-weight:normal}
a:hover{font-weight:bold}
.signInOut
{
padding-left:5px;
color:black;
background-color:#E0E0E0;
clear:both;
padding-bottom:5px;
padding-top:5px;
border-bottom:solid 1px #C0C0C0;
}

Kickin It Old-School

You invoke the Python compiler from a command prompt as shown below.


Once compilation completes, you can run the app from localhost on port 8080 as shown below.


Uploading To Google App Engine

Once you have developed your app locally, you can create a developer account and upload it to run on Google App Engine via the App Engine Admin Console.

Resources

Programming Google App Engine by Dan Sanderson.