diff --git a/README.md b/README.md index bd90f58..92b624e 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,20 @@ This library provides a pure Python interface to the LinkedIn **Profile**, **Gro You can install **python-linkedin** library via pip: $ pip install python-linkedin - $ pip install requests - $ pip install requests_oauthlib - ## Authentication -The LinkedIn REST API now supports the **Oauth 2.0** protocol for authentication. This package provides a full OAuth 2.0 implementation for connecting to LinkedIn as well as an option for using an OAuth 1.0a flow that can be helpful for development purposes or just accessing your own data. +The LinkedIn REST API now supports the **OAuth 2.0** protocol for authentication. This package provides a full OAuth 2.0 implementation for connecting to LinkedIn as well as an option for using an OAuth 1.0a flow that can be helpful for development purposes or just accessing your own data. + +### HTTP API example + +Set `LINKEDIN_API_KEY` and `LINKEDIN_API_SECRET`, configure your app to redirect to `http://localhost:8080/code`, then execute: + + 0. `http_api.py` + 1. Visit `http://localhost:8080` in your browser, curl or similar + 2. A tab in your browser will open up, give LinkedIn permission there + 3. You'll then be presented with a list of available routes, hit any, e.g.: + 4. `curl -XGET http://localhost:8080/get_profile` ### Developer Authentication diff --git a/examples/http_api.py b/examples/http_api.py new file mode 100644 index 0000000..92c63a4 --- /dev/null +++ b/examples/http_api.py @@ -0,0 +1,72 @@ +__author__ = 'Samuel Marks ' +__version__ = '0.1.0' + +from SocketServer import ThreadingTCPServer +from SimpleHTTPServer import SimpleHTTPRequestHandler +from webbrowser import open_new_tab +from json import dumps +from urlparse import urlparse +from os import environ +from types import NoneType + +from linkedin.linkedin import LinkedInAuthentication, LinkedInApplication, PERMISSIONS + +PORT = 8080 + + +class LinkedInWrapper(object): + """ Simple namespacing """ + API_KEY = environ.get('LINKEDIN_API_KEY') + API_SECRET = environ.get('LINKEDIN_API_SECRET') + RETURN_URL = 'http://localhost:{0}/code'.format(globals()['PORT']) + authentication = LinkedInAuthentication(API_KEY, API_SECRET, RETURN_URL, PERMISSIONS.enums.values()) + application = LinkedInApplication(authentication) + + +liw = LinkedInWrapper() +run_already = False +params_to_d = lambda params: { + l[0]: l[1] for l in map(lambda j: j.split('='), urlparse(params).query.split('&')) +} + + +class CustomHandler(SimpleHTTPRequestHandler): + def json_headers(self, status_code=200): + self.send_response(status_code) + self.send_header('Content-type', 'application/json') + self.end_headers() + + def do_GET(self): + parsedurl = urlparse(self.path) + authed = type(liw.authentication.token) is not NoneType + + if parsedurl.path == '/code': + self.json_headers() + + liw.authentication.authorization_code = params_to_d(self.path).get('code') + self.wfile.write(dumps({'access_token': liw.authentication.get_access_token(), + 'routes': filter(lambda d: not d.startswith('_'), dir(liw.application))})) + elif parsedurl.path == '/routes': + self.json_headers() + + self.wfile.write(dumps({'routes': filter(lambda d: not d.startswith('_'), dir(liw.application))})) + elif not authed: + self.json_headers() + + if not globals()['run_already']: + open_new_tab(liw.authentication.authorization_url) + globals()['run_already'] = True + self.wfile.write(dumps({'path': self.path, 'authed': type(liw.authentication.token) is NoneType})) + elif authed and len(parsedurl.path) and parsedurl.path[1:] in dir(liw.application): + self.json_headers() + self.wfile.write(dumps(getattr(liw.application, parsedurl.path[1:])())) + else: + self.json_headers(501) + self.wfile.write(dumps({'error': 'NotImplemented'})) + + +if __name__ == '__main__': + httpd = ThreadingTCPServer(('localhost', PORT), CustomHandler) + + print 'Server started on port:', PORT + httpd.serve_forever() diff --git a/linkedin/__init__.py b/linkedin/__init__.py index 5d5e187..d989274 100644 --- a/linkedin/__init__.py +++ b/linkedin/__init__.py @@ -1,2 +1,2 @@ -__version__ = '4.1' +__version__ = '4.2' VERSION = tuple(map(int, __version__.split('.'))) diff --git a/linkedin/exceptions.py b/linkedin/exceptions.py index d82e779..754732c 100644 --- a/linkedin/exceptions.py +++ b/linkedin/exceptions.py @@ -3,35 +3,44 @@ class LinkedInError(Exception): pass + class LinkedInBadRequestError(LinkedInError): pass + class LinkedInUnauthorizedError(LinkedInError): pass + class LinkedInPaymentRequiredError(LinkedInError): pass + class LinkedInNotFoundError(LinkedInError): pass + class LinkedInConflictError(LinkedInError): pass + class LinkedInForbiddenError(LinkedInError): pass + class LinkedInInternalServiceError(LinkedInError): pass + ERROR_CODE_EXCEPTION_MAPPING = { - 400: LinkedInBadRequestError, - 401: LinkedInUnauthorizedError, - 402: LinkedInPaymentRequiredError, - 403: LinkedInForbiddenError, - 404: LinkedInNotFoundError, - 409: LinkedInForbiddenError, - 500: LinkedInInternalServiceError} + 400: LinkedInBadRequestError, + 401: LinkedInUnauthorizedError, + 402: LinkedInPaymentRequiredError, + 403: LinkedInForbiddenError, + 404: LinkedInNotFoundError, + 409: LinkedInForbiddenError, + 500: LinkedInInternalServiceError} + def get_exception_for_error_code(error_code): return ERROR_CODE_EXCEPTION_MAPPING.get(error_code, LinkedInError) diff --git a/linkedin/linkedin.py b/linkedin/linkedin.py index 4cb0d9e..323d344 100644 --- a/linkedin/linkedin.py +++ b/linkedin/linkedin.py @@ -15,39 +15,37 @@ __all__ = ['LinkedInAuthentication', 'LinkedInApplication', 'PERMISSIONS'] PERMISSIONS = enum('Permission', - COMPANY_ADMIN='rw_company_admin', - BASIC_PROFILE='r_basicprofile', - FULL_PROFILE='r_fullprofile', - EMAIL_ADDRESS='r_emailaddress', - NETWORK='r_network', - CONTACT_INFO='r_contactinfo', - NETWORK_UPDATES='rw_nus', - GROUPS='rw_groups', - MESSAGES='w_messages') - + COMPANY_ADMIN='rw_company_admin', + BASIC_PROFILE='r_basicprofile', + FULL_PROFILE='r_fullprofile', + EMAIL_ADDRESS='r_emailaddress', + NETWORK='r_network', + CONTACT_INFO='r_contactinfo', + NETWORK_UPDATES='rw_nus', + GROUPS='rw_groups', + MESSAGES='w_messages') ENDPOINTS = enum('LinkedInURL', - PEOPLE='https://api.linkedin.com/v1/people', - PEOPLE_SEARCH='https://api.linkedin.com/v1/people-search', - GROUPS='https://api.linkedin.com/v1/groups', - POSTS='https://api.linkedin.com/v1/posts', - COMPANIES='https://api.linkedin.com/v1/companies', - COMPANY_SEARCH='https://api.linkedin.com/v1/company-search', - JOBS='https://api.linkedin.com/v1/jobs', - JOB_SEARCH='https://api.linkedin.com/v1/job-search') - + PEOPLE='https://api.linkedin.com/v1/people', + PEOPLE_SEARCH='https://api.linkedin.com/v1/people-search', + GROUPS='https://api.linkedin.com/v1/groups', + POSTS='https://api.linkedin.com/v1/posts', + COMPANIES='https://api.linkedin.com/v1/companies', + COMPANY_SEARCH='https://api.linkedin.com/v1/company-search', + JOBS='https://api.linkedin.com/v1/jobs', + JOB_SEARCH='https://api.linkedin.com/v1/job-search') NETWORK_UPDATES = enum('NetworkUpdate', - APPLICATION='APPS', - COMPANY='CMPY', - CONNECTION='CONN', - JOB='JOBS', - GROUP='JGRP', - PICTURE='PICT', - EXTENDED_PROFILE='PRFX', - CHANGED_PROFILE='PRFU', - SHARED='SHAR', - VIRAL='VIRL') + APPLICATION='APPS', + COMPANY='CMPY', + CONNECTION='CONN', + JOB='JOBS', + GROUP='JGRP', + PICTURE='PICT', + EXTENDED_PROFILE='PRFX', + CHANGED_PROFILE='PRFU', + SHARED='SHAR', + VIRAL='VIRL') class LinkedInDeveloperAuthentication(object): @@ -57,6 +55,7 @@ class LinkedInDeveloperAuthentication(object): Useful for situations in which users would like to access their own data or during the development process. """ + def __init__(self, consumer_key, consumer_secret, user_token, user_secret, redirect_uri, permissions=[]): self.consumer_key = consumer_key @@ -197,9 +196,9 @@ def search_profile(self, selectors=None, params=None, headers=None): return response.json() def get_picture_urls(self, member_id=None, member_url=None, - params=None, headers=None): + params=None, headers=None): if member_id: - url = '%s/id=%s/picture-urls::(original)' % (ENDPOINTS.PEOPLE, str(member_id)) + url = '%s/id=%s/picture-urls::(original)' % (ENDPOINTS.PEOPLE, str(member_id)) elif member_url: url = '%s/url=%s/picture-urls::(original)' % (ENDPOINTS.PEOPLE, urllib.quote_plus(member_url)) @@ -268,7 +267,7 @@ def get_posts(self, group_id, post_ids=None, selectors=None, params=None, def join_group(self, group_id): url = '%s/~/group-memberships/%s' % (ENDPOINTS.PEOPLE, str(group_id)) response = self.make_request('PUT', url, - data=json.dumps({'membershipState': {'code': 'member'}})) + data=json.dumps({'membershipState': {'code': 'member'}})) raise_for_error(response) return True @@ -431,7 +430,7 @@ def get_network_updates(self, types, member_id=None, self_scope=True, params=None, headers=None): if member_id: url = '%s/id=%s/network/updates' % (ENDPOINTS.PEOPLE, - str(member_id)) + str(member_id)) else: url = '%s/~/network/updates' % ENDPOINTS.PEOPLE @@ -449,7 +448,7 @@ def get_network_updates(self, types, member_id=None, return response.json() def get_network_update(self, types, update_key, - self_scope=True, params=None, headers=None): + self_scope=True, params=None, headers=None): url = '%s/~/network/updates/key=%s' % (ENDPOINTS.PEOPLE, str(update_key)) if not params: diff --git a/linkedin/utils.py b/linkedin/utils.py index 5f93f0a..e960631 100644 --- a/linkedin/utils.py +++ b/linkedin/utils.py @@ -68,5 +68,6 @@ def raise_for_error(response): except (ValueError, TypeError): raise LinkedInError(error.message) + HTTP_METHODS = enum('HTTPMethod', GET='GET', POST='POST', PUT='PUT', DELETE='DELETE', PATCH='PATCH') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..11b99d9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests +requests_oauthlib \ No newline at end of file diff --git a/setup.py b/setup.py index 3b04d4d..18b3ed3 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,6 @@ with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: long_description = readme.read() - setup(name='python-linkedin', version=__version__, description='Python Interface to the LinkedIn API',