GTG

Merge lp:~gtg-user/gtg/twitter-backend into lp:~gtg/gtg/old-trunk

Proposed by Luca Invernizzi
Status: Merged
Merged at revision: 880
Proposed branch: lp:~gtg-user/gtg/twitter-backend
Merge into: lp:~gtg/gtg/old-trunk
Diff against target: 3279 lines (+3210/-0)
13 files modified
GTG/backends/backend_twitter.py (+338/-0)
GTG/backends/tweepy/__init__.py (+27/-0)
GTG/backends/tweepy/api.py (+735/-0)
GTG/backends/tweepy/auth.py (+163/-0)
GTG/backends/tweepy/binder.py (+191/-0)
GTG/backends/tweepy/cache.py (+264/-0)
GTG/backends/tweepy/cursor.py (+128/-0)
GTG/backends/tweepy/error.py (+14/-0)
GTG/backends/tweepy/models.py (+313/-0)
GTG/backends/tweepy/oauth.py (+655/-0)
GTG/backends/tweepy/parsers.py (+84/-0)
GTG/backends/tweepy/streaming.py (+200/-0)
GTG/backends/tweepy/utils.py (+98/-0)
To merge this branch: bzr merge lp:~gtg-user/gtg/twitter-backend
Reviewer Review Type Date Requested Status
Gtg developers Pending
Review via email: mp+33351@code.launchpad.net

Description of the change

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.

To post a comment you must log in.
lp:~gtg-user/gtg/twitter-backend updated
876. By Luca Invernizzi

refactor!

877. By Luca Invernizzi

