Move some scripts that were in development/tools/scripts back in this project

They were moved into sdk/scripts when sdk was split from development.

Change-Id: I8404ae5fdeb9060adb76357f29b42c4c8e2054ee
diff --git a/scripts/app_engine_server/LICENSE b/scripts/app_engine_server/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/scripts/app_engine_server/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/scripts/app_engine_server/app.yaml b/scripts/app_engine_server/app.yaml
new file mode 100755
index 0000000..1fb50c7
--- /dev/null
+++ b/scripts/app_engine_server/app.yaml
@@ -0,0 +1,16 @@
+application: androidappdocs-staging
+version: 1
+runtime: python
+api_version: 1
+
+handlers:
+- url: /gae_shell/static
+  static_dir: gae_shell/static
+  expiration: 1d
+
+- url: /gae_shell/.*
+  script: /gae_shell/shell.py
+  login: admin
+
+- url: .*
+  script: main.py
diff --git a/scripts/app_engine_server/gae_shell/README b/scripts/app_engine_server/gae_shell/README
new file mode 100644
index 0000000..5b0089f
--- /dev/null
+++ b/scripts/app_engine_server/gae_shell/README
@@ -0,0 +1,17 @@
+An interactive, stateful AJAX shell that runs Python code on the server.
+
+Part of http://code.google.com/p/google-app-engine-samples/.
+
+May be run as a standalone app or in an existing app as an admin-only handler.
+Can be used for system administration tasks, as an interactive way to try out
+APIs, or as a debugging aid during development.
+
+The logging, os, sys, db, and users modules are imported automatically.
+
+Interpreter state is stored in the datastore so that variables, function
+definitions, and other values in the global and local namespaces can be used
+across commands.
+
+To use the shell in your app, copy shell.py, static/*, and templates/* into
+your app's source directory. Then, copy the URL handlers from app.yaml into
+your app.yaml.
diff --git a/scripts/app_engine_server/gae_shell/__init__.py b/scripts/app_engine_server/gae_shell/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/scripts/app_engine_server/gae_shell/__init__.py
diff --git a/scripts/app_engine_server/gae_shell/shell.py b/scripts/app_engine_server/gae_shell/shell.py
new file mode 100755
index 0000000..df2fb17
--- /dev/null
+++ b/scripts/app_engine_server/gae_shell/shell.py
@@ -0,0 +1,308 @@
+#!/usr/bin/python
+#
+# Copyright 2007 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+An interactive, stateful AJAX shell that runs Python code on the server.
+
+Part of http://code.google.com/p/google-app-engine-samples/.
+
+May be run as a standalone app or in an existing app as an admin-only handler.
+Can be used for system administration tasks, as an interactive way to try out
+APIs, or as a debugging aid during development.
+
+The logging, os, sys, db, and users modules are imported automatically.
+
+Interpreter state is stored in the datastore so that variables, function
+definitions, and other values in the global and local namespaces can be used
+across commands.
+
+To use the shell in your app, copy shell.py, static/*, and templates/* into
+your app's source directory. Then, copy the URL handlers from app.yaml into
+your app.yaml.
+
+TODO: unit tests!
+"""
+
+import logging
+import new
+import os
+import pickle
+import sys
+import traceback
+import types
+import wsgiref.handlers
+
+from google.appengine.api import users
+from google.appengine.ext import db
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp import template
+
+
+# Set to True if stack traces should be shown in the browser, etc.
+_DEBUG = True
+
+# The entity kind for shell sessions. Feel free to rename to suit your app.
+_SESSION_KIND = '_Shell_Session'
+
+# Types that can't be pickled.
+UNPICKLABLE_TYPES = (
+  types.ModuleType,
+  types.TypeType,
+  types.ClassType,
+  types.FunctionType,
+  )
+
+# Unpicklable statements to seed new sessions with.
+INITIAL_UNPICKLABLES = [
+  'import logging',
+  'import os',
+  'import sys',
+  'from google.appengine.ext import db',
+  'from google.appengine.api import users',
+  ]
+
+
+class Session(db.Model):
+  """A shell session. Stores the session's globals.
+
+  Each session globals is stored in one of two places:
+
+  If the global is picklable, it's stored in the parallel globals and
+  global_names list properties. (They're parallel lists to work around the
+  unfortunate fact that the datastore can't store dictionaries natively.)
+
+  If the global is not picklable (e.g. modules, classes, and functions), or if
+  it was created by the same statement that created an unpicklable global,
+  it's not stored directly. Instead, the statement is stored in the
+  unpicklables list property. On each request, before executing the current
+  statement, the unpicklable statements are evaluated to recreate the
+  unpicklable globals.
+
+  The unpicklable_names property stores all of the names of globals that were
+  added by unpicklable statements. When we pickle and store the globals after
+  executing a statement, we skip the ones in unpicklable_names.
+
+  Using Text instead of string is an optimization. We don't query on any of
+  these properties, so they don't need to be indexed.
+  """
+  global_names = db.ListProperty(db.Text)
+  globals = db.ListProperty(db.Blob)
+  unpicklable_names = db.ListProperty(db.Text)
+  unpicklables = db.ListProperty(db.Text)
+
+  def set_global(self, name, value):
+    """Adds a global, or updates it if it already exists.
+
+    Also removes the global from the list of unpicklable names.
+
+    Args:
+      name: the name of the global to remove
+      value: any picklable value
+    """
+    blob = db.Blob(pickle.dumps(value))
+
+    if name in self.global_names:
+      index = self.global_names.index(name)
+      self.globals[index] = blob
+    else:
+      self.global_names.append(db.Text(name))
+      self.globals.append(blob)
+
+    self.remove_unpicklable_name(name)
+
+  def remove_global(self, name):
+    """Removes a global, if it exists.
+
+    Args:
+      name: string, the name of the global to remove
+    """
+    if name in self.global_names:
+      index = self.global_names.index(name)
+      del self.global_names[index]
+      del self.globals[index]
+
+  def globals_dict(self):
+    """Returns a dictionary view of the globals.
+    """
+    return dict((name, pickle.loads(val))
+                for name, val in zip(self.global_names, self.globals))
+
+  def add_unpicklable(self, statement, names):
+    """Adds a statement and list of names to the unpicklables.
+
+    Also removes the names from the globals.
+
+    Args:
+      statement: string, the statement that created new unpicklable global(s).
+      names: list of strings; the names of the globals created by the statement.
+    """
+    self.unpicklables.append(db.Text(statement))
+
+    for name in names:
+      self.remove_global(name)
+      if name not in self.unpicklable_names:
+        self.unpicklable_names.append(db.Text(name))
+
+  def remove_unpicklable_name(self, name):
+    """Removes a name from the list of unpicklable names, if it exists.
+
+    Args:
+      name: string, the name of the unpicklable global to remove
+    """
+    if name in self.unpicklable_names:
+      self.unpicklable_names.remove(name)
+
+
+class FrontPageHandler(webapp.RequestHandler):
+  """Creates a new session and renders the shell.html template.
+  """
+
+  def get(self):
+    # set up the session. TODO: garbage collect old shell sessions
+    session_key = self.request.get('session')
+    if session_key:
+      session = Session.get(session_key)
+    else:
+      # create a new session
+      session = Session()
+      session.unpicklables = [db.Text(line) for line in INITIAL_UNPICKLABLES]
+      session_key = session.put()
+
+    template_file = os.path.join(os.path.dirname(__file__), 'templates',
+                                 'shell.html')
+    session_url = '/?session=%s' % session_key
+    vars = { 'server_software': os.environ['SERVER_SOFTWARE'],
+             'python_version': sys.version,
+             'session': str(session_key),
+             'user': users.get_current_user(),
+             'login_url': users.create_login_url(session_url),
+             'logout_url': users.create_logout_url(session_url),
+             }
+    rendered = webapp.template.render(template_file, vars, debug=_DEBUG)
+    self.response.out.write(rendered)
+
+
+class StatementHandler(webapp.RequestHandler):
+  """Evaluates a python statement in a given session and returns the result.
+  """
+
+  def get(self):
+    self.response.headers['Content-Type'] = 'text/plain'
+
+    # extract the statement to be run
+    statement = self.request.get('statement')
+    if not statement:
+      return
+
+    # the python compiler doesn't like network line endings
+    statement = statement.replace('\r\n', '\n')
+
+    # add a couple newlines at the end of the statement. this makes
+    # single-line expressions such as 'class Foo: pass' evaluate happily.
+    statement += '\n\n'
+
+    # log and compile the statement up front
+    try:
+      logging.info('Compiling and evaluating:\n%s' % statement)
+      compiled = compile(statement, '<string>', 'single')
+    except:
+      self.response.out.write(traceback.format_exc())
+      return
+
+    # create a dedicated module to be used as this statement's __main__
+    statement_module = new.module('__main__')
+
+    # use this request's __builtin__, since it changes on each request.
+    # this is needed for import statements, among other things.
+    import __builtin__
+    statement_module.__builtins__ = __builtin__
+
+    # load the session from the datastore
+    session = Session.get(self.request.get('session'))
+
+    # swap in our custom module for __main__. then unpickle the session
+    # globals, run the statement, and re-pickle the session globals, all
+    # inside it.
+    old_main = sys.modules.get('__main__')
+    try:
+      sys.modules['__main__'] = statement_module
+      statement_module.__name__ = '__main__'
+
+      # re-evaluate the unpicklables
+      for code in session.unpicklables:
+        exec code in statement_module.__dict__
+
+      # re-initialize the globals
+      for name, val in session.globals_dict().items():
+        try:
+          statement_module.__dict__[name] = val
+        except:
+          msg = 'Dropping %s since it could not be unpickled.\n' % name
+          self.response.out.write(msg)
+          logging.warning(msg + traceback.format_exc())
+          session.remove_global(name)
+
+      # run!
+      old_globals = dict(statement_module.__dict__)
+      try:
+        old_stdout = sys.stdout
+        old_stderr = sys.stderr
+        try:
+          sys.stdout = self.response.out
+          sys.stderr = self.response.out
+          exec compiled in statement_module.__dict__
+        finally:
+          sys.stdout = old_stdout
+          sys.stderr = old_stderr
+      except:
+        self.response.out.write(traceback.format_exc())
+        return
+
+      # extract the new globals that this statement added
+      new_globals = {}
+      for name, val in statement_module.__dict__.items():
+        if name not in old_globals or val != old_globals[name]:
+          new_globals[name] = val
+
+      if True in [isinstance(val, UNPICKLABLE_TYPES)
+                  for val in new_globals.values()]:
+        # this statement added an unpicklable global. store the statement and
+        # the names of all of the globals it added in the unpicklables.
+        session.add_unpicklable(statement, new_globals.keys())
+        logging.debug('Storing this statement as an unpicklable.')
+
+      else:
+        # this statement didn't add any unpicklables. pickle and store the
+        # new globals back into the datastore.
+        for name, val in new_globals.items():
+          if not name.startswith('__'):
+            session.set_global(name, val)
+
+    finally:
+      sys.modules['__main__'] = old_main
+
+    session.put()
+
+
+def main():
+  application = webapp.WSGIApplication(
+    [('/gae_shell/', FrontPageHandler),
+     ('/gae_shell/shell.do', StatementHandler)], debug=_DEBUG)
+  wsgiref.handlers.CGIHandler().run(application)
+
+
+if __name__ == '__main__':
+  main()
diff --git a/scripts/app_engine_server/gae_shell/static/shell.js b/scripts/app_engine_server/gae_shell/static/shell.js
new file mode 100644
index 0000000..4aa1583
--- /dev/null
+++ b/scripts/app_engine_server/gae_shell/static/shell.js
@@ -0,0 +1,195 @@
+// Copyright 2007 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview
+ * Javascript code for the interactive AJAX shell.
+ *
+ * Part of http://code.google.com/p/google-app-engine-samples/.
+ *
+ * Includes a function (shell.runStatement) that sends the current python
+ * statement in the shell prompt text box to the server, and a callback
+ * (shell.done) that displays the results when the XmlHttpRequest returns.
+ *
+ * Also includes cross-browser code (shell.getXmlHttpRequest) to get an
+ * XmlHttpRequest.
+ */
+
+/**
+ * Shell namespace.
+ * @type {Object}
+ */
+var shell = {}
+
+/**
+ * The shell history. history is an array of strings, ordered oldest to
+ * newest. historyCursor is the current history element that the user is on.
+ *
+ * The last history element is the statement that the user is currently
+ * typing. When a statement is run, it's frozen in the history, a new history
+ * element is added to the end of the array for the new statement, and
+ * historyCursor is updated to point to the new element.
+ *
+ * @type {Array}
+ */
+shell.history = [''];
+
+/**
+ * See {shell.history}
+ * @type {number}
+ */
+shell.historyCursor = 0;
+
+/**
+ * A constant for the XmlHttpRequest 'done' state.
+ * @type Number
+ */
+shell.DONE_STATE = 4;
+
+/**
+ * A cross-browser function to get an XmlHttpRequest object.
+ *
+ * @return {XmlHttpRequest?} a new XmlHttpRequest
+ */
+shell.getXmlHttpRequest = function() {
+  if (window.XMLHttpRequest) {
+    return new XMLHttpRequest();
+  } else if (window.ActiveXObject) {
+    try {
+      return new ActiveXObject('Msxml2.XMLHTTP');
+    } catch(e) {
+      return new ActiveXObject('Microsoft.XMLHTTP');
+    }
+  }
+
+  return null;
+};
+
+/**
+ * This is the prompt textarea's onkeypress handler. Depending on the key that
+ * was pressed, it will run the statement, navigate the history, or update the
+ * current statement in the history.
+ *
+ * @param {Event} event the keypress event
+ * @return {Boolean} false to tell the browser not to submit the form.
+ */
+shell.onPromptKeyPress = function(event) {
+  var statement = document.getElementById('statement');
+
+  if (this.historyCursor == this.history.length - 1) {
+    // we're on the current statement. update it in the history before doing
+    // anything.
+    this.history[this.historyCursor] = statement.value;
+  }
+
+  // should we pull something from the history?
+  if (event.ctrlKey && event.keyCode == 38 /* up arrow */) {
+    if (this.historyCursor > 0) {
+      statement.value = this.history[--this.historyCursor];
+    }
+    return false;
+  } else if (event.ctrlKey && event.keyCode == 40 /* down arrow */) {
+    if (this.historyCursor < this.history.length - 1) {
+      statement.value = this.history[++this.historyCursor];
+    }
+    return false;
+  } else if (!event.altKey) {
+    // probably changing the statement. update it in the history.
+    this.historyCursor = this.history.length - 1;
+    this.history[this.historyCursor] = statement.value;
+  }
+
+  // should we submit?
+  var ctrlEnter = (document.getElementById('submit_key').value == 'ctrl-enter');
+  if (event.keyCode == 13 /* enter */ && !event.altKey && !event.shiftKey &&
+      event.ctrlKey == ctrlEnter) {
+    return this.runStatement();
+  }
+};
+
+/**
+ * The XmlHttpRequest callback. If the request succeeds, it adds the command
+ * and its resulting output to the shell history div.
+ *
+ * @param {XmlHttpRequest} req the XmlHttpRequest we used to send the current
+ *     statement to the server
+ */
+shell.done = function(req) {
+  if (req.readyState == this.DONE_STATE) {
+    var statement = document.getElementById('statement')
+    statement.className = 'prompt';
+
+    // add the command to the shell output
+    var output = document.getElementById('output');
+
+    output.value += '\n>>> ' + statement.value;
+    statement.value = '';
+
+    // add a new history element
+    this.history.push('');
+    this.historyCursor = this.history.length - 1;
+
+    // add the command's result
+    var result = req.responseText.replace(/^\s*|\s*$/g, '');  // trim whitespace
+    if (result != '')
+      output.value += '\n' + result;
+
+    // scroll to the bottom
+    output.scrollTop = output.scrollHeight;
+    if (output.createTextRange) {
+      var range = output.createTextRange();
+      range.collapse(false);
+      range.select();
+    }
+  }
+};
+
+/**
+ * This is the form's onsubmit handler. It sends the python statement to the
+ * server, and registers shell.done() as the callback to run when it returns.
+ *
+ * @return {Boolean} false to tell the browser not to submit the form.
+ */
+shell.runStatement = function() {
+  var form = document.getElementById('form');
+
+  // build a XmlHttpRequest
+  var req = this.getXmlHttpRequest();
+  if (!req) {
+    document.getElementById('ajax-status').innerHTML =
+        "<span class='error'>Your browser doesn't support AJAX. :(</span>";
+    return false;
+  }
+
+  req.onreadystatechange = function() { shell.done(req); };
+
+  // build the query parameter string
+  var params = '';
+  for (i = 0; i < form.elements.length; i++) {
+    var elem = form.elements[i];
+    if (elem.type != 'submit' && elem.type != 'button' && elem.id != 'caret') {
+      var value = escape(elem.value).replace(/\+/g, '%2B'); // escape ignores +
+      params += '&' + elem.name + '=' + value;
+    }
+  }
+
+  // send the request and tell the user.
+  document.getElementById('statement').className = 'prompt processing';
+  req.open(form.method, form.action + '?' + params, true);
+  req.setRequestHeader('Content-type',
+                       'application/x-www-form-urlencoded;charset=UTF-8');
+  req.send(null);
+
+  return false;
+};
diff --git a/scripts/app_engine_server/gae_shell/static/spinner.gif b/scripts/app_engine_server/gae_shell/static/spinner.gif
new file mode 100644
index 0000000..3e58d6e
--- /dev/null
+++ b/scripts/app_engine_server/gae_shell/static/spinner.gif
Binary files differ
diff --git a/scripts/app_engine_server/gae_shell/templates/shell.html b/scripts/app_engine_server/gae_shell/templates/shell.html
new file mode 100644
index 0000000..123b200
--- /dev/null
+++ b/scripts/app_engine_server/gae_shell/templates/shell.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<head>
+<meta http-equiv="content-type" content="text/html; charset=utf-8" />
+<title> Interactive Shell </title>
+<script type="text/javascript" src="/gae_shell/static/shell.js"></script>
+<style type="text/css">
+body {
+  font-family: monospace;
+  font-size: 10pt;
+}
+
+p {
+  margin: 0.5em;
+}
+
+.prompt, #output {
+  width: 45em;
+  border: 1px solid silver;
+  background-color: #f5f5f5;
+  font-size: 10pt;
+  margin: 0.5em;
+  padding: 0.5em;
+  padding-right: 0em;
+  overflow-x: hidden;
+}
+
+#toolbar {
+  margin-left: 0.5em;
+  padding-left: 0.5em;
+}
+
+#caret {
+  width: 2.5em;
+  margin-right: 0px;
+  padding-right: 0px;
+  border-right: 0px;
+}
+
+#statement {
+  width: 43em;
+  margin-left: -1em;
+  padding-left: 0px;
+  border-left: 0px;
+  background-position: top right;
+  background-repeat: no-repeat;
+}
+
+.processing {
+  background-image: url("/gae_shell/static/spinner.gif");
+}
+
+#ajax-status {
+  font-weight: bold;
+}
+
+.message {
+  color: #8AD;
+  font-weight: bold;
+  font-style: italic;
+}
+
+.error {
+  color: #F44;
+}
+
+.username {
+  font-weight: bold;
+}
+
+</style>
+</head>
+
+<body>
+
+<p> Interactive server-side Python shell for
+<a href="http://code.google.com/appengine/">Google App Engine</a>.
+(<a href="http://code.google.com/p/google-app-engine-samples/">source</a>)
+</p>
+
+<textarea id="output" rows="22" readonly="readonly">
+{{ server_software }}
+Python {{ python_version }}
+</textarea>
+
+<form id="form" action="shell.do" method="get">
+  <nobr>
+  <textarea class="prompt" id="caret" readonly="readonly" rows="4"
+            onfocus="document.getElementById('statement').focus()"
+            >&gt;&gt;&gt;</textarea>
+  <textarea class="prompt" name="statement" id="statement" rows="4"
+            onkeypress="return shell.onPromptKeyPress(event);"></textarea>
+  </nobr>
+  <input type="hidden" name="session" value="{{ session }}" />
+  <input type="submit" style="display: none" />
+</form>
+
+<p id="ajax-status"></p>
+
+<p id="toolbar">
+{% if user %}
+  <span class="username">{{ user.nickname }}</span>
+  (<a href="{{ logout_url }}">log out</a>)
+{% else %}
+  <a href="{{ login_url }}">log in</a>
+{% endif %}
+ | Ctrl-Up/Down for history |
+<select id="submit_key">
+  <option value="enter">Enter</option>
+  <option value="ctrl-enter" selected="selected">Ctrl-Enter</option>
+</select>
+<label for="submit_key">submits</label>
+</p>
+
+<script type="text/javascript">
+document.getElementById('statement').focus();
+</script>
+
+</body>
+</html>
+
diff --git a/scripts/app_engine_server/index.yaml b/scripts/app_engine_server/index.yaml
new file mode 100644
index 0000000..8e6046d
--- /dev/null
+++ b/scripts/app_engine_server/index.yaml
@@ -0,0 +1,12 @@
+indexes:
+
+# AUTOGENERATED
+
+# This index.yaml is automatically updated whenever the dev_appserver
+# detects that a new type of query is run.  If you want to manage the
+# index.yaml file manually, remove the above marker line (the line
+# saying "# AUTOGENERATED").  If you want to manage some indexes
+# manually, move them above the marker line.  The index.yaml file is
+# automatically uploaded to the admin console when you next deploy
+# your application using appcfg.py.
+
diff --git a/scripts/app_engine_server/memcache_zipserve.py b/scripts/app_engine_server/memcache_zipserve.py
new file mode 100644
index 0000000..43826b0
--- /dev/null
+++ b/scripts/app_engine_server/memcache_zipserve.py
@@ -0,0 +1,573 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""A class to serve pages from zip files and use memcache for performance.
+
+This contains a class and a function to create an anonymous instance of the
+class to serve HTTP GET requests. Memcache is used to increase response speed
+and lower processing cycles used in serving. Credit to Guido van Rossum and
+his implementation of zipserve which served as a reference as I wrote this.
+
+  MemcachedZipHandler: Class that serves request
+  create_handler: method to create instance of MemcachedZipHandler
+"""
+
+__author__ = 'jmatt@google.com (Justin Mattson)'
+
+import email.Utils
+import logging
+import mimetypes
+import time
+import zipfile
+
+from google.appengine.api import memcache
+from google.appengine.ext import webapp
+from google.appengine.ext.webapp import util
+from time import localtime, strftime
+
+def create_handler(zip_files, max_age=None, public=None):
+  """Factory method to create a MemcachedZipHandler instance.
+
+  Args:
+    zip_files: A list of file names, or a list of lists of file name, first
+        member of file mappings. See MemcachedZipHandler documentation for
+        more information about using the list of lists format
+    max_age: The maximum client-side cache lifetime
+    public: Whether this should be declared public in the client-side cache
+  Returns:
+    A MemcachedZipHandler wrapped in a pretty, anonymous bow for use with App
+    Engine
+
+  Raises:
+    ValueError: if the zip_files argument is not a list
+  """
+  # verify argument integrity. If the argument is passed in list format,
+  # convert it to list of lists format
+  if zip_files and type(zip_files).__name__ == 'list':
+    num_items = len(zip_files)
+    while num_items > 0:
+      if type(zip_files[num_items - 1]).__name__ != 'list':
+        zip_files[num_items - 1] = [zip_files[num_items-1]]
+      num_items -= 1
+  else:
+    raise ValueError('File name arguments must be a list')
+
+  class HandlerWrapper(MemcachedZipHandler):
+    """Simple wrapper for an instance of MemcachedZipHandler.
+
+    I'm still not sure why this is needed
+    """
+    def get(self, name):
+      self.zipfilenames = zip_files
+      self.TrueGet(name)
+      if max_age is not None:
+        MAX_AGE = max_age
+      if public is not None:
+        PUBLIC = public
+
+  return HandlerWrapper
+
+
+class MemcachedZipHandler(webapp.RequestHandler):
+  """Handles get requests for a given URL.
+
+  Serves a GET request from a series of zip files. As files are served they are
+  put into memcache, which is much faster than retreiving them from the zip
+  source file again. It also uses considerably fewer CPU cycles.
+  """
+  zipfile_cache = {}                # class cache of source zip files
+  MAX_AGE = 600                     # max client-side cache lifetime
+  PUBLIC = True                     # public cache setting
+  CACHE_PREFIX = 'cache://'         # memcache key prefix for actual URLs
+  NEG_CACHE_PREFIX = 'noncache://'  # memcache key prefix for non-existant URL
+  intlString = 'intl/'
+  validLangs = ['en', 'de', 'es', 'fr','it','ja','zh-CN','zh-TW']
+  
+  def TrueGet(self, reqUri):
+    """The top-level entry point to serving requests.
+
+    Called 'True' get because it does the work when called from the wrapper
+    class' get method. Some logic is applied to the request to serve files
+    from an intl/<lang>/... directory or fall through to the default language.
+
+    Args:
+      name: URL requested
+
+    Returns:
+      None
+    """
+    langName = 'en'
+    resetLangCookie = False
+    urlLangName = None
+    retry = False
+    isValidIntl = False
+
+    # Try to retrieve the user's lang pref from the cookie. If there is no
+    # lang pref cookie in the request, add set-cookie to the response with the 
+    # default value of 'en'.
+    try:
+      langName = self.request.cookies['android_developer_pref_lang']
+    except KeyError:
+      resetLangCookie = True
+      #logging.info('==========================EXCEPTION: NO LANG COOKIE FOUND, USING [%s]', langName)
+    logging.info('==========================REQ INIT name [%s] langName [%s]', reqUri, langName)
+
+    # Preprocess the req url. If it references a directory or the domain itself,
+    # append '/index.html' to the url and 302 redirect. Otherwise, continue
+    # processing the request below.
+    name = self.PreprocessUrl(reqUri, langName)
+    if name:
+      # Do some prep for handling intl requests. Parse the url and validate
+      # the intl/lang substring, extract the url lang code (urlLangName) and the
+      # the uri that follows the intl/lang substring(contentUri)
+      sections = name.split("/", 2)
+      contentUri = 0
+      isIntl = len(sections) > 1 and (sections[0] == "intl")
+      if isIntl:
+        isValidIntl = sections[1] in self.validLangs
+        if isValidIntl:
+          urlLangName = sections[1]
+          contentUri = sections[2]
+          if (langName != urlLangName):
+            # if the lang code in the request is different from that in 
+            # the cookie, reset the cookie to the url lang value.
+            langName = urlLangName
+            resetLangCookie = True
+            #logging.info('INTL PREP resetting langName to urlLangName [%s]', langName)
+          #else: 
+          #  logging.info('INTL PREP no need to reset langName')
+
+      # Send for processing
+      if self.isCleanUrl(name, langName, isValidIntl):
+        # handle a 'clean' request.
+        # Try to form a response using the actual request url.
+        if not self.CreateResponse(name, langName, isValidIntl, resetLangCookie):
+          # If CreateResponse returns False, there was no such document
+          # in the intl/lang tree. Before going to 404, see if there is an
+          # English-language version of the doc in the default
+          # default tree and return it, else go to 404.
+          self.CreateResponse(contentUri, langName, False, resetLangCookie)
+
+      elif isIntl:
+        # handle the case where we need to pass through an invalid intl req 
+        # for processing (so as to get 404 as appropriate). This is needed
+        # because intl urls are passed through clean and retried in English,
+        # if necessary.
+        logging.info('  Handling an invalid intl request...')
+        self.CreateResponse(name, langName, isValidIntl, resetLangCookie)
+
+      else:
+        # handle the case where we have a non-clean url (usually a non-intl
+        # url) that we need to interpret in the context of any lang pref
+        # that is set. Prepend an intl/lang string to the request url and
+        # send it as a 302 redirect. After the redirect, the subsequent
+        # request will be handled as a clean url.
+        self.RedirToIntl(name, self.intlString, langName)
+
+  def isCleanUrl(self, name, langName, isValidIntl):
+    """Determine whether to pass an incoming url straight to processing. 
+
+       Args:
+         name: The incoming URL
+
+       Returns:
+         boolean: Whether the URL should be sent straight to processing
+    """
+    if (langName == 'en') or isValidIntl or not ('.html' in name) or (not isValidIntl and not langName):
+      return True
+
+  def PreprocessUrl(self, name, langName):
+    """Any preprocessing work on the URL when it comes in.
+
+    Put any work related to interpretting the incoming URL here. For example,
+    this is used to redirect requests for a directory to the index.html file
+    in that directory. Subclasses should override this method to do different
+    preprocessing.
+
+    Args:
+      name: The incoming URL
+
+    Returns:
+      False if the request was redirected to '/index.html', or
+      The processed URL, otherwise
+    """
+    # determine if this is a request for a directory
+    final_path_segment = name
+    final_slash_offset = name.rfind('/')
+    if final_slash_offset != len(name) - 1:
+      final_path_segment = name[final_slash_offset + 1:]
+      if final_path_segment.find('.') == -1:
+        name = ''.join([name, '/'])
+
+    # if this is a directory or the domain itself, redirect to /index.html
+    if not name or (name[len(name) - 1:] == '/'):
+      uri = ''.join(['/', name, 'index.html'])
+      logging.info('--->PREPROCESSING REDIRECT [%s] to [%s] with langName [%s]', name, uri, langName)
+      self.redirect(uri, False)
+      return False
+    else:
+      return name
+
+  def RedirToIntl(self, name, intlString, langName):
+    """Redirect an incoming request to the appropriate intl uri.
+
+       Builds the intl/lang string from a base (en) string
+       and redirects (302) the request to look for a version 
+       of the file in the language that matches the client-
+       supplied cookie value.
+
+    Args:
+      name: The incoming, preprocessed URL
+
+    Returns:
+      The lang-specific URL
+    """
+    builtIntlLangUri = ''.join([intlString, langName, '/', name, '?', self.request.query_string])
+    uri = ''.join(['/', builtIntlLangUri])
+    logging.info('-->>REDIRECTING %s to  %s', name, uri)
+    self.redirect(uri, False)
+    return uri
+
+  def CreateResponse(self, name, langName, isValidIntl, resetLangCookie):
+    """Process the url and form a response, if appropriate.
+
+       Attempts to retrieve the requested file (name) from cache, 
+       negative cache, or store (zip) and form the response. 
+       For intl requests that are not found (in the localized tree), 
+       returns False rather than forming a response, so that
+       the request can be retried with the base url (this is the 
+       fallthrough to default language). 
+
+       For requests that are found, forms the headers and
+       adds the content to the response entity. If the request was
+       for an intl (localized) url, also resets the language cookie 
+       to the language specified in the url if needed, to ensure that 
+       the client language and response data remain harmonious. 
+
+    Args:
+      name: The incoming, preprocessed URL
+      langName: The language id. Used as necessary to reset the
+                language cookie in the response.
+      isValidIntl: If present, indicates whether the request is
+                   for a language-specific url
+      resetLangCookie: Whether the response should reset the
+                       language cookie to 'langName'
+
+    Returns:
+      True: A response was successfully created for the request
+      False: No response was created.
+    """
+    # see if we have the page in the memcache
+    logging.info('PROCESSING %s langName [%s] isValidIntl [%s] resetLang [%s]', 
+      name, langName, isValidIntl, resetLangCookie)
+    resp_data = self.GetFromCache(name)
+    if resp_data is None:
+      logging.info('  Cache miss for %s', name)
+      resp_data = self.GetFromNegativeCache(name)
+      if resp_data is None:
+        resp_data = self.GetFromStore(name)
+
+        # IF we have the file, put it in the memcache
+        # ELSE put it in the negative cache
+        if resp_data is not None:
+          self.StoreOrUpdateInCache(name, resp_data)
+        elif isValidIntl:
+          # couldn't find the intl doc. Try to fall through to English.
+          #logging.info('  Retrying with base uri...')
+          return False
+        else:
+          logging.info('  Adding %s to negative cache, serving 404', name)
+          self.StoreInNegativeCache(name)
+          self.Write404Error()
+          return True
+      else:
+        # found it in negative cache
+        self.Write404Error()
+        return True
+
+    # found content from cache or store
+    logging.info('FOUND CLEAN')
+    if resetLangCookie:
+      logging.info('  Resetting android_developer_pref_lang cookie to [%s]',
+      langName)
+      expireDate = time.mktime(localtime()) + 60 * 60 * 24 * 365 * 10
+      self.response.headers.add_header('Set-Cookie', 
+      'android_developer_pref_lang=%s; path=/; expires=%s' % 
+      (langName, strftime("%a, %d %b %Y %H:%M:%S", localtime(expireDate))))
+    mustRevalidate = False
+    if ('.html' in name):
+      # revalidate html files -- workaround for cache inconsistencies for 
+      # negotiated responses
+      mustRevalidate = True
+      logging.info('  Adding [Vary: Cookie] to response...')
+      self.response.headers.add_header('Vary', 'Cookie')
+    content_type, encoding = mimetypes.guess_type(name)
+    if content_type:
+      self.response.headers['Content-Type'] = content_type
+      self.SetCachingHeaders(mustRevalidate)
+      self.response.out.write(resp_data)
+    elif (name == 'favicon.ico'):
+      self.response.headers['Content-Type'] = 'image/x-icon'
+      self.SetCachingHeaders(mustRevalidate)
+      self.response.out.write(resp_data)
+    elif name.endswith('.psd'):
+      self.response.headers['Content-Type'] = 'application/octet-stream'
+      self.SetCachingHeaders(mustRevalidate)
+      self.response.out.write(resp_data)
+    return True
+
+  def GetFromStore(self, file_path):
+    """Retrieve file from zip files.
+
+    Get the file from the source, it must not have been in the memcache. If
+    possible, we'll use the zip file index to quickly locate where the file
+    should be found. (See MapToFileArchive documentation for assumptions about
+    file ordering.) If we don't have an index or don't find the file where the
+    index says we should, look through all the zip files to find it.
+
+    Args:
+      file_path: the file that we're looking for
+
+    Returns:
+      The contents of the requested file
+    """
+    resp_data = None
+    file_itr = iter(self.zipfilenames)
+
+    # check the index, if we have one, to see what archive the file is in
+    archive_name = self.MapFileToArchive(file_path)
+    if not archive_name:
+      archive_name = file_itr.next()[0]
+
+    while resp_data is None and archive_name:
+      zip_archive = self.LoadZipFile(archive_name)
+      if zip_archive:
+
+        # we expect some lookups will fail, and that's okay, 404s will deal
+        # with that
+        try:
+          resp_data = zip_archive.read(file_path)
+        except (KeyError, RuntimeError), err:
+          # no op
+          x = False
+        if resp_data is not None:
+          logging.info('%s read from %s', file_path, archive_name)
+          
+      try:
+        archive_name = file_itr.next()[0]
+      except (StopIteration), err:
+        archive_name = False
+
+    return resp_data
+
+  def LoadZipFile(self, zipfilename):
+    """Convenience method to load zip file.
+
+    Just a convenience method to load the zip file from the data store. This is
+    useful if we ever want to change data stores and also as a means of
+    dependency injection for testing. This method will look at our file cache
+    first, and then load and cache the file if there's a cache miss
+
+    Args:
+      zipfilename: the name of the zip file to load
+
+    Returns:
+      The zip file requested, or None if there is an I/O error
+    """
+    zip_archive = None
+    zip_archive = self.zipfile_cache.get(zipfilename)
+    if zip_archive is None:
+      try:
+        zip_archive = zipfile.ZipFile(zipfilename)
+        self.zipfile_cache[zipfilename] = zip_archive
+      except (IOError, RuntimeError), err:
+        logging.error('Can\'t open zipfile %s, cause: %s' % (zipfilename,
+                                                             err))
+    return zip_archive
+
+  def MapFileToArchive(self, file_path):
+    """Given a file name, determine what archive it should be in.
+
+    This method makes two critical assumptions.
+    (1) The zip files passed as an argument to the handler, if concatenated
+        in that same order, would result in a total ordering
+        of all the files. See (2) for ordering type.
+    (2) Upper case letters before lower case letters. The traversal of a
+        directory tree is depth first. A parent directory's files are added
+        before the files of any child directories
+
+    Args:
+      file_path: the file to be mapped to an archive
+
+    Returns:
+      The name of the archive where we expect the file to be
+    """
+    num_archives = len(self.zipfilenames)
+    while num_archives > 0:
+      target = self.zipfilenames[num_archives - 1]
+      if len(target) > 1:
+        if self.CompareFilenames(target[1], file_path) >= 0:
+          return target[0]
+      num_archives -= 1
+
+    return None
+
+  def CompareFilenames(self, file1, file2):
+    """Determines whether file1 is lexigraphically 'before' file2.
+
+    WARNING: This method assumes that paths are output in a depth-first,
+    with parent directories' files stored before childs'
+
+    We say that file1 is lexigraphically before file2 if the last non-matching
+    path segment of file1 is alphabetically before file2.
+    
+    Args:
+      file1: the first file path
+      file2: the second file path
+
+    Returns:
+      A positive number if file1 is before file2
+      A negative number if file2 is before file1
+      0 if filenames are the same
+    """
+    f1_segments = file1.split('/')
+    f2_segments = file2.split('/')
+
+    segment_ptr = 0
+    while (segment_ptr < len(f1_segments) and
+           segment_ptr < len(f2_segments) and
+           f1_segments[segment_ptr] == f2_segments[segment_ptr]):
+      segment_ptr += 1
+
+    if len(f1_segments) == len(f2_segments):
+
+      # we fell off the end, the paths much be the same
+      if segment_ptr == len(f1_segments):
+        return 0
+
+      # we didn't fall of the end, compare the segments where they differ
+      if f1_segments[segment_ptr] < f2_segments[segment_ptr]:
+        return 1
+      elif f1_segments[segment_ptr] > f2_segments[segment_ptr]:
+        return -1
+      else:
+        return 0
+
+      # the number of segments differs, we either mismatched comparing
+      # directories, or comparing a file to a directory
+    else:
+
+      # IF we were looking at the last segment of one of the paths,
+      # the one with fewer segments is first because files come before
+      # directories
+      # ELSE we just need to compare directory names
+      if (segment_ptr + 1 == len(f1_segments) or
+          segment_ptr + 1 == len(f2_segments)):
+        return len(f2_segments) - len(f1_segments)
+      else:
+        if f1_segments[segment_ptr] < f2_segments[segment_ptr]:
+          return 1
+        elif f1_segments[segment_ptr] > f2_segments[segment_ptr]:
+          return -1
+        else:
+          return 0
+
+  def SetCachingHeaders(self, revalidate):
+    """Set caching headers for the request."""
+    max_age = self.MAX_AGE
+    #self.response.headers['Expires'] = email.Utils.formatdate(
+    #    time.time() + max_age, usegmt=True)
+	cache_control = []
+    if self.PUBLIC:
+      cache_control.append('public')
+    cache_control.append('max-age=%d' % max_age)
+    if revalidate:
+      cache_control.append('must-revalidate')
+    self.response.headers['Cache-Control'] = ', '.join(cache_control)
+
+  def GetFromCache(self, filename):
+    """Get file from memcache, if available.
+
+    Args:
+      filename: The URL of the file to return
+
+    Returns:
+      The content of the file
+    """
+    return memcache.get('%s%s' % (self.CACHE_PREFIX, filename))
+
+  def StoreOrUpdateInCache(self, filename, data):
+    """Store data in the cache.
+
+    Store a piece of data in the memcache. Memcache has a maximum item size of
+    1*10^6 bytes. If the data is too large, fail, but log the failure. Future
+    work will consider compressing the data before storing or chunking it
+
+    Args:
+      filename: the name of the file to store
+      data: the data of the file
+
+    Returns:
+      None
+    """
+    try:
+      if not memcache.add('%s%s' % (self.CACHE_PREFIX, filename), data):
+        memcache.replace('%s%s' % (self.CACHE_PREFIX, filename), data)
+    except (ValueError), err:
+      logging.warning('Data size too large to cache\n%s' % err)
+
+  def Write404Error(self):
+    """Ouptut a simple 404 response."""
+    self.error(404)
+    self.response.out.write(
+        ''.join(['<html><head><title>404: Not Found</title></head>',
+                 '<body><b><h2>Error 404</h2><br/>',
+                 'File not found</b></body></html>']))
+
+  def StoreInNegativeCache(self, filename):
+    """If a non-existant URL is accessed, cache this result as well.
+
+    Future work should consider setting a maximum negative cache size to
+    prevent it from from negatively impacting the real cache.
+
+    Args:
+      filename: URL to add ot negative cache
+
+    Returns:
+      None
+    """
+    memcache.add('%s%s' % (self.NEG_CACHE_PREFIX, filename), -1)
+
+  def GetFromNegativeCache(self, filename):
+    """Retrieve from negative cache.
+
+    Args:
+      filename: URL to retreive
+
+    Returns:
+      The file contents if present in the negative cache.
+    """
+    return memcache.get('%s%s' % (self.NEG_CACHE_PREFIX, filename))
+
+def main():
+  application = webapp.WSGIApplication([('/([^/]+)/(.*)',
+                                         MemcachedZipHandler)])
+  util.run_wsgi_app(application)
+
+
+if __name__ == '__main__':
+  main()