| 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:
- 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.
- 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. <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 <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.