merge w/ trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'GTG/backends/backend_twitter.py'
2--- GTG/backends/backend_twitter.py 1970-01-01 00:00:00 +0000
3+++ GTG/backends/backend_twitter.py 2010-08-25 16:31:45 +0000
4@@ -0,0 +1,338 @@
5+# -*- coding: utf-8 -*-
6+# -----------------------------------------------------------------------------
7+# Getting Things Gnome! - a personal organizer for the GNOME desktop
8+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
9+#
10+# This program is free software: you can redistribute it and/or modify it under
11+# the terms of the GNU General Public License as published by the Free Software
12+# Foundation, either version 3 of the License, or (at your option) any later
13+# version.
14+#
15+# This program is distributed in the hope that it will be useful, but WITHOUT
16+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
18+# details.
19+#
20+# You should have received a copy of the GNU General Public License along with
21+# this program. If not, see <http://www.gnu.org/licenses/>.
22+# -----------------------------------------------------------------------------
23+
24+'''
25+Twitter backend: imports direct messages, replies and/or the user timeline.
26+Authenticates through OAuth.
27+'''
28+import os
29+import re
30+import sys
31+import uuid
32+import subprocess
33+
34+#the tweepy library is not packaged for Debian/Ubuntu. Thus, a copy of it is
35+# kept in the GTG/backends directory
36+sys.path.append("GTG/backends")
37+import tweepy as tweepy
38+
39+from GTG import _
40+from GTG.backends.genericbackend import GenericBackend
41+from GTG.core import CoreConfig
42+from GTG.backends.backendsignals import BackendSignals
43+from GTG.backends.periodicimportbackend import PeriodicImportBackend
44+from GTG.backends.syncengine import SyncEngine
45+from GTG.tools.logger import Log
46+
47+
48+class Backend(PeriodicImportBackend):
49+ '''
50+ Twitter backend: imports direct messages, replies and/or the user timeline.
51+ Authenticates through OAuth.
52+ '''
53+
54+
55+ _general_description = { \
56+ GenericBackend.BACKEND_NAME: "backend_twitter", \
57+ GenericBackend.BACKEND_HUMAN_NAME: _("Twitter"), \
58+ GenericBackend.BACKEND_AUTHORS: ["Luca Invernizzi"], \
59+ GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_IMPORT, \
60+ GenericBackend.BACKEND_DESCRIPTION: \
61+ _("Imports your twitter messages into your GTG " + \
62+ "tasks. You can choose to either import all your " + \
63+ "messages or just those with a set of hash tags. \n" + \
64+ "The message will be interpreted following this" + \
65+ " format: \n" + \
66+ "<b>my task title, task description #tag @anothertag</b>\n" + \
67+ " Tags can be anywhere in the message"),\
68+ }
69+
70+ _static_parameters = { \
71+ "period": { \
72+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \
73+ GenericBackend.PARAM_DEFAULT_VALUE: 2, },
74+ "import-tags": { \
75+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_LIST_OF_STRINGS, \
76+ GenericBackend.PARAM_DEFAULT_VALUE: ["#todo"], },
77+ "import-from-replies": { \
78+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
79+ GenericBackend.PARAM_DEFAULT_VALUE: False, },
80+ "import-from-my-tweets": { \
81+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
82+ GenericBackend.PARAM_DEFAULT_VALUE: False, },
83+ "import-from-direct-messages": { \
84+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
85+ GenericBackend.PARAM_DEFAULT_VALUE: True, },
86+ }
87+
88+ CONSUMER_KEY = "UDRov5YF3ZUinftvVBoeyA"
89+ #This is supposed to be secret (because of OAuth), but that's not possible.
90+ #A xAuth alternative is possible, but it's enabled on mail request if the
91+ # twitter staff considers your application worthy of such honour.
92+ CONSUMER_SECRET = "BApykCPskoZ0g4QpVS7yC7TrZntm87KruSeJwvqTg"
93+
94+ def __init__(self, parameters):
95+ '''
96+ See GenericBackend for an explanation of this function.
97+ Re-loads the saved state of the synchronization
98+ '''
99+ super(Backend, self).__init__(parameters)
100+ #loading the list of already imported tasks
101+ self.data_path = os.path.join('backends/twitter/', "tasks_dict-%s" %\
102+ self.get_id())
103+ self.sync_engine = self._load_pickled_file(self.data_path, \
104+ SyncEngine())
105+ #loading the parameters for oauth
106+ self.auth_path = os.path.join('backends/twitter/', "auth-%s" %\
107+ self.get_id())
108+ self.auth_params = self._load_pickled_file(self.auth_path, None)
109+ self.authenticated = False
110+ self.authenticating = False
111+
112+ def save_state(self):
113+ '''
114+ See GenericBackend for an explanation of this function.
115+ Saves the state of the synchronization.
116+ '''
117+ self._store_pickled_file(self.data_path, self.sync_engine)
118+
119+###############################################################################
120+### IMPORTING TWEETS ##########################################################
121+###############################################################################
122+
123+ def do_periodic_import(self):
124+ '''
125+ See GenericBackend for an explanation of this function.
126+ '''
127+ #abort if authentication is in progress or hasn't been done (in which
128+ # case, start it)
129+ self.cancellation_point()
130+ if not self.authenticated:
131+ if not self.authenticating:
132+ self._start_authentication()
133+ return
134+ #select what to import
135+ tweets_to_import = []
136+ if self._parameters["import-from-direct-messages"]:
137+ tweets_to_import += self.api.direct_messages()
138+ if self._parameters["import-from-my-tweets"]:
139+ tweets_to_import += self.api.user_timeline()
140+ if self._parameters["import-from-replies"]:
141+ tweets_to_import += self.api.mentions()
142+ #do the import
143+ for tweet in tweets_to_import:
144+ self._process_tweet(tweet)
145+
146+ def _process_tweet(self, tweet):
147+ '''
148+ Given a tweet, checks if a task representing it must be
149+ created in GTG and, if so, it creates it.
150+
151+ @param tweet: a tweet.
152+ '''
153+ self.cancellation_point()
154+ tweet_id = str(tweet.id)
155+ is_syncable = self._is_tweet_syncable(tweet)
156+ #the "lambda" is because we don't consider tweets deletion (to be
157+ # faster)
158+ action, tid = self.sync_engine.analyze_remote_id(\
159+ tweet_id, \
160+ self.datastore.has_task, \
161+ lambda tweet_id: True, \
162+ is_syncable)
163+ Log.debug("processing tweet (%s, %s)" % (action, is_syncable))
164+
165+ self.cancellation_point()
166+ if action == None or action == SyncEngine.UPDATE:
167+ return
168+
169+ elif action == SyncEngine.ADD:
170+ tid = str(uuid.uuid4())
171+ task = self.datastore.task_factory(tid)
172+ self._populate_task(task, tweet)
173+ #we care only to add tweets and if the list of tags which must be
174+ #imported changes (lost-syncability can happen). Thus, we don't
175+ # care about SyncMeme(s)
176+ self.sync_engine.record_relationship(local_id = tid,\
177+ remote_id = tweet_id, \
178+ meme = None)
179+ self.datastore.push_task(task)
180+
181+ elif action == SyncEngine.LOST_SYNCABILITY:
182+ self.sync_engine.break_relationship(remote_id = tweet_id)
183+ self.datastore.request_task_deletion(tid)
184+
185+ self.save_state()
186+
187+
188+ def _populate_task(self, task, message):
189+ '''
190+ Given a twitter message and a GTG task, fills the task with the content
191+ of the message
192+ '''
193+ #adding the sender as a tag
194+ #this works only for some messages types (not for the user timeline)
195+ user = None
196+ try:
197+ user = message.user.screen_name
198+ except:
199+ pass
200+ if user:
201+ task.add_tag("@" + user)
202+
203+ #setting title, text and tags
204+ text = message.text
205+ #convert #hastags to @tags
206+ matches = re.finditer("(?<![^|\s])(#\w+)", text)
207+ for g in matches:
208+ text = text[:g.start()] + '@' + text[g.start() + 1:]
209+ #add tags objects (it's not enough to have @tag in the text to add a
210+ # tag
211+ for tag in self._extract_tags_from_text(text):
212+ task.add_tag(tag)
213+
214+ split_text = text.split(",", 1)
215+ task.set_title(split_text[0])
216+ if len(split_text) > 1:
217+ task.set_text(split_text[1])
218+
219+ task.add_remote_id(self.get_id(), str(message.id))
220+
221+ def _is_tweet_syncable(self, tweet):
222+ '''
223+ Returns True if the given tweet matches the user-specified tags to be
224+ synced
225+
226+ @param tweet: a tweet
227+ '''
228+ if CoreConfig.ALLTASKS_TAG in self._parameters["import-tags"]:
229+ return True
230+ else:
231+ tags = set(Backend._extract_tags_from_text(tweet.text))
232+ return tags.intersection(set(self._parameters["import-tags"])) \
233+ != set()
234+
235+ @staticmethod
236+ def _extract_tags_from_text(text):
237+ '''
238+ Given a string, returns a list of @tags and #hashtags
239+ '''
240+ return list(re.findall(r'(?:^|[\s])((?:#|@)\w+)', text))
241+
242+###############################################################################
243+### AUTHENTICATION ############################################################
244+###############################################################################
245+
246+ def _start_authentication(self):
247+ '''
248+ Fist step of authentication: opening the browser with the oauth page
249+ '''
250+
251+ #NOTE: just found out that tweepy works with identi.ca (update:
252+ # currently broken!).
253+ # However, twitter is moving to oauth only authentication, while
254+ # identica uses standard login. For now, I'll keep the backends
255+ # separate, using two different libraries (Invernizzi)
256+ #auth = tweepy.BasicAuthHandler(username, password,
257+ #host ='identi.ca', api_root = '/api',
258+ #secure=True)
259+ self.auth = tweepy.OAuthHandler(self.CONSUMER_KEY, \
260+ self.CONSUMER_SECRET)
261+ self.cancellation_point()
262+ if self.auth_params == None:
263+ #no previous contact with the server has been made: no stored
264+ # oauth token found
265+ self.authenticating = True
266+ subprocess.Popen(['xdg-open', self.auth.get_authorization_url()])
267+ BackendSignals().interaction_requested(self.get_id(),
268+ "You need to authenticate to <b>Twitter</b>. A browser"
269+ " is opening with the correct page. When you have "
270+ " received a PIN code, press 'Continue'.", \
271+ BackendSignals().INTERACTION_TEXT,
272+ "on_authentication_step")
273+ else:
274+ #we have gone through authentication successfully before.
275+ self.cancellation_point()
276+ try:
277+ self.auth.set_access_token(self.auth_params[0],\
278+ self.auth_params[1])
279+ except tweepy.TweepError, e:
280+ self._on_auth_error(e)
281+ return
282+ self.cancellation_point()
283+ self._end_authentication()
284+
285+ def on_authentication_step(self, step_type = "", pin = ""):
286+ '''
287+ Handles the various steps of authentication. It's the only callback
288+ function the UI knows about this backend.
289+
290+ @param step_type: if "get_ui_dialog_text", returns the text to be put
291+ in the dialog requesting the pin.
292+ if "set_text", the UI is feeding the backend with
293+ the pin the user provided
294+ @param pin: contains the pin if step_type == "set_text"
295+ '''
296+ if step_type == "get_ui_dialog_text":
297+ return "PIN request", "Insert the PIN you should have received "\
298+ "through your web browser here:"
299+ elif step_type == "set_text":
300+ try:
301+ token = self.auth.get_access_token(verifier = pin)
302+ except tweepy.TweepError, e:
303+ self._on_auth_error(e)
304+ return
305+ self.auth_params = (token.key, token.secret)
306+ self._store_pickled_file(self.auth_path, self.auth_params)
307+ self._end_authentication()
308+
309+ def _end_authentication(self):
310+ '''
311+ Last step of authentication. Creates the API objects and starts
312+ importing tweets
313+ '''
314+ self.authenticated = True
315+ self.authenticating = False
316+ self.api = tweepy.API(auth_handler = self.auth, \
317+ secure = True, \
318+ retry_count = 3)
319+ self.cancellation_point()
320+ self.start_get_tasks()
321+
322+ def _on_auth_error(self, exception):
323+ '''
324+ On authentication error, informs the user.
325+
326+ @param exception: the Exception object that was raised during
327+ authentication
328+ '''
329+ if isinstance(exception, tweepy.TweepError):
330+ if exception.reason == "HTTP Error 401: Unauthorized":
331+ self.auth_params = None
332+ self._store_pickled_file(self.auth_path, self.auth_params)
333+ self.quit(disable = True)
334+ BackendSignals().backend_failed(self.get_id(), \
335+ BackendSignals.ERRNO_AUTHENTICATION)
336+
337+ def signal_network_down(self):
338+ '''
339+ If the network is unresponsive, inform the user
340+ '''
341+ BackendSignals().backend_failed(self.get_id(), \
342+ BackendSignals.ERRNO_NETWORK)
343
344=== added directory 'GTG/backends/tweepy'
345=== added file 'GTG/backends/tweepy/__init__.py'
346--- GTG/backends/tweepy/__init__.py 1970-01-01 00:00:00 +0000
347+++ GTG/backends/tweepy/__init__.py 2010-08-25 16:31:45 +0000
348@@ -0,0 +1,27 @@
349+# Tweepy
350+# Copyright 2009-2010 Joshua Roesslein
351+# See LICENSE for details.
352+
353+"""
354+Tweepy Twitter API library
355+"""
356+__version__ = '1.7.1'
357+__author__ = 'Joshua Roesslein'
358+__license__ = 'MIT'
359+
360+from tweepy.models import Status, User, DirectMessage, Friendship, SavedSearch, SearchResult, ModelFactory
361+from tweepy.error import TweepError
362+from tweepy.api import API
363+from tweepy.cache import Cache, MemoryCache, FileCache
364+from tweepy.auth import BasicAuthHandler, OAuthHandler
365+from tweepy.streaming import Stream, StreamListener
366+from tweepy.cursor import Cursor
367+
368+# Global, unauthenticated instance of API
369+api = API()
370+
371+def debug(enable=True, level=1):
372+
373+ import httplib
374+ httplib.HTTPConnection.debuglevel = level
375+
376
377=== added file 'GTG/backends/tweepy/api.py'
378--- GTG/backends/tweepy/api.py 1970-01-01 00:00:00 +0000
379+++ GTG/backends/tweepy/api.py 2010-08-25 16:31:45 +0000
380@@ -0,0 +1,735 @@
381+# Tweepy
382+# Copyright 2009-2010 Joshua Roesslein
383+# See LICENSE for details.
384+
385+import os
386+import mimetypes
387+
388+from tweepy.binder import bind_api
389+from tweepy.error import TweepError
390+from tweepy.parsers import ModelParser
391+from tweepy.utils import list_to_csv
392+
393+
394+class API(object):
395+ """Twitter API"""
396+
397+ def __init__(self, auth_handler=None,
398+ host='api.twitter.com', search_host='search.twitter.com',
399+ cache=None, secure=False, api_root='/1', search_root='',
400+ retry_count=0, retry_delay=0, retry_errors=None,
401+ parser=None):
402+ self.auth = auth_handler
403+ self.host = host
404+ self.search_host = search_host
405+ self.api_root = api_root
406+ self.search_root = search_root
407+ self.cache = cache
408+ self.secure = secure
409+ self.retry_count = retry_count
410+ self.retry_delay = retry_delay
411+ self.retry_errors = retry_errors
412+ self.parser = parser or ModelParser()
413+
414+ """ statuses/public_timeline """
415+ public_timeline = bind_api(
416+ path = '/statuses/public_timeline.json',
417+ payload_type = 'status', payload_list = True,
418+ allowed_param = []
419+ )
420+
421+ """ statuses/home_timeline """
422+ home_timeline = bind_api(
423+ path = '/statuses/home_timeline.json',
424+ payload_type = 'status', payload_list = True,
425+ allowed_param = ['since_id', 'max_id', 'count', 'page'],
426+ require_auth = True
427+ )
428+
429+ """ statuses/friends_timeline """
430+ friends_timeline = bind_api(
431+ path = '/statuses/friends_timeline.json',
432+ payload_type = 'status', payload_list = True,
433+ allowed_param = ['since_id', 'max_id', 'count', 'page'],
434+ require_auth = True
435+ )
436+
437+ """ statuses/user_timeline """
438+ user_timeline = bind_api(
439+ path = '/statuses/user_timeline.json',
440+ payload_type = 'status', payload_list = True,
441+ allowed_param = ['id', 'user_id', 'screen_name', 'since_id',
442+ 'max_id', 'count', 'page']
443+ )
444+
445+ """ statuses/mentions """
446+ mentions = bind_api(
447+ path = '/statuses/mentions.json',
448+ payload_type = 'status', payload_list = True,
449+ allowed_param = ['since_id', 'max_id', 'count', 'page'],
450+ require_auth = True
451+ )
452+
453+ """/statuses/:id/retweeted_by.format"""
454+ retweeted_by = bind_api(
455+ path = '/statuses/{id}/retweeted_by.json',
456+ payload_type = 'status', payload_list = True,
457+ allowed_param = ['id', 'count', 'page'],
458+ require_auth = True
459+ )
460+
461+ """/statuses/:id/retweeted_by/ids.format"""
462+ retweeted_by_ids = bind_api(
463+ path = '/statuses/{id}/retweeted_by/ids.json',
464+ payload_type = 'ids',
465+ allowed_param = ['id', 'count', 'page'],
466+ require_auth = True
467+ )
468+
469+ """ statuses/retweeted_by_me """
470+ retweeted_by_me = bind_api(
471+ path = '/statuses/retweeted_by_me.json',
472+ payload_type = 'status', payload_list = True,
473+ allowed_param = ['since_id', 'max_id', 'count', 'page'],
474+ require_auth = True
475+ )
476+
477+ """ statuses/retweeted_to_me """
478+ retweeted_to_me = bind_api(
479+ path = '/statuses/retweeted_to_me.json',
480+ payload_type = 'status', payload_list = True,
481+ allowed_param = ['since_id', 'max_id', 'count', 'page'],
482+ require_auth = True
483+ )
484+
485+ """ statuses/retweets_of_me """
486+ retweets_of_me = bind_api(
487+ path = '/statuses/retweets_of_me.json',
488+ payload_type = 'status', payload_list = True,
489+ allowed_param = ['since_id', 'max_id', 'count', 'page'],
490+ require_auth = True
491+ )
492+
493+ """ statuses/show """
494+ get_status = bind_api(
495+ path = '/statuses/show.json',
496+ payload_type = 'status',
497+ allowed_param = ['id']
498+ )
499+
500+ """ statuses/update """
501+ update_status = bind_api(
502+ path = '/statuses/update.json',
503+ method = 'POST',
504+ payload_type = 'status',
505+ allowed_param = ['status', 'in_reply_to_status_id', 'lat', 'long', 'source', 'place_id'],
506+ require_auth = True
507+ )
508+
509+ """ statuses/destroy """
510+ destroy_status = bind_api(
511+ path = '/statuses/destroy.json',
512+ method = 'DELETE',
513+ payload_type = 'status',
514+ allowed_param = ['id'],
515+ require_auth = True
516+ )
517+
518+ """ statuses/retweet """
519+ retweet = bind_api(
520+ path = '/statuses/retweet/{id}.json',
521+ method = 'POST',
522+ payload_type = 'status',
523+ allowed_param = ['id'],
524+ require_auth = True
525+ )
526+
527+ """ statuses/retweets """
528+ retweets = bind_api(
529+ path = '/statuses/retweets/{id}.json',
530+ payload_type = 'status', payload_list = True,
531+ allowed_param = ['id', 'count'],
532+ require_auth = True
533+ )
534+
535+ """ users/show """
536+ get_user = bind_api(
537+ path = '/users/show.json',
538+ payload_type = 'user',
539+ allowed_param = ['id', 'user_id', 'screen_name']
540+ )
541+
542+ """ Perform bulk look up of users from user ID or screenname """
543+ def lookup_users(self, user_ids=None, screen_names=None):
544+ return self._lookup_users(list_to_csv(user_ids), list_to_csv(screen_names))
545+
546+ _lookup_users = bind_api(
547+ path = '/users/lookup.json',
548+ payload_type = 'user', payload_list = True,
549+ allowed_param = ['user_id', 'screen_name'],
550+ require_auth = True
551+ )
552+
553+ """ Get the authenticated user """
554+ def me(self):
555+ return self.get_user(screen_name=self.auth.get_username())
556+
557+ """ users/search """
558+ search_users = bind_api(
559+ path = '/users/search.json',
560+ payload_type = 'user', payload_list = True,
561+ require_auth = True,
562+ allowed_param = ['q', 'per_page', 'page']
563+ )
564+
565+ """ statuses/friends """
566+ friends = bind_api(
567+ path = '/statuses/friends.json',
568+ payload_type = 'user', payload_list = True,
569+ allowed_param = ['id', 'user_id', 'screen_name', 'page', 'cursor']
570+ )
571+
572+ """ statuses/followers """
573+ followers = bind_api(
574+ path = '/statuses/followers.json',
575+ payload_type = 'user', payload_list = True,
576+ allowed_param = ['id', 'user_id', 'screen_name', 'page', 'cursor']
577+ )
578+
579+ """ direct_messages """
580+ direct_messages = bind_api(
581+ path = '/direct_messages.json',
582+ payload_type = 'direct_message', payload_list = True,
583+ allowed_param = ['since_id', 'max_id', 'count', 'page'],
584+ require_auth = True
585+ )
586+
587+ """ direct_messages/sent """
588+ sent_direct_messages = bind_api(
589+ path = '/direct_messages/sent.json',
590+ payload_type = 'direct_message', payload_list = True,
591+ allowed_param = ['since_id', 'max_id', 'count', 'page'],
592+ require_auth = True
593+ )
594+
595+ """ direct_messages/new """
596+ send_direct_message = bind_api(
597+ path = '/direct_messages/new.json',
598+ method = 'POST',
599+ payload_type = 'direct_message',
600+ allowed_param = ['user', 'screen_name', 'user_id', 'text'],
601+ require_auth = True
602+ )
603+
604+ """ direct_messages/destroy """
605+ destroy_direct_message = bind_api(
606+ path = '/direct_messages/destroy.json',
607+ method = 'DELETE',
608+ payload_type = 'direct_message',
609+ allowed_param = ['id'],
610+ require_auth = True
611+ )
612+
613+ """ friendships/create """
614+ create_friendship = bind_api(
615+ path = '/friendships/create.json',
616+ method = 'POST',
617+ payload_type = 'user',
618+ allowed_param = ['id', 'user_id', 'screen_name', 'follow'],
619+ require_auth = True
620+ )
621+
622+ """ friendships/destroy """
623+ destroy_friendship = bind_api(
624+ path = '/friendships/destroy.json',
625+ method = 'DELETE',
626+ payload_type = 'user',
627+ allowed_param = ['id', 'user_id', 'screen_name'],
628+ require_auth = True
629+ )
630+
631+ """ friendships/exists """
632+ exists_friendship = bind_api(
633+ path = '/friendships/exists.json',
634+ payload_type = 'json',
635+ allowed_param = ['user_a', 'user_b']
636+ )
637+
638+ """ friendships/show """
639+ show_friendship = bind_api(
640+ path = '/friendships/show.json',
641+ payload_type = 'friendship',
642+ allowed_param = ['source_id', 'source_screen_name',
643+ 'target_id', 'target_screen_name']
644+ )
645+
646+ """ friends/ids """
647+ friends_ids = bind_api(
648+ path = '/friends/ids.json',
649+ payload_type = 'ids',
650+ allowed_param = ['id', 'user_id', 'screen_name', 'cursor']
651+ )
652+
653+ """ friendships/incoming """
654+ friendships_incoming = bind_api(
655+ path = '/friendships/incoming.json',
656+ payload_type = 'ids',
657+ allowed_param = ['cursor']
658+ )
659+
660+ """ friendships/outgoing"""
661+ friendships_outgoing = bind_api(
662+ path = '/friendships/outgoing.json',
663+ payload_type = 'ids',
664+ allowed_param = ['cursor']
665+ )
666+
667+ """ followers/ids """
668+ followers_ids = bind_api(
669+ path = '/followers/ids.json',
670+ payload_type = 'ids',
671+ allowed_param = ['id', 'user_id', 'screen_name', 'cursor']
672+ )
673+
674+ """ account/verify_credentials """
675+ def verify_credentials(self):
676+ try:
677+ return bind_api(
678+ path = '/account/verify_credentials.json',
679+ payload_type = 'user',
680+ require_auth = True
681+ )(self)
682+ except TweepError:
683+ return False
684+
685+ """ account/rate_limit_status """
686+ rate_limit_status = bind_api(
687+ path = '/account/rate_limit_status.json',
688+ payload_type = 'json'
689+ )
690+
691+ """ account/update_delivery_device """
692+ set_delivery_device = bind_api(
693+ path = '/account/update_delivery_device.json',
694+ method = 'POST',
695+ allowed_param = ['device'],
696+ payload_type = 'user',
697+ require_auth = True
698+ )
699+
700+ """ account/update_profile_colors """
701+ update_profile_colors = bind_api(
702+ path = '/account/update_profile_colors.json',
703+ method = 'POST',
704+ payload_type = 'user',
705+ allowed_param = ['profile_background_color', 'profile_text_color',
706+ 'profile_link_color', 'profile_sidebar_fill_color',
707+ 'profile_sidebar_border_color'],
708+ require_auth = True
709+ )
710+
711+ """ account/update_profile_image """
712+ def update_profile_image(self, filename):
713+ headers, post_data = API._pack_image(filename, 700)
714+ return bind_api(
715+ path = '/account/update_profile_image.json',
716+ method = 'POST',
717+ payload_type = 'user',
718+ require_auth = True
719+ )(self, post_data=post_data, headers=headers)
720+
721+ """ account/update_profile_background_image """
722+ def update_profile_background_image(self, filename, *args, **kargs):
723+ headers, post_data = API._pack_image(filename, 800)
724+ bind_api(
725+ path = '/account/update_profile_background_image.json',
726+ method = 'POST',
727+ payload_type = 'user',
728+ allowed_param = ['tile'],
729+ require_auth = True
730+ )(self, post_data=post_data, headers=headers)
731+
732+ """ account/update_profile """
733+ update_profile = bind_api(
734+ path = '/account/update_profile.json',
735+ method = 'POST',
736+ payload_type = 'user',
737+ allowed_param = ['name', 'url', 'location', 'description'],
738+ require_auth = True
739+ )
740+
741+ """ favorites """
742+ favorites = bind_api(
743+ path = '/favorites.json',
744+ payload_type = 'status', payload_list = True,
745+ allowed_param = ['id', 'page']
746+ )
747+
748+ """ favorites/create """
749+ create_favorite = bind_api(
750+ path = '/favorites/create/{id}.json',
751+ method = 'POST',
752+ payload_type = 'status',
753+ allowed_param = ['id'],
754+ require_auth = True
755+ )
756+
757+ """ favorites/destroy """
758+ destroy_favorite = bind_api(
759+ path = '/favorites/destroy/{id}.json',
760+ method = 'DELETE',
761+ payload_type = 'status',
762+ allowed_param = ['id'],
763+ require_auth = True
764+ )
765+
766+ """ notifications/follow """
767+ enable_notifications = bind_api(
768+ path = '/notifications/follow.json',
769+ method = 'POST',
770+ payload_type = 'user',
771+ allowed_param = ['id', 'user_id', 'screen_name'],
772+ require_auth = True
773+ )
774+
775+ """ notifications/leave """
776+ disable_notifications = bind_api(
777+ path = '/notifications/leave.json',
778+ method = 'POST',
779+ payload_type = 'user',
780+ allowed_param = ['id', 'user_id', 'screen_name'],
781+ require_auth = True
782+ )
783+
784+ """ blocks/create """
785+ create_block = bind_api(
786+ path = '/blocks/create.json',
787+ method = 'POST',
788+ payload_type = 'user',
789+ allowed_param = ['id', 'user_id', 'screen_name'],
790+ require_auth = True
791+ )
792+
793+ """ blocks/destroy """
794+ destroy_block = bind_api(
795+ path = '/blocks/destroy.json',
796+ method = 'DELETE',
797+ payload_type = 'user',
798+ allowed_param = ['id', 'user_id', 'screen_name'],
799+ require_auth = True
800+ )
801+
802+ """ blocks/exists """
803+ def exists_block(self, *args, **kargs):
804+ try:
805+ bind_api(
806+ path = '/blocks/exists.json',
807+ allowed_param = ['id', 'user_id', 'screen_name'],
808+ require_auth = True
809+ )(self, *args, **kargs)
810+ except TweepError:
811+ return False
812+ return True
813+
814+ """ blocks/blocking """
815+ blocks = bind_api(
816+ path = '/blocks/blocking.json',
817+ payload_type = 'user', payload_list = True,
818+ allowed_param = ['page'],
819+ require_auth = True
820+ )
821+
822+ """ blocks/blocking/ids """
823+ blocks_ids = bind_api(
824+ path = '/blocks/blocking/ids.json',
825+ payload_type = 'json',
826+ require_auth = True
827+ )
828+
829+ """ report_spam """
830+ report_spam = bind_api(
831+ path = '/report_spam.json',
832+ method = 'POST',
833+ payload_type = 'user',
834+ allowed_param = ['id', 'user_id', 'screen_name'],
835+ require_auth = True
836+ )
837+
838+ """ saved_searches """
839+ saved_searches = bind_api(
840+ path = '/saved_searches.json',
841+ payload_type = 'saved_search', payload_list = True,
842+ require_auth = True
843+ )
844+
845+ """ saved_searches/show """
846+ get_saved_search = bind_api(
847+ path = '/saved_searches/show/{id}.json',
848+ payload_type = 'saved_search',
849+ allowed_param = ['id'],
850+ require_auth = True
851+ )
852+
853+ """ saved_searches/create """
854+ create_saved_search = bind_api(
855+ path = '/saved_searches/create.json',
856+ method = 'POST',
857+ payload_type = 'saved_search',
858+ allowed_param = ['query'],
859+ require_auth = True
860+ )
861+
862+ """ saved_searches/destroy """
863+ destroy_saved_search = bind_api(
864+ path = '/saved_searches/destroy/{id}.json',
865+ method = 'DELETE',
866+ payload_type = 'saved_search',
867+ allowed_param = ['id'],
868+ require_auth = True
869+ )
870+
871+ """ help/test """
872+ def test(self):
873+ try:
874+ bind_api(
875+ path = '/help/test.json',
876+ )(self)
877+ except TweepError:
878+ return False
879+ return True
880+
881+ def create_list(self, *args, **kargs):
882+ return bind_api(
883+ path = '/%s/lists.json' % self.auth.get_username(),
884+ method = 'POST',
885+ payload_type = 'list',
886+ allowed_param = ['name', 'mode', 'description'],
887+ require_auth = True
888+ )(self, *args, **kargs)
889+
890+ def destroy_list(self, slug):
891+ return bind_api(
892+ path = '/%s/lists/%s.json' % (self.auth.get_username(), slug),
893+ method = 'DELETE',
894+ payload_type = 'list',
895+ require_auth = True
896+ )(self)
897+
898+ def update_list(self, slug, *args, **kargs):
899+ return bind_api(
900+ path = '/%s/lists/%s.json' % (self.auth.get_username(), slug),
901+ method = 'POST',
902+ payload_type = 'list',
903+ allowed_param = ['name', 'mode', 'description'],
904+ require_auth = True
905+ )(self, *args, **kargs)
906+
907+ lists = bind_api(
908+ path = '/{user}/lists.json',
909+ payload_type = 'list', payload_list = True,
910+ allowed_param = ['user', 'cursor'],
911+ require_auth = True
912+ )
913+
914+ lists_memberships = bind_api(
915+ path = '/{user}/lists/memberships.json',
916+ payload_type = 'list', payload_list = True,
917+ allowed_param = ['user', 'cursor'],
918+ require_auth = True
919+ )
920+
921+ lists_subscriptions = bind_api(
922+ path = '/{user}/lists/subscriptions.json',
923+ payload_type = 'list', payload_list = True,
924+ allowed_param = ['user', 'cursor'],
925+ require_auth = True
926+ )
927+
928+ list_timeline = bind_api(
929+ path = '/{owner}/lists/{slug}/statuses.json',
930+ payload_type = 'status', payload_list = True,
931+ allowed_param = ['owner', 'slug', 'since_id', 'max_id', 'per_page', 'page']
932+ )
933+
934+ get_list = bind_api(
935+ path = '/{owner}/lists/{slug}.json',
936+ payload_type = 'list',
937+ allowed_param = ['owner', 'slug']
938+ )
939+
940+ def add_list_member(self, slug, *args, **kargs):
941+ return bind_api(
942+ path = '/%s/%s/members.json' % (self.auth.get_username(), slug),
943+ method = 'POST',
944+ payload_type = 'list',
945+ allowed_param = ['id'],
946+ require_auth = True
947+ )(self, *args, **kargs)
948+
949+ def remove_list_member(self, slug, *args, **kargs):
950+ return bind_api(
951+ path = '/%s/%s/members.json' % (self.auth.get_username(), slug),
952+ method = 'DELETE',
953+ payload_type = 'list',
954+ allowed_param = ['id'],
955+ require_auth = True
956+ )(self, *args, **kargs)
957+
958+ list_members = bind_api(
959+ path = '/{owner}/{slug}/members.json',
960+ payload_type = 'user', payload_list = True,
961+ allowed_param = ['owner', 'slug', 'cursor']
962+ )
963+
964+ def is_list_member(self, owner, slug, user_id):
965+ try:
966+ return bind_api(
967+ path = '/%s/%s/members/%s.json' % (owner, slug, user_id),
968+ payload_type = 'user'
969+ )(self)
970+ except TweepError:
971+ return False
972+
973+ subscribe_list = bind_api(
974+ path = '/{owner}/{slug}/subscribers.json',
975+ method = 'POST',
976+ payload_type = 'list',
977+ allowed_param = ['owner', 'slug'],
978+ require_auth = True
979+ )
980+
981+ unsubscribe_list = bind_api(
982+ path = '/{owner}/{slug}/subscribers.json',
983+ method = 'DELETE',
984+ payload_type = 'list',
985+ allowed_param = ['owner', 'slug'],
986+ require_auth = True
987+ )
988+
989+ list_subscribers = bind_api(
990+ path = '/{owner}/{slug}/subscribers.json',
991+ payload_type = 'user', payload_list = True,
992+ allowed_param = ['owner', 'slug', 'cursor']
993+ )
994+
995+ def is_subscribed_list(self, owner, slug, user_id):
996+ try:
997+ return bind_api(
998+ path = '/%s/%s/subscribers/%s.json' % (owner, slug, user_id),
999+ payload_type = 'user'
1000+ )(self)
1001+ except TweepError:
1002+ return False
1003+
1004+ """ trends/available """
1005+ trends_available = bind_api(
1006+ path = '/trends/available.json',
1007+ payload_type = 'json',
1008+ allowed_param = ['lat', 'long']
1009+ )
1010+
1011+ """ trends/location """
1012+ trends_location = bind_api(
1013+ path = '/trends/{woeid}.json',
1014+ payload_type = 'json',
1015+ allowed_param = ['woeid']
1016+ )
1017+
1018+ """ search """
1019+ search = bind_api(
1020+ search_api = True,
1021+ path = '/search.json',
1022+ payload_type = 'search_result', payload_list = True,
1023+ allowed_param = ['q', 'lang', 'locale', 'rpp', 'page', 'since_id', 'geocode', 'show_user', 'max_id', 'since', 'until', 'result_type']
1024+ )
1025+ search.pagination_mode = 'page'
1026+
1027+ """ trends """
1028+ trends = bind_api(
1029+ path = '/trends.json',
1030+ payload_type = 'json'
1031+ )
1032+
1033+ """ trends/current """
1034+ trends_current = bind_api(
1035+ path = '/trends/current.json',
1036+ payload_type = 'json',
1037+ allowed_param = ['exclude']
1038+ )
1039+
1040+ """ trends/daily """
1041+ trends_daily = bind_api(
1042+ path = '/trends/daily.json',
1043+ payload_type = 'json',
1044+ allowed_param = ['date', 'exclude']
1045+ )
1046+
1047+ """ trends/weekly """
1048+ trends_weekly = bind_api(
1049+ path = '/trends/weekly.json',
1050+ payload_type = 'json',
1051+ allowed_param = ['date', 'exclude']
1052+ )
1053+
1054+ """ geo/reverse_geocode """
1055+ reverse_geocode = bind_api(
1056+ path = '/geo/reverse_geocode.json',
1057+ payload_type = 'json',
1058+ allowed_param = ['lat', 'long', 'accuracy', 'granularity', 'max_results']
1059+ )
1060+
1061+ """ geo/nearby_places """
1062+ nearby_places = bind_api(
1063+ path = '/geo/nearby_places.json',
1064+ payload_type = 'json',
1065+ allowed_param = ['lat', 'long', 'ip', 'accuracy', 'granularity', 'max_results']
1066+ )
1067+
1068+ """ geo/id """
1069+ geo_id = bind_api(
1070+ path = '/geo/id/{id}.json',
1071+ payload_type = 'json',
1072+ allowed_param = ['id']
1073+ )
1074+
1075+ """ Internal use only """
1076+ @staticmethod
1077+ def _pack_image(filename, max_size):
1078+ """Pack image from file into multipart-formdata post body"""
1079+ # image must be less than 700kb in size
1080+ try:
1081+ if os.path.getsize(filename) > (max_size * 1024):
1082+ raise TweepError('File is too big, must be less than 700kb.')
1083+ except os.error, e:
1084+ raise TweepError('Unable to access file')
1085+
1086+ # image must be gif, jpeg, or png
1087+ file_type = mimetypes.guess_type(filename)
1088+ if file_type is None:
1089+ raise TweepError('Could not determine file type')
1090+ file_type = file_type[0]
1091+ if file_type not in ['image/gif', 'image/jpeg', 'image/png']:
1092+ raise TweepError('Invalid file type for image: %s' % file_type)
1093+
1094+ # build the mulitpart-formdata body
1095+ fp = open(filename, 'rb')
1096+ BOUNDARY = 'Tw3ePy'
1097+ body = []
1098+ body.append('--' + BOUNDARY)
1099+ body.append('Content-Disposition: form-data; name="image"; filename="%s"' % filename)
1100+ body.append('Content-Type: %s' % file_type)
1101+ body.append('')
1102+ body.append(fp.read())
1103+ body.append('--' + BOUNDARY + '--')
1104+ body.append('')
1105+ fp.close()
1106+ body = '\r\n'.join(body)
1107+
1108+ # build headers
1109+ headers = {
1110+ 'Content-Type': 'multipart/form-data; boundary=Tw3ePy',
1111+ 'Content-Length': len(body)
1112+ }
1113+
1114+ return headers, body
1115+
1116
1117=== added file 'GTG/backends/tweepy/auth.py'
1118--- GTG/backends/tweepy/auth.py 1970-01-01 00:00:00 +0000
1119+++ GTG/backends/tweepy/auth.py 2010-08-25 16:31:45 +0000
1120@@ -0,0 +1,163 @@
1121+# Tweepy
1122+# Copyright 2009-2010 Joshua Roesslein
1123+# See LICENSE for details.
1124+
1125+from urllib2 import Request, urlopen
1126+import base64
1127+
1128+from tweepy import oauth
1129+from tweepy.error import TweepError
1130+from tweepy.api import API
1131+
1132+
1133+class AuthHandler(object):
1134+
1135+ def apply_auth(self, url, method, headers, parameters):
1136+ """Apply authentication headers to request"""
1137+ raise NotImplementedError
1138+
1139+ def get_username(self):
1140+ """Return the username of the authenticated user"""
1141+ raise NotImplementedError
1142+
1143+
1144+class BasicAuthHandler(AuthHandler):
1145+
1146+ def __init__(self, username, password):
1147+ self.username = username
1148+ self._b64up = base64.b64encode('%s:%s' % (username, password))
1149+
1150+ def apply_auth(self, url, method, headers, parameters):
1151+ headers['Authorization'] = 'Basic %s' % self._b64up
1152+
1153+ def get_username(self):
1154+ return self.username
1155+
1156+
1157+class OAuthHandler(AuthHandler):
1158+ """OAuth authentication handler"""
1159+
1160+ OAUTH_HOST = 'twitter.com'
1161+ OAUTH_ROOT = '/oauth/'
1162+
1163+ def __init__(self, consumer_key, consumer_secret, callback=None, secure=False):
1164+ self._consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
1165+ self._sigmethod = oauth.OAuthSignatureMethod_HMAC_SHA1()
1166+ self.request_token = None
1167+ self.access_token = None
1168+ self.callback = callback
1169+ self.username = None
1170+ self.secure = secure
1171+
1172+ def _get_oauth_url(self, endpoint, secure=False):
1173+ if self.secure or secure:
1174+ prefix = 'https://'
1175+ else:
1176+ prefix = 'http://'
1177+
1178+ return prefix + self.OAUTH_HOST + self.OAUTH_ROOT + endpoint
1179+
1180+ def apply_auth(self, url, method, headers, parameters):
1181+ request = oauth.OAuthRequest.from_consumer_and_token(
1182+ self._consumer, http_url=url, http_method=method,
1183+ token=self.access_token, parameters=parameters
1184+ )
1185+ request.sign_request(self._sigmethod, self._consumer, self.access_token)
1186+ headers.update(request.to_header())
1187+
1188+ def _get_request_token(self):
1189+ try:
1190+ url = self._get_oauth_url('request_token')
1191+ request = oauth.OAuthRequest.from_consumer_and_token(
1192+ self._consumer, http_url=url, callback=self.callback
1193+ )
1194+ request.sign_request(self._sigmethod, self._consumer, None)
1195+ resp = urlopen(Request(url, headers=request.to_header()))
1196+ return oauth.OAuthToken.from_string(resp.read())
1197+ except Exception, e:
1198+ raise TweepError(e)
1199+
1200+ def set_request_token(self, key, secret):
1201+ self.request_token = oauth.OAuthToken(key, secret)
1202+
1203+ def set_access_token(self, key, secret):
1204+ self.access_token = oauth.OAuthToken(key, secret)
1205+
1206+ def get_authorization_url(self, signin_with_twitter=False):
1207+ """Get the authorization URL to redirect the user"""
1208+ try:
1209+ # get the request token
1210+ self.request_token = self._get_request_token()
1211+
1212+ # build auth request and return as url
1213+ if signin_with_twitter:
1214+ url = self._get_oauth_url('authenticate')
1215+ else:
1216+ url = self._get_oauth_url('authorize')
1217+ request = oauth.OAuthRequest.from_token_and_callback(
1218+ token=self.request_token, http_url=url
1219+ )
1220+
1221+ return request.to_url()
1222+ except Exception, e:
1223+ raise TweepError(e)
1224+
1225+ def get_access_token(self, verifier=None):
1226+ """
1227+ After user has authorized the request token, get access token
1228+ with user supplied verifier.
1229+ """
1230+ try:
1231+ url = self._get_oauth_url('access_token')
1232+
1233+ # build request
1234+ request = oauth.OAuthRequest.from_consumer_and_token(
1235+ self._consumer,
1236+ token=self.request_token, http_url=url,
1237+ verifier=str(verifier)
1238+ )
1239+ request.sign_request(self._sigmethod, self._consumer, self.request_token)
1240+
1241+ # send request
1242+ resp = urlopen(Request(url, headers=request.to_header()))
1243+ self.access_token = oauth.OAuthToken.from_string(resp.read())
1244+ return self.access_token
1245+ except Exception, e:
1246+ raise TweepError(e)
1247+
1248+ def get_xauth_access_token(self, username, password):
1249+ """
1250+ Get an access token from an username and password combination.
1251+ In order to get this working you need to create an app at
1252+ http://twitter.com/apps, after that send a mail to api@twitter.com
1253+ and request activation of xAuth for it.
1254+ """
1255+ try:
1256+ url = self._get_oauth_url('access_token', secure=True) # must use HTTPS
1257+ request = oauth.OAuthRequest.from_consumer_and_token(
1258+ oauth_consumer=self._consumer,
1259+ http_method='POST', http_url=url,
1260+ parameters = {
1261+ 'x_auth_mode': 'client_auth',
1262+ 'x_auth_username': username,
1263+ 'x_auth_password': password
1264+ }
1265+ )
1266+ request.sign_request(self._sigmethod, self._consumer, None)
1267+
1268+ resp = urlopen(Request(url, data=request.to_postdata()))
1269+ self.access_token = oauth.OAuthToken.from_string(resp.read())
1270+ return self.access_token
1271+ except Exception, e:
1272+ raise TweepError(e)
1273+
1274+ def get_username(self):
1275+ if self.username is None:
1276+ api = API(self)
1277+ user = api.verify_credentials()
1278+ if user:
1279+ self.username = user.screen_name
1280+ else:
1281+ raise TweepError("Unable to get username, invalid oauth token!")
1282+ return self.username
1283+
1284
1285=== added file 'GTG/backends/tweepy/binder.py'
1286--- GTG/backends/tweepy/binder.py 1970-01-01 00:00:00 +0000
1287+++ GTG/backends/tweepy/binder.py 2010-08-25 16:31:45 +0000
1288@@ -0,0 +1,191 @@
1289+# Tweepy
1290+# Copyright 2009-2010 Joshua Roesslein
1291+# See LICENSE for details.
1292+
1293+import httplib
1294+import urllib
1295+import time
1296+import re
1297+
1298+from tweepy.error import TweepError
1299+from tweepy.utils import convert_to_utf8_str
1300+
1301+re_path_template = re.compile('{\w+}')
1302+
1303+
1304+def bind_api(**config):
1305+
1306+ class APIMethod(object):
1307+
1308+ path = config['path']
1309+ payload_type = config.get('payload_type', None)
1310+ payload_list = config.get('payload_list', False)
1311+ allowed_param = config.get('allowed_param', [])
1312+ method = config.get('method', 'GET')
1313+ require_auth = config.get('require_auth', False)
1314+ search_api = config.get('search_api', False)
1315+
1316+ def __init__(self, api, args, kargs):
1317+ # If authentication is required and no credentials
1318+ # are provided, throw an error.
1319+ if self.require_auth and not api.auth:
1320+ raise TweepError('Authentication required!')
1321+
1322+ self.api = api
1323+ self.post_data = kargs.pop('post_data', None)
1324+ self.retry_count = kargs.pop('retry_count', api.retry_count)
1325+ self.retry_delay = kargs.pop('retry_delay', api.retry_delay)
1326+ self.retry_errors = kargs.pop('retry_errors', api.retry_errors)
1327+ self.headers = kargs.pop('headers', {})
1328+ self.build_parameters(args, kargs)
1329+
1330+ # Pick correct URL root to use
1331+ if self.search_api:
1332+ self.api_root = api.search_root
1333+ else:
1334+ self.api_root = api.api_root
1335+
1336+ # Perform any path variable substitution
1337+ self.build_path()
1338+
1339+ if api.secure:
1340+ self.scheme = 'https://'
1341+ else:
1342+ self.scheme = 'http://'
1343+
1344+ if self.search_api:
1345+ self.host = api.search_host
1346+ else:
1347+ self.host = api.host
1348+
1349+ # Manually set Host header to fix an issue in python 2.5
1350+ # or older where Host is set including the 443 port.
1351+ # This causes Twitter to issue 301 redirect.
1352+ # See Issue http://github.com/joshthecoder/tweepy/issues/#issue/12
1353+ self.headers['Host'] = self.host
1354+
1355+ def build_parameters(self, args, kargs):
1356+ self.parameters = {}
1357+ for idx, arg in enumerate(args):
1358+ if arg is None:
1359+ continue
1360+
1361+ try:
1362+ self.parameters[self.allowed_param[idx]] = convert_to_utf8_str(arg)
1363+ except IndexError:
1364+ raise TweepError('Too many parameters supplied!')
1365+
1366+ for k, arg in kargs.items():
1367+ if arg is None:
1368+ continue
1369+ if k in self.parameters:
1370+ raise TweepError('Multiple values for parameter %s supplied!' % k)
1371+
1372+ self.parameters[k] = convert_to_utf8_str(arg)
1373+
1374+ def build_path(self):
1375+ for variable in re_path_template.findall(self.path):
1376+ name = variable.strip('{}')
1377+
1378+ if name == 'user' and 'user' not in self.parameters and self.api.auth:
1379+ # No 'user' parameter provided, fetch it from Auth instead.
1380+ value = self.api.auth.get_username()
1381+ else:
1382+ try:
1383+ value = urllib.quote(self.parameters[name])
1384+ except KeyError:
1385+ raise TweepError('No parameter value found for path variable: %s' % name)
1386+ del self.parameters[name]
1387+
1388+ self.path = self.path.replace(variable, value)
1389+
1390+ def execute(self):
1391+ # Build the request URL
1392+ url = self.api_root + self.path
1393+ if len(self.parameters):
1394+ url = '%s?%s' % (url, urllib.urlencode(self.parameters))
1395+
1396+ # Query the cache if one is available
1397+ # and this request uses a GET method.
1398+ if self.api.cache and self.method == 'GET':
1399+ cache_result = self.api.cache.get(url)
1400+ # if cache result found and not expired, return it
1401+ if cache_result:
1402+ # must restore api reference
1403+ if isinstance(cache_result, list):
1404+ for result in cache_result:
1405+ result._api = self.api
1406+ else:
1407+ cache_result._api = self.api
1408+ return cache_result
1409+
1410+ # Continue attempting request until successful
1411+ # or maximum number of retries is reached.
1412+ retries_performed = 0
1413+ while retries_performed < self.retry_count + 1:
1414+ # Open connection
1415+ # FIXME: add timeout
1416+ if self.api.secure:
1417+ conn = httplib.HTTPSConnection(self.host)
1418+ else:
1419+ conn = httplib.HTTPConnection(self.host)
1420+
1421+ # Apply authentication
1422+ if self.api.auth:
1423+ self.api.auth.apply_auth(
1424+ self.scheme + self.host + url,
1425+ self.method, self.headers, self.parameters
1426+ )
1427+
1428+ # Execute request
1429+ try:
1430+ conn.request(self.method, url, headers=self.headers, body=self.post_data)
1431+ resp = conn.getresponse()
1432+ except Exception, e:
1433+ raise TweepError('Failed to send request: %s' % e)
1434+
1435+ # Exit request loop if non-retry error code
1436+ if self.retry_errors:
1437+ if resp.status not in self.retry_errors: break
1438+ else:
1439+ if resp.status == 200: break
1440+
1441+ # Sleep before retrying request again
1442+ time.sleep(self.retry_delay)
1443+ retries_performed += 1
1444+
1445+ # If an error was returned, throw an exception
1446+ self.api.last_response = resp
1447+ if resp.status != 200:
1448+ try:
1449+ error_msg = self.api.parser.parse_error(resp.read())
1450+ except Exception:
1451+ error_msg = "Twitter error response: status code = %s" % resp.status
1452+ raise TweepError(error_msg, resp)
1453+
1454+ # Parse the response payload
1455+ result = self.api.parser.parse(self, resp.read())
1456+
1457+ conn.close()
1458+
1459+ # Store result into cache if one is available.
1460+ if self.api.cache and self.method == 'GET' and result:
1461+ self.api.cache.store(url, result)
1462+
1463+ return result
1464+
1465+
1466+ def _call(api, *args, **kargs):
1467+
1468+ method = APIMethod(api, args, kargs)
1469+ return method.execute()
1470+
1471+
1472+ # Set pagination mode
1473+ if 'cursor' in APIMethod.allowed_param:
1474+ _call.pagination_mode = 'cursor'
1475+ elif 'page' in APIMethod.allowed_param:
1476+ _call.pagination_mode = 'page'
1477+
1478+ return _call
1479+
1480
1481=== added file 'GTG/backends/tweepy/cache.py'
1482--- GTG/backends/tweepy/cache.py 1970-01-01 00:00:00 +0000
1483+++ GTG/backends/tweepy/cache.py 2010-08-25 16:31:45 +0000
1484@@ -0,0 +1,264 @@
1485+# Tweepy
1486+# Copyright 2009-2010 Joshua Roesslein
1487+# See LICENSE for details.
1488+
1489+import time
1490+import threading
1491+import os
1492+import cPickle as pickle
1493+
1494+try:
1495+ import hashlib
1496+except ImportError:
1497+ # python 2.4
1498+ import md5 as hashlib
1499+
1500+try:
1501+ import fcntl
1502+except ImportError:
1503+ # Probably on a windows system
1504+ # TODO: use win32file
1505+ pass
1506+
1507+
1508+class Cache(object):
1509+ """Cache interface"""
1510+
1511+ def __init__(self, timeout=60):
1512+ """Initialize the cache
1513+ timeout: number of seconds to keep a cached entry
1514+ """
1515+ self.timeout = timeout
1516+
1517+ def store(self, key, value):
1518+ """Add new record to cache
1519+ key: entry key
1520+ value: data of entry
1521+ """
1522+ raise NotImplementedError
1523+
1524+ def get(self, key, timeout=None):
1525+ """Get cached entry if exists and not expired
1526+ key: which entry to get
1527+ timeout: override timeout with this value [optional]
1528+ """
1529+ raise NotImplementedError
1530+
1531+ def count(self):
1532+ """Get count of entries currently stored in cache"""
1533+ raise NotImplementedError
1534+
1535+ def cleanup(self):
1536+ """Delete any expired entries in cache."""
1537+ raise NotImplementedError
1538+
1539+ def flush(self):
1540+ """Delete all cached entries"""
1541+ raise NotImplementedError
1542+
1543+
1544+class MemoryCache(Cache):
1545+ """In-memory cache"""
1546+
1547+ def __init__(self, timeout=60):
1548+ Cache.__init__(self, timeout)
1549+ self._entries = {}
1550+ self.lock = threading.Lock()
1551+
1552+ def __getstate__(self):
1553+ # pickle
1554+ return {'entries': self._entries, 'timeout': self.timeout}
1555+
1556+ def __setstate__(self, state):
1557+ # unpickle
1558+ self.lock = threading.Lock()
1559+ self._entries = state['entries']
1560+ self.timeout = state['timeout']
1561+
1562+ def _is_expired(self, entry, timeout):
1563+ return timeout > 0 and (time.time() - entry[0]) >= timeout
1564+
1565+ def store(self, key, value):
1566+ self.lock.acquire()
1567+ self._entries[key] = (time.time(), value)
1568+ self.lock.release()
1569+
1570+ def get(self, key, timeout=None):
1571+ self.lock.acquire()
1572+ try:
1573+ # check to see if we have this key
1574+ entry = self._entries.get(key)
1575+ if not entry:
1576+ # no hit, return nothing
1577+ return None
1578+
1579+ # use provided timeout in arguments if provided
1580+ # otherwise use the one provided during init.
1581+ if timeout is None:
1582+ timeout = self.timeout
1583+
1584+ # make sure entry is not expired
1585+ if self._is_expired(entry, timeout):
1586+ # entry expired, delete and return nothing
1587+ del self._entries[key]
1588+ return None
1589+
1590+ # entry found and not expired, return it
1591+ return entry[1]
1592+ finally:
1593+ self.lock.release()
1594+
1595+ def count(self):
1596+ return len(self._entries)
1597+
1598+ def cleanup(self):
1599+ self.lock.acquire()
1600+ try:
1601+ for k, v in self._entries.items():
1602+ if self._is_expired(v, self.timeout):
1603+ del self._entries[k]
1604+ finally:
1605+ self.lock.release()
1606+
1607+ def flush(self):
1608+ self.lock.acquire()
1609+ self._entries.clear()
1610+ self.lock.release()
1611+
1612+
1613+class FileCache(Cache):
1614+ """File-based cache"""
1615+
1616+ # locks used to make cache thread-safe
1617+ cache_locks = {}
1618+
1619+ def __init__(self, cache_dir, timeout=60):
1620+ Cache.__init__(self, timeout)
1621+ if os.path.exists(cache_dir) is False:
1622+ os.mkdir(cache_dir)
1623+ self.cache_dir = cache_dir
1624+ if cache_dir in FileCache.cache_locks:
1625+ self.lock = FileCache.cache_locks[cache_dir]
1626+ else:
1627+ self.lock = threading.Lock()
1628+ FileCache.cache_locks[cache_dir] = self.lock
1629+
1630+ if os.name == 'posix':
1631+ self._lock_file = self._lock_file_posix
1632+ self._unlock_file = self._unlock_file_posix
1633+ elif os.name == 'nt':
1634+ self._lock_file = self._lock_file_win32
1635+ self._unlock_file = self._unlock_file_win32
1636+ else:
1637+ print 'Warning! FileCache locking not supported on this system!'
1638+ self._lock_file = self._lock_file_dummy
1639+ self._unlock_file = self._unlock_file_dummy
1640+
1641+ def _get_path(self, key):
1642+ md5 = hashlib.md5()
1643+ md5.update(key)
1644+ return os.path.join(self.cache_dir, md5.hexdigest())
1645+
1646+ def _lock_file_dummy(self, path, exclusive=True):
1647+ return None
1648+
1649+ def _unlock_file_dummy(self, lock):
1650+ return
1651+
1652+ def _lock_file_posix(self, path, exclusive=True):
1653+ lock_path = path + '.lock'
1654+ if exclusive is True:
1655+ f_lock = open(lock_path, 'w')
1656+ fcntl.lockf(f_lock, fcntl.LOCK_EX)
1657+ else:
1658+ f_lock = open(lock_path, 'r')
1659+ fcntl.lockf(f_lock, fcntl.LOCK_SH)
1660+ if os.path.exists(lock_path) is False:
1661+ f_lock.close()
1662+ return None
1663+ return f_lock
1664+
1665+ def _unlock_file_posix(self, lock):
1666+ lock.close()
1667+
1668+ def _lock_file_win32(self, path, exclusive=True):
1669+ # TODO: implement
1670+ return None
1671+
1672+ def _unlock_file_win32(self, lock):
1673+ # TODO: implement
1674+ return
1675+
1676+ def _delete_file(self, path):
1677+ os.remove(path)
1678+ if os.path.exists(path + '.lock'):
1679+ os.remove(path + '.lock')
1680+
1681+ def store(self, key, value):
1682+ path = self._get_path(key)
1683+ self.lock.acquire()
1684+ try:
1685+ # acquire lock and open file
1686+ f_lock = self._lock_file(path)
1687+ datafile = open(path, 'wb')
1688+
1689+ # write data
1690+ pickle.dump((time.time(), value), datafile)
1691+
1692+ # close and unlock file
1693+ datafile.close()
1694+ self._unlock_file(f_lock)
1695+ finally:
1696+ self.lock.release()
1697+
1698+ def get(self, key, timeout=None):
1699+ return self._get(self._get_path(key), timeout)
1700+
1701+ def _get(self, path, timeout):
1702+ if os.path.exists(path) is False:
1703+ # no record
1704+ return None
1705+ self.lock.acquire()
1706+ try:
1707+ # acquire lock and open
1708+ f_lock = self._lock_file(path, False)
1709+ datafile = open(path, 'rb')
1710+
1711+ # read pickled object
1712+ created_time, value = pickle.load(datafile)
1713+ datafile.close()
1714+
1715+ # check if value is expired
1716+ if timeout is None:
1717+ timeout = self.timeout
1718+ if timeout > 0 and (time.time() - created_time) >= timeout:
1719+ # expired! delete from cache
1720+ value = None
1721+ self._delete_file(path)
1722+
1723+ # unlock and return result
1724+ self._unlock_file(f_lock)
1725+ return value
1726+ finally:
1727+ self.lock.release()
1728+
1729+ def count(self):
1730+ c = 0
1731+ for entry in os.listdir(self.cache_dir):
1732+ if entry.endswith('.lock'):
1733+ continue
1734+ c += 1
1735+ return c
1736+
1737+ def cleanup(self):
1738+ for entry in os.listdir(self.cache_dir):
1739+ if entry.endswith('.lock'):
1740+ continue
1741+ self._get(os.path.join(self.cache_dir, entry), None)
1742+
1743+ def flush(self):
1744+ for entry in os.listdir(self.cache_dir):
1745+ if entry.endswith('.lock'):
1746+ continue
1747+ self._delete_file(os.path.join(self.cache_dir, entry))
1748+
1749
1750=== added file 'GTG/backends/tweepy/cursor.py'
1751--- GTG/backends/tweepy/cursor.py 1970-01-01 00:00:00 +0000
1752+++ GTG/backends/tweepy/cursor.py 2010-08-25 16:31:45 +0000
1753@@ -0,0 +1,128 @@
1754+# Tweepy
1755+# Copyright 2009-2010 Joshua Roesslein
1756+# See LICENSE for details.
1757+
1758+from tweepy.error import TweepError
1759+
1760+class Cursor(object):
1761+ """Pagination helper class"""
1762+
1763+ def __init__(self, method, *args, **kargs):
1764+ if hasattr(method, 'pagination_mode'):
1765+ if method.pagination_mode == 'cursor':
1766+ self.iterator = CursorIterator(method, args, kargs)
1767+ else:
1768+ self.iterator = PageIterator(method, args, kargs)
1769+ else:
1770+ raise TweepError('This method does not perform pagination')
1771+
1772+ def pages(self, limit=0):
1773+ """Return iterator for pages"""
1774+ if limit > 0:
1775+ self.iterator.limit = limit
1776+ return self.iterator
1777+
1778+ def items(self, limit=0):
1779+ """Return iterator for items in each page"""
1780+ i = ItemIterator(self.iterator)
1781+ i.limit = limit
1782+ return i
1783+
1784+class BaseIterator(object):
1785+
1786+ def __init__(self, method, args, kargs):
1787+ self.method = method
1788+ self.args = args
1789+ self.kargs = kargs
1790+ self.limit = 0
1791+
1792+ def next(self):
1793+ raise NotImplementedError
1794+
1795+ def prev(self):
1796+ raise NotImplementedError
1797+
1798+ def __iter__(self):
1799+ return self
1800+
1801+class CursorIterator(BaseIterator):
1802+
1803+ def __init__(self, method, args, kargs):
1804+ BaseIterator.__init__(self, method, args, kargs)
1805+ self.next_cursor = -1
1806+ self.prev_cursor = 0
1807+ self.count = 0
1808+
1809+ def next(self):
1810+ if self.next_cursor == 0 or (self.limit and self.count == self.limit):
1811+ raise StopIteration
1812+ data, cursors = self.method(
1813+ cursor=self.next_cursor, *self.args, **self.kargs
1814+ )
1815+ self.prev_cursor, self.next_cursor = cursors
1816+ if len(data) == 0:
1817+ raise StopIteration
1818+ self.count += 1
1819+ return data
1820+
1821+ def prev(self):
1822+ if self.prev_cursor == 0:
1823+ raise TweepError('Can not page back more, at first page')
1824+ data, self.next_cursor, self.prev_cursor = self.method(
1825+ cursor=self.prev_cursor, *self.args, **self.kargs
1826+ )
1827+ self.count -= 1
1828+ return data
1829+
1830+class PageIterator(BaseIterator):
1831+
1832+ def __init__(self, method, args, kargs):
1833+ BaseIterator.__init__(self, method, args, kargs)
1834+ self.current_page = 0
1835+
1836+ def next(self):
1837+ self.current_page += 1
1838+ items = self.method(page=self.current_page, *self.args, **self.kargs)
1839+ if len(items) == 0 or (self.limit > 0 and self.current_page > self.limit):
1840+ raise StopIteration
1841+ return items
1842+
1843+ def prev(self):
1844+ if (self.current_page == 1):
1845+ raise TweepError('Can not page back more, at first page')
1846+ self.current_page -= 1
1847+ return self.method(page=self.current_page, *self.args, **self.kargs)
1848+
1849+class ItemIterator(BaseIterator):
1850+
1851+ def __init__(self, page_iterator):
1852+ self.page_iterator = page_iterator
1853+ self.limit = 0
1854+ self.current_page = None
1855+ self.page_index = -1
1856+ self.count = 0
1857+
1858+ def next(self):
1859+ if self.limit > 0 and self.count == self.limit:
1860+ raise StopIteration
1861+ if self.current_page is None or self.page_index == len(self.current_page) - 1:
1862+ # Reached end of current page, get the next page...
1863+ self.current_page = self.page_iterator.next()
1864+ self.page_index = -1
1865+ self.page_index += 1
1866+ self.count += 1
1867+ return self.current_page[self.page_index]
1868+
1869+ def prev(self):
1870+ if self.current_page is None:
1871+ raise TweepError('Can not go back more, at first page')
1872+ if self.page_index == 0:
1873+ # At the beginning of the current page, move to next...
1874+ self.current_page = self.page_iterator.prev()
1875+ self.page_index = len(self.current_page)
1876+ if self.page_index == 0:
1877+ raise TweepError('No more items')
1878+ self.page_index -= 1
1879+ self.count -= 1
1880+ return self.current_page[self.page_index]
1881+
1882
1883=== added file 'GTG/backends/tweepy/error.py'
1884--- GTG/backends/tweepy/error.py 1970-01-01 00:00:00 +0000
1885+++ GTG/backends/tweepy/error.py 2010-08-25 16:31:45 +0000
1886@@ -0,0 +1,14 @@
1887+# Tweepy
1888+# Copyright 2009-2010 Joshua Roesslein
1889+# See LICENSE for details.
1890+
1891+class TweepError(Exception):
1892+ """Tweepy exception"""
1893+
1894+ def __init__(self, reason, response=None):
1895+ self.reason = str(reason)
1896+ self.response = response
1897+
1898+ def __str__(self):
1899+ return self.reason
1900+
1901
1902=== added file 'GTG/backends/tweepy/models.py'
1903--- GTG/backends/tweepy/models.py 1970-01-01 00:00:00 +0000
1904+++ GTG/backends/tweepy/models.py 2010-08-25 16:31:45 +0000
1905@@ -0,0 +1,313 @@
1906+# Tweepy
1907+# Copyright 2009-2010 Joshua Roesslein
1908+# See LICENSE for details.
1909+
1910+from tweepy.error import TweepError
1911+from tweepy.utils import parse_datetime, parse_html_value, parse_a_href, \
1912+ parse_search_datetime, unescape_html
1913+
1914+
1915+class ResultSet(list):
1916+ """A list like object that holds results from a Twitter API query."""
1917+
1918+
1919+class Model(object):
1920+
1921+ def __init__(self, api=None):
1922+ self._api = api
1923+
1924+ def __getstate__(self):
1925+ # pickle
1926+ pickle = dict(self.__dict__)
1927+ try:
1928+ del pickle['_api'] # do not pickle the API reference
1929+ except KeyError:
1930+ pass
1931+ return pickle
1932+
1933+ @classmethod
1934+ def parse(cls, api, json):
1935+ """Parse a JSON object into a model instance."""
1936+ raise NotImplementedError
1937+
1938+ @classmethod
1939+ def parse_list(cls, api, json_list):
1940+ """Parse a list of JSON objects into a result set of model instances."""
1941+ results = ResultSet()
1942+ for obj in json_list:
1943+ results.append(cls.parse(api, obj))
1944+ return results
1945+
1946+
1947+class Status(Model):
1948+
1949+ @classmethod
1950+ def parse(cls, api, json):
1951+ status = cls(api)
1952+ for k, v in json.items():
1953+ if k == 'user':
1954+ user = User.parse(api, v)
1955+ setattr(status, 'author', user)
1956+ setattr(status, 'user', user) # DEPRECIATED
1957+ elif k == 'created_at':
1958+ setattr(status, k, parse_datetime(v))
1959+ elif k == 'source':
1960+ if '<' in v:
1961+ setattr(status, k, parse_html_value(v))
1962+ setattr(status, 'source_url', parse_a_href(v))
1963+ else:
1964+ setattr(status, k, v)
1965+ setattr(status, 'source_url', None)
1966+ elif k == 'retweeted_status':
1967+ setattr(status, k, Status.parse(api, v))
1968+ else:
1969+ setattr(status, k, v)
1970+ return status
1971+
1972+ def destroy(self):
1973+ return self._api.destroy_status(self.id)
1974+
1975+ def retweet(self):
1976+ return self._api.retweet(self.id)
1977+
1978+ def retweets(self):
1979+ return self._api.retweets(self.id)
1980+
1981+ def favorite(self):
1982+ return self._api.create_favorite(self.id)
1983+
1984+
1985+class User(Model):
1986+
1987+ @classmethod
1988+ def parse(cls, api, json):
1989+ user = cls(api)
1990+ for k, v in json.items():
1991+ if k == 'created_at':
1992+ setattr(user, k, parse_datetime(v))
1993+ elif k == 'status':
1994+ setattr(user, k, Status.parse(api, v))
1995+ elif k == 'following':
1996+ # twitter sets this to null if it is false
1997+ if v is True:
1998+ setattr(user, k, True)
1999+ else:
2000+ setattr(user, k, False)
2001+ else:
2002+ setattr(user, k, v)
2003+ return user
2004+
2005+ @classmethod
2006+ def parse_list(cls, api, json_list):
2007+ if isinstance(json_list, list):
2008+ item_list = json_list
2009+ else:
2010+ item_list = json_list['users']
2011+
2012+ results = ResultSet()
2013+ for obj in item_list:
2014+ results.append(cls.parse(api, obj))
2015+ return results
2016+
2017+ def timeline(self, **kargs):
2018+ return self._api.user_timeline(user_id=self.id, **kargs)
2019+
2020+ def friends(self, **kargs):
2021+ return self._api.friends(user_id=self.id, **kargs)
2022+
2023+ def followers(self, **kargs):
2024+ return self._api.followers(user_id=self.id, **kargs)
2025+
2026+ def follow(self):
2027+ self._api.create_friendship(user_id=self.id)
2028+ self.following = True
2029+
2030+ def unfollow(self):
2031+ self._api.destroy_friendship(user_id=self.id)
2032+ self.following = False
2033+
2034+ def lists_memberships(self, *args, **kargs):
2035+ return self._api.lists_memberships(user=self.screen_name, *args, **kargs)
2036+
2037+ def lists_subscriptions(self, *args, **kargs):
2038+ return self._api.lists_subscriptions(user=self.screen_name, *args, **kargs)
2039+
2040+ def lists(self, *args, **kargs):
2041+ return self._api.lists(user=self.screen_name, *args, **kargs)
2042+
2043+ def followers_ids(self, *args, **kargs):
2044+ return self._api.followers_ids(user_id=self.id, *args, **kargs)
2045+
2046+
2047+class DirectMessage(Model):
2048+
2049+ @classmethod
2050+ def parse(cls, api, json):
2051+ dm = cls(api)
2052+ for k, v in json.items():
2053+ if k == 'sender' or k == 'recipient':
2054+ setattr(dm, k, User.parse(api, v))
2055+ elif k == 'created_at':
2056+ setattr(dm, k, parse_datetime(v))
2057+ else:
2058+ setattr(dm, k, v)
2059+ return dm
2060+
2061+ def destroy(self):
2062+ return self._api.destroy_direct_message(self.id)
2063+
2064+
2065+class Friendship(Model):
2066+
2067+ @classmethod
2068+ def parse(cls, api, json):
2069+ relationship = json['relationship']
2070+
2071+ # parse source
2072+ source = cls(api)
2073+ for k, v in relationship['source'].items():
2074+ setattr(source, k, v)
2075+
2076+ # parse target
2077+ target = cls(api)
2078+ for k, v in relationship['target'].items():
2079+ setattr(target, k, v)
2080+
2081+ return source, target
2082+
2083+
2084+class SavedSearch(Model):
2085+
2086+ @classmethod
2087+ def parse(cls, api, json):
2088+ ss = cls(api)
2089+ for k, v in json.items():
2090+ if k == 'created_at':
2091+ setattr(ss, k, parse_datetime(v))
2092+ else:
2093+ setattr(ss, k, v)
2094+ return ss
2095+
2096+ def destroy(self):
2097+ return self._api.destroy_saved_search(self.id)
2098+
2099+
2100+class SearchResult(Model):
2101+
2102+ @classmethod
2103+ def parse(cls, api, json):
2104+ result = cls()
2105+ for k, v in json.items():
2106+ if k == 'created_at':
2107+ setattr(result, k, parse_search_datetime(v))
2108+ elif k == 'source':
2109+ setattr(result, k, parse_html_value(unescape_html(v)))
2110+ else:
2111+ setattr(result, k, v)
2112+ return result
2113+
2114+ @classmethod
2115+ def parse_list(cls, api, json_list, result_set=None):
2116+ results = ResultSet()
2117+ results.max_id = json_list.get('max_id')
2118+ results.since_id = json_list.get('since_id')
2119+ results.refresh_url = json_list.get('refresh_url')
2120+ results.next_page = json_list.get('next_page')
2121+ results.results_per_page = json_list.get('results_per_page')
2122+ results.page = json_list.get('page')
2123+ results.completed_in = json_list.get('completed_in')
2124+ results.query = json_list.get('query')
2125+
2126+ for obj in json_list['results']:
2127+ results.append(cls.parse(api, obj))
2128+ return results
2129+
2130+
2131+class List(Model):
2132+
2133+ @classmethod
2134+ def parse(cls, api, json):
2135+ lst = List(api)
2136+ for k,v in json.items():
2137+ if k == 'user':
2138+ setattr(lst, k, User.parse(api, v))
2139+ else:
2140+ setattr(lst, k, v)
2141+ return lst
2142+
2143+ @classmethod
2144+ def parse_list(cls, api, json_list, result_set=None):
2145+ results = ResultSet()
2146+ for obj in json_list['lists']:
2147+ results.append(cls.parse(api, obj))
2148+ return results
2149+
2150+ def update(self, **kargs):
2151+ return self._api.update_list(self.slug, **kargs)
2152+
2153+ def destroy(self):
2154+ return self._api.destroy_list(self.slug)
2155+
2156+ def timeline(self, **kargs):
2157+ return self._api.list_timeline(self.user.screen_name, self.slug, **kargs)
2158+
2159+ def add_member(self, id):
2160+ return self._api.add_list_member(self.slug, id)
2161+
2162+ def remove_member(self, id):
2163+ return self._api.remove_list_member(self.slug, id)
2164+
2165+ def members(self, **kargs):
2166+ return self._api.list_members(self.user.screen_name, self.slug, **kargs)
2167+
2168+ def is_member(self, id):
2169+ return self._api.is_list_member(self.user.screen_name, self.slug, id)
2170+
2171+ def subscribe(self):
2172+ return self._api.subscribe_list(self.user.screen_name, self.slug)
2173+
2174+ def unsubscribe(self):
2175+ return self._api.unsubscribe_list(self.user.screen_name, self.slug)
2176+
2177+ def subscribers(self, **kargs):
2178+ return self._api.list_subscribers(self.user.screen_name, self.slug, **kargs)
2179+
2180+ def is_subscribed(self, id):
2181+ return self._api.is_subscribed_list(self.user.screen_name, self.slug, id)
2182+
2183+
2184+class JSONModel(Model):
2185+
2186+ @classmethod
2187+ def parse(cls, api, json):
2188+ return json
2189+
2190+
2191+class IDModel(Model):
2192+
2193+ @classmethod
2194+ def parse(cls, api, json):
2195+ if isinstance(json, list):
2196+ return json
2197+ else:
2198+ return json['ids']
2199+
2200+
2201+class ModelFactory(object):
2202+ """
2203+ Used by parsers for creating instances
2204+ of models. You may subclass this factory
2205+ to add your own extended models.
2206+ """
2207+
2208+ status = Status
2209+ user = User
2210+ direct_message = DirectMessage
2211+ friendship = Friendship
2212+ saved_search = SavedSearch
2213+ search_result = SearchResult
2214+ list = List
2215+
2216+ json = JSONModel
2217+ ids = IDModel
2218+
2219
2220=== added file 'GTG/backends/tweepy/oauth.py'
2221--- GTG/backends/tweepy/oauth.py 1970-01-01 00:00:00 +0000
2222+++ GTG/backends/tweepy/oauth.py 2010-08-25 16:31:45 +0000
2223@@ -0,0 +1,655 @@
2224+"""
2225+The MIT License
2226+
2227+Copyright (c) 2007 Leah Culver
2228+
2229+Permission is hereby granted, free of charge, to any person obtaining a copy
2230+of this software and associated documentation files (the "Software"), to deal
2231+in the Software without restriction, including without limitation the rights
2232+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
2233+copies of the Software, and to permit persons to whom the Software is
2234+furnished to do so, subject to the following conditions:
2235+
2236+The above copyright notice and this permission notice shall be included in
2237+all copies or substantial portions of the Software.
2238+
2239+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
2240+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
2241+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2242+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
2243+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2244+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2245+THE SOFTWARE.
2246+"""
2247+
2248+import cgi
2249+import urllib
2250+import time
2251+import random
2252+import urlparse
2253+import hmac
2254+import binascii
2255+
2256+
2257+VERSION = '1.0' # Hi Blaine!
2258+HTTP_METHOD = 'GET'
2259+SIGNATURE_METHOD = 'PLAINTEXT'
2260+
2261+
2262+class OAuthError(RuntimeError):
2263+ """Generic exception class."""
2264+ def __init__(self, message='OAuth error occured.'):
2265+ self.message = message
2266+
2267+def build_authenticate_header(realm=''):
2268+ """Optional WWW-Authenticate header (401 error)"""
2269+ return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
2270+
2271+def escape(s):
2272+ """Escape a URL including any /."""
2273+ return urllib.quote(s, safe='~')
2274+
2275+def _utf8_str(s):
2276+ """Convert unicode to utf-8."""
2277+ if isinstance(s, unicode):
2278+ return s.encode("utf-8")
2279+ else:
2280+ return str(s)
2281+
2282+def generate_timestamp():
2283+ """Get seconds since epoch (UTC)."""
2284+ return int(time.time())
2285+
2286+def generate_nonce(length=8):
2287+ """Generate pseudorandom number."""
2288+ return ''.join([str(random.randint(0, 9)) for i in range(length)])
2289+
2290+def generate_verifier(length=8):
2291+ """Generate pseudorandom number."""
2292+ return ''.join([str(random.randint(0, 9)) for i in range(length)])
2293+
2294+
2295+class OAuthConsumer(object):
2296+ """Consumer of OAuth authentication.
2297+
2298+ OAuthConsumer is a data type that represents the identity of the Consumer
2299+ via its shared secret with the Service Provider.
2300+
2301+ """
2302+ key = None
2303+ secret = None
2304+
2305+ def __init__(self, key, secret):
2306+ self.key = key
2307+ self.secret = secret
2308+
2309+
2310+class OAuthToken(object):
2311+ """OAuthToken is a data type that represents an End User via either an access
2312+ or request token.
2313+
2314+ key -- the token
2315+ secret -- the token secret
2316+
2317+ """
2318+ key = None
2319+ secret = None
2320+ callback = None
2321+ callback_confirmed = None
2322+ verifier = None
2323+
2324+ def __init__(self, key, secret):
2325+ self.key = key
2326+ self.secret = secret
2327+
2328+ def set_callback(self, callback):
2329+ self.callback = callback
2330+ self.callback_confirmed = 'true'
2331+
2332+ def set_verifier(self, verifier=None):
2333+ if verifier is not None:
2334+ self.verifier = verifier
2335+ else:
2336+ self.verifier = generate_verifier()
2337+
2338+ def get_callback_url(self):
2339+ if self.callback and self.verifier:
2340+ # Append the oauth_verifier.
2341+ parts = urlparse.urlparse(self.callback)
2342+ scheme, netloc, path, params, query, fragment = parts[:6]
2343+ if query:
2344+ query = '%s&oauth_verifier=%s' % (query, self.verifier)
2345+ else:
2346+ query = 'oauth_verifier=%s' % self.verifier
2347+ return urlparse.urlunparse((scheme, netloc, path, params,
2348+ query, fragment))
2349+ return self.callback
2350+
2351+ def to_string(self):
2352+ data = {
2353+ 'oauth_token': self.key,
2354+ 'oauth_token_secret': self.secret,
2355+ }
2356+ if self.callback_confirmed is not None:
2357+ data['oauth_callback_confirmed'] = self.callback_confirmed
2358+ return urllib.urlencode(data)
2359+
2360+ def from_string(s):
2361+ """ Returns a token from something like:
2362+ oauth_token_secret=xxx&oauth_token=xxx
2363+ """
2364+ params = cgi.parse_qs(s, keep_blank_values=False)
2365+ key = params['oauth_token'][0]
2366+ secret = params['oauth_token_secret'][0]
2367+ token = OAuthToken(key, secret)
2368+ try:
2369+ token.callback_confirmed = params['oauth_callback_confirmed'][0]
2370+ except KeyError:
2371+ pass # 1.0, no callback confirmed.
2372+ return token
2373+ from_string = staticmethod(from_string)
2374+
2375+ def __str__(self):
2376+ return self.to_string()
2377+
2378+
2379+class OAuthRequest(object):
2380+ """OAuthRequest represents the request and can be serialized.
2381+
2382+ OAuth parameters:
2383+ - oauth_consumer_key
2384+ - oauth_token
2385+ - oauth_signature_method
2386+ - oauth_signature
2387+ - oauth_timestamp
2388+ - oauth_nonce
2389+ - oauth_version
2390+ - oauth_verifier
2391+ ... any additional parameters, as defined by the Service Provider.
2392+ """
2393+ parameters = None # OAuth parameters.
2394+ http_method = HTTP_METHOD
2395+ http_url = None
2396+ version = VERSION
2397+
2398+ def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
2399+ self.http_method = http_method
2400+ self.http_url = http_url
2401+ self.parameters = parameters or {}
2402+
2403+ def set_parameter(self, parameter, value):
2404+ self.parameters[parameter] = value
2405+
2406+ def get_parameter(self, parameter):
2407+ try:
2408+ return self.parameters[parameter]
2409+ except:
2410+ raise OAuthError('Parameter not found: %s' % parameter)
2411+
2412+ def _get_timestamp_nonce(self):
2413+ return self.get_parameter('oauth_timestamp'), self.get_parameter(
2414+ 'oauth_nonce')
2415+
2416+ def get_nonoauth_parameters(self):
2417+ """Get any non-OAuth parameters."""
2418+ parameters = {}
2419+ for k, v in self.parameters.iteritems():
2420+ # Ignore oauth parameters.
2421+ if k.find('oauth_') < 0:
2422+ parameters[k] = v
2423+ return parameters
2424+
2425+ def to_header(self, realm=''):
2426+ """Serialize as a header for an HTTPAuth request."""
2427+ auth_header = 'OAuth realm="%s"' % realm
2428+ # Add the oauth parameters.
2429+ if self.parameters:
2430+ for k, v in self.parameters.iteritems():
2431+ if k[:6] == 'oauth_':
2432+ auth_header += ', %s="%s"' % (k, escape(str(v)))
2433+ return {'Authorization': auth_header}
2434+
2435+ def to_postdata(self):
2436+ """Serialize as post data for a POST request."""
2437+ return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
2438+ for k, v in self.parameters.iteritems()])
2439+
2440+ def to_url(self):
2441+ """Serialize as a URL for a GET request."""
2442+ return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
2443+
2444+ def get_normalized_parameters(self):
2445+ """Return a string that contains the parameters that must be signed."""
2446+ params = self.parameters
2447+ try:
2448+ # Exclude the signature if it exists.
2449+ del params['oauth_signature']
2450+ except:
2451+ pass
2452+ # Escape key values before sorting.
2453+ key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
2454+ for k,v in params.items()]
2455+ # Sort lexicographically, first after key, then after value.
2456+ key_values.sort()
2457+ # Combine key value pairs into a string.
2458+ return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
2459+
2460+ def get_normalized_http_method(self):
2461+ """Uppercases the http method."""
2462+ return self.http_method.upper()
2463+
2464+ def get_normalized_http_url(self):
2465+ """Parses the URL and rebuilds it to be scheme://host/path."""
2466+ parts = urlparse.urlparse(self.http_url)
2467+ scheme, netloc, path = parts[:3]
2468+ # Exclude default port numbers.
2469+ if scheme == 'http' and netloc[-3:] == ':80':
2470+ netloc = netloc[:-3]
2471+ elif scheme == 'https' and netloc[-4:] == ':443':
2472+ netloc = netloc[:-4]
2473+ return '%s://%s%s' % (scheme, netloc, path)
2474+
2475+ def sign_request(self, signature_method, consumer, token):
2476+ """Set the signature parameter to the result of build_signature."""
2477+ # Set the signature method.
2478+ self.set_parameter('oauth_signature_method',
2479+ signature_method.get_name())
2480+ # Set the signature.
2481+ self.set_parameter('oauth_signature',
2482+ self.build_signature(signature_method, consumer, token))
2483+
2484+ def build_signature(self, signature_method, consumer, token):
2485+ """Calls the build signature method within the signature method."""
2486+ return signature_method.build_signature(self, consumer, token)
2487+
2488+ def from_request(http_method, http_url, headers=None, parameters=None,
2489+ query_string=None):
2490+ """Combines multiple parameter sources."""
2491+ if parameters is None:
2492+ parameters = {}
2493+
2494+ # Headers
2495+ if headers and 'Authorization' in headers:
2496+ auth_header = headers['Authorization']
2497+ # Check that the authorization header is OAuth.
2498+ if auth_header[:6] == 'OAuth ':
2499+ auth_header = auth_header[6:]
2500+ try:
2501+ # Get the parameters from the header.
2502+ header_params = OAuthRequest._split_header(auth_header)
2503+ parameters.update(header_params)
2504+ except:
2505+ raise OAuthError('Unable to parse OAuth parameters from '
2506+ 'Authorization header.')
2507+
2508+ # GET or POST query string.
2509+ if query_string:
2510+ query_params = OAuthRequest._split_url_string(query_string)
2511+ parameters.update(query_params)
2512+
2513+ # URL parameters.
2514+ param_str = urlparse.urlparse(http_url)[4] # query
2515+ url_params = OAuthRequest._split_url_string(param_str)
2516+ parameters.update(url_params)
2517+
2518+ if parameters:
2519+ return OAuthRequest(http_method, http_url, parameters)
2520+
2521+ return None
2522+ from_request = staticmethod(from_request)
2523+
2524+ def from_consumer_and_token(oauth_consumer, token=None,
2525+ callback=None, verifier=None, http_method=HTTP_METHOD,
2526+ http_url=None, parameters=None):
2527+ if not parameters:
2528+ parameters = {}
2529+
2530+ defaults = {
2531+ 'oauth_consumer_key': oauth_consumer.key,
2532+ 'oauth_timestamp': generate_timestamp(),
2533+ 'oauth_nonce': generate_nonce(),
2534+ 'oauth_version': OAuthRequest.version,
2535+ }
2536+
2537+ defaults.update(parameters)
2538+ parameters = defaults
2539+
2540+ if token:
2541+ parameters['oauth_token'] = token.key
2542+ if token.callback:
2543+ parameters['oauth_callback'] = token.callback
2544+ # 1.0a support for verifier.
2545+ if verifier:
2546+ parameters['oauth_verifier'] = verifier
2547+ elif callback:
2548+ # 1.0a support for callback in the request token request.
2549+ parameters['oauth_callback'] = callback
2550+
2551+ return OAuthRequest(http_method, http_url, parameters)
2552+ from_consumer_and_token = staticmethod(from_consumer_and_token)
2553+
2554+ def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
2555+ http_url=None, parameters=None):
2556+ if not parameters:
2557+ parameters = {}
2558+
2559+ parameters['oauth_token'] = token.key
2560+
2561+ if callback:
2562+ parameters['oauth_callback'] = callback
2563+
2564+ return OAuthRequest(http_method, http_url, parameters)
2565+ from_token_and_callback = staticmethod(from_token_and_callback)
2566+
2567+ def _split_header(header):
2568+ """Turn Authorization: header into parameters."""
2569+ params = {}
2570+ parts = header.split(',')
2571+ for param in parts:
2572+ # Ignore realm parameter.
2573+ if param.find('realm') > -1:
2574+ continue
2575+ # Remove whitespace.
2576+ param = param.strip()
2577+ # Split key-value.
2578+ param_parts = param.split('=', 1)
2579+ # Remove quotes and unescape the value.
2580+ params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
2581+ return params
2582+ _split_header = staticmethod(_split_header)
2583+
2584+ def _split_url_string(param_str):
2585+ """Turn URL string into parameters."""
2586+ parameters = cgi.parse_qs(param_str, keep_blank_values=False)
2587+ for k, v in parameters.iteritems():
2588+ parameters[k] = urllib.unquote(v[0])
2589+ return parameters
2590+ _split_url_string = staticmethod(_split_url_string)
2591+
2592+class OAuthServer(object):
2593+ """A worker to check the validity of a request against a data store."""
2594+ timestamp_threshold = 300 # In seconds, five minutes.
2595+ version = VERSION
2596+ signature_methods = None
2597+ data_store = None
2598+
2599+ def __init__(self, data_store=None, signature_methods=None):
2600+ self.data_store = data_store
2601+ self.signature_methods = signature_methods or {}
2602+
2603+ def set_data_store(self, data_store):
2604+ self.data_store = data_store
2605+
2606+ def get_data_store(self):
2607+ return self.data_store
2608+
2609+ def add_signature_method(self, signature_method):
2610+ self.signature_methods[signature_method.get_name()] = signature_method
2611+ return self.signature_methods
2612+
2613+ def fetch_request_token(self, oauth_request):
2614+ """Processes a request_token request and returns the
2615+ request token on success.
2616+ """
2617+ try:
2618+ # Get the request token for authorization.
2619+ token = self._get_token(oauth_request, 'request')
2620+ except OAuthError:
2621+ # No token required for the initial token request.
2622+ version = self._get_version(oauth_request)
2623+ consumer = self._get_consumer(oauth_request)
2624+ try:
2625+ callback = self.get_callback(oauth_request)
2626+ except OAuthError:
2627+ callback = None # 1.0, no callback specified.
2628+ self._check_signature(oauth_request, consumer, None)
2629+ # Fetch a new token.
2630+ token = self.data_store.fetch_request_token(consumer, callback)
2631+ return token
2632+
2633+ def fetch_access_token(self, oauth_request):
2634+ """Processes an access_token request and returns the
2635+ access token on success.
2636+ """
2637+ version = self._get_version(oauth_request)
2638+ consumer = self._get_consumer(oauth_request)
2639+ try:
2640+ verifier = self._get_verifier(oauth_request)
2641+ except OAuthError:
2642+ verifier = None
2643+ # Get the request token.
2644+ token = self._get_token(oauth_request, 'request')
2645+ self._check_signature(oauth_request, consumer, token)
2646+ new_token = self.data_store.fetch_access_token(consumer, token, verifier)
2647+ return new_token
2648+
2649+ def verify_request(self, oauth_request):
2650+ """Verifies an api call and checks all the parameters."""
2651+ # -> consumer and token
2652+ version = self._get_version(oauth_request)
2653+ consumer = self._get_consumer(oauth_request)
2654+ # Get the access token.
2655+ token = self._get_token(oauth_request, 'access')
2656+ self._check_signature(oauth_request, consumer, token)
2657+ parameters = oauth_request.get_nonoauth_parameters()
2658+ return consumer, token, parameters
2659+
2660+ def authorize_token(self, token, user):
2661+ """Authorize a request token."""
2662+ return self.data_store.authorize_request_token(token, user)
2663+
2664+ def get_callback(self, oauth_request):
2665+ """Get the callback URL."""
2666+ return oauth_request.get_parameter('oauth_callback')
2667+
2668+ def build_authenticate_header(self, realm=''):
2669+ """Optional support for the authenticate header."""
2670+ return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
2671+
2672+ def _get_version(self, oauth_request):
2673+ """Verify the correct version request for this server."""
2674+ try:
2675+ version = oauth_request.get_parameter('oauth_version')
2676+ except:
2677+ version = VERSION
2678+ if version and version != self.version:
2679+ raise OAuthError('OAuth version %s not supported.' % str(version))
2680+ return version
2681+
2682+ def _get_signature_method(self, oauth_request):
2683+ """Figure out the signature with some defaults."""
2684+ try:
2685+ signature_method = oauth_request.get_parameter(
2686+ 'oauth_signature_method')
2687+ except:
2688+ signature_method = SIGNATURE_METHOD
2689+ try:
2690+ # Get the signature method object.
2691+ signature_method = self.signature_methods[signature_method]
2692+ except:
2693+ signature_method_names = ', '.join(self.signature_methods.keys())
2694+ raise OAuthError('Signature method %s not supported try one of the '
2695+ 'following: %s' % (signature_method, signature_method_names))
2696+
2697+ return signature_method
2698+
2699+ def _get_consumer(self, oauth_request):
2700+ consumer_key = oauth_request.get_parameter('oauth_consumer_key')
2701+ consumer = self.data_store.lookup_consumer(consumer_key)
2702+ if not consumer:
2703+ raise OAuthError('Invalid consumer.')
2704+ return consumer
2705+
2706+ def _get_token(self, oauth_request, token_type='access'):
2707+ """Try to find the token for the provided request token key."""
2708+ token_field = oauth_request.get_parameter('oauth_token')
2709+ token = self.data_store.lookup_token(token_type, token_field)
2710+ if not token:
2711+ raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
2712+ return token
2713+
2714+ def _get_verifier(self, oauth_request):
2715+ return oauth_request.get_parameter('oauth_verifier')
2716+
2717+ def _check_signature(self, oauth_request, consumer, token):
2718+ timestamp, nonce = oauth_request._get_timestamp_nonce()
2719+ self._check_timestamp(timestamp)
2720+ self._check_nonce(consumer, token, nonce)
2721+ signature_method = self._get_signature_method(oauth_request)
2722+ try:
2723+ signature = oauth_request.get_parameter('oauth_signature')
2724+ except:
2725+ raise OAuthError('Missing signature.')
2726+ # Validate the signature.
2727+ valid_sig = signature_method.check_signature(oauth_request, consumer,
2728+ token, signature)
2729+ if not valid_sig:
2730+ key, base = signature_method.build_signature_base_string(
2731+ oauth_request, consumer, token)
2732+ raise OAuthError('Invalid signature. Expected signature base '
2733+ 'string: %s' % base)
2734+ built = signature_method.build_signature(oauth_request, consumer, token)
2735+
2736+ def _check_timestamp(self, timestamp):
2737+ """Verify that timestamp is recentish."""
2738+ timestamp = int(timestamp)
2739+ now = int(time.time())
2740+ lapsed = abs(now - timestamp)
2741+ if lapsed > self.timestamp_threshold:
2742+ raise OAuthError('Expired timestamp: given %d and now %s has a '
2743+ 'greater difference than threshold %d' %
2744+ (timestamp, now, self.timestamp_threshold))
2745+
2746+ def _check_nonce(self, consumer, token, nonce):
2747+ """Verify that the nonce is uniqueish."""
2748+ nonce = self.data_store.lookup_nonce(consumer, token, nonce)
2749+ if nonce:
2750+ raise OAuthError('Nonce already used: %s' % str(nonce))
2751+
2752+
2753+class OAuthClient(object):
2754+ """OAuthClient is a worker to attempt to execute a request."""
2755+ consumer = None
2756+ token = None
2757+
2758+ def __init__(self, oauth_consumer, oauth_token):
2759+ self.consumer = oauth_consumer
2760+ self.token = oauth_token
2761+
2762+ def get_consumer(self):
2763+ return self.consumer
2764+
2765+ def get_token(self):
2766+ return self.token
2767+
2768+ def fetch_request_token(self, oauth_request):
2769+ """-> OAuthToken."""
2770+ raise NotImplementedError
2771+
2772+ def fetch_access_token(self, oauth_request):
2773+ """-> OAuthToken."""
2774+ raise NotImplementedError
2775+
2776+ def access_resource(self, oauth_request):
2777+ """-> Some protected resource."""
2778+ raise NotImplementedError
2779+
2780+
2781+class OAuthDataStore(object):
2782+ """A database abstraction used to lookup consumers and tokens."""
2783+
2784+ def lookup_consumer(self, key):
2785+ """-> OAuthConsumer."""
2786+ raise NotImplementedError
2787+
2788+ def lookup_token(self, oauth_consumer, token_type, token_token):
2789+ """-> OAuthToken."""
2790+ raise NotImplementedError
2791+
2792+ def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
2793+ """-> OAuthToken."""
2794+ raise NotImplementedError
2795+
2796+ def fetch_request_token(self, oauth_consumer, oauth_callback):
2797+ """-> OAuthToken."""
2798+ raise NotImplementedError
2799+
2800+ def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
2801+ """-> OAuthToken."""
2802+ raise NotImplementedError
2803+
2804+ def authorize_request_token(self, oauth_token, user):
2805+ """-> OAuthToken."""
2806+ raise NotImplementedError
2807+
2808+
2809+class OAuthSignatureMethod(object):
2810+ """A strategy class that implements a signature method."""
2811+ def get_name(self):
2812+ """-> str."""
2813+ raise NotImplementedError
2814+
2815+ def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
2816+ """-> str key, str raw."""
2817+ raise NotImplementedError
2818+
2819+ def build_signature(self, oauth_request, oauth_consumer, oauth_token):
2820+ """-> str."""
2821+ raise NotImplementedError
2822+
2823+ def check_signature(self, oauth_request, consumer, token, signature):
2824+ built = self.build_signature(oauth_request, consumer, token)
2825+ return built == signature
2826+
2827+
2828+class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
2829+
2830+ def get_name(self):
2831+ return 'HMAC-SHA1'
2832+
2833+ def build_signature_base_string(self, oauth_request, consumer, token):
2834+ sig = (
2835+ escape(oauth_request.get_normalized_http_method()),
2836+ escape(oauth_request.get_normalized_http_url()),
2837+ escape(oauth_request.get_normalized_parameters()),
2838+ )
2839+
2840+ key = '%s&' % escape(consumer.secret)
2841+ if token:
2842+ key += escape(token.secret)
2843+ raw = '&'.join(sig)
2844+ return key, raw
2845+
2846+ def build_signature(self, oauth_request, consumer, token):
2847+ """Builds the base signature string."""
2848+ key, raw = self.build_signature_base_string(oauth_request, consumer,
2849+ token)
2850+
2851+ # HMAC object.
2852+ try:
2853+ import hashlib # 2.5
2854+ hashed = hmac.new(key, raw, hashlib.sha1)
2855+ except:
2856+ import sha # Deprecated
2857+ hashed = hmac.new(key, raw, sha)
2858+
2859+ # Calculate the digest base 64.
2860+ return binascii.b2a_base64(hashed.digest())[:-1]
2861+
2862+
2863+class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
2864+
2865+ def get_name(self):
2866+ return 'PLAINTEXT'
2867+
2868+ def build_signature_base_string(self, oauth_request, consumer, token):
2869+ """Concatenates the consumer key and secret."""
2870+ sig = '%s&' % escape(consumer.secret)
2871+ if token:
2872+ sig = sig + escape(token.secret)
2873+ return sig, sig
2874+
2875+ def build_signature(self, oauth_request, consumer, token):
2876+ key, raw = self.build_signature_base_string(oauth_request, consumer,
2877+ token)
2878+ return key
2879\ No newline at end of file
2880
2881=== added file 'GTG/backends/tweepy/parsers.py'
2882--- GTG/backends/tweepy/parsers.py 1970-01-01 00:00:00 +0000
2883+++ GTG/backends/tweepy/parsers.py 2010-08-25 16:31:45 +0000
2884@@ -0,0 +1,84 @@
2885+# Tweepy
2886+# Copyright 2009-2010 Joshua Roesslein
2887+# See LICENSE for details.
2888+
2889+from tweepy.models import ModelFactory
2890+from tweepy.utils import import_simplejson
2891+from tweepy.error import TweepError
2892+
2893+
2894+class Parser(object):
2895+
2896+ def parse(self, method, payload):
2897+ """
2898+ Parse the response payload and return the result.
2899+ Returns a tuple that contains the result data and the cursors
2900+ (or None if not present).
2901+ """
2902+ raise NotImplementedError
2903+
2904+ def parse_error(self, payload):
2905+ """
2906+ Parse the error message from payload.
2907+ If unable to parse the message, throw an exception
2908+ and default error message will be used.
2909+ """
2910+ raise NotImplementedError
2911+
2912+
2913+class JSONParser(Parser):
2914+
2915+ payload_format = 'json'
2916+
2917+ def __init__(self):
2918+ self.json_lib = import_simplejson()
2919+
2920+ def parse(self, method, payload):
2921+ try:
2922+ json = self.json_lib.loads(payload)
2923+ except Exception, e:
2924+ raise TweepError('Failed to parse JSON payload: %s' % e)
2925+
2926+ if isinstance(json, dict) and 'previous_cursor' in json and 'next_cursor' in json:
2927+ cursors = json['previous_cursor'], json['next_cursor']
2928+ return json, cursors
2929+ else:
2930+ return json
2931+
2932+ def parse_error(self, payload):
2933+ error = self.json_lib.loads(payload)
2934+ if error.has_key('error'):
2935+ return error['error']
2936+ else:
2937+ return error['errors']
2938+
2939+
2940+class ModelParser(JSONParser):
2941+
2942+ def __init__(self, model_factory=None):
2943+ JSONParser.__init__(self)
2944+ self.model_factory = model_factory or ModelFactory
2945+
2946+ def parse(self, method, payload):
2947+ try:
2948+ if method.payload_type is None: return
2949+ model = getattr(self.model_factory, method.payload_type)
2950+ except AttributeError:
2951+ raise TweepError('No model for this payload type: %s' % method.payload_type)
2952+
2953+ json = JSONParser.parse(self, method, payload)
2954+ if isinstance(json, tuple):
2955+ json, cursors = json
2956+ else:
2957+ cursors = None
2958+
2959+ if method.payload_list:
2960+ result = model.parse_list(method.api, json)
2961+ else:
2962+ result = model.parse(method.api, json)
2963+
2964+ if cursors:
2965+ return result, cursors
2966+ else:
2967+ return result
2968+
2969
2970=== added file 'GTG/backends/tweepy/streaming.py'
2971--- GTG/backends/tweepy/streaming.py 1970-01-01 00:00:00 +0000
2972+++ GTG/backends/tweepy/streaming.py 2010-08-25 16:31:45 +0000
2973@@ -0,0 +1,200 @@
2974+# Tweepy
2975+# Copyright 2009-2010 Joshua Roesslein
2976+# See LICENSE for details.
2977+
2978+import httplib
2979+from socket import timeout
2980+from threading import Thread
2981+from time import sleep
2982+import urllib
2983+
2984+from tweepy.auth import BasicAuthHandler
2985+from tweepy.models import Status
2986+from tweepy.api import API
2987+from tweepy.error import TweepError
2988+
2989+from tweepy.utils import import_simplejson
2990+json = import_simplejson()
2991+
2992+STREAM_VERSION = 1
2993+
2994+
2995+class StreamListener(object):
2996+
2997+ def __init__(self, api=None):
2998+ self.api = api or API()
2999+
3000+ def on_data(self, data):
3001+ """Called when raw data is received from connection.
3002+
3003+ Override this method if you wish to manually handle
3004+ the stream data. Return False to stop stream and close connection.
3005+ """
3006+
3007+ if 'in_reply_to_status_id' in data:
3008+ status = Status.parse(self.api, json.loads(data))
3009+ if self.on_status(status) is False:
3010+ return False
3011+ elif 'delete' in data:
3012+ delete = json.loads(data)['delete']['status']
3013+ if self.on_delete(delete['id'], delete['user_id']) is False:
3014+ return False
3015+ elif 'limit' in data:
3016+ if self.on_limit(json.loads(data)['limit']['track']) is False:
3017+ return False
3018+
3019+ def on_status(self, status):
3020+ """Called when a new status arrives"""
3021+ return
3022+
3023+ def on_delete(self, status_id, user_id):
3024+ """Called when a delete notice arrives for a status"""
3025+ return
3026+
3027+ def on_limit(self, track):
3028+ """Called when a limitation notice arrvies"""
3029+ return
3030+
3031+ def on_error(self, status_code):
3032+ """Called when a non-200 status code is returned"""
3033+ return False
3034+
3035+ def on_timeout(self):
3036+ """Called when stream connection times out"""
3037+ return
3038+
3039+
3040+class Stream(object):
3041+
3042+ host = 'stream.twitter.com'
3043+
3044+ def __init__(self, username, password, listener, timeout=5.0, retry_count = None,
3045+ retry_time = 10.0, snooze_time = 5.0, buffer_size=1500, headers=None):
3046+ self.auth = BasicAuthHandler(username, password)
3047+ self.running = False
3048+ self.timeout = timeout
3049+ self.retry_count = retry_count
3050+ self.retry_time = retry_time
3051+ self.snooze_time = snooze_time
3052+ self.buffer_size = buffer_size
3053+ self.listener = listener
3054+ self.api = API()
3055+ self.headers = headers or {}
3056+ self.body = None
3057+
3058+ def _run(self):
3059+ # setup
3060+ self.auth.apply_auth(None, None, self.headers, None)
3061+
3062+ # enter loop
3063+ error_counter = 0
3064+ conn = None
3065+ exception = None
3066+ while self.running:
3067+ if self.retry_count and error_counter > self.retry_count:
3068+ # quit if error count greater than retry count
3069+ break
3070+ try:
3071+ conn = httplib.HTTPConnection(self.host)
3072+ conn.connect()
3073+ conn.sock.settimeout(self.timeout)
3074+ conn.request('POST', self.url, self.body, headers=self.headers)
3075+ resp = conn.getresponse()
3076+ if resp.status != 200:
3077+ if self.listener.on_error(resp.status) is False:
3078+ break
3079+ error_counter += 1
3080+ sleep(self.retry_time)
3081+ else:
3082+ error_counter = 0
3083+ self._read_loop(resp)
3084+ except timeout:
3085+ if self.listener.on_timeout() == False:
3086+ break
3087+ if self.running is False:
3088+ break
3089+ conn.close()
3090+ sleep(self.snooze_time)
3091+ except Exception, exception:
3092+ # any other exception is fatal, so kill loop
3093+ break
3094+
3095+ # cleanup
3096+ self.running = False
3097+ if conn:
3098+ conn.close()
3099+
3100+ if exception:
3101+ raise exception
3102+
3103+ def _read_loop(self, resp):
3104+ data = ''
3105+ while self.running:
3106+ if resp.isclosed():
3107+ break
3108+
3109+ # read length
3110+ length = ''
3111+ while True:
3112+ c = resp.read(1)
3113+ if c == '\n':
3114+ break
3115+ length += c
3116+ length = length.strip()
3117+ if length.isdigit():
3118+ length = int(length)
3119+ else:
3120+ continue
3121+
3122+ # read data and pass into listener
3123+ data = resp.read(length)
3124+ if self.listener.on_data(data) is False:
3125+ self.running = False
3126+
3127+ def _start(self, async):
3128+ self.running = True
3129+ if async:
3130+ Thread(target=self._run).start()
3131+ else:
3132+ self._run()
3133+
3134+ def firehose(self, count=None, async=False):
3135+ if self.running:
3136+ raise TweepError('Stream object already connected!')
3137+ self.url = '/%i/statuses/firehose.json?delimited=length' % STREAM_VERSION
3138+ if count:
3139+ self.url += '&count=%s' % count
3140+ self._start(async)
3141+
3142+ def retweet(self, async=False):
3143+ if self.running:
3144+ raise TweepError('Stream object already connected!')
3145+ self.url = '/%i/statuses/retweet.json?delimited=length' % STREAM_VERSION
3146+ self._start(async)
3147+
3148+ def sample(self, count=None, async=False):
3149+ if self.running:
3150+ raise TweepError('Stream object already connected!')
3151+ self.url = '/%i/statuses/sample.json?delimited=length' % STREAM_VERSION
3152+ if count:
3153+ self.url += '&count=%s' % count
3154+ self._start(async)
3155+
3156+ def filter(self, follow=None, track=None, async=False):
3157+ params = {}
3158+ self.headers['Content-type'] = "application/x-www-form-urlencoded"
3159+ if self.running:
3160+ raise TweepError('Stream object already connected!')
3161+ self.url = '/%i/statuses/filter.json?delimited=length' % STREAM_VERSION
3162+ if follow:
3163+ params['follow'] = ','.join(map(str, follow))
3164+ if track:
3165+ params['track'] = ','.join(map(str, track))
3166+ self.body = urllib.urlencode(params)
3167+ self._start(async)
3168+
3169+ def disconnect(self):
3170+ if self.running is False:
3171+ return
3172+ self.running = False
3173+
3174
3175=== added file 'GTG/backends/tweepy/utils.py'
3176--- GTG/backends/tweepy/utils.py 1970-01-01 00:00:00 +0000
3177+++ GTG/backends/tweepy/utils.py 2010-08-25 16:31:45 +0000
3178@@ -0,0 +1,98 @@
3179+# Tweepy
3180+# Copyright 2010 Joshua Roesslein
3181+# See LICENSE for details.
3182+
3183+from datetime import datetime
3184+import time
3185+import htmlentitydefs
3186+import re
3187+import locale
3188+
3189+
3190+def parse_datetime(string):
3191+ # Set locale for date parsing
3192+ locale.setlocale(locale.LC_TIME, 'C')
3193+
3194+ # We must parse datetime this way to work in python 2.4
3195+ date = datetime(*(time.strptime(string, '%a %b %d %H:%M:%S +0000 %Y')[0:6]))
3196+
3197+ # Reset locale back to the default setting
3198+ locale.setlocale(locale.LC_TIME, '')
3199+ return date
3200+
3201+
3202+def parse_html_value(html):
3203+
3204+ return html[html.find('>')+1:html.rfind('<')]
3205+
3206+
3207+def parse_a_href(atag):
3208+
3209+ start = atag.find('"') + 1
3210+ end = atag.find('"', start)
3211+ return atag[start:end]
3212+
3213+
3214+def parse_search_datetime(string):
3215+ # Set locale for date parsing
3216+ locale.setlocale(locale.LC_TIME, 'C')
3217+
3218+ # We must parse datetime this way to work in python 2.4
3219+ date = datetime(*(time.strptime(string, '%a, %d %b %Y %H:%M:%S +0000')[0:6]))
3220+
3221+ # Reset locale back to the default setting
3222+ locale.setlocale(locale.LC_TIME, '')
3223+ return date
3224+
3225+
3226+def unescape_html(text):
3227+ """Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)"""
3228+ def fixup(m):
3229+ text = m.group(0)
3230+ if text[:2] == "&#":
3231+ # character reference
3232+ try:
3233+ if text[:3] == "&#x":
3234+ return unichr(int(text[3:-1], 16))
3235+ else:
3236+ return unichr(int(text[2:-1]))
3237+ except ValueError:
3238+ pass
3239+ else:
3240+ # named entity
3241+ try:
3242+ text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
3243+ except KeyError:
3244+ pass
3245+ return text # leave as is
3246+ return re.sub("&#?\w+;", fixup, text)
3247+
3248+
3249+def convert_to_utf8_str(arg):
3250+ # written by Michael Norton (http://docondev.blogspot.com/)
3251+ if isinstance(arg, unicode):
3252+ arg = arg.encode('utf-8')
3253+ elif not isinstance(arg, str):
3254+ arg = str(arg)
3255+ return arg
3256+
3257+
3258+
3259+def import_simplejson():
3260+ try:
3261+ import simplejson as json
3262+ except ImportError:
3263+ try:
3264+ import json # Python 2.6+
3265+ except ImportError:
3266+ try:
3267+ from django.utils import simplejson as json # Google App Engine
3268+ except ImportError:
3269+ raise ImportError, "Can't load a json library"
3270+
3271+ return json
3272+
3273+def list_to_csv(item_list):
3274+ if item_list:
3275+ return ','.join([str(i) for i in item_list])
3276+
3277
3278=== added file 'data/icons/hicolor/scalable/apps/backend_twitter.png'
3279Binary 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-25 16:31:45 +0000 differ

Subscribers

People subscribed via source and target branches

to status/vote changes: