Index: twitter/twitter.py
===================================================================
--- twitter/twitter.py	(revision 118)
+++ twitter/twitter.py	(working copy)
@@ -11,7 +11,7 @@
 import base64
 import md5
 import os
-import simplejson
+from django.utils import simplejson
 import sys
 import tempfile
 import time
@@ -21,6 +21,9 @@
 import urlparse
 import twitter
 
+from google.appengine.ext import db
+from google.appengine.api import urlfetch
+
 class TwitterError(Exception):
   '''Base class for Twitter errors'''
 
@@ -924,7 +927,7 @@
       input_encoding: The encoding used to encode input strings. [optional]
       request_header: A dictionary of additional HTTP request headers. [optional]
     '''
-    self._cache = _FileCache()
+    self._cache = _DbCache()
     self._urllib = urllib2
     self._cache_timeout = Api.DEFAULT_CACHE_TIMEOUT
     self._InitializeRequestHeaders(request_headers)
@@ -1414,18 +1417,6 @@
     if self._request_headers and 'Authorization' in self._request_headers:
       del self._request_headers['Authorization']
 
-  def _GetOpener(self, url, username=None, password=None):
-    if username and password:
-      self._AddAuthorizationHeader(username, password)
-      handler = self._urllib.HTTPBasicAuthHandler()
-      (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url)
-      handler.add_password(Api._API_REALM, netloc, username, password)
-      opener = self._urllib.build_opener(handler)
-    else:
-      opener = self._urllib.build_opener()
-    opener.addheaders = self._request_headers.items()
-    return opener
-
   def _Encode(self, s):
     if self._input_encoding:
       return unicode(s, self._input_encoding).encode('utf-8')
@@ -1496,14 +1487,12 @@
     # Add key/value parameters to the query string of the url
     url = self._BuildUrl(url, extra_params=extra_params)
 
-    # Get a url opener that can handle basic auth
-    opener = self._GetOpener(url, username=self._username, password=self._password)
-
     encoded_post_data = self._EncodePostData(post_data)
-
-    # Open and return the URL immediately if we're not going to cache
+    
+    # We can't cache in some circumstances
     if encoded_post_data or no_cache or not self._cache or not self._cache_timeout:
-      url_data = opener.open(url, encoded_post_data).read()
+      should_fetch = True
+      should_cache = False
     else:
       # Unique keys are a combination of the url and the username
       if self._username:
@@ -1514,12 +1503,24 @@
       # See if it has been cached before
       last_cached = self._cache.GetCachedTime(key)
 
-      # If the cached version is outdated then fetch another and store it
-      if not last_cached or time.time() >= last_cached + self._cache_timeout:
-        url_data = opener.open(url, encoded_post_data).read()
+      # If the cached version is outdated
+      should_fetch = not last_cached or time.time() >= last_cached + self._cache_timeout
+      should_cache = True
+
+    if should_fetch:
+      if self._username and self._password:
+        self._AddAuthorizationHeader(self._username, self._password)
+      result = urlfetch.fetch(
+          url,
+          payload=encoded_post_data,
+          method=post_data and urlfetch.POST or urlfetch.GET,
+          headers=self._request_headers)
+      
+      url_data = result.content
+      if should_cache and result.status_code == 200:
         self._cache.Set(key, url_data)
-      else:
-        url_data = self._cache.Get(key)
+    else:
+      url_data = self._cache.Get(key)
 
     # Always return the latest version
     return url_data
@@ -1609,3 +1610,44 @@
 
   def _GetPrefix(self,hashed_key):
     return os.path.sep.join(hashed_key[0:_FileCache.DEPTH])
+
+class _DbCacheEntry(db.Model):
+  value = db.TextProperty(required=True)
+  timestamp = db.DateTimeProperty(required=True, auto_now=True)
+
+class _DbCache(object):
+  '''Simple cache on top of Google App engine's datastore'''
+  def Get(self, key):
+    entry = _DbCacheEntry.get_by_key_name(key)
+    if entry:
+      return entry.value
+    else:
+      return None
+  
+  def Set(self, key, data):
+    entry = _DbCacheEntry.get_by_key_name(key)
+    if not entry:
+      entry = _DbCacheEntry(
+          key_name = key,
+          value = data)
+    else:
+      entry.value = data
+    entry.put()
+    
+  def GetCachedTime(self, key):
+    entry = _DbCacheEntry.get_by_key_name(key)
+    if entry:
+      try:
+        # All cached data must be valid JSON, and if we mistakenly cache
+        # error response, we should ignore them
+        data = simplejson.loads(entry.value)
+        if isinstance(data, dict) and data.has_key('error'):
+          return None
+      except:
+        return None
+    
+      return time.mktime(entry.timestamp.timetuple())
+    else:
+      return None
+
+    return None
