diff --git a/twitter/__init__.py b/twitter/__init__.py index 8bfe48f7..2854cd18 100644 --- a/twitter/__init__.py +++ b/twitter/__init__.py @@ -37,5 +37,7 @@ from .url import Url from .status import Status from .user import User, UserStatus +from .category import Category +from .media import Media from .list import List from .api import Api diff --git a/twitter/api.py b/twitter/api.py index cf5c288c..a4ee280e 100644 --- a/twitter/api.py +++ b/twitter/api.py @@ -36,6 +36,7 @@ from twitter import (__version__, _FileCache, json, DirectMessage, List, Status, Trend, TwitterError, User, UserStatus) +from twitter.category import Category CHARACTER_LIMIT = 140 @@ -341,7 +342,7 @@ def GetSearch(self, if until: parameters['until'] = until - + if since: parameters['since'] = since @@ -474,6 +475,40 @@ def GetTrendsWoeid(self, id, exclude=None): trends.append(Trend.NewFromJsonDict(trend, timestamp=timestamp)) return trends + def GetUserSuggestionCategories(self): + """ Return the list of suggested user categories, this can be used in + GetUserSuggestion function + Returns: + A list of categories + """ + url = '%s/users/suggestions.json' % (self.base_url) + json_data = self._RequestUrl(url, verb='GET') + data = self._ParseAndCheckTwitter(json_data.content) + + categories = [] + + for category in data: + categories.append(Category.NewFromJsonDict(category)) + return categories + + def GetUserSuggestion(self, category): + """ Returns a list of users in a category + Args: + category: + The Category object to limit the search by + Returns: + A list of users in that category + """ + url = '%s/users/suggestions/%s.json' % (self.base_url, category.Slug) + + json_data = self._RequestUrl(url, verb='GET') + data = self._ParseAndCheckTwitter(json_data.content) + + users = [] + for user in data['users']: + users.append(User.NewFromJsonDict(user)) + return users + def GetHomeTimeline(self, count=None, since_id=None, @@ -557,7 +592,6 @@ def GetHomeTimeline(self, parameters['include_entities'] = 'false' json_data = self._RequestUrl(url, 'GET', data=parameters) data = self._ParseAndCheckTwitter(json_data.content) - return [Status.NewFromJsonDict(x) for x in data] def GetUserTimeline(self, @@ -1579,12 +1613,12 @@ def GetFollowerIDsPaged(self, if count is not None: parameters['count'] = count result = [] - + parameters['cursor'] = cursor - + json = self._RequestUrl(url, 'GET', data=parameters) data = self._ParseAndCheckTwitter(json.content) - + if 'next_cursor' in data: next_cursor = data['next_cursor'] else: @@ -1593,7 +1627,7 @@ def GetFollowerIDsPaged(self, previous_cursor = data['previous_cursor'] else: previous_cursor = 0 - + return next_cursor, previous_cursor, data def GetFollowerIDs(self, @@ -1632,13 +1666,14 @@ def GetFollowerIDs(self, url = '%s/followers/ids.json' % self.base_url if not self.__auth: raise TwitterError({'message': "twitter.Api instance must be authenticated"}) - + result = [] if total_count and total_count < count: count = total_count - + while True: - next_cursor, previous_cursor, data = self.GetFollowerIDsPaged(user_id, screen_name, cursor, stringify_ids, count) + next_cursor, previous_cursor, data = self.GetFollowerIDsPaged(user_id, screen_name, cursor, stringify_ids, + count) result += [x for x in data['ids']] if next_cursor == 0 or next_cursor == previous_cursor: break @@ -1650,7 +1685,7 @@ def GetFollowerIDs(self, break sec = self.GetSleepTime('/followers/ids') time.sleep(sec) - + return result def GetFollowersPaged(self, @@ -2063,7 +2098,7 @@ def CreateFriendship(self, user_id=None, screen_name=None, follow=True): A twitter.User instance representing the befriended user. """ return self._AddOrEditFriendship(user_id=user_id, screen_name=screen_name, follow=follow) - + def _AddOrEditFriendship(self, user_id=None, screen_name=None, uri_end='create', follow_key='follow', follow=True): """ Shared method for Create/Update Friendship. @@ -2104,7 +2139,8 @@ def UpdateFriendship(self, user_id=None, screen_name=None, follow=True, **kwargs A twitter.User instance representing the befriended user. """ follow = kwargs.get('device', follow) - return self._AddOrEditFriendship(user_id=user_id, screen_name=screen_name, follow=follow, follow_key='device', uri_end='update') + return self._AddOrEditFriendship(user_id=user_id, screen_name=screen_name, follow=follow, follow_key='device', + uri_end='update') def DestroyFriendship(self, user_id=None, screen_name=None): """Discontinues friendship with a user_id or screen_name. @@ -2883,7 +2919,7 @@ def GetListTimeline(self, """ parameters = {'slug': slug, 'list_id': list_id, - } + } url = '%s/lists/statuses.json' % (self.base_url) parameters['slug'] = slug parameters['list_id'] = list_id @@ -2892,7 +2928,7 @@ def GetListTimeline(self, raise TwitterError({'message': "list_id or slug required"}) if owner_id is None and not owner_screen_name: raise TwitterError({ - 'message': "if list_id is not given you have to include an owner to help identify the proper list"}) + 'message': "if list_id is not given you have to include an owner to help identify the proper list"}) if owner_id: parameters['owner_id'] = owner_id if owner_screen_name: @@ -2966,7 +3002,7 @@ def GetListMembers(self, """ parameters = {'slug': slug, 'list_id': list_id, - } + } url = '%s/lists/members.json' % (self.base_url) parameters['slug'] = slug parameters['list_id'] = list_id @@ -2975,7 +3011,7 @@ def GetListMembers(self, raise TwitterError({'message': "list_id or slug required"}) if owner_id is None and not owner_screen_name: raise TwitterError({ - 'message': "if list_id is not given you have to include an owner to help identify the proper list"}) + 'message': "if list_id is not given you have to include an owner to help identify the proper list"}) if owner_id: parameters['owner_id'] = owner_id if owner_screen_name: @@ -3287,22 +3323,22 @@ def UpdateImage(self, url = '%s/account/update_profile_image.json' % (self.base_url) with open(image, 'rb') as image_file: - encoded_image = base64.b64encode(image_file.read()) + encoded_image = base64.b64encode(image_file.read()) data = { - 'image':encoded_image + 'image': encoded_image } if include_entities: - data['include_entities'] = 1 + data['include_entities'] = 1 if skip_status: - data['skip_status'] = 1 + data['skip_status'] = 1 json = self._RequestUrl(url, 'POST', data=data) if json.status_code in [200, 201, 202]: - return True + return True if json.status_code == 400: - raise TwitterError({'message': "Image data could not be processed"}) + raise TwitterError({'message': "Image data could not be processed"}) if json.status_code == 422: - raise TwitterError({'message': "The image could not be resized or is too large."}) + raise TwitterError({'message': "The image could not be resized or is too large."}) def UpdateBanner(self, image, @@ -3832,7 +3868,7 @@ def _RequestStream(self, url, verb, data=None): return requests.post(url, data=data, stream=True, auth=self.__auth, timeout=self._timeout - ) + ) except requests.RequestException as e: raise TwitterError(str(e)) if verb == 'GET': @@ -3840,7 +3876,7 @@ def _RequestStream(self, url, verb, data=None): try: return requests.get(url, stream=True, auth=self.__auth, timeout=self._timeout - ) + ) except requests.RequestException as e: raise TwitterError(str(e)) return 0 # if not a POST or GET request diff --git a/twitter/category.py b/twitter/category.py new file mode 100644 index 00000000..c382a1e6 --- /dev/null +++ b/twitter/category.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python + + +class Category(object): + """A class representing the suggested user category structure used by the twitter API. + + The UserStatus structure exposes the following properties: + + category.name + category.slug + category.size + """ + + def __init__(self, **kwargs): + """An object to hold a Twitter suggested user category . + This class is normally instantiated by the twitter.Api class and + returned in a sequence. + + Args: + name: + name of the category + slug: + + size: + """ + param_defaults = { + 'name': None, + 'slug': None, + 'size': None, + } + + for (param, default) in param_defaults.iteritems(): + setattr(self, param, kwargs.get(param, default)) + + @property + def Name(self): + return self.name or False + + @property + def Slug(self): + return self.slug or False + + @property + def Size(self): + return self.size or False + + @staticmethod + def NewFromJsonDict(data): + """Create a new instance based on a JSON dict. + + Args: + data: A JSON dict, as converted from the JSON in the twitter API + Returns: + A twitter.Category instance + """ + + return Category(name=data.get('name', None), + slug=data.get('slug', None), + size=data.get('size', None)) diff --git a/twitter/media.py b/twitter/media.py new file mode 100644 index 00000000..945e7ecd --- /dev/null +++ b/twitter/media.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python + + +class Media(object): + """A class representing the Media component of a tweet. + + The Media structure exposes the following properties: + + media.expanded_url + media.display_url + media.url + media.media_url_https + media.media_url + media.type + """ + + def __init__(self, **kwargs): + """An object to the information for each Media entity for a tweet + This class is normally instantiated by the twitter.Api class and + returned in a sequence. + """ + param_defaults = { + 'expanded_url': None, + 'display_url': None, + 'url': None, + 'media_url_https': None, + 'media_url': None, + 'type': None, + 'variants': None + } + + for (param, default) in param_defaults.iteritems(): + setattr(self, param, kwargs.get(param, default)) + + @property + def Expanded_url(self): + return self.expanded_url or False + + @property + def Url(self): + return self.url or False + + @property + def Media_url_https(self): + return self.media_url_https or False + + @property + def Media_url(self): + return self.media_url or False + + @property + def Type(self): + return self.type or False + + @property + def Variants(self): + return self.variants or False + + def __eq__(self, other): + return other.Media_url == self.Media_url and other.Type == self.Type + + def __hash__(self): + return hash((self.Media_url, self.Type)) + + def AsDict(self): + """A dict representation of this twitter.Media instance. + + The return value uses the same key names as the JSON representation. + + Return: + A dict representing this twitter.Media instance + """ + data = {} + if self.expanded_url: + data['expanded_url'] = self.expanded_url + if self.display_url: + data['display_url'] = self.display_url + if self.url: + data['url'] = self.url + if self.media_url_https: + data['media_url_https'] = self.media_url_https + if self.media_url: + data['media_url'] = self.media_url + if self.type: + data['type'] = self.type + if self.variants: + data['variants'] = self.variants + return data + + + @staticmethod + def NewFromJsonDict(data): + """Create a new instance based on a JSON dict. + + Args: + data: A JSON dict, as converted from the JSON in the twitter API + Returns: + A twitter.Media instance + """ + variants = None + if 'video_info' in data: + variants = data['video_info']['variants'] + + return Media(expanded_url=data.get('expanded_url', None), + display_url=data.get('display_url', None), + url=data.get('url', None), + media_url_https=data.get('media_url_https', None), + media_url=data.get('media_url', None), + type=data.get('type', None), + variants=variants + ) diff --git a/twitter/status.py b/twitter/status.py index 8772f9b2..3904a1b2 100644 --- a/twitter/status.py +++ b/twitter/status.py @@ -3,8 +3,10 @@ from calendar import timegm import rfc822 import time +from sets import Set from twitter import json, Hashtag, TwitterError, Url +from twitter.media import Media class Status(object): @@ -118,7 +120,8 @@ def __init__(self, **kwargs): 'media': None, 'withheld_copyright': None, 'withheld_in_countries': None, - 'withheld_scope': None} + 'withheld_scope': None, + } for (param, default) in param_defaults.iteritems(): setattr(self, param, kwargs.get(param, default)) @@ -400,19 +403,19 @@ def __str__(self): return self.AsJsonString() def __repr__(self): - """A string representation of this twitter.Status instance. + """A string representation of this twitter.Status instance. The return value is the ID of status, username and datetime. Returns: A string representation of this twitter.Status instance with the ID of status, username and datetime. """ - if self.user: - representation = "Status(ID=%s, screen_name='%s', created_at='%s')" % ( - self.id, self.user.screen_name, self.created_at) - else: - representation = "Status(ID=%s, created_at='%s')" % ( - self.id, self.created_at) - return representation + if self.user: + representation = "Status(ID=%s, screen_name='%s', created_at='%s')" % ( + self.id, self.user.screen_name, self.created_at) + else: + representation = "Status(ID=%s, created_at='%s')" % ( + self.id, self.created_at) + return representation def AsJsonString(self, allow_non_ascii=False): """A JSON string representation of this twitter.Status instance. @@ -525,7 +528,7 @@ def NewFromJsonDict(data): urls = None user_mentions = None hashtags = None - media = None + media = Set() if 'entities' in data: if 'urls' in data['entities']: urls = [Url.NewFromJsonDict(u) for u in data['entities']['urls']] @@ -536,14 +539,14 @@ def NewFromJsonDict(data): if 'hashtags' in data['entities']: hashtags = [Hashtag.NewFromJsonDict(h) for h in data['entities']['hashtags']] if 'media' in data['entities']: - media = data['entities']['media'] - else: - media = [] + for m in data['entities']['media']: + media.add(Media.NewFromJsonDict(m)) # the new extended entities if 'extended_entities' in data: if 'media' in data['extended_entities']: - media = [m for m in data['extended_entities']['media']] + for m in data['extended_entities']['media']: + media.add(Media.NewFromJsonDict(m)) return Status(created_at=data.get('created_at', None), favorited=data.get('favorited', None), @@ -574,4 +577,5 @@ def NewFromJsonDict(data): scopes=data.get('scopes', None), withheld_copyright=data.get('withheld_copyright', None), withheld_in_countries=data.get('withheld_in_countries', None), - withheld_scope=data.get('withheld_scope', None)) + withheld_scope=data.get('withheld_scope', None), + )