├── tests ├── __init__.py ├── images │ └── africa.jpg ├── base.py ├── collection.py ├── blog.py ├── upload.py ├── oauth.py ├── user.py └── photo.py ├── .gitignore ├── fivehundredpx ├── settings.py ├── errors.py ├── __init__.py ├── utils.py ├── auth.py ├── client.py ├── bind.py └── oauth.py ├── setup.py └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | *.swp 4 | python_500px.egg-info/ 5 | -------------------------------------------------------------------------------- /tests/images/africa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akirahrkw/python-500px/HEAD/tests/images/africa.jpg -------------------------------------------------------------------------------- /fivehundredpx/settings.py: -------------------------------------------------------------------------------- 1 | API_HOST = 'api.500px.com' 2 | API_VERSION = '/v1' 3 | OAUTH_ROOT = '/oauth/' 4 | RETRY_COUNT = 1 5 | RETRY_DELAY = 0 6 | ALLOWED_FILE_TYPES = ['image/jpeg'] -------------------------------------------------------------------------------- /fivehundredpx/errors.py: -------------------------------------------------------------------------------- 1 | class FiveHundredClientError(Exception): 2 | 3 | def __init__(self,error_message,status=None): 4 | self.error_message = error_message 5 | self.status = status 6 | 7 | def __str__(self): 8 | return self.error_message 9 | -------------------------------------------------------------------------------- /fivehundredpx/__init__.py: -------------------------------------------------------------------------------- 1 | from fivehundredpx.client import FiveHundredPXAPI 2 | from fivehundredpx.auth import * 3 | from fivehundredpx.errors import * 4 | 5 | """ 6 | 500px API library 7 | inspired by tweepy(https://github.com/tweepy/tweepy), python-instagram(https://github.com/Instagram/python-instagram) 8 | """ 9 | __version__ = '0.3.0' 10 | __author__ = 'Akira Hirakawa' 11 | __license__ = 'MIT' 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | 4 | setup(name="python-500px", 5 | version="0.3.0", 6 | description="500PX API client", 7 | license="MIT", 8 | install_requires=["simplejson"], 9 | author="Akira Hirakawa", 10 | author_email="akirahrkw@gmail.com", 11 | url="https://github.com/akirahrkw/python-500px", 12 | packages = find_packages(), 13 | keywords= "500px", 14 | zip_safe = True) 15 | -------------------------------------------------------------------------------- /tests/base.py: -------------------------------------------------------------------------------- 1 | import mimetypes, httplib, time, sys, os 2 | import unittest 3 | 4 | path = os.path.abspath(os.path.join(os.path.dirname(__file__),"..")) 5 | sys.path.append(path) 6 | 7 | from fivehundredpx.client import FiveHundredPXAPI 8 | from fivehundredpx.auth import * 9 | from fivehundredpx.errors import FiveHundredClientError 10 | 11 | class BaseTestCase(unittest.TestCase): 12 | 13 | def setUp(self): 14 | super(BaseTestCase, self).setUp() 15 | self.handler = OAuthHandler(self.consumer_key, self.consumer_secret) 16 | self.handler.set_access_token(self.oauth_token, self.oauth_token_secret) 17 | self.api = FiveHundredPXAPI(auth_handler=self.handler) 18 | self.unauthorized_api = FiveHundredPXAPI() 19 | self.follower_id = '925306' # test user id 20 | self.user_id = '727199' # this is akirahrkw's id 21 | self.photo = None # sample photo for test 22 | 23 | def tearDown(self): 24 | pass 25 | 26 | def _get_sample_photo(self): 27 | if not self.photo: 28 | json = self.api.photos(require_auth=True, feature='user', user_id=self.follower_id, rpp=1) 29 | self.assertIsNotNone(json) 30 | self.photo = json['photos'][0] 31 | return self.photo -------------------------------------------------------------------------------- /tests/collection.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | 3 | if len(sys.argv) < 5: 4 | print 'Usage: # python %s consumer_key consumer_secret request_token request_token_secret' % sys.argv[0] 5 | quit() 6 | 7 | CONSUMER_KEY = str(sys.argv[1]) 8 | CONSUMER_SECRET = str(sys.argv[2]) 9 | OAUTH_TOKEN = str(sys.argv[3]) 10 | OAUTH_TOKEN_SECRET = str(sys.argv[4]) 11 | 12 | # https://github.com/500px/api-documentation/tree/master/endpoints/collections 13 | class CollectionTestCase(BaseTestCase): 14 | 15 | def setUp(self): 16 | self.consumer_key = CONSUMER_KEY 17 | self.consumer_secret = CONSUMER_SECRET 18 | self.oauth_token = OAUTH_TOKEN 19 | self.oauth_token_secret = OAUTH_TOKEN_SECRET 20 | super(CollectionTestCase, self).setUp() 21 | 22 | def test_collections(self): 23 | json = self.api.collections() 24 | self.assertIsNotNone(json) 25 | 26 | def test_collection(self): 27 | try: 28 | json = self.api.collections_post(title="Test Title", path='test') 29 | self.assertIsNotNone(json) 30 | json = self.api.collections_id(id=json['id']) 31 | self.assertIsNotNone(json) 32 | json = self.api.collections_update(id=json['id'], title="Test Title2", path='test2') 33 | self.assertIsNotNone(json) 34 | json = self.api.collections_delete(id=json['id']) 35 | self.assertIsNotNone(json) 36 | except FiveHundredClientError as e: 37 | if not e.status == 403: 38 | raise e 39 | 40 | if __name__ == '__main__': 41 | del sys.argv[1:] 42 | unittest.main() -------------------------------------------------------------------------------- /tests/blog.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | 3 | if len(sys.argv) < 5: 4 | print 'Usage: # python %s consumer_key consumer_secret request_token request_token_secret' % sys.argv[0] 5 | quit() 6 | 7 | CONSUMER_KEY = str(sys.argv[1]) 8 | CONSUMER_SECRET = str(sys.argv[2]) 9 | OAUTH_TOKEN = str(sys.argv[3]) 10 | OAUTH_TOKEN_SECRET = str(sys.argv[4]) 11 | 12 | # https://github.com/500px/api-documentation/tree/master/endpoints/blog 13 | class BlogTestCase(BaseTestCase): 14 | 15 | def setUp(self): 16 | self.consumer_key = CONSUMER_KEY 17 | self.consumer_secret = CONSUMER_SECRET 18 | self.oauth_token = OAUTH_TOKEN 19 | self.oauth_token_secret = OAUTH_TOKEN_SECRET 20 | super(BlogTestCase, self).setUp() 21 | 22 | def test_blogs(self): 23 | json = self.api.blogs(require_auth=True,rpp=1,page=1) 24 | self.assertIsNotNone(json) 25 | for blog in json['blog_posts']: 26 | json = self.api.blogs_id(require_auth=True, id=blog['id']) 27 | self.assertIsNotNone(json) 28 | json = self.api.blogs_comments(require_auth=True, id=blog['id']) 29 | self.assertIsNotNone(json) 30 | 31 | # def test_blog(self): 32 | # json = self.api.blogs_post(title='title test', body='body test') 33 | # self.assertIsNotNone(json) 34 | # 35 | # json = self.api.blogs_update(id=json['id'], title='title test 2', body='body test 2') 36 | # self.assertIsNotNone(json) 37 | # 38 | # json_comment = self.api.blogs_comments_post(id=json['id'], body='test comment') 39 | # self.assertIsNotNone(json_comment) 40 | # 41 | # blogs = self.api.blogs_comments(id=json['id']) 42 | # self.assertIsNotNone(blogs) 43 | # 44 | # json_comment = self.api.comments_post(id=json_comment['comment']['id'], body='reply test comment') 45 | # self.assertIsNotNone(json_comment) 46 | # 47 | # json = self.api.blogs_delete(id=json['id']) 48 | # self.assertIsNotNone(json) 49 | 50 | if __name__ == '__main__': 51 | del sys.argv[1:] 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /tests/upload.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | 3 | if len(sys.argv) < 5: 4 | print 'Usage: # python %s consumer_key consumer_secret request_token request_token_secret' % sys.argv[0] 5 | quit() 6 | 7 | CONSUMER_KEY = str(sys.argv[1]) 8 | CONSUMER_SECRET = str(sys.argv[2]) 9 | OAUTH_TOKEN = str(sys.argv[3]) 10 | OAUTH_TOKEN_SECRET = str(sys.argv[4]) 11 | 12 | # https://github.com/500px/api-documentation/blob/master/endpoints/photo/POST_photos.md 13 | # https://github.com/500px/api-documentation/blob/master/endpoints/upload/POST_upload.md 14 | class UploadTestCase(BaseTestCase): 15 | 16 | def setUp(self): 17 | self.consumer_key = CONSUMER_KEY 18 | self.consumer_secret = CONSUMER_SECRET 19 | self.oauth_token = OAUTH_TOKEN 20 | self.oauth_token_secret = OAUTH_TOKEN_SECRET 21 | path = os.path.abspath(os.path.join(os.path.dirname(__file__),".")) 22 | self.filepath = path + '/images/africa.jpg' 23 | super(UploadTestCase, self).setUp() 24 | 25 | def test_photo_upload_path(self): 26 | 27 | json = self.api.photos_post(name='test photo',description='test description') 28 | self.assertIsNotNone(json) 29 | 30 | photo_id = json['photo']['id'] 31 | upload_key = str(json['upload_key']) 32 | 33 | json = self.api.upload_photo( 34 | photo_id=photo_id, 35 | filename=self.filepath, 36 | consumer_key=self.consumer_key, 37 | upload_key=upload_key, 38 | access_key=self.oauth_token 39 | ) 40 | self.assertIsNotNone(json) 41 | time.sleep(2) 42 | 43 | json = self.api.photos_update(id=photo_id,name='test photo 2',description='test description 2', privacy="0") 44 | self.assertIsNotNone(json) 45 | 46 | time.sleep(10) 47 | json = self.api.photos_delete(id=photo_id) 48 | self.assertIsNotNone(json) 49 | 50 | def test_photo_upload_binary(self): 51 | json = self.api.photos_post() 52 | self.assertIsNotNone(json) 53 | 54 | photo_id = json['photo']['id'] 55 | upload_key = str(json['upload_key']) 56 | file_type = mimetypes.guess_type(self.filepath) 57 | 58 | fp = open(self.filepath,'rb') 59 | 60 | json = self.api.upload_photo( 61 | photo_id=photo_id, 62 | fp=fp, 63 | file_type=file_type[0], 64 | consumer_key=self.consumer_key, 65 | upload_key=upload_key, 66 | access_key=self.oauth_token 67 | ) 68 | fp.close() 69 | self.assertIsNotNone(json) 70 | time.sleep(10) 71 | self.api.photos_delete(id=photo_id) 72 | 73 | if __name__ == '__main__': 74 | del sys.argv[1:] 75 | unittest.main() -------------------------------------------------------------------------------- /fivehundredpx/utils.py: -------------------------------------------------------------------------------- 1 | from fivehundredpx.settings import * 2 | from fivehundredpx.errors import * 3 | import os, mimetypes 4 | import urllib 5 | 6 | class Util(object): 7 | @staticmethod 8 | def replace_space(value): 9 | return str(value).replace(" ", "+") 10 | 11 | @staticmethod 12 | def encode_string(value): 13 | return value.encode('utf-8') if isinstance(value,unicode) else str(value) 14 | 15 | class FileUtil(object): 16 | @staticmethod 17 | def create_body_by_filepath(filepath,name,parameters): 18 | file_type = mimetypes.guess_type(filepath) 19 | if file_type[0] not in ALLOWED_FILE_TYPES: 20 | raise FiveHundredsClientError('Invalid file type for image: %s' % file_type[0]) 21 | 22 | fp = open(filepath,'rb') 23 | headers,body = FileUtil._create_body(fp,name,file_type[0],parameters) 24 | fp.close() 25 | return headers,body 26 | 27 | @staticmethod 28 | def create_body_by_fp(fp,name,file_type,parameters): 29 | if file_type not in ALLOWED_FILE_TYPES: 30 | raise FiveHundredsClientError('Invalid file type for image: %s' % file_type) 31 | return FileUtil._create_body(fp, name, file_type, parameters) 32 | 33 | @staticmethod 34 | def _create_body(fp,name,filetype,parameters): 35 | BOUNDARY = 'fsdeklzzpo4oopsp' 36 | body = [] 37 | body.append('--' + BOUNDARY) 38 | for key,value in parameters.iteritems(): 39 | body.append('Content-Disposition: form-data; name="%s"' % key ) 40 | body.append('') 41 | body.append(str(value)) 42 | body.append('--' + BOUNDARY) 43 | body.append('') 44 | 45 | body.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (name,name) ) 46 | body.append('Content-Type: %s' % filetype) 47 | body.append('') 48 | body.append(fp.read()) 49 | body.append('--' + BOUNDARY + '--') 50 | body.append('') 51 | body = '\r\n'.join(body) 52 | 53 | headers = { 54 | 'Content-Type' : 'multipart/form-data; boundary=%s' % BOUNDARY, 55 | 'Content-Length' : str(len(body)) 56 | } 57 | return headers,body 58 | 59 | @staticmethod 60 | def create_body(parameters): 61 | BOUNDARY = 'fsdeklzzpo4oopsp' 62 | body = [] 63 | for key,value in parameters.iteritems(): 64 | body.append('--' + BOUNDARY) 65 | body.append('Content-Disposition: form-data; name="%s"' % key ) 66 | body.append('') 67 | body.append(str(value)) 68 | body.append('') 69 | 70 | body.append('--' + BOUNDARY + '--') 71 | body = '\r\n'.join(body) 72 | headers = { 73 | 'Content-Type' : 'multipart/form-data; boundary=%s' % BOUNDARY, 74 | 'Content-Length' : str(len(body)) 75 | } 76 | return headers,body -------------------------------------------------------------------------------- /tests/oauth.py: -------------------------------------------------------------------------------- 1 | import mimetypes, httplib, time, sys, os 2 | import unittest 3 | 4 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__),".."))) 5 | 6 | from fivehundredpx.client import FiveHundredPXAPI 7 | from fivehundredpx.auth import * 8 | 9 | if len(sys.argv) < 3: 10 | print 'Usage: # python %s consumer_key consumer_secret' % sys.argv[0] 11 | quit() 12 | 13 | CONSUMER_KEY = str(sys.argv[1]) 14 | CONSUMER_SECRET = str(sys.argv[2]) 15 | 16 | class AuthTestCase(unittest.TestCase): 17 | def setUp(self): 18 | super(AuthTestCase, self).setUp() 19 | self.handler = OAuthHandler(CONSUMER_KEY,CONSUMER_SECRET) 20 | 21 | def test_auth_request_token(self): 22 | print 'test_auth_request_token...\n' 23 | headers = {} 24 | self.handler.apply_auth('https://api.500px.com/v1/oauth/request_token', 'POST', headers, { 'oauth_callback' : 'http://localhost' }) 25 | conn = httplib.HTTPSConnection('api.500px.com') 26 | conn.request('POST', 'https://api.500px.com/v1/oauth/request_token', headers=headers) 27 | response = conn.getresponse() 28 | self.assert_( response.status == 200 ) 29 | result = response.read() 30 | conn.close() 31 | print "request token: %s\n" % result 32 | 33 | def test_authorization_url_with_verifier(self): 34 | print 'test_authorization_url_with_verifier...\n' 35 | print "Please visit this endpoint and authorize at:\n%s\n" % self.handler.get_authorization_url() 36 | verifier = raw_input("Paste received oauth_verifier (blank to exit): ").strip() 37 | if not verifier: 38 | return 39 | 40 | token = self.handler.get_access_token(verifier) 41 | print "access token: %s\n" % token.key 42 | print "access token secret: %s\n" % token.secret 43 | 44 | def test_xauth(self): 45 | print 'test_xauth...\n' 46 | token = self.handler.get_request_token() 47 | print "request token: %s\n" % token.key 48 | print "request token secret: %s\n" % token.secret 49 | 50 | request_token = raw_input("Input request token (blank to exit): ").strip() 51 | if not request_token: 52 | return 53 | 54 | request_token_secret = raw_input("Input request token secret (blank to exit): ").strip() 55 | if not request_token_secret: 56 | return 57 | 58 | self.handler.set_request_token(request_token,request_token_secret) 59 | 60 | username = raw_input("Input your username (blank to exit): ").strip() 61 | if not username: 62 | return 63 | 64 | password = raw_input("Input your password (blank to exit): ").strip() 65 | if not password: 66 | return 67 | 68 | token = self.handler.get_xauth_access_token(username,password) 69 | print "access token: %s\n" % token.key 70 | print "access token secret: %s\n" % token.secret 71 | 72 | if __name__ == '__main__': 73 | del sys.argv[1:] 74 | unittest.main() -------------------------------------------------------------------------------- /tests/user.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | 3 | if len(sys.argv) < 5: 4 | print 'Usage: # python %s consumer_key consumer_secret request_token request_token_secret' % sys.argv[0] 5 | quit() 6 | 7 | CONSUMER_KEY = str(sys.argv[1]) 8 | CONSUMER_SECRET = str(sys.argv[2]) 9 | OAUTH_TOKEN = str(sys.argv[3]) 10 | OAUTH_TOKEN_SECRET = str(sys.argv[4]) 11 | 12 | # https://github.com/500px/api-documentation/tree/master/endpoints/user 13 | class UserTestCase(BaseTestCase): 14 | 15 | def setUp(self): 16 | self.consumer_key = CONSUMER_KEY 17 | self.consumer_secret = CONSUMER_SECRET 18 | self.oauth_token = OAUTH_TOKEN 19 | self.oauth_token_secret = OAUTH_TOKEN_SECRET 20 | super(UserTestCase, self).setUp() 21 | 22 | def test_users(self): 23 | json = self.api.users() 24 | self.assertIsNotNone(json) 25 | 26 | def test_users_show(self): 27 | json = self.unauthorized_api.users_show(consumer_key=self.consumer_key, id='727199') 28 | self.assertIsNotNone(json) 29 | json = self.unauthorized_api.users_show(consumer_key=self.consumer_key, username='akirahrkw') 30 | self.assertIsNotNone(json) 31 | 32 | with self.assertRaises(FiveHundredClientError): 33 | self.unauthorized_api.users_show(consumer_key=self.consumer_key, id='0') 34 | 35 | json = self.api.users_show(require_auth=True,id='727199') 36 | self.assertIsNotNone(json) 37 | users = self.api.users_show(require_auth=True,username='akirahrkw') 38 | self.assertIsNotNone(json) 39 | 40 | def test_user_friends_a_get(self): 41 | json = self.unauthorized_api.users_friends(consumer_key=self.consumer_key, id=self.user_id, rpp=5, page=2) 42 | self.assertIsNotNone(json) 43 | self.assertEqual(json["friends"].__class__, dict) 44 | 45 | json = self.api.users_friends(require_auth=True, id=self.user_id, rpp=5, page=2) 46 | self.assertIsNotNone(json) 47 | self.assertEqual(json["friends"].__class__, dict) 48 | 49 | def test_user_friends_b_post(self): 50 | json = self.api.users_friends_post(require_auth=True,id=self.follower_id) 51 | self.assertIsNotNone(json) 52 | 53 | def test_user_friends_c_delete(self): 54 | json = self.api.users_friends_delete(require_auth=True,id=self.follower_id) 55 | self.assertIsNotNone(json) 56 | 57 | def test_user_followers(self): 58 | json = self.unauthorized_api.users_followers(consumer_key=self.consumer_key,id=self.user_id,rpp=5,page=2) 59 | self.assertIsNotNone(json) 60 | self.assertIsNotNone(json['followers_count']) 61 | self.assertIsNotNone(json['followers']) 62 | 63 | json = self.api.users_followers(require_auth=True,id=self.user_id,rpp=5,page=2) 64 | self.assertIsNotNone(json) 65 | self.assertIsNotNone(json['followers_count']) 66 | self.assertIsNotNone(json['followers']) 67 | 68 | def test_user_followers(self): 69 | json = self.unauthorized_api.users_search(consumer_key=self.consumer_key,term="akira") 70 | self.assertIsNotNone(json) 71 | self.assertIsNotNone(json['total_items']) 72 | self.assertIsNotNone(json['users']) 73 | 74 | if __name__ == '__main__': 75 | del sys.argv[1:] 76 | unittest.main() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-500px 2 | 3 | ### Note: 4 | *The client doesn't work post June 15th 2018, due to [500px shutting down access to its API](https://support.500px.com/hc/en-us/articles/360002435653-API-)* 5 | 6 | A Python client for the [500px API](https://github.com/500px/api-documentation). 7 | 8 | this library was inspired by [tweepy](https://github.com/tweepy/tweepy) and [python-instagram](https://github.com/Instagram/python-instagram) 9 | 10 | *** 11 | 12 | ## Installation 13 | pip install python-500px 14 | 15 | ## Requires 16 | * simplejson 17 | 18 | ## Usage 19 | 20 | ```python 21 | from fivehundredpx.client import FiveHundredPXAPI 22 | from fivehundredpx.auth import * 23 | 24 | unauthorized_api = FiveHundredPXAPI(handler) 25 | unauthorized_api.users_show(consumer_key=CONSUMER_KEY, id='727199') 26 | 27 | handler = OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET) 28 | handler.set_access_token(OAUTH_TOKEN, OAUTH_TOKEN_SECRET) 29 | api = FiveHundredPXAPI(handler) 30 | api.users() 31 | ``` 32 | 33 | ## Authentication 34 | 35 | Please check 500px's [authentication document](https://github.com/500px/api-documentation/tree/master/authentication). `tests/oauth.py` shows how to get request/access token. 36 | 37 | ```python 38 | # verifier: 39 | self.handler.get_authorization_url() # go to this url and get verifier 40 | token = self.handler.get_access_token(verifier) 41 | token.key, token.secret 42 | 43 | # xauth: 44 | token = self.handler.get_request_token() 45 | self.handler.set_request_token(token.key, token.secret) 46 | token = self.handler.get_xauth_access_token(username, password) 47 | token.key, token.secret 48 | ``` 49 | 50 | ## Methods 51 | 52 | * api.photos() 53 | * api.photos_search() 54 | * api.photos_id() 55 | * api.photos_post() 56 | * api.photos_update() 57 | * api.photos_delete() 58 | * api.photos_comments() 59 | * api.photos_comments_post() 60 | * api.photos_favorites() 61 | * api.photos_favorite_post() 62 | * api.photos_favorite_delete() 63 | * api.photos_tags_post() 64 | * api.photos_tags_delete() 65 | * api.photos_votes() 66 | * api.photos_vote_post() 67 | * api.upload_photo() 68 | * api.users() 69 | * api.users_show() 70 | * api.users_search() 71 | * api.users_friends() 72 | * api.users_followers() 73 | * api.users_friends_post() 74 | * api.users_friends_delete() 75 | * api.blogs() 76 | * api.blogs_id() 77 | * api.blogs_comments() 78 | * api.comments_post() 79 | * api.collections() 80 | * api.collections_id() 81 | * api.collections_post() 82 | * api.collections_update() 83 | * api.collections_delete() 84 | 85 | ## Test 86 | python tests/oauth.py [cunsumer_key] [consumer_secret] 87 | python tests/blog.py [cunsumer_key] [consumer_secret] [oauth_token] [oauth_token_secret] 88 | python tests/collection.py [cunsumer_key] [consumer_secret] [oauth_token] [oauth_token_secret] 89 | python tests/user.py [cunsumer_key] [consumer_secret] [oauth_token] [oauth_token_secret] 90 | python tests/photo.py [cunsumer_key] [consumer_secret] [oauth_token] [oauth_token_secret] 91 | python tests/upload.py [cunsumer_key] [consumer_secret] [oauth_token] [oauth_token_secret] 92 | 93 | [authentication]: https://github.com/500px/api-documentation/tree/master/authentication 94 | [authorize]: https://github.com/500px/api-documentation/blob/master/authentication/POST_oauth_authorize.md 95 | [request_token]: https://github.com/500px/api-documentation/blob/master/authentication/POST_oauth_requesttoken.md 96 | [access_token]: https://github.com/500px/api-documentation/blob/master/authentication/POST_oauth_accesstoken.md 97 | -------------------------------------------------------------------------------- /tests/photo.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | import datetime 3 | 4 | if len(sys.argv) < 5: 5 | print 'Usage: # python %s consumer_key consumer_secret request_token request_token_secret' % sys.argv[0] 6 | quit() 7 | 8 | CONSUMER_KEY = str(sys.argv[1]) 9 | CONSUMER_SECRET = str(sys.argv[2]) 10 | OAUTH_TOKEN = str(sys.argv[3]) 11 | OAUTH_TOKEN_SECRET = str(sys.argv[4]) 12 | 13 | # https://github.com/500px/api-documentation/tree/master/endpoints/photo 14 | class PhotoTestCase(BaseTestCase): 15 | 16 | def setUp(self): 17 | self.consumer_key = CONSUMER_KEY 18 | self.consumer_secret = CONSUMER_SECRET 19 | self.oauth_token = OAUTH_TOKEN 20 | self.oauth_token_secret = OAUTH_TOKEN_SECRET 21 | super(PhotoTestCase, self).setUp() 22 | 23 | def test_photos(self): 24 | json = self.unauthorized_api.photos(feature='popular',consumer_key=self.consumer_key) 25 | self.assertIsNotNone(json) 26 | 27 | json = self.api.photos(require_auth=True, feature='popular',rpp=1, image_size=[2,3]) 28 | self.assertIsNotNone(json) 29 | self.assertTrue(len(json['photos']) != 0) 30 | 31 | for photo in json['photos']: 32 | photo_id = photo['id'] 33 | json = self.api.photos_id(require_auth=True, id=photo_id) 34 | self.assertIsNotNone(json) 35 | json = self.api.photos_comments(require_auth=True, id=photo_id) 36 | self.assertIsNotNone(json) 37 | json = self.api.photos_favorites(require_auth=True, id=photo_id) 38 | self.assertIsNotNone(json) 39 | json = self.api.photos_votes(require_auth=True, id=photo_id) 40 | self.assertIsNotNone(json) 41 | 42 | def test_photos_search(self): 43 | json = self.api.photos_search(term='test',consumer_key=self.consumer_key) 44 | self.assertIsNotNone(json) 45 | 46 | def test_photos_comments_post(self): 47 | photo = self._get_sample_photo() 48 | # to prevent 403 error 49 | i = datetime.datetime.now() 50 | json = self.api.photos_comments_post(id=photo['id'], body='this is akira' + str(i)) 51 | self.assertIsNotNone(json) 52 | 53 | def test_photo_vote(self): 54 | photo = self._get_sample_photo() 55 | try: 56 | self.api.photos_vote_post(id=photo['id'], vote="1") 57 | except FiveHundredClientError as e: 58 | if not e.status == 403: 59 | raise e 60 | 61 | def test_photo_favirite(self): 62 | photo = self._get_sample_photo() 63 | json = self.api.photos_favorite_post(id=photo['id']) 64 | self.assertIsNotNone(json) 65 | json = self.api.photos_favorite_delete(id=photo['id']) 66 | self.assertIsNotNone(json) 67 | 68 | def test_photos_tags_post(self): 69 | photo = self._get_sample_photo() 70 | json = self.api.photos_tags_post(id=photo['id'], tags="test,test2") 71 | self.assertIsNotNone(json) 72 | 73 | def test_photos_tags_delete(self): 74 | photo = self._get_sample_photo() 75 | json = self.api.photos_tags_delete(id=photo['id'], tags="test,test2") 76 | self.assertIsNotNone(json) 77 | 78 | def test_photos_report(self): 79 | photo = self._get_sample_photo() 80 | json = self.api.photos_report(id=photo['id'], reason="1") 81 | self.assertIsNotNone(json) 82 | 83 | def test_photos_generator(self): 84 | gen = self.api.photos(require_auth=True, feature='popular',rpp=1, as_generator=True) 85 | for json in gen: self.assertIsNotNone(json) 86 | 87 | if __name__ == '__main__': 88 | del sys.argv[1:] 89 | unittest.main() -------------------------------------------------------------------------------- /fivehundredpx/auth.py: -------------------------------------------------------------------------------- 1 | from urllib2 import Request, urlopen 2 | 3 | from fivehundredpx import oauth 4 | from fivehundredpx.settings import * 5 | from fivehundredpx.errors import * 6 | 7 | class OAuthHandler(object): 8 | 9 | def __init__(self,consumer_key,consumer_secret,callback=None,secure=True): 10 | self._consumer = oauth.OAuthConsumer(consumer_key,consumer_secret) 11 | self._sigmethod = oauth.OAuthSignatureMethod_HMAC_SHA1() 12 | self.request_token = None 13 | self.access_token = None 14 | self.callback = callback 15 | self.username = None 16 | self.secure = secure 17 | self.host = API_HOST 18 | self.version = API_VERSION 19 | self.root = OAUTH_ROOT 20 | 21 | def _get_oauth_url(self,endpoint,secure=True): 22 | prefix = 'https://' if (self.secure or secure) else 'http://' 23 | return prefix + self.host + self.version + self.root + endpoint 24 | 25 | def set_request_token(self, key, secret): 26 | self.request_token = oauth.OAuthToken(key, secret) 27 | 28 | def set_access_token(self, key, secret): 29 | self.access_token = oauth.OAuthToken(key, secret) 30 | 31 | def get_request_token(self): 32 | url = self._get_oauth_url('request_token') 33 | request = oauth.OAuthRequest.from_consumer_and_token(self._consumer, http_url=url, callback=self.callback) 34 | request.sign_request(self._sigmethod,self._consumer,None) 35 | response = urlopen(Request(url,headers=request.to_header())) 36 | return oauth.OAuthToken.from_string(response.read()) 37 | 38 | def apply_auth(self,url,method,headers,parameters): 39 | request = oauth.OAuthRequest.from_consumer_and_token( 40 | self._consumer, 41 | http_url = url, 42 | http_method = method, 43 | token = self.access_token, 44 | parameters = parameters 45 | ) 46 | request.sign_request(self._sigmethod,self._consumer,self.access_token) 47 | headers.update(request.to_header()) 48 | return request 49 | 50 | def get_authorization_url(self): 51 | try: 52 | self.request_token = self.get_request_token() 53 | request = oauth.OAuthRequest.from_token_and_callback( 54 | token=self.request_token, http_url=self._get_oauth_url('authorize') 55 | ) 56 | return request.to_url() 57 | except Exception, e: 58 | raise FiveHundredClientError(e) 59 | 60 | def get_access_token(self,verifier=None): 61 | try: 62 | url = self._get_oauth_url('access_token') 63 | request = oauth.OAuthRequest.from_consumer_and_token( 64 | self._consumer, token=self.request_token, http_url=url, verifier=str(verifier) 65 | ) 66 | request.sign_request(self._sigmethod, self._consumer, self.request_token) 67 | response = urlopen(Request(url,headers=request.to_header())) 68 | self.access_token = oauth.OAuthToken.from_string(response.read()) 69 | return self.access_token 70 | except Exception, e: 71 | raise FiveHundredClientError(e) 72 | 73 | def get_xauth_access_token(self,username,password): 74 | try: 75 | url = self._get_oauth_url('access_token',secure=True) 76 | request = oauth.OAuthRequest.from_consumer_and_token( 77 | oauth_consumer=self._consumer, 78 | token=self.request_token, 79 | http_method='POST', 80 | http_url=url, 81 | parameters={ 82 | 'x_auth_mode': 'client_auth', 83 | 'x_auth_username': username, 84 | 'x_auth_password': password 85 | } 86 | ) 87 | request.sign_request(self._sigmethod,self._consumer,self.request_token) 88 | response = urlopen(Request(url,data=request.to_postdata())) 89 | self.access_token = oauth.OAuthToken.from_string(response.read()) 90 | return self.access_token 91 | except Exception, e: 92 | raise FiveHundredClientError(e) 93 | -------------------------------------------------------------------------------- /fivehundredpx/client.py: -------------------------------------------------------------------------------- 1 | from fivehundredpx import settings 2 | from fivehundredpx.auth import * 3 | from fivehundredpx.bind import bind_api 4 | from fivehundredpx.utils import FileUtil 5 | 6 | class FiveHundredPXAPI(object): 7 | 8 | def __init__(self,auth_handler=None,host=None,secure=True,version=None,retry_count=None,retry_delay=None,retry_errors=None): 9 | self.format = 'json' 10 | self.auth_handler = auth_handler 11 | self.secure = secure 12 | self.host = host or settings.API_HOST 13 | self.version = version or settings.API_VERSION 14 | self.retry_count = retry_count or settings.RETRY_COUNT 15 | self.retry_delay = retry_delay or settings.RETRY_DELAY 16 | self.retry_errors = retry_errors 17 | 18 | #### Photo API 19 | # https://github.com/500px/api-documentation/tree/master/endpoints/photo 20 | photos = bind_api(path='/photos') 21 | photos_search = bind_api(path='/photos/search') 22 | photos_id = bind_api(path='/photos/{id}', allowed_params=['id']) 23 | photos_post = bind_api(path='/photos', method='POST', require_auth=True, as_query=True) 24 | photos_update = bind_api(path='/photos/{id}', method='PUT', require_auth=True, as_query=True) 25 | photos_delete = bind_api(path='/photos/{id}', method='DELETE', allowed_params=['id'],require_auth=True) 26 | photos_comments = bind_api(path='/photos/{id}/comments', allowed_params=['id']) 27 | photos_comments_post = bind_api(path='/photos/{id}/comments', method='POST', allowed_params=['id'], require_auth=True, as_query=True) 28 | photos_favorites = bind_api(path='/photos/{id}/favorites', allowed_params=['id'], require_auth=True) 29 | photos_favorite_post = bind_api(path='/photos/{id}/favorite', method='POST', allowed_params=['id'], require_auth=True) 30 | photos_favorite_delete = bind_api(path='/photos/{id}/favorite', method='DELETE', allowed_params=['id'], require_auth=True) 31 | photos_tags_post = bind_api(path='/photos/{id}/tags', method='POST', allowed_params=['id'], require_auth=True, as_query=True) 32 | photos_tags_delete = bind_api(path='/photos/{id}/tags', method='DELETE', allowed_params=['id'], require_auth=True, as_query=True) 33 | photos_votes = bind_api(path='/photos/{id}/votes', allowed_params=['id'], require_auth=True) 34 | photos_vote_post = bind_api(path='/photos/{id}/vote', method='POST', allowed_params=['id'], require_auth=True, as_query=True) 35 | photos_report = bind_api(path='/photos/{id}/report', method='POST', allowed_params=['id'], require_auth=True, as_query=True) 36 | 37 | def upload_photo(self, filename=None,fp=None,file_type=None, **kwargs): 38 | headers,body = FileUtil.create_body_by_filepath(filename,'file',kwargs) if fp==None else FileUtil.create_body_by_fp(fp, 'file', file_type, kwargs) 39 | return bind_api( 40 | path = '/upload', 41 | method = 'POST' 42 | )(self,http_body=body, headers=headers) 43 | 44 | #### User API 45 | # https://github.com/500px/api-documentation/tree/master/endpoints/user 46 | users = bind_api(path='/users', require_auth=True) 47 | users_show = bind_api(path='/users/show') 48 | users_search = bind_api(path='/users/search') 49 | users_friends = bind_api(path='/users/{id}/friends', allowed_params=['id']) 50 | users_followers = bind_api(path='/users/{id}/followers', allowed_params=['id']) 51 | users_friends_post = bind_api(path='/users/{id}/friends', method='POST', allowed_params=['id']) 52 | users_friends_delete = bind_api(path='/users/{id}/friends', method='DELETE', allowed_params=['id']) 53 | 54 | #### Blog API 55 | # https://github.com/500px/api-documentation/tree/master/endpoints/blog 56 | blogs = bind_api(path='/blogs') 57 | blogs_id = bind_api(path='/blogs/{id}', allowed_params=['id']) 58 | blogs_comments = bind_api(path='/blogs/{id}/comments', allowed_params=['id']) 59 | # blogs_comments_post = bind_api(path='/blogs/{id}/comments', require_auth=True, allowed_params=['id'], method='POST') 60 | # blogs_post = bind_api(path='/blogs', require_auth=True, method='POST') 61 | # blogs_update = bind_api(path='/blogs/{id}', require_auth=True, allowed_params=['id'], method='PUT') 62 | # blogs_delete = bind_api(path='/blogs/{id}', require_auth=True, allowed_params=['id'], method='DELETE') 63 | 64 | #### Comment API 65 | # https://github.com/500px/api-documentation/tree/master/endpoints/comments 66 | comments_post = bind_api(path='/comments/{id}/comments', require_auth=True, allowed_params=['id'], method='POST') 67 | 68 | #### Collection API 69 | # https://github.com/500px/api-documentation/tree/master/endpoints/collections 70 | collections = bind_api(path='/collections', require_auth=True) 71 | collections_id = bind_api(path='/collections/{id}', require_auth=True, allowed_params=['id']) 72 | collections_post = bind_api(path='/collections', require_auth=True, method='POST', as_query=True) 73 | collections_update = bind_api(path='/collections/{id}', require_auth=True, method='PUT', allowed_params=['id'], as_query=True) 74 | collections_delete = bind_api(path='/collections/{id}', require_auth=True, method='DELETE', allowed_params=['id'], as_query=True) 75 | -------------------------------------------------------------------------------- /fivehundredpx/bind.py: -------------------------------------------------------------------------------- 1 | from fivehundredpx.utils import Util 2 | from fivehundredpx.errors import * 3 | import urllib, re, httplib, time, simplejson 4 | 5 | re_path_template = re.compile('{\w+}') 6 | 7 | def bind_api(**config): 8 | 9 | class APIMethod(object): 10 | '''common parameters for a api action''' 11 | path = config['path'] 12 | method = config.get('method','GET') 13 | allowed_params = config.get('allowed_params',[]) 14 | model_class = config.get('model_class',None) 15 | response_type = config.get('response_type','list') 16 | require_auth = config.get('require_auth',False) 17 | as_query = config.get('as_query',False) 18 | 19 | def __init__(self, api, *args, **kwargs): 20 | '''parameters for every request''' 21 | if self.require_auth and not api.auth_handler: raise FiveHundredClientError('auth handler is required') 22 | 23 | self.api = api 24 | self.parameters = {} 25 | self.as_generator = kwargs.pop("as_generator", False) 26 | self.max_pages = kwargs.pop("max_pages", 3) 27 | self.headers = kwargs.pop('headers', {}) 28 | self.body = kwargs.pop('http_body', {}) 29 | self.protocol = 'https://' if self.api.secure else 'http://' 30 | if 'method' in kwargs: self.method = kwargs.pop('method') 31 | if 'require_auth' in kwargs: self.require_auth = kwargs.pop('require_auth') 32 | self._build_parameters(args, kwargs) 33 | self._build_path() 34 | 35 | def _build_parameters(self, args, kwargs): 36 | for index,value in enumerate(args): 37 | if value is None: continue 38 | try: 39 | self.parameters[self.allowed_params[index]] = value 40 | except IndexError: 41 | raise FiveHundredsClientError("Too many arguments supplied") 42 | 43 | for key,value in kwargs.iteritems(): 44 | if value is None: continue 45 | if type(value) in (list, tuple): 46 | self.parameters[key + "[]"] = value 47 | else: 48 | self.parameters[key] = value 49 | 50 | def _build_path(self): 51 | for variable in re_path_template.findall(self.path): 52 | name = variable.strip('{}') 53 | try: 54 | value = urllib.quote(str(self.parameters[name])) 55 | del self.parameters[name] 56 | self.path = self.path.replace(variable,value) 57 | except KeyError: 58 | raise FiveHundredsClientError('No parameter value found for path variable: %s' % name) 59 | 60 | def _execute(self): 61 | url = "%s%s%s%s" % (self.protocol,self.api.host,self.api.version,self.path) 62 | 63 | if self.method == "GET" or self.as_query == True: 64 | param_list = [] 65 | for key, value in self.parameters.iteritems(): 66 | if type(value) in (list, tuple): 67 | for v in value: 68 | param_list.append("%s=%s" % (key, v)) 69 | else: 70 | param_list.append("%s=%s" % (key, Util.replace_space(value))) 71 | 72 | if len(param_list) != 0 : url = "%s?%s" % (url, "&".join(param_list)) 73 | 74 | if self.method == "POST" or self.method == "PUT": 75 | if self.headers.has_key('Content-Type') == False: 76 | self.headers['Content-Type'] = "application/x-www-form-urlencoded; charset=UTF-8" 77 | if self.headers.has_key('Content-Length') == False: 78 | self.headers["Content-Length"] = 0 79 | 80 | postdata = None 81 | if self.require_auth and self.api.auth_handler: 82 | request = self.api.auth_handler.apply_auth(url, self.method, self.headers, self.parameters) 83 | if not self.method == "GET": postdata = request.to_postdata() 84 | 85 | if self.path == "/upload": postdata = self.body 86 | 87 | for count in xrange(self.api.retry_count): 88 | conn = httplib.HTTPSConnection(self.api.host) if self.api.secure else httplib.HTTPConnection(self.api.host) 89 | try: 90 | conn.request(self.method, url, body=postdata, headers=self.headers) 91 | response = conn.getresponse() 92 | except Exception, e: 93 | conn.close() 94 | raise FiveHundredClientError('Failed to send request: %s' % e) 95 | 96 | if self.api.retry_errors: 97 | if response.status not in self.api.retry_errors: break 98 | else: 99 | if response.status == 200: break 100 | conn.close() 101 | time.sleep(self.api.retry_delay) 102 | 103 | if response.status > 199 and response.status < 300: 104 | result = response.read() 105 | conn.close() 106 | return simplejson.loads(result) 107 | else: 108 | try: 109 | error_msg = self.api.parser.parse_error(response.read()) 110 | except Exception: 111 | error_msg = "500PX error response: status code = %s" % response.status 112 | finally: 113 | conn.close() 114 | raise FiveHundredClientError(error_msg,status=response.status) 115 | 116 | def _generator(self): 117 | base = self.parameters['page'] if 'page' in self.parameters else 1 118 | for count in xrange(self.max_pages): 119 | self.parameters['page'] = base + count 120 | yield self._execute() 121 | return 122 | 123 | def execute(self): 124 | return self._generator() if self.as_generator else self._execute() 125 | 126 | def _call(api, *args, **kwargs): 127 | method = APIMethod(api, *args, **kwargs) 128 | return method.execute() 129 | 130 | return _call 131 | -------------------------------------------------------------------------------- /fivehundredpx/oauth.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License 3 | 4 | Copyright (c) 2007 Leah Culver 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import cgi 26 | import urllib 27 | import time 28 | import random 29 | import urlparse 30 | import hmac 31 | import binascii 32 | 33 | 34 | VERSION = '1.0' # Hi Blaine! 35 | HTTP_METHOD = 'GET' 36 | SIGNATURE_METHOD = 'PLAINTEXT' 37 | 38 | 39 | class OAuthError(RuntimeError): 40 | """Generic exception class.""" 41 | def __init__(self, message='OAuth error occured.'): 42 | self.message = message 43 | 44 | def build_authenticate_header(realm=''): 45 | """Optional WWW-Authenticate header (401 error)""" 46 | return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} 47 | 48 | def escape(s): 49 | """Escape a URL including any /.""" 50 | return urllib.quote(s, safe='~') 51 | 52 | def _utf8_str(s): 53 | """Convert unicode to utf-8.""" 54 | if isinstance(s, unicode): 55 | return s.encode("utf-8") 56 | else: 57 | return str(s) 58 | 59 | def generate_timestamp(): 60 | """Get seconds since epoch (UTC).""" 61 | return int(time.time()) 62 | 63 | def generate_nonce(length=8): 64 | """Generate pseudorandom number.""" 65 | return ''.join([str(random.randint(0, 9)) for i in range(length)]) 66 | 67 | def generate_verifier(length=8): 68 | """Generate pseudorandom number.""" 69 | return ''.join([str(random.randint(0, 9)) for i in range(length)]) 70 | 71 | 72 | class OAuthConsumer(object): 73 | """Consumer of OAuth authentication. 74 | 75 | OAuthConsumer is a data type that represents the identity of the Consumer 76 | via its shared secret with the Service Provider. 77 | 78 | """ 79 | key = None 80 | secret = None 81 | 82 | def __init__(self, key, secret): 83 | self.key = key 84 | self.secret = secret 85 | 86 | 87 | class OAuthToken(object): 88 | """OAuthToken is a data type that represents an End User via either an access 89 | or request token. 90 | 91 | key -- the token 92 | secret -- the token secret 93 | 94 | """ 95 | key = None 96 | secret = None 97 | callback = None 98 | callback_confirmed = None 99 | verifier = None 100 | 101 | def __init__(self, key, secret): 102 | self.key = key 103 | self.secret = secret 104 | 105 | def set_callback(self, callback): 106 | self.callback = callback 107 | self.callback_confirmed = 'true' 108 | 109 | def set_verifier(self, verifier=None): 110 | if verifier is not None: 111 | self.verifier = verifier 112 | else: 113 | self.verifier = generate_verifier() 114 | 115 | def get_callback_url(self): 116 | if self.callback and self.verifier: 117 | # Append the oauth_verifier. 118 | parts = urlparse.urlparse(self.callback) 119 | scheme, netloc, path, params, query, fragment = parts[:6] 120 | if query: 121 | query = '%s&oauth_verifier=%s' % (query, self.verifier) 122 | else: 123 | query = 'oauth_verifier=%s' % self.verifier 124 | return urlparse.urlunparse((scheme, netloc, path, params, 125 | query, fragment)) 126 | return self.callback 127 | 128 | def to_string(self): 129 | data = { 130 | 'oauth_token': self.key, 131 | 'oauth_token_secret': self.secret, 132 | } 133 | if self.callback_confirmed is not None: 134 | data['oauth_callback_confirmed'] = self.callback_confirmed 135 | return urllib.urlencode(data) 136 | 137 | def from_string(s): 138 | """ Returns a token from something like: 139 | oauth_token_secret=xxx&oauth_token=xxx 140 | """ 141 | params = cgi.parse_qs(s, keep_blank_values=False) 142 | key = params['oauth_token'][0] 143 | secret = params['oauth_token_secret'][0] 144 | token = OAuthToken(key, secret) 145 | try: 146 | token.callback_confirmed = params['oauth_callback_confirmed'][0] 147 | except KeyError: 148 | pass # 1.0, no callback confirmed. 149 | return token 150 | from_string = staticmethod(from_string) 151 | 152 | def __str__(self): 153 | return self.to_string() 154 | 155 | 156 | class OAuthRequest(object): 157 | """OAuthRequest represents the request and can be serialized. 158 | 159 | OAuth parameters: 160 | - oauth_consumer_key 161 | - oauth_token 162 | - oauth_signature_method 163 | - oauth_signature 164 | - oauth_timestamp 165 | - oauth_nonce 166 | - oauth_version 167 | - oauth_verifier 168 | ... any additional parameters, as defined by the Service Provider. 169 | """ 170 | parameters = None # OAuth parameters. 171 | http_method = HTTP_METHOD 172 | http_url = None 173 | version = VERSION 174 | 175 | def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None): 176 | self.http_method = http_method 177 | self.http_url = http_url 178 | self.parameters = parameters or {} 179 | 180 | def set_parameter(self, parameter, value): 181 | self.parameters[parameter] = value 182 | 183 | def get_parameter(self, parameter): 184 | try: 185 | return self.parameters[parameter] 186 | except: 187 | raise OAuthError('Parameter not found: %s' % parameter) 188 | 189 | def _get_timestamp_nonce(self): 190 | return self.get_parameter('oauth_timestamp'), self.get_parameter( 191 | 'oauth_nonce') 192 | 193 | def get_nonoauth_parameters(self): 194 | """Get any non-OAuth parameters.""" 195 | parameters = {} 196 | for k, v in self.parameters.iteritems(): 197 | # Ignore oauth parameters. 198 | if k.find('oauth_') < 0: 199 | parameters[k] = v 200 | return parameters 201 | 202 | def to_header(self, realm=''): 203 | """Serialize as a header for an HTTPAuth request.""" 204 | auth_header = 'OAuth realm="%s"' % realm 205 | # Add the oauth parameters. 206 | if self.parameters: 207 | for k, v in self.parameters.iteritems(): 208 | if k[:6] == 'oauth_': 209 | auth_header += ', %s="%s"' % (k, escape(str(v))) 210 | return {'Authorization': auth_header} 211 | 212 | def to_postdata(self): 213 | """Serialize as post data for a POST request.""" 214 | # return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \ 215 | # for k, v in self.parameters.iteritems()]) 216 | return '&'.join(['%s=%s' % (escape(str(k)), str(v).replace(" ", "+")) \ 217 | for k, v in self.parameters.iteritems()]) 218 | 219 | def to_url(self): 220 | """Serialize as a URL for a GET request.""" 221 | return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata()) 222 | 223 | def get_normalized_parameters(self): 224 | """Return a string that contains the parameters that must be signed.""" 225 | params = self.parameters 226 | try: 227 | # Exclude the signature if it exists. 228 | del params['oauth_signature'] 229 | except: 230 | pass 231 | # Escape key values before sorting. 232 | # key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \ 233 | # for k,v in params.items()] 234 | 235 | # for this param: image_size[]=2&image_size[]=3 236 | key_values = [] 237 | for k,v in params.items(): 238 | if type(v) in (list, tuple): 239 | [ key_values.append( (escape(_utf8_str(k)), escape(_utf8_str(_v)) )) for _v in v ] 240 | else: 241 | key_values.append( (escape(_utf8_str(k)), escape(_utf8_str(v)) ) ) 242 | 243 | # Sort lexicographically, first after key, then after value. 244 | key_values.sort() 245 | # Combine key value pairs into a string. 246 | return '&'.join(['%s=%s' % (k, v) for k, v in key_values]) 247 | 248 | def get_normalized_http_method(self): 249 | """Uppercases the http method.""" 250 | return self.http_method.upper() 251 | 252 | def get_normalized_http_url(self): 253 | """Parses the URL and rebuilds it to be scheme://host/path.""" 254 | parts = urlparse.urlparse(self.http_url) 255 | scheme, netloc, path = parts[:3] 256 | # Exclude default port numbers. 257 | if scheme == 'http' and netloc[-3:] == ':80': 258 | netloc = netloc[:-3] 259 | elif scheme == 'https' and netloc[-4:] == ':443': 260 | netloc = netloc[:-4] 261 | return '%s://%s%s' % (scheme, netloc, path) 262 | 263 | def sign_request(self, signature_method, consumer, token): 264 | """Set the signature parameter to the result of build_signature.""" 265 | # Set the signature method. 266 | self.set_parameter('oauth_signature_method', 267 | signature_method.get_name()) 268 | # Set the signature. 269 | self.set_parameter('oauth_signature', 270 | self.build_signature(signature_method, consumer, token)) 271 | 272 | def build_signature(self, signature_method, consumer, token): 273 | """Calls the build signature method within the signature method.""" 274 | return signature_method.build_signature(self, consumer, token) 275 | 276 | def from_request(http_method, http_url, headers=None, parameters=None, 277 | query_string=None): 278 | """Combines multiple parameter sources.""" 279 | if parameters is None: 280 | parameters = {} 281 | 282 | # Headers 283 | if headers and 'Authorization' in headers: 284 | auth_header = headers['Authorization'] 285 | # Check that the authorization header is OAuth. 286 | if auth_header[:6] == 'OAuth ': 287 | auth_header = auth_header[6:] 288 | try: 289 | # Get the parameters from the header. 290 | header_params = OAuthRequest._split_header(auth_header) 291 | parameters.update(header_params) 292 | except: 293 | raise OAuthError('Unable to parse OAuth parameters from ' 294 | 'Authorization header.') 295 | 296 | # GET or POST query string. 297 | if query_string: 298 | query_params = OAuthRequest._split_url_string(query_string) 299 | parameters.update(query_params) 300 | 301 | # URL parameters. 302 | param_str = urlparse.urlparse(http_url)[4] # query 303 | url_params = OAuthRequest._split_url_string(param_str) 304 | parameters.update(url_params) 305 | 306 | if parameters: 307 | return OAuthRequest(http_method, http_url, parameters) 308 | 309 | return None 310 | from_request = staticmethod(from_request) 311 | 312 | def from_consumer_and_token(oauth_consumer, token=None, 313 | callback=None, verifier=None, http_method=HTTP_METHOD, 314 | http_url=None, parameters=None): 315 | if not parameters: 316 | parameters = {} 317 | 318 | defaults = { 319 | 'oauth_consumer_key': oauth_consumer.key, 320 | 'oauth_timestamp': generate_timestamp(), 321 | 'oauth_nonce': generate_nonce(), 322 | 'oauth_version': OAuthRequest.version, 323 | } 324 | 325 | defaults.update(parameters) 326 | parameters = defaults 327 | 328 | if token: 329 | parameters['oauth_token'] = token.key 330 | if token.callback: 331 | parameters['oauth_callback'] = token.callback 332 | # 1.0a support for verifier. 333 | if verifier: 334 | parameters['oauth_verifier'] = verifier 335 | elif callback: 336 | # 1.0a support for callback in the request token request. 337 | parameters['oauth_callback'] = callback 338 | 339 | return OAuthRequest(http_method, http_url, parameters) 340 | from_consumer_and_token = staticmethod(from_consumer_and_token) 341 | 342 | def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, 343 | http_url=None, parameters=None): 344 | if not parameters: 345 | parameters = {} 346 | 347 | parameters['oauth_token'] = token.key 348 | 349 | if callback: 350 | parameters['oauth_callback'] = callback 351 | 352 | return OAuthRequest(http_method, http_url, parameters) 353 | from_token_and_callback = staticmethod(from_token_and_callback) 354 | 355 | def _split_header(header): 356 | """Turn Authorization: header into parameters.""" 357 | params = {} 358 | parts = header.split(',') 359 | for param in parts: 360 | # Ignore realm parameter. 361 | if param.find('realm') > -1: 362 | continue 363 | # Remove whitespace. 364 | param = param.strip() 365 | # Split key-value. 366 | param_parts = param.split('=', 1) 367 | # Remove quotes and unescape the value. 368 | params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) 369 | return params 370 | _split_header = staticmethod(_split_header) 371 | 372 | def _split_url_string(param_str): 373 | """Turn URL string into parameters.""" 374 | parameters = cgi.parse_qs(param_str, keep_blank_values=False) 375 | for k, v in parameters.iteritems(): 376 | parameters[k] = urllib.unquote(v[0]) 377 | return parameters 378 | _split_url_string = staticmethod(_split_url_string) 379 | 380 | class OAuthServer(object): 381 | """A worker to check the validity of a request against a data store.""" 382 | timestamp_threshold = 300 # In seconds, five minutes. 383 | version = VERSION 384 | signature_methods = None 385 | data_store = None 386 | 387 | def __init__(self, data_store=None, signature_methods=None): 388 | self.data_store = data_store 389 | self.signature_methods = signature_methods or {} 390 | 391 | def set_data_store(self, data_store): 392 | self.data_store = data_store 393 | 394 | def get_data_store(self): 395 | return self.data_store 396 | 397 | def add_signature_method(self, signature_method): 398 | self.signature_methods[signature_method.get_name()] = signature_method 399 | return self.signature_methods 400 | 401 | def fetch_request_token(self, oauth_request): 402 | """Processes a request_token request and returns the 403 | request token on success. 404 | """ 405 | try: 406 | # Get the request token for authorization. 407 | token = self._get_token(oauth_request, 'request') 408 | except OAuthError: 409 | # No token required for the initial token request. 410 | version = self._get_version(oauth_request) 411 | consumer = self._get_consumer(oauth_request) 412 | try: 413 | callback = self.get_callback(oauth_request) 414 | except OAuthError: 415 | callback = None # 1.0, no callback specified. 416 | self._check_signature(oauth_request, consumer, None) 417 | # Fetch a new token. 418 | token = self.data_store.fetch_request_token(consumer, callback) 419 | return token 420 | 421 | def fetch_access_token(self, oauth_request): 422 | """Processes an access_token request and returns the 423 | access token on success. 424 | """ 425 | version = self._get_version(oauth_request) 426 | consumer = self._get_consumer(oauth_request) 427 | try: 428 | verifier = self._get_verifier(oauth_request) 429 | except OAuthError: 430 | verifier = None 431 | # Get the request token. 432 | token = self._get_token(oauth_request, 'request') 433 | self._check_signature(oauth_request, consumer, token) 434 | new_token = self.data_store.fetch_access_token(consumer, token, verifier) 435 | return new_token 436 | 437 | def verify_request(self, oauth_request): 438 | """Verifies an api call and checks all the parameters.""" 439 | # -> consumer and token 440 | version = self._get_version(oauth_request) 441 | consumer = self._get_consumer(oauth_request) 442 | # Get the access token. 443 | token = self._get_token(oauth_request, 'access') 444 | self._check_signature(oauth_request, consumer, token) 445 | parameters = oauth_request.get_nonoauth_parameters() 446 | return consumer, token, parameters 447 | 448 | def authorize_token(self, token, user): 449 | """Authorize a request token.""" 450 | return self.data_store.authorize_request_token(token, user) 451 | 452 | def get_callback(self, oauth_request): 453 | """Get the callback URL.""" 454 | return oauth_request.get_parameter('oauth_callback') 455 | 456 | def build_authenticate_header(self, realm=''): 457 | """Optional support for the authenticate header.""" 458 | return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} 459 | 460 | def _get_version(self, oauth_request): 461 | """Verify the correct version request for this server.""" 462 | try: 463 | version = oauth_request.get_parameter('oauth_version') 464 | except: 465 | version = VERSION 466 | if version and version != self.version: 467 | raise OAuthError('OAuth version %s not supported.' % str(version)) 468 | return version 469 | 470 | def _get_signature_method(self, oauth_request): 471 | """Figure out the signature with some defaults.""" 472 | try: 473 | signature_method = oauth_request.get_parameter( 474 | 'oauth_signature_method') 475 | except: 476 | signature_method = SIGNATURE_METHOD 477 | try: 478 | # Get the signature method object. 479 | signature_method = self.signature_methods[signature_method] 480 | except: 481 | signature_method_names = ', '.join(self.signature_methods.keys()) 482 | raise OAuthError('Signature method %s not supported try one of the ' 483 | 'following: %s' % (signature_method, signature_method_names)) 484 | 485 | return signature_method 486 | 487 | def _get_consumer(self, oauth_request): 488 | consumer_key = oauth_request.get_parameter('oauth_consumer_key') 489 | consumer = self.data_store.lookup_consumer(consumer_key) 490 | if not consumer: 491 | raise OAuthError('Invalid consumer.') 492 | return consumer 493 | 494 | def _get_token(self, oauth_request, token_type='access'): 495 | """Try to find the token for the provided request token key.""" 496 | token_field = oauth_request.get_parameter('oauth_token') 497 | token = self.data_store.lookup_token(token_type, token_field) 498 | if not token: 499 | raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) 500 | return token 501 | 502 | def _get_verifier(self, oauth_request): 503 | return oauth_request.get_parameter('oauth_verifier') 504 | 505 | def _check_signature(self, oauth_request, consumer, token): 506 | timestamp, nonce = oauth_request._get_timestamp_nonce() 507 | self._check_timestamp(timestamp) 508 | self._check_nonce(consumer, token, nonce) 509 | signature_method = self._get_signature_method(oauth_request) 510 | try: 511 | signature = oauth_request.get_parameter('oauth_signature') 512 | except: 513 | raise OAuthError('Missing signature.') 514 | # Validate the signature. 515 | valid_sig = signature_method.check_signature(oauth_request, consumer, 516 | token, signature) 517 | if not valid_sig: 518 | key, base = signature_method.build_signature_base_string( 519 | oauth_request, consumer, token) 520 | raise OAuthError('Invalid signature. Expected signature base ' 521 | 'string: %s' % base) 522 | built = signature_method.build_signature(oauth_request, consumer, token) 523 | 524 | def _check_timestamp(self, timestamp): 525 | """Verify that timestamp is recentish.""" 526 | timestamp = int(timestamp) 527 | now = int(time.time()) 528 | lapsed = abs(now - timestamp) 529 | if lapsed > self.timestamp_threshold: 530 | raise OAuthError('Expired timestamp: given %d and now %s has a ' 531 | 'greater difference than threshold %d' % 532 | (timestamp, now, self.timestamp_threshold)) 533 | 534 | def _check_nonce(self, consumer, token, nonce): 535 | """Verify that the nonce is uniqueish.""" 536 | nonce = self.data_store.lookup_nonce(consumer, token, nonce) 537 | if nonce: 538 | raise OAuthError('Nonce already used: %s' % str(nonce)) 539 | 540 | 541 | class OAuthClient(object): 542 | """OAuthClient is a worker to attempt to execute a request.""" 543 | consumer = None 544 | token = None 545 | 546 | def __init__(self, oauth_consumer, oauth_token): 547 | self.consumer = oauth_consumer 548 | self.token = oauth_token 549 | 550 | def get_consumer(self): 551 | return self.consumer 552 | 553 | def get_token(self): 554 | return self.token 555 | 556 | def fetch_request_token(self, oauth_request): 557 | """-> OAuthToken.""" 558 | raise NotImplementedError 559 | 560 | def fetch_access_token(self, oauth_request): 561 | """-> OAuthToken.""" 562 | raise NotImplementedError 563 | 564 | def access_resource(self, oauth_request): 565 | """-> Some protected resource.""" 566 | raise NotImplementedError 567 | 568 | 569 | class OAuthDataStore(object): 570 | """A database abstraction used to lookup consumers and tokens.""" 571 | 572 | def lookup_consumer(self, key): 573 | """-> OAuthConsumer.""" 574 | raise NotImplementedError 575 | 576 | def lookup_token(self, oauth_consumer, token_type, token_token): 577 | """-> OAuthToken.""" 578 | raise NotImplementedError 579 | 580 | def lookup_nonce(self, oauth_consumer, oauth_token, nonce): 581 | """-> OAuthToken.""" 582 | raise NotImplementedError 583 | 584 | def fetch_request_token(self, oauth_consumer, oauth_callback): 585 | """-> OAuthToken.""" 586 | raise NotImplementedError 587 | 588 | def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier): 589 | """-> OAuthToken.""" 590 | raise NotImplementedError 591 | 592 | def authorize_request_token(self, oauth_token, user): 593 | """-> OAuthToken.""" 594 | raise NotImplementedError 595 | 596 | 597 | class OAuthSignatureMethod(object): 598 | """A strategy class that implements a signature method.""" 599 | def get_name(self): 600 | """-> str.""" 601 | raise NotImplementedError 602 | 603 | def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token): 604 | """-> str key, str raw.""" 605 | raise NotImplementedError 606 | 607 | def build_signature(self, oauth_request, oauth_consumer, oauth_token): 608 | """-> str.""" 609 | raise NotImplementedError 610 | 611 | def check_signature(self, oauth_request, consumer, token, signature): 612 | built = self.build_signature(oauth_request, consumer, token) 613 | return built == signature 614 | 615 | 616 | class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): 617 | 618 | def get_name(self): 619 | return 'HMAC-SHA1' 620 | 621 | def build_signature_base_string(self, oauth_request, consumer, token): 622 | sig = ( 623 | escape(oauth_request.get_normalized_http_method()), 624 | escape(oauth_request.get_normalized_http_url()), 625 | escape(oauth_request.get_normalized_parameters()), 626 | ) 627 | 628 | key = '%s&' % escape(consumer.secret) 629 | if token: 630 | key += escape(token.secret) 631 | raw = '&'.join(sig) 632 | return key, raw 633 | 634 | def build_signature(self, oauth_request, consumer, token): 635 | """Builds the base signature string.""" 636 | key, raw = self.build_signature_base_string(oauth_request, consumer, 637 | token) 638 | 639 | # HMAC object. 640 | try: 641 | import hashlib # 2.5 642 | hashed = hmac.new(key, raw, hashlib.sha1) 643 | except: 644 | import sha # Deprecated 645 | hashed = hmac.new(key, raw, sha) 646 | 647 | # Calculate the digest base 64. 648 | return binascii.b2a_base64(hashed.digest())[:-1] 649 | 650 | 651 | class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod): 652 | 653 | def get_name(self): 654 | return 'PLAINTEXT' 655 | 656 | def build_signature_base_string(self, oauth_request, consumer, token): 657 | """Concatenates the consumer key and secret.""" 658 | sig = '%s&' % escape(consumer.secret) 659 | if token: 660 | sig = sig + escape(token.secret) 661 | return sig, sig 662 | 663 | def build_signature(self, oauth_request, consumer, token): 664 | key, raw = self.build_signature_base_string(oauth_request, consumer, 665 | token) 666 | return key --------------------------------------------------------------------------------