diff --git a/.gitignore b/.gitignore index 90f7f583..17611746 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .DS_Store *.swp test_settings.py - +dist +*.egg-info diff --git a/.travis.yml b/.travis.yml index 5cadb27c..ed2b151c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,3 @@ python: install: - "pip install ." script: "python tests.py" - diff --git a/README.md b/README.md index dd916e09..03af94dc 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Our [developer site](http://instagram.com/developer) documents all the Instagram Blog ---------------------------- -The [Developer Blog] features news and important announcements about the Instagram Platform. You will also find tutorials and best practices to help you build great platform integrations. Make sure to subscribe to the RSS feed not to miss out on new posts: [http://developers.instagram.com](http://developers.instagram.com). +The [Developer Blog](http://developers.instagram.com/) features news and important announcements about the Instagram Platform. You will also find tutorials and best practices to help you build great platform integrations. Make sure to subscribe to the RSS feed not to miss out on new posts: [http://developers.instagram.com](http://developers.instagram.com). Community @@ -225,6 +225,24 @@ except InstagramAPIError as e: print "\nUser is set to private." ``` +Setting Timeouts +------ +By default there is no timeout for requests to the Instagram API. You can specify a timeout in one of two ways: +``` python +from instagram.client import InstagramAPI + +# set a 30-second timeout for this particular InstagramAPI instance +api = InstagramAPI(access_token=access_token, client_secret=client_secret, timeout=30) +``` +or +``` python +import socket + +# Set the global default timeout, which applies to all sockets in your +# program where a timeout is not otherwise specified. +socket.setdefaulttimeout(30) +``` + Trouble Shooting ------ diff --git a/dist/python-instagram-1.0.0.tar.gz b/dist/python-instagram-1.0.0.tar.gz deleted file mode 100644 index 10c8c6b4..00000000 Binary files a/dist/python-instagram-1.0.0.tar.gz and /dev/null differ diff --git a/dist/python-instagram-1.0.1.tar.gz b/dist/python-instagram-1.0.1.tar.gz deleted file mode 100644 index 8d63ba34..00000000 Binary files a/dist/python-instagram-1.0.1.tar.gz and /dev/null differ diff --git a/dist/python-instagram-1.1.0.tar.gz b/dist/python-instagram-1.1.0.tar.gz deleted file mode 100644 index fc1e38b1..00000000 Binary files a/dist/python-instagram-1.1.0.tar.gz and /dev/null differ diff --git a/dist/python-instagram-1.1.1.tar.gz b/dist/python-instagram-1.1.1.tar.gz deleted file mode 100644 index 18b4be3e..00000000 Binary files a/dist/python-instagram-1.1.1.tar.gz and /dev/null differ diff --git a/dist/python-instagram-1.1.2.tar.gz b/dist/python-instagram-1.1.2.tar.gz deleted file mode 100644 index 49548290..00000000 Binary files a/dist/python-instagram-1.1.2.tar.gz and /dev/null differ diff --git a/fixtures/media_search.json b/fixtures/media_search.json index b03cf157..38c1f171 100644 --- a/fixtures/media_search.json +++ b/fixtures/media_search.json @@ -594,6 +594,114 @@ "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_113603_75sq_1287035206.jpg", "id": "113603" } + }, + { + "type": "image", + "tags": [], + "location": { + "latitude": 37.775180799999987, + "id": "68841", + "longitude": -122.2270716, + "name": "El Novillo Taco Truck" + }, + "comments": { + "count": 1 + }, + "caption": { + "created_time": "1287585453", + "text": "Image with broken comment data ", + "from": { + "username": "darodriguez", + "first_name": "David", + "last_name": "Rodriguez", + "type": "user", + "id": "113603" + }, + "id": "495311" + }, + "link": "http://localhost:8000/p/C5Wr/", + "likes": { + "count": 0 + }, + "created_time": "1287585407", + "images": { + "low_resolution": { + "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/20/1fde15405b6349ef949c9a8f5498868e_6.jpg", + "width": 480, + "height": 480 + }, + "thumbnail": { + "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/20/1fde15405b6349ef949c9a8f5498868e_5.jpg", + "width": 150, + "height": 150 + }, + "standard_resolution": { + "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/20/1fde15405b6349ef949c9a8f5498868e_7.jpg", + "width": 612, + "height": 612 + } + }, + "user_has_liked": false, + "id": "759211", + "user": { + "username": "darodriguez", + "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_113603_75sq_1287035206.jpg", + "id": "113603" + } + }, + { + "type": "video", + "tags": [], + "location": { + "latitude": 37.775180799999987, + "id": "68841", + "longitude": -122.2270716, + "name": "El Novillo Taco Truck" + }, + "comments": { + "count": 0 + }, + "caption": { + "created_time": "1287585453", + "text": "Type video without having videos in data", + "from": { + "username": "darodriguez", + "first_name": "David", + "last_name": "Rodriguez", + "type": "user", + "id": "113603" + }, + "id": "495311" + }, + "link": "http://localhost:8000/p/C5Wr/", + "likes": { + "count": 0 + }, + "created_time": "1287585407", + "images": { + "low_resolution": { + "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/20/1fde15405b6349ef949c9a8f5498868e_6.jpg", + "width": 480, + "height": 480 + }, + "thumbnail": { + "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/20/1fde15405b6349ef949c9a8f5498868e_5.jpg", + "width": 150, + "height": 150 + }, + "standard_resolution": { + "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/20/1fde15405b6349ef949c9a8f5498868e_7.jpg", + "width": 612, + "height": 612 + } + }, + "user_has_liked": false, + "id": "759211", + "user": { + "username": "darodriguez", + "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_113603_75sq_1287035206.jpg", + "id": "113603" + } } ] } \ No newline at end of file diff --git a/instagram/bind.py b/instagram/bind.py index cce758bf..452cf3a1 100644 --- a/instagram/bind.py +++ b/instagram/bind.py @@ -125,6 +125,8 @@ def _do_api_request(self, url, method="GET", body=None, headers=None): response, content = OAuth2Request(self.api).make_request(url, method=method, body=body, headers=headers) if response['status'] == '503' or response['status'] == '429': raise InstagramAPIError(response['status'], "Rate limited", "Your client is making too many request per second") + if hasattr(content, "decode"): + content = content.decode('utf-8') try: content_obj = simplejson.loads(content) except ValueError: diff --git a/instagram/client.py b/instagram/client.py index 624bc0ec..c30c7eef 100644 --- a/instagram/client.py +++ b/instagram/client.py @@ -94,7 +94,7 @@ def __init__(self, *args, **kwargs): root_class=Media) user_media_feed = bind_method( - path="/users/self/feed", + path="/users/self/media/recent", accepts_parameters=MEDIA_ACCEPT_PARAMETERS, root_class=Media, paginates=True) diff --git a/instagram/helper.py b/instagram/helper.py index 62bcf5b5..ef96d53a 100644 --- a/instagram/helper.py +++ b/instagram/helper.py @@ -1,9 +1,11 @@ import calendar from datetime import datetime +import pytz def timestamp_to_datetime(ts): - return datetime.utcfromtimestamp(float(ts)) + naive = datetime.utcfromtimestamp(float(ts)) + return naive.replace(tzinfo=pytz.UTC) def datetime_to_timestamp(dt): diff --git a/instagram/models.py b/instagram/models.py index d2517ca2..87a73111 100644 --- a/instagram/models.py +++ b/instagram/models.py @@ -3,21 +3,17 @@ class ApiModel(object): - @classmethod def object_from_dictionary(cls, entry): # make dict keys all strings if entry is None: return "" + entry_str_dict = dict([(str(key), value) for key, value in entry.items()]) return cls(**entry_str_dict) def __repr__(self): return str(self) - # if six.PY2: - # return six.text_type(self).encode('utf8') - # else: - # return self.encode('utf8') def __str__(self): if six.PY3: @@ -27,7 +23,6 @@ def __str__(self): class Image(ApiModel): - def __init__(self, url, width, height): self.url = url self.height = height @@ -38,13 +33,11 @@ def __unicode__(self): class Video(Image): - def __unicode__(self): return "Video: %s" % self.url class Media(ApiModel): - def __init__(self, id=None, **kwargs): self.id = id for key, value in six.iteritems(kwargs): @@ -62,11 +55,9 @@ def get_low_resolution_url(self): else: return self.videos['low_resolution'].url - def get_thumbnail_url(self): return self.images['thumbnail'].url - def __unicode__(self): return "Media: %s" % self.id @@ -74,59 +65,55 @@ def __unicode__(self): def object_from_dictionary(cls, entry): new_media = Media(id=entry['id']) new_media.type = entry['type'] - new_media.user = User.object_from_dictionary(entry['user']) new_media.images = {} - for version, version_info in six.iteritems(entry['images']): + for version, version_info in six.iteritems(entry.get('images', {})): new_media.images[version] = Image.object_from_dictionary(version_info) if new_media.type == 'video': new_media.videos = {} - for version, version_info in six.iteritems(entry['videos']): + for version, version_info in six.iteritems(entry.get('videos', {})): new_media.videos[version] = Video.object_from_dictionary(version_info) - if 'user_has_liked' in entry: - new_media.user_has_liked = entry['user_has_liked'] - new_media.like_count = entry['likes']['count'] + new_media.user_has_liked = entry.get('user_has_liked', False) + + new_media.like_count = entry.get('likes', {}).get('count', 0) new_media.likes = [] - if 'data' in entry['likes']: - for like in entry['likes']['data']: + if new_media.like_count: + for like in entry.get('likes', {}).get('data', []): new_media.likes.append(User.object_from_dictionary(like)) - new_media.comment_count = entry['comments']['count'] + new_media.comment_count = entry.get('comments', {}).get('count', 0) new_media.comments = [] - for comment in entry['comments']['data']: - new_media.comments.append(Comment.object_from_dictionary(comment)) + if new_media.comment_count: + for comment in entry.get('comments', {}).get('data', []): + new_media.comments.append(Comment.object_from_dictionary(comment)) new_media.users_in_photo = [] - if entry.get('users_in_photo'): - for user_in_photo in entry['users_in_photo']: - new_media.users_in_photo.append(UserInPhoto.object_from_dictionary(user_in_photo)) + for user_in_photo in entry.get('users_in_photo') or []: + new_media.users_in_photo.append(UserInPhoto.object_from_dictionary(user_in_photo)) new_media.created_time = timestamp_to_datetime(entry['created_time']) - if entry['location'] and 'id' in entry: + if entry.get('location') and entry.get('id'): new_media.location = Location.object_from_dictionary(entry['location']) new_media.caption = None - if entry['caption']: + if entry.get('caption'): new_media.caption = Comment.object_from_dictionary(entry['caption']) - + new_media.tags = [] - if entry['tags']: - for tag in entry['tags']: - new_media.tags.append(Tag.object_from_dictionary({'name': tag})) + for tag in entry.get('tags', []): + new_media.tags.append(Tag.object_from_dictionary({'name': tag})) new_media.link = entry['link'] - new_media.filter = entry.get('filter') return new_media class MediaShortcode(Media): - def __init__(self, shortcode=None, **kwargs): self.shortcode = shortcode for key, value in six.iteritems(kwargs): @@ -179,11 +166,9 @@ def __init__(self, id, *args, **kwargs): def object_from_dictionary(cls, entry): point = None if 'latitude' in entry: - point = Point(entry.get('latitude'), - entry.get('longitude')) - location = Location(entry.get('id', 0), - point=point, - name=entry.get('name', '')) + point = Point(entry.get('latitude'), entry.get('longitude')) + + location = Location(entry.get('id', 0), point=point, name=entry.get('name', '')) return location def __unicode__(self): @@ -191,7 +176,6 @@ def __unicode__(self): class User(ApiModel): - def __init__(self, id, *args, **kwargs): self.id = id for key, value in six.iteritems(kwargs): @@ -202,7 +186,6 @@ def __unicode__(self): class Relationship(ApiModel): - def __init__(self, incoming_status="none", outgoing_status="none", target_user_is_private=False): self.incoming_status = incoming_status self.outgoing_status = outgoing_status diff --git a/instagram/oauth2.py b/instagram/oauth2.py index 053b1be8..b5db0fe9 100644 --- a/instagram/oauth2.py +++ b/instagram/oauth2.py @@ -27,12 +27,13 @@ class OAuth2API(object): # override with 'Instagram', etc api_name = "Generic API" - def __init__(self, client_id=None, client_secret=None, client_ips=None, access_token=None, redirect_uri=None): + def __init__(self, client_id=None, client_secret=None, client_ips=None, access_token=None, redirect_uri=None, timeout=None): self.client_id = client_id self.client_secret = client_secret self.client_ips = client_ips self.access_token = access_token self.redirect_uri = redirect_uri + self.timeout = timeout def get_authorize_url(self, scope=None): req = OAuth2AuthExchangeRequest(self) @@ -54,8 +55,7 @@ def exchange_user_id_for_access_token(self, user_id): def exchange_xauth_login_for_access_token(self, username, password, scope=None): """ scope should be a tuple or list of requested scope access levels """ req = OAuth2AuthExchangeRequest(self) - return req.exchange_for_access_token(username=username, password=password, - scope=scope) + return req.exchange_for_access_token(username=username, password=password, scope=scope) class OAuth2AuthExchangeRequest(object): @@ -68,8 +68,10 @@ def _url_for_authorize(self, scope=None): "response_type": "code", "redirect_uri": self.api.redirect_uri } + if scope: client_params.update(scope=' '.join(scope)) + url_params = urlencode(client_params) return "%s?%s" % (self.api.authorize_url, url_params) @@ -80,6 +82,7 @@ def _data_for_exchange(self, code=None, username=None, password=None, scope=None "redirect_uri": self.api.redirect_uri, "grant_type": "authorization_code" } + if code: client_params.update(code=code) elif username and password: @@ -88,31 +91,38 @@ def _data_for_exchange(self, code=None, username=None, password=None, scope=None grant_type="password") if scope: client_params.update(scope=' '.join(scope)) + elif user_id: client_params.update(user_id=user_id) + return urlencode(client_params) def get_authorize_url(self, scope=None): return self._url_for_authorize(scope=scope) def get_authorize_login_url(self, scope=None): - http_object = Http(disable_ssl_certificate_validation=True) + http_object = Http(timeout=self.api.timeout, disable_ssl_certificate_validation=True) url = self._url_for_authorize(scope=scope) response, content = http_object.request(url) if response['status'] != '200': raise OAuth2AuthExchangeError("The server returned a non-200 response for URL %s" % url) - redirected_to = response['content-location'] + + redirected_to = response['Content-Location'] return redirected_to def exchange_for_access_token(self, code=None, username=None, password=None, scope=None, user_id=None): data = self._data_for_exchange(code, username, password, scope=scope, user_id=user_id) - http_object = Http(disable_ssl_certificate_validation=True) + http_object = Http(timeout=self.api.timeout, disable_ssl_certificate_validation=True) url = self.api.access_token_url - response, content = http_object.request(url, method="POST", body=data) + + headers = {"Content-Type": "application/x-www-form-urlencoded"} + response, content = http_object.request(url, method="POST", body=data, headers=headers) parsed_content = simplejson.loads(content.decode()) + if int(response['status']) != 200: raise OAuth2AuthExchangeError(parsed_content.get("error_message", "")) + return parsed_content['access_token'], parsed_content['user'] @@ -121,9 +131,12 @@ def __init__(self, api): self.api = api def _generate_sig(self, endpoint, params, secret): - sig = endpoint - for key in sorted(params.keys()): - sig += '|%s=%s' % (key, params[key]) + # handle unicode when signing, urlencode can't handle otherwise. + def enc_if_str(p): + return p.encode('utf-8') if isinstance(p, unicode) else p + + p = ''.join('|{}={}'.format(k, enc_if_str(params[k])) for k in sorted(params.keys())) + sig = '{}{}'.format(endpoint, p) return hmac.new(secret.encode(), sig.encode(), sha256).hexdigest() def url_for_get(self, path, parameters): @@ -136,15 +149,12 @@ def post_request(self, path, **kwargs): return self.make_request(self.prepare_request("POST", path, kwargs)) def _full_url(self, path, include_secret=False, include_signed_request=True): - return "%s://%s%s%s%s%s" % (self.api.protocol, - self.api.host, - self.api.base_path, - path, - self._auth_query(include_secret), - self._signed_request(path, {}, include_signed_request, include_secret)) + return "%s://%s%s%s%s%s" % (self.api.protocol, self.api.host, self.api.base_path, path, + self._auth_query(include_secret), + self._signed_request(path, {}, include_signed_request, include_secret)) def _full_url_with_params(self, path, params, include_secret=False, include_signed_request=True): - return (self._full_url(path, include_secret) + + return (self._full_url(path, include_secret) + self._full_query_with_params(params) + self._signed_request(path, params, include_signed_request, include_secret)) @@ -167,8 +177,10 @@ def _signed_request(self, path, params, include_signed_request, include_secret): params['access_token'] = self.api.access_token elif self.api.client_id: params['client_id'] = self.api.client_id + if include_secret and self.api.client_secret: params['client_secret'] = self.api.client_secret + return "&sig=%s" % self._generate_sig(path, params, self.api.client_secret) else: return '' @@ -199,6 +211,7 @@ def encode_file(field_name): lines.extend(encode_field(field)) for field in files: lines.extend(encode_file(field)) + lines.extend(("--%s--" % (boundary), "")) body = "\r\n".join(lines) @@ -218,10 +231,9 @@ def prepare_request(self, method, path, params, include_secret=False): if not params.get('files'): if method == "POST": body = self._post_body(params) - headers = {'Content-type': 'application/x-www-form-urlencoded'} - url = self._full_url(path, include_secret) - else: - url = self._full_url_with_params(path, params, include_secret) + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + + url = self._full_url_with_params(path, params, include_secret) else: body, headers = self._encode_multipart(params, params['files']) url = self._full_url(path) @@ -230,9 +242,14 @@ def prepare_request(self, method, path, params, include_secret=False): def make_request(self, url, method="GET", body=None, headers=None): headers = headers or {} - if not 'User-Agent' in headers: + if 'User-Agent' not in headers: headers.update({"User-Agent": "%s Python Client" % self.api.api_name}) + # https://github.com/jcgregorio/httplib2/issues/173 # bug in httplib2 w/ Python 3 and disable_ssl_certificate_validation=True - http_obj = Http() if six.PY3 else Http(disable_ssl_certificate_validation=True) + if six.PY3: + http_obj = Http(timeout=self.api.timeout) + else: + http_obj = Http(timeout=self.api.timeout, disable_ssl_certificate_validation=True) + return http_obj.request(url, method, body=body, headers=headers) diff --git a/python_instagram.egg-info/PKG-INFO b/python_instagram.egg-info/PKG-INFO deleted file mode 100644 index 13b71c3b..00000000 --- a/python_instagram.egg-info/PKG-INFO +++ /dev/null @@ -1,11 +0,0 @@ -Metadata-Version: 1.0 -Name: python-instagram -Version: 1.1.3 -Summary: Instagram API client -Home-page: http://github.com/Instagram/python-instagram -Author: Instagram, Inc -Author-email: apidevelopers@instagram.com -License: MIT -Description: UNKNOWN -Keywords: instagram -Platform: UNKNOWN diff --git a/python_instagram.egg-info/SOURCES.txt b/python_instagram.egg-info/SOURCES.txt deleted file mode 100644 index 02db67d7..00000000 --- a/python_instagram.egg-info/SOURCES.txt +++ /dev/null @@ -1,15 +0,0 @@ -setup.py -instagram/__init__.py -instagram/bind.py -instagram/client.py -instagram/helper.py -instagram/json_import.py -instagram/models.py -instagram/oauth2.py -instagram/subscriptions.py -python_instagram.egg-info/PKG-INFO -python_instagram.egg-info/SOURCES.txt -python_instagram.egg-info/dependency_links.txt -python_instagram.egg-info/requires.txt -python_instagram.egg-info/top_level.txt -python_instagram.egg-info/zip-safe \ No newline at end of file diff --git a/python_instagram.egg-info/dependency_links.txt b/python_instagram.egg-info/dependency_links.txt deleted file mode 100644 index 8b137891..00000000 --- a/python_instagram.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/python_instagram.egg-info/requires.txt b/python_instagram.egg-info/requires.txt deleted file mode 100644 index b85397f3..00000000 --- a/python_instagram.egg-info/requires.txt +++ /dev/null @@ -1,2 +0,0 @@ -simplejson -httplib2 \ No newline at end of file diff --git a/python_instagram.egg-info/top_level.txt b/python_instagram.egg-info/top_level.txt deleted file mode 100644 index aebfbd98..00000000 --- a/python_instagram.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -instagram diff --git a/python_instagram.egg-info/zip-safe b/python_instagram.egg-info/zip-safe deleted file mode 100644 index 8b137891..00000000 --- a/python_instagram.egg-info/zip-safe +++ /dev/null @@ -1 +0,0 @@ - diff --git a/requirements.txt b/requirements.txt index c0eed9ba..0bc7b0e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ bottle==0.12.7 httplib2==0.9 -python-instagram==1.1.3 redis==2.10.3 simplejson==3.6.3 beaker==1.6.4 six==1.8.0 +pytz==2015.4 diff --git a/setup.py b/setup.py index b0187529..d45be9c5 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,21 @@ #!/usr/bin/env python from setuptools import setup, find_packages -setup(name="python-instagram", - version="1.3.2", - description="Instagram API client", - license="MIT", - install_requires=["simplejson","httplib2","six"], - author="Instagram, Inc", - author_email="apidevelopers@instagram.com", - url="http://github.com/Instagram/python-instagram", - packages = find_packages(), - keywords= "instagram", - zip_safe = True) +setup( + name="instagram", + version="1.3.3", + description="Instagram API client", + license="MIT", + install_requires=[ + "simplejson", + "httplib2", + "six", + "pytz", + ], + author="instagram, wkoot", + author_email="pypi@rondarchief.nl", + url="http://github.com/wkoot/python-instagram", + packages=find_packages(), + keywords="instagram", + zip_safe=True +) diff --git a/tests.py b/tests.py index fcf705a7..d18e26fa 100755 --- a/tests.py +++ b/tests.py @@ -1,7 +1,8 @@ #!/usr/bin/env python - import types import six +import pytz + try: import simplejson as json except ImportError: @@ -10,6 +11,7 @@ import unittest from six.moves.urllib.parse import urlparse, parse_qs from instagram import client, oauth2, InstagramAPIError +from instagram.helper import timestamp_to_datetime TEST_AUTH = False client_id = "DEBUG" @@ -17,15 +19,13 @@ access_token = "DEBUG" redirect_uri = "http://example.com" -class MockHttp(object): +class MockHttp(object): def __init__(self, *args, **kwargs): pass def request(self, url, method="GET", body=None, headers={}): - fail_state = { - 'status':'400' - }, "{}" + fail_state = {'status': '400'}, "{}" parsed = urlparse(url) options = parse_qs(parsed.query) @@ -33,27 +33,28 @@ def request(self, url, method="GET", body=None, headers={}): fn_name = str(active_call) if fn_name == 'get_authorize_login_url': return { - 'status': '200', - 'content-location':'http://example.com/redirect/login' - }, None + 'status': '200', + 'Content-Location': 'http://example.com/redirect/login' + }, None - if not 'access_token' in options and not 'client_id' in options: + if 'access_token' not in options and 'client_id' not in options: fn_name += '_unauthorized' - if 'self' in url and not 'access_token' in options: + if 'self' in url and 'access_token' not in options: fn_name += '_no_auth_user' fl = open('fixtures/%s.json' % fn_name) content = fl.read() fl.close() + json_content = json.loads(content) status = json_content['meta']['code'] - return { - 'status': status - }, content + return {'status': status}, content -oauth2.Http = MockHttp +oauth2.Http = MockHttp active_call = None + + class TestInstagramAPI(client.InstagramAPI): def __getattribute__(self, attr): global active_call @@ -62,13 +63,18 @@ def __getattribute__(self, attr): active_call = attr return actual_val + class InstagramAuthTests(unittest.TestCase): def setUp(self): + if not TEST_AUTH: + raise unittest.SkipTest() + self.unauthenticated_api = TestInstagramAPI(client_id=client_id, redirect_uri=redirect_uri, client_secret=client_secret) def test_authorize_login_url(self): redirect_uri = self.unauthenticated_api.get_authorize_login_url() assert redirect_uri + print("Please visit and authorize at:\n%s" % redirect_uri) code = raw_input("Paste received code (blank to skip): ").strip() if not code: @@ -83,12 +89,13 @@ def test_xauth_exchange(self): username = raw_input("Enter username for XAuth (blank to skip): ").strip() if not username: return - password = getpass.getpass("Enter password for XAuth (blank to skip): ").strip() + + password = getpass.getpass("Enter password for XAuth (blank to skip): ").strip() access_token = self.unauthenticated_api.exchange_xauth_login_for_access_token(username, password) assert access_token -class InstagramAPITests(unittest.TestCase): +class InstagramAPITests(unittest.TestCase): def setUp(self): super(InstagramAPITests, self).setUp() self.client_only_api = TestInstagramAPI(client_id=client_id) @@ -98,8 +105,8 @@ def test_media_popular(self): self.api.media_popular(count=10) def test_media_search(self): - self.client_only_api.media_search(lat=37.7,lng=-122.22) - self.api.media_search(lat=37.7,lng=-122.22) + self.client_only_api.media_search(lat=37.7, lng=-122.22) + self.api.media_search(lat=37.7, lng=-122.22) def test_media_shortcode(self): self.client_only_api.media_shortcode('os1NQjxtvF') @@ -144,43 +151,39 @@ def test_user_liked_media(self): def test_user_recent_media(self): media, url = self.api.user_recent_media(count=10) - self.assertTrue( all( [hasattr(obj, 'type') for obj in media] ) ) + self.assertTrue(all([hasattr(obj, 'type') for obj in media])) image = media[0] self.assertEqual( - image.get_standard_resolution_url(), - "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/1ce5f3f490a640ca9068e6000c91adc5_7.jpg") + image.get_standard_resolution_url(), + "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/1ce5f3f490a640ca9068e6000c91adc5_7.jpg") self.assertEqual( - image.get_low_resolution_url(), - "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/1ce5f3f490a640ca9068e6000c91adc5_6.jpg") + image.get_low_resolution_url(), + "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/1ce5f3f490a640ca9068e6000c91adc5_6.jpg") self.assertEqual( - image.get_thumbnail_url(), - "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/1ce5f3f490a640ca9068e6000c91adc5_5.jpg") + image.get_thumbnail_url(), + "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/1ce5f3f490a640ca9068e6000c91adc5_5.jpg") - self.assertEqual( False, hasattr(image, 'videos') ) + self.assertEqual(False, hasattr(image, 'videos')) video = media[1] self.assertEqual( - video.get_standard_resolution_url(), - video.videos['standard_resolution'].url) + video.get_standard_resolution_url(), + video.videos['standard_resolution'].url) self.assertEqual( - video.get_standard_resolution_url(), - "http://distilleryvesper9-13.ak.instagram.com/090d06dad9cd11e2aa0912313817975d_101.mp4") + video.get_standard_resolution_url(), + "http://distilleryvesper9-13.ak.instagram.com/090d06dad9cd11e2aa0912313817975d_101.mp4") self.assertEqual( - video.get_low_resolution_url(), - "http://distilleryvesper9-13.ak.instagram.com/090d06dad9cd11e2aa0912313817975d_102.mp4") + video.get_low_resolution_url(), + "http://distilleryvesper9-13.ak.instagram.com/090d06dad9cd11e2aa0912313817975d_102.mp4") self.assertEqual( - video.get_thumbnail_url(), - "http://distilleryimage2.ak.instagram.com/11f75f1cd9cc11e2a0fd22000aa8039a_5.jpg") - - - - + video.get_thumbnail_url(), + "http://distilleryimage2.ak.instagram.com/11f75f1cd9cc11e2a0fd22000aa8039a_5.jpg") def test_user_search(self): self.api.user_search('mikeyk', 10) @@ -204,7 +207,7 @@ def test_location_recent_media(self): self.api.location_recent_media(location_id=1) def test_location_search(self): - self.api.location_search(lat=37.7,lng=-122.22, distance=2500) + self.api.location_search(lat=37.7, lng=-122.22, distance=2500) def test_location(self): self.api.location(1) @@ -225,12 +228,6 @@ def test_tag(self): def test_user_follows(self): self.api.user_follows() - def test_user_followed_by(self): - self.api.user_followed_by() - - def test_user_followed_by(self): - self.api.user_followed_by() - def test_user_requested_by(self): self.api.user_followed_by() @@ -246,6 +243,17 @@ def test_change_relationship(self): def test_geography_recent_media(self): self.api.geography_recent_media(geography_id=1) + +class InstagramHelperTests(unittest.TestCase): + def setUp(self): + self.timestamp = 1439822186 + + def test_timestamp_to_datetime(self): + date_time = timestamp_to_datetime(float(self.timestamp)) + self.assertTrue(date_time.tzinfo is not None) + self.assertEqual(date_time.tzinfo, pytz.UTC) + + if __name__ == '__main__': if not TEST_AUTH: del InstagramAuthTests