From d4fe1f50d92d86ddc7d1cdafbe332a0ce0852078 Mon Sep 17 00:00:00 2001 From: Alex Ch Date: Wed, 21 Oct 2015 00:02:09 +0300 Subject: [PATCH 01/54] beat > the JSON object must be str, not 'bytes' --- instagram/bind.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/instagram/bind.py b/instagram/bind.py index cce758bf..e77b3d29 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() try: content_obj = simplejson.loads(content) except ValueError: From 57a09ad249d9cbe708dfcac39489916d7a7765cb Mon Sep 17 00:00:00 2001 From: Alex Ch Date: Wed, 21 Oct 2015 00:09:02 +0300 Subject: [PATCH 02/54] encoding --- instagram/bind.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instagram/bind.py b/instagram/bind.py index e77b3d29..452cf3a1 100644 --- a/instagram/bind.py +++ b/instagram/bind.py @@ -126,7 +126,7 @@ def _do_api_request(self, url, method="GET", body=None, headers=None): 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() + content = content.decode('utf-8') try: content_obj = simplejson.loads(content) except ValueError: From 05f18c79581d3f1b2cce1db049680c0badda7b49 Mon Sep 17 00:00:00 2001 From: Krish Munot Date: Sun, 15 Nov 2015 00:02:46 +0530 Subject: [PATCH 03/54] Making the word an alive hyperlink --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 223c0dbf..54b87319 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,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 From 8b02858e4257d492d40346a0d19cdf300a8a06c1 Mon Sep 17 00:00:00 2001 From: Konstantin Lebedev Date: Sat, 27 Aug 2016 13:03:20 +0300 Subject: [PATCH 04/54] Update user_media_feed https://www.instagram.com/developer/endpoints/users/#get_users_media_recent_self --- instagram/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 17f11a0c8258f3f2b367849861e445b7d2cd816e Mon Sep 17 00:00:00 2001 From: Hossein Amin Date: Tue, 31 Jan 2017 01:25:05 +0300 Subject: [PATCH 05/54] specify urlencoded Content-Type (exchange_for_...) In my situation Instagram was not able to read post content. And reason was content-type. --- instagram/oauth2.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/instagram/oauth2.py b/instagram/oauth2.py index 053b1be8..002032a4 100644 --- a/instagram/oauth2.py +++ b/instagram/oauth2.py @@ -109,7 +109,8 @@ def exchange_for_access_token(self, code=None, username=None, password=None, sco data = self._data_for_exchange(code, username, password, scope=scope, user_id=user_id) http_object = Http(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", "")) From 0b67a18f659255b084d8d6f80f555debb112dd06 Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 10:42:55 +0100 Subject: [PATCH 06/54] Safely fetch comment data, by checking availability before calling --- instagram/models.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/instagram/models.py b/instagram/models.py index d2517ca2..4f04192b 100644 --- a/instagram/models.py +++ b/instagram/models.py @@ -94,10 +94,12 @@ def object_from_dictionary(cls, entry): for like in entry['likes']['data']: new_media.likes.append(User.object_from_dictionary(like)) - new_media.comment_count = entry['comments']['count'] - new_media.comments = [] - for comment in entry['comments']['data']: - new_media.comments.append(Comment.object_from_dictionary(comment)) + new_media.comment_count = entry.get('comments', {}).get('count', 0) + if new_media.comment_count: + new_media.comments = [] + if entry.get('comments', {}).get('data'): + for comment in entry['comments']['data']: + new_media.comments.append(Comment.object_from_dictionary(comment)) new_media.users_in_photo = [] if entry.get('users_in_photo'): From 93ee521802cbb05fd1f3a8c1ee5968af152d1bb9 Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 11:47:12 +0100 Subject: [PATCH 07/54] pep8 tests --- .gitignore | 1 - tests.py | 83 +++++++++++++++++++++++++----------------------------- 2 files changed, 38 insertions(+), 46 deletions(-) diff --git a/.gitignore b/.gitignore index 90f7f583..bf3ba0be 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ .DS_Store *.swp test_settings.py - diff --git a/tests.py b/tests.py index fcf705a7..6b0d43b0 100755 --- a/tests.py +++ b/tests.py @@ -1,7 +1,7 @@ #!/usr/bin/env python - import types import six + try: import simplejson as json except ImportError: @@ -17,15 +17,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 +31,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,6 +61,7 @@ def __getattribute__(self, attr): active_call = attr return actual_val + class InstagramAuthTests(unittest.TestCase): def setUp(self): self.unauthenticated_api = TestInstagramAPI(client_id=client_id, redirect_uri=redirect_uri, client_secret=client_secret) @@ -69,6 +69,7 @@ def setUp(self): 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 +84,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 +100,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 +146,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 +202,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 +223,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 +238,7 @@ def test_change_relationship(self): def test_geography_recent_media(self): self.api.geography_recent_media(geography_id=1) + if __name__ == '__main__': if not TEST_AUTH: del InstagramAuthTests From cfe9b14251472d3c2bd00d503b1a8cab32948164 Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 12:58:48 +0100 Subject: [PATCH 08/54] Add test case for fixed logic --- fixtures/media_search.json | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/fixtures/media_search.json b/fixtures/media_search.json index b03cf157..cf95c2c0 100644 --- a/fixtures/media_search.json +++ b/fixtures/media_search.json @@ -594,6 +594,60 @@ "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" + } } ] } \ No newline at end of file From f04d1780c4a057f9f6f06ec46bf3a508e36060de Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 13:10:44 +0100 Subject: [PATCH 09/54] pep8 formatting --- instagram/models.py | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/instagram/models.py b/instagram/models.py index 4f04192b..3ee2f7a7 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,7 +65,6 @@ 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 = {} @@ -114,21 +104,19 @@ def object_from_dictionary(cls, entry): new_media.caption = None if entry['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})) 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): @@ -181,11 +169,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): @@ -193,7 +179,6 @@ def __unicode__(self): class User(ApiModel): - def __init__(self, id, *args, **kwargs): self.id = id for key, value in six.iteritems(kwargs): @@ -204,7 +189,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 From f2e1c3143f134831e222012ec283372c0c8ed049 Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 13:19:31 +0100 Subject: [PATCH 10/54] Safely fetch entry data, by checking availability before calling --- instagram/models.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/instagram/models.py b/instagram/models.py index 3ee2f7a7..550305a5 100644 --- a/instagram/models.py +++ b/instagram/models.py @@ -68,25 +68,28 @@ def object_from_dictionary(cls, entry): new_media.user = User.object_from_dictionary(entry['user']) new_media.images = {} - for version, version_info in six.iteritems(entry['images']): - new_media.images[version] = Image.object_from_dictionary(version_info) + if entry.get('images'): + for version, version_info in six.iteritems(entry['images']): + new_media.images[version] = Image.object_from_dictionary(version_info) - if new_media.type == 'video': + if new_media.type == 'video' and entry.get('videos'): new_media.videos = {} for version, version_info in six.iteritems(entry['videos']): new_media.videos[version] = Video.object_from_dictionary(version_info) - if 'user_has_liked' in entry: + if entry.get('user_has_liked'): new_media.user_has_liked = entry['user_has_liked'] - new_media.like_count = entry['likes']['count'] + + new_media.like_count = entry.get('likes', {}).get('count', 0) new_media.likes = [] - if 'data' in entry['likes']: - for like in entry['likes']['data']: - new_media.likes.append(User.object_from_dictionary(like)) + if new_media.like_count: + if entry.get('likes', {}).get('data'): + for like in entry['likes']['data']: + new_media.likes.append(User.object_from_dictionary(like)) new_media.comment_count = entry.get('comments', {}).get('count', 0) + new_media.comments = [] if new_media.comment_count: - new_media.comments = [] if entry.get('comments', {}).get('data'): for comment in entry['comments']['data']: new_media.comments.append(Comment.object_from_dictionary(comment)) @@ -98,15 +101,15 @@ def object_from_dictionary(cls, entry): 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']: + if entry.get('tags'): for tag in entry['tags']: new_media.tags.append(Tag.object_from_dictionary({'name': tag})) From e40306cc4200861cca4b2e88831b4aa07a797d7d Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 13:21:31 +0100 Subject: [PATCH 11/54] Add test case for missing field videos --- fixtures/media_search.json | 54 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/fixtures/media_search.json b/fixtures/media_search.json index cf95c2c0..38c1f171 100644 --- a/fixtures/media_search.json +++ b/fixtures/media_search.json @@ -648,6 +648,60 @@ "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 From bbca28d7a8177d953a3f48213bd7480c5d4a95f6 Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 13:34:12 +0100 Subject: [PATCH 12/54] pep8 formatting --- instagram/oauth2.py | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/instagram/oauth2.py b/instagram/oauth2.py index 002032a4..c9ceee0b 100644 --- a/instagram/oauth2.py +++ b/instagram/oauth2.py @@ -54,8 +54,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 +67,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 +81,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,8 +90,10 @@ 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): @@ -102,18 +106,22 @@ def get_authorize_login_url(self, scope=None): 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) url = self.api.access_token_url - headers={"Content-Type":"application/x-www-form-urlencoded"} + + 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'] @@ -137,15 +145,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)) @@ -168,8 +173,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 '' @@ -200,6 +207,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) @@ -219,7 +227,7 @@ 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'} + 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) @@ -231,9 +239,11 @@ 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) + http_obj = Http() if six.PY3 else Http(disable_ssl_certificate_validation=True) + return http_obj.request(url, method, body=body, headers=headers) From 08096be8e02c20f43c88bd5b2c257aa8f345c0c9 Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 13:54:54 +0100 Subject: [PATCH 13/54] Clean up and use six directly --- instagram/models.py | 32 +++++++++++--------------- python_instagram.egg-info/requires.txt | 3 ++- requirements.txt | 1 - 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/instagram/models.py b/instagram/models.py index 550305a5..e1fa888b 100644 --- a/instagram/models.py +++ b/instagram/models.py @@ -68,36 +68,31 @@ def object_from_dictionary(cls, entry): new_media.user = User.object_from_dictionary(entry['user']) new_media.images = {} - if entry.get('images'): - for version, version_info in six.iteritems(entry['images']): - new_media.images[version] = Image.object_from_dictionary(version_info) + 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' and entry.get('videos'): + 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 entry.get('user_has_liked'): - new_media.user_has_liked = entry['user_has_liked'] + 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 new_media.like_count: - if entry.get('likes', {}).get('data'): - for like in entry['likes']['data']: - new_media.likes.append(User.object_from_dictionary(like)) + for like in entry.get('likes', {}).get('data', []): + new_media.likes.append(User.object_from_dictionary(like)) new_media.comment_count = entry.get('comments', {}).get('count', 0) new_media.comments = [] if new_media.comment_count: - if entry.get('comments', {}).get('data'): - for comment in entry['comments']['data']: - new_media.comments.append(Comment.object_from_dictionary(comment)) + 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', []): + new_media.users_in_photo.append(UserInPhoto.object_from_dictionary(user_in_photo)) new_media.created_time = timestamp_to_datetime(entry['created_time']) @@ -109,9 +104,8 @@ def object_from_dictionary(cls, entry): new_media.caption = Comment.object_from_dictionary(entry['caption']) new_media.tags = [] - if entry.get('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') diff --git a/python_instagram.egg-info/requires.txt b/python_instagram.egg-info/requires.txt index b85397f3..34b941ea 100644 --- a/python_instagram.egg-info/requires.txt +++ b/python_instagram.egg-info/requires.txt @@ -1,2 +1,3 @@ simplejson -httplib2 \ No newline at end of file +httplib2 +six \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c0eed9ba..a57426b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ bottle==0.12.7 httplib2==0.9 -python-instagram==1.1.3 redis==2.10.3 simplejson==3.6.3 beaker==1.6.4 From daa9854f54ac8ec6f76db86345c882e1f807703d Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 14:12:01 +0100 Subject: [PATCH 14/54] Fix travis build --- .travis.yml | 1 - instagram/models.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) 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/instagram/models.py b/instagram/models.py index e1fa888b..87a73111 100644 --- a/instagram/models.py +++ b/instagram/models.py @@ -91,7 +91,7 @@ def object_from_dictionary(cls, entry): new_media.comments.append(Comment.object_from_dictionary(comment)) new_media.users_in_photo = [] - for user_in_photo in entry.get('users_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']) From 22f062f27ba83869981fa2d0db66bfde1f33fdf4 Mon Sep 17 00:00:00 2001 From: Kaviraj Date: Mon, 17 Aug 2015 20:25:48 +0530 Subject: [PATCH 15/54] Cherry pick 75c32cf1db32650a05236220758df369423e8c58 --- instagram/helper.py | 4 +++- tests.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) 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/tests.py b/tests.py index 6b0d43b0..f0715c91 100755 --- a/tests.py +++ b/tests.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import types import six +import pytz try: import simplejson as json @@ -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" @@ -239,6 +241,16 @@ 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.assertIsNotNone(date_time.tzinfo) + self.assertEqual(date_time.tzinfo, pytz.UTC) + + if __name__ == '__main__': if not TEST_AUTH: del InstagramAuthTests From 5bd95e632f959e820571092f5bc670a32c7feb7b Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 14:28:13 +0100 Subject: [PATCH 16/54] Cherry pick a28e4b7493f47913bf2b91876cd28b5c5ffa503f and also add in requires.txt --- python_instagram.egg-info/requires.txt | 3 ++- requirements.txt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/python_instagram.egg-info/requires.txt b/python_instagram.egg-info/requires.txt index 34b941ea..eb906474 100644 --- a/python_instagram.egg-info/requires.txt +++ b/python_instagram.egg-info/requires.txt @@ -1,3 +1,4 @@ simplejson httplib2 -six \ No newline at end of file +six +pytz \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a57426b8..bae04fca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ redis==2.10.3 simplejson==3.6.3 beaker==1.6.4 six==1.8.0 +pytz==2015.4 \ No newline at end of file From 537735d2837c81be929eb3f9f9b60f04abad9ea6 Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 14:32:50 +0100 Subject: [PATCH 17/54] Incorrect place to add pytz requirement, needs setup.py --- python_instagram.egg-info/requires.txt | 2 +- requirements.txt | 2 +- setup.py | 29 ++++++++++++++++---------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/python_instagram.egg-info/requires.txt b/python_instagram.egg-info/requires.txt index eb906474..14223b98 100644 --- a/python_instagram.egg-info/requires.txt +++ b/python_instagram.egg-info/requires.txt @@ -1,4 +1,4 @@ simplejson httplib2 six -pytz \ No newline at end of file +pytz diff --git a/requirements.txt b/requirements.txt index bae04fca..0bc7b0e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,4 @@ redis==2.10.3 simplejson==3.6.3 beaker==1.6.4 six==1.8.0 -pytz==2015.4 \ No newline at end of file +pytz==2015.4 diff --git a/setup.py b/setup.py index b0187529..898d620f 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="python-instagram", + version="1.3.2", + description="Instagram API client", + license="MIT", + install_requires=[ + "simplejson", + "httplib2", + "six", + "pytz", + ], + author="Instagram, Inc", + author_email="apidevelopers@instagram.com", + url="http://github.com/Instagram/python-instagram", + packages=find_packages(), + keywords="instagram", + zip_safe=True +) From f33638e57b29f6ff1787371fd745bed9c6b24318 Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 14:35:38 +0100 Subject: [PATCH 18/54] Support python 2.6 for tests --- tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests.py b/tests.py index f0715c91..21629fc8 100755 --- a/tests.py +++ b/tests.py @@ -247,7 +247,7 @@ def setUp(self): def test_timestamp_to_datetime(self): date_time = timestamp_to_datetime(float(self.timestamp)) - self.assertIsNotNone(date_time.tzinfo) + self.assertTrue(date_time.tzinfo is not None) self.assertEqual(date_time.tzinfo, pytz.UTC) From 248f55acfa3f4f9206bfab175cb781efc4e72a3f Mon Sep 17 00:00:00 2001 From: Colin Stolley Date: Mon, 5 Oct 2015 09:38:42 -0500 Subject: [PATCH 19/54] Cherry pick 93cd4fd4fbf0c3b3d1e050fbc950089aba1c23e3 --- README.md | 18 ++++++++++++++++++ instagram/oauth2.py | 12 ++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8802fc1f..03af94dc 100644 --- a/README.md +++ b/README.md @@ -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/instagram/oauth2.py b/instagram/oauth2.py index c9ceee0b..8211a06d 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) @@ -100,7 +101,7 @@ 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) @@ -112,7 +113,7 @@ def get_authorize_login_url(self, scope=None): 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 headers = {"Content-Type": "application/x-www-form-urlencoded"} @@ -244,6 +245,9 @@ def make_request(self, url, method="GET", body=None, headers=None): # 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) From ef43834d60904bb0a99dbf6c21d54dc30a6564fe Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 15:13:05 +0100 Subject: [PATCH 20/54] Raise SkipTest instead of del AuthTests and make test pass on case-sensitive systems --- tests.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests.py b/tests.py index 21629fc8..8ff3cf7d 100755 --- a/tests.py +++ b/tests.py @@ -34,7 +34,7 @@ def request(self, url, method="GET", body=None, headers={}): if fn_name == 'get_authorize_login_url': return { 'status': '200', - 'content-location': 'http://example.com/redirect/login' + 'Content-Location': 'http://example.com/redirect/login' }, None if 'access_token' not in options and 'client_id' not in options: @@ -66,6 +66,9 @@ def __getattribute__(self, attr): 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): @@ -252,7 +255,4 @@ def test_timestamp_to_datetime(self): if __name__ == '__main__': - if not TEST_AUTH: - del InstagramAuthTests - unittest.main() From d3bc7d7232dcffec37fd73775584b7fdd62984b9 Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 15:18:47 +0100 Subject: [PATCH 21/54] Cherry pick 2d5768b6f71f4312da0bf4084a6ca1fb7eba7525 --- instagram/oauth2.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/instagram/oauth2.py b/instagram/oauth2.py index 8211a06d..d3fa624e 100644 --- a/instagram/oauth2.py +++ b/instagram/oauth2.py @@ -131,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): @@ -229,7 +232,7 @@ def prepare_request(self, method, path, params, include_secret=False): if method == "POST": body = self._post_body(params) headers = {'Content-Type': 'application/x-www-form-urlencoded'} - url = self._full_url(path, include_secret) + url = self._full_url_with_params(path, params, include_secret) else: url = self._full_url_with_params(path, params, include_secret) else: From b283cb955725ff03d75b2f27e6fc1e04c42f1bf9 Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 15:20:13 +0100 Subject: [PATCH 22/54] Remove now obsolete else --- instagram/oauth2.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/instagram/oauth2.py b/instagram/oauth2.py index d3fa624e..b5db0fe9 100644 --- a/instagram/oauth2.py +++ b/instagram/oauth2.py @@ -232,9 +232,8 @@ def prepare_request(self, method, path, params, include_secret=False): if method == "POST": body = self._post_body(params) headers = {'Content-Type': 'application/x-www-form-urlencoded'} - url = self._full_url_with_params(path, params, include_secret) - else: - url = self._full_url_with_params(path, params, include_secret) + + url = self._full_url_with_params(path, params, include_secret) else: body, headers = self._encode_multipart(params, params['files']) url = self._full_url(path) From 6b07bdff1426eb974109e6a1d22dbf18c28ab35e Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 15:27:41 +0100 Subject: [PATCH 23/54] Skip AuthTests in a python 2.6 friendly way --- tests.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests.py b/tests.py index 8ff3cf7d..f7db3227 100755 --- a/tests.py +++ b/tests.py @@ -64,11 +64,9 @@ def __getattribute__(self, attr): return actual_val +@unittest.skipUnless(TEST_AUTH, 'Skipping InstagramAuthTests') 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): From 97af5fb95c1a6139d0009ee3d57dd6193519511c Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 15:38:14 +0100 Subject: [PATCH 24/54] All forms of skipping are not there, don't want to import unittest2.. try old way --- tests.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests.py b/tests.py index f7db3227..e88b3e0b 100755 --- a/tests.py +++ b/tests.py @@ -253,4 +253,7 @@ def test_timestamp_to_datetime(self): if __name__ == '__main__': + if not TEST_AUTH: + del InstagramAuthTests + unittest.main() From 9702c7f1194848fefef86ebd8061b3160cac019b Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 15:42:55 +0100 Subject: [PATCH 25/54] Revert previous attempt; try within the class --- tests.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests.py b/tests.py index e88b3e0b..d18e26fa 100755 --- a/tests.py +++ b/tests.py @@ -64,9 +64,11 @@ def __getattribute__(self, attr): return actual_val -@unittest.skipUnless(TEST_AUTH, 'Skipping InstagramAuthTests') 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): From db766d697263c1058768b06645624cab531dd2d4 Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 16:03:37 +0100 Subject: [PATCH 26/54] Toss and ignore old folders --- .gitignore | 2 ++ dist/python-instagram-1.0.0.tar.gz | Bin 8017 -> 0 bytes dist/python-instagram-1.0.1.tar.gz | Bin 8165 -> 0 bytes dist/python-instagram-1.1.0.tar.gz | Bin 8164 -> 0 bytes dist/python-instagram-1.1.1.tar.gz | Bin 8227 -> 0 bytes dist/python-instagram-1.1.2.tar.gz | Bin 8240 -> 0 bytes python_instagram.egg-info/PKG-INFO | 11 ----------- python_instagram.egg-info/SOURCES.txt | 15 --------------- python_instagram.egg-info/dependency_links.txt | 1 - python_instagram.egg-info/requires.txt | 4 ---- python_instagram.egg-info/top_level.txt | 1 - python_instagram.egg-info/zip-safe | 1 - 12 files changed, 2 insertions(+), 33 deletions(-) delete mode 100644 dist/python-instagram-1.0.0.tar.gz delete mode 100644 dist/python-instagram-1.0.1.tar.gz delete mode 100644 dist/python-instagram-1.1.0.tar.gz delete mode 100644 dist/python-instagram-1.1.1.tar.gz delete mode 100644 dist/python-instagram-1.1.2.tar.gz delete mode 100644 python_instagram.egg-info/PKG-INFO delete mode 100644 python_instagram.egg-info/SOURCES.txt delete mode 100644 python_instagram.egg-info/dependency_links.txt delete mode 100644 python_instagram.egg-info/requires.txt delete mode 100644 python_instagram.egg-info/top_level.txt delete mode 100644 python_instagram.egg-info/zip-safe diff --git a/.gitignore b/.gitignore index bf3ba0be..17611746 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .DS_Store *.swp test_settings.py +dist +*.egg-info 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 10c8c6b4235cf09c44cf98ff6434fbf2920d1bc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8017 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 Q5|lDf$yOKc1`h6j0P~{U@c;k- 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 8d63ba3486fc80db466e183ecbc2b1d65edf0eb4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8165 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 H0|oUz^@afa 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 fc1e38b162a9e6476da0aa668ec39704afd69daa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8164 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 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 14223b98..00000000 --- a/python_instagram.egg-info/requires.txt +++ /dev/null @@ -1,4 +0,0 @@ -simplejson -httplib2 -six -pytz 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 @@ - From f025c756ba3bac11ae8616222cb0f067cd755de5 Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 16:05:48 +0100 Subject: [PATCH 27/54] Update pypi package info --- setup.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 898d620f..cd82aaec 100644 --- a/setup.py +++ b/setup.py @@ -2,8 +2,8 @@ from setuptools import setup, find_packages setup( - name="python-instagram", - version="1.3.2", + name="instagram", + version="1.3", description="Instagram API client", license="MIT", install_requires=[ @@ -12,9 +12,9 @@ "six", "pytz", ], - author="Instagram, Inc", - author_email="apidevelopers@instagram.com", - url="http://github.com/Instagram/python-instagram", + author="instagram, wkoot", + author_email="pypi@rondarchief.nl", + url="http://github.com/wkoot/python-instagram", packages=find_packages(), keywords="instagram", zip_safe=True From 4fd43343f19fa9ecce361a3d0f03a3cc2639b03d Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 16:08:08 +0100 Subject: [PATCH 28/54] Set version to 1.3.3 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index cd82aaec..d45be9c5 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="instagram", - version="1.3", + version="1.3.3", description="Instagram API client", license="MIT", install_requires=[ From b45c293bec844b3b3fadc3748a05241c93566ed4 Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 16:13:35 +0100 Subject: [PATCH 29/54] Update link and image of travis build to new location --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 03af94dc..3ee0eb13 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ - - - -[![Build Status](https://api.travis-ci.org/Instagram/python-instagram.svg)](https://travis-ci.org/Instagram/python-instagram) +[![Build Status](https://api.travis-ci.org/wkoot/python-instagram.svg)](https://travis-ci.org/wkoot/python-instagram) python-instagram ====== From fa9c0ccd106312909a16e61949e918ef4642caae Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 27 Feb 2017 16:52:20 +0100 Subject: [PATCH 30/54] Add twine to push to pypi --- .gitignore | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 17611746..b49f8ea3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ *.swp test_settings.py dist +build *.egg-info diff --git a/requirements.txt b/requirements.txt index 0bc7b0e8..8f8ead1d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ simplejson==3.6.3 beaker==1.6.4 six==1.8.0 pytz==2015.4 +twine==1.8.1 From 18ce70894f3a7bc8017368d508f3b9e2f1018c1b Mon Sep 17 00:00:00 2001 From: wkoot Date: Tue, 28 Feb 2017 11:29:31 +0100 Subject: [PATCH 31/54] Define __all__ and add models for importing --- instagram/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/instagram/__init__.py b/instagram/__init__.py index 837cc3ef..56156d3b 100644 --- a/instagram/__init__.py +++ b/instagram/__init__.py @@ -1,2 +1,5 @@ from .bind import InstagramAPIError, InstagramClientError from .client import InstagramAPI +from . import models + +__all__ = ['InstagramAPI', 'InstagramAPIError', 'InstagramClientError', 'models'] From 1b0b2a717e84980defa2cd1f42b5d5eadde36449 Mon Sep 17 00:00:00 2001 From: wkoot Date: Tue, 28 Feb 2017 13:27:38 +0100 Subject: [PATCH 32/54] Remove dupe tests --- tests.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/tests.py b/tests.py index d18e26fa..8e0bfded 100755 --- a/tests.py +++ b/tests.py @@ -189,7 +189,7 @@ def test_user_search(self): self.api.user_search('mikeyk', 10) def test_user_follows(self): - for page in self.api.user_followed_by(as_generator=True): + for page in self.api.user_follows(as_generator=True): str(page) def test_user_followed_by(self): @@ -225,12 +225,6 @@ def test_tag_search(self): def test_tag(self): self.api.tag("coffee") - def test_user_follows(self): - self.api.user_follows() - - def test_user_requested_by(self): - self.api.user_followed_by() - def test_user_incoming_requests(self): self.api.user_incoming_requests() From 51cb414dc625c57d9b092e07f78f188ab6bf9285 Mon Sep 17 00:00:00 2001 From: wkoot Date: Tue, 28 Feb 2017 13:28:10 +0100 Subject: [PATCH 33/54] pep8 formatting --- instagram/client.py | 240 ++++++++++++++++++++++---------------------- 1 file changed, 120 insertions(+), 120 deletions(-) diff --git a/instagram/client.py b/instagram/client.py index c30c7eef..e397442c 100644 --- a/instagram/client.py +++ b/instagram/client.py @@ -9,7 +9,6 @@ class InstagramAPI(oauth2.OAuth2API): - host = "api.instagram.com" base_path = "/v1" access_token_field = "access_token" @@ -17,7 +16,7 @@ class InstagramAPI(oauth2.OAuth2API): access_token_url = "https://api.instagram.com/oauth/access_token" protocol = "https" api_name = "Instagram" - x_ratelimit_remaining = None + x_ratelimit_remaining = None x_ratelimit = None def __init__(self, *args, **kwargs): @@ -29,179 +28,178 @@ def __init__(self, *args, **kwargs): super(InstagramAPI, self).__init__(**kwargs) media_popular = bind_method( - path="/media/popular", - accepts_parameters=MEDIA_ACCEPT_PARAMETERS, - root_class=Media) + path="/media/popular", + accepts_parameters=MEDIA_ACCEPT_PARAMETERS, + root_class=Media) media_search = bind_method( - path="/media/search", - accepts_parameters=SEARCH_ACCEPT_PARAMETERS + ['lat', 'lng', 'min_timestamp', 'max_timestamp', 'distance'], - root_class=Media) + path="/media/search", + accepts_parameters=SEARCH_ACCEPT_PARAMETERS + ['lat', 'lng', 'min_timestamp', 'max_timestamp', 'distance'], + root_class=Media) media_shortcode = bind_method( - path="/media/shortcode/{shortcode}", - accepts_parameters=['shortcode'], - response_type="entry", - root_class=MediaShortcode, - exclude_format=True) - + path="/media/shortcode/{shortcode}", + accepts_parameters=['shortcode'], + response_type="entry", + root_class=MediaShortcode, + exclude_format=True) media_likes = bind_method( - path="/media/{media_id}/likes", - accepts_parameters=['media_id'], - root_class=User) + path="/media/{media_id}/likes", + accepts_parameters=['media_id'], + root_class=User) like_media = bind_method( - path="/media/{media_id}/likes", - method="POST", - signature=True, - accepts_parameters=['media_id'], - response_type="empty") + path="/media/{media_id}/likes", + method="POST", + signature=True, + accepts_parameters=['media_id'], + response_type="empty") unlike_media = bind_method( - path="/media/{media_id}/likes", - method="DELETE", - signature=True, - accepts_parameters=['media_id'], - response_type="empty") + path="/media/{media_id}/likes", + method="DELETE", + signature=True, + accepts_parameters=['media_id'], + response_type="empty") create_media_comment = bind_method( - path="/media/{media_id}/comments", - method="POST", - signature=True, - accepts_parameters=['media_id', 'text'], - response_type="empty", - root_class=Comment) + path="/media/{media_id}/comments", + method="POST", + signature=True, + accepts_parameters=['media_id', 'text'], + response_type="empty", + root_class=Comment) delete_comment = bind_method( - path="/media/{media_id}/comments/{comment_id}", - method="DELETE", - signature=True, - accepts_parameters=['media_id', 'comment_id'], - response_type="empty") + path="/media/{media_id}/comments/{comment_id}", + method="DELETE", + signature=True, + accepts_parameters=['media_id', 'comment_id'], + response_type="empty") media_comments = bind_method( - path="/media/{media_id}/comments", - method="GET", - accepts_parameters=['media_id'], - response_type="list", - root_class=Comment) + path="/media/{media_id}/comments", + method="GET", + accepts_parameters=['media_id'], + response_type="list", + root_class=Comment) media = bind_method( - path="/media/{media_id}", - accepts_parameters=['media_id'], - response_type="entry", - root_class=Media) + path="/media/{media_id}", + accepts_parameters=['media_id'], + response_type="entry", + root_class=Media) user_media_feed = bind_method( - path="/users/self/media/recent", - accepts_parameters=MEDIA_ACCEPT_PARAMETERS, - root_class=Media, - paginates=True) + path="/users/self/media/recent", + accepts_parameters=MEDIA_ACCEPT_PARAMETERS, + root_class=Media, + paginates=True) user_liked_media = bind_method( - path="/users/self/media/liked", - accepts_parameters=MEDIA_ACCEPT_PARAMETERS, - root_class=Media, - paginates=True) + path="/users/self/media/liked", + accepts_parameters=MEDIA_ACCEPT_PARAMETERS, + root_class=Media, + paginates=True) user_recent_media = bind_method( - path="/users/{user_id}/media/recent", - accepts_parameters=MEDIA_ACCEPT_PARAMETERS + ['user_id', 'min_id', 'max_timestamp', 'min_timestamp'], - root_class=Media, - paginates=True) + path="/users/{user_id}/media/recent", + accepts_parameters=MEDIA_ACCEPT_PARAMETERS + ['user_id', 'min_id', 'max_timestamp', 'min_timestamp'], + root_class=Media, + paginates=True) user_search = bind_method( - path="/users/search", - accepts_parameters=SEARCH_ACCEPT_PARAMETERS, - root_class=User) + path="/users/search", + accepts_parameters=SEARCH_ACCEPT_PARAMETERS, + root_class=User) user_follows = bind_method( - path="/users/{user_id}/follows", - accepts_parameters=["user_id"], - paginates=True, - root_class=User) + path="/users/{user_id}/follows", + accepts_parameters=["user_id"], + paginates=True, + root_class=User) user_followed_by = bind_method( - path="/users/{user_id}/followed-by", - accepts_parameters=["user_id"], - paginates=True, - root_class=User) + path="/users/{user_id}/followed-by", + accepts_parameters=["user_id"], + paginates=True, + root_class=User) user = bind_method( - path="/users/{user_id}", - accepts_parameters=["user_id"], - root_class=User, - response_type="entry") + path="/users/{user_id}", + accepts_parameters=["user_id"], + root_class=User, + response_type="entry") location_recent_media = bind_method( - path="/locations/{location_id}/media/recent", - accepts_parameters=MEDIA_ACCEPT_PARAMETERS + ['location_id'], - root_class=Media, - paginates=True) + path="/locations/{location_id}/media/recent", + accepts_parameters=MEDIA_ACCEPT_PARAMETERS + ['location_id'], + root_class=Media, + paginates=True) location_search = bind_method( - path="/locations/search", - accepts_parameters=SEARCH_ACCEPT_PARAMETERS + ['lat', 'lng', 'foursquare_id', 'foursquare_v2_id'], - root_class=Location) + path="/locations/search", + accepts_parameters=SEARCH_ACCEPT_PARAMETERS + ['lat', 'lng', 'foursquare_id', 'foursquare_v2_id'], + root_class=Location) location = bind_method( - path="/locations/{location_id}", - accepts_parameters=["location_id"], - root_class=Location, - response_type="entry") + path="/locations/{location_id}", + accepts_parameters=["location_id"], + root_class=Location, + response_type="entry") geography_recent_media = bind_method( - path="/geographies/{geography_id}/media/recent", - accepts_parameters=MEDIA_ACCEPT_PARAMETERS + ["geography_id"], - root_class=Media, - paginates=True) + path="/geographies/{geography_id}/media/recent", + accepts_parameters=MEDIA_ACCEPT_PARAMETERS + ["geography_id"], + root_class=Media, + paginates=True) tag_recent_media = bind_method( - path="/tags/{tag_name}/media/recent", - accepts_parameters=['count', 'max_tag_id', 'tag_name'], - root_class=Media, - paginates=True) + path="/tags/{tag_name}/media/recent", + accepts_parameters=['count', 'max_tag_id', 'tag_name'], + root_class=Media, + paginates=True) tag_search = bind_method( - path="/tags/search", - accepts_parameters=SEARCH_ACCEPT_PARAMETERS, - root_class=Tag, - paginates=True) + path="/tags/search", + accepts_parameters=SEARCH_ACCEPT_PARAMETERS, + root_class=Tag, + paginates=True) tag = bind_method( - path="/tags/{tag_name}", - accepts_parameters=["tag_name"], - root_class=Tag, - response_type="entry") + path="/tags/{tag_name}", + accepts_parameters=["tag_name"], + root_class=Tag, + response_type="entry") user_incoming_requests = bind_method( - path="/users/self/requested-by", - root_class=User) + path="/users/self/requested-by", + root_class=User) change_user_relationship = bind_method( - method="POST", - path="/users/{user_id}/relationship", - signature=True, - root_class=Relationship, - accepts_parameters=["user_id", "action"], - paginates=True, - requires_target_user=True, - response_type="entry") + method="POST", + path="/users/{user_id}/relationship", + signature=True, + root_class=Relationship, + accepts_parameters=["user_id", "action"], + paginates=True, + requires_target_user=True, + response_type="entry") user_relationship = bind_method( - method="GET", - path="/users/{user_id}/relationship", - root_class=Relationship, - accepts_parameters=["user_id"], - paginates=False, - requires_target_user=True, - response_type="entry") + method="GET", + path="/users/{user_id}/relationship", + root_class=Relationship, + accepts_parameters=["user_id"], + paginates=False, + requires_target_user=True, + response_type="entry") def _make_relationship_shortcut(action): def _inner(self, *args, **kwargs): - return self.change_user_relationship(user_id=kwargs.get("user_id"), - action=action) + return self.change_user_relationship(user_id=kwargs.get("user_id"), action=action) + return _inner follow_user = _make_relationship_shortcut('follow') @@ -225,7 +223,9 @@ def _make_subscription_action(method, include=None, exclude=None): accepts_parameters.extend(include) if exclude: accepts_parameters = [x for x in accepts_parameters if x not in exclude] + signature = False if method == 'GET' else True + return bind_method( path="/subscriptions", method=method, From 1de32e07be3e1bf721cba6ccdc5c2ee593938281 Mon Sep 17 00:00:00 2001 From: wkoot Date: Tue, 28 Feb 2017 13:40:13 +0100 Subject: [PATCH 34/54] Move api client helper methods outside class --- instagram/client.py | 68 +++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/instagram/client.py b/instagram/client.py index e397442c..df283eea 100644 --- a/instagram/client.py +++ b/instagram/client.py @@ -8,6 +8,40 @@ SUPPORTED_FORMATS = ['json'] +def _make_relationship_shortcut(action): + def _inner(api, *args, **kwargs): + return api.change_user_relationship(user_id=kwargs.get("user_id"), action=action) + + return _inner + + +def _make_subscription_action(method, include=None, exclude=None): + accepts_parameters = ["object", + "aspect", + "object_id", # Optional if subscribing to all users + "callback_url", + "lat", # Geography + "lng", # Geography + "radius", # Geography + "verify_token"] + + if include: + accepts_parameters.extend(include) + if exclude: + accepts_parameters = [x for x in accepts_parameters if x not in exclude] + + signature = False if method == 'GET' else True + + return bind_method( + path="/subscriptions", + method=method, + accepts_parameters=accepts_parameters, + include_secret=True, + objectify_response=False, + signature=signature, + ) + + class InstagramAPI(oauth2.OAuth2API): host = "api.instagram.com" base_path = "/v1" @@ -196,12 +230,6 @@ def __init__(self, *args, **kwargs): requires_target_user=True, response_type="entry") - def _make_relationship_shortcut(action): - def _inner(self, *args, **kwargs): - return self.change_user_relationship(user_id=kwargs.get("user_id"), action=action) - - return _inner - follow_user = _make_relationship_shortcut('follow') unfollow_user = _make_relationship_shortcut('unfollow') block_user = _make_relationship_shortcut('block') @@ -209,32 +237,6 @@ def _inner(self, *args, **kwargs): approve_user_request = _make_relationship_shortcut('approve') ignore_user_request = _make_relationship_shortcut('ignore') - def _make_subscription_action(method, include=None, exclude=None): - accepts_parameters = ["object", - "aspect", - "object_id", # Optional if subscribing to all users - "callback_url", - "lat", # Geography - "lng", # Geography - "radius", # Geography - "verify_token"] - - if include: - accepts_parameters.extend(include) - if exclude: - accepts_parameters = [x for x in accepts_parameters if x not in exclude] - - signature = False if method == 'GET' else True - - return bind_method( - path="/subscriptions", - method=method, - accepts_parameters=accepts_parameters, - include_secret=True, - objectify_response=False, - signature=signature, - ) - create_subscription = _make_subscription_action('POST') list_subscriptions = _make_subscription_action('GET') - delete_subscriptions = _make_subscription_action('DELETE', exclude=['object_id'], include=['id']) + delete_subscriptions = _make_subscription_action('DELETE', include=['id'], exclude=['object_id']) From ce571471e01b5564bd000cafea17bf42ba96c311 Mon Sep 17 00:00:00 2001 From: wkoot Date: Tue, 28 Feb 2017 13:45:08 +0100 Subject: [PATCH 35/54] Clean up warnings --- instagram/bind.py | 50 +++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/instagram/bind.py b/instagram/bind.py index 452cf3a1..2288e9e1 100644 --- a/instagram/bind.py +++ b/instagram/bind.py @@ -1,4 +1,3 @@ -import urllib from .oauth2 import OAuth2Request import re from .json_import import simplejson @@ -6,14 +5,12 @@ from hashlib import sha256 import six from six.moves.urllib.parse import quote -import sys re_path_template = re.compile('{\w+}') def encode_string(value): - return value.encode('utf-8') \ - if isinstance(value, six.text_type) else str(value) + return value.encode('utf-8') if isinstance(value, six.text_type) else str(value) class InstagramClientError(Exception): @@ -29,7 +26,6 @@ def __str__(self): class InstagramAPIError(Exception): - def __init__(self, status_code, error_type, error_message, *args, **kwargs): self.status_code = status_code self.error_type = error_type @@ -40,7 +36,6 @@ def __str__(self): def bind_method(**config): - class InstagramAPIMethod(object): path = config['path'] @@ -62,6 +57,7 @@ def __init__(self, api, *args, **kwargs): self.pagination_format = 'next_url' else: self.pagination_format = kwargs.pop('pagination_format', 'next_url') + self.return_json = kwargs.pop("return_json", False) self.max_pages = kwargs.pop("max_pages", 3) self.with_next_url = kwargs.pop("with_next_url", None) @@ -85,9 +81,10 @@ def _build_parameters(self, args, kwargs): continue if key in self.parameters: raise InstagramClientError("Parameter %s already supplied" % key) + self.parameters[key] = encode_string(value) - if 'user_id' in self.accepts_parameters and not 'user_id' in self.parameters \ - and not self.requires_target_user: + + if 'user_id' in self.accepts_parameters and 'user_id' not in self.parameters and not self.requires_target_user: self.parameters['user_id'] = 'self' def _build_path(self): @@ -103,7 +100,7 @@ def _build_path(self): self.path = self.path.replace(variable, value) if self.api.format and not self.exclude_format: - self.path = self.path + '.%s' % self.api.format + self.path += '.%s' % self.api.format def _build_pagination_info(self, content_obj): """Extract pagination information in the desired format.""" @@ -112,11 +109,12 @@ def _build_pagination_info(self, content_obj): return pagination.get('next_url') if self.pagination_format == 'dict': return pagination + raise Exception('Invalid value for pagination_format: %s' % self.pagination_format) - + def _do_api_request(self, url, method="GET", body=None, headers=None): headers = headers or {} - if self.signature and self.api.client_ips != None and self.api.client_secret != None: + if self.signature and self.api.client_ips is not None and self.api.client_secret is not None: secret = self.api.client_secret ips = self.api.client_ips signature = hmac.new(secret, ips, sha256).hexdigest() @@ -127,20 +125,24 @@ def _do_api_request(self, url, method="GET", body=None, headers=None): 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: raise InstagramClientError('Unable to parse response, not valid JSON.', status_code=response['status']) + # Handle OAuthRateLimitExceeded from Instagram's Nginx which uses different format to documented api responses if 'meta' not in content_obj: if content_obj.get('code') == 420 or content_obj.get('code') == 429: error_message = content_obj.get('error_message') or "Your client is making too many request per second" raise InstagramAPIError(content_obj.get('code'), "Rate limited", error_message) + raise InstagramAPIError(content_obj.get('code'), content_obj.get('error_type'), content_obj.get('error_message')) + api_responses = [] status_code = content_obj['meta']['code'] - self.api.x_ratelimit_remaining = response.get("x-ratelimit-remaining",None) - self.api.x_ratelimit = response.get("x-ratelimit-limit",None) + self.api.x_ratelimit_remaining = response.get("x-ratelimit-remaining", None) + self.api.x_ratelimit = response.get("x-ratelimit-limit", None) if status_code == 200: if not self.objectify_response: return content_obj, None @@ -152,14 +154,17 @@ def _do_api_request(self, url, method="GET", body=None, headers=None): else: obj = self.root_class.object_from_dictionary(entry) api_responses.append(obj) + elif self.response_type == 'entry': data = content_obj['data'] if self.return_json: api_responses = data else: api_responses = self.root_class.object_from_dictionary(data) + elif self.response_type == 'empty': pass + return api_responses, self._build_pagination_info(content_obj) else: raise InstagramAPIError(status_code, content_obj['meta']['error_type'], content_obj['meta']['error_message']) @@ -167,30 +172,33 @@ def _do_api_request(self, url, method="GET", body=None, headers=None): def _paginator_with_url(self, url, method="GET", body=None, headers=None): headers = headers or {} pages_read = 0 + while url and (self.max_pages is None or pages_read < self.max_pages): api_responses, url = self._do_api_request(url, method, body, headers) pages_read += 1 yield api_responses, url + return def _get_with_next_url(self, url, method="GET", body=None, headers=None): headers = headers or {} - content, next = self._do_api_request(url, method, body, headers) - return content, next + content, next_url = self._do_api_request(url, method, body, headers) + + return content, next_url def execute(self): - url, method, body, headers = OAuth2Request(self.api).prepare_request(self.method, - self.path, - self.parameters, - include_secret=self.include_secret) + url, method, body, headers = OAuth2Request(self.api).prepare_request(self.method, self.path, self.parameters, include_secret=self.include_secret) + if self.with_next_url: return self._get_with_next_url(self.with_next_url, method, body, headers) + if self.as_generator: return self._paginator_with_url(url, method, body, headers) else: - content, next = self._do_api_request(url, method, body, headers) + content, next_url = self._do_api_request(url, method, body, headers) + if self.paginates: - return content, next + return content, next_url else: return content From 5f41b33d672465bfb8f56d8d1d4aa02c33e32867 Mon Sep 17 00:00:00 2001 From: wkoot Date: Tue, 28 Feb 2017 13:47:29 +0100 Subject: [PATCH 36/54] pep8 --- instagram/subscriptions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/instagram/subscriptions.py b/instagram/subscriptions.py index d84d1fe9..22ba5e77 100644 --- a/instagram/subscriptions.py +++ b/instagram/subscriptions.py @@ -2,6 +2,7 @@ import hashlib from .json_import import simplejson + class SubscriptionType: TAG = 'tag' USER = 'user' From de7c13628b0ec1a0dd6a330c5cab6b905434c06c Mon Sep 17 00:00:00 2001 From: Sylvain Date: Wed, 8 Mar 2017 15:26:57 +0100 Subject: [PATCH 37/54] Add most recent Python versions in Travis CI Add more recent Python versions including development branches and nightly build. --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 5cadb27c..34c04739 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,11 @@ python: - "2.7" - "3.3" - "3.4" + - "3.5" + - "3.6" + - "3.6-dev" + - "3.7-dev" + - "nightly" install: - "pip install ." script: "python tests.py" From 91819f49b41c3217b2018e4649c2ccdaa05a102c Mon Sep 17 00:00:00 2001 From: wkoot Date: Fri, 29 Sep 2017 10:41:38 +0200 Subject: [PATCH 38/54] Add relevant test case --- fixtures/tag_recent_media.json | 129 +++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/fixtures/tag_recent_media.json b/fixtures/tag_recent_media.json index 56c5a3a5..a574406a 100644 --- a/fixtures/tag_recent_media.json +++ b/fixtures/tag_recent_media.json @@ -189,6 +189,135 @@ "id": "5273", "last_name": "" } + }, + { + "id": "1614083460929239315_25025320", + "user": { + "id": "25025320", + "full_name": "Instagram", + "profile_picture": "https://scontent.cdninstagram.com/t51.2885-19/s150x150/14719833_310540259320655_1605122788543168512_a.jpg", + "username": "instagram" + }, + "images": { + "thumbnail": { + "width": 150, + "height": 150, + "url": "https://scontent.cdninstagram.com/t51.2885-15/s150x150/e15/21980918_1513931928693772_9033863567060762624_n.jpg" + }, + "low_resolution": { + "width": 320, + "height": 320, + "url": "https://scontent.cdninstagram.com/t51.2885-15/s320x320/e15/21980918_1513931928693772_9033863567060762624_n.jpg" + }, + "standard_resolution": { + "width": 640, + "height": 640, + "url": "https://scontent.cdninstagram.com/t51.2885-15/s640x640/e15/21980918_1513931928693772_9033863567060762624_n.jpg" + } + }, + "created_time": "1506633764", + "caption": { + "id": "17900429008029332", + "text": "Hello, world! It\u2019s time for another dose of #WeeklyFluff. Meet Kuzuki (@sou_ham), a happy hamster from Japan who has been described as a \u201csmall vacuum cleaner.\u201d Follow @sou_ham to make sure you never miss one of Kuzuki\u2019s \u2014 or Uzuki's! (the other hamster in the family) \u2014 adventures. \ud83d\udc39\ud83d\udc39", + "created_time": "1506633764", + "from": { + "id": "25025320", + "full_name": "Instagram", + "profile_picture": "https://scontent.cdninstagram.com/t51.2885-19/s150x150/14719833_310540259320655_1605122788543168512_a.jpg", + "username": "instagram" + } + }, + "user_has_liked": false, + "likes": { + "count": 821506 + }, + "tags": ["weeklyfluff"], + "filter": "Normal", + "comments": { + "count": 15562 + }, + "type": "video", + "link": "http://localhost:8000/p/BZmYFsehTkT/", + "location": null, + "attribution": null, + "users_in_photo": [], + "videos": { + "standard_resolution": { + "width": 640, + "height": 640, + "url": "https://scontent.cdninstagram.com/t50.2886-16/22137988_1621796317884357_179940820256817152_n.mp4", + "id": "0" + }, + "low_bandwidth": { + "width": 480, + "height": 480, + "url": "https://scontent.cdninstagram.com/t50.2886-16/22112778_357761027970441_6775862658118713344_n.mp4", + "id": "0" + }, + "low_resolution": { + "width": 480, + "height": 480, + "url": "https://scontent.cdninstagram.com/t50.2886-16/22112778_357761027970441_6775862658118713344_n.mp4", + "id": "0" + } + } + }, + { + "id": "1613267810091631142_25025320", + "user": { + "id": "25025320", + "full_name": "Instagram", + "profile_picture": "https://scontent.cdninstagram.com/t51.2885-19/s150x150/14719833_310540259320655_1605122788543168512_a.jpg", + "username": "instagram" + }, + "images": { + "thumbnail": { + "width": 150, + "height": 150, + "url": "https://scontent.cdninstagram.com/t51.2885-15/s150x150/e35/21985313_400032113744601_677307280198205440_n.jpg" + }, + "low_resolution": { + "width": 320, + "height": 320, + "url": "https://scontent.cdninstagram.com/t51.2885-15/s320x320/e35/21985313_400032113744601_677307280198205440_n.jpg" + }, + "standard_resolution": { + "width": 640, + "height": 640, + "url": "https://scontent.cdninstagram.com/t51.2885-15/s640x640/sh0.08/e35/21985313_400032113744601_677307280198205440_n.jpg" + } + }, + "created_time": "1506536531", + "caption": { + "id": "17874787510182141", + "text": "Photo by @naomijon\nBlue hair. Blue eyes. \u201cThese two things are kind of my trademark,\u201d says German beauty blogger Naomi Jon (@naomijon). \u201cIf I had to choose between black and any color, I would always go for the color, because it\u2019s way more exciting, and it always attracts attention.\u201d\nWatch Naomi apply a colorful eye shadow combination on our Instagram story right now. \ud83d\udc99", + "created_time": "1506536531", + "from": { + "id": "25025320", + "full_name": "Instagram", + "profile_picture": "https://scontent.cdninstagram.com/t51.2885-19/s150x150/14719833_310540259320655_1605122788543168512_a.jpg", + "username": "instagram" + } + }, + "user_has_liked": false, + "likes": { + "count": 590414 + }, + "tags": [], + "filter": "Normal", + "comments": { + "count": 6413 + }, + "type": "image", + "link": "https://www.instagram.com/p/BZjeoacFYIm/", + "location": { + "latitude": 52.3667, + "longitude": 9.71667, + "name": "Hanover, Germany", + "id": 213305671 + }, + "attribution": null, + "users_in_photo": [] } ] } \ No newline at end of file From d49461fa13336f95aba4ef3499c2bc8db3f20b18 Mon Sep 17 00:00:00 2001 From: wkoot Date: Fri, 29 Sep 2017 10:57:22 +0200 Subject: [PATCH 39/54] Add support for id passing in Image and Video objects --- instagram/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/instagram/models.py b/instagram/models.py index 87a73111..576088a0 100644 --- a/instagram/models.py +++ b/instagram/models.py @@ -23,11 +23,14 @@ def __str__(self): class Image(ApiModel): - def __init__(self, url, width, height): + def __init__(self, url, width, height, id=None): self.url = url self.height = height self.width = width + if id: # don't store ids None and 0, just provide support for what seems to be an upcoming API change + self.id = id + def __unicode__(self): return "Image: %s" % self.url From 85277d6fc75844e7a6f4f9c2ec9fcfada381d2e2 Mon Sep 17 00:00:00 2001 From: wkoot Date: Fri, 29 Sep 2017 11:49:22 +0200 Subject: [PATCH 40/54] Add OAuth2RequestTests with test_generate_sig --- tests.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests.py b/tests.py index 8e0bfded..85f5ffe0 100755 --- a/tests.py +++ b/tests.py @@ -95,6 +95,16 @@ def test_xauth_exchange(self): assert access_token +class OAuth2RequestTests(unittest.TestCase): + def setUp(self): + super(OAuth2RequestTests, self).setUp() + self.api = TestInstagramAPI(access_token=access_token) + self.request = oauth2.OAuth2Request(self.api) + + def test_generate_sig(self): + self.request._generate_sig(endpoint='/', params=dict(count=1), secret=client_secret) + + class InstagramAPITests(unittest.TestCase): def setUp(self): super(InstagramAPITests, self).setUp() From 544756908c4ca25baf3ccba11917e1d9a72e7e45 Mon Sep 17 00:00:00 2001 From: wkoot Date: Fri, 29 Sep 2017 11:52:01 +0200 Subject: [PATCH 41/54] Make python3 compatible unicode call --- instagram/oauth2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instagram/oauth2.py b/instagram/oauth2.py index b5db0fe9..f3d56970 100644 --- a/instagram/oauth2.py +++ b/instagram/oauth2.py @@ -133,7 +133,7 @@ def __init__(self, api): def _generate_sig(self, endpoint, params, secret): # 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 + return p.encode('utf-8') if isinstance(p, six.text_type) else p p = ''.join('|{}={}'.format(k, enc_if_str(params[k])) for k in sorted(params.keys())) sig = '{}{}'.format(endpoint, p) From a0e8fe49a680b5d3b2c7abee1228cd32a800d827 Mon Sep 17 00:00:00 2001 From: wkoot Date: Fri, 29 Sep 2017 11:57:19 +0200 Subject: [PATCH 42/54] Don't use {} string format shorthand --- instagram/oauth2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/instagram/oauth2.py b/instagram/oauth2.py index f3d56970..a738c503 100644 --- a/instagram/oauth2.py +++ b/instagram/oauth2.py @@ -135,8 +135,8 @@ def _generate_sig(self, endpoint, params, secret): def enc_if_str(p): return p.encode('utf-8') if isinstance(p, six.text_type) else p - p = ''.join('|{}={}'.format(k, enc_if_str(params[k])) for k in sorted(params.keys())) - sig = '{}{}'.format(endpoint, p) + path = ''.join('|{key}={val}'.format(key=key, val=enc_if_str(params[key])) for key in sorted(params.keys())) + sig = '{endpoint}{path}'.format(endpoint=endpoint, path=path) return hmac.new(secret.encode(), sig.encode(), sha256).hexdigest() def url_for_get(self, path, parameters): From f09ac1b88d3163032da79269cf8a532482678172 Mon Sep 17 00:00:00 2001 From: wkoot Date: Fri, 29 Sep 2017 12:13:48 +0200 Subject: [PATCH 43/54] Ensure that all other string munging instances in oauth2.py use .format with named groups --- instagram/oauth2.py | 50 ++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/instagram/oauth2.py b/instagram/oauth2.py index a738c503..7d6ec6ba 100644 --- a/instagram/oauth2.py +++ b/instagram/oauth2.py @@ -73,7 +73,7 @@ def _url_for_authorize(self, scope=None): client_params.update(scope=' '.join(scope)) url_params = urlencode(client_params) - return "%s?%s" % (self.api.authorize_url, url_params) + return "{url}?{params}".format(url=self.api.authorize_url, params=url_params) def _data_for_exchange(self, code=None, username=None, password=None, scope=None, user_id=None): client_params = { @@ -106,7 +106,7 @@ def get_authorize_login_url(self, scope=None): 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) + raise OAuth2AuthExchangeError("The server returned a non-200 response for URL {url}".format(url)) redirected_to = response['Content-Location'] return redirected_to @@ -148,27 +148,30 @@ def get_request(self, path, **kwargs): def post_request(self, path, **kwargs): return self.make_request(self.prepare_request("POST", path, kwargs)) + # TODO - make use of six.moves.urllib.parse.urlparse for all this string munging 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)) + signed_request = self._signed_request(path, {}, include_signed_request, include_secret) + return "{protocol}://{host}{basepath}{path}{query}{signed}".format( + protocol=self.api.protocol, host=self.api.host, basepath=self.api.base_path, path=path, + query=self._auth_query(include_secret), signed=signed_request) def _full_url_with_params(self, path, params, include_secret=False, include_signed_request=True): - return (self._full_url(path, include_secret) + - self._full_query_with_params(params) + - self._signed_request(path, params, include_signed_request, include_secret)) + signed_request = self._signed_request(path, params, include_signed_request, include_secret) + return "{url}{query}{signed}".format( + url=self._full_url(path, include_secret), query=self._full_query_with_params(params), signed=signed_request) def _full_query_with_params(self, params): - params = ("&" + urlencode(params)) if params else "" - return params + if not params: + return "" + return "&{params}".format(params=urlencode(params)) def _auth_query(self, include_secret=False): if self.api.access_token: - return ("?%s=%s" % (self.api.access_token_field, self.api.access_token)) + return "?{field}={token}".format(field=self.api.access_token_field, token=self.api.access_token) elif self.api.client_id: - base = ("?client_id=%s" % (self.api.client_id)) + base = "?client_id={client_id}".format(client_id=self.api.client_id) if include_secret: - base += "&client_secret=%s" % (self.api.client_secret) + base += "&client_secret={client_secret}".format(client_secret=self.api.client_secret) return base def _signed_request(self, path, params, include_signed_request, include_secret): @@ -181,7 +184,7 @@ def _signed_request(self, path, params, include_signed_request, include_secret): 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) + return "&sig={signed}".format(signed=self._generate_sig(path, params, self.api.client_secret)) else: return '' @@ -195,15 +198,16 @@ def get_content_type(file_name): return mimetypes.guess_type(file_name)[0] or "application/octet-stream" def encode_field(field_name): - return ("--" + boundary, - 'Content-Disposition: form-data; name="%s"' % (field_name), + return ("--{boundary}".format(boundary=boundary), + 'Content-Disposition: form-data; name="{field_name}"'.format(field_name=field_name), "", str(params[field_name])) def encode_file(field_name): file_name, file_handle = files[field_name] - return ("--" + boundary, - 'Content-Disposition: form-data; name="%s"; filename="%s"' % (field_name, file_name), - "Content-Type: " + get_content_type(file_name), + return ("--{boundary}".format(boundary=boundary), + 'Content-Disposition: form-data; name="{field_name}"; filename="{file_name}"'.format( + field_name=field_name, file_name=file_name), + "Content-Type: {content_type}".format(content_type=get_content_type(file_name)), "", file_handle.read()) lines = [] @@ -212,10 +216,10 @@ def encode_file(field_name): for field in files: lines.extend(encode_file(field)) - lines.extend(("--%s--" % (boundary), "")) + lines.extend(("--{boundary}--".format(boundary=boundary), "")) body = "\r\n".join(lines) - headers = {"Content-Type": "multipart/form-data; boundary=" + boundary, + headers = {"Content-Type": "multipart/form-data; boundary={boundary}".format(boundary=boundary), "Content-Length": str(len(body))} return body, headers @@ -243,12 +247,12 @@ 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 'User-Agent' not in headers: - headers.update({"User-Agent": "%s Python Client" % self.api.api_name}) + headers.update({"User-Agent": "{api_name} Python Client".format(api_name=self.api.api_name)}) # https://github.com/jcgregorio/httplib2/issues/173 # bug in httplib2 w/ Python 3 and disable_ssl_certificate_validation=True if six.PY3: - http_obj = Http(timeout=self.api.timeout) + http_obj = Http(timeout=self.api.timeout) else: http_obj = Http(timeout=self.api.timeout, disable_ssl_certificate_validation=True) From 4e1e6227c995e050318c170f583a40addf33e0ff Mon Sep 17 00:00:00 2001 From: wkoot Date: Fri, 29 Sep 2017 12:27:04 +0200 Subject: [PATCH 44/54] Set new release version 1.3.4 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d45be9c5..f5de51d7 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="instagram", - version="1.3.3", + version="1.3.4", description="Instagram API client", license="MIT", install_requires=[ From 39276f80ebc8bb93d0dbdb5b8bdd9ff4766304aa Mon Sep 17 00:00:00 2001 From: wkoot Date: Thu, 5 Oct 2017 15:54:04 +0200 Subject: [PATCH 45/54] Add coverage plugin --- .gitignore | 2 ++ .travis.yml | 6 +++++- build_requirements.txt | 3 +++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 build_requirements.txt diff --git a/.gitignore b/.gitignore index b49f8ea3..bf7cb040 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ test_settings.py dist build *.egg-info +.coverage +coverage.xml diff --git a/.travis.yml b/.travis.yml index 3e0c92f3..e166351c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,4 +11,8 @@ python: - "nightly" install: - "pip install ." -script: "python tests.py" + - "pip install -r build_requirements.txt" +script: + - "coverage run tests.py" + - "coverage xml -o coverage.xml" + - "python-codacy-coverage -r coverage.xml" diff --git a/build_requirements.txt b/build_requirements.txt new file mode 100644 index 00000000..35a0868f --- /dev/null +++ b/build_requirements.txt @@ -0,0 +1,3 @@ +twine==1.8.1 +coverage==4.4.1 +codacy-coverage==1.3.3 From cb3e1fca73a29612bf66bbce6c4c5c992d4f8cfe Mon Sep 17 00:00:00 2001 From: wkoot Date: Thu, 5 Oct 2017 15:54:32 +0200 Subject: [PATCH 46/54] Remove sample_app.py and trim requirements --- README.md | 86 +++++++-------- requirements.txt | 4 - sample_app.py | 272 ----------------------------------------------- 3 files changed, 44 insertions(+), 318 deletions(-) delete mode 100644 sample_app.py diff --git a/README.md b/README.md index 3ee0eb13..0993d37c 100644 --- a/README.md +++ b/README.md @@ -6,39 +6,46 @@ [![Build Status](https://api.travis-ci.org/wkoot/python-instagram.svg)](https://travis-ci.org/wkoot/python-instagram) + python-instagram ====== A Python 2/3 client for the Instagram REST and Search APIs + Installation ------ +------------ ``` -pip install python-instagram +pip install instagram ``` -Requires ------ + + +Requirements +------------ * httplib2 * simplejson * six + * pytz Instagram REST and Search APIs ------------------------------ -Our [developer site](http://instagram.com/developer) documents all the Instagram REST and Search APIs. +The [Instagram developer page](http://instagram.com/developer) supposedly documents all the Instagram REST and Search APIs. -Blog ----------------------------- -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). +Instagram for developers blog +----------------------------- +The [Instagram for developers 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. -Community ----------------------- -The [Stack Overflow community](http://stackoverflow.com/questions/tagged/instagram/) is a great place to ask API related questions or if you need help with your code. Make sure to tag your questions with the Instagram tag to get fast answers from other fellow developers and members of the Instagram team. +Stack Overflow community +------------------------ +The Stack Overflow [instagram tag](http://stackoverflow.com/questions/tagged/instagram/) is a great place to ask API related questions or if you need help with your code. +Make sure to tag your questions with the Instagram tag to get fast answers from other fellow developers and members of the Instagram team. Authentication ------ +-------------- Instagram API uses the OAuth2 protocol for authentication, but not all functionality requires authentication. See the docs for more information: http://instagram.com/developer/authentication/ @@ -66,7 +73,7 @@ recent_media, next_ = api.user_recent_media(user_id="userid", count=10) for media in recent_media: print media.caption.text ``` - + ### Making unauthenticated requests For methods that don't require authentication, you can just pass your client ID and optionally client secret into the InstagramAPI @@ -79,8 +86,9 @@ for media in popular_media: print media.images['standard_resolution'].url ``` -Real-time Subscriptions: ------ + +Real-time Subscriptions +----------------------- See the docs for more on real-time subscriptions: http://instagr.am/developer/realtime/ @@ -107,7 +115,7 @@ Along with that, you would typically register subscription "reactors" for proces reactor = subscriptions.SubscriptionsReactor() reactor.register_callback(subscriptions.SubscriptionType.USER, process_user_update) ``` - + See the provided sample app for an example of making a subscription, reacting to it, an processing the updates. You can also use the API to list and delete subscriptions: @@ -116,10 +124,10 @@ You can also use the API to list and delete subscriptions: api.list_subscriptions() api.delete_subscriptions(id=342342) ``` - -Data Retrieval: ------ + +Data Retrieval +-------------- See the endpoints docs for more on these methods: http://instagr.am/developer/endpoints/ @@ -142,7 +150,7 @@ while next_: ``` Users: http://instagr.am/developer/endpoints/users/ - + ``` python api.user(user_id) api.user_media_feed()* @@ -150,7 +158,7 @@ api.user_liked_media()* api.user_recent_media(user_id, count, max_id)* api.user_search(q, count, lat, lng, min_timestamp, max_timestamp) ``` - + Relationships: http://instagr.am/developer/endpoints/relationships/ ``` python @@ -173,7 +181,7 @@ api.media(media_id) api.media_popular(count, max_id) api.media_search(q, count, lat, lng, min_timestamp, max_timestamp) ``` - + Comments: http://instagr.am/developer/endpoints/comments/ ``` python @@ -181,7 +189,7 @@ api.media_comments(media_id) api.create_media_comment(media_id, text) api.delete_comment(media_id, comment_id) ``` - + Likes: http://instagr.am/developer/endpoints/likes/ ``` python @@ -189,7 +197,7 @@ api.media_likes(media_id) api.like_media(media_id) api.unlike_media(media_id) ``` - + Tags: http://instagr.am/developer/endpoints/tags/ ``` python @@ -197,7 +205,7 @@ api.tag(tag_name) api.tag_recent_media(count, max_tag_id, tag_name)* api.tag_search(q, count)* ``` - + Locations: http://instagr.am/developer/endpoints/locations/ ``` python @@ -205,15 +213,17 @@ api.location(location_id) api.location_recent_media(count, max_id, location_id)* api.location_search(q, count, lat, lng, foursquare_id, foursquare_v2_id) ``` - + Geographies: http://instagr.am/developer/endpoints/geographies/ ``` python api.geography_recent_media(count, max_id, geography_id)* ``` + Error handling ------- +-------------- + Importing the bind module allows handling of specific error status codes. An example is provided below: ``` python from instagram.bind import InstagramAPIError @@ -225,8 +235,10 @@ 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 @@ -243,26 +255,15 @@ import socket socket.setdefaulttimeout(30) ``` + Trouble Shooting ------- +---------------- If you get an error of a module not being defined during the Instagram import call, this might update a necessary package. ``` sudo pip install --upgrade six ``` -Sample app ------- -This repository includes a one-file sample app that uses the bottle framework and demonstrates -authentication, subscriptions, and update processing. To try it out: - - * Download bottle if you don't already have it: pip install bottle - * Download bottle-session if you don't already have it: pip install bottle-session - * Download and run a redis instance on port 6379 if you don't already have it. Check http://redis.io for instructions. - * Set your redirect URI to 'http://localhost:8515/oauth_callback' in your dev profile - * Open up sample\_app.py, update it with your client\_id and secret, and set redirect URI to 'http://localhost:8515/oauth_callback' - * Run the file; it will host a local server on port 8515. - * Try visiting http://localhost:8515 in your browser Contributing ------------ @@ -307,8 +308,9 @@ Submitting a Pull Request 8. Submit a pull request. 9. If you haven't already, complete the Contributor License Agreement ("CLA"). + Contributor License Agreement ("CLA") -_____________________________________ +------------------------------------- In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Instagram's or Facebook's open source projects. diff --git a/requirements.txt b/requirements.txt index 8f8ead1d..9420d78d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,4 @@ -bottle==0.12.7 httplib2==0.9 -redis==2.10.3 simplejson==3.6.3 -beaker==1.6.4 six==1.8.0 pytz==2015.4 -twine==1.8.1 diff --git a/sample_app.py b/sample_app.py deleted file mode 100644 index 82cc13a9..00000000 --- a/sample_app.py +++ /dev/null @@ -1,272 +0,0 @@ -import bottle -import beaker.middleware -from bottle import route, redirect, post, run, request, hook -from instagram import client, subscriptions - -bottle.debug(True) - -session_opts = { - 'session.type': 'file', - 'session.data_dir': './session/', - 'session.auto': True, -} - -app = beaker.middleware.SessionMiddleware(bottle.app(), session_opts) - -CONFIG = { - 'client_id': '', - 'client_secret': '', - 'redirect_uri': 'http://localhost:8515/oauth_callback' -} - -unauthenticated_api = client.InstagramAPI(**CONFIG) - -@hook('before_request') -def setup_request(): - request.session = request.environ['beaker.session'] - -def process_tag_update(update): - print(update) - -reactor = subscriptions.SubscriptionsReactor() -reactor.register_callback(subscriptions.SubscriptionType.TAG, process_tag_update) - -@route('/') -def home(): - try: - url = unauthenticated_api.get_authorize_url(scope=["likes","comments"]) - return 'Connect with Instagram' % url - except Exception as e: - print(e) - -def get_nav(): - nav_menu = ("

