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
=== added file 'GTG/backends/backend_twitter.py'
--- GTG/backends/backend_twitter.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/backend_twitter.py 2010-08-25 16:31:45 +0000
@@ -0,0 +1,338 @@
1# -*- coding: utf-8 -*-
2# -----------------------------------------------------------------------------
3# Getting Things Gnome! - a personal organizer for the GNOME desktop
4# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
5#
6# This program is free software: you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation, either version 3 of the License, or (at your option) any later
9# version.
10#
11# This program is distributed in the hope that it will be useful, but WITHOUT
12# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14# details.
15#
16# You should have received a copy of the GNU General Public License along with
17# this program. If not, see <http://www.gnu.org/licenses/>.
18# -----------------------------------------------------------------------------
19
20'''
21Twitter backend: imports direct messages, replies and/or the user timeline.
22Authenticates through OAuth.
23'''
24import os
25import re
26import sys
27import uuid
28import subprocess
29
30#the tweepy library is not packaged for Debian/Ubuntu. Thus, a copy of it is
31# kept in the GTG/backends directory
32sys.path.append("GTG/backends")
33import tweepy as tweepy
34
35from GTG import _
36from GTG.backends.genericbackend import GenericBackend
37from GTG.core import CoreConfig
38from GTG.backends.backendsignals import BackendSignals
39from GTG.backends.periodicimportbackend import PeriodicImportBackend
40from GTG.backends.syncengine import SyncEngine
41from GTG.tools.logger import Log
42
43
44class Backend(PeriodicImportBackend):
45 '''
46 Twitter backend: imports direct messages, replies and/or the user timeline.
47 Authenticates through OAuth.
48 '''
49
50
51 _general_description = { \
52 GenericBackend.BACKEND_NAME: "backend_twitter", \
53 GenericBackend.BACKEND_HUMAN_NAME: _("Twitter"), \
54 GenericBackend.BACKEND_AUTHORS: ["Luca Invernizzi"], \
55 GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_IMPORT, \
56 GenericBackend.BACKEND_DESCRIPTION: \
57 _("Imports your twitter messages into your GTG " + \
58 "tasks. You can choose to either import all your " + \
59 "messages or just those with a set of hash tags. \n" + \
60 "The message will be interpreted following this" + \
61 " format: \n" + \
62 "<b>my task title, task description #tag @anothertag</b>\n" + \
63 " Tags can be anywhere in the message"),\
64 }
65
66 _static_parameters = { \
67 "period": { \
68 GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \
69 GenericBackend.PARAM_DEFAULT_VALUE: 2, },
70 "import-tags": { \
71 GenericBackend.PARAM_TYPE: GenericBackend.TYPE_LIST_OF_STRINGS, \
72 GenericBackend.PARAM_DEFAULT_VALUE: ["#todo"], },
73 "import-from-replies": { \
74 GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
75 GenericBackend.PARAM_DEFAULT_VALUE: False, },
76 "import-from-my-tweets": { \
77 GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
78 GenericBackend.PARAM_DEFAULT_VALUE: False, },
79 "import-from-direct-messages": { \
80 GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
81 GenericBackend.PARAM_DEFAULT_VALUE: True, },
82 }
83
84 CONSUMER_KEY = "UDRov5YF3ZUinftvVBoeyA"
85 #This is supposed to be secret (because of OAuth), but that's not possible.
86 #A xAuth alternative is possible, but it's enabled on mail request if the
87 # twitter staff considers your application worthy of such honour.
88 CONSUMER_SECRET = "BApykCPskoZ0g4QpVS7yC7TrZntm87KruSeJwvqTg"
89
90 def __init__(self, parameters):
91 '''
92 See GenericBackend for an explanation of this function.
93 Re-loads the saved state of the synchronization
94 '''
95 super(Backend, self).__init__(parameters)
96 #loading the list of already imported tasks
97 self.data_path = os.path.join('backends/twitter/', "tasks_dict-%s" %\
98 self.get_id())
99 self.sync_engine = self._load_pickled_file(self.data_path, \
100 SyncEngine())
101 #loading the parameters for oauth
102 self.auth_path = os.path.join('backends/twitter/', "auth-%s" %\
103 self.get_id())
104 self.auth_params = self._load_pickled_file(self.auth_path, None)
105 self.authenticated = False
106 self.authenticating = False
107
108 def save_state(self):
109 '''
110 See GenericBackend for an explanation of this function.
111 Saves the state of the synchronization.
112 '''
113 self._store_pickled_file(self.data_path, self.sync_engine)
114
115###############################################################################
116### IMPORTING TWEETS ##########################################################
117###############################################################################
118
119 def do_periodic_import(self):
120 '''
121 See GenericBackend for an explanation of this function.
122 '''
123 #abort if authentication is in progress or hasn't been done (in which
124 # case, start it)
125 self.cancellation_point()
126 if not self.authenticated:
127 if not self.authenticating:
128 self._start_authentication()
129 return
130 #select what to import
131 tweets_to_import = []
132 if self._parameters["import-from-direct-messages"]:
133 tweets_to_import += self.api.direct_messages()
134 if self._parameters["import-from-my-tweets"]:
135 tweets_to_import += self.api.user_timeline()
136 if self._parameters["import-from-replies"]:
137 tweets_to_import += self.api.mentions()
138 #do the import
139 for tweet in tweets_to_import:
140 self._process_tweet(tweet)
141
142 def _process_tweet(self, tweet):
143 '''
144 Given a tweet, checks if a task representing it must be
145 created in GTG and, if so, it creates it.
146
147 @param tweet: a tweet.
148 '''
149 self.cancellation_point()
150 tweet_id = str(tweet.id)
151 is_syncable = self._is_tweet_syncable(tweet)
152 #the "lambda" is because we don't consider tweets deletion (to be
153 # faster)
154 action, tid = self.sync_engine.analyze_remote_id(\
155 tweet_id, \
156 self.datastore.has_task, \
157 lambda tweet_id: True, \
158 is_syncable)
159 Log.debug("processing tweet (%s, %s)" % (action, is_syncable))
160
161 self.cancellation_point()
162 if action == None or action == SyncEngine.UPDATE:
163 return
164
165 elif action == SyncEngine.ADD:
166 tid = str(uuid.uuid4())
167 task = self.datastore.task_factory(tid)
168 self._populate_task(task, tweet)
169 #we care only to add tweets and if the list of tags which must be
170 #imported changes (lost-syncability can happen). Thus, we don't
171 # care about SyncMeme(s)
172 self.sync_engine.record_relationship(local_id = tid,\
173 remote_id = tweet_id, \
174 meme = None)
175 self.datastore.push_task(task)
176
177 elif action == SyncEngine.LOST_SYNCABILITY:
178 self.sync_engine.break_relationship(remote_id = tweet_id)
179 self.datastore.request_task_deletion(tid)
180
181 self.save_state()
182
183
184 def _populate_task(self, task, message):
185 '''
186 Given a twitter message and a GTG task, fills the task with the content
187 of the message
188 '''
189 #adding the sender as a tag
190 #this works only for some messages types (not for the user timeline)
191 user = None
192 try:
193 user = message.user.screen_name
194 except:
195 pass
196 if user:
197 task.add_tag("@" + user)
198
199 #setting title, text and tags
200 text = message.text
201 #convert #hastags to @tags
202 matches = re.finditer("(?<![^|\s])(#\w+)", text)
203 for g in matches:
204 text = text[:g.start()] + '@' + text[g.start() + 1:]
205 #add tags objects (it's not enough to have @tag in the text to add a
206 # tag
207 for tag in self._extract_tags_from_text(text):
208 task.add_tag(tag)
209
210 split_text = text.split(",", 1)
211 task.set_title(split_text[0])
212 if len(split_text) > 1:
213 task.set_text(split_text[1])
214
215 task.add_remote_id(self.get_id(), str(message.id))
216
217 def _is_tweet_syncable(self, tweet):
218 '''
219 Returns True if the given tweet matches the user-specified tags to be
220 synced
221
222 @param tweet: a tweet
223 '''
224 if CoreConfig.ALLTASKS_TAG in self._parameters["import-tags"]:
225 return True
226 else:
227 tags = set(Backend._extract_tags_from_text(tweet.text))
228 return tags.intersection(set(self._parameters["import-tags"])) \
229 != set()
230
231 @staticmethod
232 def _extract_tags_from_text(text):
233 '''
234 Given a string, returns a list of @tags and #hashtags
235 '''
236 return list(re.findall(r'(?:^|[\s])((?:#|@)\w+)', text))
237
238###############################################################################
239### AUTHENTICATION ############################################################
240###############################################################################
241
242 def _start_authentication(self):
243 '''
244 Fist step of authentication: opening the browser with the oauth page
245 '''
246
247 #NOTE: just found out that tweepy works with identi.ca (update:
248 # currently broken!).
249 # However, twitter is moving to oauth only authentication, while
250 # identica uses standard login. For now, I'll keep the backends
251 # separate, using two different libraries (Invernizzi)
252 #auth = tweepy.BasicAuthHandler(username, password,
253 #host ='identi.ca', api_root = '/api',
254 #secure=True)
255 self.auth = tweepy.OAuthHandler(self.CONSUMER_KEY, \
256 self.CONSUMER_SECRET)
257 self.cancellation_point()
258 if self.auth_params == None:
259 #no previous contact with the server has been made: no stored
260 # oauth token found
261 self.authenticating = True
262 subprocess.Popen(['xdg-open', self.auth.get_authorization_url()])
263 BackendSignals().interaction_requested(self.get_id(),
264 "You need to authenticate to <b>Twitter</b>. A browser"
265 " is opening with the correct page. When you have "
266 " received a PIN code, press 'Continue'.", \
267 BackendSignals().INTERACTION_TEXT,
268 "on_authentication_step")
269 else:
270 #we have gone through authentication successfully before.
271 self.cancellation_point()
272 try:
273 self.auth.set_access_token(self.auth_params[0],\
274 self.auth_params[1])
275 except tweepy.TweepError, e:
276 self._on_auth_error(e)
277 return
278 self.cancellation_point()
279 self._end_authentication()
280
281 def on_authentication_step(self, step_type = "", pin = ""):
282 '''
283 Handles the various steps of authentication. It's the only callback
284 function the UI knows about this backend.
285
286 @param step_type: if "get_ui_dialog_text", returns the text to be put
287 in the dialog requesting the pin.
288 if "set_text", the UI is feeding the backend with
289 the pin the user provided
290 @param pin: contains the pin if step_type == "set_text"
291 '''
292 if step_type == "get_ui_dialog_text":
293 return "PIN request", "Insert the PIN you should have received "\
294 "through your web browser here:"
295 elif step_type == "set_text":
296 try:
297 token = self.auth.get_access_token(verifier = pin)
298 except tweepy.TweepError, e:
299 self._on_auth_error(e)
300 return
301 self.auth_params = (token.key, token.secret)
302 self._store_pickled_file(self.auth_path, self.auth_params)
303 self._end_authentication()
304
305 def _end_authentication(self):
306 '''
307 Last step of authentication. Creates the API objects and starts
308 importing tweets
309 '''
310 self.authenticated = True
311 self.authenticating = False
312 self.api = tweepy.API(auth_handler = self.auth, \
313 secure = True, \
314 retry_count = 3)
315 self.cancellation_point()
316 self.start_get_tasks()
317
318 def _on_auth_error(self, exception):
319 '''
320 On authentication error, informs the user.
321
322 @param exception: the Exception object that was raised during
323 authentication
324 '''
325 if isinstance(exception, tweepy.TweepError):
326 if exception.reason == "HTTP Error 401: Unauthorized":
327 self.auth_params = None
328 self._store_pickled_file(self.auth_path, self.auth_params)
329 self.quit(disable = True)
330 BackendSignals().backend_failed(self.get_id(), \
331 BackendSignals.ERRNO_AUTHENTICATION)
332
333 def signal_network_down(self):
334 '''
335 If the network is unresponsive, inform the user
336 '''
337 BackendSignals().backend_failed(self.get_id(), \
338 BackendSignals.ERRNO_NETWORK)
0339
=== added directory 'GTG/backends/tweepy'
=== added file 'GTG/backends/tweepy/__init__.py'
--- GTG/backends/tweepy/__init__.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/__init__.py 2010-08-25 16:31:45 +0000
@@ -0,0 +1,27 @@
1# Tweepy
2# Copyright 2009-2010 Joshua Roesslein
3# See LICENSE for details.
4
5"""
6Tweepy Twitter API library
7"""
8__version__ = '1.7.1'
9__author__ = 'Joshua Roesslein'
10__license__ = 'MIT'
11
12from tweepy.models import Status, User, DirectMessage, Friendship, SavedSearch, SearchResult, ModelFactory
13from tweepy.error import TweepError
14from tweepy.api import API
15from tweepy.cache import Cache, MemoryCache, FileCache
16from tweepy.auth import BasicAuthHandler, OAuthHandler
17from tweepy.streaming import Stream, StreamListener
18from tweepy.cursor import Cursor
19
20# Global, unauthenticated instance of API
21api = API()
22
23def debug(enable=True, level=1):
24
25 import httplib
26 httplib.HTTPConnection.debuglevel = level
27
028
=== added file 'GTG/backends/tweepy/api.py'
--- GTG/backends/tweepy/api.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/api.py 2010-08-25 16:31:45 +0000
@@ -0,0 +1,735 @@
1# Tweepy
2# Copyright 2009-2010 Joshua Roesslein
3# See LICENSE for details.
4
5import os
6import mimetypes
7
8from tweepy.binder import bind_api
9from tweepy.error import TweepError
10from tweepy.parsers import ModelParser
11from tweepy.utils import list_to_csv
12
13
14class API(object):
15 """Twitter API"""
16
17 def __init__(self, auth_handler=None,
18 host='api.twitter.com', search_host='search.twitter.com',
19 cache=None, secure=False, api_root='/1', search_root='',
20 retry_count=0, retry_delay=0, retry_errors=None,
21 parser=None):
22 self.auth = auth_handler
23 self.host = host
24 self.search_host = search_host
25 self.api_root = api_root
26 self.search_root = search_root
27 self.cache = cache
28 self.secure = secure
29 self.retry_count = retry_count
30 self.retry_delay = retry_delay
31 self.retry_errors = retry_errors
32 self.parser = parser or ModelParser()
33
34 """ statuses/public_timeline """
35 public_timeline = bind_api(
36 path = '/statuses/public_timeline.json',
37 payload_type = 'status', payload_list = True,
38 allowed_param = []
39 )
40
41 """ statuses/home_timeline """
42 home_timeline = bind_api(
43 path = '/statuses/home_timeline.json',
44 payload_type = 'status', payload_list = True,
45 allowed_param = ['since_id', 'max_id', 'count', 'page'],
46 require_auth = True
47 )
48
49 """ statuses/friends_timeline """
50 friends_timeline = bind_api(
51 path = '/statuses/friends_timeline.json',
52 payload_type = 'status', payload_list = True,
53 allowed_param = ['since_id', 'max_id', 'count', 'page'],
54 require_auth = True
55 )
56
57 """ statuses/user_timeline """
58 user_timeline = bind_api(
59 path = '/statuses/user_timeline.json',
60 payload_type = 'status', payload_list = True,
61 allowed_param = ['id', 'user_id', 'screen_name', 'since_id',
62 'max_id', 'count', 'page']
63 )
64
65 """ statuses/mentions """
66 mentions = bind_api(
67 path = '/statuses/mentions.json',
68 payload_type = 'status', payload_list = True,
69 allowed_param = ['since_id', 'max_id', 'count', 'page'],
70 require_auth = True
71 )
72
73 """/statuses/:id/retweeted_by.format"""
74 retweeted_by = bind_api(
75 path = '/statuses/{id}/retweeted_by.json',
76 payload_type = 'status', payload_list = True,
77 allowed_param = ['id', 'count', 'page'],
78 require_auth = True
79 )
80
81 """/statuses/:id/retweeted_by/ids.format"""
82 retweeted_by_ids = bind_api(
83 path = '/statuses/{id}/retweeted_by/ids.json',
84 payload_type = 'ids',
85 allowed_param = ['id', 'count', 'page'],
86 require_auth = True
87 )
88
89 """ statuses/retweeted_by_me """
90 retweeted_by_me = bind_api(
91 path = '/statuses/retweeted_by_me.json',
92 payload_type = 'status', payload_list = True,
93 allowed_param = ['since_id', 'max_id', 'count', 'page'],
94 require_auth = True
95 )
96
97 """ statuses/retweeted_to_me """
98 retweeted_to_me = bind_api(
99 path = '/statuses/retweeted_to_me.json',
100 payload_type = 'status', payload_list = True,
101 allowed_param = ['since_id', 'max_id', 'count', 'page'],
102 require_auth = True
103 )
104
105 """ statuses/retweets_of_me """
106 retweets_of_me = bind_api(
107 path = '/statuses/retweets_of_me.json',
108 payload_type = 'status', payload_list = True,
109 allowed_param = ['since_id', 'max_id', 'count', 'page'],
110 require_auth = True
111 )
112
113 """ statuses/show """
114 get_status = bind_api(
115 path = '/statuses/show.json',
116 payload_type = 'status',
117 allowed_param = ['id']
118 )
119
120 """ statuses/update """
121 update_status = bind_api(
122 path = '/statuses/update.json',
123 method = 'POST',
124 payload_type = 'status',
125 allowed_param = ['status', 'in_reply_to_status_id', 'lat', 'long', 'source', 'place_id'],
126 require_auth = True
127 )
128
129 """ statuses/destroy """
130 destroy_status = bind_api(
131 path = '/statuses/destroy.json',
132 method = 'DELETE',
133 payload_type = 'status',
134 allowed_param = ['id'],
135 require_auth = True
136 )
137
138 """ statuses/retweet """
139 retweet = bind_api(
140 path = '/statuses/retweet/{id}.json',
141 method = 'POST',
142 payload_type = 'status',
143 allowed_param = ['id'],
144 require_auth = True
145 )
146
147 """ statuses/retweets """
148 retweets = bind_api(
149 path = '/statuses/retweets/{id}.json',
150 payload_type = 'status', payload_list = True,
151 allowed_param = ['id', 'count'],
152 require_auth = True
153 )
154
155 """ users/show """
156 get_user = bind_api(
157 path = '/users/show.json',
158 payload_type = 'user',
159 allowed_param = ['id', 'user_id', 'screen_name']
160 )
161
162 """ Perform bulk look up of users from user ID or screenname """
163 def lookup_users(self, user_ids=None, screen_names=None):
164 return self._lookup_users(list_to_csv(user_ids), list_to_csv(screen_names))
165
166 _lookup_users = bind_api(
167 path = '/users/lookup.json',
168 payload_type = 'user', payload_list = True,
169 allowed_param = ['user_id', 'screen_name'],
170 require_auth = True
171 )
172
173 """ Get the authenticated user """
174 def me(self):
175 return self.get_user(screen_name=self.auth.get_username())
176
177 """ users/search """
178 search_users = bind_api(
179 path = '/users/search.json',
180 payload_type = 'user', payload_list = True,
181 require_auth = True,
182 allowed_param = ['q', 'per_page', 'page']
183 )
184
185 """ statuses/friends """
186 friends = bind_api(
187 path = '/statuses/friends.json',
188 payload_type = 'user', payload_list = True,
189 allowed_param = ['id', 'user_id', 'screen_name', 'page', 'cursor']
190 )
191
192 """ statuses/followers """
193 followers = bind_api(
194 path = '/statuses/followers.json',
195 payload_type = 'user', payload_list = True,
196 allowed_param = ['id', 'user_id', 'screen_name', 'page', 'cursor']
197 )
198
199 """ direct_messages """
200 direct_messages = bind_api(
201 path = '/direct_messages.json',
202 payload_type = 'direct_message', payload_list = True,
203 allowed_param = ['since_id', 'max_id', 'count', 'page'],
204 require_auth = True
205 )
206
207 """ direct_messages/sent """
208 sent_direct_messages = bind_api(
209 path = '/direct_messages/sent.json',
210 payload_type = 'direct_message', payload_list = True,
211 allowed_param = ['since_id', 'max_id', 'count', 'page'],
212 require_auth = True
213 )
214
215 """ direct_messages/new """
216 send_direct_message = bind_api(
217 path = '/direct_messages/new.json',
218 method = 'POST',
219 payload_type = 'direct_message',
220 allowed_param = ['user', 'screen_name', 'user_id', 'text'],
221 require_auth = True
222 )
223
224 """ direct_messages/destroy """
225 destroy_direct_message = bind_api(
226 path = '/direct_messages/destroy.json',
227 method = 'DELETE',
228 payload_type = 'direct_message',
229 allowed_param = ['id'],
230 require_auth = True
231 )
232
233 """ friendships/create """
234 create_friendship = bind_api(
235 path = '/friendships/create.json',
236 method = 'POST',
237 payload_type = 'user',
238 allowed_param = ['id', 'user_id', 'screen_name', 'follow'],
239 require_auth = True
240 )
241
242 """ friendships/destroy """
243 destroy_friendship = bind_api(
244 path = '/friendships/destroy.json',
245 method = 'DELETE',
246 payload_type = 'user',
247 allowed_param = ['id', 'user_id', 'screen_name'],
248 require_auth = True
249 )
250
251 """ friendships/exists """
252 exists_friendship = bind_api(
253 path = '/friendships/exists.json',
254 payload_type = 'json',
255 allowed_param = ['user_a', 'user_b']
256 )
257
258 """ friendships/show """
259 show_friendship = bind_api(
260 path = '/friendships/show.json',
261 payload_type = 'friendship',
262 allowed_param = ['source_id', 'source_screen_name',
263 'target_id', 'target_screen_name']
264 )
265
266 """ friends/ids """
267 friends_ids = bind_api(
268 path = '/friends/ids.json',
269 payload_type = 'ids',
270 allowed_param = ['id', 'user_id', 'screen_name', 'cursor']
271 )
272
273 """ friendships/incoming """
274 friendships_incoming = bind_api(
275 path = '/friendships/incoming.json',
276 payload_type = 'ids',
277 allowed_param = ['cursor']
278 )
279
280 """ friendships/outgoing"""
281 friendships_outgoing = bind_api(
282 path = '/friendships/outgoing.json',
283 payload_type = 'ids',
284 allowed_param = ['cursor']
285 )
286
287 """ followers/ids """
288 followers_ids = bind_api(
289 path = '/followers/ids.json',
290 payload_type = 'ids',
291 allowed_param = ['id', 'user_id', 'screen_name', 'cursor']
292 )
293
294 """ account/verify_credentials """
295 def verify_credentials(self):
296 try:
297 return bind_api(
298 path = '/account/verify_credentials.json',
299 payload_type = 'user',
300 require_auth = True
301 )(self)
302 except TweepError:
303 return False
304
305 """ account/rate_limit_status """
306 rate_limit_status = bind_api(
307 path = '/account/rate_limit_status.json',
308 payload_type = 'json'
309 )
310
311 """ account/update_delivery_device """
312 set_delivery_device = bind_api(
313 path = '/account/update_delivery_device.json',
314 method = 'POST',
315 allowed_param = ['device'],
316 payload_type = 'user',
317 require_auth = True
318 )
319
320 """ account/update_profile_colors """
321 update_profile_colors = bind_api(
322 path = '/account/update_profile_colors.json',
323 method = 'POST',
324 payload_type = 'user',
325 allowed_param = ['profile_background_color', 'profile_text_color',
326 'profile_link_color', 'profile_sidebar_fill_color',
327 'profile_sidebar_border_color'],
328 require_auth = True
329 )
330
331 """ account/update_profile_image """
332 def update_profile_image(self, filename):
333 headers, post_data = API._pack_image(filename, 700)
334 return bind_api(
335 path = '/account/update_profile_image.json',
336 method = 'POST',
337 payload_type = 'user',
338 require_auth = True
339 )(self, post_data=post_data, headers=headers)
340
341 """ account/update_profile_background_image """
342 def update_profile_background_image(self, filename, *args, **kargs):
343 headers, post_data = API._pack_image(filename, 800)
344 bind_api(
345 path = '/account/update_profile_background_image.json',
346 method = 'POST',
347 payload_type = 'user',
348 allowed_param = ['tile'],
349 require_auth = True
350 )(self, post_data=post_data, headers=headers)
351
352 """ account/update_profile """
353 update_profile = bind_api(
354 path = '/account/update_profile.json',
355 method = 'POST',
356 payload_type = 'user',
357 allowed_param = ['name', 'url', 'location', 'description'],
358 require_auth = True
359 )
360
361 """ favorites """
362 favorites = bind_api(
363 path = '/favorites.json',
364 payload_type = 'status', payload_list = True,
365 allowed_param = ['id', 'page']
366 )
367
368 """ favorites/create """
369 create_favorite = bind_api(
370 path = '/favorites/create/{id}.json',
371 method = 'POST',
372 payload_type = 'status',
373 allowed_param = ['id'],
374 require_auth = True
375 )
376
377 """ favorites/destroy """
378 destroy_favorite = bind_api(
379 path = '/favorites/destroy/{id}.json',
380 method = 'DELETE',
381 payload_type = 'status',
382 allowed_param = ['id'],
383 require_auth = True
384 )
385
386 """ notifications/follow """
387 enable_notifications = bind_api(
388 path = '/notifications/follow.json',
389 method = 'POST',
390 payload_type = 'user',
391 allowed_param = ['id', 'user_id', 'screen_name'],
392 require_auth = True
393 )
394
395 """ notifications/leave """
396 disable_notifications = bind_api(
397 path = '/notifications/leave.json',
398 method = 'POST',
399 payload_type = 'user',
400 allowed_param = ['id', 'user_id', 'screen_name'],
401 require_auth = True
402 )
403
404 """ blocks/create """
405 create_block = bind_api(
406 path = '/blocks/create.json',
407 method = 'POST',
408 payload_type = 'user',
409 allowed_param = ['id', 'user_id', 'screen_name'],
410 require_auth = True
411 )
412
413 """ blocks/destroy """
414 destroy_block = bind_api(
415 path = '/blocks/destroy.json',
416 method = 'DELETE',
417 payload_type = 'user',
418 allowed_param = ['id', 'user_id', 'screen_name'],
419 require_auth = True
420 )
421
422 """ blocks/exists """
423 def exists_block(self, *args, **kargs):
424 try:
425 bind_api(
426 path = '/blocks/exists.json',
427 allowed_param = ['id', 'user_id', 'screen_name'],
428 require_auth = True
429 )(self, *args, **kargs)
430 except TweepError:
431 return False
432 return True
433
434 """ blocks/blocking """
435 blocks = bind_api(
436 path = '/blocks/blocking.json',
437 payload_type = 'user', payload_list = True,
438 allowed_param = ['page'],
439 require_auth = True
440 )
441
442 """ blocks/blocking/ids """
443 blocks_ids = bind_api(
444 path = '/blocks/blocking/ids.json',
445 payload_type = 'json',
446 require_auth = True
447 )
448
449 """ report_spam """
450 report_spam = bind_api(
451 path = '/report_spam.json',
452 method = 'POST',
453 payload_type = 'user',
454 allowed_param = ['id', 'user_id', 'screen_name'],
455 require_auth = True
456 )
457
458 """ saved_searches """
459 saved_searches = bind_api(
460 path = '/saved_searches.json',
461 payload_type = 'saved_search', payload_list = True,
462 require_auth = True
463 )
464
465 """ saved_searches/show """
466 get_saved_search = bind_api(
467 path = '/saved_searches/show/{id}.json',
468 payload_type = 'saved_search',
469 allowed_param = ['id'],
470 require_auth = True
471 )
472
473 """ saved_searches/create """
474 create_saved_search = bind_api(
475 path = '/saved_searches/create.json',
476 method = 'POST',
477 payload_type = 'saved_search',
478 allowed_param = ['query'],
479 require_auth = True
480 )
481
482 """ saved_searches/destroy """
483 destroy_saved_search = bind_api(
484 path = '/saved_searches/destroy/{id}.json',
485 method = 'DELETE',
486 payload_type = 'saved_search',
487 allowed_param = ['id'],
488 require_auth = True
489 )
490
491 """ help/test """
492 def test(self):
493 try:
494 bind_api(
495 path = '/help/test.json',
496 )(self)
497 except TweepError:
498 return False
499 return True
500
501 def create_list(self, *args, **kargs):
502 return bind_api(
503 path = '/%s/lists.json' % self.auth.get_username(),
504 method = 'POST',
505 payload_type = 'list',
506 allowed_param = ['name', 'mode', 'description'],
507 require_auth = True
508 )(self, *args, **kargs)
509
510 def destroy_list(self, slug):
511 return bind_api(
512 path = '/%s/lists/%s.json' % (self.auth.get_username(), slug),
513 method = 'DELETE',
514 payload_type = 'list',
515 require_auth = True
516 )(self)
517
518 def update_list(self, slug, *args, **kargs):
519 return bind_api(
520 path = '/%s/lists/%s.json' % (self.auth.get_username(), slug),
521 method = 'POST',
522 payload_type = 'list',
523 allowed_param = ['name', 'mode', 'description'],
524 require_auth = True
525 )(self, *args, **kargs)
526
527 lists = bind_api(
528 path = '/{user}/lists.json',
529 payload_type = 'list', payload_list = True,
530 allowed_param = ['user', 'cursor'],
531 require_auth = True
532 )
533
534 lists_memberships = bind_api(
535 path = '/{user}/lists/memberships.json',
536 payload_type = 'list', payload_list = True,
537 allowed_param = ['user', 'cursor'],
538 require_auth = True
539 )
540
541 lists_subscriptions = bind_api(
542 path = '/{user}/lists/subscriptions.json',
543 payload_type = 'list', payload_list = True,
544 allowed_param = ['user', 'cursor'],
545 require_auth = True
546 )
547
548 list_timeline = bind_api(
549 path = '/{owner}/lists/{slug}/statuses.json',
550 payload_type = 'status', payload_list = True,
551 allowed_param = ['owner', 'slug', 'since_id', 'max_id', 'per_page', 'page']
552 )
553
554 get_list = bind_api(
555 path = '/{owner}/lists/{slug}.json',
556 payload_type = 'list',
557 allowed_param = ['owner', 'slug']
558 )
559
560 def add_list_member(self, slug, *args, **kargs):
561 return bind_api(
562 path = '/%s/%s/members.json' % (self.auth.get_username(), slug),
563 method = 'POST',
564 payload_type = 'list',
565 allowed_param = ['id'],
566 require_auth = True
567 )(self, *args, **kargs)
568
569 def remove_list_member(self, slug, *args, **kargs):
570 return bind_api(
571 path = '/%s/%s/members.json' % (self.auth.get_username(), slug),
572 method = 'DELETE',
573 payload_type = 'list',
574 allowed_param = ['id'],
575 require_auth = True
576 )(self, *args, **kargs)
577
578 list_members = bind_api(
579 path = '/{owner}/{slug}/members.json',
580 payload_type = 'user', payload_list = True,
581 allowed_param = ['owner', 'slug', 'cursor']
582 )
583
584 def is_list_member(self, owner, slug, user_id):
585 try:
586 return bind_api(
587 path = '/%s/%s/members/%s.json' % (owner, slug, user_id),
588 payload_type = 'user'
589 )(self)
590 except TweepError:
591 return False
592
593 subscribe_list = bind_api(
594 path = '/{owner}/{slug}/subscribers.json',
595 method = 'POST',
596 payload_type = 'list',
597 allowed_param = ['owner', 'slug'],
598 require_auth = True
599 )
600
601 unsubscribe_list = bind_api(
602 path = '/{owner}/{slug}/subscribers.json',
603 method = 'DELETE',
604 payload_type = 'list',
605 allowed_param = ['owner', 'slug'],
606 require_auth = True
607 )
608
609 list_subscribers = bind_api(
610 path = '/{owner}/{slug}/subscribers.json',
611 payload_type = 'user', payload_list = True,
612 allowed_param = ['owner', 'slug', 'cursor']
613 )
614
615 def is_subscribed_list(self, owner, slug, user_id):
616 try:
617 return bind_api(
618 path = '/%s/%s/subscribers/%s.json' % (owner, slug, user_id),
619 payload_type = 'user'
620 )(self)
621 except TweepError:
622 return False
623
624 """ trends/available """
625 trends_available = bind_api(
626 path = '/trends/available.json',
627 payload_type = 'json',
628 allowed_param = ['lat', 'long']
629 )
630
631 """ trends/location """
632 trends_location = bind_api(
633 path = '/trends/{woeid}.json',
634 payload_type = 'json',
635 allowed_param = ['woeid']
636 )
637
638 """ search """
639 search = bind_api(
640 search_api = True,
641 path = '/search.json',
642 payload_type = 'search_result', payload_list = True,
643 allowed_param = ['q', 'lang', 'locale', 'rpp', 'page', 'since_id', 'geocode', 'show_user', 'max_id', 'since', 'until', 'result_type']
644 )
645 search.pagination_mode = 'page'
646
647 """ trends """
648 trends = bind_api(
649 path = '/trends.json',
650 payload_type = 'json'
651 )
652
653 """ trends/current """
654 trends_current = bind_api(
655 path = '/trends/current.json',
656 payload_type = 'json',
657 allowed_param = ['exclude']
658 )
659
660 """ trends/daily """
661 trends_daily = bind_api(
662 path = '/trends/daily.json',
663 payload_type = 'json',
664 allowed_param = ['date', 'exclude']
665 )
666
667 """ trends/weekly """
668 trends_weekly = bind_api(
669 path = '/trends/weekly.json',
670 payload_type = 'json',
671 allowed_param = ['date', 'exclude']
672 )
673
674 """ geo/reverse_geocode """
675 reverse_geocode = bind_api(
676 path = '/geo/reverse_geocode.json',
677 payload_type = 'json',
678 allowed_param = ['lat', 'long', 'accuracy', 'granularity', 'max_results']
679 )
680
681 """ geo/nearby_places """
682 nearby_places = bind_api(
683 path = '/geo/nearby_places.json',
684 payload_type = 'json',
685 allowed_param = ['lat', 'long', 'ip', 'accuracy', 'granularity', 'max_results']
686 )
687
688 """ geo/id """
689 geo_id = bind_api(
690 path = '/geo/id/{id}.json',
691 payload_type = 'json',
692 allowed_param = ['id']
693 )
694
695 """ Internal use only """
696 @staticmethod
697 def _pack_image(filename, max_size):
698 """Pack image from file into multipart-formdata post body"""
699 # image must be less than 700kb in size
700 try:
701 if os.path.getsize(filename) > (max_size * 1024):
702 raise TweepError('File is too big, must be less than 700kb.')
703 except os.error, e:
704 raise TweepError('Unable to access file')
705
706 # image must be gif, jpeg, or png
707 file_type = mimetypes.guess_type(filename)
708 if file_type is None:
709 raise TweepError('Could not determine file type')
710 file_type = file_type[0]
711 if file_type not in ['image/gif', 'image/jpeg', 'image/png']:
712 raise TweepError('Invalid file type for image: %s' % file_type)
713
714 # build the mulitpart-formdata body
715 fp = open(filename, 'rb')
716 BOUNDARY = 'Tw3ePy'
717 body = []
718 body.append('--' + BOUNDARY)
719 body.append('Content-Disposition: form-data; name="image"; filename="%s"' % filename)
720 body.append('Content-Type: %s' % file_type)
721 body.append('')
722 body.append(fp.read())
723 body.append('--' + BOUNDARY + '--')
724 body.append('')
725 fp.close()
726 body = '\r\n'.join(body)
727
728 # build headers
729 headers = {
730 'Content-Type': 'multipart/form-data; boundary=Tw3ePy',
731 'Content-Length': len(body)
732 }
733
734 return headers, body
735
0736
=== added file 'GTG/backends/tweepy/auth.py'
--- GTG/backends/tweepy/auth.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/auth.py 2010-08-25 16:31:45 +0000
@@ -0,0 +1,163 @@
1# Tweepy
2# Copyright 2009-2010 Joshua Roesslein
3# See LICENSE for details.
4
5from urllib2 import Request, urlopen
6import base64
7
8from tweepy import oauth
9from tweepy.error import TweepError
10from tweepy.api import API
11
12
13class AuthHandler(object):
14
15 def apply_auth(self, url, method, headers, parameters):
16 """Apply authentication headers to request"""
17 raise NotImplementedError
18
19 def get_username(self):
20 """Return the username of the authenticated user"""
21 raise NotImplementedError
22
23
24class BasicAuthHandler(AuthHandler):
25
26 def __init__(self, username, password):
27 self.username = username
28 self._b64up = base64.b64encode('%s:%s' % (username, password))
29
30 def apply_auth(self, url, method, headers, parameters):
31 headers['Authorization'] = 'Basic %s' % self._b64up
32
33 def get_username(self):
34 return self.username
35
36
37class OAuthHandler(AuthHandler):
38 """OAuth authentication handler"""
39
40 OAUTH_HOST = 'twitter.com'
41 OAUTH_ROOT = '/oauth/'
42
43 def __init__(self, consumer_key, consumer_secret, callback=None, secure=False):
44 self._consumer = oauth.OAuthConsumer(consumer_key, consumer_secret)
45 self._sigmethod = oauth.OAuthSignatureMethod_HMAC_SHA1()
46 self.request_token = None
47 self.access_token = None
48 self.callback = callback
49 self.username = None
50 self.secure = secure
51
52 def _get_oauth_url(self, endpoint, secure=False):
53 if self.secure or secure:
54 prefix = 'https://'
55 else:
56 prefix = 'http://'
57
58 return prefix + self.OAUTH_HOST + self.OAUTH_ROOT + endpoint
59
60 def apply_auth(self, url, method, headers, parameters):
61 request = oauth.OAuthRequest.from_consumer_and_token(
62 self._consumer, http_url=url, http_method=method,
63 token=self.access_token, parameters=parameters
64 )
65 request.sign_request(self._sigmethod, self._consumer, self.access_token)
66 headers.update(request.to_header())
67
68 def _get_request_token(self):
69 try:
70 url = self._get_oauth_url('request_token')
71 request = oauth.OAuthRequest.from_consumer_and_token(
72 self._consumer, http_url=url, callback=self.callback
73 )
74 request.sign_request(self._sigmethod, self._consumer, None)
75 resp = urlopen(Request(url, headers=request.to_header()))
76 return oauth.OAuthToken.from_string(resp.read())
77 except Exception, e:
78 raise TweepError(e)
79
80 def set_request_token(self, key, secret):
81 self.request_token = oauth.OAuthToken(key, secret)
82
83 def set_access_token(self, key, secret):
84 self.access_token = oauth.OAuthToken(key, secret)
85
86 def get_authorization_url(self, signin_with_twitter=False):
87 """Get the authorization URL to redirect the user"""
88 try:
89 # get the request token
90 self.request_token = self._get_request_token()
91
92 # build auth request and return as url
93 if signin_with_twitter:
94 url = self._get_oauth_url('authenticate')
95 else:
96 url = self._get_oauth_url('authorize')
97 request = oauth.OAuthRequest.from_token_and_callback(
98 token=self.request_token, http_url=url
99 )
100
101 return request.to_url()
102 except Exception, e:
103 raise TweepError(e)
104
105 def get_access_token(self, verifier=None):
106 """
107 After user has authorized the request token, get access token
108 with user supplied verifier.
109 """
110 try:
111 url = self._get_oauth_url('access_token')
112
113 # build request
114 request = oauth.OAuthRequest.from_consumer_and_token(
115 self._consumer,
116 token=self.request_token, http_url=url,
117 verifier=str(verifier)
118 )
119 request.sign_request(self._sigmethod, self._consumer, self.request_token)
120
121 # send request
122 resp = urlopen(Request(url, headers=request.to_header()))
123 self.access_token = oauth.OAuthToken.from_string(resp.read())
124 return self.access_token
125 except Exception, e:
126 raise TweepError(e)
127
128 def get_xauth_access_token(self, username, password):
129 """
130 Get an access token from an username and password combination.
131 In order to get this working you need to create an app at
132 http://twitter.com/apps, after that send a mail to api@twitter.com
133 and request activation of xAuth for it.
134 """
135 try:
136 url = self._get_oauth_url('access_token', secure=True) # must use HTTPS
137 request = oauth.OAuthRequest.from_consumer_and_token(
138 oauth_consumer=self._consumer,
139 http_method='POST', http_url=url,
140 parameters = {
141 'x_auth_mode': 'client_auth',
142 'x_auth_username': username,
143 'x_auth_password': password
144 }
145 )
146 request.sign_request(self._sigmethod, self._consumer, None)
147
148 resp = urlopen(Request(url, data=request.to_postdata()))
149 self.access_token = oauth.OAuthToken.from_string(resp.read())
150 return self.access_token
151 except Exception, e:
152 raise TweepError(e)
153
154 def get_username(self):
155 if self.username is None:
156 api = API(self)
157 user = api.verify_credentials()
158 if user:
159 self.username = user.screen_name
160 else:
161 raise TweepError("Unable to get username, invalid oauth token!")
162 return self.username
163
0164
=== added file 'GTG/backends/tweepy/binder.py'
--- GTG/backends/tweepy/binder.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/binder.py 2010-08-25 16:31:45 +0000
@@ -0,0 +1,191 @@
1# Tweepy
2# Copyright 2009-2010 Joshua Roesslein
3# See LICENSE for details.
4
5import httplib
6import urllib
7import time
8import re
9
10from tweepy.error import TweepError
11from tweepy.utils import convert_to_utf8_str
12
13re_path_template = re.compile('{\w+}')
14
15
16def bind_api(**config):
17
18 class APIMethod(object):
19
20 path = config['path']
21 payload_type = config.get('payload_type', None)
22 payload_list = config.get('payload_list', False)
23 allowed_param = config.get('allowed_param', [])
24 method = config.get('method', 'GET')
25 require_auth = config.get('require_auth', False)
26 search_api = config.get('search_api', False)
27
28 def __init__(self, api, args, kargs):
29 # If authentication is required and no credentials
30 # are provided, throw an error.
31 if self.require_auth and not api.auth:
32 raise TweepError('Authentication required!')
33
34 self.api = api
35 self.post_data = kargs.pop('post_data', None)
36 self.retry_count = kargs.pop('retry_count', api.retry_count)
37 self.retry_delay = kargs.pop('retry_delay', api.retry_delay)
38 self.retry_errors = kargs.pop('retry_errors', api.retry_errors)
39 self.headers = kargs.pop('headers', {})
40 self.build_parameters(args, kargs)
41
42 # Pick correct URL root to use
43 if self.search_api:
44 self.api_root = api.search_root
45 else:
46 self.api_root = api.api_root
47
48 # Perform any path variable substitution
49 self.build_path()
50
51 if api.secure:
52 self.scheme = 'https://'
53 else:
54 self.scheme = 'http://'
55
56 if self.search_api:
57 self.host = api.search_host
58 else:
59 self.host = api.host
60
61 # Manually set Host header to fix an issue in python 2.5
62 # or older where Host is set including the 443 port.
63 # This causes Twitter to issue 301 redirect.
64 # See Issue http://github.com/joshthecoder/tweepy/issues/#issue/12
65 self.headers['Host'] = self.host
66
67 def build_parameters(self, args, kargs):
68 self.parameters = {}
69 for idx, arg in enumerate(args):
70 if arg is None:
71 continue
72
73 try:
74 self.parameters[self.allowed_param[idx]] = convert_to_utf8_str(arg)
75 except IndexError:
76 raise TweepError('Too many parameters supplied!')
77
78 for k, arg in kargs.items():
79 if arg is None:
80 continue
81 if k in self.parameters:
82 raise TweepError('Multiple values for parameter %s supplied!' % k)
83
84 self.parameters[k] = convert_to_utf8_str(arg)
85
86 def build_path(self):
87 for variable in re_path_template.findall(self.path):
88 name = variable.strip('{}')
89
90 if name == 'user' and 'user' not in self.parameters and self.api.auth:
91 # No 'user' parameter provided, fetch it from Auth instead.
92 value = self.api.auth.get_username()
93 else:
94 try:
95 value = urllib.quote(self.parameters[name])
96 except KeyError:
97 raise TweepError('No parameter value found for path variable: %s' % name)
98 del self.parameters[name]
99
100 self.path = self.path.replace(variable, value)
101
102 def execute(self):
103 # Build the request URL
104 url = self.api_root + self.path
105 if len(self.parameters):
106 url = '%s?%s' % (url, urllib.urlencode(self.parameters))
107
108 # Query the cache if one is available
109 # and this request uses a GET method.
110 if self.api.cache and self.method == 'GET':
111 cache_result = self.api.cache.get(url)
112 # if cache result found and not expired, return it
113 if cache_result:
114 # must restore api reference
115 if isinstance(cache_result, list):
116 for result in cache_result:
117 result._api = self.api
118 else:
119 cache_result._api = self.api
120 return cache_result
121
122 # Continue attempting request until successful
123 # or maximum number of retries is reached.
124 retries_performed = 0
125 while retries_performed < self.retry_count + 1:
126 # Open connection
127 # FIXME: add timeout
128 if self.api.secure:
129 conn = httplib.HTTPSConnection(self.host)
130 else:
131 conn = httplib.HTTPConnection(self.host)
132
133 # Apply authentication
134 if self.api.auth:
135 self.api.auth.apply_auth(
136 self.scheme + self.host + url,
137 self.method, self.headers, self.parameters
138 )
139
140 # Execute request
141 try:
142 conn.request(self.method, url, headers=self.headers, body=self.post_data)
143 resp = conn.getresponse()
144 except Exception, e:
145 raise TweepError('Failed to send request: %s' % e)
146
147 # Exit request loop if non-retry error code
148 if self.retry_errors:
149 if resp.status not in self.retry_errors: break
150 else:
151 if resp.status == 200: break
152
153 # Sleep before retrying request again
154 time.sleep(self.retry_delay)
155 retries_performed += 1
156
157 # If an error was returned, throw an exception
158 self.api.last_response = resp
159 if resp.status != 200:
160 try:
161 error_msg = self.api.parser.parse_error(resp.read())
162 except Exception:
163 error_msg = "Twitter error response: status code = %s" % resp.status
164 raise TweepError(error_msg, resp)
165
166 # Parse the response payload
167 result = self.api.parser.parse(self, resp.read())
168
169 conn.close()
170
171 # Store result into cache if one is available.
172 if self.api.cache and self.method == 'GET' and result:
173 self.api.cache.store(url, result)
174
175 return result
176
177
178 def _call(api, *args, **kargs):
179
180 method = APIMethod(api, args, kargs)
181 return method.execute()
182
183
184 # Set pagination mode
185 if 'cursor' in APIMethod.allowed_param:
186 _call.pagination_mode = 'cursor'
187 elif 'page' in APIMethod.allowed_param:
188 _call.pagination_mode = 'page'
189
190 return _call
191
0192
=== added file 'GTG/backends/tweepy/cache.py'
--- GTG/backends/tweepy/cache.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/cache.py 2010-08-25 16:31:45 +0000
@@ -0,0 +1,264 @@
1# Tweepy
2# Copyright 2009-2010 Joshua Roesslein
3# See LICENSE for details.
4
5import time
6import threading
7import os
8import cPickle as pickle
9
10try:
11 import hashlib
12except ImportError:
13 # python 2.4
14 import md5 as hashlib
15
16try:
17 import fcntl
18except ImportError:
19 # Probably on a windows system
20 # TODO: use win32file
21 pass
22
23
24class Cache(object):
25 """Cache interface"""
26
27 def __init__(self, timeout=60):
28 """Initialize the cache
29 timeout: number of seconds to keep a cached entry
30 """
31 self.timeout = timeout
32
33 def store(self, key, value):
34 """Add new record to cache
35 key: entry key
36 value: data of entry
37 """
38 raise NotImplementedError
39
40 def get(self, key, timeout=None):
41 """Get cached entry if exists and not expired
42 key: which entry to get
43 timeout: override timeout with this value [optional]
44 """
45 raise NotImplementedError
46
47 def count(self):
48 """Get count of entries currently stored in cache"""
49 raise NotImplementedError
50
51 def cleanup(self):
52 """Delete any expired entries in cache."""
53 raise NotImplementedError
54
55 def flush(self):
56 """Delete all cached entries"""
57 raise NotImplementedError
58
59
60class MemoryCache(Cache):
61 """In-memory cache"""
62
63 def __init__(self, timeout=60):
64 Cache.__init__(self, timeout)
65 self._entries = {}
66 self.lock = threading.Lock()
67
68 def __getstate__(self):
69 # pickle
70 return {'entries': self._entries, 'timeout': self.timeout}
71
72 def __setstate__(self, state):
73 # unpickle
74 self.lock = threading.Lock()
75 self._entries = state['entries']
76 self.timeout = state['timeout']
77
78 def _is_expired(self, entry, timeout):
79 return timeout > 0 and (time.time() - entry[0]) >= timeout
80
81 def store(self, key, value):
82 self.lock.acquire()
83 self._entries[key] = (time.time(), value)
84 self.lock.release()
85
86 def get(self, key, timeout=None):
87 self.lock.acquire()
88 try:
89 # check to see if we have this key
90 entry = self._entries.get(key)
91 if not entry:
92 # no hit, return nothing
93 return None
94
95 # use provided timeout in arguments if provided
96 # otherwise use the one provided during init.
97 if timeout is None:
98 timeout = self.timeout
99
100 # make sure entry is not expired
101 if self._is_expired(entry, timeout):
102 # entry expired, delete and return nothing
103 del self._entries[key]
104 return None
105
106 # entry found and not expired, return it
107 return entry[1]
108 finally:
109 self.lock.release()
110
111 def count(self):
112 return len(self._entries)
113
114 def cleanup(self):
115 self.lock.acquire()
116 try:
117 for k, v in self._entries.items():
118 if self._is_expired(v, self.timeout):
119 del self._entries[k]
120 finally:
121 self.lock.release()
122
123 def flush(self):
124 self.lock.acquire()
125 self._entries.clear()
126 self.lock.release()
127
128
129class FileCache(Cache):
130 """File-based cache"""
131
132 # locks used to make cache thread-safe
133 cache_locks = {}
134
135 def __init__(self, cache_dir, timeout=60):
136 Cache.__init__(self, timeout)
137 if os.path.exists(cache_dir) is False:
138 os.mkdir(cache_dir)
139 self.cache_dir = cache_dir
140 if cache_dir in FileCache.cache_locks:
141 self.lock = FileCache.cache_locks[cache_dir]
142 else:
143 self.lock = threading.Lock()
144 FileCache.cache_locks[cache_dir] = self.lock
145
146 if os.name == 'posix':
147 self._lock_file = self._lock_file_posix
148 self._unlock_file = self._unlock_file_posix
149 elif os.name == 'nt':
150 self._lock_file = self._lock_file_win32
151 self._unlock_file = self._unlock_file_win32
152 else:
153 print 'Warning! FileCache locking not supported on this system!'
154 self._lock_file = self._lock_file_dummy
155 self._unlock_file = self._unlock_file_dummy
156
157 def _get_path(self, key):
158 md5 = hashlib.md5()
159 md5.update(key)
160 return os.path.join(self.cache_dir, md5.hexdigest())
161
162 def _lock_file_dummy(self, path, exclusive=True):
163 return None
164
165 def _unlock_file_dummy(self, lock):
166 return
167
168 def _lock_file_posix(self, path, exclusive=True):
169 lock_path = path + '.lock'
170 if exclusive is True:
171 f_lock = open(lock_path, 'w')
172 fcntl.lockf(f_lock, fcntl.LOCK_EX)
173 else:
174 f_lock = open(lock_path, 'r')
175 fcntl.lockf(f_lock, fcntl.LOCK_SH)
176 if os.path.exists(lock_path) is False:
177 f_lock.close()
178 return None
179 return f_lock
180
181 def _unlock_file_posix(self, lock):
182 lock.close()
183
184 def _lock_file_win32(self, path, exclusive=True):
185 # TODO: implement
186 return None
187
188 def _unlock_file_win32(self, lock):
189 # TODO: implement
190 return
191
192 def _delete_file(self, path):
193 os.remove(path)
194 if os.path.exists(path + '.lock'):
195 os.remove(path + '.lock')
196
197 def store(self, key, value):
198 path = self._get_path(key)
199 self.lock.acquire()
200 try:
201 # acquire lock and open file
202 f_lock = self._lock_file(path)
203 datafile = open(path, 'wb')
204
205 # write data
206 pickle.dump((time.time(), value), datafile)
207
208 # close and unlock file
209 datafile.close()
210 self._unlock_file(f_lock)
211 finally:
212 self.lock.release()
213
214 def get(self, key, timeout=None):
215 return self._get(self._get_path(key), timeout)
216
217 def _get(self, path, timeout):
218 if os.path.exists(path) is False:
219 # no record
220 return None
221 self.lock.acquire()
222 try:
223 # acquire lock and open
224 f_lock = self._lock_file(path, False)
225 datafile = open(path, 'rb')
226
227 # read pickled object
228 created_time, value = pickle.load(datafile)
229 datafile.close()
230
231 # check if value is expired
232 if timeout is None:
233 timeout = self.timeout
234 if timeout > 0 and (time.time() - created_time) >= timeout:
235 # expired! delete from cache
236 value = None
237 self._delete_file(path)
238
239 # unlock and return result
240 self._unlock_file(f_lock)
241 return value
242 finally:
243 self.lock.release()
244
245 def count(self):
246 c = 0
247 for entry in os.listdir(self.cache_dir):
248 if entry.endswith('.lock'):
249 continue
250 c += 1
251 return c
252
253 def cleanup(self):
254 for entry in os.listdir(self.cache_dir):
255 if entry.endswith('.lock'):
256 continue
257 self._get(os.path.join(self.cache_dir, entry), None)
258
259 def flush(self):
260 for entry in os.listdir(self.cache_dir):
261 if entry.endswith('.lock'):
262 continue
263 self._delete_file(os.path.join(self.cache_dir, entry))
264
0265
=== added file 'GTG/backends/tweepy/cursor.py'
--- GTG/backends/tweepy/cursor.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/cursor.py 2010-08-25 16:31:45 +0000
@@ -0,0 +1,128 @@
1# Tweepy
2# Copyright 2009-2010 Joshua Roesslein
3# See LICENSE for details.
4
5from tweepy.error import TweepError
6
7class Cursor(object):
8 """Pagination helper class"""
9
10 def __init__(self, method, *args, **kargs):
11 if hasattr(method, 'pagination_mode'):
12 if method.pagination_mode == 'cursor':
13 self.iterator = CursorIterator(method, args, kargs)
14 else:
15 self.iterator = PageIterator(method, args, kargs)
16 else:
17 raise TweepError('This method does not perform pagination')
18
19 def pages(self, limit=0):
20 """Return iterator for pages"""
21 if limit > 0:
22 self.iterator.limit = limit
23 return self.iterator
24
25 def items(self, limit=0):
26 """Return iterator for items in each page"""
27 i = ItemIterator(self.iterator)
28 i.limit = limit
29 return i
30
31class BaseIterator(object):
32
33 def __init__(self, method, args, kargs):
34 self.method = method
35 self.args = args
36 self.kargs = kargs
37 self.limit = 0
38
39 def next(self):
40 raise NotImplementedError
41
42 def prev(self):
43 raise NotImplementedError
44
45 def __iter__(self):
46 return self
47
48class CursorIterator(BaseIterator):
49
50 def __init__(self, method, args, kargs):
51 BaseIterator.__init__(self, method, args, kargs)
52 self.next_cursor = -1
53 self.prev_cursor = 0
54 self.count = 0
55
56 def next(self):
57 if self.next_cursor == 0 or (self.limit and self.count == self.limit):
58 raise StopIteration
59 data, cursors = self.method(
60 cursor=self.next_cursor, *self.args, **self.kargs
61 )
62 self.prev_cursor, self.next_cursor = cursors
63 if len(data) == 0:
64 raise StopIteration
65 self.count += 1
66 return data
67
68 def prev(self):
69 if self.prev_cursor == 0:
70 raise TweepError('Can not page back more, at first page')
71 data, self.next_cursor, self.prev_cursor = self.method(
72 cursor=self.prev_cursor, *self.args, **self.kargs
73 )
74 self.count -= 1
75 return data
76
77class PageIterator(BaseIterator):
78
79 def __init__(self, method, args, kargs):
80 BaseIterator.__init__(self, method, args, kargs)
81 self.current_page = 0
82
83 def next(self):
84 self.current_page += 1
85 items = self.method(page=self.current_page, *self.args, **self.kargs)
86 if len(items) == 0 or (self.limit > 0 and self.current_page > self.limit):
87 raise StopIteration
88 return items
89
90 def prev(self):
91 if (self.current_page == 1):
92 raise TweepError('Can not page back more, at first page')
93 self.current_page -= 1
94 return self.method(page=self.current_page, *self.args, **self.kargs)
95
96class ItemIterator(BaseIterator):
97
98 def __init__(self, page_iterator):
99 self.page_iterator = page_iterator
100 self.limit = 0
101 self.current_page = None
102 self.page_index = -1
103 self.count = 0
104
105 def next(self):
106 if self.limit > 0 and self.count == self.limit:
107 raise StopIteration
108 if self.current_page is None or self.page_index == len(self.current_page) - 1:
109 # Reached end of current page, get the next page...
110 self.current_page = self.page_iterator.next()
111 self.page_index = -1
112 self.page_index += 1
113 self.count += 1
114 return self.current_page[self.page_index]
115
116 def prev(self):
117 if self.current_page is None:
118 raise TweepError('Can not go back more, at first page')
119 if self.page_index == 0:
120 # At the beginning of the current page, move to next...
121 self.current_page = self.page_iterator.prev()
122 self.page_index = len(self.current_page)
123 if self.page_index == 0:
124 raise TweepError('No more items')
125 self.page_index -= 1
126 self.count -= 1
127 return self.current_page[self.page_index]
128
0129
=== added file 'GTG/backends/tweepy/error.py'
--- GTG/backends/tweepy/error.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/error.py 2010-08-25 16:31:45 +0000
@@ -0,0 +1,14 @@
1# Tweepy
2# Copyright 2009-2010 Joshua Roesslein
3# See LICENSE for details.
4
5class TweepError(Exception):
6 """Tweepy exception"""
7
8 def __init__(self, reason, response=None):
9 self.reason = str(reason)
10 self.response = response
11
12 def __str__(self):
13 return self.reason
14
015
=== added file 'GTG/backends/tweepy/models.py'
--- GTG/backends/tweepy/models.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/models.py 2010-08-25 16:31:45 +0000
@@ -0,0 +1,313 @@
1# Tweepy
2# Copyright 2009-2010 Joshua Roesslein
3# See LICENSE for details.
4
5from tweepy.error import TweepError
6from tweepy.utils import parse_datetime, parse_html_value, parse_a_href, \
7 parse_search_datetime, unescape_html
8
9
10class ResultSet(list):
11 """A list like object that holds results from a Twitter API query."""
12
13
14class Model(object):
15
16 def __init__(self, api=None):
17 self._api = api
18
19 def __getstate__(self):
20 # pickle
21 pickle = dict(self.__dict__)
22 try:
23 del pickle['_api'] # do not pickle the API reference
24 except KeyError:
25 pass
26 return pickle
27
28 @classmethod
29 def parse(cls, api, json):
30 """Parse a JSON object into a model instance."""
31 raise NotImplementedError
32
33 @classmethod
34 def parse_list(cls, api, json_list):
35 """Parse a list of JSON objects into a result set of model instances."""
36 results = ResultSet()
37 for obj in json_list:
38 results.append(cls.parse(api, obj))
39 return results
40
41
42class Status(Model):
43
44 @classmethod
45 def parse(cls, api, json):
46 status = cls(api)
47 for k, v in json.items():
48 if k == 'user':
49 user = User.parse(api, v)
50 setattr(status, 'author', user)
51 setattr(status, 'user', user) # DEPRECIATED
52 elif k == 'created_at':
53 setattr(status, k, parse_datetime(v))
54 elif k == 'source':
55 if '<' in v:
56 setattr(status, k, parse_html_value(v))
57 setattr(status, 'source_url', parse_a_href(v))
58 else:
59 setattr(status, k, v)
60 setattr(status, 'source_url', None)
61 elif k == 'retweeted_status':
62 setattr(status, k, Status.parse(api, v))
63 else:
64 setattr(status, k, v)
65 return status
66
67 def destroy(self):
68 return self._api.destroy_status(self.id)
69
70 def retweet(self):
71 return self._api.retweet(self.id)
72
73 def retweets(self):
74 return self._api.retweets(self.id)
75
76 def favorite(self):
77 return self._api.create_favorite(self.id)
78
79
80class User(Model):
81
82 @classmethod
83 def parse(cls, api, json):
84 user = cls(api)
85 for k, v in json.items():
86 if k == 'created_at':
87 setattr(user, k, parse_datetime(v))
88 elif k == 'status':
89 setattr(user, k, Status.parse(api, v))
90 elif k == 'following':
91 # twitter sets this to null if it is false
92 if v is True:
93 setattr(user, k, True)
94 else:
95 setattr(user, k, False)
96 else:
97 setattr(user, k, v)
98 return user
99
100 @classmethod
101 def parse_list(cls, api, json_list):
102 if isinstance(json_list, list):
103 item_list = json_list
104 else:
105 item_list = json_list['users']
106
107 results = ResultSet()
108 for obj in item_list:
109 results.append(cls.parse(api, obj))
110 return results
111
112 def timeline(self, **kargs):
113 return self._api.user_timeline(user_id=self.id, **kargs)
114
115 def friends(self, **kargs):
116 return self._api.friends(user_id=self.id, **kargs)
117
118 def followers(self, **kargs):
119 return self._api.followers(user_id=self.id, **kargs)
120
121 def follow(self):
122 self._api.create_friendship(user_id=self.id)
123 self.following = True
124
125 def unfollow(self):
126 self._api.destroy_friendship(user_id=self.id)
127 self.following = False
128
129 def lists_memberships(self, *args, **kargs):
130 return self._api.lists_memberships(user=self.screen_name, *args, **kargs)
131
132 def lists_subscriptions(self, *args, **kargs):
133 return self._api.lists_subscriptions(user=self.screen_name, *args, **kargs)
134
135 def lists(self, *args, **kargs):
136 return self._api.lists(user=self.screen_name, *args, **kargs)
137
138 def followers_ids(self, *args, **kargs):
139 return self._api.followers_ids(user_id=self.id, *args, **kargs)
140
141
142class DirectMessage(Model):
143
144 @classmethod
145 def parse(cls, api, json):
146 dm = cls(api)
147 for k, v in json.items():
148 if k == 'sender' or k == 'recipient':
149 setattr(dm, k, User.parse(api, v))
150 elif k == 'created_at':
151 setattr(dm, k, parse_datetime(v))
152 else:
153 setattr(dm, k, v)
154 return dm
155
156 def destroy(self):
157 return self._api.destroy_direct_message(self.id)
158
159
160class Friendship(Model):
161
162 @classmethod
163 def parse(cls, api, json):
164 relationship = json['relationship']
165
166 # parse source
167 source = cls(api)
168 for k, v in relationship['source'].items():
169 setattr(source, k, v)
170
171 # parse target
172 target = cls(api)
173 for k, v in relationship['target'].items():
174 setattr(target, k, v)
175
176 return source, target
177
178
179class SavedSearch(Model):
180
181 @classmethod
182 def parse(cls, api, json):
183 ss = cls(api)
184 for k, v in json.items():
185 if k == 'created_at':
186 setattr(ss, k, parse_datetime(v))
187 else:
188 setattr(ss, k, v)
189 return ss
190
191 def destroy(self):
192 return self._api.destroy_saved_search(self.id)
193
194
195class SearchResult(Model):
196
197 @classmethod
198 def parse(cls, api, json):
199 result = cls()
200 for k, v in json.items():
201 if k == 'created_at':
202 setattr(result, k, parse_search_datetime(v))
203 elif k == 'source':
204 setattr(result, k, parse_html_value(unescape_html(v)))
205 else:
206 setattr(result, k, v)
207 return result
208
209 @classmethod
210 def parse_list(cls, api, json_list, result_set=None):
211 results = ResultSet()
212 results.max_id = json_list.get('max_id')
213 results.since_id = json_list.get('since_id')
214 results.refresh_url = json_list.get('refresh_url')
215 results.next_page = json_list.get('next_page')
216 results.results_per_page = json_list.get('results_per_page')
217 results.page = json_list.get('page')
218 results.completed_in = json_list.get('completed_in')
219 results.query = json_list.get('query')
220
221 for obj in json_list['results']:
222 results.append(cls.parse(api, obj))
223 return results
224
225
226class List(Model):
227
228 @classmethod
229 def parse(cls, api, json):
230 lst = List(api)
231 for k,v in json.items():
232 if k == 'user':
233 setattr(lst, k, User.parse(api, v))
234 else:
235 setattr(lst, k, v)
236 return lst
237
238 @classmethod
239 def parse_list(cls, api, json_list, result_set=None):
240 results = ResultSet()
241 for obj in json_list['lists']:
242 results.append(cls.parse(api, obj))
243 return results
244
245 def update(self, **kargs):
246 return self._api.update_list(self.slug, **kargs)
247
248 def destroy(self):
249 return self._api.destroy_list(self.slug)
250
251 def timeline(self, **kargs):
252 return self._api.list_timeline(self.user.screen_name, self.slug, **kargs)
253
254 def add_member(self, id):
255 return self._api.add_list_member(self.slug, id)
256
257 def remove_member(self, id):
258 return self._api.remove_list_member(self.slug, id)
259
260 def members(self, **kargs):
261 return self._api.list_members(self.user.screen_name, self.slug, **kargs)
262
263 def is_member(self, id):
264 return self._api.is_list_member(self.user.screen_name, self.slug, id)
265
266 def subscribe(self):
267 return self._api.subscribe_list(self.user.screen_name, self.slug)
268
269 def unsubscribe(self):
270 return self._api.unsubscribe_list(self.user.screen_name, self.slug)
271
272 def subscribers(self, **kargs):
273 return self._api.list_subscribers(self.user.screen_name, self.slug, **kargs)
274
275 def is_subscribed(self, id):
276 return self._api.is_subscribed_list(self.user.screen_name, self.slug, id)
277
278
279class JSONModel(Model):
280
281 @classmethod
282 def parse(cls, api, json):
283 return json
284
285
286class IDModel(Model):
287
288 @classmethod
289 def parse(cls, api, json):
290 if isinstance(json, list):
291 return json
292 else:
293 return json['ids']
294
295
296class ModelFactory(object):
297 """
298 Used by parsers for creating instances
299 of models. You may subclass this factory
300 to add your own extended models.
301 """
302
303 status = Status
304 user = User
305 direct_message = DirectMessage
306 friendship = Friendship
307 saved_search = SavedSearch
308 search_result = SearchResult
309 list = List
310
311 json = JSONModel
312 ids = IDModel
313
0314
=== added file 'GTG/backends/tweepy/oauth.py'
--- GTG/backends/tweepy/oauth.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/oauth.py 2010-08-25 16:31:45 +0000
@@ -0,0 +1,655 @@
1"""
2The MIT License
3
4Copyright (c) 2007 Leah Culver
5
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12
13The above copyright notice and this permission notice shall be included in
14all copies or substantial portions of the Software.
15
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22THE SOFTWARE.
23"""
24
25import cgi
26import urllib
27import time
28import random
29import urlparse
30import hmac
31import binascii
32
33
34VERSION = '1.0' # Hi Blaine!
35HTTP_METHOD = 'GET'
36SIGNATURE_METHOD = 'PLAINTEXT'
37
38
39class OAuthError(RuntimeError):
40 """Generic exception class."""
41 def __init__(self, message='OAuth error occured.'):
42 self.message = message
43
44def build_authenticate_header(realm=''):
45 """Optional WWW-Authenticate header (401 error)"""
46 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
47
48def escape(s):
49 """Escape a URL including any /."""
50 return urllib.quote(s, safe='~')
51
52def _utf8_str(s):
53 """Convert unicode to utf-8."""
54 if isinstance(s, unicode):
55 return s.encode("utf-8")
56 else:
57 return str(s)
58
59def generate_timestamp():
60 """Get seconds since epoch (UTC)."""
61 return int(time.time())
62
63def generate_nonce(length=8):
64 """Generate pseudorandom number."""
65 return ''.join([str(random.randint(0, 9)) for i in range(length)])
66
67def generate_verifier(length=8):
68 """Generate pseudorandom number."""
69 return ''.join([str(random.randint(0, 9)) for i in range(length)])
70
71
72class OAuthConsumer(object):
73 """Consumer of OAuth authentication.
74
75 OAuthConsumer is a data type that represents the identity of the Consumer
76 via its shared secret with the Service Provider.
77
78 """
79 key = None
80 secret = None
81
82 def __init__(self, key, secret):
83 self.key = key
84 self.secret = secret
85
86
87class OAuthToken(object):
88 """OAuthToken is a data type that represents an End User via either an access
89 or request token.
90
91 key -- the token
92 secret -- the token secret
93
94 """
95 key = None
96 secret = None
97 callback = None
98 callback_confirmed = None
99 verifier = None
100
101 def __init__(self, key, secret):
102 self.key = key
103 self.secret = secret
104
105 def set_callback(self, callback):
106 self.callback = callback
107 self.callback_confirmed = 'true'
108
109 def set_verifier(self, verifier=None):
110 if verifier is not None:
111 self.verifier = verifier
112 else:
113 self.verifier = generate_verifier()
114
115 def get_callback_url(self):
116 if self.callback and self.verifier:
117 # Append the oauth_verifier.
118 parts = urlparse.urlparse(self.callback)
119 scheme, netloc, path, params, query, fragment = parts[:6]
120 if query:
121 query = '%s&oauth_verifier=%s' % (query, self.verifier)
122 else:
123 query = 'oauth_verifier=%s' % self.verifier
124 return urlparse.urlunparse((scheme, netloc, path, params,
125 query, fragment))
126 return self.callback
127
128 def to_string(self):
129 data = {
130 'oauth_token': self.key,
131 'oauth_token_secret': self.secret,
132 }
133 if self.callback_confirmed is not None:
134 data['oauth_callback_confirmed'] = self.callback_confirmed
135 return urllib.urlencode(data)
136
137 def from_string(s):
138 """ Returns a token from something like:
139 oauth_token_secret=xxx&oauth_token=xxx
140 """
141 params = cgi.parse_qs(s, keep_blank_values=False)
142 key = params['oauth_token'][0]
143 secret = params['oauth_token_secret'][0]
144 token = OAuthToken(key, secret)
145 try:
146 token.callback_confirmed = params['oauth_callback_confirmed'][0]
147 except KeyError:
148 pass # 1.0, no callback confirmed.
149 return token
150 from_string = staticmethod(from_string)
151
152 def __str__(self):
153 return self.to_string()
154
155
156class OAuthRequest(object):
157 """OAuthRequest represents the request and can be serialized.
158
159 OAuth parameters:
160 - oauth_consumer_key
161 - oauth_token
162 - oauth_signature_method
163 - oauth_signature
164 - oauth_timestamp
165 - oauth_nonce
166 - oauth_version
167 - oauth_verifier
168 ... any additional parameters, as defined by the Service Provider.
169 """
170 parameters = None # OAuth parameters.
171 http_method = HTTP_METHOD
172 http_url = None
173 version = VERSION
174
175 def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
176 self.http_method = http_method
177 self.http_url = http_url
178 self.parameters = parameters or {}
179
180 def set_parameter(self, parameter, value):
181 self.parameters[parameter] = value
182
183 def get_parameter(self, parameter):
184 try:
185 return self.parameters[parameter]
186 except:
187 raise OAuthError('Parameter not found: %s' % parameter)
188
189 def _get_timestamp_nonce(self):
190 return self.get_parameter('oauth_timestamp'), self.get_parameter(
191 'oauth_nonce')
192
193 def get_nonoauth_parameters(self):
194 """Get any non-OAuth parameters."""
195 parameters = {}
196 for k, v in self.parameters.iteritems():
197 # Ignore oauth parameters.
198 if k.find('oauth_') < 0:
199 parameters[k] = v
200 return parameters
201
202 def to_header(self, realm=''):
203 """Serialize as a header for an HTTPAuth request."""
204 auth_header = 'OAuth realm="%s"' % realm
205 # Add the oauth parameters.
206 if self.parameters:
207 for k, v in self.parameters.iteritems():
208 if k[:6] == 'oauth_':
209 auth_header += ', %s="%s"' % (k, escape(str(v)))
210 return {'Authorization': auth_header}
211
212 def to_postdata(self):
213 """Serialize as post data for a POST request."""
214 return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
215 for k, v in self.parameters.iteritems()])
216
217 def to_url(self):
218 """Serialize as a URL for a GET request."""
219 return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
220
221 def get_normalized_parameters(self):
222 """Return a string that contains the parameters that must be signed."""
223 params = self.parameters
224 try:
225 # Exclude the signature if it exists.
226 del params['oauth_signature']
227 except:
228 pass
229 # Escape key values before sorting.
230 key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
231 for k,v in params.items()]
232 # Sort lexicographically, first after key, then after value.
233 key_values.sort()
234 # Combine key value pairs into a string.
235 return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
236
237 def get_normalized_http_method(self):
238 """Uppercases the http method."""
239 return self.http_method.upper()
240
241 def get_normalized_http_url(self):
242 """Parses the URL and rebuilds it to be scheme://host/path."""
243 parts = urlparse.urlparse(self.http_url)
244 scheme, netloc, path = parts[:3]
245 # Exclude default port numbers.
246 if scheme == 'http' and netloc[-3:] == ':80':
247 netloc = netloc[:-3]
248 elif scheme == 'https' and netloc[-4:] == ':443':
249 netloc = netloc[:-4]
250 return '%s://%s%s' % (scheme, netloc, path)
251
252 def sign_request(self, signature_method, consumer, token):
253 """Set the signature parameter to the result of build_signature."""
254 # Set the signature method.
255 self.set_parameter('oauth_signature_method',
256 signature_method.get_name())
257 # Set the signature.
258 self.set_parameter('oauth_signature',
259 self.build_signature(signature_method, consumer, token))
260
261 def build_signature(self, signature_method, consumer, token):
262 """Calls the build signature method within the signature method."""
263 return signature_method.build_signature(self, consumer, token)
264
265 def from_request(http_method, http_url, headers=None, parameters=None,
266 query_string=None):
267 """Combines multiple parameter sources."""
268 if parameters is None:
269 parameters = {}
270
271 # Headers
272 if headers and 'Authorization' in headers:
273 auth_header = headers['Authorization']
274 # Check that the authorization header is OAuth.
275 if auth_header[:6] == 'OAuth ':
276 auth_header = auth_header[6:]
277 try:
278 # Get the parameters from the header.
279 header_params = OAuthRequest._split_header(auth_header)
280 parameters.update(header_params)
281 except:
282 raise OAuthError('Unable to parse OAuth parameters from '
283 'Authorization header.')
284
285 # GET or POST query string.
286 if query_string:
287 query_params = OAuthRequest._split_url_string(query_string)
288 parameters.update(query_params)
289
290 # URL parameters.
291 param_str = urlparse.urlparse(http_url)[4] # query
292 url_params = OAuthRequest._split_url_string(param_str)
293 parameters.update(url_params)
294
295 if parameters:
296 return OAuthRequest(http_method, http_url, parameters)
297
298 return None
299 from_request = staticmethod(from_request)
300
301 def from_consumer_and_token(oauth_consumer, token=None,
302 callback=None, verifier=None, http_method=HTTP_METHOD,
303 http_url=None, parameters=None):
304 if not parameters:
305 parameters = {}
306
307 defaults = {
308 'oauth_consumer_key': oauth_consumer.key,
309 'oauth_timestamp': generate_timestamp(),
310 'oauth_nonce': generate_nonce(),
311 'oauth_version': OAuthRequest.version,
312 }
313
314 defaults.update(parameters)
315 parameters = defaults
316
317 if token:
318 parameters['oauth_token'] = token.key
319 if token.callback:
320 parameters['oauth_callback'] = token.callback
321 # 1.0a support for verifier.
322 if verifier:
323 parameters['oauth_verifier'] = verifier
324 elif callback:
325 # 1.0a support for callback in the request token request.
326 parameters['oauth_callback'] = callback
327
328 return OAuthRequest(http_method, http_url, parameters)
329 from_consumer_and_token = staticmethod(from_consumer_and_token)
330
331 def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
332 http_url=None, parameters=None):
333 if not parameters:
334 parameters = {}
335
336 parameters['oauth_token'] = token.key
337
338 if callback:
339 parameters['oauth_callback'] = callback
340
341 return OAuthRequest(http_method, http_url, parameters)
342 from_token_and_callback = staticmethod(from_token_and_callback)
343
344 def _split_header(header):
345 """Turn Authorization: header into parameters."""
346 params = {}
347 parts = header.split(',')
348 for param in parts:
349 # Ignore realm parameter.
350 if param.find('realm') > -1:
351 continue
352 # Remove whitespace.
353 param = param.strip()
354 # Split key-value.
355 param_parts = param.split('=', 1)
356 # Remove quotes and unescape the value.
357 params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
358 return params
359 _split_header = staticmethod(_split_header)
360
361 def _split_url_string(param_str):
362 """Turn URL string into parameters."""
363 parameters = cgi.parse_qs(param_str, keep_blank_values=False)
364 for k, v in parameters.iteritems():
365 parameters[k] = urllib.unquote(v[0])
366 return parameters
367 _split_url_string = staticmethod(_split_url_string)
368
369class OAuthServer(object):
370 """A worker to check the validity of a request against a data store."""
371 timestamp_threshold = 300 # In seconds, five minutes.
372 version = VERSION
373 signature_methods = None
374 data_store = None
375
376 def __init__(self, data_store=None, signature_methods=None):
377 self.data_store = data_store
378 self.signature_methods = signature_methods or {}
379
380 def set_data_store(self, data_store):
381 self.data_store = data_store
382
383 def get_data_store(self):
384 return self.data_store
385
386 def add_signature_method(self, signature_method):
387 self.signature_methods[signature_method.get_name()] = signature_method
388 return self.signature_methods
389
390 def fetch_request_token(self, oauth_request):
391 """Processes a request_token request and returns the
392 request token on success.
393 """
394 try:
395 # Get the request token for authorization.
396 token = self._get_token(oauth_request, 'request')
397 except OAuthError:
398 # No token required for the initial token request.
399 version = self._get_version(oauth_request)
400 consumer = self._get_consumer(oauth_request)
401 try:
402 callback = self.get_callback(oauth_request)
403 except OAuthError:
404 callback = None # 1.0, no callback specified.
405 self._check_signature(oauth_request, consumer, None)
406 # Fetch a new token.
407 token = self.data_store.fetch_request_token(consumer, callback)
408 return token
409
410 def fetch_access_token(self, oauth_request):
411 """Processes an access_token request and returns the
412 access token on success.
413 """
414 version = self._get_version(oauth_request)
415 consumer = self._get_consumer(oauth_request)
416 try:
417 verifier = self._get_verifier(oauth_request)
418 except OAuthError:
419 verifier = None
420 # Get the request token.
421 token = self._get_token(oauth_request, 'request')
422 self._check_signature(oauth_request, consumer, token)
423 new_token = self.data_store.fetch_access_token(consumer, token, verifier)
424 return new_token
425
426 def verify_request(self, oauth_request):
427 """Verifies an api call and checks all the parameters."""
428 # -> consumer and token
429 version = self._get_version(oauth_request)
430 consumer = self._get_consumer(oauth_request)
431 # Get the access token.
432 token = self._get_token(oauth_request, 'access')
433 self._check_signature(oauth_request, consumer, token)
434 parameters = oauth_request.get_nonoauth_parameters()
435 return consumer, token, parameters
436
437 def authorize_token(self, token, user):
438 """Authorize a request token."""
439 return self.data_store.authorize_request_token(token, user)
440
441 def get_callback(self, oauth_request):
442 """Get the callback URL."""
443 return oauth_request.get_parameter('oauth_callback')
444
445 def build_authenticate_header(self, realm=''):
446 """Optional support for the authenticate header."""
447 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
448
449 def _get_version(self, oauth_request):
450 """Verify the correct version request for this server."""
451 try:
452 version = oauth_request.get_parameter('oauth_version')
453 except:
454 version = VERSION
455 if version and version != self.version:
456 raise OAuthError('OAuth version %s not supported.' % str(version))
457 return version
458
459 def _get_signature_method(self, oauth_request):
460 """Figure out the signature with some defaults."""
461 try:
462 signature_method = oauth_request.get_parameter(
463 'oauth_signature_method')
464 except:
465 signature_method = SIGNATURE_METHOD
466 try:
467 # Get the signature method object.
468 signature_method = self.signature_methods[signature_method]
469 except:
470 signature_method_names = ', '.join(self.signature_methods.keys())
471 raise OAuthError('Signature method %s not supported try one of the '
472 'following: %s' % (signature_method, signature_method_names))
473
474 return signature_method
475
476 def _get_consumer(self, oauth_request):
477 consumer_key = oauth_request.get_parameter('oauth_consumer_key')
478 consumer = self.data_store.lookup_consumer(consumer_key)
479 if not consumer:
480 raise OAuthError('Invalid consumer.')
481 return consumer
482
483 def _get_token(self, oauth_request, token_type='access'):
484 """Try to find the token for the provided request token key."""
485 token_field = oauth_request.get_parameter('oauth_token')
486 token = self.data_store.lookup_token(token_type, token_field)
487 if not token:
488 raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
489 return token
490
491 def _get_verifier(self, oauth_request):
492 return oauth_request.get_parameter('oauth_verifier')
493
494 def _check_signature(self, oauth_request, consumer, token):
495 timestamp, nonce = oauth_request._get_timestamp_nonce()
496 self._check_timestamp(timestamp)
497 self._check_nonce(consumer, token, nonce)
498 signature_method = self._get_signature_method(oauth_request)
499 try:
500 signature = oauth_request.get_parameter('oauth_signature')
501 except:
502 raise OAuthError('Missing signature.')
503 # Validate the signature.
504 valid_sig = signature_method.check_signature(oauth_request, consumer,
505 token, signature)
506 if not valid_sig:
507 key, base = signature_method.build_signature_base_string(
508 oauth_request, consumer, token)
509 raise OAuthError('Invalid signature. Expected signature base '
510 'string: %s' % base)
511 built = signature_method.build_signature(oauth_request, consumer, token)
512
513 def _check_timestamp(self, timestamp):
514 """Verify that timestamp is recentish."""
515 timestamp = int(timestamp)
516 now = int(time.time())
517 lapsed = abs(now - timestamp)
518 if lapsed > self.timestamp_threshold:
519 raise OAuthError('Expired timestamp: given %d and now %s has a '
520 'greater difference than threshold %d' %
521 (timestamp, now, self.timestamp_threshold))
522
523 def _check_nonce(self, consumer, token, nonce):
524 """Verify that the nonce is uniqueish."""
525 nonce = self.data_store.lookup_nonce(consumer, token, nonce)
526 if nonce:
527 raise OAuthError('Nonce already used: %s' % str(nonce))
528
529
530class OAuthClient(object):
531 """OAuthClient is a worker to attempt to execute a request."""
532 consumer = None
533 token = None
534
535 def __init__(self, oauth_consumer, oauth_token):
536 self.consumer = oauth_consumer
537 self.token = oauth_token
538
539 def get_consumer(self):
540 return self.consumer
541
542 def get_token(self):
543 return self.token
544
545 def fetch_request_token(self, oauth_request):
546 """-> OAuthToken."""
547 raise NotImplementedError
548
549 def fetch_access_token(self, oauth_request):
550 """-> OAuthToken."""
551 raise NotImplementedError
552
553 def access_resource(self, oauth_request):
554 """-> Some protected resource."""
555 raise NotImplementedError
556
557
558class OAuthDataStore(object):
559 """A database abstraction used to lookup consumers and tokens."""
560
561 def lookup_consumer(self, key):
562 """-> OAuthConsumer."""
563 raise NotImplementedError
564
565 def lookup_token(self, oauth_consumer, token_type, token_token):
566 """-> OAuthToken."""
567 raise NotImplementedError
568
569 def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
570 """-> OAuthToken."""
571 raise NotImplementedError
572
573 def fetch_request_token(self, oauth_consumer, oauth_callback):
574 """-> OAuthToken."""
575 raise NotImplementedError
576
577 def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
578 """-> OAuthToken."""
579 raise NotImplementedError
580
581 def authorize_request_token(self, oauth_token, user):
582 """-> OAuthToken."""
583 raise NotImplementedError
584
585
586class OAuthSignatureMethod(object):
587 """A strategy class that implements a signature method."""
588 def get_name(self):
589 """-> str."""
590 raise NotImplementedError
591
592 def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
593 """-> str key, str raw."""
594 raise NotImplementedError
595
596 def build_signature(self, oauth_request, oauth_consumer, oauth_token):
597 """-> str."""
598 raise NotImplementedError
599
600 def check_signature(self, oauth_request, consumer, token, signature):
601 built = self.build_signature(oauth_request, consumer, token)
602 return built == signature
603
604
605class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
606
607 def get_name(self):
608 return 'HMAC-SHA1'
609
610 def build_signature_base_string(self, oauth_request, consumer, token):
611 sig = (
612 escape(oauth_request.get_normalized_http_method()),
613 escape(oauth_request.get_normalized_http_url()),
614 escape(oauth_request.get_normalized_parameters()),
615 )
616
617 key = '%s&' % escape(consumer.secret)
618 if token:
619 key += escape(token.secret)
620 raw = '&'.join(sig)
621 return key, raw
622
623 def build_signature(self, oauth_request, consumer, token):
624 """Builds the base signature string."""
625 key, raw = self.build_signature_base_string(oauth_request, consumer,
626 token)
627
628 # HMAC object.
629 try:
630 import hashlib # 2.5
631 hashed = hmac.new(key, raw, hashlib.sha1)
632 except:
633 import sha # Deprecated
634 hashed = hmac.new(key, raw, sha)
635
636 # Calculate the digest base 64.
637 return binascii.b2a_base64(hashed.digest())[:-1]
638
639
640class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
641
642 def get_name(self):
643 return 'PLAINTEXT'
644
645 def build_signature_base_string(self, oauth_request, consumer, token):
646 """Concatenates the consumer key and secret."""
647 sig = '%s&' % escape(consumer.secret)
648 if token:
649 sig = sig + escape(token.secret)
650 return sig, sig
651
652 def build_signature(self, oauth_request, consumer, token):
653 key, raw = self.build_signature_base_string(oauth_request, consumer,
654 token)
655 return key
0\ No newline at end of file656\ No newline at end of file
1657
=== added file 'GTG/backends/tweepy/parsers.py'
--- GTG/backends/tweepy/parsers.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/parsers.py 2010-08-25 16:31:45 +0000
@@ -0,0 +1,84 @@
1# Tweepy
2# Copyright 2009-2010 Joshua Roesslein
3# See LICENSE for details.
4
5from tweepy.models import ModelFactory
6from tweepy.utils import import_simplejson
7from tweepy.error import TweepError
8
9
10class Parser(object):
11
12 def parse(self, method, payload):
13 """
14 Parse the response payload and return the result.
15 Returns a tuple that contains the result data and the cursors
16 (or None if not present).
17 """
18 raise NotImplementedError
19
20 def parse_error(self, payload):
21 """
22 Parse the error message from payload.
23 If unable to parse the message, throw an exception
24 and default error message will be used.
25 """
26 raise NotImplementedError
27
28
29class JSONParser(Parser):
30
31 payload_format = 'json'
32
33 def __init__(self):
34 self.json_lib = import_simplejson()
35
36 def parse(self, method, payload):
37 try:
38 json = self.json_lib.loads(payload)
39 except Exception, e:
40 raise TweepError('Failed to parse JSON payload: %s' % e)
41
42 if isinstance(json, dict) and 'previous_cursor' in json and 'next_cursor' in json:
43 cursors = json['previous_cursor'], json['next_cursor']
44 return json, cursors
45 else:
46 return json
47
48 def parse_error(self, payload):
49 error = self.json_lib.loads(payload)
50 if error.has_key('error'):
51 return error['error']
52 else:
53 return error['errors']
54
55
56class ModelParser(JSONParser):
57
58 def __init__(self, model_factory=None):
59 JSONParser.__init__(self)
60 self.model_factory = model_factory or ModelFactory
61
62 def parse(self, method, payload):
63 try:
64 if method.payload_type is None: return
65 model = getattr(self.model_factory, method.payload_type)
66 except AttributeError:
67 raise TweepError('No model for this payload type: %s' % method.payload_type)
68
69 json = JSONParser.parse(self, method, payload)
70 if isinstance(json, tuple):
71 json, cursors = json
72 else:
73 cursors = None
74
75 if method.payload_list:
76 result = model.parse_list(method.api, json)
77 else:
78 result = model.parse(method.api, json)
79
80 if cursors:
81 return result, cursors
82 else:
83 return result
84
085
=== added file 'GTG/backends/tweepy/streaming.py'
--- GTG/backends/tweepy/streaming.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/streaming.py 2010-08-25 16:31:45 +0000
@@ -0,0 +1,200 @@
1# Tweepy
2# Copyright 2009-2010 Joshua Roesslein
3# See LICENSE for details.
4
5import httplib
6from socket import timeout
7from threading import Thread
8from time import sleep
9import urllib
10
11from tweepy.auth import BasicAuthHandler
12from tweepy.models import Status
13from tweepy.api import API
14from tweepy.error import TweepError
15
16from tweepy.utils import import_simplejson
17json = import_simplejson()
18
19STREAM_VERSION = 1
20
21
22class StreamListener(object):
23
24 def __init__(self, api=None):
25 self.api = api or API()
26
27 def on_data(self, data):
28 """Called when raw data is received from connection.
29
30 Override this method if you wish to manually handle
31 the stream data. Return False to stop stream and close connection.
32 """
33
34 if 'in_reply_to_status_id' in data:
35 status = Status.parse(self.api, json.loads(data))
36 if self.on_status(status) is False:
37 return False
38 elif 'delete' in data:
39 delete = json.loads(data)['delete']['status']
40 if self.on_delete(delete['id'], delete['user_id']) is False:
41 return False
42 elif 'limit' in data:
43 if self.on_limit(json.loads(data)['limit']['track']) is False:
44 return False
45
46 def on_status(self, status):
47 """Called when a new status arrives"""
48 return
49
50 def on_delete(self, status_id, user_id):
51 """Called when a delete notice arrives for a status"""
52 return
53
54 def on_limit(self, track):
55 """Called when a limitation notice arrvies"""
56 return
57
58 def on_error(self, status_code):
59 """Called when a non-200 status code is returned"""
60 return False
61
62 def on_timeout(self):
63 """Called when stream connection times out"""
64 return
65
66
67class Stream(object):
68
69 host = 'stream.twitter.com'
70
71 def __init__(self, username, password, listener, timeout=5.0, retry_count = None,
72 retry_time = 10.0, snooze_time = 5.0, buffer_size=1500, headers=None):
73 self.auth = BasicAuthHandler(username, password)
74 self.running = False
75 self.timeout = timeout
76 self.retry_count = retry_count
77 self.retry_time = retry_time
78 self.snooze_time = snooze_time
79 self.buffer_size = buffer_size
80 self.listener = listener
81 self.api = API()
82 self.headers = headers or {}
83 self.body = None
84
85 def _run(self):
86 # setup
87 self.auth.apply_auth(None, None, self.headers, None)
88
89 # enter loop
90 error_counter = 0
91 conn = None
92 exception = None
93 while self.running:
94 if self.retry_count and error_counter > self.retry_count:
95 # quit if error count greater than retry count
96 break
97 try:
98 conn = httplib.HTTPConnection(self.host)
99 conn.connect()
100 conn.sock.settimeout(self.timeout)
101 conn.request('POST', self.url, self.body, headers=self.headers)
102 resp = conn.getresponse()
103 if resp.status != 200:
104 if self.listener.on_error(resp.status) is False:
105 break
106 error_counter += 1
107 sleep(self.retry_time)
108 else:
109 error_counter = 0
110 self._read_loop(resp)
111 except timeout:
112 if self.listener.on_timeout() == False:
113 break
114 if self.running is False:
115 break
116 conn.close()
117 sleep(self.snooze_time)
118 except Exception, exception:
119 # any other exception is fatal, so kill loop
120 break
121
122 # cleanup
123 self.running = False
124 if conn:
125 conn.close()
126
127 if exception:
128 raise exception
129
130 def _read_loop(self, resp):
131 data = ''
132 while self.running:
133 if resp.isclosed():
134 break
135
136 # read length
137 length = ''
138 while True:
139 c = resp.read(1)
140 if c == '\n':
141 break
142 length += c
143 length = length.strip()
144 if length.isdigit():
145 length = int(length)
146 else:
147 continue
148
149 # read data and pass into listener
150 data = resp.read(length)
151 if self.listener.on_data(data) is False:
152 self.running = False
153
154 def _start(self, async):
155 self.running = True
156 if async:
157 Thread(target=self._run).start()
158 else:
159 self._run()
160
161 def firehose(self, count=None, async=False):
162 if self.running:
163 raise TweepError('Stream object already connected!')
164 self.url = '/%i/statuses/firehose.json?delimited=length' % STREAM_VERSION
165 if count:
166 self.url += '&count=%s' % count
167 self._start(async)
168
169 def retweet(self, async=False):
170 if self.running:
171 raise TweepError('Stream object already connected!')
172 self.url = '/%i/statuses/retweet.json?delimited=length' % STREAM_VERSION
173 self._start(async)
174
175 def sample(self, count=None, async=False):
176 if self.running:
177 raise TweepError('Stream object already connected!')
178 self.url = '/%i/statuses/sample.json?delimited=length' % STREAM_VERSION
179 if count:
180 self.url += '&count=%s' % count
181 self._start(async)
182
183 def filter(self, follow=None, track=None, async=False):
184 params = {}
185 self.headers['Content-type'] = "application/x-www-form-urlencoded"
186 if self.running:
187 raise TweepError('Stream object already connected!')
188 self.url = '/%i/statuses/filter.json?delimited=length' % STREAM_VERSION
189 if follow:
190 params['follow'] = ','.join(map(str, follow))
191 if track:
192 params['track'] = ','.join(map(str, track))
193 self.body = urllib.urlencode(params)
194 self._start(async)
195
196 def disconnect(self):
197 if self.running is False:
198 return
199 self.running = False
200
0201
=== added file 'GTG/backends/tweepy/utils.py'
--- GTG/backends/tweepy/utils.py 1970-01-01 00:00:00 +0000
+++ GTG/backends/tweepy/utils.py 2010-08-25 16:31:45 +0000
@@ -0,0 +1,98 @@
1# Tweepy
2# Copyright 2010 Joshua Roesslein
3# See LICENSE for details.
4
5from datetime import datetime
6import time
7import htmlentitydefs
8import re
9import locale
10
11
12def parse_datetime(string):
13 # Set locale for date parsing
14 locale.setlocale(locale.LC_TIME, 'C')
15
16 # We must parse datetime this way to work in python 2.4
17 date = datetime(*(time.strptime(string, '%a %b %d %H:%M:%S +0000 %Y')[0:6]))
18
19 # Reset locale back to the default setting
20 locale.setlocale(locale.LC_TIME, '')
21 return date
22
23
24def parse_html_value(html):
25
26 return html[html.find('>')+1:html.rfind('<')]
27
28
29def parse_a_href(atag):
30
31 start = atag.find('"') + 1
32 end = atag.find('"', start)
33 return atag[start:end]
34
35
36def parse_search_datetime(string):
37 # Set locale for date parsing
38 locale.setlocale(locale.LC_TIME, 'C')
39
40 # We must parse datetime this way to work in python 2.4
41 date = datetime(*(time.strptime(string, '%a, %d %b %Y %H:%M:%S +0000')[0:6]))
42
43 # Reset locale back to the default setting
44 locale.setlocale(locale.LC_TIME, '')
45 return date
46
47
48def unescape_html(text):
49 """Created by Fredrik Lundh (http://effbot.org/zone/re-sub.htm#unescape-html)"""
50 def fixup(m):
51 text = m.group(0)
52 if text[:2] == "&#":
53 # character reference
54 try:
55 if text[:3] == "&#x":
56 return unichr(int(text[3:-1], 16))
57 else:
58 return unichr(int(text[2:-1]))
59 except ValueError:
60 pass
61 else:
62 # named entity
63 try:
64 text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
65 except KeyError:
66 pass
67 return text # leave as is
68 return re.sub("&#?\w+;", fixup, text)
69
70
71def convert_to_utf8_str(arg):
72 # written by Michael Norton (http://docondev.blogspot.com/)
73 if isinstance(arg, unicode):
74 arg = arg.encode('utf-8')
75 elif not isinstance(arg, str):
76 arg = str(arg)
77 return arg
78
79
80
81def import_simplejson():
82 try:
83 import simplejson as json
84 except ImportError:
85 try:
86 import json # Python 2.6+
87 except ImportError:
88 try:
89 from django.utils import simplejson as json # Google App Engine
90 except ImportError:
91 raise ImportError, "Can't load a json library"
92
93 return json
94
95def list_to_csv(item_list):
96 if item_list:
97 return ','.join([str(i) for i in item_list])
98
099
=== added file 'data/icons/hicolor/scalable/apps/backend_twitter.png'
1Binary 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 differ100Binary 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: