← Back to team overview

gtg team mailing list archive

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

 

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

Requested reviews:
  Gtg developers (gtg)


Twitter backend (uses oauth for login). Reviewed, tested, documented and ready for merge.

As the other backends, the core part of the backend system must be merged before this.
-- 
https://code.launchpad.net/~gtg-user/gtg/twitter-backend/+merge/33351
Your team Gtg developers is requested to review the proposed merge of lp:~gtg-user/gtg/twitter-backend into lp:gtg.
=== added file 'GTG/backends/backend_twitter.py'
--- GTG/backends/backend_twitter.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/backend_twitter.py	2010-08-23 03:00:57 +0000
@@ -0,0 +1,344 @@
+# -*- 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/>.
+# -----------------------------------------------------------------------------
+
+'''
+Twitter backend: imports direct messages, replies and/or the user timeline.
+Authenticates through OAuth.
+'''
+import os
+import re
+import sys
+import uuid
+import subprocess
+
+#the tweepy library is not packaged for Debian/Ubuntu. Thus, a copy of it is
+# kept in the GTG/backends directory
+sys.path.append("GTG/backends")
+import tweepy as tweepy
+
+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
+
+
+class Backend(PeriodicImportBackend):
+    '''
+    Twitter backend: imports direct messages, replies and/or the user timeline.
+    Authenticates through OAuth.
+    '''
+
+
+    _general_description = { \
+        GenericBackend.BACKEND_NAME: "backend_twitter", \
+        GenericBackend.BACKEND_HUMAN_NAME: _("Twitter"), \
+        GenericBackend.BACKEND_AUTHORS:    ["Luca Invernizzi"], \
+        GenericBackend.BACKEND_TYPE:       GenericBackend.TYPE_IMPORT, \
+        GenericBackend.BACKEND_DESCRIPTION: \
+            _("Imports your twitter  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"),\
+        }
+
+    _static_parameters = { \
+        "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, },
+        }
+    
+    CONSUMER_KEY = "UDRov5YF3ZUinftvVBoeyA"
+    #This is supposed to be secret (because of OAuth), but that's not possible.
+    #A xAuth alternative is possible, but it's enabled on mail request if the 
+    # twitter staff considers your application worthy of such honour.
+    CONSUMER_SECRET = "BApykCPskoZ0g4QpVS7yC7TrZntm87KruSeJwvqTg"
+
+    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/twitter/', "tasks_dict-%s" %\
+                                     self.get_id())
+        self.sync_engine = self._load_pickled_file(self.data_path, \
+                                                   SyncEngine())
+        #loading the parameters for oauth
+        self.auth_path = os.path.join('backends/twitter/', "auth-%s" %\
+                                     self.get_id())
+        self.auth_params = self._load_pickled_file(self.auth_path, None)
+        self.authenticated  = False
+        self.authenticating = False
+
+    def initialize(self):
+        '''
+        See GenericBackend for an explanation of this function.
+        '''
+        super(Backend, self).initialize()
+
+    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)
+
+###############################################################################
+### IMPORTING TWEETS ##########################################################
+###############################################################################
+
+    def do_periodic_import(self):
+        '''
+        See GenericBackend for an explanation of this function.
+        '''
+        #abort if authentication is in progress or hasn't been done (in which
+        # case, start it)
+        self.cancellation_point()
+        if not self.authenticated:
+            if not self.authenticating:
+                self._start_authentication()
+            return
+        #do the import
+        if self._parameters["import-from-direct-messages"]:
+            for tweet in self.api.direct_messages():
+                self._process_tweet(tweet)
+        if self._parameters["import-from-my-tweets"]:
+            for tweet in self.api.user_timeline():
+                self._process_tweet(tweet)
+        if self._parameters["import-from-replies"]:
+            for tweet in self.api.mentions():
+                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.id)
+        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))
+        
+        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
+        '''
+        #adding the sender as a tag
+        #this works only for some messages types (not for the user timeline)
+        user = None
+        try:
+            user = message.user.screen_name
+        except:
+            pass
+        if user:
+            task.add_tag("@" + user)
+
+        #setting title, text and tags
+        text = message.text    
+        #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.id))
+
+    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.text))
+            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 ############################################################
+###############################################################################
+
+    def _start_authentication(self):
+        '''
+        Fist step of authentication: opening the browser with the oauth page
+        '''
+
+        #NOTE: just found out that tweepy works with identi.ca (update:
+        #      currently broken!).
+        #      However, twitter is moving to oauth only authentication, while
+        #      identica uses standard login. For now, I'll keep the backends
+        #      separate, using two different libraries (Invernizzi)
+                #auth = tweepy.BasicAuthHandler(username, password, 
+                #host ='identi.ca', api_root = '/api', 
+                #secure=True)
+        self.auth = tweepy.OAuthHandler(self.CONSUMER_KEY, \
+                                        self.CONSUMER_SECRET)
+        self.cancellation_point()
+        if self.auth_params == None:
+            #no previous contact with the server has been made: no stored
+            # oauth token found
+            self.authenticating = True
+            subprocess.Popen(['xdg-open', self.auth.get_authorization_url()])
+            BackendSignals().interaction_requested(self.get_id(),
+                "You need to authenticate to <b>Twitter</b>. A browser"
+                " is opening with the correct page. When you have "
+                " received a PIN code, press 'Continue'.", \
+                BackendSignals().INTERACTION_TEXT,
+                "on_authentication_step")
+        else:
+            #we have gone through authentication successfully before.
+            self.cancellation_point()
+            try:
+                self.auth.set_access_token(self.auth_params[0],\
+                                       self.auth_params[1])
+            except tweepy.TweepError, e:
+                self._on_auth_error(e)
+                return
+            self.cancellation_point()
+            self._end_authentication()
+
+    def on_authentication_step(self, step_type = "", pin = ""):
+        '''
+        Handles the various steps of authentication. It's the only callback
+        function the UI knows about this backend.
+
+        @param step_type: if "get_ui_dialog_text", returns the text to be put
+                          in the dialog requesting the pin.
+                          if "set_text", the UI is feeding the backend with
+                          the pin the user provided
+        @param pin: contains the pin if step_type == "set_text"
+        '''
+        if step_type == "get_ui_dialog_text":
+            return "PIN request", "Insert the PIN you should have received "\
+                                  "through your web browser here:"
+        elif step_type == "set_text":
+            try:
+                token = self.auth.get_access_token(verifier = pin)
+            except tweepy.TweepError, e:
+                self._on_auth_error(e)
+                return
+            self.auth_params = (token.key, token.secret)
+            self._store_pickled_file(self.auth_path, self.auth_params)
+            self._end_authentication()
+    
+    def _end_authentication(self):
+        '''
+        Last step of authentication. Creates the API objects and starts
+        importing tweets
+        '''
+        self.authenticated = True
+        self.authenticating = False
+        self.api = tweepy.API(auth_handler = self.auth, \
+                              secure = True, \
+                              retry_count = 3)
+        self.cancellation_point()
+        self.start_get_tasks()
+
+    def _on_auth_error(self, exception):
+        '''
+        On authentication error, informs the user.
+
+        @param exception: the Exception object that was raised during
+                          authentication
+        '''
+        if isinstance(exception, tweepy.TweepError):
+            if exception.reason == "HTTP Error 401: Unauthorized":
+                self.auth_params = None
+                self._store_pickled_file(self.auth_path, self.auth_params)
+                self.quit(disable = True)
+                BackendSignals().backend_failed(self.get_id(), \
+                                BackendSignals.ERRNO_AUTHENTICATION)
+
+    def signal_network_down(self):
+        '''
+        If the network is unresponsive, inform the user
+        '''
+        BackendSignals().backend_failed(self.get_id(), \
+                        BackendSignals.ERRNO_NETWORK)

=== added directory 'GTG/backends/tweepy'
=== added file 'GTG/backends/tweepy/__init__.py'
--- GTG/backends/tweepy/__init__.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/__init__.py	2010-08-23 03:00:57 +0000
@@ -0,0 +1,27 @@
+# Tweepy
+# Copyright 2009-2010 Joshua Roesslein
+# See LICENSE for details.
+
+"""
+Tweepy Twitter API library
+"""
+__version__ = '1.7.1'
+__author__ = 'Joshua Roesslein'
+__license__ = 'MIT'
+
+from tweepy.models import Status, User, DirectMessage, Friendship, SavedSearch, SearchResult, ModelFactory
+from tweepy.error import TweepError
+from tweepy.api import API
+from tweepy.cache import Cache, MemoryCache, FileCache
+from tweepy.auth import BasicAuthHandler, OAuthHandler
+from tweepy.streaming import Stream, StreamListener
+from tweepy.cursor import Cursor
+
+# Global, unauthenticated instance of API
+api = API()
+
+def debug(enable=True, level=1):
+
+    import httplib
+    httplib.HTTPConnection.debuglevel = level
+

=== added file 'GTG/backends/tweepy/api.py'
--- GTG/backends/tweepy/api.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/api.py	2010-08-23 03:00:57 +0000
@@ -0,0 +1,735 @@
+# Tweepy
+# Copyright 2009-2010 Joshua Roesslein
+# See LICENSE for details.
+
+import os
+import mimetypes
+
+from tweepy.binder import bind_api
+from tweepy.error import TweepError
+from tweepy.parsers import ModelParser
+from tweepy.utils import list_to_csv
+
+
+class API(object):
+    """Twitter API"""
+
+    def __init__(self, auth_handler=None,
+            host='api.twitter.com', search_host='search.twitter.com',
+             cache=None, secure=False, api_root='/1', search_root='',
+            retry_count=0, retry_delay=0, retry_errors=None,
+            parser=None):
+        self.auth = auth_handler
+        self.host = host
+        self.search_host = search_host
+        self.api_root = api_root
+        self.search_root = search_root
+        self.cache = cache
+        self.secure = secure
+        self.retry_count = retry_count
+        self.retry_delay = retry_delay
+        self.retry_errors = retry_errors
+        self.parser = parser or ModelParser()
+
+    """ statuses/public_timeline """
+    public_timeline = bind_api(
+        path = '/statuses/public_timeline.json',
+        payload_type = 'status', payload_list = True,
+        allowed_param = []
+    )
+
+    """ statuses/home_timeline """
+    home_timeline = bind_api(
+        path = '/statuses/home_timeline.json',
+        payload_type = 'status', payload_list = True,
+        allowed_param = ['since_id', 'max_id', 'count', 'page'],
+        require_auth = True
+    )
+
+    """ statuses/friends_timeline """
+    friends_timeline = bind_api(
+        path = '/statuses/friends_timeline.json',
+        payload_type = 'status', payload_list = True,
+        allowed_param = ['since_id', 'max_id', 'count', 'page'],
+        require_auth = True
+    )
+
+    """ statuses/user_timeline """
+    user_timeline = bind_api(
+        path = '/statuses/user_timeline.json',
+        payload_type = 'status', payload_list = True,
+        allowed_param = ['id', 'user_id', 'screen_name', 'since_id',
+                          'max_id', 'count', 'page']
+    )
+
+    """ statuses/mentions """
+    mentions = bind_api(
+        path = '/statuses/mentions.json',
+        payload_type = 'status', payload_list = True,
+        allowed_param = ['since_id', 'max_id', 'count', 'page'],
+        require_auth = True
+    )
+
+    """/statuses/:id/retweeted_by.format"""
+    retweeted_by = bind_api(
+        path = '/statuses/{id}/retweeted_by.json',
+        payload_type = 'status', payload_list = True,
+        allowed_param = ['id', 'count', 'page'],
+        require_auth = True
+    )
+
+    """/statuses/:id/retweeted_by/ids.format"""
+    retweeted_by_ids = bind_api(
+        path = '/statuses/{id}/retweeted_by/ids.json',
+        payload_type = 'ids',
+        allowed_param = ['id', 'count', 'page'],
+        require_auth = True
+    )
+
+    """ statuses/retweeted_by_me """
+    retweeted_by_me = bind_api(
+        path = '/statuses/retweeted_by_me.json',
+        payload_type = 'status', payload_list = True,
+        allowed_param = ['since_id', 'max_id', 'count', 'page'],
+        require_auth = True
+    )
+
+    """ statuses/retweeted_to_me """
+    retweeted_to_me = bind_api(
+        path = '/statuses/retweeted_to_me.json',
+        payload_type = 'status', payload_list = True,
+        allowed_param = ['since_id', 'max_id', 'count', 'page'],
+        require_auth = True
+    )
+
+    """ statuses/retweets_of_me """
+    retweets_of_me = bind_api(
+        path = '/statuses/retweets_of_me.json',
+        payload_type = 'status', payload_list = True,
+        allowed_param = ['since_id', 'max_id', 'count', 'page'],
+        require_auth = True
+    )
+
+    """ statuses/show """
+    get_status = bind_api(
+        path = '/statuses/show.json',
+        payload_type = 'status',
+        allowed_param = ['id']
+    )
+
+    """ statuses/update """
+    update_status = bind_api(
+        path = '/statuses/update.json',
+        method = 'POST',
+        payload_type = 'status',
+        allowed_param = ['status', 'in_reply_to_status_id', 'lat', 'long', 'source', 'place_id'],
+        require_auth = True
+    )
+
+    """ statuses/destroy """
+    destroy_status = bind_api(
+        path = '/statuses/destroy.json',
+        method = 'DELETE',
+        payload_type = 'status',
+        allowed_param = ['id'],
+        require_auth = True
+    )
+
+    """ statuses/retweet """
+    retweet = bind_api(
+        path = '/statuses/retweet/{id}.json',
+        method = 'POST',
+        payload_type = 'status',
+        allowed_param = ['id'],
+        require_auth = True
+    )
+
+    """ statuses/retweets """
+    retweets = bind_api(
+        path = '/statuses/retweets/{id}.json',
+        payload_type = 'status', payload_list = True,
+        allowed_param = ['id', 'count'],
+        require_auth = True
+    )
+
+    """ users/show """
+    get_user = bind_api(
+        path = '/users/show.json',
+        payload_type = 'user',
+        allowed_param = ['id', 'user_id', 'screen_name']
+    )
+
+    """ Perform bulk look up of users from user ID or screenname """
+    def lookup_users(self, user_ids=None, screen_names=None):
+        return self._lookup_users(list_to_csv(user_ids), list_to_csv(screen_names))
+
+    _lookup_users = bind_api(
+        path = '/users/lookup.json',
+        payload_type = 'user', payload_list = True,
+        allowed_param = ['user_id', 'screen_name'],
+        require_auth = True
+    )
+
+    """ Get the authenticated user """
+    def me(self):
+        return self.get_user(screen_name=self.auth.get_username())
+
+    """ users/search """
+    search_users = bind_api(
+        path = '/users/search.json',
+        payload_type = 'user', payload_list = True,
+        require_auth = True,
+        allowed_param = ['q', 'per_page', 'page']
+    )
+
+    """ statuses/friends """
+    friends = bind_api(
+        path = '/statuses/friends.json',
+        payload_type = 'user', payload_list = True,
+        allowed_param = ['id', 'user_id', 'screen_name', 'page', 'cursor']
+    )
+
+    """ statuses/followers """
+    followers = bind_api(
+        path = '/statuses/followers.json',
+        payload_type = 'user', payload_list = True,
+        allowed_param = ['id', 'user_id', 'screen_name', 'page', 'cursor']
+    )
+
+    """ direct_messages """
+    direct_messages = bind_api(
+        path = '/direct_messages.json',
+        payload_type = 'direct_message', payload_list = True,
+        allowed_param = ['since_id', 'max_id', 'count', 'page'],
+        require_auth = True
+    )
+
+    """ direct_messages/sent """
+    sent_direct_messages = bind_api(
+        path = '/direct_messages/sent.json',
+        payload_type = 'direct_message', payload_list = True,
+        allowed_param = ['since_id', 'max_id', 'count', 'page'],
+        require_auth = True
+    )
+
+    """ direct_messages/new """
+    send_direct_message = bind_api(
+        path = '/direct_messages/new.json',
+        method = 'POST',
+        payload_type = 'direct_message',
+        allowed_param = ['user', 'screen_name', 'user_id', 'text'],
+        require_auth = True
+    )
+
+    """ direct_messages/destroy """
+    destroy_direct_message = bind_api(
+        path = '/direct_messages/destroy.json',
+        method = 'DELETE',
+        payload_type = 'direct_message',
+        allowed_param = ['id'],
+        require_auth = True
+    )
+
+    """ friendships/create """
+    create_friendship = bind_api(
+        path = '/friendships/create.json',
+        method = 'POST',
+        payload_type = 'user',
+        allowed_param = ['id', 'user_id', 'screen_name', 'follow'],
+        require_auth = True
+    )
+
+    """ friendships/destroy """
+    destroy_friendship = bind_api(
+        path = '/friendships/destroy.json',
+        method = 'DELETE',
+        payload_type = 'user',
+        allowed_param = ['id', 'user_id', 'screen_name'],
+        require_auth = True
+    )
+
+    """ friendships/exists """
+    exists_friendship = bind_api(
+        path = '/friendships/exists.json',
+        payload_type = 'json',
+        allowed_param = ['user_a', 'user_b']
+    )
+
+    """ friendships/show """
+    show_friendship = bind_api(
+        path = '/friendships/show.json',
+        payload_type = 'friendship',
+        allowed_param = ['source_id', 'source_screen_name',
+                          'target_id', 'target_screen_name']
+    )
+
+    """ friends/ids """
+    friends_ids = bind_api(
+        path = '/friends/ids.json',
+        payload_type = 'ids',
+        allowed_param = ['id', 'user_id', 'screen_name', 'cursor']
+    )
+
+    """ friendships/incoming """
+    friendships_incoming = bind_api(
+        path = '/friendships/incoming.json',
+        payload_type = 'ids',
+        allowed_param = ['cursor']
+    )
+
+    """ friendships/outgoing"""
+    friendships_outgoing = bind_api(
+        path = '/friendships/outgoing.json',
+        payload_type = 'ids',
+        allowed_param = ['cursor']
+    )
+
+    """ followers/ids """
+    followers_ids = bind_api(
+        path = '/followers/ids.json',
+        payload_type = 'ids',
+        allowed_param = ['id', 'user_id', 'screen_name', 'cursor']
+    )
+
+    """ account/verify_credentials """
+    def verify_credentials(self):
+        try:
+            return bind_api(
+                path = '/account/verify_credentials.json',
+                payload_type = 'user',
+                require_auth = True
+            )(self)
+        except TweepError:
+            return False
+
+    """ account/rate_limit_status """
+    rate_limit_status = bind_api(
+        path = '/account/rate_limit_status.json',
+        payload_type = 'json'
+    )
+
+    """ account/update_delivery_device """
+    set_delivery_device = bind_api(
+        path = '/account/update_delivery_device.json',
+        method = 'POST',
+        allowed_param = ['device'],
+        payload_type = 'user',
+        require_auth = True
+    )
+
+    """ account/update_profile_colors """
+    update_profile_colors = bind_api(
+        path = '/account/update_profile_colors.json',
+        method = 'POST',
+        payload_type = 'user',
+        allowed_param = ['profile_background_color', 'profile_text_color',
+                          'profile_link_color', 'profile_sidebar_fill_color',
+                          'profile_sidebar_border_color'],
+        require_auth = True
+    )
+
+    """ account/update_profile_image """
+    def update_profile_image(self, filename):
+        headers, post_data = API._pack_image(filename, 700)
+        return bind_api(
+            path = '/account/update_profile_image.json',
+            method = 'POST',
+            payload_type = 'user',
+            require_auth = True
+        )(self, post_data=post_data, headers=headers)
+
+    """ account/update_profile_background_image """
+    def update_profile_background_image(self, filename, *args, **kargs):
+        headers, post_data = API._pack_image(filename, 800)
+        bind_api(
+            path = '/account/update_profile_background_image.json',
+            method = 'POST',
+            payload_type = 'user',
+            allowed_param = ['tile'],
+            require_auth = True
+        )(self, post_data=post_data, headers=headers)
+
+    """ account/update_profile """
+    update_profile = bind_api(
+        path = '/account/update_profile.json',
+        method = 'POST',
+        payload_type = 'user',
+        allowed_param = ['name', 'url', 'location', 'description'],
+        require_auth = True
+    )
+
+    """ favorites """
+    favorites = bind_api(
+        path = '/favorites.json',
+        payload_type = 'status', payload_list = True,
+        allowed_param = ['id', 'page']
+    )
+
+    """ favorites/create """
+    create_favorite = bind_api(
+        path = '/favorites/create/{id}.json',
+        method = 'POST',
+        payload_type = 'status',
+        allowed_param = ['id'],
+        require_auth = True
+    )
+
+    """ favorites/destroy """
+    destroy_favorite = bind_api(
+        path = '/favorites/destroy/{id}.json',
+        method = 'DELETE',
+        payload_type = 'status',
+        allowed_param = ['id'],
+        require_auth = True
+    )
+
+    """ notifications/follow """
+    enable_notifications = bind_api(
+        path = '/notifications/follow.json',
+        method = 'POST',
+        payload_type = 'user',
+        allowed_param = ['id', 'user_id', 'screen_name'],
+        require_auth = True
+    )
+
+    """ notifications/leave """
+    disable_notifications = bind_api(
+        path = '/notifications/leave.json',
+        method = 'POST',
+        payload_type = 'user',
+        allowed_param = ['id', 'user_id', 'screen_name'],
+        require_auth = True
+    )
+
+    """ blocks/create """
+    create_block = bind_api(
+        path = '/blocks/create.json',
+        method = 'POST',
+        payload_type = 'user',
+        allowed_param = ['id', 'user_id', 'screen_name'],
+        require_auth = True
+    )
+
+    """ blocks/destroy """
+    destroy_block = bind_api(
+        path = '/blocks/destroy.json',
+        method = 'DELETE',
+        payload_type = 'user',
+        allowed_param = ['id', 'user_id', 'screen_name'],
+        require_auth = True
+    )
+
+    """ blocks/exists """
+    def exists_block(self, *args, **kargs):
+        try:
+            bind_api(
+                path = '/blocks/exists.json',
+                allowed_param = ['id', 'user_id', 'screen_name'],
+                require_auth = True
+            )(self, *args, **kargs)
+        except TweepError:
+            return False
+        return True
+
+    """ blocks/blocking """
+    blocks = bind_api(
+        path = '/blocks/blocking.json',
+        payload_type = 'user', payload_list = True,
+        allowed_param = ['page'],
+        require_auth = True
+    )
+
+    """ blocks/blocking/ids """
+    blocks_ids = bind_api(
+        path = '/blocks/blocking/ids.json',
+        payload_type = 'json',
+        require_auth = True
+    )
+
+    """ report_spam """
+    report_spam = bind_api(
+        path = '/report_spam.json',
+        method = 'POST',
+        payload_type = 'user',
+        allowed_param = ['id', 'user_id', 'screen_name'],
+        require_auth = True
+    )
+
+    """ saved_searches """
+    saved_searches = bind_api(
+        path = '/saved_searches.json',
+        payload_type = 'saved_search', payload_list = True,
+        require_auth = True
+    )
+
+    """ saved_searches/show """
+    get_saved_search = bind_api(
+        path = '/saved_searches/show/{id}.json',
+        payload_type = 'saved_search',
+        allowed_param = ['id'],
+        require_auth = True
+    )
+
+    """ saved_searches/create """
+    create_saved_search = bind_api(
+        path = '/saved_searches/create.json',
+        method = 'POST',
+        payload_type = 'saved_search',
+        allowed_param = ['query'],
+        require_auth = True
+    )
+
+    """ saved_searches/destroy """
+    destroy_saved_search = bind_api(
+        path = '/saved_searches/destroy/{id}.json',
+        method = 'DELETE',
+        payload_type = 'saved_search',
+        allowed_param = ['id'],
+        require_auth = True
+    )
+
+    """ help/test """
+    def test(self):
+        try:
+            bind_api(
+                path = '/help/test.json',
+            )(self)
+        except TweepError:
+            return False
+        return True
+
+    def create_list(self, *args, **kargs):
+        return bind_api(
+            path = '/%s/lists.json' % self.auth.get_username(),
+            method = 'POST',
+            payload_type = 'list',
+            allowed_param = ['name', 'mode', 'description'],
+            require_auth = True
+        )(self, *args, **kargs)
+
+    def destroy_list(self, slug):
+        return bind_api(
+            path = '/%s/lists/%s.json' % (self.auth.get_username(), slug),
+            method = 'DELETE',
+            payload_type = 'list',
+            require_auth = True
+        )(self)
+
+    def update_list(self, slug, *args, **kargs):
+        return bind_api(
+            path = '/%s/lists/%s.json' % (self.auth.get_username(), slug),
+            method = 'POST',
+            payload_type = 'list',
+            allowed_param = ['name', 'mode', 'description'],
+            require_auth = True
+        )(self, *args, **kargs)
+
+    lists = bind_api(
+        path = '/{user}/lists.json',
+        payload_type = 'list', payload_list = True,
+        allowed_param = ['user', 'cursor'],
+        require_auth = True
+    )
+
+    lists_memberships = bind_api(
+        path = '/{user}/lists/memberships.json',
+        payload_type = 'list', payload_list = True,
+        allowed_param = ['user', 'cursor'],
+        require_auth = True
+    )
+
+    lists_subscriptions = bind_api(
+        path = '/{user}/lists/subscriptions.json',
+        payload_type = 'list', payload_list = True,
+        allowed_param = ['user', 'cursor'],
+        require_auth = True
+    )
+
+    list_timeline = bind_api(
+        path = '/{owner}/lists/{slug}/statuses.json',
+        payload_type = 'status', payload_list = True,
+        allowed_param = ['owner', 'slug', 'since_id', 'max_id', 'per_page', 'page']
+    )
+
+    get_list = bind_api(
+        path = '/{owner}/lists/{slug}.json',
+        payload_type = 'list',
+        allowed_param = ['owner', 'slug']
+    )
+
+    def add_list_member(self, slug, *args, **kargs):
+        return bind_api(
+            path = '/%s/%s/members.json' % (self.auth.get_username(), slug),
+            method = 'POST',
+            payload_type = 'list',
+            allowed_param = ['id'],
+            require_auth = True
+        )(self, *args, **kargs)
+
+    def remove_list_member(self, slug, *args, **kargs):
+        return bind_api(
+            path = '/%s/%s/members.json' % (self.auth.get_username(), slug),
+            method = 'DELETE',
+            payload_type = 'list',
+            allowed_param = ['id'],
+            require_auth = True
+        )(self, *args, **kargs)
+
+    list_members = bind_api(
+        path = '/{owner}/{slug}/members.json',
+        payload_type = 'user', payload_list = True,
+        allowed_param = ['owner', 'slug', 'cursor']
+    )
+
+    def is_list_member(self, owner, slug, user_id):
+        try:
+            return bind_api(
+                path = '/%s/%s/members/%s.json' % (owner, slug, user_id),
+                payload_type = 'user'
+            )(self)
+        except TweepError:
+            return False
+
+    subscribe_list = bind_api(
+        path = '/{owner}/{slug}/subscribers.json',
+        method = 'POST',
+        payload_type = 'list',
+        allowed_param = ['owner', 'slug'],
+        require_auth = True
+    )
+
+    unsubscribe_list = bind_api(
+        path = '/{owner}/{slug}/subscribers.json',
+        method = 'DELETE',
+        payload_type = 'list',
+        allowed_param = ['owner', 'slug'],
+        require_auth = True
+    )
+
+    list_subscribers = bind_api(
+        path = '/{owner}/{slug}/subscribers.json',
+        payload_type = 'user', payload_list = True,
+        allowed_param = ['owner', 'slug', 'cursor']
+    )
+
+    def is_subscribed_list(self, owner, slug, user_id):
+        try:
+            return bind_api(
+                path = '/%s/%s/subscribers/%s.json' % (owner, slug, user_id),
+                payload_type = 'user'
+            )(self)
+        except TweepError:
+            return False
+
+    """ trends/available """
+    trends_available = bind_api(
+        path = '/trends/available.json',
+        payload_type = 'json',
+        allowed_param = ['lat', 'long']
+    )
+
+    """ trends/location """
+    trends_location = bind_api(
+        path = '/trends/{woeid}.json',
+        payload_type = 'json',
+        allowed_param = ['woeid']
+    )
+
+    """ search """
+    search = bind_api(
+        search_api = True,
+        path = '/search.json',
+        payload_type = 'search_result', payload_list = True,
+        allowed_param = ['q', 'lang', 'locale', 'rpp', 'page', 'since_id', 'geocode', 'show_user', 'max_id', 'since', 'until', 'result_type']
+    )
+    search.pagination_mode = 'page'
+
+    """ trends """
+    trends = bind_api(
+        path = '/trends.json',
+        payload_type = 'json'
+    )
+
+    """ trends/current """
+    trends_current = bind_api(
+        path = '/trends/current.json',
+        payload_type = 'json',
+        allowed_param = ['exclude']
+    )
+
+    """ trends/daily """
+    trends_daily = bind_api(
+        path = '/trends/daily.json',
+        payload_type = 'json',
+        allowed_param = ['date', 'exclude']
+    )
+
+    """ trends/weekly """
+    trends_weekly = bind_api(
+        path = '/trends/weekly.json',
+        payload_type = 'json',
+        allowed_param = ['date', 'exclude']
+    )
+
+    """ geo/reverse_geocode """
+    reverse_geocode = bind_api(
+        path = '/geo/reverse_geocode.json',
+        payload_type = 'json',
+        allowed_param = ['lat', 'long', 'accuracy', 'granularity', 'max_results']
+    )
+
+    """ geo/nearby_places """
+    nearby_places = bind_api(
+        path = '/geo/nearby_places.json',
+        payload_type = 'json',
+        allowed_param = ['lat', 'long', 'ip', 'accuracy', 'granularity', 'max_results']
+    )
+
+    """ geo/id """
+    geo_id = bind_api(
+        path = '/geo/id/{id}.json',
+        payload_type = 'json',
+        allowed_param = ['id']
+    )
+
+    """ Internal use only """
+    @staticmethod
+    def _pack_image(filename, max_size):
+        """Pack image from file into multipart-formdata post body"""
+        # image must be less than 700kb in size
+        try:
+            if os.path.getsize(filename) > (max_size * 1024):
+                raise TweepError('File is too big, must be less than 700kb.')
+        except os.error, e:
+            raise TweepError('Unable to access file')
+
+        # image must be gif, jpeg, or png
+        file_type = mimetypes.guess_type(filename)
+        if file_type is None:
+            raise TweepError('Could not determine file type')
+        file_type = file_type[0]
+        if file_type not in ['image/gif', 'image/jpeg', 'image/png']:
+            raise TweepError('Invalid file type for image: %s' % file_type)
+
+        # build the mulitpart-formdata body
+        fp = open(filename, 'rb')
+        BOUNDARY = 'Tw3ePy'
+        body = []
+        body.append('--' + BOUNDARY)
+        body.append('Content-Disposition: form-data; name="image"; filename="%s"' % filename)
+        body.append('Content-Type: %s' % file_type)
+        body.append('')
+        body.append(fp.read())
+        body.append('--' + BOUNDARY + '--')
+        body.append('')
+        fp.close()
+        body = '\r\n'.join(body)
+
+        # build headers
+        headers = {
+            'Content-Type': 'multipart/form-data; boundary=Tw3ePy',
+            'Content-Length': len(body)
+        }
+
+        return headers, body
+

=== added file 'GTG/backends/tweepy/auth.py'
--- GTG/backends/tweepy/auth.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/auth.py	2010-08-23 03:00:57 +0000
@@ -0,0 +1,163 @@
+# Tweepy
+# Copyright 2009-2010 Joshua Roesslein
+# See LICENSE for details.
+
+from urllib2 import Request, urlopen
+import base64
+
+from tweepy import oauth
+from tweepy.error import TweepError
+from tweepy.api import API
+
+
+class AuthHandler(object):
+
+    def apply_auth(self, url, method, headers, parameters):
+        """Apply authentication headers to request"""
+        raise NotImplementedError
+
+    def get_username(self):
+        """Return the username of the authenticated user"""
+        raise NotImplementedError
+
+
+class BasicAuthHandler(AuthHandler):
+
+    def __init__(self, username, password):
+        self.username = username
+        self._b64up = base64.b64encode('%s:%s' % (username, password))
+
+    def apply_auth(self, url, method, headers, parameters):
+        headers['Authorization'] = 'Basic %s' % self._b64up
+
+    def get_username(self):
+        return self.username
+
+
+class OAuthHandler(AuthHandler):
+    """OAuth authentication handler"""
+
+    OAUTH_HOST = 'twitter.com'
+    OAUTH_ROOT = '/oauth/'
+
+    def __init__(self, consumer_key, consumer_secret, callback=None, secure=False):
+        self._consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
+        self._sigmethod = oauth.OAuthSignatureMethod_HMAC_SHA1()
+        self.request_token = None
+        self.access_token = None
+        self.callback = callback
+        self.username = None
+        self.secure = secure
+
+    def _get_oauth_url(self, endpoint, secure=False):
+        if self.secure or secure:
+            prefix = 'https://'
+        else:
+            prefix = 'http://'
+
+        return prefix + self.OAUTH_HOST + self.OAUTH_ROOT + endpoint
+
+    def apply_auth(self, url, method, headers, parameters):
+        request = oauth.OAuthRequest.from_consumer_and_token(
+            self._consumer, http_url=url, http_method=method,
+            token=self.access_token, parameters=parameters
+        )
+        request.sign_request(self._sigmethod, self._consumer, self.access_token)
+        headers.update(request.to_header())
+
+    def _get_request_token(self):
+        try:
+            url = self._get_oauth_url('request_token')
+            request = oauth.OAuthRequest.from_consumer_and_token(
+                self._consumer, http_url=url, callback=self.callback
+            )
+            request.sign_request(self._sigmethod, self._consumer, None)
+            resp = urlopen(Request(url, headers=request.to_header()))
+            return oauth.OAuthToken.from_string(resp.read())
+        except Exception, e:
+            raise TweepError(e)
+
+    def set_request_token(self, key, secret):
+        self.request_token = oauth.OAuthToken(key, secret)
+
+    def set_access_token(self, key, secret):
+        self.access_token = oauth.OAuthToken(key, secret)
+
+    def get_authorization_url(self, signin_with_twitter=False):
+        """Get the authorization URL to redirect the user"""
+        try:
+            # get the request token
+            self.request_token = self._get_request_token()
+
+            # build auth request and return as url
+            if signin_with_twitter:
+                url = self._get_oauth_url('authenticate')
+            else:
+                url = self._get_oauth_url('authorize')
+            request = oauth.OAuthRequest.from_token_and_callback(
+                token=self.request_token, http_url=url
+            )
+
+            return request.to_url()
+        except Exception, e:
+            raise TweepError(e)
+
+    def get_access_token(self, verifier=None):
+        """
+        After user has authorized the request token, get access token
+        with user supplied verifier.
+        """
+        try:
+            url = self._get_oauth_url('access_token')
+
+            # build request
+            request = oauth.OAuthRequest.from_consumer_and_token(
+                self._consumer,
+                token=self.request_token, http_url=url,
+                verifier=str(verifier)
+            )
+            request.sign_request(self._sigmethod, self._consumer, self.request_token)
+
+            # send request
+            resp = urlopen(Request(url, headers=request.to_header()))
+            self.access_token = oauth.OAuthToken.from_string(resp.read())
+            return self.access_token
+        except Exception, e:
+            raise TweepError(e)
+
+    def get_xauth_access_token(self, username, password):
+        """
+        Get an access token from an username and password combination.
+        In order to get this working you need to create an app at
+        http://twitter.com/apps, after that send a mail to api@xxxxxxxxxxx
+        and request activation of xAuth for it.
+        """
+        try:
+            url = self._get_oauth_url('access_token', secure=True) # must use HTTPS
+            request = oauth.OAuthRequest.from_consumer_and_token(
+                oauth_consumer=self._consumer,
+                http_method='POST', http_url=url,
+                parameters = {
+		            'x_auth_mode': 'client_auth',
+		            'x_auth_username': username,
+		            'x_auth_password': password
+                }
+            )
+            request.sign_request(self._sigmethod, self._consumer, None)
+
+            resp = urlopen(Request(url, data=request.to_postdata()))
+            self.access_token = oauth.OAuthToken.from_string(resp.read())
+            return self.access_token
+        except Exception, e:
+            raise TweepError(e)
+
+    def get_username(self):
+        if self.username is None:
+            api = API(self)
+            user = api.verify_credentials()
+            if user:
+                self.username = user.screen_name
+            else:
+                raise TweepError("Unable to get username, invalid oauth token!")
+        return self.username
+

=== added file 'GTG/backends/tweepy/binder.py'
--- GTG/backends/tweepy/binder.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/binder.py	2010-08-23 03:00:57 +0000
@@ -0,0 +1,191 @@
+# Tweepy
+# Copyright 2009-2010 Joshua Roesslein
+# See LICENSE for details.
+
+import httplib
+import urllib
+import time
+import re
+
+from tweepy.error import TweepError
+from tweepy.utils import convert_to_utf8_str
+
+re_path_template = re.compile('{\w+}')
+
+
+def bind_api(**config):
+
+    class APIMethod(object):
+
+        path = config['path']
+        payload_type = config.get('payload_type', None)
+        payload_list = config.get('payload_list', False)
+        allowed_param = config.get('allowed_param', [])
+        method = config.get('method', 'GET')
+        require_auth = config.get('require_auth', False)
+        search_api = config.get('search_api', False)
+
+        def __init__(self, api, args, kargs):
+            # If authentication is required and no credentials
+            # are provided, throw an error.
+            if self.require_auth and not api.auth:
+                raise TweepError('Authentication required!')
+
+            self.api = api
+            self.post_data = kargs.pop('post_data', None)
+            self.retry_count = kargs.pop('retry_count', api.retry_count)
+            self.retry_delay = kargs.pop('retry_delay', api.retry_delay)
+            self.retry_errors = kargs.pop('retry_errors', api.retry_errors)
+            self.headers = kargs.pop('headers', {})
+            self.build_parameters(args, kargs)
+
+            # Pick correct URL root to use
+            if self.search_api:
+                self.api_root = api.search_root
+            else:
+                self.api_root = api.api_root
+
+            # Perform any path variable substitution
+            self.build_path()
+
+            if api.secure:
+                self.scheme = 'https://'
+            else:
+                self.scheme = 'http://'
+
+            if self.search_api:
+                self.host = api.search_host
+            else:
+                self.host = api.host
+
+            # Manually set Host header to fix an issue in python 2.5
+            # or older where Host is set including the 443 port.
+            # This causes Twitter to issue 301 redirect.
+            # See Issue http://github.com/joshthecoder/tweepy/issues/#issue/12
+            self.headers['Host'] = self.host
+
+        def build_parameters(self, args, kargs):
+            self.parameters = {}
+            for idx, arg in enumerate(args):
+                if arg is None:
+                    continue
+
+                try:
+                    self.parameters[self.allowed_param[idx]] = convert_to_utf8_str(arg)
+                except IndexError:
+                    raise TweepError('Too many parameters supplied!')
+
+            for k, arg in kargs.items():
+                if arg is None:
+                    continue
+                if k in self.parameters:
+                    raise TweepError('Multiple values for parameter %s supplied!' % k)
+
+                self.parameters[k] = convert_to_utf8_str(arg)
+
+        def build_path(self):
+            for variable in re_path_template.findall(self.path):
+                name = variable.strip('{}')
+
+                if name == 'user' and 'user' not in self.parameters and self.api.auth:
+                    # No 'user' parameter provided, fetch it from Auth instead.
+                    value = self.api.auth.get_username()
+                else:
+                    try:
+                        value = urllib.quote(self.parameters[name])
+                    except KeyError:
+                        raise TweepError('No parameter value found for path variable: %s' % name)
+                    del self.parameters[name]
+
+                self.path = self.path.replace(variable, value)
+
+        def execute(self):
+            # Build the request URL
+            url = self.api_root + self.path
+            if len(self.parameters):
+                url = '%s?%s' % (url, urllib.urlencode(self.parameters))
+
+            # Query the cache if one is available
+            # and this request uses a GET method.
+            if self.api.cache and self.method == 'GET':
+                cache_result = self.api.cache.get(url)
+                # if cache result found and not expired, return it
+                if cache_result:
+                    # must restore api reference
+                    if isinstance(cache_result, list):
+                        for result in cache_result:
+                            result._api = self.api
+                    else:
+                        cache_result._api = self.api
+                    return cache_result
+
+            # Continue attempting request until successful
+            # or maximum number of retries is reached.
+            retries_performed = 0
+            while retries_performed < self.retry_count + 1:
+                # Open connection
+                # FIXME: add timeout
+                if self.api.secure:
+                    conn = httplib.HTTPSConnection(self.host)
+                else:
+                    conn = httplib.HTTPConnection(self.host)
+
+                # Apply authentication
+                if self.api.auth:
+                    self.api.auth.apply_auth(
+                            self.scheme + self.host + url,
+                            self.method, self.headers, self.parameters
+                    )
+
+                # Execute request
+                try:
+                    conn.request(self.method, url, headers=self.headers, body=self.post_data)
+                    resp = conn.getresponse()
+                except Exception, e:
+                    raise TweepError('Failed to send request: %s' % e)
+
+                # Exit request loop if non-retry error code
+                if self.retry_errors:
+                    if resp.status not in self.retry_errors: break
+                else:
+                    if resp.status == 200: break
+
+                # Sleep before retrying request again
+                time.sleep(self.retry_delay)
+                retries_performed += 1
+
+            # If an error was returned, throw an exception
+            self.api.last_response = resp
+            if resp.status != 200:
+                try:
+                    error_msg = self.api.parser.parse_error(resp.read())
+                except Exception:
+                    error_msg = "Twitter error response: status code = %s" % resp.status
+                raise TweepError(error_msg, resp)
+
+            # Parse the response payload
+            result = self.api.parser.parse(self, resp.read())
+
+            conn.close()
+
+            # Store result into cache if one is available.
+            if self.api.cache and self.method == 'GET' and result:
+                self.api.cache.store(url, result)
+
+            return result
+
+
+    def _call(api, *args, **kargs):
+
+        method = APIMethod(api, args, kargs)
+        return method.execute()
+
+
+    # Set pagination mode
+    if 'cursor' in APIMethod.allowed_param:
+        _call.pagination_mode = 'cursor'
+    elif 'page' in APIMethod.allowed_param:
+        _call.pagination_mode = 'page'
+
+    return _call
+

=== added file 'GTG/backends/tweepy/cache.py'
--- GTG/backends/tweepy/cache.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/cache.py	2010-08-23 03:00:57 +0000
@@ -0,0 +1,264 @@
+# Tweepy
+# Copyright 2009-2010 Joshua Roesslein
+# See LICENSE for details.
+
+import time
+import threading
+import os
+import cPickle as pickle
+
+try:
+    import hashlib
+except ImportError:
+    # python 2.4
+    import md5 as hashlib
+
+try:
+    import fcntl
+except ImportError:
+    # Probably on a windows system
+    # TODO: use win32file
+    pass
+
+
+class Cache(object):
+    """Cache interface"""
+
+    def __init__(self, timeout=60):
+        """Initialize the cache
+            timeout: number of seconds to keep a cached entry
+        """
+        self.timeout = timeout
+
+    def store(self, key, value):
+        """Add new record to cache
+            key: entry key
+            value: data of entry
+        """
+        raise NotImplementedError
+
+    def get(self, key, timeout=None):
+        """Get cached entry if exists and not expired
+            key: which entry to get
+            timeout: override timeout with this value [optional]
+        """
+        raise NotImplementedError
+
+    def count(self):
+        """Get count of entries currently stored in cache"""
+        raise NotImplementedError
+
+    def cleanup(self):
+        """Delete any expired entries in cache."""
+        raise NotImplementedError
+
+    def flush(self):
+        """Delete all cached entries"""
+        raise NotImplementedError
+
+
+class MemoryCache(Cache):
+    """In-memory cache"""
+
+    def __init__(self, timeout=60):
+        Cache.__init__(self, timeout)
+        self._entries = {}
+        self.lock = threading.Lock()
+
+    def __getstate__(self):
+        # pickle
+        return {'entries': self._entries, 'timeout': self.timeout}
+
+    def __setstate__(self, state):
+        # unpickle
+        self.lock = threading.Lock()
+        self._entries = state['entries']
+        self.timeout = state['timeout']
+
+    def _is_expired(self, entry, timeout):
+        return timeout > 0 and (time.time() - entry[0]) >= timeout
+
+    def store(self, key, value):
+        self.lock.acquire()
+        self._entries[key] = (time.time(), value)
+        self.lock.release()
+
+    def get(self, key, timeout=None):
+        self.lock.acquire()
+        try:
+            # check to see if we have this key
+            entry = self._entries.get(key)
+            if not entry:
+                # no hit, return nothing
+                return None
+
+            # use provided timeout in arguments if provided
+            # otherwise use the one provided during init.
+            if timeout is None:
+                timeout = self.timeout
+
+            # make sure entry is not expired
+            if self._is_expired(entry, timeout):
+                # entry expired, delete and return nothing
+                del self._entries[key]
+                return None
+
+            # entry found and not expired, return it
+            return entry[1]
+        finally:
+            self.lock.release()
+
+    def count(self):
+        return len(self._entries)
+
+    def cleanup(self):
+        self.lock.acquire()
+        try:
+            for k, v in self._entries.items():
+                if self._is_expired(v, self.timeout):
+                    del self._entries[k]
+        finally:
+            self.lock.release()
+
+    def flush(self):
+        self.lock.acquire()
+        self._entries.clear()
+        self.lock.release()
+
+
+class FileCache(Cache):
+    """File-based cache"""
+
+    # locks used to make cache thread-safe
+    cache_locks = {}
+
+    def __init__(self, cache_dir, timeout=60):
+        Cache.__init__(self, timeout)
+        if os.path.exists(cache_dir) is False:
+            os.mkdir(cache_dir)
+        self.cache_dir = cache_dir
+        if cache_dir in FileCache.cache_locks:
+            self.lock = FileCache.cache_locks[cache_dir]
+        else:
+            self.lock = threading.Lock()
+            FileCache.cache_locks[cache_dir] = self.lock
+
+        if os.name == 'posix':
+            self._lock_file = self._lock_file_posix
+            self._unlock_file = self._unlock_file_posix
+        elif os.name == 'nt':
+            self._lock_file = self._lock_file_win32
+            self._unlock_file = self._unlock_file_win32
+        else:
+            print 'Warning! FileCache locking not supported on this system!'
+            self._lock_file = self._lock_file_dummy
+            self._unlock_file = self._unlock_file_dummy
+
+    def _get_path(self, key):
+        md5 = hashlib.md5()
+        md5.update(key)
+        return os.path.join(self.cache_dir, md5.hexdigest())
+
+    def _lock_file_dummy(self, path, exclusive=True):
+        return None
+
+    def _unlock_file_dummy(self, lock):
+        return
+
+    def _lock_file_posix(self, path, exclusive=True):
+        lock_path = path + '.lock'
+        if exclusive is True:
+            f_lock = open(lock_path, 'w')
+            fcntl.lockf(f_lock, fcntl.LOCK_EX)
+        else:
+            f_lock = open(lock_path, 'r')
+            fcntl.lockf(f_lock, fcntl.LOCK_SH)
+        if os.path.exists(lock_path) is False:
+            f_lock.close()
+            return None
+        return f_lock
+
+    def _unlock_file_posix(self, lock):
+        lock.close()
+
+    def _lock_file_win32(self, path, exclusive=True):
+        # TODO: implement
+        return None
+
+    def _unlock_file_win32(self, lock):
+        # TODO: implement
+        return
+
+    def _delete_file(self, path):
+        os.remove(path)
+        if os.path.exists(path + '.lock'):
+            os.remove(path + '.lock')
+
+    def store(self, key, value):
+        path = self._get_path(key)
+        self.lock.acquire()
+        try:
+            # acquire lock and open file
+            f_lock = self._lock_file(path)
+            datafile = open(path, 'wb')
+
+            # write data
+            pickle.dump((time.time(), value), datafile)
+
+            # close and unlock file
+            datafile.close()
+            self._unlock_file(f_lock)
+        finally:
+            self.lock.release()
+
+    def get(self, key, timeout=None):
+        return self._get(self._get_path(key), timeout)
+
+    def _get(self, path, timeout):
+        if os.path.exists(path) is False:
+            # no record
+            return None
+        self.lock.acquire()
+        try:
+            # acquire lock and open
+            f_lock = self._lock_file(path, False)
+            datafile = open(path, 'rb')
+
+            # read pickled object
+            created_time, value = pickle.load(datafile)
+            datafile.close()
+
+            # check if value is expired
+            if timeout is None:
+                timeout = self.timeout
+            if timeout > 0 and (time.time() - created_time) >= timeout:
+                # expired! delete from cache
+                value = None
+                self._delete_file(path)
+
+            # unlock and return result
+            self._unlock_file(f_lock)
+            return value
+        finally:
+            self.lock.release()
+
+    def count(self):
+        c = 0
+        for entry in os.listdir(self.cache_dir):
+            if entry.endswith('.lock'):
+                continue
+            c += 1
+        return c
+
+    def cleanup(self):
+        for entry in os.listdir(self.cache_dir):
+            if entry.endswith('.lock'):
+                continue
+            self._get(os.path.join(self.cache_dir, entry), None)
+
+    def flush(self):
+        for entry in os.listdir(self.cache_dir):
+            if entry.endswith('.lock'):
+                continue
+            self._delete_file(os.path.join(self.cache_dir, entry))
+

=== added file 'GTG/backends/tweepy/cursor.py'
--- GTG/backends/tweepy/cursor.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/cursor.py	2010-08-23 03:00:57 +0000
@@ -0,0 +1,128 @@
+# Tweepy
+# Copyright 2009-2010 Joshua Roesslein
+# See LICENSE for details.
+
+from tweepy.error import TweepError
+
+class Cursor(object):
+    """Pagination helper class"""
+
+    def __init__(self, method, *args, **kargs):
+        if hasattr(method, 'pagination_mode'):
+            if method.pagination_mode == 'cursor':
+                self.iterator = CursorIterator(method, args, kargs)
+            else:
+                self.iterator = PageIterator(method, args, kargs)
+        else:
+            raise TweepError('This method does not perform pagination')
+
+    def pages(self, limit=0):
+        """Return iterator for pages"""
+        if limit > 0:
+            self.iterator.limit = limit
+        return self.iterator
+
+    def items(self, limit=0):
+        """Return iterator for items in each page"""
+        i = ItemIterator(self.iterator)
+        i.limit = limit
+        return i
+
+class BaseIterator(object):
+
+    def __init__(self, method, args, kargs):
+        self.method = method
+        self.args = args
+        self.kargs = kargs
+        self.limit = 0
+
+    def next(self):
+        raise NotImplementedError
+
+    def prev(self):
+        raise NotImplementedError
+
+    def __iter__(self):
+        return self
+
+class CursorIterator(BaseIterator):
+
+    def __init__(self, method, args, kargs):
+        BaseIterator.__init__(self, method, args, kargs)
+        self.next_cursor = -1
+        self.prev_cursor = 0
+        self.count = 0
+
+    def next(self):
+        if self.next_cursor == 0 or (self.limit and self.count == self.limit):
+            raise StopIteration
+        data, cursors = self.method(
+                cursor=self.next_cursor, *self.args, **self.kargs
+        )
+        self.prev_cursor, self.next_cursor = cursors
+        if len(data) == 0:
+            raise StopIteration
+        self.count += 1
+        return data
+
+    def prev(self):
+        if self.prev_cursor == 0:
+            raise TweepError('Can not page back more, at first page')
+        data, self.next_cursor, self.prev_cursor = self.method(
+                cursor=self.prev_cursor, *self.args, **self.kargs
+        )
+        self.count -= 1
+        return data
+
+class PageIterator(BaseIterator):
+
+    def __init__(self, method, args, kargs):
+        BaseIterator.__init__(self, method, args, kargs)
+        self.current_page = 0
+
+    def next(self):
+        self.current_page += 1
+        items = self.method(page=self.current_page, *self.args, **self.kargs)
+        if len(items) == 0 or (self.limit > 0 and self.current_page > self.limit):
+            raise StopIteration
+        return items
+
+    def prev(self):
+        if (self.current_page == 1):
+            raise TweepError('Can not page back more, at first page')
+        self.current_page -= 1
+        return self.method(page=self.current_page, *self.args, **self.kargs)
+
+class ItemIterator(BaseIterator):
+
+    def __init__(self, page_iterator):
+        self.page_iterator = page_iterator
+        self.limit = 0
+        self.current_page = None
+        self.page_index = -1
+        self.count = 0
+
+    def next(self):
+        if self.limit > 0 and self.count == self.limit:
+            raise StopIteration
+        if self.current_page is None or self.page_index == len(self.current_page) - 1:
+            # Reached end of current page, get the next page...
+            self.current_page = self.page_iterator.next()
+            self.page_index = -1
+        self.page_index += 1
+        self.count += 1
+        return self.current_page[self.page_index]
+
+    def prev(self):
+        if self.current_page is None:
+            raise TweepError('Can not go back more, at first page')
+        if self.page_index == 0:
+            # At the beginning of the current page, move to next...
+            self.current_page = self.page_iterator.prev()
+            self.page_index = len(self.current_page)
+            if self.page_index == 0:
+                raise TweepError('No more items')
+        self.page_index -= 1
+        self.count -= 1
+        return self.current_page[self.page_index]
+

=== added file 'GTG/backends/tweepy/error.py'
--- GTG/backends/tweepy/error.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/error.py	2010-08-23 03:00:57 +0000
@@ -0,0 +1,14 @@
+# Tweepy
+# Copyright 2009-2010 Joshua Roesslein
+# See LICENSE for details.
+
+class TweepError(Exception):
+    """Tweepy exception"""
+
+    def __init__(self, reason, response=None):
+        self.reason = str(reason)
+        self.response = response
+
+    def __str__(self):
+        return self.reason
+

=== added file 'GTG/backends/tweepy/models.py'
--- GTG/backends/tweepy/models.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/models.py	2010-08-23 03:00:57 +0000
@@ -0,0 +1,313 @@
+# Tweepy
+# Copyright 2009-2010 Joshua Roesslein
+# See LICENSE for details.
+
+from tweepy.error import TweepError
+from tweepy.utils import parse_datetime, parse_html_value, parse_a_href, \
+        parse_search_datetime, unescape_html
+
+
+class ResultSet(list):
+    """A list like object that holds results from a Twitter API query."""
+
+
+class Model(object):
+
+    def __init__(self, api=None):
+        self._api = api
+
+    def __getstate__(self):
+        # pickle
+        pickle = dict(self.__dict__)
+        try:
+            del pickle['_api']  # do not pickle the API reference
+        except KeyError:
+            pass
+        return pickle
+
+    @classmethod
+    def parse(cls, api, json):
+        """Parse a JSON object into a model instance."""
+        raise NotImplementedError
+
+    @classmethod
+    def parse_list(cls, api, json_list):
+        """Parse a list of JSON objects into a result set of model instances."""
+        results = ResultSet()
+        for obj in json_list:
+            results.append(cls.parse(api, obj))
+        return results
+
+
+class Status(Model):
+
+    @classmethod
+    def parse(cls, api, json):
+        status = cls(api)
+        for k, v in json.items():
+            if k == 'user':
+                user = User.parse(api, v)
+                setattr(status, 'author', user)
+                setattr(status, 'user', user)  # DEPRECIATED
+            elif k == 'created_at':
+                setattr(status, k, parse_datetime(v))
+            elif k == 'source':
+                if '<' in v:
+                    setattr(status, k, parse_html_value(v))
+                    setattr(status, 'source_url', parse_a_href(v))
+                else:
+                    setattr(status, k, v)
+                    setattr(status, 'source_url', None)
+            elif k == 'retweeted_status':
+                setattr(status, k, Status.parse(api, v))
+            else:
+                setattr(status, k, v)
+        return status
+
+    def destroy(self):
+        return self._api.destroy_status(self.id)
+
+    def retweet(self):
+        return self._api.retweet(self.id)
+
+    def retweets(self):
+        return self._api.retweets(self.id)
+
+    def favorite(self):
+        return self._api.create_favorite(self.id)
+
+
+class User(Model):
+
+    @classmethod
+    def parse(cls, api, json):
+        user = cls(api)
+        for k, v in json.items():
+            if k == 'created_at':
+                setattr(user, k, parse_datetime(v))
+            elif k == 'status':
+                setattr(user, k, Status.parse(api, v))
+            elif k == 'following':
+                # twitter sets this to null if it is false
+                if v is True:
+                    setattr(user, k, True)
+                else:
+                    setattr(user, k, False)
+            else:
+                setattr(user, k, v)
+        return user
+
+    @classmethod
+    def parse_list(cls, api, json_list):
+        if isinstance(json_list, list):
+            item_list = json_list
+        else:
+            item_list = json_list['users']
+
+        results = ResultSet()
+        for obj in item_list:
+            results.append(cls.parse(api, obj))
+        return results
+
+    def timeline(self, **kargs):
+        return self._api.user_timeline(user_id=self.id, **kargs)
+
+    def friends(self, **kargs):
+        return self._api.friends(user_id=self.id, **kargs)
+
+    def followers(self, **kargs):
+        return self._api.followers(user_id=self.id, **kargs)
+
+    def follow(self):
+        self._api.create_friendship(user_id=self.id)
+        self.following = True
+
+    def unfollow(self):
+        self._api.destroy_friendship(user_id=self.id)
+        self.following = False
+
+    def lists_memberships(self, *args, **kargs):
+        return self._api.lists_memberships(user=self.screen_name, *args, **kargs)
+
+    def lists_subscriptions(self, *args, **kargs):
+        return self._api.lists_subscriptions(user=self.screen_name, *args, **kargs)
+
+    def lists(self, *args, **kargs):
+        return self._api.lists(user=self.screen_name, *args, **kargs)
+
+    def followers_ids(self, *args, **kargs):
+        return self._api.followers_ids(user_id=self.id, *args, **kargs)
+
+
+class DirectMessage(Model):
+
+    @classmethod
+    def parse(cls, api, json):
+        dm = cls(api)
+        for k, v in json.items():
+            if k == 'sender' or k == 'recipient':
+                setattr(dm, k, User.parse(api, v))
+            elif k == 'created_at':
+                setattr(dm, k, parse_datetime(v))
+            else:
+                setattr(dm, k, v)
+        return dm
+
+    def destroy(self):
+        return self._api.destroy_direct_message(self.id)
+
+
+class Friendship(Model):
+
+    @classmethod
+    def parse(cls, api, json):
+        relationship = json['relationship']
+
+        # parse source
+        source = cls(api)
+        for k, v in relationship['source'].items():
+            setattr(source, k, v)
+
+        # parse target
+        target = cls(api)
+        for k, v in relationship['target'].items():
+            setattr(target, k, v)
+
+        return source, target
+
+
+class SavedSearch(Model):
+
+    @classmethod
+    def parse(cls, api, json):
+        ss = cls(api)
+        for k, v in json.items():
+            if k == 'created_at':
+                setattr(ss, k, parse_datetime(v))
+            else:
+                setattr(ss, k, v)
+        return ss
+
+    def destroy(self):
+        return self._api.destroy_saved_search(self.id)
+
+
+class SearchResult(Model):
+
+    @classmethod
+    def parse(cls, api, json):
+        result = cls()
+        for k, v in json.items():
+            if k == 'created_at':
+                setattr(result, k, parse_search_datetime(v))
+            elif k == 'source':
+                setattr(result, k, parse_html_value(unescape_html(v)))
+            else:
+                setattr(result, k, v)
+        return result
+
+    @classmethod
+    def parse_list(cls, api, json_list, result_set=None):
+        results = ResultSet()
+        results.max_id = json_list.get('max_id')
+        results.since_id = json_list.get('since_id')
+        results.refresh_url = json_list.get('refresh_url')
+        results.next_page = json_list.get('next_page')
+        results.results_per_page = json_list.get('results_per_page')
+        results.page = json_list.get('page')
+        results.completed_in = json_list.get('completed_in')
+        results.query = json_list.get('query')
+
+        for obj in json_list['results']:
+            results.append(cls.parse(api, obj))
+        return results
+
+
+class List(Model):
+
+    @classmethod
+    def parse(cls, api, json):
+        lst = List(api)
+        for k,v in json.items():
+            if k == 'user':
+                setattr(lst, k, User.parse(api, v))
+            else:
+                setattr(lst, k, v)
+        return lst
+
+    @classmethod
+    def parse_list(cls, api, json_list, result_set=None):
+        results = ResultSet()
+        for obj in json_list['lists']:
+            results.append(cls.parse(api, obj))
+        return results
+
+    def update(self, **kargs):
+        return self._api.update_list(self.slug, **kargs)
+
+    def destroy(self):
+        return self._api.destroy_list(self.slug)
+
+    def timeline(self, **kargs):
+        return self._api.list_timeline(self.user.screen_name, self.slug, **kargs)
+
+    def add_member(self, id):
+        return self._api.add_list_member(self.slug, id)
+
+    def remove_member(self, id):
+        return self._api.remove_list_member(self.slug, id)
+
+    def members(self, **kargs):
+        return self._api.list_members(self.user.screen_name, self.slug, **kargs)
+
+    def is_member(self, id):
+        return self._api.is_list_member(self.user.screen_name, self.slug, id)
+
+    def subscribe(self):
+        return self._api.subscribe_list(self.user.screen_name, self.slug)
+
+    def unsubscribe(self):
+        return self._api.unsubscribe_list(self.user.screen_name, self.slug)
+
+    def subscribers(self, **kargs):
+        return self._api.list_subscribers(self.user.screen_name, self.slug, **kargs)
+
+    def is_subscribed(self, id):
+        return self._api.is_subscribed_list(self.user.screen_name, self.slug, id)
+
+
+class JSONModel(Model):
+
+    @classmethod
+    def parse(cls, api, json):
+        return json
+
+
+class IDModel(Model):
+
+    @classmethod
+    def parse(cls, api, json):
+        if isinstance(json, list):
+            return json
+        else:
+            return json['ids']
+
+
+class ModelFactory(object):
+    """
+    Used by parsers for creating instances
+    of models. You may subclass this factory
+    to add your own extended models.
+    """
+
+    status = Status
+    user = User
+    direct_message = DirectMessage
+    friendship = Friendship
+    saved_search = SavedSearch
+    search_result = SearchResult
+    list = List
+
+    json = JSONModel
+    ids = IDModel
+

=== added file 'GTG/backends/tweepy/oauth.py'
--- GTG/backends/tweepy/oauth.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/oauth.py	2010-08-23 03:00:57 +0000
@@ -0,0 +1,655 @@
+"""
+The MIT License
+
+Copyright (c) 2007 Leah Culver
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import cgi
+import urllib
+import time
+import random
+import urlparse
+import hmac
+import binascii
+
+
+VERSION = '1.0' # Hi Blaine!
+HTTP_METHOD = 'GET'
+SIGNATURE_METHOD = 'PLAINTEXT'
+
+
+class OAuthError(RuntimeError):
+    """Generic exception class."""
+    def __init__(self, message='OAuth error occured.'):
+        self.message = message
+
+def build_authenticate_header(realm=''):
+    """Optional WWW-Authenticate header (401 error)"""
+    return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+def escape(s):
+    """Escape a URL including any /."""
+    return urllib.quote(s, safe='~')
+
+def _utf8_str(s):
+    """Convert unicode to utf-8."""
+    if isinstance(s, unicode):
+        return s.encode("utf-8")
+    else:
+        return str(s)
+
+def generate_timestamp():
+    """Get seconds since epoch (UTC)."""
+    return int(time.time())
+
+def generate_nonce(length=8):
+    """Generate pseudorandom number."""
+    return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+def generate_verifier(length=8):
+    """Generate pseudorandom number."""
+    return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+
+class OAuthConsumer(object):
+    """Consumer of OAuth authentication.
+
+    OAuthConsumer is a data type that represents the identity of the Consumer
+    via its shared secret with the Service Provider.
+
+    """
+    key = None
+    secret = None
+
+    def __init__(self, key, secret):
+        self.key = key
+        self.secret = secret
+
+
+class OAuthToken(object):
+    """OAuthToken is a data type that represents an End User via either an access
+    or request token.
+    
+    key -- the token
+    secret -- the token secret
+
+    """
+    key = None
+    secret = None
+    callback = None
+    callback_confirmed = None
+    verifier = None
+
+    def __init__(self, key, secret):
+        self.key = key
+        self.secret = secret
+
+    def set_callback(self, callback):
+        self.callback = callback
+        self.callback_confirmed = 'true'
+
+    def set_verifier(self, verifier=None):
+        if verifier is not None:
+            self.verifier = verifier
+        else:
+            self.verifier = generate_verifier()
+
+    def get_callback_url(self):
+        if self.callback and self.verifier:
+            # Append the oauth_verifier.
+            parts = urlparse.urlparse(self.callback)
+            scheme, netloc, path, params, query, fragment = parts[:6]
+            if query:
+                query = '%s&oauth_verifier=%s' % (query, self.verifier)
+            else:
+                query = 'oauth_verifier=%s' % self.verifier
+            return urlparse.urlunparse((scheme, netloc, path, params,
+                query, fragment))
+        return self.callback
+
+    def to_string(self):
+        data = {
+            'oauth_token': self.key,
+            'oauth_token_secret': self.secret,
+        }
+        if self.callback_confirmed is not None:
+            data['oauth_callback_confirmed'] = self.callback_confirmed
+        return urllib.urlencode(data)
+ 
+    def from_string(s):
+        """ Returns a token from something like:
+        oauth_token_secret=xxx&oauth_token=xxx
+        """
+        params = cgi.parse_qs(s, keep_blank_values=False)
+        key = params['oauth_token'][0]
+        secret = params['oauth_token_secret'][0]
+        token = OAuthToken(key, secret)
+        try:
+            token.callback_confirmed = params['oauth_callback_confirmed'][0]
+        except KeyError:
+            pass # 1.0, no callback confirmed.
+        return token
+    from_string = staticmethod(from_string)
+
+    def __str__(self):
+        return self.to_string()
+
+
+class OAuthRequest(object):
+    """OAuthRequest represents the request and can be serialized.
+
+    OAuth parameters:
+        - oauth_consumer_key 
+        - oauth_token
+        - oauth_signature_method
+        - oauth_signature 
+        - oauth_timestamp 
+        - oauth_nonce
+        - oauth_version
+        - oauth_verifier
+        ... any additional parameters, as defined by the Service Provider.
+    """
+    parameters = None # OAuth parameters.
+    http_method = HTTP_METHOD
+    http_url = None
+    version = VERSION
+
+    def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
+        self.http_method = http_method
+        self.http_url = http_url
+        self.parameters = parameters or {}
+
+    def set_parameter(self, parameter, value):
+        self.parameters[parameter] = value
+
+    def get_parameter(self, parameter):
+        try:
+            return self.parameters[parameter]
+        except:
+            raise OAuthError('Parameter not found: %s' % parameter)
+
+    def _get_timestamp_nonce(self):
+        return self.get_parameter('oauth_timestamp'), self.get_parameter(
+            'oauth_nonce')
+
+    def get_nonoauth_parameters(self):
+        """Get any non-OAuth parameters."""
+        parameters = {}
+        for k, v in self.parameters.iteritems():
+            # Ignore oauth parameters.
+            if k.find('oauth_') < 0:
+                parameters[k] = v
+        return parameters
+
+    def to_header(self, realm=''):
+        """Serialize as a header for an HTTPAuth request."""
+        auth_header = 'OAuth realm="%s"' % realm
+        # Add the oauth parameters.
+        if self.parameters:
+            for k, v in self.parameters.iteritems():
+                if k[:6] == 'oauth_':
+                    auth_header += ', %s="%s"' % (k, escape(str(v)))
+        return {'Authorization': auth_header}
+
+    def to_postdata(self):
+        """Serialize as post data for a POST request."""
+        return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
+            for k, v in self.parameters.iteritems()])
+
+    def to_url(self):
+        """Serialize as a URL for a GET request."""
+        return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
+
+    def get_normalized_parameters(self):
+        """Return a string that contains the parameters that must be signed."""
+        params = self.parameters
+        try:
+            # Exclude the signature if it exists.
+            del params['oauth_signature']
+        except:
+            pass
+        # Escape key values before sorting.
+        key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
+            for k,v in params.items()]
+        # Sort lexicographically, first after key, then after value.
+        key_values.sort()
+        # Combine key value pairs into a string.
+        return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
+
+    def get_normalized_http_method(self):
+        """Uppercases the http method."""
+        return self.http_method.upper()
+
+    def get_normalized_http_url(self):
+        """Parses the URL and rebuilds it to be scheme://host/path."""
+        parts = urlparse.urlparse(self.http_url)
+        scheme, netloc, path = parts[:3]
+        # Exclude default port numbers.
+        if scheme == 'http' and netloc[-3:] == ':80':
+            netloc = netloc[:-3]
+        elif scheme == 'https' and netloc[-4:] == ':443':
+            netloc = netloc[:-4]
+        return '%s://%s%s' % (scheme, netloc, path)
+
+    def sign_request(self, signature_method, consumer, token):
+        """Set the signature parameter to the result of build_signature."""
+        # Set the signature method.
+        self.set_parameter('oauth_signature_method',
+            signature_method.get_name())
+        # Set the signature.
+        self.set_parameter('oauth_signature',
+            self.build_signature(signature_method, consumer, token))
+
+    def build_signature(self, signature_method, consumer, token):
+        """Calls the build signature method within the signature method."""
+        return signature_method.build_signature(self, consumer, token)
+
+    def from_request(http_method, http_url, headers=None, parameters=None,
+            query_string=None):
+        """Combines multiple parameter sources."""
+        if parameters is None:
+            parameters = {}
+
+        # Headers
+        if headers and 'Authorization' in headers:
+            auth_header = headers['Authorization']
+            # Check that the authorization header is OAuth.
+            if auth_header[:6] == 'OAuth ':
+                auth_header = auth_header[6:]
+                try:
+                    # Get the parameters from the header.
+                    header_params = OAuthRequest._split_header(auth_header)
+                    parameters.update(header_params)
+                except:
+                    raise OAuthError('Unable to parse OAuth parameters from '
+                        'Authorization header.')
+
+        # GET or POST query string.
+        if query_string:
+            query_params = OAuthRequest._split_url_string(query_string)
+            parameters.update(query_params)
+
+        # URL parameters.
+        param_str = urlparse.urlparse(http_url)[4] # query
+        url_params = OAuthRequest._split_url_string(param_str)
+        parameters.update(url_params)
+
+        if parameters:
+            return OAuthRequest(http_method, http_url, parameters)
+
+        return None
+    from_request = staticmethod(from_request)
+
+    def from_consumer_and_token(oauth_consumer, token=None,
+            callback=None, verifier=None, http_method=HTTP_METHOD,
+            http_url=None, parameters=None):
+        if not parameters:
+            parameters = {}
+
+        defaults = {
+            'oauth_consumer_key': oauth_consumer.key,
+            'oauth_timestamp': generate_timestamp(),
+            'oauth_nonce': generate_nonce(),
+            'oauth_version': OAuthRequest.version,
+        }
+
+        defaults.update(parameters)
+        parameters = defaults
+
+        if token:
+            parameters['oauth_token'] = token.key
+            if token.callback:
+                parameters['oauth_callback'] = token.callback
+            # 1.0a support for verifier.
+            if verifier:
+                parameters['oauth_verifier'] = verifier
+        elif callback:
+            # 1.0a support for callback in the request token request.
+            parameters['oauth_callback'] = callback
+
+        return OAuthRequest(http_method, http_url, parameters)
+    from_consumer_and_token = staticmethod(from_consumer_and_token)
+
+    def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
+            http_url=None, parameters=None):
+        if not parameters:
+            parameters = {}
+
+        parameters['oauth_token'] = token.key
+
+        if callback:
+            parameters['oauth_callback'] = callback
+
+        return OAuthRequest(http_method, http_url, parameters)
+    from_token_and_callback = staticmethod(from_token_and_callback)
+
+    def _split_header(header):
+        """Turn Authorization: header into parameters."""
+        params = {}
+        parts = header.split(',')
+        for param in parts:
+            # Ignore realm parameter.
+            if param.find('realm') > -1:
+                continue
+            # Remove whitespace.
+            param = param.strip()
+            # Split key-value.
+            param_parts = param.split('=', 1)
+            # Remove quotes and unescape the value.
+            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
+        return params
+    _split_header = staticmethod(_split_header)
+
+    def _split_url_string(param_str):
+        """Turn URL string into parameters."""
+        parameters = cgi.parse_qs(param_str, keep_blank_values=False)
+        for k, v in parameters.iteritems():
+            parameters[k] = urllib.unquote(v[0])
+        return parameters
+    _split_url_string = staticmethod(_split_url_string)
+
+class OAuthServer(object):
+    """A worker to check the validity of a request against a data store."""
+    timestamp_threshold = 300 # In seconds, five minutes.
+    version = VERSION
+    signature_methods = None
+    data_store = None
+
+    def __init__(self, data_store=None, signature_methods=None):
+        self.data_store = data_store
+        self.signature_methods = signature_methods or {}
+
+    def set_data_store(self, data_store):
+        self.data_store = data_store
+
+    def get_data_store(self):
+        return self.data_store
+
+    def add_signature_method(self, signature_method):
+        self.signature_methods[signature_method.get_name()] = signature_method
+        return self.signature_methods
+
+    def fetch_request_token(self, oauth_request):
+        """Processes a request_token request and returns the
+        request token on success.
+        """
+        try:
+            # Get the request token for authorization.
+            token = self._get_token(oauth_request, 'request')
+        except OAuthError:
+            # No token required for the initial token request.
+            version = self._get_version(oauth_request)
+            consumer = self._get_consumer(oauth_request)
+            try:
+                callback = self.get_callback(oauth_request)
+            except OAuthError:
+                callback = None # 1.0, no callback specified.
+            self._check_signature(oauth_request, consumer, None)
+            # Fetch a new token.
+            token = self.data_store.fetch_request_token(consumer, callback)
+        return token
+
+    def fetch_access_token(self, oauth_request):
+        """Processes an access_token request and returns the
+        access token on success.
+        """
+        version = self._get_version(oauth_request)
+        consumer = self._get_consumer(oauth_request)
+        try:
+            verifier = self._get_verifier(oauth_request)
+        except OAuthError:
+            verifier = None
+        # Get the request token.
+        token = self._get_token(oauth_request, 'request')
+        self._check_signature(oauth_request, consumer, token)
+        new_token = self.data_store.fetch_access_token(consumer, token, verifier)
+        return new_token
+
+    def verify_request(self, oauth_request):
+        """Verifies an api call and checks all the parameters."""
+        # -> consumer and token
+        version = self._get_version(oauth_request)
+        consumer = self._get_consumer(oauth_request)
+        # Get the access token.
+        token = self._get_token(oauth_request, 'access')
+        self._check_signature(oauth_request, consumer, token)
+        parameters = oauth_request.get_nonoauth_parameters()
+        return consumer, token, parameters
+
+    def authorize_token(self, token, user):
+        """Authorize a request token."""
+        return self.data_store.authorize_request_token(token, user)
+
+    def get_callback(self, oauth_request):
+        """Get the callback URL."""
+        return oauth_request.get_parameter('oauth_callback')
+ 
+    def build_authenticate_header(self, realm=''):
+        """Optional support for the authenticate header."""
+        return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+    def _get_version(self, oauth_request):
+        """Verify the correct version request for this server."""
+        try:
+            version = oauth_request.get_parameter('oauth_version')
+        except:
+            version = VERSION
+        if version and version != self.version:
+            raise OAuthError('OAuth version %s not supported.' % str(version))
+        return version
+
+    def _get_signature_method(self, oauth_request):
+        """Figure out the signature with some defaults."""
+        try:
+            signature_method = oauth_request.get_parameter(
+                'oauth_signature_method')
+        except:
+            signature_method = SIGNATURE_METHOD
+        try:
+            # Get the signature method object.
+            signature_method = self.signature_methods[signature_method]
+        except:
+            signature_method_names = ', '.join(self.signature_methods.keys())
+            raise OAuthError('Signature method %s not supported try one of the '
+                'following: %s' % (signature_method, signature_method_names))
+
+        return signature_method
+
+    def _get_consumer(self, oauth_request):
+        consumer_key = oauth_request.get_parameter('oauth_consumer_key')
+        consumer = self.data_store.lookup_consumer(consumer_key)
+        if not consumer:
+            raise OAuthError('Invalid consumer.')
+        return consumer
+
+    def _get_token(self, oauth_request, token_type='access'):
+        """Try to find the token for the provided request token key."""
+        token_field = oauth_request.get_parameter('oauth_token')
+        token = self.data_store.lookup_token(token_type, token_field)
+        if not token:
+            raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
+        return token
+    
+    def _get_verifier(self, oauth_request):
+        return oauth_request.get_parameter('oauth_verifier')
+
+    def _check_signature(self, oauth_request, consumer, token):
+        timestamp, nonce = oauth_request._get_timestamp_nonce()
+        self._check_timestamp(timestamp)
+        self._check_nonce(consumer, token, nonce)
+        signature_method = self._get_signature_method(oauth_request)
+        try:
+            signature = oauth_request.get_parameter('oauth_signature')
+        except:
+            raise OAuthError('Missing signature.')
+        # Validate the signature.
+        valid_sig = signature_method.check_signature(oauth_request, consumer,
+            token, signature)
+        if not valid_sig:
+            key, base = signature_method.build_signature_base_string(
+                oauth_request, consumer, token)
+            raise OAuthError('Invalid signature. Expected signature base '
+                'string: %s' % base)
+        built = signature_method.build_signature(oauth_request, consumer, token)
+
+    def _check_timestamp(self, timestamp):
+        """Verify that timestamp is recentish."""
+        timestamp = int(timestamp)
+        now = int(time.time())
+        lapsed = abs(now - timestamp)
+        if lapsed > self.timestamp_threshold:
+            raise OAuthError('Expired timestamp: given %d and now %s has a '
+                'greater difference than threshold %d' %
+                (timestamp, now, self.timestamp_threshold))
+
+    def _check_nonce(self, consumer, token, nonce):
+        """Verify that the nonce is uniqueish."""
+        nonce = self.data_store.lookup_nonce(consumer, token, nonce)
+        if nonce:
+            raise OAuthError('Nonce already used: %s' % str(nonce))
+
+
+class OAuthClient(object):
+    """OAuthClient is a worker to attempt to execute a request."""
+    consumer = None
+    token = None
+
+    def __init__(self, oauth_consumer, oauth_token):
+        self.consumer = oauth_consumer
+        self.token = oauth_token
+
+    def get_consumer(self):
+        return self.consumer
+
+    def get_token(self):
+        return self.token
+
+    def fetch_request_token(self, oauth_request):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def fetch_access_token(self, oauth_request):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def access_resource(self, oauth_request):
+        """-> Some protected resource."""
+        raise NotImplementedError
+
+
+class OAuthDataStore(object):
+    """A database abstraction used to lookup consumers and tokens."""
+
+    def lookup_consumer(self, key):
+        """-> OAuthConsumer."""
+        raise NotImplementedError
+
+    def lookup_token(self, oauth_consumer, token_type, token_token):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def fetch_request_token(self, oauth_consumer, oauth_callback):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+    def authorize_request_token(self, oauth_token, user):
+        """-> OAuthToken."""
+        raise NotImplementedError
+
+
+class OAuthSignatureMethod(object):
+    """A strategy class that implements a signature method."""
+    def get_name(self):
+        """-> str."""
+        raise NotImplementedError
+
+    def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
+        """-> str key, str raw."""
+        raise NotImplementedError
+
+    def build_signature(self, oauth_request, oauth_consumer, oauth_token):
+        """-> str."""
+        raise NotImplementedError
+
+    def check_signature(self, oauth_request, consumer, token, signature):
+        built = self.build_signature(oauth_request, consumer, token)
+        return built == signature
+
+
+class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
+
+    def get_name(self):
+        return 'HMAC-SHA1'
+        
+    def build_signature_base_string(self, oauth_request, consumer, token):
+        sig = (
+            escape(oauth_request.get_normalized_http_method()),
+            escape(oauth_request.get_normalized_http_url()),
+            escape(oauth_request.get_normalized_parameters()),
+        )
+
+        key = '%s&' % escape(consumer.secret)
+        if token:
+            key += escape(token.secret)
+        raw = '&'.join(sig)
+        return key, raw
+
+    def build_signature(self, oauth_request, consumer, token):
+        """Builds the base signature string."""
+        key, raw = self.build_signature_base_string(oauth_request, consumer,
+            token)
+
+        # HMAC object.
+        try:
+            import hashlib # 2.5
+            hashed = hmac.new(key, raw, hashlib.sha1)
+        except:
+            import sha # Deprecated
+            hashed = hmac.new(key, raw, sha)
+
+        # Calculate the digest base 64.
+        return binascii.b2a_base64(hashed.digest())[:-1]
+
+
+class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
+
+    def get_name(self):
+        return 'PLAINTEXT'
+
+    def build_signature_base_string(self, oauth_request, consumer, token):
+        """Concatenates the consumer key and secret."""
+        sig = '%s&' % escape(consumer.secret)
+        if token:
+            sig = sig + escape(token.secret)
+        return sig, sig
+
+    def build_signature(self, oauth_request, consumer, token):
+        key, raw = self.build_signature_base_string(oauth_request, consumer,
+            token)
+        return key
\ No newline at end of file

=== added file 'GTG/backends/tweepy/parsers.py'
--- GTG/backends/tweepy/parsers.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/parsers.py	2010-08-23 03:00:57 +0000
@@ -0,0 +1,84 @@
+# Tweepy
+# Copyright 2009-2010 Joshua Roesslein
+# See LICENSE for details.
+
+from tweepy.models import ModelFactory
+from tweepy.utils import import_simplejson
+from tweepy.error import TweepError
+
+
+class Parser(object):
+
+    def parse(self, method, payload):
+        """
+        Parse the response payload and return the result.
+        Returns a tuple that contains the result data and the cursors
+        (or None if not present).
+        """
+        raise NotImplementedError
+
+    def parse_error(self, payload):
+        """
+        Parse the error message from payload.
+        If unable to parse the message, throw an exception
+        and default error message will be used.
+        """
+        raise NotImplementedError
+
+
+class JSONParser(Parser):
+
+    payload_format = 'json'
+
+    def __init__(self):
+        self.json_lib = import_simplejson()
+
+    def parse(self, method, payload):
+        try:
+            json = self.json_lib.loads(payload)
+        except Exception, e:
+            raise TweepError('Failed to parse JSON payload: %s' % e)
+
+        if isinstance(json, dict) and 'previous_cursor' in json and 'next_cursor' in json:
+            cursors = json['previous_cursor'], json['next_cursor']
+            return json, cursors
+        else:
+            return json
+
+    def parse_error(self, payload):
+        error = self.json_lib.loads(payload)
+        if error.has_key('error'):
+            return error['error']
+        else:
+            return error['errors']
+
+
+class ModelParser(JSONParser):
+
+    def __init__(self, model_factory=None):
+        JSONParser.__init__(self)
+        self.model_factory = model_factory or ModelFactory
+
+    def parse(self, method, payload):
+        try:
+            if method.payload_type is None: return
+            model = getattr(self.model_factory, method.payload_type)
+        except AttributeError:
+            raise TweepError('No model for this payload type: %s' % method.payload_type)
+
+        json = JSONParser.parse(self, method, payload)
+        if isinstance(json, tuple):
+            json, cursors = json
+        else:
+            cursors = None
+
+        if method.payload_list:
+            result = model.parse_list(method.api, json)
+        else:
+            result = model.parse(method.api, json)
+
+        if cursors:
+            return result, cursors
+        else:
+            return result
+

=== added file 'GTG/backends/tweepy/streaming.py'
--- GTG/backends/tweepy/streaming.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/streaming.py	2010-08-23 03:00:57 +0000
@@ -0,0 +1,200 @@
+# Tweepy
+# Copyright 2009-2010 Joshua Roesslein
+# See LICENSE for details.
+
+import httplib
+from socket import timeout
+from threading import Thread
+from time import sleep
+import urllib
+
+from tweepy.auth import BasicAuthHandler
+from tweepy.models import Status
+from tweepy.api import API
+from tweepy.error import TweepError
+
+from tweepy.utils import import_simplejson
+json = import_simplejson()
+
+STREAM_VERSION = 1
+
+
+class StreamListener(object):
+
+    def __init__(self, api=None):
+        self.api = api or API()
+
+    def on_data(self, data):
+        """Called when raw data is received from connection.
+
+        Override this method if you wish to manually handle
+        the stream data. Return False to stop stream and close connection.
+        """
+
+        if 'in_reply_to_status_id' in data:
+            status = Status.parse(self.api, json.loads(data))
+            if self.on_status(status) is False:
+                return False
+        elif 'delete' in data:
+            delete = json.loads(data)['delete']['status']
+            if self.on_delete(delete['id'], delete['user_id']) is False:
+                return False
+        elif 'limit' in data:
+            if self.on_limit(json.loads(data)['limit']['track']) is False:
+                return False
+
+    def on_status(self, status):
+        """Called when a new status arrives"""
+        return
+
+    def on_delete(self, status_id, user_id):
+        """Called when a delete notice arrives for a status"""
+        return
+
+    def on_limit(self, track):
+        """Called when a limitation notice arrvies"""
+        return
+
+    def on_error(self, status_code):
+        """Called when a non-200 status code is returned"""
+        return False
+
+    def on_timeout(self):
+        """Called when stream connection times out"""
+        return
+
+
+class Stream(object):
+
+    host = 'stream.twitter.com'
+
+    def __init__(self, username, password, listener, timeout=5.0, retry_count = None,
+                    retry_time = 10.0, snooze_time = 5.0, buffer_size=1500, headers=None):
+        self.auth = BasicAuthHandler(username, password)
+        self.running = False
+        self.timeout = timeout
+        self.retry_count = retry_count
+        self.retry_time = retry_time
+        self.snooze_time = snooze_time
+        self.buffer_size = buffer_size
+        self.listener = listener
+        self.api = API()
+        self.headers = headers or {}
+        self.body = None
+
+    def _run(self):
+        # setup
+        self.auth.apply_auth(None, None, self.headers, None)
+
+        # enter loop
+        error_counter = 0
+        conn = None
+        exception = None
+        while self.running:
+            if self.retry_count and error_counter > self.retry_count:
+                # quit if error count greater than retry count
+                break
+            try:
+                conn = httplib.HTTPConnection(self.host)
+                conn.connect()
+                conn.sock.settimeout(self.timeout)
+                conn.request('POST', self.url, self.body, headers=self.headers)
+                resp = conn.getresponse()
+                if resp.status != 200:
+                    if self.listener.on_error(resp.status) is False:
+                        break
+                    error_counter += 1
+                    sleep(self.retry_time)
+                else:
+                    error_counter = 0
+                    self._read_loop(resp)
+            except timeout:
+                if self.listener.on_timeout() == False:
+                    break
+                if self.running is False:
+                    break
+                conn.close()
+                sleep(self.snooze_time)
+            except Exception, exception:
+                # any other exception is fatal, so kill loop
+                break
+
+        # cleanup
+        self.running = False
+        if conn:
+            conn.close()
+
+        if exception:
+            raise exception
+
+    def _read_loop(self, resp):
+        data = ''
+        while self.running:
+            if resp.isclosed():
+                break
+
+            # read length
+            length = ''
+            while True:
+                c = resp.read(1)
+                if c == '\n':
+                    break
+                length += c
+            length = length.strip()
+            if length.isdigit():
+                length = int(length)
+            else:
+                continue
+
+            # read data and pass into listener
+            data = resp.read(length)
+            if self.listener.on_data(data) is False:
+                self.running = False
+
+    def _start(self, async):
+        self.running = True
+        if async:
+            Thread(target=self._run).start()
+        else:
+            self._run()
+
+    def firehose(self, count=None, async=False):
+        if self.running:
+            raise TweepError('Stream object already connected!')
+        self.url = '/%i/statuses/firehose.json?delimited=length' % STREAM_VERSION
+        if count:
+            self.url += '&count=%s' % count
+        self._start(async)
+
+    def retweet(self, async=False):
+        if self.running:
+            raise TweepError('Stream object already connected!')
+        self.url = '/%i/statuses/retweet.json?delimited=length' % STREAM_VERSION
+        self._start(async)
+
+    def sample(self, count=None, async=False):
+        if self.running:
+            raise TweepError('Stream object already connected!')
+        self.url = '/%i/statuses/sample.json?delimited=length' % STREAM_VERSION
+        if count:
+            self.url += '&count=%s' % count
+        self._start(async)
+
+    def filter(self, follow=None, track=None, async=False):
+        params = {}
+        self.headers['Content-type'] = "application/x-www-form-urlencoded"
+        if self.running:
+            raise TweepError('Stream object already connected!')
+        self.url = '/%i/statuses/filter.json?delimited=length' % STREAM_VERSION
+        if follow:
+            params['follow'] = ','.join(map(str, follow))
+        if track:
+            params['track'] = ','.join(map(str, track))
+        self.body = urllib.urlencode(params)
+        self._start(async)
+
+    def disconnect(self):
+        if self.running is False:
+            return
+        self.running = False
+

=== added file 'GTG/backends/tweepy/utils.py'
--- GTG/backends/tweepy/utils.py	1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/utils.py	2010-08-23 03:00:57 +0000
@@ -0,0 +1,98 @@
+# Tweepy
+# Copyright 2010 Joshua Roesslein
+# See LICENSE for details.
+
+from datetime import datetime
+import time
+import htmlentitydefs
+import re
+import locale
+
+
+def parse_datetime(string):
+    # Set locale for date parsing
+    locale.setlocale(locale.LC_TIME, 'C')
+
+    # We must parse datetime this way to work in python 2.4
+    date = datetime(*(time.strptime(string, '%a %b %d %H:%M:%S +0000 %Y')[0:6]))
+
+    # Reset locale back to the default setting
+    locale.setlocale(locale.LC_TIME, '')
+    return date
+
+
+def parse_html_value(html):
+
+    return html[html.find('>')+1:html.rfind('<')]
+
+
+def parse_a_href(atag):
+
+    start = atag.find('"') + 1
+    end = atag.find('"', start)
+    return atag[start:end]
+
+
+def parse_search_datetime(string):
+    # Set locale for date parsing
+    locale.setlocale(locale.LC_TIME, 'C')
+
+    # We must parse datetime this way to work in python 2.4
+    date = datetime(*(time.strptime(string, '%a, %d %b %Y %H:%M:%S +0000')[0:6]))
+
+    # Reset locale back to the default setting
+    locale.setlocale(locale.LC_TIME, '')
+    return date
+
+
+def unescape_html(text):
+    """Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)"""
+    def fixup(m):
+        text = m.group(0)
+        if text[:2] == "&#":
+            # character reference
+            try:
+                if text[:3] == "&#x":
+                    return unichr(int(text[3:-1], 16))
+                else:
+                    return unichr(int(text[2:-1]))
+            except ValueError:
+                pass
+        else:
+            # named entity
+            try:
+                text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
+            except KeyError:
+                pass
+        return text # leave as is
+    return re.sub("&#?\w+;", fixup, text)
+
+
+def convert_to_utf8_str(arg):
+    # written by Michael Norton (http://docondev.blogspot.com/)
+    if isinstance(arg, unicode):
+        arg = arg.encode('utf-8')
+    elif not isinstance(arg, str):
+        arg = str(arg)
+    return arg
+
+
+
+def import_simplejson():
+    try:
+        import simplejson as json
+    except ImportError:
+        try:
+            import json  # Python 2.6+
+        except ImportError:
+            try:
+                from django.utils import simplejson as json  # Google App Engine
+            except ImportError:
+                raise ImportError, "Can't load a json library"
+
+    return json
+
+def list_to_csv(item_list):
+    if item_list:
+        return ','.join([str(i) for i in item_list])
+

=== added file 'data/icons/hicolor/scalable/apps/backend_twitter.png'
Binary files data/icons/hicolor/scalable/apps/backend_twitter.png	1970-01-01 00:00:00 +0000 and data/icons/hicolor/scalable/apps/backend_twitter.png	2010-08-23 03:00:57 +0000 differ

Follow ups