Python Instagram

" - "
    " - "
  • User Recent Media Calls user_recent_media - Get a list of a user's most recent media
  • " - "
  • User Media Feed Calls user_media_feed - Get the currently authenticated user's media feed uses pagination
  • " - "
  • Location Recent Media Calls location_recent_media - Get a list of recent media at a given location, in this case, the Instagram office
  • " - "
  • Media Search Calls media_search - Get a list of media close to a given latitude and longitude
  • " - "
  • Popular Media Calls media_popular - Get a list of the overall most popular media items
  • " - "
  • User Search Calls user_search - Search for users on instagram, by name or username
  • " - "
  • User Follows Get the followers of @instagram uses pagination
  • " - "
  • Location Search Calls location_search - Search for a location by lat/lng
  • " - "
  • Tags Search for tags, view tag info and get media by tag
  • " - "
") - return nav_menu - -@route('/oauth_callback') -def on_callback(): - code = request.GET.get("code") - if not code: - return 'Missing code' - try: - access_token, user_info = unauthenticated_api.exchange_code_for_access_token(code) - if not access_token: - return 'Could not get access token' - api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) - request.session['access_token'] = access_token - except Exception as e: - print(e) - return get_nav() - -@route('/recent') -def on_recent(): - content = "

User Recent Media

