GTG

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

Proposed by Luca Invernizzi
Status: Merged
Merged at revision: 880
Proposed branch: lp:~gtg-user/gtg/identica-backend
Merge into: lp:~gtg/gtg/old-trunk
Diff against target: 2839 lines (+2827/-0)
2 files modified
GTG/backends/backend_identica.py (+280/-0)
GTG/backends/twitter.py (+2547/-0)
To merge this branch: bzr merge lp:~gtg-user/gtg/identica-backend
Reviewer Review Type Date Requested Status
Gtg developers Pending
Review via email: mp+33479@code.launchpad.net

Description of the change

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

Note: i've added a branch with all my merge requests merged. https://code.edge.launchpad.net/~gtg-user/gtg/all_the_backends_merge_requests/

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

removed print statements

877. By Luca Invernizzi

merge w/ trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'GTG/backends/backend_identica.py'
2--- GTG/backends/backend_identica.py 1970-01-01 00:00:00 +0000
3+++ GTG/backends/backend_identica.py 2010-08-25 16:29:48 +0000
4@@ -0,0 +1,280 @@
5+# -*- coding: utf-8 -*-
6+# -----------------------------------------------------------------------------
7+# Getting Things Gnome! - a personal organizer for the GNOME desktop
8+# Copyright (c) 2008-2009 - Lionel Dricot & Bertrand Rousseau
9+#
10+# This program is free software: you can redistribute it and/or modify it under
11+# the terms of the GNU General Public License as published by the Free Software
12+# Foundation, either version 3 of the License, or (at your option) any later
13+# version.
14+#
15+# This program is distributed in the hope that it will be useful, but WITHOUT
16+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
18+# details.
19+#
20+# You should have received a copy of the GNU General Public License along with
21+# this program. If not, see <http://www.gnu.org/licenses/>.
22+# -----------------------------------------------------------------------------
23+
24+'''
25+Identi.ca backend: imports direct messages, replies and/or the user timeline.
26+'''
27+
28+import os
29+import re
30+import sys
31+import uuid
32+import urllib2
33+
34+from GTG import _
35+from GTG.backends.genericbackend import GenericBackend
36+from GTG.core import CoreConfig
37+from GTG.backends.backendsignals import BackendSignals
38+from GTG.backends.periodicimportbackend import PeriodicImportBackend
39+from GTG.backends.syncengine import SyncEngine
40+from GTG.tools.logger import Log
41+
42+#The Ubuntu version of python twitter is not updated:
43+# it does not have identi.ca support. Meanwhile, we ship the right version
44+# with our code.
45+import GTG.backends.twitter as twitter
46+
47+
48+
49+class Backend(PeriodicImportBackend):
50+ '''
51+ Identi.ca backend: imports direct messages, replies and/or the user
52+ timeline.
53+ '''
54+
55+
56+ _general_description = { \
57+ GenericBackend.BACKEND_NAME: "backend_identica", \
58+ GenericBackend.BACKEND_HUMAN_NAME: _("Identi.ca"), \
59+ GenericBackend.BACKEND_AUTHORS: ["Luca Invernizzi"], \
60+ GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_IMPORT, \
61+ GenericBackend.BACKEND_DESCRIPTION: \
62+ _("Imports your identi.ca messages into your GTG " + \
63+ "tasks. You can choose to either import all your " + \
64+ "messages or just those with a set of hash tags. \n" + \
65+ "The message will be interpreted following this" + \
66+ " format: \n" + \
67+ "<b>my task title, task description #tag @anothertag</b>\n" + \
68+ " Tags can be anywhere in the message"),\
69+ }
70+
71+ base_url = "http://identi.ca/api/"
72+
73+ _static_parameters = { \
74+ "username": { \
75+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING, \
76+ GenericBackend.PARAM_DEFAULT_VALUE: "", },
77+ "password": { \
78+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_PASSWORD, \
79+ GenericBackend.PARAM_DEFAULT_VALUE: "", },
80+ "period": { \
81+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \
82+ GenericBackend.PARAM_DEFAULT_VALUE: 2, },
83+ "import-tags": { \
84+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_LIST_OF_STRINGS, \
85+ GenericBackend.PARAM_DEFAULT_VALUE: ["#todo"], },
86+ "import-from-replies": { \
87+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
88+ GenericBackend.PARAM_DEFAULT_VALUE: False, },
89+ "import-from-my-tweets": { \
90+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
91+ GenericBackend.PARAM_DEFAULT_VALUE: False, },
92+ "import-from-direct-messages": { \
93+ GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
94+ GenericBackend.PARAM_DEFAULT_VALUE: True, },
95+ }
96+
97+ def __init__(self, parameters):
98+ '''
99+ See GenericBackend for an explanation of this function.
100+ Re-loads the saved state of the synchronization
101+ '''
102+ super(Backend, self).__init__(parameters)
103+ #loading the list of already imported tasks
104+ self.data_path = os.path.join('backends/identica/', "tasks_dict-%s" %\
105+ self.get_id())
106+ self.sync_engine = self._load_pickled_file(self.data_path, \
107+ SyncEngine())
108+
109+ def save_state(self):
110+ '''
111+ See GenericBackend for an explanation of this function.
112+ Saves the state of the synchronization.
113+ '''
114+ self._store_pickled_file(self.data_path, self.sync_engine)
115+
116+ def do_periodic_import(self):
117+ '''
118+ See GenericBackend for an explanation of this function.
119+ '''
120+ #we need to authenticate only to see the direct messages or the replies
121+ # (why the replies? Don't know. But python-twitter requires that)
122+ # (invernizzi)
123+ with self.controlled_execution(self._parameters['username'],\
124+ self._parameters['password'], \
125+ self.base_url, \
126+ self) as api:
127+ #select what to import
128+ tweets_to_import = []
129+ if self._parameters["import-from-direct-messages"]:
130+ tweets_to_import += api.GetDirectMessages()
131+ if self._parameters["import-from-my-tweets"]:
132+ tweets_to_import += \
133+ api.GetUserTimeline(self._parameters["username"])
134+ if self._parameters["import-from-replies"]:
135+ tweets_to_import += \
136+ api.GetReplies(self._parameters["username"])
137+ #do the import
138+ for tweet in tweets_to_import:
139+ self._process_tweet(tweet)
140+
141+ def _process_tweet(self, tweet):
142+ '''
143+ Given a tweet, checks if a task representing it must be
144+ created in GTG and, if so, it creates it.
145+
146+ @param tweet: a tweet.
147+ '''
148+ self.cancellation_point()
149+ tweet_id = str(tweet.GetId())
150+ is_syncable = self._is_tweet_syncable(tweet)
151+ #the "lambda" is because we don't consider tweets deletion (to be
152+ # faster)
153+ action, tid = self.sync_engine.analyze_remote_id(\
154+ tweet_id, \
155+ self.datastore.has_task, \
156+ lambda tweet_id: True, \
157+ is_syncable)
158+ Log.debug("processing tweet (%s, %s)" % (action, is_syncable))
159+
160+ self.cancellation_point()
161+ if action == None or action == SyncEngine.UPDATE:
162+ return
163+
164+ elif action == SyncEngine.ADD:
165+ tid = str(uuid.uuid4())
166+ task = self.datastore.task_factory(tid)
167+ self._populate_task(task, tweet)
168+ #we care only to add tweets and if the list of tags which must be
169+ #imported changes (lost-syncability can happen). Thus, we don't
170+ # care about SyncMeme(s)
171+ self.sync_engine.record_relationship(local_id = tid,\
172+ remote_id = tweet_id, \
173+ meme = None)
174+ self.datastore.push_task(task)
175+
176+ elif action == SyncEngine.LOST_SYNCABILITY:
177+ self.sync_engine.break_relationship(remote_id = tweet_id)
178+ self.datastore.request_task_deletion(tid)
179+
180+ self.save_state()
181+
182+ def _populate_task(self, task, message):
183+ '''
184+ Given a twitter message and a GTG task, fills the task with the content
185+ of the message
186+ '''
187+ try:
188+ #this works only for some messages
189+ task.add_tag("@" + message.GetSenderScreenName())
190+ except:
191+ pass
192+ text = message.GetText()
193+
194+ #convert #hastags to @tags
195+ matches = re.finditer("(?<![^|\s])(#\w+)", text)
196+ for g in matches:
197+ text = text[:g.start()] + '@' + text[g.start() + 1:]
198+ #add tags objects (it's not enough to have @tag in the text to add a
199+ # tag
200+ for tag in self._extract_tags_from_text(text):
201+ task.add_tag(tag)
202+
203+ split_text = text.split(",", 1)
204+ task.set_title(split_text[0])
205+ if len(split_text) > 1:
206+ task.set_text(split_text[1])
207+
208+ task.add_remote_id(self.get_id(), str(message.GetId()))
209+
210+ def _is_tweet_syncable(self, tweet):
211+ '''
212+ Returns True if the given tweet matches the user-specified tags to be
213+ synced
214+
215+ @param tweet: a tweet
216+ '''
217+ if CoreConfig.ALLTASKS_TAG in self._parameters["import-tags"]:
218+ return True
219+ else:
220+ tags = set(Backend._extract_tags_from_text(tweet.GetText()))
221+ return tags.intersection(set(self._parameters["import-tags"])) \
222+ != set()
223+
224+ @staticmethod
225+ def _extract_tags_from_text(text):
226+ '''
227+ Given a string, returns a list of @tags and #hashtags
228+ '''
229+ return list(re.findall(r'(?:^|[\s])((?:#|@)\w+)', text))
230+
231+###############################################################################
232+### AUTHENTICATION ############################################################
233+###############################################################################
234+
235+ class controlled_execution(object):
236+ '''
237+ This class performs the login to identica and execute the appropriate
238+ response if something goes wrong during authentication or at network
239+ level
240+ '''
241+
242+ def __init__(self, username, password, base_url, backend):
243+ '''
244+ Sets the login parameters
245+ '''
246+ self.username = username
247+ self.password = password
248+ self.backend = backend
249+ self.base_url = base_url
250+
251+ def __enter__(self):
252+ '''
253+ Logins to identica and returns the Api object
254+ '''
255+ return twitter.Api(self.username, self.password, \
256+ base_url = self.base_url)
257+
258+ def __exit__(self, type, value, traceback):
259+ '''
260+ Analyzes the eventual exception risen during the connection to
261+ identica
262+ '''
263+ if isinstance(value, urllib2.HTTPError):
264+ if value.getcode() == 401:
265+ self.signal_authentication_wrong()
266+ if value.getcode() in [502, 404]:
267+ self.signal_network_down()
268+ elif isinstance(value, twitter.TwitterError):
269+ self.signal_authentication_wrong()
270+ elif isinstance(value, urllib2.URLError):
271+ self.signal_network_down()
272+ else:
273+ return False
274+ return True
275+
276+ def signal_authentication_wrong(self):
277+ self.backend.quit(disable = True)
278+ BackendSignals().backend_failed(self.backend.get_id(), \
279+ BackendSignals.ERRNO_AUTHENTICATION)
280+
281+ def signal_network_down(self):
282+ BackendSignals().backend_failed(self.backend.get_id(), \
283+ BackendSignals.ERRNO_NETWORK)
284+
285
286=== added file 'GTG/backends/twitter.py'
287--- GTG/backends/twitter.py 1970-01-01 00:00:00 +0000
288+++ GTG/backends/twitter.py 2010-08-25 16:29:48 +0000
289@@ -0,0 +1,2547 @@
290+#!/usr/bin/python2.4
291+#
292+# Copyright 2007 Google Inc. All Rights Reserved.
293+#
294+# Licensed under the Apache License, Version 2.0 (the "License");
295+# you may not use this file except in compliance with the License.
296+# You may obtain a copy of the License at
297+#
298+# http://www.apache.org/licenses/LICENSE-2.0
299+#
300+# Unless required by applicable law or agreed to in writing, software
301+# distributed under the License is distributed on an "AS IS" BASIS,
302+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
303+# See the License for the specific language governing permissions and
304+# limitations under the License.
305+
306+'''A library that provides a python interface to the Twitter API'''
307+
308+__author__ = 'dewitt@google.com'
309+__version__ = '0.7-devel'
310+
311+
312+import base64
313+import calendar
314+import httplib
315+import os
316+import rfc822
317+import simplejson
318+import sys
319+import tempfile
320+import textwrap
321+import time
322+import urllib
323+import urllib2
324+import urlparse
325+import gzip
326+import StringIO
327+
328+try:
329+ from hashlib import md5
330+except ImportError:
331+ from md5 import md5
332+
333+
334+CHARACTER_LIMIT = 140
335+
336+# A singleton representing a lazily instantiated FileCache.
337+DEFAULT_CACHE = object()
338+
339+
340+class TwitterError(Exception):
341+ '''Base class for Twitter errors'''
342+
343+ @property
344+ def message(self):
345+ '''Returns the first argument used to construct this error.'''
346+ return self.args[0]
347+
348+
349+class Status(object):
350+ '''A class representing the Status structure used by the twitter API.
351+
352+ The Status structure exposes the following properties:
353+
354+ status.created_at
355+ status.created_at_in_seconds # read only
356+ status.favorited
357+ status.in_reply_to_screen_name
358+ status.in_reply_to_user_id
359+ status.in_reply_to_status_id
360+ status.truncated
361+ status.source
362+ status.id
363+ status.text
364+ status.location
365+ status.relative_created_at # read only
366+ status.user
367+ '''
368+ def __init__(self,
369+ created_at=None,
370+ favorited=None,
371+ id=None,
372+ text=None,
373+ location=None,
374+ user=None,
375+ in_reply_to_screen_name=None,
376+ in_reply_to_user_id=None,
377+ in_reply_to_status_id=None,
378+ truncated=None,
379+ source=None,
380+ now=None):
381+ '''An object to hold a Twitter status message.
382+
383+ This class is normally instantiated by the twitter.Api class and
384+ returned in a sequence.
385+
386+ Note: Dates are posted in the form "Sat Jan 27 04:17:38 +0000 2007"
387+
388+ Args:
389+ created_at: The time this status message was posted
390+ favorited: Whether this is a favorite of the authenticated user
391+ id: The unique id of this status message
392+ text: The text of this status message
393+ location: the geolocation string associated with this message
394+ relative_created_at:
395+ A human readable string representing the posting time
396+ user:
397+ A twitter.User instance representing the person posting the message
398+ now:
399+ The current time, if the client choses to set it. Defaults to the
400+ wall clock time.
401+ '''
402+ self.created_at = created_at
403+ self.favorited = favorited
404+ self.id = id
405+ self.text = text
406+ self.location = location
407+ self.user = user
408+ self.now = now
409+ self.in_reply_to_screen_name = in_reply_to_screen_name
410+ self.in_reply_to_user_id = in_reply_to_user_id
411+ self.in_reply_to_status_id = in_reply_to_status_id
412+ self.truncated = truncated
413+ self.source = source
414+
415+ def GetCreatedAt(self):
416+ '''Get the time this status message was posted.
417+
418+ Returns:
419+ The time this status message was posted
420+ '''
421+ return self._created_at
422+
423+ def SetCreatedAt(self, created_at):
424+ '''Set the time this status message was posted.
425+
426+ Args:
427+ created_at: The time this status message was created
428+ '''
429+ self._created_at = created_at
430+
431+ created_at = property(GetCreatedAt, SetCreatedAt,
432+ doc='The time this status message was posted.')
433+
434+ def GetCreatedAtInSeconds(self):
435+ '''Get the time this status message was posted, in seconds since the epoch.
436+
437+ Returns:
438+ The time this status message was posted, in seconds since the epoch.
439+ '''
440+ return calendar.timegm(rfc822.parsedate(self.created_at))
441+
442+ created_at_in_seconds = property(GetCreatedAtInSeconds,
443+ doc="The time this status message was "
444+ "posted, in seconds since the epoch")
445+
446+ def GetFavorited(self):
447+ '''Get the favorited setting of this status message.
448+
449+ Returns:
450+ True if this status message is favorited; False otherwise
451+ '''
452+ return self._favorited
453+
454+ def SetFavorited(self, favorited):
455+ '''Set the favorited state of this status message.
456+
457+ Args:
458+ favorited: boolean True/False favorited state of this status message
459+ '''
460+ self._favorited = favorited
461+
462+ favorited = property(GetFavorited, SetFavorited,
463+ doc='The favorited state of this status message.')
464+
465+ def GetId(self):
466+ '''Get the unique id of this status message.
467+
468+ Returns:
469+ The unique id of this status message
470+ '''
471+ return self._id
472+
473+ def SetId(self, id):
474+ '''Set the unique id of this status message.
475+
476+ Args:
477+ id: The unique id of this status message
478+ '''
479+ self._id = id
480+
481+ id = property(GetId, SetId,
482+ doc='The unique id of this status message.')
483+
484+ def GetInReplyToScreenName(self):
485+ return self._in_reply_to_screen_name
486+
487+ def SetInReplyToScreenName(self, in_reply_to_screen_name):
488+ self._in_reply_to_screen_name = in_reply_to_screen_name
489+
490+ in_reply_to_screen_name = property(GetInReplyToScreenName, SetInReplyToScreenName,
491+ doc='')
492+
493+ def GetInReplyToUserId(self):
494+ return self._in_reply_to_user_id
495+
496+ def SetInReplyToUserId(self, in_reply_to_user_id):
497+ self._in_reply_to_user_id = in_reply_to_user_id
498+
499+ in_reply_to_user_id = property(GetInReplyToUserId, SetInReplyToUserId,
500+ doc='')
501+
502+ def GetInReplyToStatusId(self):
503+ return self._in_reply_to_status_id
504+
505+ def SetInReplyToStatusId(self, in_reply_to_status_id):
506+ self._in_reply_to_status_id = in_reply_to_status_id
507+
508+ in_reply_to_status_id = property(GetInReplyToStatusId, SetInReplyToStatusId,
509+ doc='')
510+
511+ def GetTruncated(self):
512+ return self._truncated
513+
514+ def SetTruncated(self, truncated):
515+ self._truncated = truncated
516+
517+ truncated = property(GetTruncated, SetTruncated,
518+ doc='')
519+
520+ def GetSource(self):
521+ return self._source
522+
523+ def SetSource(self, source):
524+ self._source = source
525+
526+ source = property(GetSource, SetSource,
527+ doc='')
528+
529+ def GetText(self):
530+ '''Get the text of this status message.
531+
532+ Returns:
533+ The text of this status message.
534+ '''
535+ return self._text
536+
537+ def SetText(self, text):
538+ '''Set the text of this status message.
539+
540+ Args:
541+ text: The text of this status message
542+ '''
543+ self._text = text
544+
545+ text = property(GetText, SetText,
546+ doc='The text of this status message')
547+
548+ def GetLocation(self):
549+ '''Get the geolocation associated with this status message
550+
551+ Returns:
552+ The geolocation string of this status message.
553+ '''
554+ return self._location
555+
556+ def SetLocation(self, location):
557+ '''Set the geolocation associated with this status message
558+
559+ Args:
560+ location: The geolocation string of this status message
561+ '''
562+ self._location = location
563+
564+ location = property(GetLocation, SetLocation,
565+ doc='The geolocation string of this status message')
566+
567+ def GetRelativeCreatedAt(self):
568+ '''Get a human redable string representing the posting time
569+
570+ Returns:
571+ A human readable string representing the posting time
572+ '''
573+ fudge = 1.25
574+ delta = long(self.now) - long(self.created_at_in_seconds)
575+
576+ if delta < (1 * fudge):
577+ return 'about a second ago'
578+ elif delta < (60 * (1/fudge)):
579+ return 'about %d seconds ago' % (delta)
580+ elif delta < (60 * fudge):
581+ return 'about a minute ago'
582+ elif delta < (60 * 60 * (1/fudge)):
583+ return 'about %d minutes ago' % (delta / 60)
584+ elif delta < (60 * 60 * fudge) or delta / (60 * 60) == 1:
585+ return 'about an hour ago'
586+ elif delta < (60 * 60 * 24 * (1/fudge)):
587+ return 'about %d hours ago' % (delta / (60 * 60))
588+ elif delta < (60 * 60 * 24 * fudge) or delta / (60 * 60 * 24) == 1:
589+ return 'about a day ago'
590+ else:
591+ return 'about %d days ago' % (delta / (60 * 60 * 24))
592+
593+ relative_created_at = property(GetRelativeCreatedAt,
594+ doc='Get a human readable string representing'
595+ 'the posting time')
596+
597+ def GetUser(self):
598+ '''Get a twitter.User reprenting the entity posting this status message.
599+
600+ Returns:
601+ A twitter.User reprenting the entity posting this status message
602+ '''
603+ return self._user
604+
605+ def SetUser(self, user):
606+ '''Set a twitter.User reprenting the entity posting this status message.
607+
608+ Args:
609+ user: A twitter.User reprenting the entity posting this status message
610+ '''
611+ self._user = user
612+
613+ user = property(GetUser, SetUser,
614+ doc='A twitter.User reprenting the entity posting this '
615+ 'status message')
616+
617+ def GetNow(self):
618+ '''Get the wallclock time for this status message.
619+
620+ Used to calculate relative_created_at. Defaults to the time
621+ the object was instantiated.
622+
623+ Returns:
624+ Whatever the status instance believes the current time to be,
625+ in seconds since the epoch.
626+ '''
627+ if self._now is None:
628+ self._now = time.time()
629+ return self._now
630+
631+ def SetNow(self, now):
632+ '''Set the wallclock time for this status message.
633+
634+ Used to calculate relative_created_at. Defaults to the time
635+ the object was instantiated.
636+
637+ Args:
638+ now: The wallclock time for this instance.
639+ '''
640+ self._now = now
641+
642+ now = property(GetNow, SetNow,
643+ doc='The wallclock time for this status instance.')
644+
645+
646+ def __ne__(self, other):
647+ return not self.__eq__(other)
648+
649+ def __eq__(self, other):
650+ try:
651+ return other and \
652+ self.created_at == other.created_at and \
653+ self.id == other.id and \
654+ self.text == other.text and \
655+ self.location == other.location and \
656+ self.user == other.user and \
657+ self.in_reply_to_screen_name == other.in_reply_to_screen_name and \
658+ self.in_reply_to_user_id == other.in_reply_to_user_id and \
659+ self.in_reply_to_status_id == other.in_reply_to_status_id and \
660+ self.truncated == other.truncated and \
661+ self.favorited == other.favorited and \
662+ self.source == other.source
663+ except AttributeError:
664+ return False
665+
666+ def __str__(self):
667+ '''A string representation of this twitter.Status instance.
668+
669+ The return value is the same as the JSON string representation.
670+
671+ Returns:
672+ A string representation of this twitter.Status instance.
673+ '''
674+ return self.AsJsonString()
675+
676+ def AsJsonString(self):
677+ '''A JSON string representation of this twitter.Status instance.
678+
679+ Returns:
680+ A JSON string representation of this twitter.Status instance
681+ '''
682+ return simplejson.dumps(self.AsDict(), sort_keys=True)
683+
684+ def AsDict(self):
685+ '''A dict representation of this twitter.Status instance.
686+
687+ The return value uses the same key names as the JSON representation.
688+
689+ Return:
690+ A dict representing this twitter.Status instance
691+ '''
692+ data = {}
693+ if self.created_at:
694+ data['created_at'] = self.created_at
695+ if self.favorited:
696+ data['favorited'] = self.favorited
697+ if self.id:
698+ data['id'] = self.id
699+ if self.text:
700+ data['text'] = self.text
701+ if self.location:
702+ data['location'] = self.location
703+ if self.user:
704+ data['user'] = self.user.AsDict()
705+ if self.in_reply_to_screen_name:
706+ data['in_reply_to_screen_name'] = self.in_reply_to_screen_name
707+ if self.in_reply_to_user_id:
708+ data['in_reply_to_user_id'] = self.in_reply_to_user_id
709+ if self.in_reply_to_status_id:
710+ data['in_reply_to_status_id'] = self.in_reply_to_status_id
711+ if self.truncated is not None:
712+ data['truncated'] = self.truncated
713+ if self.favorited is not None:
714+ data['favorited'] = self.favorited
715+ if self.source:
716+ data['source'] = self.source
717+ return data
718+
719+ @staticmethod
720+ def NewFromJsonDict(data):
721+ '''Create a new instance based on a JSON dict.
722+
723+ Args:
724+ data: A JSON dict, as converted from the JSON in the twitter API
725+ Returns:
726+ A twitter.Status instance
727+ '''
728+ if 'user' in data:
729+ user = User.NewFromJsonDict(data['user'])
730+ else:
731+ user = None
732+ return Status(created_at=data.get('created_at', None),
733+ favorited=data.get('favorited', None),
734+ id=data.get('id', None),
735+ text=data.get('text', None),
736+ location=data.get('location', None),
737+ in_reply_to_screen_name=data.get('in_reply_to_screen_name', None),
738+ in_reply_to_user_id=data.get('in_reply_to_user_id', None),
739+ in_reply_to_status_id=data.get('in_reply_to_status_id', None),
740+ truncated=data.get('truncated', None),
741+ source=data.get('source', None),
742+ user=user)
743+
744+
745+class User(object):
746+ '''A class representing the User structure used by the twitter API.
747+
748+ The User structure exposes the following properties:
749+
750+ user.id
751+ user.name
752+ user.screen_name
753+ user.location
754+ user.description
755+ user.profile_image_url
756+ user.profile_background_tile
757+ user.profile_background_image_url
758+ user.profile_sidebar_fill_color
759+ user.profile_background_color
760+ user.profile_link_color
761+ user.profile_text_color
762+ user.protected
763+ user.utc_offset
764+ user.time_zone
765+ user.url
766+ user.status
767+ user.statuses_count
768+ user.followers_count
769+ user.friends_count
770+ user.favourites_count
771+ '''
772+ def __init__(self,
773+ id=None,
774+ name=None,
775+ screen_name=None,
776+ location=None,
777+ description=None,
778+ profile_image_url=None,
779+ profile_background_tile=None,
780+ profile_background_image_url=None,
781+ profile_sidebar_fill_color=None,
782+ profile_background_color=None,
783+ profile_link_color=None,
784+ profile_text_color=None,
785+ protected=None,
786+ utc_offset=None,
787+ time_zone=None,
788+ followers_count=None,
789+ friends_count=None,
790+ statuses_count=None,
791+ favourites_count=None,
792+ url=None,
793+ status=None):
794+ self.id = id
795+ self.name = name
796+ self.screen_name = screen_name
797+ self.location = location
798+ self.description = description
799+ self.profile_image_url = profile_image_url
800+ self.profile_background_tile = profile_background_tile
801+ self.profile_background_image_url = profile_background_image_url
802+ self.profile_sidebar_fill_color = profile_sidebar_fill_color
803+ self.profile_background_color = profile_background_color
804+ self.profile_link_color = profile_link_color
805+ self.profile_text_color = profile_text_color
806+ self.protected = protected
807+ self.utc_offset = utc_offset
808+ self.time_zone = time_zone
809+ self.followers_count = followers_count
810+ self.friends_count = friends_count
811+ self.statuses_count = statuses_count
812+ self.favourites_count = favourites_count
813+ self.url = url
814+ self.status = status
815+
816+
817+ def GetId(self):
818+ '''Get the unique id of this user.
819+
820+ Returns:
821+ The unique id of this user
822+ '''
823+ return self._id
824+
825+ def SetId(self, id):
826+ '''Set the unique id of this user.
827+
828+ Args:
829+ id: The unique id of this user.
830+ '''
831+ self._id = id
832+
833+ id = property(GetId, SetId,
834+ doc='The unique id of this user.')
835+
836+ def GetName(self):
837+ '''Get the real name of this user.
838+
839+ Returns:
840+ The real name of this user
841+ '''
842+ return self._name
843+
844+ def SetName(self, name):
845+ '''Set the real name of this user.
846+
847+ Args:
848+ name: The real name of this user
849+ '''
850+ self._name = name
851+
852+ name = property(GetName, SetName,
853+ doc='The real name of this user.')
854+
855+ def GetScreenName(self):
856+ '''Get the short username of this user.
857+
858+ Returns:
859+ The short username of this user
860+ '''
861+ return self._screen_name
862+
863+ def SetScreenName(self, screen_name):
864+ '''Set the short username of this user.
865+
866+ Args:
867+ screen_name: the short username of this user
868+ '''
869+ self._screen_name = screen_name
870+
871+ screen_name = property(GetScreenName, SetScreenName,
872+ doc='The short username of this user.')
873+
874+ def GetLocation(self):
875+ '''Get the geographic location of this user.
876+
877+ Returns:
878+ The geographic location of this user
879+ '''
880+ return self._location
881+
882+ def SetLocation(self, location):
883+ '''Set the geographic location of this user.
884+
885+ Args:
886+ location: The geographic location of this user
887+ '''
888+ self._location = location
889+
890+ location = property(GetLocation, SetLocation,
891+ doc='The geographic location of this user.')
892+
893+ def GetDescription(self):
894+ '''Get the short text description of this user.
895+
896+ Returns:
897+ The short text description of this user
898+ '''
899+ return self._description
900+
901+ def SetDescription(self, description):
902+ '''Set the short text description of this user.
903+
904+ Args:
905+ description: The short text description of this user
906+ '''
907+ self._description = description
908+
909+ description = property(GetDescription, SetDescription,
910+ doc='The short text description of this user.')
911+
912+ def GetUrl(self):
913+ '''Get the homepage url of this user.
914+
915+ Returns:
916+ The homepage url of this user
917+ '''
918+ return self._url
919+
920+ def SetUrl(self, url):
921+ '''Set the homepage url of this user.
922+
923+ Args:
924+ url: The homepage url of this user
925+ '''
926+ self._url = url
927+
928+ url = property(GetUrl, SetUrl,
929+ doc='The homepage url of this user.')
930+
931+ def GetProfileImageUrl(self):
932+ '''Get the url of the thumbnail of this user.
933+
934+ Returns:
935+ The url of the thumbnail of this user
936+ '''
937+ return self._profile_image_url
938+
939+ def SetProfileImageUrl(self, profile_image_url):
940+ '''Set the url of the thumbnail of this user.
941+
942+ Args:
943+ profile_image_url: The url of the thumbnail of this user
944+ '''
945+ self._profile_image_url = profile_image_url
946+
947+ profile_image_url= property(GetProfileImageUrl, SetProfileImageUrl,
948+ doc='The url of the thumbnail of this user.')
949+
950+ def GetProfileBackgroundTile(self):
951+ '''Boolean for whether to tile the profile background image.
952+
953+ Returns:
954+ True if the background is to be tiled, False if not, None if unset.
955+ '''
956+ return self._profile_background_tile
957+
958+ def SetProfileBackgroundTile(self, profile_background_tile):
959+ '''Set the boolean flag for whether to tile the profile background image.
960+
961+ Args:
962+ profile_background_tile: Boolean flag for whether to tile or not.
963+ '''
964+ self._profile_background_tile = profile_background_tile
965+
966+ profile_background_tile = property(GetProfileBackgroundTile, SetProfileBackgroundTile,
967+ doc='Boolean for whether to tile the background image.')
968+
969+ def GetProfileBackgroundImageUrl(self):
970+ return self._profile_background_image_url
971+
972+ def SetProfileBackgroundImageUrl(self, profile_background_image_url):
973+ self._profile_background_image_url = profile_background_image_url
974+
975+ profile_background_image_url = property(GetProfileBackgroundImageUrl, SetProfileBackgroundImageUrl,
976+ doc='The url of the profile background of this user.')
977+
978+ def GetProfileSidebarFillColor(self):
979+ return self._profile_sidebar_fill_color
980+
981+ def SetProfileSidebarFillColor(self, profile_sidebar_fill_color):
982+ self._profile_sidebar_fill_color = profile_sidebar_fill_color
983+
984+ profile_sidebar_fill_color = property(GetProfileSidebarFillColor, SetProfileSidebarFillColor)
985+
986+ def GetProfileBackgroundColor(self):
987+ return self._profile_background_color
988+
989+ def SetProfileBackgroundColor(self, profile_background_color):
990+ self._profile_background_color = profile_background_color
991+
992+ profile_background_color = property(GetProfileBackgroundColor, SetProfileBackgroundColor)
993+
994+ def GetProfileLinkColor(self):
995+ return self._profile_link_color
996+
997+ def SetProfileLinkColor(self, profile_link_color):
998+ self._profile_link_color = profile_link_color
999+
1000+ profile_link_color = property(GetProfileLinkColor, SetProfileLinkColor)
1001+
1002+ def GetProfileTextColor(self):
1003+ return self._profile_text_color
1004+
1005+ def SetProfileTextColor(self, profile_text_color):
1006+ self._profile_text_color = profile_text_color
1007+
1008+ profile_text_color = property(GetProfileTextColor, SetProfileTextColor)
1009+
1010+ def GetProtected(self):
1011+ return self._protected
1012+
1013+ def SetProtected(self, protected):
1014+ self._protected = protected
1015+
1016+ protected = property(GetProtected, SetProtected)
1017+
1018+ def GetUtcOffset(self):
1019+ return self._utc_offset
1020+
1021+ def SetUtcOffset(self, utc_offset):
1022+ self._utc_offset = utc_offset
1023+
1024+ utc_offset = property(GetUtcOffset, SetUtcOffset)
1025+
1026+ def GetTimeZone(self):
1027+ '''Returns the current time zone string for the user.
1028+
1029+ Returns:
1030+ The descriptive time zone string for the user.
1031+ '''
1032+ return self._time_zone
1033+
1034+ def SetTimeZone(self, time_zone):
1035+ '''Sets the user's time zone string.
1036+
1037+ Args:
1038+ time_zone: The descriptive time zone to assign for the user.
1039+ '''
1040+ self._time_zone = time_zone
1041+
1042+ time_zone = property(GetTimeZone, SetTimeZone)
1043+
1044+ def GetStatus(self):
1045+ '''Get the latest twitter.Status of this user.
1046+
1047+ Returns:
1048+ The latest twitter.Status of this user
1049+ '''
1050+ return self._status
1051+
1052+ def SetStatus(self, status):
1053+ '''Set the latest twitter.Status of this user.
1054+
1055+ Args:
1056+ status: The latest twitter.Status of this user
1057+ '''
1058+ self._status = status
1059+
1060+ status = property(GetStatus, SetStatus,
1061+ doc='The latest twitter.Status of this user.')
1062+
1063+ def GetFriendsCount(self):
1064+ '''Get the friend count for this user.
1065+
1066+ Returns:
1067+ The number of users this user has befriended.
1068+ '''
1069+ return self._friends_count
1070+
1071+ def SetFriendsCount(self, count):
1072+ '''Set the friend count for this user.
1073+
1074+ Args:
1075+ count: The number of users this user has befriended.
1076+ '''
1077+ self._friends_count = count
1078+
1079+ friends_count = property(GetFriendsCount, SetFriendsCount,
1080+ doc='The number of friends for this user.')
1081+
1082+ def GetFollowersCount(self):
1083+ '''Get the follower count for this user.
1084+
1085+ Returns:
1086+ The number of users following this user.
1087+ '''
1088+ return self._followers_count
1089+
1090+ def SetFollowersCount(self, count):
1091+ '''Set the follower count for this user.
1092+
1093+ Args:
1094+ count: The number of users following this user.
1095+ '''
1096+ self._followers_count = count
1097+
1098+ followers_count = property(GetFollowersCount, SetFollowersCount,
1099+ doc='The number of users following this user.')
1100+
1101+ def GetStatusesCount(self):
1102+ '''Get the number of status updates for this user.
1103+
1104+ Returns:
1105+ The number of status updates for this user.
1106+ '''
1107+ return self._statuses_count
1108+
1109+ def SetStatusesCount(self, count):
1110+ '''Set the status update count for this user.
1111+
1112+ Args:
1113+ count: The number of updates for this user.
1114+ '''
1115+ self._statuses_count = count
1116+
1117+ statuses_count = property(GetStatusesCount, SetStatusesCount,
1118+ doc='The number of updates for this user.')
1119+
1120+ def GetFavouritesCount(self):
1121+ '''Get the number of favourites for this user.
1122+
1123+ Returns:
1124+ The number of favourites for this user.
1125+ '''
1126+ return self._favourites_count
1127+
1128+ def SetFavouritesCount(self, count):
1129+ '''Set the favourite count for this user.
1130+
1131+ Args:
1132+ count: The number of favourites for this user.
1133+ '''
1134+ self._favourites_count = count
1135+
1136+ favourites_count = property(GetFavouritesCount, SetFavouritesCount,
1137+ doc='The number of favourites for this user.')
1138+
1139+ def __ne__(self, other):
1140+ return not self.__eq__(other)
1141+
1142+ def __eq__(self, other):
1143+ try:
1144+ return other and \
1145+ self.id == other.id and \
1146+ self.name == other.name and \
1147+ self.screen_name == other.screen_name and \
1148+ self.location == other.location and \
1149+ self.description == other.description and \
1150+ self.profile_image_url == other.profile_image_url and \
1151+ self.profile_background_tile == other.profile_background_tile and \
1152+ self.profile_background_image_url == other.profile_background_image_url and \
1153+ self.profile_sidebar_fill_color == other.profile_sidebar_fill_color and \
1154+ self.profile_background_color == other.profile_background_color and \
1155+ self.profile_link_color == other.profile_link_color and \
1156+ self.profile_text_color == other.profile_text_color and \
1157+ self.protected == other.protected and \
1158+ self.utc_offset == other.utc_offset and \
1159+ self.time_zone == other.time_zone and \
1160+ self.url == other.url and \
1161+ self.statuses_count == other.statuses_count and \
1162+ self.followers_count == other.followers_count and \
1163+ self.favourites_count == other.favourites_count and \
1164+ self.friends_count == other.friends_count and \
1165+ self.status == other.status
1166+ except AttributeError:
1167+ return False
1168+
1169+ def __str__(self):
1170+ '''A string representation of this twitter.User instance.
1171+
1172+ The return value is the same as the JSON string representation.
1173+
1174+ Returns:
1175+ A string representation of this twitter.User instance.
1176+ '''
1177+ return self.AsJsonString()
1178+
1179+ def AsJsonString(self):
1180+ '''A JSON string representation of this twitter.User instance.
1181+
1182+ Returns:
1183+ A JSON string representation of this twitter.User instance
1184+ '''
1185+ return simplejson.dumps(self.AsDict(), sort_keys=True)
1186+
1187+ def AsDict(self):
1188+ '''A dict representation of this twitter.User instance.
1189+
1190+ The return value uses the same key names as the JSON representation.
1191+
1192+ Return:
1193+ A dict representing this twitter.User instance
1194+ '''
1195+ data = {}
1196+ if self.id:
1197+ data['id'] = self.id
1198+ if self.name:
1199+ data['name'] = self.name
1200+ if self.screen_name:
1201+ data['screen_name'] = self.screen_name
1202+ if self.location:
1203+ data['location'] = self.location
1204+ if self.description:
1205+ data['description'] = self.description
1206+ if self.profile_image_url:
1207+ data['profile_image_url'] = self.profile_image_url
1208+ if self.profile_background_tile is not None:
1209+ data['profile_background_tile'] = self.profile_background_tile
1210+ if self.profile_background_image_url:
1211+ data['profile_sidebar_fill_color'] = self.profile_background_image_url
1212+ if self.profile_background_color:
1213+ data['profile_background_color'] = self.profile_background_color
1214+ if self.profile_link_color:
1215+ data['profile_link_color'] = self.profile_link_color
1216+ if self.profile_text_color:
1217+ data['profile_text_color'] = self.profile_text_color
1218+ if self.protected is not None:
1219+ data['protected'] = self.protected
1220+ if self.utc_offset:
1221+ data['utc_offset'] = self.utc_offset
1222+ if self.time_zone:
1223+ data['time_zone'] = self.time_zone
1224+ if self.url:
1225+ data['url'] = self.url
1226+ if self.status:
1227+ data['status'] = self.status.AsDict()
1228+ if self.friends_count:
1229+ data['friends_count'] = self.friends_count
1230+ if self.followers_count:
1231+ data['followers_count'] = self.followers_count
1232+ if self.statuses_count:
1233+ data['statuses_count'] = self.statuses_count
1234+ if self.favourites_count:
1235+ data['favourites_count'] = self.favourites_count
1236+ return data
1237+
1238+ @staticmethod
1239+ def NewFromJsonDict(data):
1240+ '''Create a new instance based on a JSON dict.
1241+
1242+ Args:
1243+ data: A JSON dict, as converted from the JSON in the twitter API
1244+ Returns:
1245+ A twitter.User instance
1246+ '''
1247+ if 'status' in data:
1248+ status = Status.NewFromJsonDict(data['status'])
1249+ else:
1250+ status = None
1251+ return User(id=data.get('id', None),
1252+ name=data.get('name', None),
1253+ screen_name=data.get('screen_name', None),
1254+ location=data.get('location', None),
1255+ description=data.get('description', None),
1256+ statuses_count=data.get('statuses_count', None),
1257+ followers_count=data.get('followers_count', None),
1258+ favourites_count=data.get('favourites_count', None),
1259+ friends_count=data.get('friends_count', None),
1260+ profile_image_url=data.get('profile_image_url', None),
1261+ profile_background_tile = data.get('profile_background_tile', None),
1262+ profile_background_image_url = data.get('profile_background_image_url', None),
1263+ profile_sidebar_fill_color = data.get('profile_sidebar_fill_color', None),
1264+ profile_background_color = data.get('profile_background_color', None),
1265+ profile_link_color = data.get('profile_link_color', None),
1266+ profile_text_color = data.get('profile_text_color', None),
1267+ protected = data.get('protected', None),
1268+ utc_offset = data.get('utc_offset', None),
1269+ time_zone = data.get('time_zone', None),
1270+ url=data.get('url', None),
1271+ status=status)
1272+
1273+class DirectMessage(object):
1274+ '''A class representing the DirectMessage structure used by the twitter API.
1275+
1276+ The DirectMessage structure exposes the following properties:
1277+
1278+ direct_message.id
1279+ direct_message.created_at
1280+ direct_message.created_at_in_seconds # read only
1281+ direct_message.sender_id
1282+ direct_message.sender_screen_name
1283+ direct_message.recipient_id
1284+ direct_message.recipient_screen_name
1285+ direct_message.text
1286+ '''
1287+
1288+ def __init__(self,
1289+ id=None,
1290+ created_at=None,
1291+ sender_id=None,
1292+ sender_screen_name=None,
1293+ recipient_id=None,
1294+ recipient_screen_name=None,
1295+ text=None):
1296+ '''An object to hold a Twitter direct message.
1297+
1298+ This class is normally instantiated by the twitter.Api class and
1299+ returned in a sequence.
1300+
1301+ Note: Dates are posted in the form "Sat Jan 27 04:17:38 +0000 2007"
1302+
1303+ Args:
1304+ id: The unique id of this direct message
1305+ created_at: The time this direct message was posted
1306+ sender_id: The id of the twitter user that sent this message
1307+ sender_screen_name: The name of the twitter user that sent this message
1308+ recipient_id: The id of the twitter that received this message
1309+ recipient_screen_name: The name of the twitter that received this message
1310+ text: The text of this direct message
1311+ '''
1312+ self.id = id
1313+ self.created_at = created_at
1314+ self.sender_id = sender_id
1315+ self.sender_screen_name = sender_screen_name
1316+ self.recipient_id = recipient_id
1317+ self.recipient_screen_name = recipient_screen_name
1318+ self.text = text
1319+
1320+ def GetId(self):
1321+ '''Get the unique id of this direct message.
1322+
1323+ Returns:
1324+ The unique id of this direct message
1325+ '''
1326+ return self._id
1327+
1328+ def SetId(self, id):
1329+ '''Set the unique id of this direct message.
1330+
1331+ Args:
1332+ id: The unique id of this direct message
1333+ '''
1334+ self._id = id
1335+
1336+ id = property(GetId, SetId,
1337+ doc='The unique id of this direct message.')
1338+
1339+ def GetCreatedAt(self):
1340+ '''Get the time this direct message was posted.
1341+
1342+ Returns:
1343+ The time this direct message was posted
1344+ '''
1345+ return self._created_at
1346+
1347+ def SetCreatedAt(self, created_at):
1348+ '''Set the time this direct message was posted.
1349+
1350+ Args:
1351+ created_at: The time this direct message was created
1352+ '''
1353+ self._created_at = created_at
1354+
1355+ created_at = property(GetCreatedAt, SetCreatedAt,
1356+ doc='The time this direct message was posted.')
1357+
1358+ def GetCreatedAtInSeconds(self):
1359+ '''Get the time this direct message was posted, in seconds since the epoch.
1360+
1361+ Returns:
1362+ The time this direct message was posted, in seconds since the epoch.
1363+ '''
1364+ return calendar.timegm(rfc822.parsedate(self.created_at))
1365+
1366+ created_at_in_seconds = property(GetCreatedAtInSeconds,
1367+ doc="The time this direct message was "
1368+ "posted, in seconds since the epoch")
1369+
1370+ def GetSenderId(self):
1371+ '''Get the unique sender id of this direct message.
1372+
1373+ Returns:
1374+ The unique sender id of this direct message
1375+ '''
1376+ return self._sender_id
1377+
1378+ def SetSenderId(self, sender_id):
1379+ '''Set the unique sender id of this direct message.
1380+
1381+ Args:
1382+ sender id: The unique sender id of this direct message
1383+ '''
1384+ self._sender_id = sender_id
1385+
1386+ sender_id = property(GetSenderId, SetSenderId,
1387+ doc='The unique sender id of this direct message.')
1388+
1389+ def GetSenderScreenName(self):
1390+ '''Get the unique sender screen name of this direct message.
1391+
1392+ Returns:
1393+ The unique sender screen name of this direct message
1394+ '''
1395+ return self._sender_screen_name
1396+
1397+ def SetSenderScreenName(self, sender_screen_name):
1398+ '''Set the unique sender screen name of this direct message.
1399+
1400+ Args:
1401+ sender_screen_name: The unique sender screen name of this direct message
1402+ '''
1403+ self._sender_screen_name = sender_screen_name
1404+
1405+ sender_screen_name = property(GetSenderScreenName, SetSenderScreenName,
1406+ doc='The unique sender screen name of this direct message.')
1407+
1408+ def GetRecipientId(self):
1409+ '''Get the unique recipient id of this direct message.
1410+
1411+ Returns:
1412+ The unique recipient id of this direct message
1413+ '''
1414+ return self._recipient_id
1415+
1416+ def SetRecipientId(self, recipient_id):
1417+ '''Set the unique recipient id of this direct message.
1418+
1419+ Args:
1420+ recipient id: The unique recipient id of this direct message
1421+ '''
1422+ self._recipient_id = recipient_id
1423+
1424+ recipient_id = property(GetRecipientId, SetRecipientId,
1425+ doc='The unique recipient id of this direct message.')
1426+
1427+ def GetRecipientScreenName(self):
1428+ '''Get the unique recipient screen name of this direct message.
1429+
1430+ Returns:
1431+ The unique recipient screen name of this direct message
1432+ '''
1433+ return self._recipient_screen_name
1434+
1435+ def SetRecipientScreenName(self, recipient_screen_name):
1436+ '''Set the unique recipient screen name of this direct message.
1437+
1438+ Args:
1439+ recipient_screen_name: The unique recipient screen name of this direct message
1440+ '''
1441+ self._recipient_screen_name = recipient_screen_name
1442+
1443+ recipient_screen_name = property(GetRecipientScreenName, SetRecipientScreenName,
1444+ doc='The unique recipient screen name of this direct message.')
1445+
1446+ def GetText(self):
1447+ '''Get the text of this direct message.
1448+
1449+ Returns:
1450+ The text of this direct message.
1451+ '''
1452+ return self._text
1453+
1454+ def SetText(self, text):
1455+ '''Set the text of this direct message.
1456+
1457+ Args:
1458+ text: The text of this direct message
1459+ '''
1460+ self._text = text
1461+
1462+ text = property(GetText, SetText,
1463+ doc='The text of this direct message')
1464+
1465+ def __ne__(self, other):
1466+ return not self.__eq__(other)
1467+
1468+ def __eq__(self, other):
1469+ try:
1470+ return other and \
1471+ self.id == other.id and \
1472+ self.created_at == other.created_at and \
1473+ self.sender_id == other.sender_id and \
1474+ self.sender_screen_name == other.sender_screen_name and \
1475+ self.recipient_id == other.recipient_id and \
1476+ self.recipient_screen_name == other.recipient_screen_name and \
1477+ self.text == other.text
1478+ except AttributeError:
1479+ return False
1480+
1481+ def __str__(self):
1482+ '''A string representation of this twitter.DirectMessage instance.
1483+
1484+ The return value is the same as the JSON string representation.
1485+
1486+ Returns:
1487+ A string representation of this twitter.DirectMessage instance.
1488+ '''
1489+ return self.AsJsonString()
1490+
1491+ def AsJsonString(self):
1492+ '''A JSON string representation of this twitter.DirectMessage instance.
1493+
1494+ Returns:
1495+ A JSON string representation of this twitter.DirectMessage instance
1496+ '''
1497+ return simplejson.dumps(self.AsDict(), sort_keys=True)
1498+
1499+ def AsDict(self):
1500+ '''A dict representation of this twitter.DirectMessage instance.
1501+
1502+ The return value uses the same key names as the JSON representation.
1503+
1504+ Return:
1505+ A dict representing this twitter.DirectMessage instance
1506+ '''
1507+ data = {}
1508+ if self.id:
1509+ data['id'] = self.id
1510+ if self.created_at:
1511+ data['created_at'] = self.created_at
1512+ if self.sender_id:
1513+ data['sender_id'] = self.sender_id
1514+ if self.sender_screen_name:
1515+ data['sender_screen_name'] = self.sender_screen_name
1516+ if self.recipient_id:
1517+ data['recipient_id'] = self.recipient_id
1518+ if self.recipient_screen_name:
1519+ data['recipient_screen_name'] = self.recipient_screen_name
1520+ if self.text:
1521+ data['text'] = self.text
1522+ return data
1523+
1524+ @staticmethod
1525+ def NewFromJsonDict(data):
1526+ '''Create a new instance based on a JSON dict.
1527+
1528+ Args:
1529+ data: A JSON dict, as converted from the JSON in the twitter API
1530+ Returns:
1531+ A twitter.DirectMessage instance
1532+ '''
1533+ return DirectMessage(created_at=data.get('created_at', None),
1534+ recipient_id=data.get('recipient_id', None),
1535+ sender_id=data.get('sender_id', None),
1536+ text=data.get('text', None),
1537+ sender_screen_name=data.get('sender_screen_name', None),
1538+ id=data.get('id', None),
1539+ recipient_screen_name=data.get('recipient_screen_name', None))
1540+
1541+class Api(object):
1542+ '''A python interface into the Twitter API
1543+
1544+ By default, the Api caches results for 1 minute.
1545+
1546+ Example usage:
1547+
1548+ To create an instance of the twitter.Api class, with no authentication:
1549+
1550+ >>> import twitter
1551+ >>> api = twitter.Api()
1552+
1553+ To fetch the most recently posted public twitter status messages:
1554+
1555+ >>> statuses = api.GetPublicTimeline()
1556+ >>> print [s.user.name for s in statuses]
1557+ [u'DeWitt', u'Kesuke Miyagi', u'ev', u'Buzz Andersen', u'Biz Stone'] #...
1558+
1559+ To fetch a single user's public status messages, where "user" is either
1560+ a Twitter "short name" or their user id.
1561+
1562+ >>> statuses = api.GetUserTimeline(user)
1563+ >>> print [s.text for s in statuses]
1564+
1565+ To use authentication, instantiate the twitter.Api class with a
1566+ username and password:
1567+
1568+ >>> api = twitter.Api(username='twitter user', password='twitter pass')
1569+
1570+ To fetch your friends (after being authenticated):
1571+
1572+ >>> users = api.GetFriends()
1573+ >>> print [u.name for u in users]
1574+
1575+ To post a twitter status message (after being authenticated):
1576+
1577+ >>> status = api.PostUpdate('I love python-twitter!')
1578+ >>> print status.text
1579+ I love python-twitter!
1580+
1581+ There are many other methods, including:
1582+
1583+ >>> api.PostUpdates(status)
1584+ >>> api.PostDirectMessage(user, text)
1585+ >>> api.GetUser(user)
1586+ >>> api.GetReplies()
1587+ >>> api.GetUserTimeline(user)
1588+ >>> api.GetStatus(id)
1589+ >>> api.DestroyStatus(id)
1590+ >>> api.GetFriendsTimeline(user)
1591+ >>> api.GetFriends(user)
1592+ >>> api.GetFollowers()
1593+ >>> api.GetFeatured()
1594+ >>> api.GetDirectMessages()
1595+ >>> api.PostDirectMessage(user, text)
1596+ >>> api.DestroyDirectMessage(id)
1597+ >>> api.DestroyFriendship(user)
1598+ >>> api.CreateFriendship(user)
1599+ >>> api.GetUserByEmail(email)
1600+ >>> api.VerifyCredentials()
1601+ '''
1602+
1603+ DEFAULT_CACHE_TIMEOUT = 60 # cache for 1 minute
1604+
1605+ _API_REALM = 'Twitter API'
1606+
1607+ def __init__(self,
1608+ username=None,
1609+ password=None,
1610+ input_encoding=None,
1611+ request_headers=None,
1612+ cache=DEFAULT_CACHE,
1613+ shortner=None,
1614+ base_url=None,
1615+ use_gzip_compression=False):
1616+ '''Instantiate a new twitter.Api object.
1617+
1618+ Args:
1619+ username:
1620+ The username of the twitter account. [optional]
1621+ password:
1622+ The password for the twitter account. [optional]
1623+ input_encoding:
1624+ The encoding used to encode input strings. [optional]
1625+ request_header:
1626+ A dictionary of additional HTTP request headers. [optional]
1627+ cache:
1628+ The cache instance to use. Defaults to DEFAULT_CACHE.
1629+ Use None to disable caching. [optional]
1630+ shortner:
1631+ The shortner instance to use. Defaults to None.
1632+ See shorten_url.py for an example shortner. [optional]
1633+ base_url:
1634+ The base URL to use to contact the Twitter API.
1635+ Defaults to https://twitter.com. [optional]
1636+ use_gzip_compression:
1637+ Set to True to tell enable gzip compression for any call
1638+ made to Twitter. Defaults to False. [optional]
1639+ '''
1640+ self.SetCache(cache)
1641+ self._urllib = urllib2
1642+ self._cache_timeout = Api.DEFAULT_CACHE_TIMEOUT
1643+ self._InitializeRequestHeaders(request_headers)
1644+ self._InitializeUserAgent()
1645+ self._InitializeDefaultParameters()
1646+ self._input_encoding = input_encoding
1647+ self._use_gzip = use_gzip_compression
1648+ self.SetCredentials(username, password)
1649+ if base_url is None:
1650+ self.base_url = 'https://twitter.com'
1651+ else:
1652+ self.base_url = base_url
1653+
1654+ def GetPublicTimeline(self,
1655+ since_id=None):
1656+ '''Fetch the sequnce of public twitter.Status message for all users.
1657+
1658+ Args:
1659+ since_id:
1660+ Returns only public statuses with an ID greater than
1661+ (that is, more recent than) the specified ID. [optional]
1662+
1663+ Returns:
1664+ An sequence of twitter.Status instances, one for each message
1665+ '''
1666+ parameters = {}
1667+
1668+ if since_id:
1669+ parameters['since_id'] = since_id
1670+
1671+ url = '%s/statuses/public_timeline.json' % self.base_url
1672+ json = self._FetchUrl(url, parameters=parameters)
1673+ data = simplejson.loads(json)
1674+
1675+ self._CheckForTwitterError(data)
1676+
1677+ return [Status.NewFromJsonDict(x) for x in data]
1678+
1679+ def FilterPublicTimeline(self,
1680+ term,
1681+ since_id=None):
1682+ '''Filter the public twitter timeline by a given search term on
1683+ the local machine.
1684+
1685+ Args:
1686+ term:
1687+ term to search by.
1688+ since_id:
1689+ Returns only public statuses with an ID greater than
1690+ (that is, more recent than) the specified ID. [optional]
1691+
1692+ Returns:
1693+ A sequence of twitter.Status instances, one for each message
1694+ containing the term
1695+ '''
1696+ statuses = self.GetPublicTimeline(since_id)
1697+ results = []
1698+
1699+ for s in statuses:
1700+ if s.text.lower().find(term.lower()) != -1:
1701+ results.append(s)
1702+
1703+ return results
1704+
1705+ def GetSearch(self,
1706+ term,
1707+ geocode=None,
1708+ since_id=None,
1709+ per_page=15,
1710+ page=1,
1711+ lang="en",
1712+ show_user="true",
1713+ query_users=False):
1714+ '''Return twitter search results for a given term.
1715+
1716+ Args:
1717+ term:
1718+ term to search by.
1719+ since_id:
1720+ Returns only public statuses with an ID greater than
1721+ (that is, more recent than) the specified ID. [optional]
1722+ geocode:
1723+ geolocation information in the form (latitude, longitude, radius)
1724+ [optional]
1725+ per_page:
1726+ number of results to return. Default is 15 [optional]
1727+ page:
1728+ which page of search results to return
1729+ lang:
1730+ language for results. Default is English [optional]
1731+ show_user:
1732+ prefixes screen name in status
1733+ query_users:
1734+ If set to False, then all users only have screen_name and
1735+ profile_image_url available.
1736+ If set to True, all information of users are available,
1737+ but it uses lots of request quota, one per status.
1738+ Returns:
1739+ A sequence of twitter.Status instances, one for each message containing
1740+ the term
1741+ '''
1742+ # Build request parameters
1743+ parameters = {}
1744+
1745+ if since_id:
1746+ parameters['since_id'] = since_id
1747+
1748+ if not term:
1749+ return []
1750+
1751+ parameters['q'] = urllib.quote_plus(term)
1752+ parameters['show_user'] = show_user
1753+ parameters['lang'] = lang
1754+ parameters['rpp'] = per_page
1755+ parameters['page'] = page
1756+
1757+ if geocode is not None:
1758+ parameters['geocode'] = ','.join(map(str, geocode))
1759+
1760+ # Make and send requests
1761+ url = 'http://search.twitter.com/search.json'
1762+ json = self._FetchUrl(url, parameters=parameters)
1763+ data = simplejson.loads(json)
1764+
1765+ self._CheckForTwitterError(data)
1766+
1767+ results = []
1768+
1769+ for x in data['results']:
1770+ temp = Status.NewFromJsonDict(x)
1771+
1772+ if query_users:
1773+ # Build user object with new request
1774+ temp.user = self.GetUser(urllib.quote(x['from_user']))
1775+ else:
1776+ temp.user = User(screen_name=x['from_user'], profile_image_url=x['profile_image_url'])
1777+
1778+ results.append(temp)
1779+
1780+ # Return built list of statuses
1781+ return results # [Status.NewFromJsonDict(x) for x in data['results']]
1782+
1783+ def GetFriendsTimeline(self,
1784+ user=None,
1785+ count=None,
1786+ since=None,
1787+ since_id=None):
1788+ '''Fetch the sequence of twitter.Status messages for a user's friends
1789+
1790+ The twitter.Api instance must be authenticated if the user is private.
1791+
1792+ Args:
1793+ user:
1794+ Specifies the ID or screen name of the user for whom to return
1795+ the friends_timeline. If unspecified, the username and password
1796+ must be set in the twitter.Api instance. [Optional]
1797+ count:
1798+ Specifies the number of statuses to retrieve. May not be
1799+ greater than 200. [Optional]
1800+ since:
1801+ Narrows the returned results to just those statuses created
1802+ after the specified HTTP-formatted date. [Optional]
1803+ since_id:
1804+ Returns only public statuses with an ID greater than (that is,
1805+ more recent than) the specified ID. [Optional]
1806+
1807+ Returns:
1808+ A sequence of twitter.Status instances, one for each message
1809+ '''
1810+ if not user and not self._username:
1811+ raise TwitterError("User must be specified if API is not authenticated.")
1812+ if user:
1813+ url = '%s/statuses/friends_timeline/%s.json' % (self.base_url, user)
1814+ else:
1815+ url = '%s/statuses/friends_timeline.json' % self.base_url
1816+ parameters = {}
1817+ if count is not None:
1818+ try:
1819+ if int(count) > 200:
1820+ raise TwitterError("'count' may not be greater than 200")
1821+ except ValueError:
1822+ raise TwitterError("'count' must be an integer")
1823+ parameters['count'] = count
1824+ if since:
1825+ parameters['since'] = since
1826+ if since_id:
1827+ parameters['since_id'] = since_id
1828+ json = self._FetchUrl(url, parameters=parameters)
1829+ data = simplejson.loads(json)
1830+ self._CheckForTwitterError(data)
1831+ return [Status.NewFromJsonDict(x) for x in data]
1832+
1833+ def GetUserTimeline(self,
1834+ id=None,
1835+ user_id=None,
1836+ screen_name=None,
1837+ since_id=None,
1838+ max_id=None,
1839+ count=None,
1840+ page=None):
1841+ '''Fetch the sequence of public Status messages for a single user.
1842+
1843+ The twitter.Api instance must be authenticated if the user is private.
1844+
1845+ Args:
1846+ id:
1847+ Specifies the ID or screen name of the user for whom to return
1848+ the user_timeline. [optional]
1849+ user_id:
1850+ Specfies the ID of the user for whom to return the
1851+ user_timeline. Helpful for disambiguating when a valid user ID
1852+ is also a valid screen name. [optional]
1853+ screen_name:
1854+ Specfies the screen name of the user for whom to return the
1855+ user_timeline. Helpful for disambiguating when a valid screen
1856+ name is also a user ID. [optional]
1857+ since_id:
1858+ Returns only public statuses with an ID greater than (that is,
1859+ more recent than) the specified ID. [optional]
1860+ max_id:
1861+ Returns only statuses with an ID less than (that is, older
1862+ than) or equal to the specified ID. [optional]
1863+ count:
1864+ Specifies the number of statuses to retrieve. May not be
1865+ greater than 200. [optional]
1866+ page:
1867+ Specifies the page of results to retrieve. Note: there are
1868+ pagination limits. [optional]
1869+
1870+ Returns:
1871+ A sequence of Status instances, one for each message up to count
1872+ '''
1873+ parameters = {}
1874+
1875+ if id:
1876+ url = '%s/statuses/user_timeline/%s.json' % (self.base_url, id)
1877+ elif user_id:
1878+ url = '%s/statuses/user_timeline.json?user_id=%d' % (self.base_url, user_id)
1879+ elif screen_name:
1880+ url = ('%s/statuses/user_timeline.json?screen_name=%s' % (self.base_url,
1881+ screen_name))
1882+ elif not self._username:
1883+ raise TwitterError("User must be specified if API is not authenticated.")
1884+ else:
1885+ url = '%s/statuses/user_timeline.json' % self.base_url
1886+
1887+ if since_id:
1888+ try:
1889+ parameters['since_id'] = long(since_id)
1890+ except:
1891+ raise TwitterError("since_id must be an integer")
1892+
1893+ if max_id:
1894+ try:
1895+ parameters['max_id'] = long(max_id)
1896+ except:
1897+ raise TwitterError("max_id must be an integer")
1898+
1899+ if count:
1900+ try:
1901+ parameters['count'] = int(count)
1902+ except:
1903+ raise TwitterError("count must be an integer")
1904+
1905+ if page:
1906+ try:
1907+ parameters['page'] = int(page)
1908+ except:
1909+ raise TwitterError("page must be an integer")
1910+
1911+ json = self._FetchUrl(url, parameters=parameters)
1912+ data = simplejson.loads(json)
1913+ self._CheckForTwitterError(data)
1914+ return [Status.NewFromJsonDict(x) for x in data]
1915+
1916+ def GetStatus(self, id):
1917+ '''Returns a single status message.
1918+
1919+ The twitter.Api instance must be authenticated if the status message is private.
1920+
1921+ Args:
1922+ id: The numerical ID of the status you're trying to retrieve.
1923+
1924+ Returns:
1925+ A twitter.Status instance representing that status message
1926+ '''
1927+ try:
1928+ if id:
1929+ long(id)
1930+ except:
1931+ raise TwitterError("id must be an long integer")
1932+ url = '%s/statuses/show/%s.json' % (self.base_url, id)
1933+ json = self._FetchUrl(url)
1934+ data = simplejson.loads(json)
1935+ self._CheckForTwitterError(data)
1936+ return Status.NewFromJsonDict(data)
1937+
1938+ def DestroyStatus(self, id):
1939+ '''Destroys the status specified by the required ID parameter.
1940+
1941+ The twitter.Api instance must be authenticated and thee
1942+ authenticating user must be the author of the specified status.
1943+
1944+ Args:
1945+ id: The numerical ID of the status you're trying to destroy.
1946+
1947+ Returns:
1948+ A twitter.Status instance representing the destroyed status message
1949+ '''
1950+ try:
1951+ if id:
1952+ long(id)
1953+ except:
1954+ raise TwitterError("id must be an integer")
1955+ url = '%s/statuses/destroy/%s.json' % (self.base_url, id)
1956+ json = self._FetchUrl(url, post_data={})
1957+ data = simplejson.loads(json)
1958+ self._CheckForTwitterError(data)
1959+ return Status.NewFromJsonDict(data)
1960+
1961+ def PostUpdate(self, status, in_reply_to_status_id=None):
1962+ '''Post a twitter status message from the authenticated user.
1963+
1964+ The twitter.Api instance must be authenticated.
1965+
1966+ Args:
1967+ status:
1968+ The message text to be posted. Must be less than or equal to
1969+ 140 characters.
1970+ in_reply_to_status_id:
1971+ The ID of an existing status that the status to be posted is
1972+ in reply to. This implicitly sets the in_reply_to_user_id
1973+ attribute of the resulting status to the user ID of the
1974+ message being replied to. Invalid/missing status IDs will be
1975+ ignored. [Optional]
1976+ Returns:
1977+ A twitter.Status instance representing the message posted.
1978+ '''
1979+ if not self._username:
1980+ raise TwitterError("The twitter.Api instance must be authenticated.")
1981+
1982+ url = '%s/statuses/update.json' % self.base_url
1983+
1984+ if len(status) > CHARACTER_LIMIT:
1985+ raise TwitterError("Text must be less than or equal to %d characters. "
1986+ "Consider using PostUpdates." % CHARACTER_LIMIT)
1987+
1988+ data = {'status': status}
1989+ if in_reply_to_status_id:
1990+ data['in_reply_to_status_id'] = in_reply_to_status_id
1991+ json = self._FetchUrl(url, post_data=data)
1992+ data = simplejson.loads(json)
1993+ self._CheckForTwitterError(data)
1994+ return Status.NewFromJsonDict(data)
1995+
1996+ def PostUpdates(self, status, continuation=None, **kwargs):
1997+ '''Post one or more twitter status messages from the authenticated user.
1998+
1999+ Unlike api.PostUpdate, this method will post multiple status updates
2000+ if the message is longer than 140 characters.
2001+
2002+ The twitter.Api instance must be authenticated.
2003+
2004+ Args:
2005+ status:
2006+ The message text to be posted. May be longer than 140 characters.
2007+ continuation:
2008+ The character string, if any, to be appended to all but the
2009+ last message. Note that Twitter strips trailing '...' strings
2010+ from messages. Consider using the unicode \u2026 character
2011+ (horizontal ellipsis) instead. [Defaults to None]
2012+ **kwargs:
2013+ See api.PostUpdate for a list of accepted parameters.
2014+ Returns:
2015+ A of list twitter.Status instance representing the messages posted.
2016+ '''
2017+ results = list()
2018+ if continuation is None:
2019+ continuation = ''
2020+ line_length = CHARACTER_LIMIT - len(continuation)
2021+ lines = textwrap.wrap(status, line_length)
2022+ for line in lines[0:-1]:
2023+ results.append(self.PostUpdate(line + continuation, **kwargs))
2024+ results.append(self.PostUpdate(lines[-1], **kwargs))
2025+ return results
2026+
2027+ def GetReplies(self, since=None, since_id=None, page=None):
2028+ '''Get a sequence of status messages representing the 20 most recent
2029+ replies (status updates prefixed with @username) to the authenticating
2030+ user.
2031+
2032+ Args:
2033+ page:
2034+ since:
2035+ Narrows the returned results to just those statuses created
2036+ after the specified HTTP-formatted date. [optional]
2037+ since_id:
2038+ Returns only public statuses with an ID greater than (that is,
2039+ more recent than) the specified ID. [Optional]
2040+
2041+ Returns:
2042+ A sequence of twitter.Status instances, one for each reply to the user.
2043+ '''
2044+ url = '%s/statuses/replies.json' % self.base_url
2045+ if not self._username:
2046+ raise TwitterError("The twitter.Api instance must be authenticated.")
2047+ parameters = {}
2048+ if since:
2049+ parameters['since'] = since
2050+ if since_id:
2051+ parameters['since_id'] = since_id
2052+ if page:
2053+ parameters['page'] = page
2054+ json = self._FetchUrl(url, parameters=parameters)
2055+ data = simplejson.loads(json)
2056+ self._CheckForTwitterError(data)
2057+ return [Status.NewFromJsonDict(x) for x in data]
2058+
2059+ def GetFriends(self, user=None, page=None):
2060+ '''Fetch the sequence of twitter.User instances, one for each friend.
2061+
2062+ Args:
2063+ user: the username or id of the user whose friends you are fetching. If
2064+ not specified, defaults to the authenticated user. [optional]
2065+
2066+ The twitter.Api instance must be authenticated.
2067+
2068+ Returns:
2069+ A sequence of twitter.User instances, one for each friend
2070+ '''
2071+ if not user and not self._username:
2072+ raise TwitterError("twitter.Api instance must be authenticated")
2073+ if user:
2074+ url = '%s/statuses/friends/%s.json' % (self.base_url, user)
2075+ else:
2076+ url = '%s/statuses/friends.json' % self.base_url
2077+ parameters = {}
2078+ if page:
2079+ parameters['page'] = page
2080+ json = self._FetchUrl(url, parameters=parameters)
2081+ data = simplejson.loads(json)
2082+ self._CheckForTwitterError(data)
2083+ return [User.NewFromJsonDict(x) for x in data]
2084+
2085+ def GetFriendIDs(self, user=None, page=None):
2086+ '''Returns a list of twitter user id's for every person
2087+ the specified user is following.
2088+
2089+ Args:
2090+ user:
2091+ The id or screen_name of the user to retrieve the id list for
2092+ [optional]
2093+ page:
2094+ Specifies the page number of the results beginning at 1.
2095+ A single page contains 5000 ids. This is recommended for users
2096+ with large id lists. If not provided all id's are returned.
2097+ (Please note that the result set isn't guaranteed to be 5000
2098+ every time as suspended users will be filtered.)
2099+ [optional]
2100+
2101+ Returns:
2102+ A list of integers, one for each user id.
2103+ '''
2104+ if not user and not self._username:
2105+ raise TwitterError("twitter.Api instance must be authenticated")
2106+ if user:
2107+ url = '%s/friends/ids/%s.json' % (self.base_url, user)
2108+ else:
2109+ url = '%s/friends/ids.json' % self.base_url
2110+ parameters = {}
2111+ if page:
2112+ parameters['page'] = page
2113+ json = self._FetchUrl(url, parameters=parameters)
2114+ data = simplejson.loads(json)
2115+ self._CheckForTwitterError(data)
2116+ return data
2117+
2118+ def GetFollowers(self, page=None):
2119+ '''Fetch the sequence of twitter.User instances, one for each follower
2120+
2121+ The twitter.Api instance must be authenticated.
2122+
2123+ Returns:
2124+ A sequence of twitter.User instances, one for each follower
2125+ '''
2126+ if not self._username:
2127+ raise TwitterError("twitter.Api instance must be authenticated")
2128+ url = '%s/statuses/followers.json' % self.base_url
2129+ parameters = {}
2130+ if page:
2131+ parameters['page'] = page
2132+ json = self._FetchUrl(url, parameters=parameters)
2133+ data = simplejson.loads(json)
2134+ self._CheckForTwitterError(data)
2135+ return [User.NewFromJsonDict(x) for x in data]
2136+
2137+ def GetFeatured(self):
2138+ '''Fetch the sequence of twitter.User instances featured on twitter.com
2139+
2140+ The twitter.Api instance must be authenticated.
2141+
2142+ Returns:
2143+ A sequence of twitter.User instances
2144+ '''
2145+ url = '%s/statuses/featured.json' % self.base_url
2146+ json = self._FetchUrl(url)
2147+ data = simplejson.loads(json)
2148+ self._CheckForTwitterError(data)
2149+ return [User.NewFromJsonDict(x) for x in data]
2150+
2151+ def GetUser(self, user):
2152+ '''Returns a single user.
2153+
2154+ The twitter.Api instance must be authenticated.
2155+
2156+ Args:
2157+ user: The username or id of the user to retrieve.
2158+
2159+ Returns:
2160+ A twitter.User instance representing that user
2161+ '''
2162+ url = '%s/users/show/%s.json' % (self.base_url, user)
2163+ json = self._FetchUrl(url)
2164+ data = simplejson.loads(json)
2165+ self._CheckForTwitterError(data)
2166+ return User.NewFromJsonDict(data)
2167+
2168+ def GetDirectMessages(self, since=None, since_id=None, page=None):
2169+ '''Returns a list of the direct messages sent to the authenticating user.
2170+
2171+ The twitter.Api instance must be authenticated.
2172+
2173+ Args:
2174+ since:
2175+ Narrows the returned results to just those statuses created
2176+ after the specified HTTP-formatted date. [optional]
2177+ since_id:
2178+ Returns only public statuses with an ID greater than (that is,
2179+ more recent than) the specified ID. [Optional]
2180+
2181+ Returns:
2182+ A sequence of twitter.DirectMessage instances
2183+ '''
2184+ url = '%s/direct_messages.json' % self.base_url
2185+ if not self._username:
2186+ raise TwitterError("The twitter.Api instance must be authenticated.")
2187+ parameters = {}
2188+ if since:
2189+ parameters['since'] = since
2190+ if since_id:
2191+ parameters['since_id'] = since_id
2192+ if page:
2193+ parameters['page'] = page
2194+ json = self._FetchUrl(url, parameters=parameters)
2195+ data = simplejson.loads(json)
2196+ self._CheckForTwitterError(data)
2197+ return [DirectMessage.NewFromJsonDict(x) for x in data]
2198+
2199+ def PostDirectMessage(self, user, text):
2200+ '''Post a twitter direct message from the authenticated user
2201+
2202+ The twitter.Api instance must be authenticated.
2203+
2204+ Args:
2205+ user: The ID or screen name of the recipient user.
2206+ text: The message text to be posted. Must be less than 140 characters.
2207+
2208+ Returns:
2209+ A twitter.DirectMessage instance representing the message posted
2210+ '''
2211+ if not self._username:
2212+ raise TwitterError("The twitter.Api instance must be authenticated.")
2213+ url = '%s/direct_messages/new.json' % self.base_url
2214+ data = {'text': text, 'user': user}
2215+ json = self._FetchUrl(url, post_data=data)
2216+ data = simplejson.loads(json)
2217+ self._CheckForTwitterError(data)
2218+ return DirectMessage.NewFromJsonDict(data)
2219+
2220+ def DestroyDirectMessage(self, id):
2221+ '''Destroys the direct message specified in the required ID parameter.
2222+
2223+ The twitter.Api instance must be authenticated, and the
2224+ authenticating user must be the recipient of the specified direct
2225+ message.
2226+
2227+ Args:
2228+ id: The id of the direct message to be destroyed
2229+
2230+ Returns:
2231+ A twitter.DirectMessage instance representing the message destroyed
2232+ '''
2233+ url = '%s/direct_messages/destroy/%s.json' % (self.base_url, id)
2234+ json = self._FetchUrl(url, post_data={})
2235+ data = simplejson.loads(json)
2236+ self._CheckForTwitterError(data)
2237+ return DirectMessage.NewFromJsonDict(data)
2238+
2239+ def CreateFriendship(self, user):
2240+ '''Befriends the user specified in the user parameter as the authenticating user.
2241+
2242+ The twitter.Api instance must be authenticated.
2243+
2244+ Args:
2245+ The ID or screen name of the user to befriend.
2246+ Returns:
2247+ A twitter.User instance representing the befriended user.
2248+ '''
2249+ url = '%s/friendships/create/%s.json' % (self.base_url, user)
2250+ json = self._FetchUrl(url, post_data={})
2251+ data = simplejson.loads(json)
2252+ self._CheckForTwitterError(data)
2253+ return User.NewFromJsonDict(data)
2254+
2255+ def DestroyFriendship(self, user):
2256+ '''Discontinues friendship with the user specified in the user parameter.
2257+
2258+ The twitter.Api instance must be authenticated.
2259+
2260+ Args:
2261+ The ID or screen name of the user with whom to discontinue friendship.
2262+ Returns:
2263+ A twitter.User instance representing the discontinued friend.
2264+ '''
2265+ url = '%s/friendships/destroy/%s.json' % (self.base_url, user)
2266+ json = self._FetchUrl(url, post_data={})
2267+ data = simplejson.loads(json)
2268+ self._CheckForTwitterError(data)
2269+ return User.NewFromJsonDict(data)
2270+
2271+ def CreateFavorite(self, status):
2272+ '''Favorites the status specified in the status parameter as the authenticating user.
2273+ Returns the favorite status when successful.
2274+
2275+ The twitter.Api instance must be authenticated.
2276+
2277+ Args:
2278+ The twitter.Status instance to mark as a favorite.
2279+ Returns:
2280+ A twitter.Status instance representing the newly-marked favorite.
2281+ '''
2282+ url = '%s/favorites/create/%s.json' % (self.base_url, status.id)
2283+ json = self._FetchUrl(url, post_data={})
2284+ data = simplejson.loads(json)
2285+ self._CheckForTwitterError(data)
2286+ return Status.NewFromJsonDict(data)
2287+
2288+ def DestroyFavorite(self, status):
2289+ '''Un-favorites the status specified in the ID parameter as the authenticating user.
2290+ Returns the un-favorited status in the requested format when successful.
2291+
2292+ The twitter.Api instance must be authenticated.
2293+
2294+ Args:
2295+ The twitter.Status to unmark as a favorite.
2296+ Returns:
2297+ A twitter.Status instance representing the newly-unmarked favorite.
2298+ '''
2299+ url = '%s/favorites/destroy/%s.json' % (self.base_url, status.id)
2300+ json = self._FetchUrl(url, post_data={})
2301+ data = simplejson.loads(json)
2302+ self._CheckForTwitterError(data)
2303+ return Status.NewFromJsonDict(data)
2304+
2305+ def GetFavorites(self,
2306+ user=None,
2307+ page=None):
2308+ '''Return a list of Status objects representing favorited tweets.
2309+ By default, returns the (up to) 20 most recent tweets for the
2310+ authenticated user.
2311+
2312+ Args:
2313+ user:
2314+ The username or id of the user whose favorites you are fetching.
2315+ If not specified, defaults to the authenticated user. [optional]
2316+
2317+ page:
2318+ Retrieves the 20 next most recent favorite statuses. [optional]
2319+ '''
2320+ parameters = {}
2321+
2322+ if page:
2323+ parameters['page'] = page
2324+
2325+ if user:
2326+ url = '%s/favorites/%s.json' % (self.base_url, user)
2327+ elif not user and not self._username:
2328+ raise TwitterError("User must be specified if API is not authenticated.")
2329+ else:
2330+ url = '%s/favorites.json' % self.base_url
2331+
2332+ json = self._FetchUrl(url, parameters=parameters)
2333+ data = simplejson.loads(json)
2334+
2335+ self._CheckForTwitterError(data)
2336+
2337+ return [Status.NewFromJsonDict(x) for x in data]
2338+
2339+ def GetMentions(self,
2340+ since_id=None,
2341+ max_id=None,
2342+ page=None):
2343+ '''Returns the 20 most recent mentions (status containing @username)
2344+ for the authenticating user.
2345+
2346+ Args:
2347+ since_id:
2348+ Returns only public statuses with an ID greater than
2349+ (that is, more recent than) the specified ID. [optional]
2350+
2351+ max_id:
2352+ Returns only statuses with an ID less than
2353+ (that is, older than) the specified ID. [optional]
2354+
2355+ page:
2356+ Retrieves the 20 next most recent replies. [optional]
2357+
2358+ Returns:
2359+ A sequence of twitter.Status instances, one for each mention of the user.
2360+ see: http://apiwiki.twitter.com/REST-API-Documentation#statuses/mentions
2361+ '''
2362+
2363+ url = '%s/statuses/mentions.json' % self.base_url
2364+
2365+ if not self._username:
2366+ raise TwitterError("The twitter.Api instance must be authenticated.")
2367+
2368+ parameters = {}
2369+
2370+ if since_id:
2371+ parameters['since_id'] = since_id
2372+ if max_id:
2373+ parameters['max_id'] = max_id
2374+ if page:
2375+ parameters['page'] = page
2376+
2377+ json = self._FetchUrl(url, parameters=parameters)
2378+ data = simplejson.loads(json)
2379+
2380+ self._CheckForTwitterError(data)
2381+
2382+ return [Status.NewFromJsonDict(x) for x in data]
2383+
2384+ def GetUserByEmail(self, email):
2385+ '''Returns a single user by email address.
2386+
2387+ Args:
2388+ email: The email of the user to retrieve.
2389+ Returns:
2390+ A twitter.User instance representing that user
2391+ '''
2392+ url = '%s/users/show.json?email=%s' % (self.base_url, email)
2393+ json = self._FetchUrl(url)
2394+ data = simplejson.loads(json)
2395+ self._CheckForTwitterError(data)
2396+ return User.NewFromJsonDict(data)
2397+
2398+ def VerifyCredentials(self):
2399+ '''Returns a twitter.User instance if the authenticating user is valid.
2400+
2401+ Returns:
2402+ A twitter.User instance representing that user if the
2403+ credentials are valid, None otherwise.
2404+ '''
2405+ if not self._username:
2406+ raise TwitterError("Api instance must first be given user credentials.")
2407+ url = '%s/account/verify_credentials.json' % self.base_url
2408+ try:
2409+ json = self._FetchUrl(url, no_cache=True)
2410+ except urllib2.HTTPError, http_error:
2411+ if http_error.code == httplib.UNAUTHORIZED:
2412+ return None
2413+ else:
2414+ raise http_error
2415+ data = simplejson.loads(json)
2416+ self._CheckForTwitterError(data)
2417+ return User.NewFromJsonDict(data)
2418+
2419+ def SetCredentials(self, username, password):
2420+ '''Set the username and password for this instance
2421+
2422+ Args:
2423+ username: The twitter username.
2424+ password: The twitter password.
2425+ '''
2426+ self._username = username
2427+ self._password = password
2428+
2429+ def ClearCredentials(self):
2430+ '''Clear the username and password for this instance
2431+ '''
2432+ self._username = None
2433+ self._password = None
2434+
2435+ def SetCache(self, cache):
2436+ '''Override the default cache. Set to None to prevent caching.
2437+
2438+ Args:
2439+ cache: an instance that supports the same API as the twitter._FileCache
2440+ '''
2441+ if cache == DEFAULT_CACHE:
2442+ self._cache = _FileCache()
2443+ else:
2444+ self._cache = cache
2445+
2446+ def SetUrllib(self, urllib):
2447+ '''Override the default urllib implementation.
2448+
2449+ Args:
2450+ urllib: an instance that supports the same API as the urllib2 module
2451+ '''
2452+ self._urllib = urllib
2453+
2454+ def SetCacheTimeout(self, cache_timeout):
2455+ '''Override the default cache timeout.
2456+
2457+ Args:
2458+ cache_timeout: time, in seconds, that responses should be reused.
2459+ '''
2460+ self._cache_timeout = cache_timeout
2461+
2462+ def SetUserAgent(self, user_agent):
2463+ '''Override the default user agent
2464+
2465+ Args:
2466+ user_agent: a string that should be send to the server as the User-agent
2467+ '''
2468+ self._request_headers['User-Agent'] = user_agent
2469+
2470+ def SetXTwitterHeaders(self, client, url, version):
2471+ '''Set the X-Twitter HTTP headers that will be sent to the server.
2472+
2473+ Args:
2474+ client:
2475+ The client name as a string. Will be sent to the server as
2476+ the 'X-Twitter-Client' header.
2477+ url:
2478+ The URL of the meta.xml as a string. Will be sent to the server
2479+ as the 'X-Twitter-Client-URL' header.
2480+ version:
2481+ The client version as a string. Will be sent to the server
2482+ as the 'X-Twitter-Client-Version' header.
2483+ '''
2484+ self._request_headers['X-Twitter-Client'] = client
2485+ self._request_headers['X-Twitter-Client-URL'] = url
2486+ self._request_headers['X-Twitter-Client-Version'] = version
2487+
2488+ def SetSource(self, source):
2489+ '''Suggest the "from source" value to be displayed on the Twitter web site.
2490+
2491+ The value of the 'source' parameter must be first recognized by
2492+ the Twitter server. New source values are authorized on a case by
2493+ case basis by the Twitter development team.
2494+
2495+ Args:
2496+ source:
2497+ The source name as a string. Will be sent to the server as
2498+ the 'source' parameter.
2499+ '''
2500+ self._default_params['source'] = source
2501+
2502+ def GetRateLimitStatus(self):
2503+ '''Fetch the rate limit status for the currently authorized user.
2504+
2505+ Returns:
2506+ A dictionary containing the time the limit will reset (reset_time),
2507+ the number of remaining hits allowed before the reset (remaining_hits),
2508+ the number of hits allowed in a 60-minute period (hourly_limit), and the
2509+ time of the reset in seconds since The Epoch (reset_time_in_seconds).
2510+ '''
2511+ url = '%s/account/rate_limit_status.json' % self.base_url
2512+ json = self._FetchUrl(url, no_cache=True)
2513+ data = simplejson.loads(json)
2514+
2515+ self._CheckForTwitterError(data)
2516+
2517+ return data
2518+
2519+ def MaximumHitFrequency(self):
2520+ '''Determines the minimum number of seconds that a program must wait before
2521+ hitting the server again without exceeding the rate_limit imposed for the
2522+ currently authenticated user.
2523+
2524+ Returns:
2525+ The minimum second interval that a program must use so as to not exceed
2526+ the rate_limit imposed for the user.
2527+ '''
2528+ rate_status = self.GetRateLimitStatus()
2529+ reset_time = rate_status.get('reset_time', None)
2530+ limit = rate_status.get('remaining_hits', None)
2531+
2532+ if reset_time and limit:
2533+ # put the reset time into a datetime object
2534+ reset = datetime.datetime(*rfc822.parsedate(reset_time)[:7])
2535+
2536+ # find the difference in time between now and the reset time + 1 hour
2537+ delta = reset + datetime.timedelta(hours=1) - datetime.datetime.utcnow()
2538+
2539+ # determine the minimum number of seconds allowed as a regular interval
2540+ max_frequency = int(delta.seconds / limit)
2541+
2542+ # return the number of seconds
2543+ return max_frequency
2544+
2545+ return 0
2546+
2547+ def _BuildUrl(self, url, path_elements=None, extra_params=None):
2548+ # Break url into consituent parts
2549+ (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url)
2550+
2551+ # Add any additional path elements to the path
2552+ if path_elements:
2553+ # Filter out the path elements that have a value of None
2554+ p = [i for i in path_elements if i]
2555+ if not path.endswith('/'):
2556+ path += '/'
2557+ path += '/'.join(p)
2558+
2559+ # Add any additional query parameters to the query string
2560+ if extra_params and len(extra_params) > 0:
2561+ extra_query = self._EncodeParameters(extra_params)
2562+ # Add it to the existing query
2563+ if query:
2564+ query += '&' + extra_query
2565+ else:
2566+ query = extra_query
2567+
2568+ # Return the rebuilt URL
2569+ return urlparse.urlunparse((scheme, netloc, path, params, query, fragment))
2570+
2571+ def _InitializeRequestHeaders(self, request_headers):
2572+ if request_headers:
2573+ self._request_headers = request_headers
2574+ else:
2575+ self._request_headers = {}
2576+
2577+ def _InitializeUserAgent(self):
2578+ user_agent = 'Python-urllib/%s (python-twitter/%s)' % \
2579+ (self._urllib.__version__, __version__)
2580+ self.SetUserAgent(user_agent)
2581+
2582+ def _InitializeDefaultParameters(self):
2583+ self._default_params = {}
2584+
2585+ def _AddAuthorizationHeader(self, username, password):
2586+ if username and password:
2587+ basic_auth = base64.encodestring('%s:%s' % (username, password))[:-1]
2588+ self._request_headers['Authorization'] = 'Basic %s' % basic_auth
2589+
2590+ def _RemoveAuthorizationHeader(self):
2591+ if self._request_headers and 'Authorization' in self._request_headers:
2592+ del self._request_headers['Authorization']
2593+
2594+ def _DecompressGzippedResponse(self, response):
2595+ raw_data = response.read()
2596+ if response.headers.get('content-encoding', None) == 'gzip':
2597+ url_data = gzip.GzipFile(fileobj=StringIO.StringIO(raw_data)).read()
2598+ else:
2599+ url_data = raw_data
2600+ return url_data
2601+
2602+ def _GetOpener(self, url, username=None, password=None):
2603+ if username and password:
2604+ self._AddAuthorizationHeader(username, password)
2605+ handler = self._urllib.HTTPBasicAuthHandler()
2606+ (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url)
2607+ handler.add_password(Api._API_REALM, netloc, username, password)
2608+ opener = self._urllib.build_opener(handler)
2609+ else:
2610+ opener = self._urllib.build_opener()
2611+ opener.addheaders = self._request_headers.items()
2612+ return opener
2613+
2614+ def _Encode(self, s):
2615+ if self._input_encoding:
2616+ return unicode(s, self._input_encoding).encode('utf-8')
2617+ else:
2618+ return unicode(s).encode('utf-8')
2619+
2620+ def _EncodeParameters(self, parameters):
2621+ '''Return a string in key=value&key=value form
2622+
2623+ Values of None are not included in the output string.
2624+
2625+ Args:
2626+ parameters:
2627+ A dict of (key, value) tuples, where value is encoded as
2628+ specified by self._encoding
2629+ Returns:
2630+ A URL-encoded string in "key=value&key=value" form
2631+ '''
2632+ if parameters is None:
2633+ return None
2634+ else:
2635+ return urllib.urlencode(dict([(k, self._Encode(v)) for k, v in parameters.items() if v is not None]))
2636+
2637+ def _EncodePostData(self, post_data):
2638+ '''Return a string in key=value&key=value form
2639+
2640+ Values are assumed to be encoded in the format specified by self._encoding,
2641+ and are subsequently URL encoded.
2642+
2643+ Args:
2644+ post_data:
2645+ A dict of (key, value) tuples, where value is encoded as
2646+ specified by self._encoding
2647+ Returns:
2648+ A URL-encoded string in "key=value&key=value" form
2649+ '''
2650+ if post_data is None:
2651+ return None
2652+ else:
2653+ return urllib.urlencode(dict([(k, self._Encode(v)) for k, v in post_data.items()]))
2654+
2655+ def _CheckForTwitterError(self, data):
2656+ """Raises a TwitterError if twitter returns an error message.
2657+
2658+ Args:
2659+ data: A python dict created from the Twitter json response
2660+ Raises:
2661+ TwitterError wrapping the twitter error message if one exists.
2662+ """
2663+ # Twitter errors are relatively unlikely, so it is faster
2664+ # to check first, rather than try and catch the exception
2665+ if 'error' in data:
2666+ raise TwitterError(data['error'])
2667+
2668+ def _FetchUrl(self,
2669+ url,
2670+ post_data=None,
2671+ parameters=None,
2672+ no_cache=None,
2673+ use_gzip_compression=None):
2674+ '''Fetch a URL, optionally caching for a specified time.
2675+
2676+ Args:
2677+ url:
2678+ The URL to retrieve
2679+ post_data:
2680+ A dict of (str, unicode) key/value pairs.
2681+ If set, POST will be used.
2682+ parameters:
2683+ A dict whose key/value pairs should encoded and added
2684+ to the query string. [optional]
2685+ no_cache:
2686+ If true, overrides the cache on the current request
2687+ use_gzip_compression:
2688+ If True, tells the server to gzip-compress the response.
2689+ It does not apply to POST requests.
2690+ Defaults to None, which will get the value to use from
2691+ the instance variable self._use_gzip [optional]
2692+
2693+ Returns:
2694+ A string containing the body of the response.
2695+ '''
2696+ # Build the extra parameters dict
2697+ extra_params = {}
2698+ if self._default_params:
2699+ extra_params.update(self._default_params)
2700+ if parameters:
2701+ extra_params.update(parameters)
2702+
2703+ # Add key/value parameters to the query string of the url
2704+ url = self._BuildUrl(url, extra_params=extra_params)
2705+
2706+ # Get a url opener that can handle basic auth
2707+ opener = self._GetOpener(url, username=self._username, password=self._password)
2708+
2709+ if use_gzip_compression is None:
2710+ use_gzip = self._use_gzip
2711+ else:
2712+ use_gzip = use_gzip_compression
2713+
2714+ # Set up compression
2715+ if use_gzip and not post_data:
2716+ opener.addheaders.append(('Accept-Encoding', 'gzip'))
2717+
2718+ encoded_post_data = self._EncodePostData(post_data)
2719+
2720+ # Open and return the URL immediately if we're not going to cache
2721+ if encoded_post_data or no_cache or not self._cache or not self._cache_timeout:
2722+ response = opener.open(url, encoded_post_data)
2723+ url_data = self._DecompressGzippedResponse(response)
2724+ opener.close()
2725+ else:
2726+ # Unique keys are a combination of the url and the username
2727+ if self._username:
2728+ key = self._username + ':' + url
2729+ else:
2730+ key = url
2731+
2732+ # See if it has been cached before
2733+ last_cached = self._cache.GetCachedTime(key)
2734+
2735+ # If the cached version is outdated then fetch another and store it
2736+ if not last_cached or time.time() >= last_cached + self._cache_timeout:
2737+ response = opener.open(url, encoded_post_data)
2738+ url_data = self._DecompressGzippedResponse(response)
2739+ opener.close()
2740+ self._cache.Set(key, url_data)
2741+ else:
2742+ url_data = self._cache.Get(key)
2743+
2744+ # Always return the latest version
2745+ return url_data
2746+
2747+
2748+class _FileCacheError(Exception):
2749+ '''Base exception class for FileCache related errors'''
2750+
2751+class _FileCache(object):
2752+
2753+ DEPTH = 3
2754+
2755+ def __init__(self,root_directory=None):
2756+ self._InitializeRootDirectory(root_directory)
2757+
2758+ def Get(self,key):
2759+ path = self._GetPath(key)
2760+ if os.path.exists(path):
2761+ return open(path).read()
2762+ else:
2763+ return None
2764+
2765+ def Set(self,key,data):
2766+ path = self._GetPath(key)
2767+ directory = os.path.dirname(path)
2768+ if not os.path.exists(directory):
2769+ os.makedirs(directory)
2770+ if not os.path.isdir(directory):
2771+ raise _FileCacheError('%s exists but is not a directory' % directory)
2772+ temp_fd, temp_path = tempfile.mkstemp()
2773+ temp_fp = os.fdopen(temp_fd, 'w')
2774+ temp_fp.write(data)
2775+ temp_fp.close()
2776+ if not path.startswith(self._root_directory):
2777+ raise _FileCacheError('%s does not appear to live under %s' %
2778+ (path, self._root_directory))
2779+ if os.path.exists(path):
2780+ os.remove(path)
2781+ os.rename(temp_path, path)
2782+
2783+ def Remove(self,key):
2784+ path = self._GetPath(key)
2785+ if not path.startswith(self._root_directory):
2786+ raise _FileCacheError('%s does not appear to live under %s' %
2787+ (path, self._root_directory ))
2788+ if os.path.exists(path):
2789+ os.remove(path)
2790+
2791+ def GetCachedTime(self,key):
2792+ path = self._GetPath(key)
2793+ if os.path.exists(path):
2794+ return os.path.getmtime(path)
2795+ else:
2796+ return None
2797+
2798+ def _GetUsername(self):
2799+ '''Attempt to find the username in a cross-platform fashion.'''
2800+ try:
2801+ return os.getenv('USER') or \
2802+ os.getenv('LOGNAME') or \
2803+ os.getenv('USERNAME') or \
2804+ os.getlogin() or \
2805+ 'nobody'
2806+ except (IOError, OSError), e:
2807+ return 'nobody'
2808+
2809+ def _GetTmpCachePath(self):
2810+ username = self._GetUsername()
2811+ cache_directory = 'python.cache_' + username
2812+ return os.path.join(tempfile.gettempdir(), cache_directory)
2813+
2814+ def _InitializeRootDirectory(self, root_directory):
2815+ if not root_directory:
2816+ root_directory = self._GetTmpCachePath()
2817+ root_directory = os.path.abspath(root_directory)
2818+ if not os.path.exists(root_directory):
2819+ os.mkdir(root_directory)
2820+ if not os.path.isdir(root_directory):
2821+ raise _FileCacheError('%s exists but is not a directory' %
2822+ root_directory)
2823+ self._root_directory = root_directory
2824+
2825+ def _GetPath(self,key):
2826+ try:
2827+ hashed_key = md5(key).hexdigest()
2828+ except TypeError:
2829+ hashed_key = md5.new(key).hexdigest()
2830+
2831+ return os.path.join(self._root_directory,
2832+ self._GetPrefix(hashed_key),
2833+ hashed_key)
2834+
2835+ def _GetPrefix(self,hashed_key):
2836+ return os.path.sep.join(hashed_key[0:_FileCache.DEPTH])
2837
2838=== added file 'data/icons/hicolor/scalable/apps/backend_identica.png'
2839Binary files data/icons/hicolor/scalable/apps/backend_identica.png 1970-01-01 00:00:00 +0000 and data/icons/hicolor/scalable/apps/backend_identica.png 2010-08-25 16:29:48 +0000 differ

Subscribers

People subscribed via source and target branches

to status/vote changes: