← Back to team overview

gtg-user team mailing list archive

[Merge] lp:~gtg-user/gtg/identica-backend into lp:gtg

 

Luca Invernizzi has proposed merging lp:~gtg-user/gtg/identica-backend into lp:gtg.

Requested reviews:
  Gtg developers (gtg)


identi.ca backend, which follows the lines of the twitter backend, but does not authenticate through oauth (and uses a different library).

Note: i've added a branch with all my merge requests merged. https://code.edge.launchpad.net/~gtg-user/gtg/all_the_backends_merge_requests/
-- 
https://code.launchpad.net/~gtg-user/gtg/identica-backend/+merge/33479
Your team Gtg users is subscribed to branch lp:~gtg-user/gtg/identica-backend.
=== added file 'GTG/backends/backend_identica.py'
--- GTG/backends/backend_identica.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/backend_identica.py	2010-08-24 01:51:40 +0000
@@ -0,0 +1,282 @@
+# -*- coding: utf-8 -*-
+# -----------------------------------------------------------------------------
+# Getting Things Gnome! - a personal organizer for the GNOME desktop
+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program.  If not, see <http://www.gnu.org/licenses/>.
+# -----------------------------------------------------------------------------
+
+'''
+Identi.ca backend: imports direct messages, replies and/or the user timeline.
+'''
+
+import os
+import re
+import sys
+import uuid
+import urllib2
+
+from GTG                                import _
+from GTG.backends.genericbackend        import GenericBackend
+from GTG.core                           import CoreConfig
+from GTG.backends.backendsignals        import BackendSignals
+from GTG.backends.periodicimportbackend import PeriodicImportBackend
+from GTG.backends.syncengine            import SyncEngine
+from GTG.tools.logger                   import Log
+
+#The Ubuntu version of python twitter is not updated: 
+# it does not have identi.ca support. Meanwhile, we ship the right version
+# with our code.
+import GTG.backends.twitter as twitter
+
+
+
+class Backend(PeriodicImportBackend):
+    '''
+    Identi.ca backend: imports direct messages, replies and/or the user
+    timeline.
+    '''
+
+
+    _general_description = { \
+        GenericBackend.BACKEND_NAME: "backend_identica", \
+        GenericBackend.BACKEND_HUMAN_NAME: _("Identi.ca"), \
+        GenericBackend.BACKEND_AUTHORS:    ["Luca Invernizzi"], \
+        GenericBackend.BACKEND_TYPE:       GenericBackend.TYPE_IMPORT, \
+        GenericBackend.BACKEND_DESCRIPTION: \
+            _("Imports your identi.ca  messages into your GTG " + \
+              "tasks. You can choose to either import all your " + \
+              "messages or just those with a set of hash tags. \n" + \
+              "The message will be interpreted following this" + \
+              " format: \n" + \
+              "<b>my task title, task description #tag @anothertag</b>\n" + \
+              " Tags can be  anywhere in the message"),\
+        }
+
+    base_url = "http://identi.ca/api/";
+
+    _static_parameters = { \
+        "username": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING, \
+            GenericBackend.PARAM_DEFAULT_VALUE: "", },
+        "password": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_PASSWORD, \
+            GenericBackend.PARAM_DEFAULT_VALUE: "", },
+        "period": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \
+            GenericBackend.PARAM_DEFAULT_VALUE: 2, },
+        "import-tags": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_LIST_OF_STRINGS, \
+            GenericBackend.PARAM_DEFAULT_VALUE: ["#todo"], },
+        "import-from-replies": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
+            GenericBackend.PARAM_DEFAULT_VALUE: False, },
+        "import-from-my-tweets": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
+            GenericBackend.PARAM_DEFAULT_VALUE: False, },
+        "import-from-direct-messages": { \
+            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
+            GenericBackend.PARAM_DEFAULT_VALUE: True, },
+        }
+
+    def __init__(self, parameters):
+        '''
+        See GenericBackend for an explanation of this function.
+        Re-loads the saved state of the synchronization
+        '''
+        super(Backend, self).__init__(parameters)
+        #loading the list of already imported tasks
+        self.data_path = os.path.join('backends/identica/', "tasks_dict-%s" %\
+                                     self.get_id())
+        self.sync_engine = self._load_pickled_file(self.data_path, \
+                                                        SyncEngine())
+        
+    def save_state(self):
+        '''
+        See GenericBackend for an explanation of this function.
+        Saves the state of the synchronization.
+        '''
+        self._store_pickled_file(self.data_path, self.sync_engine)
+
+    def do_periodic_import(self):
+        '''
+        See GenericBackend for an explanation of this function.
+        '''
+        #we need to authenticate only to see the direct messages or the replies
+        # (why the replies? Don't know. But python-twitter requires that)
+        # (invernizzi)
+        print "GETTING THEM"
+        with self.controlled_execution(self._parameters['username'],\
+                                       self._parameters['password'], \
+                                       self.base_url, \
+                                       self) as api:
+            #select what to import
+            tweets_to_import = []
+            if self._parameters["import-from-direct-messages"]:
+                tweets_to_import += api.GetDirectMessages()
+            if self._parameters["import-from-my-tweets"]:
+                tweets_to_import += \
+                        api.GetUserTimeline(self._parameters["username"])
+            if self._parameters["import-from-replies"]:
+                tweets_to_import += \
+                    api.GetReplies(self._parameters["username"])
+            #do the import
+            for tweet in tweets_to_import:
+                self._process_tweet(tweet)
+
+    def _process_tweet(self, tweet):
+        '''
+        Given a tweet, checks if a task representing it must be
+        created in GTG and, if so, it creates it.
+
+        @param tweet: a tweet.
+        '''
+        self.cancellation_point()
+        tweet_id = str(tweet.GetId())
+        is_syncable = self._is_tweet_syncable(tweet)
+        #the "lambda" is because we don't consider tweets deletion (to be
+        # faster)
+        action, tid = self.sync_engine.analyze_remote_id(\
+                                        tweet_id, \
+                                        self.datastore.has_task, \
+                                        lambda tweet_id: True, \
+                                        is_syncable)
+        Log.debug("processing tweet (%s, %s)" % (action, is_syncable))
+        print action, tweet.GetText()
+        
+        self.cancellation_point()
+        if action == None or action == SyncEngine.UPDATE:
+            return
+
+        elif action == SyncEngine.ADD:
+            tid = str(uuid.uuid4())
+            task = self.datastore.task_factory(tid)
+            self._populate_task(task, tweet)
+            #we care only to add tweets and if the list of tags which must be
+            #imported changes (lost-syncability can happen). Thus, we don't
+            # care about SyncMeme(s)
+            self.sync_engine.record_relationship(local_id = tid,\
+                                     remote_id = tweet_id, \
+                                     meme = None)
+            self.datastore.push_task(task)
+
+        elif action == SyncEngine.LOST_SYNCABILITY:
+            self.sync_engine.break_relationship(remote_id = tweet_id)
+            self.datastore.request_task_deletion(tid)
+
+        self.save_state()
+
+    def _populate_task(self, task, message):
+        '''
+        Given a twitter message and a GTG task, fills the task with the content
+        of the message
+        '''
+        try:
+            #this works only for some messages
+            task.add_tag("@" + message.GetSenderScreenName())
+        except:
+            pass
+        text = message.GetText()    
+        
+        #convert #hastags to @tags
+        matches = re.finditer("(?<![^|\s])(#\w+)", text)
+        for g in matches:
+            text = text[:g.start()] + '@' + text[g.start() + 1:]
+        #add tags objects (it's not enough to have @tag in the text to add a
+        # tag
+        for tag in self._extract_tags_from_text(text):
+            task.add_tag(tag)
+
+        split_text = text.split(",", 1)
+        task.set_title(split_text[0])
+        if len(split_text) > 1:
+            task.set_text(split_text[1])
+
+        task.add_remote_id(self.get_id(), str(message.GetId()))
+
+    def _is_tweet_syncable(self, tweet):
+        '''
+        Returns True if the given tweet matches the user-specified tags to be
+        synced
+
+        @param tweet: a tweet
+        '''
+        if CoreConfig.ALLTASKS_TAG in self._parameters["import-tags"]:
+            return True
+        else:
+            tags = set(Backend._extract_tags_from_text(tweet.GetText()))
+            return tags.intersection(set(self._parameters["import-tags"])) \
+                    != set()
+
+    @staticmethod
+    def _extract_tags_from_text(text):
+        '''
+        Given a string, returns a list of @tags and #hashtags
+        '''
+        return list(re.findall(r'(?:^|[\s])((?:#|@)\w+)', text))
+
+###############################################################################
+### AUTHENTICATION ############################################################
+###############################################################################
+
+    class controlled_execution(object):
+        '''
+        This class performs the login to identica and execute the appropriate
+        response if something goes wrong during authentication or at network
+        level
+        '''
+
+        def __init__(self, username, password, base_url, backend):
+            '''
+            Sets the login parameters
+            '''
+            self.username = username
+            self.password = password
+            self.backend = backend
+            self.base_url = base_url
+
+        def __enter__(self):
+            '''
+            Logins to identica and returns the Api object
+            '''
+            return twitter.Api(self.username, self.password, \
+                            base_url = self.base_url)
+
+        def __exit__(self, type, value, traceback):
+            '''
+            Analyzes the eventual exception risen during the connection to
+            identica
+            '''
+            if isinstance(value, urllib2.HTTPError):
+                if value.getcode() == 401:
+                    self.signal_authentication_wrong()
+                if value.getcode() in [502, 404]:
+                    self.signal_network_down()
+            elif isinstance(value, twitter.TwitterError):
+                self.signal_authentication_wrong()
+            elif isinstance(value, urllib2.URLError):
+                self.signal_network_down()
+            else:
+                return False
+            return True
+
+        def signal_authentication_wrong(self):
+            self.backend.quit(disable = True)
+            BackendSignals().backend_failed(self.backend.get_id(), \
+                            BackendSignals.ERRNO_AUTHENTICATION)
+
+        def signal_network_down(self):
+            BackendSignals().backend_failed(self.backend.get_id(), \
+                            BackendSignals.ERRNO_NETWORK)
+