" - access_token = request.session['access_token'] - if not access_token: - return 'Missing Access Token' - try: - api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) - recent_media, next = api.user_recent_media() - photos = [] - for media in recent_media: - photos.append('
') - if(media.type == 'video'): - photos.append('' % (media.get_standard_resolution_url())) - else: - photos.append('' % (media.get_low_resolution_url())) - photos.append("
Like Un-Like LikesCount=%s
" % (media.id,media.id,media.like_count)) - content += ''.join(photos) - except Exception as e: - print(e) - return "%s %s
Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) - -@route('/media_like/') -def media_like(id): - access_token = request.session['access_token'] - api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) - api.like_media(media_id=id) - redirect("/recent") - -@route('/media_unlike/') -def media_unlike(id): - access_token = request.session['access_token'] - api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) - api.unlike_media(media_id=id) - redirect("/recent") - -@route('/user_media_feed') -def on_user_media_feed(): - access_token = request.session['access_token'] - content = "

User Media Feed

" - if not access_token: - return 'Missing Access Token' - try: - api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) - media_feed, next = api.user_media_feed() - photos = [] - for media in media_feed: - photos.append('' % media.get_standard_resolution_url()) - counter = 1 - while next and counter < 3: - media_feed, next = api.user_media_feed(with_next_url=next) - for media in media_feed: - photos.append('' % media.get_standard_resolution_url()) - counter += 1 - content += ''.join(photos) - except Exception as e: - print(e) - return "%s %s
Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) - -@route('/location_recent_media') -def location_recent_media(): - access_token = request.session['access_token'] - content = "

