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.

02 May 2010

Project Places

A space starts as a lonely noun. Eventually it might be filled with other nouns.

A space becomes a place when it is full of buzzing verbs.

Project teams, team coaches, and community builders, often have the notion of space as the means to facilitate and channel interaction between team members.

Space is our latitude and longitude. Place requires a bit more thought and adaptability.

Space provides our locus, but not our focus.
We're located in a space, but we act in place.
In the oft-cited PARC paper Re-Placing-Ing Space (Harrison and Dourish, 1996), the authors draw from experience in architecture and urban design, and their research findings, to make a distinction between space and place vis-à-vis collaborative environments.

Harrison and Dourish argue it is the notion of place that frames interactive behavior in real and virtual space.
Space is the opportunity; place is the understood reality ~ Harrison and Dourish
Observing a variety of collaborative systems, Harrison and Dourish say
Place derives from a tension between connectedness and distinction, rather than from a three-dimensional structure.
Think of the symbiotic relationships of a bee colony in a three-dimensional hive.

In  A Place of My Own, author Michael Pollan argues that thoughtful crafting of space has the potential to give us a place in the world that serves our bodies, minds, and aspirations.

Behavioral Framing

A place is a space that is valued like a home might be valued over a house. Harrison and Dourish point out that the same physical space can function as different places at different times framing the appropriate behavior.

We fill our places with cultural expectations and notions of appropriate behavior.

In The Timeless Way of Building, Christopher Alexander discusses reoccurring Patterns of Events. He says,
all the life and soul of a place, all our experiences there, depend not simply on the physical environment, but on the patterns of events that we experience there
Places exist within a space. A place is within a space where we've added social context and cultural understandings that help frame and cue behavior. Something is out of place when it feels out of context.

The Forward Edge

Stanford d.school believes we can design for innovation. Planners, students and staffers have spent six years brainstorming, designing, and tweaking their environment to make it fertile ground for ideas.

After a May 7th ribbon cutting at the d.school, Fast Company will go behind-the-scenes to document how
every nook, cranny, and fungible wall system has been smartly designed to maximize collaboration.
I will be following the d.school and Fast Company's findings with interest.