=== added file 'GTG/backends/twitter.py'
--- GTG/backends/twitter.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/twitter.py	2010-08-24 01:51:40 +0000
@@ -0,0 +1,2547 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2007 Google Inc. All Rights Reserved.
+#
+# 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 library that provides a python interface to the Twitter API'''
+
+__author__ = 'dewitt@xxxxxxxxxx'
+__version__ = '0.7-devel'
+
+
+import base64
+import calendar
+import httplib
+import os
+import rfc822
+import simplejson
+import sys
+import tempfile
+import textwrap
+import time
+import urllib
+import urllib2
+import urlparse
+import gzip
+import StringIO
+
+try:
+  from hashlib import md5
+except ImportError:
+  from md5 import md5
+
+
+CHARACTER_LIMIT = 140
+
+# A singleton representing a lazily instantiated FileCache.
+DEFAULT_CACHE = object()
+
+
+class TwitterError(Exception):
+  '''Base class for Twitter errors'''
+  
+  @property
+  def message(self):
+    '''Returns the first argument used to construct this error.'''
+    return self.args[0]
+
+
+class Status(object):
+  '''A class representing the Status structure used by the twitter API.
+
+  The Status structure exposes the following properties:
+
+    status.created_at
+    status.created_at_in_seconds # read only
+    status.favorited
+    status.in_reply_to_screen_name
+    status.in_reply_to_user_id
+    status.in_reply_to_status_id
+    status.truncated
+    status.source
+    status.id
+    status.text
+    status.location
+    status.relative_created_at # read only
+    status.user
+  '''
+  def __init__(self,
+               created_at=None,
+               favorited=None,
+               id=None,
+               text=None,
+               location=None,
+               user=None,
+               in_reply_to_screen_name=None,
+               in_reply_to_user_id=None,
+               in_reply_to_status_id=None,
+               truncated=None,
+               source=None,
+               now=None):
+    '''An object to hold a Twitter status message.
+
+    This class is normally instantiated by the twitter.Api class and
+    returned in a sequence.
+
+    Note: Dates are posted in the form "Sat Jan 27 04:17:38 +0000 2007"
+
+    Args:
+      created_at: The time this status message was posted
+      favorited: Whether this is a favorite of the authenticated user
+      id: The unique id of this status message
+      text: The text of this status message
+      location: the geolocation string associated with this message
+      relative_created_at:
+        A human readable string representing the posting time
+      user:
+        A twitter.User instance representing the person posting the message
+      now:
+        The current time, if the client choses to set it.  Defaults to the
+        wall clock time.
+    '''
+    self.created_at = created_at
+    self.favorited = favorited
+    self.id = id
+    self.text = text
+    self.location = location
+    self.user = user
+    self.now = now
+    self.in_reply_to_screen_name = in_reply_to_screen_name
+    self.in_reply_to_user_id = in_reply_to_user_id
+    self.in_reply_to_status_id = in_reply_to_status_id
+    self.truncated = truncated
+    self.source = source
+
+  def GetCreatedAt(self):
+    '''Get the time this status message was posted.
+
+    Returns:
+      The time this status message was posted
+    '''
+    return self._created_at
+
+  def SetCreatedAt(self, created_at):
+    '''Set the time this status message was posted.
+
+    Args:
+      created_at: The time this status message was created
+    '''
+    self._created_at = created_at
+
+  created_at = property(GetCreatedAt, SetCreatedAt,
+                        doc='The time this status message was posted.')
+
+  def GetCreatedAtInSeconds(self):
+    '''Get the time this status message was posted, in seconds since the epoch.
+
+    Returns:
+      The time this status message was posted, in seconds since the epoch.
+    '''
+    return calendar.timegm(rfc822.parsedate(self.created_at))
+
+  created_at_in_seconds = property(GetCreatedAtInSeconds,
+                                   doc="The time this status message was "
+                                       "posted, in seconds since the epoch")
+
+  def GetFavorited(self):
+    '''Get the favorited setting of this status message.
+
+    Returns:
+      True if this status message is favorited; False otherwise
+    '''
+    return self._favorited
+
+  def SetFavorited(self, favorited):
+    '''Set the favorited state of this status message.
+
+    Args:
+      favorited: boolean True/False favorited state of this status message
+    '''
+    self._favorited = favorited
+
+  favorited = property(GetFavorited, SetFavorited,
+                       doc='The favorited state of this status message.')
+
+  def GetId(self):
+    '''Get the unique id of this status message.
+
+    Returns:
+      The unique id of this status message
+    '''
+    return self._id
+
+  def SetId(self, id):
+    '''Set the unique id of this status message.
+
+    Args:
+      id: The unique id of this status message
+    '''
+    self._id = id
+
+  id = property(GetId, SetId,
+                doc='The unique id of this status message.')
+
+  def GetInReplyToScreenName(self):
+    return self._in_reply_to_screen_name
+
+  def SetInReplyToScreenName(self, in_reply_to_screen_name):
+    self._in_reply_to_screen_name = in_reply_to_screen_name
+
+  in_reply_to_screen_name = property(GetInReplyToScreenName, SetInReplyToScreenName,
+                doc='')
+
+  def GetInReplyToUserId(self):
+    return self._in_reply_to_user_id
+
+  def SetInReplyToUserId(self, in_reply_to_user_id):
+    self._in_reply_to_user_id = in_reply_to_user_id
+
+  in_reply_to_user_id = property(GetInReplyToUserId, SetInReplyToUserId,
+                doc='')
+
+  def GetInReplyToStatusId(self):
+    return self._in_reply_to_status_id
+
+  def SetInReplyToStatusId(self, in_reply_to_status_id):
+    self._in_reply_to_status_id = in_reply_to_status_id
+
+  in_reply_to_status_id = property(GetInReplyToStatusId, SetInReplyToStatusId,
+                doc='')
+
+  def GetTruncated(self):
+    return self._truncated
+
+  def SetTruncated(self, truncated):
+    self._truncated = truncated
+
+  truncated = property(GetTruncated, SetTruncated,
+                doc='')
+
+  def GetSource(self):
+    return self._source
+
+  def SetSource(self, source):
+    self._source = source
+
+  source = property(GetSource, SetSource,
+                doc='')
+
+  def GetText(self):
+    '''Get the text of this status message.
+
+    Returns:
+      The text of this status message.
+    '''
+    return self._text
+
+  def SetText(self, text):
+    '''Set the text of this status message.
+
+    Args:
+      text: The text of this status message
+    '''
+    self._text = text
+
+  text = property(GetText, SetText,
+                  doc='The text of this status message')
+
+  def GetLocation(self):
+    '''Get the geolocation associated with this status message
+
+    Returns:
+      The geolocation string of this status message.
+    '''
+    return self._location
+
+  def SetLocation(self, location):
+    '''Set the geolocation associated with this status message
+
+    Args:
+      location: The geolocation string of this status message
+    '''
+    self._location = location
+
+  location = property(GetLocation, SetLocation,
+                      doc='The geolocation string of this status message')
+
+  def GetRelativeCreatedAt(self):
+    '''Get a human redable string representing the posting time
+
+    Returns:
+      A human readable string representing the posting time
+    '''
+    fudge = 1.25
+    delta  = long(self.now) - long(self.created_at_in_seconds)
+
+    if delta < (1 * fudge):
+      return 'about a second ago'
+    elif delta < (60 * (1/fudge)):
+      return 'about %d seconds ago' % (delta)
+    elif delta < (60 * fudge):
+      return 'about a minute ago'
+    elif delta < (60 * 60 * (1/fudge)):
+      return 'about %d minutes ago' % (delta / 60)
+    elif delta < (60 * 60 * fudge) or delta / (60 * 60) == 1:
+      return 'about an hour ago'
+    elif delta < (60 * 60 * 24 * (1/fudge)):
+      return 'about %d hours ago' % (delta / (60 * 60))
+    elif delta < (60 * 60 * 24 * fudge) or delta / (60 * 60 * 24) == 1:
+      return 'about a day ago'
+    else:
+      return 'about %d days ago' % (delta / (60 * 60 * 24))
+
+  relative_created_at = property(GetRelativeCreatedAt,
+                                 doc='Get a human readable string representing'
+                                     'the posting time')
+
+  def GetUser(self):
+    '''Get a twitter.User reprenting the entity posting this status message.
+
+    Returns:
+      A twitter.User reprenting the entity posting this status message
+    '''
+    return self._user
+
+  def SetUser(self, user):
+    '''Set a twitter.User reprenting the entity posting this status message.
+
+    Args:
+      user: A twitter.User reprenting the entity posting this status message
+    '''
+    self._user = user
+
+  user = property(GetUser, SetUser,
+                  doc='A twitter.User reprenting the entity posting this '
+                      'status message')
+
+  def GetNow(self):
+    '''Get the wallclock time for this status message.
+
+    Used to calculate relative_created_at.  Defaults to the time
+    the object was instantiated.
+
+    Returns:
+      Whatever the status instance believes the current time to be,
+      in seconds since the epoch.
+    '''
+    if self._now is None:
+      self._now = time.time()
+    return self._now
+
+  def SetNow(self, now):
+    '''Set the wallclock time for this status message.
+
+    Used to calculate relative_created_at.  Defaults to the time
+    the object was instantiated.
+
+    Args:
+      now: The wallclock time for this instance.
+    '''
+    self._now = now
+
+  now = property(GetNow, SetNow,
+                 doc='The wallclock time for this status instance.')
+
+
+  def __ne__(self, other):
+    return not self.__eq__(other)
+
+  def __eq__(self, other):
+    try:
+      return other and \
+             self.created_at == other.created_at and \
+             self.id == other.id and \
+             self.text == other.text and \
+             self.location == other.location and \
+             self.user == other.user and \
+             self.in_reply_to_screen_name == other.in_reply_to_screen_name and \
+             self.in_reply_to_user_id == other.in_reply_to_user_id and \
+             self.in_reply_to_status_id == other.in_reply_to_status_id and \
+             self.truncated == other.truncated and \
+             self.favorited == other.favorited and \
+             self.source == other.source
+    except AttributeError:
+      return False
+
+  def __str__(self):
+    '''A string representation of this twitter.Status instance.
+
+    The return value is the same as the JSON string representation.
+
+    Returns:
+      A string representation of this twitter.Status instance.
+    '''
+    return self.AsJsonString()
+
+  def AsJsonString(self):
+    '''A JSON string representation of this twitter.Status instance.
+
+    Returns:
+      A JSON string representation of this twitter.Status instance
+   '''
+    return simplejson.dumps(self.AsDict(), sort_keys=True)
+
+  def AsDict(self):
+    '''A dict representation of this twitter.Status instance.
+
+    The return value uses the same key names as the JSON representation.
+
+    Return:
+      A dict representing this twitter.Status instance
+    '''
+    data = {}
+    if self.created_at:
+      data['created_at'] = self.created_at
+    if self.favorited:
+      data['favorited'] = self.favorited
+    if self.id:
+      data['id'] = self.id
+    if self.text:
+      data['text'] = self.text
+    if self.location:
+      data['location'] = self.location  
+    if self.user:
+      data['user'] = self.user.AsDict()
+    if self.in_reply_to_screen_name:
+      data['in_reply_to_screen_name'] = self.in_reply_to_screen_name
+    if self.in_reply_to_user_id:
+      data['in_reply_to_user_id'] = self.in_reply_to_user_id
+    if self.in_reply_to_status_id:
+      data['in_reply_to_status_id'] = self.in_reply_to_status_id
+    if self.truncated is not None:
+      data['truncated'] = self.truncated
+    if self.favorited is not None:
+      data['favorited'] = self.favorited
+    if self.source:
+      data['source'] = self.source
+    return data
+
+  @staticmethod
+  def NewFromJsonDict(data):
+    '''Create a new instance based on a JSON dict.
+
+    Args:
+      data: A JSON dict, as converted from the JSON in the twitter API
+    Returns:
+      A twitter.Status instance
+    '''
+    if 'user' in data:
+      user = User.NewFromJsonDict(data['user'])
+    else:
+      user = None
+    return Status(created_at=data.get('created_at', None),
+                  favorited=data.get('favorited', None),
+                  id=data.get('id', None),
+                  text=data.get('text', None),
+                  location=data.get('location', None),
+                  in_reply_to_screen_name=data.get('in_reply_to_screen_name', None),
+                  in_reply_to_user_id=data.get('in_reply_to_user_id', None),
+                  in_reply_to_status_id=data.get('in_reply_to_status_id', None),
+                  truncated=data.get('truncated', None),
+                  source=data.get('source', None),
+                  user=user)
+
+
+class User(object):
+  '''A class representing the User structure used by the twitter API.
+
+  The User structure exposes the following properties:
+
+    user.id
+    user.name
+    user.screen_name
+    user.location
+    user.description
+    user.profile_image_url
+    user.profile_background_tile
+    user.profile_background_image_url
+    user.profile_sidebar_fill_color
+    user.profile_background_color
+    user.profile_link_color
+    user.profile_text_color
+    user.protected
+    user.utc_offset
+    user.time_zone
+    user.url
+    user.status
+    user.statuses_count
+    user.followers_count
+    user.friends_count
+    user.favourites_count
+  '''
+  def __init__(self,
+               id=None,
+               name=None,
+               screen_name=None,
+               location=None,
+               description=None,
+               profile_image_url=None,
+               profile_background_tile=None,
+               profile_background_image_url=None,
+               profile_sidebar_fill_color=None,
+               profile_background_color=None,
+               profile_link_color=None,
+               profile_text_color=None,
+               protected=None,
+               utc_offset=None,
+               time_zone=None,
+               followers_count=None,
+               friends_count=None,
+               statuses_count=None,
+               favourites_count=None,
+               url=None,
+               status=None):
+    self.id = id
+    self.name = name
+    self.screen_name = screen_name
+    self.location = location
+    self.description = description
+    self.profile_image_url = profile_image_url
+    self.profile_background_tile = profile_background_tile
+    self.profile_background_image_url = profile_background_image_url
+    self.profile_sidebar_fill_color = profile_sidebar_fill_color
+    self.profile_background_color = profile_background_color
+    self.profile_link_color = profile_link_color
+    self.profile_text_color = profile_text_color
+    self.protected = protected
+    self.utc_offset = utc_offset
+    self.time_zone = time_zone
+    self.followers_count = followers_count
+    self.friends_count = friends_count
+    self.statuses_count = statuses_count
+    self.favourites_count = favourites_count
+    self.url = url
+    self.status = status
+
+
+  def GetId(self):
+    '''Get the unique id of this user.
+
+    Returns:
+      The unique id of this user
+    '''
+    return self._id
+
+  def SetId(self, id):
+    '''Set the unique id of this user.
+
+    Args:
+      id: The unique id of this user.
+    '''
+    self._id = id
+
+  id = property(GetId, SetId,
+                doc='The unique id of this user.')
+
+  def GetName(self):
+    '''Get the real name of this user.
+
+    Returns:
+      The real name of this user
+    '''
+    return self._name
+
+  def SetName(self, name):
+    '''Set the real name of this user.
+
+    Args:
+      name: The real name of this user
+    '''
+    self._name = name
+
+  name = property(GetName, SetName,
+                  doc='The real name of this user.')
+
+  def GetScreenName(self):
+    '''Get the short username of this user.
+
+    Returns:
+      The short username of this user
+    '''
+    return self._screen_name
+
+  def SetScreenName(self, screen_name):
+    '''Set the short username of this user.
+
+    Args:
+      screen_name: the short username of this user
+    '''
+    self._screen_name = screen_name
+
+  screen_name = property(GetScreenName, SetScreenName,
+                         doc='The short username of this user.')
+
+  def GetLocation(self):
+    '''Get the geographic location of this user.
+
+    Returns:
+      The geographic location of this user
+    '''
+    return self._location
+
+  def SetLocation(self, location):
+    '''Set the geographic location of this user.
+
+    Args:
+      location: The geographic location of this user
+    '''
+    self._location = location
+
+  location = property(GetLocation, SetLocation,
+                      doc='The geographic location of this user.')
+
+  def GetDescription(self):
+    '''Get the short text description of this user.
+
+    Returns:
+      The short text description of this user
+    '''
+    return self._description
+
+  def SetDescription(self, description):
+    '''Set the short text description of this user.
+
+    Args:
+      description: The short text description of this user
+    '''
+    self._description = description
+
+  description = property(GetDescription, SetDescription,
+                         doc='The short text description of this user.')
+
+  def GetUrl(self):
+    '''Get the homepage url of this user.
+
+    Returns:
+      The homepage url of this user
+    '''
+    return self._url
+
+  def SetUrl(self, url):
+    '''Set the homepage url of this user.
+
+    Args:
+      url: The homepage url of this user
+    '''
+    self._url = url
+
+  url = property(GetUrl, SetUrl,
+                 doc='The homepage url of this user.')
+
+  def GetProfileImageUrl(self):
+    '''Get the url of the thumbnail of this user.
+
+    Returns:
+      The url of the thumbnail of this user
+    '''
+    return self._profile_image_url
+
+  def SetProfileImageUrl(self, profile_image_url):
+    '''Set the url of the thumbnail of this user.
+
+    Args:
+      profile_image_url: The url of the thumbnail of this user
+    '''
+    self._profile_image_url = profile_image_url
+
+  profile_image_url= property(GetProfileImageUrl, SetProfileImageUrl,
+                              doc='The url of the thumbnail of this user.')
+
+  def GetProfileBackgroundTile(self):
+    '''Boolean for whether to tile the profile background image.
+
+    Returns:
+      True if the background is to be tiled, False if not, None if unset.
+    '''
+    return self._profile_background_tile
+
+  def SetProfileBackgroundTile(self, profile_background_tile):
+    '''Set the boolean flag for whether to tile the profile background image.
+
+    Args:
+      profile_background_tile: Boolean flag for whether to tile or not.
+    '''
+    self._profile_background_tile = profile_background_tile
+
+  profile_background_tile = property(GetProfileBackgroundTile, SetProfileBackgroundTile,
+                                     doc='Boolean for whether to tile the background image.')
+
+  def GetProfileBackgroundImageUrl(self):
+    return self._profile_background_image_url
+
+  def SetProfileBackgroundImageUrl(self, profile_background_image_url):
+    self._profile_background_image_url = profile_background_image_url
+
+  profile_background_image_url = property(GetProfileBackgroundImageUrl, SetProfileBackgroundImageUrl,
+                                          doc='The url of the profile background of this user.')
+
+  def GetProfileSidebarFillColor(self):
+    return self._profile_sidebar_fill_color
+
+  def SetProfileSidebarFillColor(self, profile_sidebar_fill_color):
+    self._profile_sidebar_fill_color = profile_sidebar_fill_color
+
+  profile_sidebar_fill_color = property(GetProfileSidebarFillColor, SetProfileSidebarFillColor)
+
+  def GetProfileBackgroundColor(self):
+    return self._profile_background_color
+
+  def SetProfileBackgroundColor(self, profile_background_color):
+    self._profile_background_color = profile_background_color
+
+  profile_background_color = property(GetProfileBackgroundColor, SetProfileBackgroundColor)
+
+  def GetProfileLinkColor(self):
+    return self._profile_link_color
+
+  def SetProfileLinkColor(self, profile_link_color):
+    self._profile_link_color = profile_link_color
+
+  profile_link_color = property(GetProfileLinkColor, SetProfileLinkColor)
+
+  def GetProfileTextColor(self):
+    return self._profile_text_color
+
+  def SetProfileTextColor(self, profile_text_color):
+    self._profile_text_color = profile_text_color
+
+  profile_text_color = property(GetProfileTextColor, SetProfileTextColor)
+
+  def GetProtected(self):
+    return self._protected
+
+  def SetProtected(self, protected):
+    self._protected = protected
+
+  protected = property(GetProtected, SetProtected)
+
+  def GetUtcOffset(self):
+    return self._utc_offset
+
+  def SetUtcOffset(self, utc_offset):
+    self._utc_offset = utc_offset
+
+  utc_offset = property(GetUtcOffset, SetUtcOffset)
+
+  def GetTimeZone(self):
+    '''Returns the current time zone string for the user.
+
+    Returns:
+      The descriptive time zone string for the user.
+    '''
+    return self._time_zone
+
+  def SetTimeZone(self, time_zone):
+    '''Sets the user's time zone string.
+
+    Args:
+      time_zone: The descriptive time zone to assign for the user.
+    '''
+    self._time_zone = time_zone
+
+  time_zone = property(GetTimeZone, SetTimeZone)
+
+  def GetStatus(self):
+    '''Get the latest twitter.Status of this user.
+
+    Returns:
+      The latest twitter.Status of this user
+    '''
+    return self._status
+
+  def SetStatus(self, status):
+    '''Set the latest twitter.Status of this user.
+
+    Args:
+      status: The latest twitter.Status of this user
+    '''
+    self._status = status
+
+  status = property(GetStatus, SetStatus,
+                  doc='The latest twitter.Status of this user.')
+
+  def GetFriendsCount(self):
+    '''Get the friend count for this user.
+    
+    Returns:
+      The number of users this user has befriended.
+    '''
+    return self._friends_count
+
+  def SetFriendsCount(self, count):
+    '''Set the friend count for this user.
+
+    Args:
+      count: The number of users this user has befriended.
+    '''
+    self._friends_count = count
+
+  friends_count = property(GetFriendsCount, SetFriendsCount,
+                  doc='The number of friends for this user.')
+
+  def GetFollowersCount(self):
+    '''Get the follower count for this user.
+    
+    Returns:
+      The number of users following this user.
+    '''
+    return self._followers_count
+
+  def SetFollowersCount(self, count):
+    '''Set the follower count for this user.
+
+    Args:
+      count: The number of users following this user.
+    '''
+    self._followers_count = count
+
+  followers_count = property(GetFollowersCount, SetFollowersCount,
+                  doc='The number of users following this user.')
+
+  def GetStatusesCount(self):
+    '''Get the number of status updates for this user.
+    
+    Returns:
+      The number of status updates for this user.
+    '''
+    return self._statuses_count
+
+  def SetStatusesCount(self, count):
+    '''Set the status update count for this user.
+
+    Args:
+      count: The number of updates for this user.
+    '''
+    self._statuses_count = count
+
+  statuses_count = property(GetStatusesCount, SetStatusesCount,
+                  doc='The number of updates for this user.')
+
+  def GetFavouritesCount(self):
+    '''Get the number of favourites for this user.
+    
+    Returns:
+      The number of favourites for this user.
+    '''
+    return self._favourites_count
+
+  def SetFavouritesCount(self, count):
+    '''Set the favourite count for this user.
+
+    Args:
+      count: The number of favourites for this user.
+    '''
+    self._favourites_count = count
+
+  favourites_count = property(GetFavouritesCount, SetFavouritesCount,
+                  doc='The number of favourites for this user.')
+
+  def __ne__(self, other):
+    return not self.__eq__(other)
+
+  def __eq__(self, other):
+    try:
+      return other and \
+             self.id == other.id and \
+             self.name == other.name and \
+             self.screen_name == other.screen_name and \
+             self.location == other.location and \
+             self.description == other.description and \
+             self.profile_image_url == other.profile_image_url and \
+             self.profile_background_tile == other.profile_background_tile and \
+             self.profile_background_image_url == other.profile_background_image_url and \
+             self.profile_sidebar_fill_color == other.profile_sidebar_fill_color and \
+             self.profile_background_color == other.profile_background_color and \
+             self.profile_link_color == other.profile_link_color and \
+             self.profile_text_color == other.profile_text_color and \
+             self.protected == other.protected and \
+             self.utc_offset == other.utc_offset and \
+             self.time_zone == other.time_zone and \
+             self.url == other.url and \
+             self.statuses_count == other.statuses_count and \
+             self.followers_count == other.followers_count and \
+             self.favourites_count == other.favourites_count and \
+             self.friends_count == other.friends_count and \
+             self.status == other.status
+    except AttributeError:
+      return False
+
+  def __str__(self):
+    '''A string representation of this twitter.User instance.
+
+    The return value is the same as the JSON string representation.
+
+    Returns:
+      A string representation of this twitter.User instance.
+    '''
+    return self.AsJsonString()
+
+  def AsJsonString(self):
+    '''A JSON string representation of this twitter.User instance.
+
+    Returns:
+      A JSON string representation of this twitter.User instance
+   '''
+    return simplejson.dumps(self.AsDict(), sort_keys=True)
+
+  def AsDict(self):
+    '''A dict representation of this twitter.User instance.
+
+    The return value uses the same key names as the JSON representation.
+
+    Return:
+      A dict representing this twitter.User instance
+    '''
+    data = {}
+    if self.id:
+      data['id'] = self.id
+    if self.name:
+      data['name'] = self.name
+    if self.screen_name:
+      data['screen_name'] = self.screen_name
+    if self.location:
+      data['location'] = self.location
+    if self.description:
+      data['description'] = self.description
+    if self.profile_image_url:
+      data['profile_image_url'] = self.profile_image_url
+    if self.profile_background_tile is not None:
+      data['profile_background_tile'] = self.profile_background_tile
+    if self.profile_background_image_url:
+      data['profile_sidebar_fill_color'] = self.profile_background_image_url
+    if self.profile_background_color:
+      data['profile_background_color'] = self.profile_background_color
+    if self.profile_link_color:
+      data['profile_link_color'] = self.profile_link_color
+    if self.profile_text_color:
+      data['profile_text_color'] = self.profile_text_color
+    if self.protected is not None:
+      data['protected'] = self.protected
+    if self.utc_offset:
+      data['utc_offset'] = self.utc_offset
+    if self.time_zone:
+      data['time_zone'] = self.time_zone
+    if self.url:
+      data['url'] = self.url
+    if self.status:
+      data['status'] = self.status.AsDict()
+    if self.friends_count:
+      data['friends_count'] = self.friends_count
+    if self.followers_count:
+      data['followers_count'] = self.followers_count
+    if self.statuses_count:
+      data['statuses_count'] = self.statuses_count
+    if self.favourites_count:
+      data['favourites_count'] = self.favourites_count
+    return data
+
+  @staticmethod
+  def NewFromJsonDict(data):
+    '''Create a new instance based on a JSON dict.
+
+    Args:
+      data: A JSON dict, as converted from the JSON in the twitter API
+    Returns:
+      A twitter.User instance
+    '''
+    if 'status' in data:
+      status = Status.NewFromJsonDict(data['status'])
+    else:
+      status = None
+    return User(id=data.get('id', None),
+                name=data.get('name', None),
+                screen_name=data.get('screen_name', None),
+                location=data.get('location', None),
+                description=data.get('description', None),
+                statuses_count=data.get('statuses_count', None),
+                followers_count=data.get('followers_count', None),
+                favourites_count=data.get('favourites_count', None),
+                friends_count=data.get('friends_count', None),
+                profile_image_url=data.get('profile_image_url', None),
+                profile_background_tile = data.get('profile_background_tile', None),
+                profile_background_image_url = data.get('profile_background_image_url', None),
+                profile_sidebar_fill_color = data.get('profile_sidebar_fill_color', None),
+                profile_background_color = data.get('profile_background_color', None),
+                profile_link_color = data.get('profile_link_color', None),
+                profile_text_color = data.get('profile_text_color', None),
+                protected = data.get('protected', None),
+                utc_offset = data.get('utc_offset', None),
+                time_zone = data.get('time_zone', None),
+                url=data.get('url', None),
+                status=status)
+
+class DirectMessage(object):
+  '''A class representing the DirectMessage structure used by the twitter API.
+
+  The DirectMessage structure exposes the following properties:
+
+    direct_message.id
+    direct_message.created_at
+    direct_message.created_at_in_seconds # read only
+    direct_message.sender_id
+    direct_message.sender_screen_name
+    direct_message.recipient_id
+    direct_message.recipient_screen_name
+    direct_message.text
+  '''
+
+  def __init__(self,
+               id=None,
+               created_at=None,
+               sender_id=None,
+               sender_screen_name=None,
+               recipient_id=None,
+               recipient_screen_name=None,
+               text=None):
+    '''An object to hold a Twitter direct message.
+
+    This class is normally instantiated by the twitter.Api class and
+    returned in a sequence.
+
+    Note: Dates are posted in the form "Sat Jan 27 04:17:38 +0000 2007"
+
+    Args:
+      id: The unique id of this direct message
+      created_at: The time this direct message was posted
+      sender_id: The id of the twitter user that sent this message
+      sender_screen_name: The name of the twitter user that sent this message
+      recipient_id: The id of the twitter that received this message
+      recipient_screen_name: The name of the twitter that received this message
+      text: The text of this direct message
+    '''
+    self.id = id
+    self.created_at = created_at
+    self.sender_id = sender_id
+    self.sender_screen_name = sender_screen_name
+    self.recipient_id = recipient_id
+    self.recipient_screen_name = recipient_screen_name
+    self.text = text
+
+  def GetId(self):
+    '''Get the unique id of this direct message.
+
+    Returns:
+      The unique id of this direct message
+    '''
+    return self._id
+
+  def SetId(self, id):
+    '''Set the unique id of this direct message.
+
+    Args:
+      id: The unique id of this direct message
+    '''
+    self._id = id
+
+  id = property(GetId, SetId,
+                doc='The unique id of this direct message.')
+
+  def GetCreatedAt(self):
+    '''Get the time this direct message was posted.
+
+    Returns:
+      The time this direct message was posted
+    '''
+    return self._created_at
+
+  def SetCreatedAt(self, created_at):
+    '''Set the time this direct message was posted.
+
+    Args:
+      created_at: The time this direct message was created
+    '''
+    self._created_at = created_at
+
+  created_at = property(GetCreatedAt, SetCreatedAt,
+                        doc='The time this direct message was posted.')
+
+  def GetCreatedAtInSeconds(self):
+    '''Get the time this direct message was posted, in seconds since the epoch.
+
+    Returns:
+      The time this direct message was posted, in seconds since the epoch.
+    '''
+    return calendar.timegm(rfc822.parsedate(self.created_at))
+
+  created_at_in_seconds = property(GetCreatedAtInSeconds,
+                                   doc="The time this direct message was "
+                                       "posted, in seconds since the epoch")
+
+  def GetSenderId(self):
+    '''Get the unique sender id of this direct message.
+
+    Returns:
+      The unique sender id of this direct message
+    '''
+    return self._sender_id
+
+  def SetSenderId(self, sender_id):
+    '''Set the unique sender id of this direct message.
+
+    Args:
+      sender id: The unique sender id of this direct message
+    '''
+    self._sender_id = sender_id
+
+  sender_id = property(GetSenderId, SetSenderId,
+                doc='The unique sender id of this direct message.')
+
+  def GetSenderScreenName(self):
+    '''Get the unique sender screen name of this direct message.
+
+    Returns:
+      The unique sender screen name of this direct message
+    '''
+    return self._sender_screen_name
+
+  def SetSenderScreenName(self, sender_screen_name):
+    '''Set the unique sender screen name of this direct message.
+
+    Args:
+      sender_screen_name: The unique sender screen name of this direct message
+    '''
+    self._sender_screen_name = sender_screen_name
+
+  sender_screen_name = property(GetSenderScreenName, SetSenderScreenName,
+                doc='The unique sender screen name of this direct message.')
+
+  def GetRecipientId(self):
+    '''Get the unique recipient id of this direct message.
+
+    Returns:
+      The unique recipient id of this direct message
+    '''
+    return self._recipient_id
+
+  def SetRecipientId(self, recipient_id):
+    '''Set the unique recipient id of this direct message.
+
+    Args:
+      recipient id: The unique recipient id of this direct message
+    '''
+    self._recipient_id = recipient_id
+
+  recipient_id = property(GetRecipientId, SetRecipientId,
+                doc='The unique recipient id of this direct message.')
+
+  def GetRecipientScreenName(self):
+    '''Get the unique recipient screen name of this direct message.
+
+    Returns:
+      The unique recipient screen name of this direct message
+    '''
+    return self._recipient_screen_name
+
+  def SetRecipientScreenName(self, recipient_screen_name):
+    '''Set the unique recipient screen name of this direct message.
+
+    Args:
+      recipient_screen_name: The unique recipient screen name of this direct message
+    '''
+    self._recipient_screen_name = recipient_screen_name
+
+  recipient_screen_name = property(GetRecipientScreenName, SetRecipientScreenName,
+                doc='The unique recipient screen name of this direct message.')
+
+  def GetText(self):
+    '''Get the text of this direct message.
+
+    Returns:
+      The text of this direct message.
+    '''
+    return self._text
+
+  def SetText(self, text):
+    '''Set the text of this direct message.
+
+    Args:
+      text: The text of this direct message
+    '''
+    self._text = text
+
+  text = property(GetText, SetText,
+                  doc='The text of this direct message')
+
+  def __ne__(self, other):
+    return not self.__eq__(other)
+
+  def __eq__(self, other):
+    try:
+      return other and \
+          self.id == other.id and \
+          self.created_at == other.created_at and \
+          self.sender_id == other.sender_id and \
+          self.sender_screen_name == other.sender_screen_name and \
+          self.recipient_id == other.recipient_id and \
+          self.recipient_screen_name == other.recipient_screen_name and \
+          self.text == other.text
+    except AttributeError:
+      return False
+
+  def __str__(self):
+    '''A string representation of this twitter.DirectMessage instance.
+
+    The return value is the same as the JSON string representation.
+
+    Returns:
+      A string representation of this twitter.DirectMessage instance.
+    '''
+    return self.AsJsonString()
+
+  def AsJsonString(self):
+    '''A JSON string representation of this twitter.DirectMessage instance.
+
+    Returns:
+      A JSON string representation of this twitter.DirectMessage instance
+   '''
+    return simplejson.dumps(self.AsDict(), sort_keys=True)
+
+  def AsDict(self):
+    '''A dict representation of this twitter.DirectMessage instance.
+
+    The return value uses the same key names as the JSON representation.
+
+    Return:
+      A dict representing this twitter.DirectMessage instance
+    '''
+    data = {}
+    if self.id:
+      data['id'] = self.id
+    if self.created_at:
+      data['created_at'] = self.created_at
+    if self.sender_id:
+      data['sender_id'] = self.sender_id
+    if self.sender_screen_name:
+      data['sender_screen_name'] = self.sender_screen_name
+    if self.recipient_id:
+      data['recipient_id'] = self.recipient_id
+    if self.recipient_screen_name:
+      data['recipient_screen_name'] = self.recipient_screen_name
+    if self.text:
+      data['text'] = self.text
+    return data
+
+  @staticmethod
+  def NewFromJsonDict(data):
+    '''Create a new instance based on a JSON dict.
+
+    Args:
+      data: A JSON dict, as converted from the JSON in the twitter API
+    Returns:
+      A twitter.DirectMessage instance
+    '''
+    return DirectMessage(created_at=data.get('created_at', None),
+                         recipient_id=data.get('recipient_id', None),
+                         sender_id=data.get('sender_id', None),
+                         text=data.get('text', None),
+                         sender_screen_name=data.get('sender_screen_name', None),
+                         id=data.get('id', None),
+                         recipient_screen_name=data.get('recipient_screen_name', None))
+
+class Api(object):
+  '''A python interface into the Twitter API
+
+  By default, the Api caches results for 1 minute.
+
+  Example usage:
+
+    To create an instance of the twitter.Api class, with no authentication:
+
+      >>> import twitter
+      >>> api = twitter.Api()
+
+    To fetch the most recently posted public twitter status messages:
+
+      >>> statuses = api.GetPublicTimeline()
+      >>> print [s.user.name for s in statuses]
+      [u'DeWitt', u'Kesuke Miyagi', u'ev', u'Buzz Andersen', u'Biz Stone'] #...
+
+    To fetch a single user's public status messages, where "user" is either
+    a Twitter "short name" or their user id.
+
+      >>> statuses = api.GetUserTimeline(user)
+      >>> print [s.text for s in statuses]
+
+    To use authentication, instantiate the twitter.Api class with a
+    username and password:
+
+      >>> api = twitter.Api(username='twitter user', password='twitter pass')
+
+    To fetch your friends (after being authenticated):
+
+      >>> users = api.GetFriends()
+      >>> print [u.name for u in users]
+
+    To post a twitter status message (after being authenticated):
+
+      >>> status = api.PostUpdate('I love python-twitter!')
+      >>> print status.text
+      I love python-twitter!
+
+    There are many other methods, including:
+
+      >>> api.PostUpdates(status)
+      >>> api.PostDirectMessage(user, text)
+      >>> api.GetUser(user)
+      >>> api.GetReplies()
+      >>> api.GetUserTimeline(user)
+      >>> api.GetStatus(id)
+      >>> api.DestroyStatus(id)
+      >>> api.GetFriendsTimeline(user)
+      >>> api.GetFriends(user)
+      >>> api.GetFollowers()
+      >>> api.GetFeatured()
+      >>> api.GetDirectMessages()
+      >>> api.PostDirectMessage(user, text)
+      >>> api.DestroyDirectMessage(id)
+      >>> api.DestroyFriendship(user)
+      >>> api.CreateFriendship(user)
+      >>> api.GetUserByEmail(email)
+      >>> api.VerifyCredentials()
+  '''
+
+  DEFAULT_CACHE_TIMEOUT = 60 # cache for 1 minute
+
+  _API_REALM = 'Twitter API'
+
+  def __init__(self,
+               username=None,
+               password=None,
+               input_encoding=None,
+               request_headers=None,
+               cache=DEFAULT_CACHE,
+               shortner=None,
+               base_url=None,
+               use_gzip_compression=False):
+    '''Instantiate a new twitter.Api object.
+
+    Args:
+      username:
+        The username of the twitter account.  [optional]
+      password:
+        The password for the twitter account. [optional]
+      input_encoding:
+        The encoding used to encode input strings. [optional]
+      request_header:
+        A dictionary of additional HTTP request headers. [optional]
+      cache:
+        The cache instance to use. Defaults to DEFAULT_CACHE.
+        Use None to disable caching. [optional]
+      shortner:
+        The shortner instance to use.  Defaults to None.
+        See shorten_url.py for an example shortner. [optional]
+      base_url:
+        The base URL to use to contact the Twitter API.
+        Defaults to https://twitter.com. [optional]
+      use_gzip_compression:
+        Set to True to tell enable gzip compression for any call
+        made to Twitter.  Defaults to False. [optional]
+    '''
+    self.SetCache(cache)
+    self._urllib = urllib2
+    self._cache_timeout = Api.DEFAULT_CACHE_TIMEOUT
+    self._InitializeRequestHeaders(request_headers)
+    self._InitializeUserAgent()
+    self._InitializeDefaultParameters()
+    self._input_encoding = input_encoding
+    self._use_gzip = use_gzip_compression
+    self.SetCredentials(username, password)
+    if base_url is None:
+      self.base_url = 'https://twitter.com'
+    else:
+      self.base_url = base_url
+
+  def GetPublicTimeline(self,
+                        since_id=None):
+    '''Fetch the sequnce of public twitter.Status message for all users.
+
+    Args:
+      since_id:
+        Returns only public statuses with an ID greater than
+        (that is, more recent than) the specified ID. [optional]
+
+    Returns:
+      An sequence of twitter.Status instances, one for each message
+    '''
+    parameters = {}
+
+    if since_id:
+      parameters['since_id'] = since_id
+
+    url  = '%s/statuses/public_timeline.json' % self.base_url
+    json = self._FetchUrl(url,  parameters=parameters)
+    data = simplejson.loads(json)
+
+    self._CheckForTwitterError(data)
+
+    return [Status.NewFromJsonDict(x) for x in data]
+
+  def FilterPublicTimeline(self,
+                           term,
+                           since_id=None):
+    '''Filter the public twitter timeline by a given search term on
+    the local machine.
+
+    Args:
+      term:
+        term to search by.
+      since_id:
+        Returns only public statuses with an ID greater than
+        (that is, more recent than) the specified ID. [optional]
+
+    Returns:
+      A sequence of twitter.Status instances, one for each message
+      containing the term
+    '''
+    statuses = self.GetPublicTimeline(since_id)
+    results  = []
+
+    for s in statuses:
+      if s.text.lower().find(term.lower()) != -1:
+        results.append(s)
+
+    return results
+
+  def GetSearch(self,
+                term,
+                geocode=None,
+                since_id=None,
+                per_page=15,
+                page=1,
+                lang="en",
+                show_user="true",
+                query_users=False):
+    '''Return twitter search results for a given term.
+
+    Args:
+      term:
+        term to search by.
+      since_id:
+        Returns only public statuses with an ID greater than
+        (that is, more recent than) the specified ID. [optional]
+      geocode:
+        geolocation information in the form (latitude, longitude, radius)
+        [optional]
+      per_page:
+        number of results to return.  Default is 15 [optional]
+      page:
+        which page of search results to return
+      lang:
+        language for results.  Default is English [optional]
+      show_user:
+        prefixes screen name in status
+      query_users:
+        If set to False, then all users only have screen_name and
+        profile_image_url available.
+        If set to True, all information of users are available,
+        but it uses lots of request quota, one per status.
+    Returns:
+      A sequence of twitter.Status instances, one for each message containing
+      the term
+    '''
+    # Build request parameters
+    parameters = {}
+
+    if since_id:
+      parameters['since_id'] = since_id
+
+    if not term:
+      return []
+
+    parameters['q'] = urllib.quote_plus(term)
+    parameters['show_user'] = show_user
+    parameters['lang'] = lang
+    parameters['rpp'] = per_page
+    parameters['page'] = page
+
+    if geocode is not None:
+      parameters['geocode'] = ','.join(map(str, geocode))
+
+    # Make and send requests
+    url  = 'http://search.twitter.com/search.json'
+    json = self._FetchUrl(url, parameters=parameters)
+    data = simplejson.loads(json)
+
+    self._CheckForTwitterError(data)
+
+    results = []
+
+    for x in data['results']:
+      temp = Status.NewFromJsonDict(x)
+
+      if query_users:
+        # Build user object with new request
+        temp.user = self.GetUser(urllib.quote(x['from_user']))
+      else:
+        temp.user = User(screen_name=x['from_user'], profile_image_url=x['profile_image_url'])
+
+      results.append(temp)
+
+    # Return built list of statuses
+    return results # [Status.NewFromJsonDict(x) for x in data['results']]
+
+  def GetFriendsTimeline(self,
+                         user=None,
+                         count=None,
+                         since=None,
+                         since_id=None):
+    '''Fetch the sequence of twitter.Status messages for a user's friends
+
+    The twitter.Api instance must be authenticated if the user is private.
+
+    Args:
+      user:
+        Specifies the ID or screen name of the user for whom to return
+        the friends_timeline.  If unspecified, the username and password
+        must be set in the twitter.Api instance.  [Optional]
+      count: 
+        Specifies the number of statuses to retrieve. May not be
+        greater than 200. [Optional]
+      since:
+        Narrows the returned results to just those statuses created
+        after the specified HTTP-formatted date. [Optional]
+      since_id:
+        Returns only public statuses with an ID greater than (that is,
+        more recent than) the specified ID. [Optional]
+
+    Returns:
+      A sequence of twitter.Status instances, one for each message
+    '''
+    if not user and not self._username:
+      raise TwitterError("User must be specified if API is not authenticated.")
+    if user:
+      url = '%s/statuses/friends_timeline/%s.json' % (self.base_url, user)
+    else:
+      url = '%s/statuses/friends_timeline.json' % self.base_url
+    parameters = {}
+    if count is not None:
+      try:
+        if int(count) > 200:
+          raise TwitterError("'count' may not be greater than 200")
+      except ValueError:
+        raise TwitterError("'count' must be an integer")
+      parameters['count'] = count
+    if since:
+      parameters['since'] = since
+    if since_id:
+      parameters['since_id'] = since_id
+    json = self._FetchUrl(url, parameters=parameters)
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return [Status.NewFromJsonDict(x) for x in data]
+
+  def GetUserTimeline(self,
+                      id=None,
+                      user_id=None,
+                      screen_name=None,
+                      since_id=None,
+                      max_id=None,
+                      count=None,
+                      page=None):
+    '''Fetch the sequence of public Status messages for a single user.
+
+    The twitter.Api instance must be authenticated if the user is private.
+
+    Args:
+      id:
+        Specifies the ID or screen name of the user for whom to return
+        the user_timeline. [optional]
+      user_id:
+        Specfies the ID of the user for whom to return the
+        user_timeline. Helpful for disambiguating when a valid user ID
+        is also a valid screen name. [optional]
+      screen_name:
+        Specfies the screen name of the user for whom to return the
+        user_timeline. Helpful for disambiguating when a valid screen
+        name is also a user ID. [optional]
+      since_id:
+        Returns only public statuses with an ID greater than (that is,
+        more recent than) the specified ID. [optional]
+      max_id:
+        Returns only statuses with an ID less than (that is, older
+        than) or equal to the specified ID. [optional]
+      count:
+        Specifies the number of statuses to retrieve. May not be
+        greater than 200.  [optional]
+      page:
+         Specifies the page of results to retrieve. Note: there are
+         pagination limits. [optional]
+
+    Returns:
+      A sequence of Status instances, one for each message up to count
+    '''
+    parameters = {}
+
+    if id:
+      url = '%s/statuses/user_timeline/%s.json' % (self.base_url, id)
+    elif user_id:
+      url = '%s/statuses/user_timeline.json?user_id=%d' % (self.base_url, user_id)
+    elif screen_name:
+      url = ('%s/statuses/user_timeline.json?screen_name=%s' % (self.base_url,
+             screen_name))
+    elif not self._username:
+      raise TwitterError("User must be specified if API is not authenticated.")
+    else:
+      url = '%s/statuses/user_timeline.json' % self.base_url
+
+    if since_id:
+      try:
+        parameters['since_id'] = long(since_id)
+      except:
+        raise TwitterError("since_id must be an integer")
+
+    if max_id:
+      try:
+        parameters['max_id'] = long(max_id)
+      except:
+        raise TwitterError("max_id must be an integer")
+
+    if count:
+      try:
+        parameters['count'] = int(count)
+      except:
+        raise TwitterError("count must be an integer")
+
+    if page:
+      try:
+        parameters['page'] = int(page)
+      except:
+        raise TwitterError("page must be an integer")
+
+    json = self._FetchUrl(url, parameters=parameters)
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return [Status.NewFromJsonDict(x) for x in data]
+
+  def GetStatus(self, id):
+    '''Returns a single status message.
+
+    The twitter.Api instance must be authenticated if the status message is private.
+
+    Args:
+      id: The numerical ID of the status you're trying to retrieve.
+
+    Returns:
+      A twitter.Status instance representing that status message
+    '''
+    try:
+      if id:
+        long(id)
+    except:
+      raise TwitterError("id must be an long integer")
+    url = '%s/statuses/show/%s.json' % (self.base_url, id)
+    json = self._FetchUrl(url)
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return Status.NewFromJsonDict(data)
+
+  def DestroyStatus(self, id):
+    '''Destroys the status specified by the required ID parameter.
+
+    The twitter.Api instance must be authenticated and thee
+    authenticating user must be the author of the specified status.
+
+    Args:
+      id: The numerical ID of the status you're trying to destroy.
+
+    Returns:
+      A twitter.Status instance representing the destroyed status message
+    '''
+    try:
+      if id:
+        long(id)
+    except:
+      raise TwitterError("id must be an integer")
+    url = '%s/statuses/destroy/%s.json' % (self.base_url, id)
+    json = self._FetchUrl(url, post_data={})
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return Status.NewFromJsonDict(data)
+
+  def PostUpdate(self, status, in_reply_to_status_id=None):
+    '''Post a twitter status message from the authenticated user.
+
+    The twitter.Api instance must be authenticated.
+
+    Args:
+      status:
+        The message text to be posted.  Must be less than or equal to
+        140 characters.
+      in_reply_to_status_id:
+        The ID of an existing status that the status to be posted is
+        in reply to.  This implicitly sets the in_reply_to_user_id
+        attribute of the resulting status to the user ID of the
+        message being replied to.  Invalid/missing status IDs will be
+        ignored. [Optional]
+    Returns:
+      A twitter.Status instance representing the message posted.
+    '''
+    if not self._username:
+      raise TwitterError("The twitter.Api instance must be authenticated.")
+
+    url = '%s/statuses/update.json' % self.base_url
+
+    if len(status) > CHARACTER_LIMIT:
+      raise TwitterError("Text must be less than or equal to %d characters. "
+                         "Consider using PostUpdates." % CHARACTER_LIMIT)
+
+    data = {'status': status}
+    if in_reply_to_status_id:
+      data['in_reply_to_status_id'] = in_reply_to_status_id
+    json = self._FetchUrl(url, post_data=data)
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return Status.NewFromJsonDict(data)
+
+  def PostUpdates(self, status, continuation=None, **kwargs):
+    '''Post one or more twitter status messages from the authenticated user.
+
+    Unlike api.PostUpdate, this method will post multiple status updates
+    if the message is longer than 140 characters.
+
+    The twitter.Api instance must be authenticated.
+
+    Args:
+      status:
+        The message text to be posted.  May be longer than 140 characters.
+      continuation:
+        The character string, if any, to be appended to all but the
+        last message.  Note that Twitter strips trailing '...' strings
+        from messages.  Consider using the unicode \u2026 character
+        (horizontal ellipsis) instead. [Defaults to None]
+      **kwargs:
+        See api.PostUpdate for a list of accepted parameters.
+    Returns:
+      A of list twitter.Status instance representing the messages posted.
+    '''
+    results = list()
+    if continuation is None:
+      continuation = ''
+    line_length = CHARACTER_LIMIT - len(continuation)
+    lines = textwrap.wrap(status, line_length)
+    for line in lines[0:-1]:
+      results.append(self.PostUpdate(line + continuation, **kwargs))
+    results.append(self.PostUpdate(lines[-1], **kwargs))
+    return results
+
+  def GetReplies(self, since=None, since_id=None, page=None): 
+    '''Get a sequence of status messages representing the 20 most recent
+    replies (status updates prefixed with @username) to the authenticating
+    user.
+
+    Args:
+      page: 
+      since:
+        Narrows the returned results to just those statuses created
+        after the specified HTTP-formatted date. [optional]
+      since_id:
+        Returns only public statuses with an ID greater than (that is,
+        more recent than) the specified ID. [Optional]
+
+    Returns:
+      A sequence of twitter.Status instances, one for each reply to the user.
+    '''
+    url = '%s/statuses/replies.json' % self.base_url
+    if not self._username:
+      raise TwitterError("The twitter.Api instance must be authenticated.")
+    parameters = {}
+    if since:
+      parameters['since'] = since
+    if since_id:
+      parameters['since_id'] = since_id
+    if page:
+      parameters['page'] = page
+    json = self._FetchUrl(url, parameters=parameters)
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return [Status.NewFromJsonDict(x) for x in data]
+
+  def GetFriends(self, user=None, page=None):
+    '''Fetch the sequence of twitter.User instances, one for each friend.
+
+    Args:
+      user: the username or id of the user whose friends you are fetching.  If
+      not specified, defaults to the authenticated user. [optional]
+
+    The twitter.Api instance must be authenticated.
+
+    Returns:
+      A sequence of twitter.User instances, one for each friend
+    '''
+    if not user and not self._username:
+      raise TwitterError("twitter.Api instance must be authenticated")
+    if user:
+      url = '%s/statuses/friends/%s.json' % (self.base_url, user)
+    else:
+      url = '%s/statuses/friends.json' % self.base_url
+    parameters = {}
+    if page:
+      parameters['page'] = page
+    json = self._FetchUrl(url, parameters=parameters)
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return [User.NewFromJsonDict(x) for x in data]
+
+  def GetFriendIDs(self, user=None, page=None):
+      '''Returns a list of twitter user id's for every person
+      the specified user is following.
+
+      Args:
+        user:
+          The id or screen_name of the user to retrieve the id list for
+          [optional]
+        page:
+          Specifies the page number of the results beginning at 1.
+          A single page contains 5000 ids. This is recommended for users
+          with large id lists. If not provided all id's are returned. 
+          (Please note that the result set isn't guaranteed to be 5000 
+          every time as suspended users will be filtered.)
+          [optional]
+
+      Returns:
+        A list of integers, one for each user id.
+      '''
+      if not user and not self._username:
+          raise TwitterError("twitter.Api instance must be authenticated")
+      if user:
+          url = '%s/friends/ids/%s.json' % (self.base_url, user)
+      else:
+          url = '%s/friends/ids.json' % self.base_url
+      parameters = {}
+      if page:
+          parameters['page'] = page
+      json = self._FetchUrl(url, parameters=parameters)
+      data = simplejson.loads(json)
+      self._CheckForTwitterError(data)
+      return data
+
+  def GetFollowers(self, page=None):
+    '''Fetch the sequence of twitter.User instances, one for each follower
+
+    The twitter.Api instance must be authenticated.
+
+    Returns:
+      A sequence of twitter.User instances, one for each follower
+    '''
+    if not self._username:
+      raise TwitterError("twitter.Api instance must be authenticated")
+    url = '%s/statuses/followers.json' % self.base_url
+    parameters = {}
+    if page:
+      parameters['page'] = page
+    json = self._FetchUrl(url, parameters=parameters)
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return [User.NewFromJsonDict(x) for x in data]
+
+  def GetFeatured(self):
+    '''Fetch the sequence of twitter.User instances featured on twitter.com
+
+    The twitter.Api instance must be authenticated.
+
+    Returns:
+      A sequence of twitter.User instances
+    '''
+    url = '%s/statuses/featured.json' % self.base_url
+    json = self._FetchUrl(url)
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return [User.NewFromJsonDict(x) for x in data]
+
+  def GetUser(self, user):
+    '''Returns a single user.
+
+    The twitter.Api instance must be authenticated.
+
+    Args:
+      user: The username or id of the user to retrieve.
+
+    Returns:
+      A twitter.User instance representing that user
+    '''
+    url = '%s/users/show/%s.json' % (self.base_url, user)
+    json = self._FetchUrl(url)
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return User.NewFromJsonDict(data)
+
+  def GetDirectMessages(self, since=None, since_id=None, page=None):
+    '''Returns a list of the direct messages sent to the authenticating user.
+
+    The twitter.Api instance must be authenticated.
+
+    Args:
+      since:
+        Narrows the returned results to just those statuses created
+        after the specified HTTP-formatted date. [optional]
+      since_id:
+        Returns only public statuses with an ID greater than (that is,
+        more recent than) the specified ID. [Optional]
+
+    Returns:
+      A sequence of twitter.DirectMessage instances
+    '''
+    url = '%s/direct_messages.json' % self.base_url
+    if not self._username:
+      raise TwitterError("The twitter.Api instance must be authenticated.")
+    parameters = {}
+    if since:
+      parameters['since'] = since
+    if since_id:
+      parameters['since_id'] = since_id
+    if page:
+      parameters['page'] = page 
+    json = self._FetchUrl(url, parameters=parameters)
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return [DirectMessage.NewFromJsonDict(x) for x in data]
+
+  def PostDirectMessage(self, user, text):
+    '''Post a twitter direct message from the authenticated user
+
+    The twitter.Api instance must be authenticated.
+
+    Args:
+      user: The ID or screen name of the recipient user.
+      text: The message text to be posted.  Must be less than 140 characters.
+
+    Returns:
+      A twitter.DirectMessage instance representing the message posted
+    '''
+    if not self._username:
+      raise TwitterError("The twitter.Api instance must be authenticated.")
+    url = '%s/direct_messages/new.json' % self.base_url
+    data = {'text': text, 'user': user}
+    json = self._FetchUrl(url, post_data=data)
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return DirectMessage.NewFromJsonDict(data)
+
+  def DestroyDirectMessage(self, id):
+    '''Destroys the direct message specified in the required ID parameter.
+
+    The twitter.Api instance must be authenticated, and the
+    authenticating user must be the recipient of the specified direct
+    message.
+
+    Args:
+      id: The id of the direct message to be destroyed
+
+    Returns:
+      A twitter.DirectMessage instance representing the message destroyed
+    '''
+    url = '%s/direct_messages/destroy/%s.json' % (self.base_url, id)
+    json = self._FetchUrl(url, post_data={})
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return DirectMessage.NewFromJsonDict(data)
+
+  def CreateFriendship(self, user):
+    '''Befriends the user specified in the user parameter as the authenticating user.
+
+    The twitter.Api instance must be authenticated.
+
+    Args:
+      The ID or screen name of the user to befriend.
+    Returns:
+      A twitter.User instance representing the befriended user.
+    '''
+    url = '%s/friendships/create/%s.json' % (self.base_url, user)
+    json = self._FetchUrl(url, post_data={})
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return User.NewFromJsonDict(data)
+
+  def DestroyFriendship(self, user):
+    '''Discontinues friendship with the user specified in the user parameter.
+
+    The twitter.Api instance must be authenticated.
+
+    Args:
+      The ID or screen name of the user  with whom to discontinue friendship.
+    Returns:
+      A twitter.User instance representing the discontinued friend.
+    '''
+    url = '%s/friendships/destroy/%s.json' % (self.base_url, user)
+    json = self._FetchUrl(url, post_data={})
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return User.NewFromJsonDict(data)
+
+  def CreateFavorite(self, status):
+    '''Favorites the status specified in the status parameter as the authenticating user.
+    Returns the favorite status when successful.
+
+    The twitter.Api instance must be authenticated.
+
+    Args:
+      The twitter.Status instance to mark as a favorite.
+    Returns:
+      A twitter.Status instance representing the newly-marked favorite.
+    '''
+    url = '%s/favorites/create/%s.json' % (self.base_url, status.id)
+    json = self._FetchUrl(url, post_data={})
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return Status.NewFromJsonDict(data)
+
+  def DestroyFavorite(self, status):
+    '''Un-favorites the status specified in the ID parameter as the authenticating user.
+    Returns the un-favorited status in the requested format when successful.
+
+    The twitter.Api instance must be authenticated.
+
+    Args:
+      The twitter.Status to unmark as a favorite.
+    Returns:
+      A twitter.Status instance representing the newly-unmarked favorite.
+    '''
+    url = '%s/favorites/destroy/%s.json' % (self.base_url, status.id)
+    json = self._FetchUrl(url, post_data={})
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return Status.NewFromJsonDict(data)
+
+  def GetFavorites(self,
+                   user=None,
+                   page=None):
+    '''Return a list of Status objects representing favorited tweets.
+    By default, returns the (up to) 20 most recent tweets for the
+    authenticated user.
+    
+    Args:
+      user:
+        The username or id of the user whose favorites you are fetching.
+        If not specified, defaults to the authenticated user. [optional]
+    
+      page:
+        Retrieves the 20 next most recent favorite statuses. [optional]
+    '''
+    parameters = {}
+
+    if page:
+      parameters['page'] = page
+
+    if user:
+      url = '%s/favorites/%s.json' % (self.base_url, user)
+    elif not user and not self._username:
+      raise TwitterError("User must be specified if API is not authenticated.")
+    else:
+      url = '%s/favorites.json' % self.base_url
+
+    json = self._FetchUrl(url, parameters=parameters)
+    data = simplejson.loads(json)
+
+    self._CheckForTwitterError(data)
+
+    return [Status.NewFromJsonDict(x) for x in data]
+
+  def GetMentions(self,
+                  since_id=None,
+                  max_id=None,
+                  page=None):
+    '''Returns the 20 most recent mentions (status containing @username)
+    for the authenticating user.
+    
+    Args:
+      since_id:
+        Returns only public statuses with an ID greater than
+        (that is, more recent than) the specified ID. [optional]
+    
+      max_id:
+        Returns only statuses with an ID less than
+        (that is, older than) the specified ID.  [optional]
+    
+      page:
+        Retrieves the 20 next most recent replies. [optional]
+    
+    Returns:
+      A sequence of twitter.Status instances, one for each mention of the user.
+      see: http://apiwiki.twitter.com/REST-API-Documentation#statuses/mentions
+    '''
+
+    url = '%s/statuses/mentions.json' % self.base_url
+
+    if not self._username:
+      raise TwitterError("The twitter.Api instance must be authenticated.")
+
+    parameters = {}
+
+    if since_id:
+      parameters['since_id'] = since_id
+    if max_id:
+      parameters['max_id'] = max_id
+    if page:
+      parameters['page'] = page
+
+    json = self._FetchUrl(url, parameters=parameters)
+    data = simplejson.loads(json)
+
+    self._CheckForTwitterError(data)
+
+    return [Status.NewFromJsonDict(x) for x in data]
+
+  def GetUserByEmail(self, email):
+    '''Returns a single user by email address.
+
+    Args:
+      email: The email of the user to retrieve.
+    Returns:
+      A twitter.User instance representing that user
+    '''
+    url = '%s/users/show.json?email=%s' % (self.base_url, email)
+    json = self._FetchUrl(url)
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return User.NewFromJsonDict(data)
+
+  def VerifyCredentials(self):
+    '''Returns a twitter.User instance if the authenticating user is valid.
+
+    Returns: 
+      A twitter.User instance representing that user if the
+      credentials are valid, None otherwise.
+    '''
+    if not self._username:
+      raise TwitterError("Api instance must first be given user credentials.")
+    url = '%s/account/verify_credentials.json' % self.base_url
+    try:
+      json = self._FetchUrl(url, no_cache=True)
+    except urllib2.HTTPError, http_error:
+      if http_error.code == httplib.UNAUTHORIZED:
+        return None
+      else:
+        raise http_error
+    data = simplejson.loads(json)
+    self._CheckForTwitterError(data)
+    return User.NewFromJsonDict(data)
+
+  def SetCredentials(self, username, password):
+    '''Set the username and password for this instance
+
+    Args:
+      username: The twitter username.
+      password: The twitter password.
+    '''
+    self._username = username
+    self._password = password
+
+  def ClearCredentials(self):
+    '''Clear the username and password for this instance
+    '''
+    self._username = None
+    self._password = None
+
+  def SetCache(self, cache):
+    '''Override the default cache.  Set to None to prevent caching.
+
+    Args:
+      cache: an instance that supports the same API as the twitter._FileCache
+    '''
+    if cache == DEFAULT_CACHE:
+      self._cache = _FileCache()
+    else:
+      self._cache = cache
+
+  def SetUrllib(self, urllib):
+    '''Override the default urllib implementation.
+
+    Args:
+      urllib: an instance that supports the same API as the urllib2 module
+    '''
+    self._urllib = urllib
+
+  def SetCacheTimeout(self, cache_timeout):
+    '''Override the default cache timeout.
+
+    Args:
+      cache_timeout: time, in seconds, that responses should be reused.
+    '''
+    self._cache_timeout = cache_timeout
+
+  def SetUserAgent(self, user_agent):
+    '''Override the default user agent
+
+    Args:
+      user_agent: a string that should be send to the server as the User-agent
+    '''
+    self._request_headers['User-Agent'] = user_agent
+
+  def SetXTwitterHeaders(self, client, url, version):
+    '''Set the X-Twitter HTTP headers that will be sent to the server.
+
+    Args:
+      client:
+         The client name as a string.  Will be sent to the server as
+         the 'X-Twitter-Client' header.
+      url:
+         The URL of the meta.xml as a string.  Will be sent to the server
+         as the 'X-Twitter-Client-URL' header.
+      version:
+         The client version as a string.  Will be sent to the server
+         as the 'X-Twitter-Client-Version' header.
+    '''
+    self._request_headers['X-Twitter-Client'] = client
+    self._request_headers['X-Twitter-Client-URL'] = url
+    self._request_headers['X-Twitter-Client-Version'] = version
+
+  def SetSource(self, source):
+    '''Suggest the "from source" value to be displayed on the Twitter web site.
+
+    The value of the 'source' parameter must be first recognized by
+    the Twitter server.  New source values are authorized on a case by
+    case basis by the Twitter development team.
+
+    Args:
+      source:
+        The source name as a string.  Will be sent to the server as
+        the 'source' parameter.
+    '''
+    self._default_params['source'] = source
+
+  def GetRateLimitStatus(self):
+    '''Fetch the rate limit status for the currently authorized user.
+    
+    Returns:
+      A dictionary containing the time the limit will reset (reset_time),
+      the number of remaining hits allowed before the reset (remaining_hits),
+      the number of hits allowed in a 60-minute period (hourly_limit), and the
+      time of the reset in seconds since The Epoch (reset_time_in_seconds).
+    '''
+    url  = '%s/account/rate_limit_status.json' % self.base_url
+    json = self._FetchUrl(url, no_cache=True)
+    data = simplejson.loads(json)
+
+    self._CheckForTwitterError(data)
+
+    return data
+
+  def MaximumHitFrequency(self):
+    '''Determines the minimum number of seconds that a program must wait before
+    hitting the server again without exceeding the rate_limit imposed for the
+    currently authenticated user.
+    
+    Returns:
+      The minimum second interval that a program must use so as to not exceed
+      the rate_limit imposed for the user.
+    '''
+    rate_status = self.GetRateLimitStatus()
+    reset_time  = rate_status.get('reset_time', None)
+    limit       = rate_status.get('remaining_hits', None)
+
+    if reset_time and limit:
+      # put the reset time into a datetime object
+      reset = datetime.datetime(*rfc822.parsedate(reset_time)[:7])
+
+      # find the difference in time between now and the reset time + 1 hour
+      delta = reset + datetime.timedelta(hours=1) - datetime.datetime.utcnow()
+
+      # determine the minimum number of seconds allowed as a regular interval
+      max_frequency = int(delta.seconds / limit)
+
+      # return the number of seconds
+      return max_frequency
+
+    return 0
+
+  def _BuildUrl(self, url, path_elements=None, extra_params=None):
+    # Break url into consituent parts
+    (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url)
+
+    # Add any additional path elements to the path
+    if path_elements:
+      # Filter out the path elements that have a value of None
+      p = [i for i in path_elements if i]
+      if not path.endswith('/'):
+        path += '/'
+      path += '/'.join(p)
+
+    # Add any additional query parameters to the query string
+    if extra_params and len(extra_params) > 0:
+      extra_query = self._EncodeParameters(extra_params)
+      # Add it to the existing query
+      if query:
+        query += '&' + extra_query
+      else:
+        query = extra_query
+
+    # Return the rebuilt URL
+    return urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
+
+  def _InitializeRequestHeaders(self, request_headers):
+    if request_headers:
+      self._request_headers = request_headers
+    else:
+      self._request_headers = {}
+
+  def _InitializeUserAgent(self):
+    user_agent = 'Python-urllib/%s (python-twitter/%s)' % \
+                 (self._urllib.__version__, __version__)
+    self.SetUserAgent(user_agent)
+
+  def _InitializeDefaultParameters(self):
+    self._default_params = {}
+
+  def _AddAuthorizationHeader(self, username, password):
+    if username and password:
+      basic_auth = base64.encodestring('%s:%s' % (username, password))[:-1]
+      self._request_headers['Authorization'] = 'Basic %s' % basic_auth
+
+  def _RemoveAuthorizationHeader(self):
+    if self._request_headers and 'Authorization' in self._request_headers:
+      del self._request_headers['Authorization']
+
+  def _DecompressGzippedResponse(self, response):
+    raw_data = response.read()
+    if response.headers.get('content-encoding', None) == 'gzip':
+      url_data = gzip.GzipFile(fileobj=StringIO.StringIO(raw_data)).read()
+    else:
+      url_data = raw_data
+    return url_data
+
+  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')
+    else:
+      return unicode(s).encode('utf-8')
+
+  def _EncodeParameters(self, parameters):
+    '''Return a string in key=value&key=value form
+
+    Values of None are not included in the output string.
+
+    Args:
+      parameters:
+        A dict of (key, value) tuples, where value is encoded as
+        specified by self._encoding
+    Returns:
+      A URL-encoded string in "key=value&key=value" form
+    '''
+    if parameters is None:
+      return None
+    else:
+      return urllib.urlencode(dict([(k, self._Encode(v)) for k, v in parameters.items() if v is not None]))
+
+  def _EncodePostData(self, post_data):
+    '''Return a string in key=value&key=value form
+
+    Values are assumed to be encoded in the format specified by self._encoding,
+    and are subsequently URL encoded.
+
+    Args:
+      post_data:
+        A dict of (key, value) tuples, where value is encoded as
+        specified by self._encoding
+    Returns:
+      A URL-encoded string in "key=value&key=value" form
+    '''
+    if post_data is None:
+      return None
+    else:
+      return urllib.urlencode(dict([(k, self._Encode(v)) for k, v in post_data.items()]))
+
+  def _CheckForTwitterError(self, data):
+    """Raises a TwitterError if twitter returns an error message.
+
+    Args:
+      data: A python dict created from the Twitter json response
+    Raises:
+      TwitterError wrapping the twitter error message if one exists.
+    """
+    # Twitter errors are relatively unlikely, so it is faster
+    # to check first, rather than try and catch the exception
+    if 'error' in data:
+      raise TwitterError(data['error'])
+
+  def _FetchUrl(self,
+                url,
+                post_data=None,
+                parameters=None,
+                no_cache=None,
+                use_gzip_compression=None):
+    '''Fetch a URL, optionally caching for a specified time.
+
+    Args:
+      url:
+        The URL to retrieve
+      post_data:
+        A dict of (str, unicode) key/value pairs.
+        If set, POST will be used.
+      parameters:
+        A dict whose key/value pairs should encoded and added
+        to the query string. [optional]
+      no_cache:
+        If true, overrides the cache on the current request
+      use_gzip_compression:
+        If True, tells the server to gzip-compress the response.
+        It does not apply to POST requests.
+        Defaults to None, which will get the value to use from
+        the instance variable self._use_gzip [optional]
+
+    Returns:
+      A string containing the body of the response.
+    '''
+    # Build the extra parameters dict
+    extra_params = {}
+    if self._default_params:
+      extra_params.update(self._default_params)
+    if parameters:
+      extra_params.update(parameters)
+
+    # 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)
+
+    if use_gzip_compression is None:
+      use_gzip = self._use_gzip
+    else:
+      use_gzip = use_gzip_compression
+      
+    # Set up compression
+    if use_gzip and not post_data:
+      opener.addheaders.append(('Accept-Encoding', 'gzip'))
+
+    encoded_post_data = self._EncodePostData(post_data)
+
+    # Open and return the URL immediately if we're not going to cache
+    if encoded_post_data or no_cache or not self._cache or not self._cache_timeout:
+      response = opener.open(url, encoded_post_data)
+      url_data = self._DecompressGzippedResponse(response)
+      opener.close()
+    else:
+      # Unique keys are a combination of the url and the username
+      if self._username:
+        key = self._username + ':' + url
+      else:
+        key = url
+
+      # 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:
+        response = opener.open(url, encoded_post_data)
+        url_data = self._DecompressGzippedResponse(response)
+        opener.close()
+        self._cache.Set(key, url_data)
+      else:
+        url_data = self._cache.Get(key)
+
+    # Always return the latest version
+    return url_data
+
+
+class _FileCacheError(Exception):
+  '''Base exception class for FileCache related errors'''
+
+class _FileCache(object):
+
+  DEPTH = 3
+
+  def __init__(self,root_directory=None):
+    self._InitializeRootDirectory(root_directory)
+
+  def Get(self,key):
+    path = self._GetPath(key)
+    if os.path.exists(path):
+      return open(path).read()
+    else:
+      return None
+
+  def Set(self,key,data):
+    path = self._GetPath(key)
+    directory = os.path.dirname(path)
+    if not os.path.exists(directory):
+      os.makedirs(directory)
+    if not os.path.isdir(directory):
+      raise _FileCacheError('%s exists but is not a directory' % directory)
+    temp_fd, temp_path = tempfile.mkstemp()
+    temp_fp = os.fdopen(temp_fd, 'w')
+    temp_fp.write(data)
+    temp_fp.close()
+    if not path.startswith(self._root_directory):
+      raise _FileCacheError('%s does not appear to live under %s' %
+                            (path, self._root_directory))
+    if os.path.exists(path):
+      os.remove(path)
+    os.rename(temp_path, path)
+
+  def Remove(self,key):
+    path = self._GetPath(key)
+    if not path.startswith(self._root_directory):
+      raise _FileCacheError('%s does not appear to live under %s' %
+                            (path, self._root_directory ))
+    if os.path.exists(path):
+      os.remove(path)
+
+  def GetCachedTime(self,key):
+    path = self._GetPath(key)
+    if os.path.exists(path):
+      return os.path.getmtime(path)
+    else:
+      return None
+
+  def _GetUsername(self):
+    '''Attempt to find the username in a cross-platform fashion.'''
+    try:
+      return os.getenv('USER') or \
+             os.getenv('LOGNAME') or \
+             os.getenv('USERNAME') or \
+             os.getlogin() or \
+             'nobody'
+    except (IOError, OSError), e:
+      return 'nobody'
+
+  def _GetTmpCachePath(self):
+    username = self._GetUsername()
+    cache_directory = 'python.cache_' + username
+    return os.path.join(tempfile.gettempdir(), cache_directory)
+
+  def _InitializeRootDirectory(self, root_directory):
+    if not root_directory:
+      root_directory = self._GetTmpCachePath()
+    root_directory = os.path.abspath(root_directory)
+    if not os.path.exists(root_directory):
+      os.mkdir(root_directory)
+    if not os.path.isdir(root_directory):
+      raise _FileCacheError('%s exists but is not a directory' %
+                            root_directory)
+    self._root_directory = root_directory
+
+  def _GetPath(self,key):
+    try:
+        hashed_key = md5(key).hexdigest()
+    except TypeError:
+        hashed_key = md5.new(key).hexdigest()
+        
+    return os.path.join(self._root_directory,
+                        self._GetPrefix(hashed_key),
+                        hashed_key)
+
+  def _GetPrefix(self,hashed_key):
+    return os.path.sep.join(hashed_key[0:_FileCache.DEPTH])

=== added file 'data/icons/hicolor/scalable/apps/backend_identica.png'
Binary files data/icons/hicolor/scalable/apps/backend_identica.png	1970-01-01 00:00:00 +0000 and data/icons/hicolor/scalable/apps/backend_identica.png	2010-08-24 01:51:40 +0000 differ

Follow ups