Location Recent Media

" - if not access_token: - return 'Missing Access Token' - try: - api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) - recent_media, next = api.location_recent_media(location_id=514276) - photos = [] - for media in recent_media: - photos.append('' % media.get_standard_resolution_url()) - content += ''.join(photos) - except Exception as e: - print(e) - return "%s %s
Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) - -@route('/media_search') -def media_search(): - access_token = request.session['access_token'] - content = "

Media Search

" - if not access_token: - return 'Missing Access Token' - try: - api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) - media_search = api.media_search(lat="37.7808851",lng="-122.3948632",distance=1000) - photos = [] - for media in media_search: - photos.append('' % media.get_standard_resolution_url()) - content += ''.join(photos) - except Exception as e: - print(e) - return "%s %s
Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) - -@route('/media_popular') -def media_popular(): - access_token = request.session['access_token'] - content = "

Popular Media

" - if not access_token: - return 'Missing Access Token' - try: - api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) - media_search = api.media_popular() - photos = [] - for media in media_search: - photos.append('' % media.get_standard_resolution_url()) - content += ''.join(photos) - except Exception as e: - print(e) - return "%s %s
Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) - -@route('/user_search') -def user_search(): - access_token = request.session['access_token'] - content = "

User Search

" - if not access_token: - return 'Missing Access Token' - try: - api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) - user_search = api.user_search(q="Instagram") - users = [] - for user in user_search: - users.append('
  • %s
  • ' % (user.profile_picture,user.username)) - content += ''.join(users) - except Exception as e: - print(e) - return "%s %s
    Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) - -@route('/user_follows') -def user_follows(): - access_token = request.session['access_token'] - content = "

    User Follows

    " - if not access_token: - return 'Missing Access Token' - try: - api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) - # 25025320 is http://instagram.com/instagram - user_follows, next = api.user_follows('25025320') - users = [] - for user in user_follows: - users.append('
  • %s
  • ' % (user.profile_picture,user.username)) - while next: - user_follows, next = api.user_follows(with_next_url=next) - for user in user_follows: - users.append('
  • %s
  • ' % (user.profile_picture,user.username)) - content += ''.join(users) - except Exception as e: - print(e) - return "%s %s
    Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) - -@route('/location_search') -def location_search(): - access_token = request.session['access_token'] - content = "

    Location Search

    " - if not access_token: - return 'Missing Access Token' - try: - api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) - location_search = api.location_search(lat="37.7808851",lng="-122.3948632",distance=1000) - locations = [] - for location in location_search: - locations.append('
  • %s Map
  • ' % (location.name,location.point.latitude,location.point.longitude)) - content += ''.join(locations) - except Exception as e: - print(e) - return "%s %s
    Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) - -@route('/tag_search') -def tag_search(): - access_token = request.session['access_token'] - content = "

    Tag Search

    " - if not access_token: - return 'Missing Access Token' - try: - api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) - tag_search, next_tag = api.tag_search(q="backclimateaction") - tag_recent_media, next = api.tag_recent_media(tag_name=tag_search[0].name) - photos = [] - for tag_media in tag_recent_media: - photos.append('' % tag_media.get_standard_resolution_url()) - content += ''.join(photos) - except Exception as e: - print(e) - return "%s %s
    Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) - -@route('/realtime_callback') -@post('/realtime_callback') -def on_realtime_callback(): - mode = request.GET.get("hub.mode") - challenge = request.GET.get("hub.challenge") - verify_token = request.GET.get("hub.verify_token") - if challenge: - return challenge - else: - x_hub_signature = request.header.get('X-Hub-Signature') - raw_response = request.body.read() - try: - reactor.process(CONFIG['client_secret'], raw_response, x_hub_signature) - except subscriptions.SubscriptionVerifyError: - print("Signature mismatch") - -bottle.run(app=app, host='localhost', port=8515, reloader=True) From a81c8958d5418c13c950bb02fb4ea41919296012 Mon Sep 17 00:00:00 2001 From: wkoot Date: Thu, 5 Oct 2017 16:18:09 +0200 Subject: [PATCH 47/54] Skip running python-codacy-coverage for python2.6 --- .travis.yml | 4 +++- README.md | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e166351c..23bec80d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,4 +15,6 @@ install: script: - "coverage run tests.py" - "coverage xml -o coverage.xml" - - "python-codacy-coverage -r coverage.xml" + - "if [[ $TRAVIS_PYTHON_VERSION != 2.6 ]]; then + python-codacy-coverage -r coverage.xml + fi" diff --git a/README.md b/README.md index 0993d37c..580e6072 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ - - - [![Build Status](https://api.travis-ci.org/wkoot/python-instagram.svg)](https://travis-ci.org/wkoot/python-instagram) +[![Codacy Badge](https://api.codacy.com/project/badge/Grade/6dd0431b8752421d95521ea59fc6ecc1)](https://www.codacy.com/app/wkoot/python-instagram) python-instagram From a0399b6f37fad3907682af8e1015874e41654242 Mon Sep 17 00:00:00 2001 From: wkoot Date: Thu, 5 Oct 2017 16:23:52 +0200 Subject: [PATCH 48/54] Add semicolon in travis.yml as the if statement seems to be concatenated to a single line anyway --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 23bec80d..20f942b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,5 +16,5 @@ script: - "coverage run tests.py" - "coverage xml -o coverage.xml" - "if [[ $TRAVIS_PYTHON_VERSION != 2.6 ]]; then - python-codacy-coverage -r coverage.xml + python-codacy-coverage -r coverage.xml; fi" From a8549a6c61533412d566facdf9f5aa9baf751468 Mon Sep 17 00:00:00 2001 From: wkoot Date: Thu, 5 Oct 2017 16:36:14 +0200 Subject: [PATCH 49/54] Add code coverage badge --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 580e6072..79af5b0f 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,12 @@ - - - [![Build Status](https://api.travis-ci.org/wkoot/python-instagram.svg)](https://travis-ci.org/wkoot/python-instagram) +[![Code Coverage](https://api.codacy.com/project/badge/coverage/6dd0431b8752421d95521ea59fc6ecc1)](https://www.codacy.com/app/wkoot/python-instagram) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/6dd0431b8752421d95521ea59fc6ecc1)](https://www.codacy.com/app/wkoot/python-instagram) python-instagram -====== +================ A Python 2/3 client for the Instagram REST and Search APIs From deb5882eb0bd6da93b6c5e328ffc1b7020da3da1 Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 9 Oct 2017 14:35:50 +0200 Subject: [PATCH 50/54] Add coveralls to build --- .travis.yml | 2 ++ build_requirements.txt | 1 + 2 files changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 20f942b7..a5196081 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,8 @@ install: script: - "coverage run tests.py" - "coverage xml -o coverage.xml" +after_success: + - "coveralls" - "if [[ $TRAVIS_PYTHON_VERSION != 2.6 ]]; then python-codacy-coverage -r coverage.xml; fi" diff --git a/build_requirements.txt b/build_requirements.txt index 35a0868f..ff4cee22 100644 --- a/build_requirements.txt +++ b/build_requirements.txt @@ -1,3 +1,4 @@ twine==1.8.1 coverage==4.4.1 +coveralls==1.2.0 codacy-coverage==1.3.3 From 533e33cbb01cfcf19581b77f418d49c9a7dfe327 Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 9 Oct 2017 14:49:21 +0200 Subject: [PATCH 51/54] Slap down coveragerc to direct coverage to instagram package only --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..85bedc84 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[run] +source = instagram +branch = True From c9f52434d647d737c6907e53858eff94b74fc18d Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 9 Oct 2017 14:53:10 +0200 Subject: [PATCH 52/54] Current test logic is not aimed at testing code branching; better disable but leave in coveragerc --- .coveragerc | 2 +- README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 85bedc84..331b8f3b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,3 @@ [run] source = instagram -branch = True +branch = False diff --git a/README.md b/README.md index 79af5b0f..2a85d2ac 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![Build Status](https://api.travis-ci.org/wkoot/python-instagram.svg)](https://travis-ci.org/wkoot/python-instagram) [![Code Coverage](https://api.codacy.com/project/badge/coverage/6dd0431b8752421d95521ea59fc6ecc1)](https://www.codacy.com/app/wkoot/python-instagram) +[![Coverage Status](https://coveralls.io/repos/github/wkoot/python-instagram/badge.svg)](https://coveralls.io/github/wkoot/python-instagram) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/6dd0431b8752421d95521ea59fc6ecc1)](https://www.codacy.com/app/wkoot/python-instagram) From 80f62a857ff9ade105425324c920423781fa3a8f Mon Sep 17 00:00:00 2001 From: wkoot Date: Mon, 9 Oct 2017 15:40:33 +0200 Subject: [PATCH 53/54] Much awesome so badge --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2a85d2ac..f5a7f339 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ - - - [![Build Status](https://api.travis-ci.org/wkoot/python-instagram.svg)](https://travis-ci.org/wkoot/python-instagram) -[![Code Coverage](https://api.codacy.com/project/badge/coverage/6dd0431b8752421d95521ea59fc6ecc1)](https://www.codacy.com/app/wkoot/python-instagram) -[![Coverage Status](https://coveralls.io/repos/github/wkoot/python-instagram/badge.svg)](https://coveralls.io/github/wkoot/python-instagram) +[![Coveralls Coverage](https://coveralls.io/repos/github/wkoot/python-instagram/badge.svg)](https://coveralls.io/github/wkoot/python-instagram) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/6dd0431b8752421d95521ea59fc6ecc1)](https://www.codacy.com/app/wkoot/python-instagram) +[![Code Climate](https://img.shields.io/codeclimate/github/wkoot/python-instagram.svg)](https://codeclimate.com/github/wkoot/python-instagram) python-instagram From 475f4b747ed10d88e72bd4908646b45dc18e58f8 Mon Sep 17 00:00:00 2001 From: wkoot Date: Fri, 11 Jan 2019 10:37:58 +0100 Subject: [PATCH 54/54] Remove old python versions from .travis.yml Python 2.6 and 3.3 have reached end-of-life --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a5196081..4fedae27 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: python python: - - "2.6" - "2.7" - - "3.3" - "3.4" - "3.5" - "3.6"