diff --git a/README.md b/README.md index 8852add..c442652 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Python interface to the LinkedIn API -[![LinkedIn](http://developer.linkedin.com/sites/default/files/LinkedIn_Logo60px.png)](http://developer.linkedin.com) +[![LinkedIn](https://content.linkedin.com/etc/designs/linkedin/katy/global/clientlibs/img/logo.png)](http://developer.linkedin.com) This library provides a pure Python interface to the LinkedIn **Profile**, **Group**, **Company**, **Jobs**, **Search**, **Share**, **Network** and **Invitation** REST APIs. @@ -295,7 +295,201 @@ application.get_companies(company_ids=[1035], universal_names=['apple'], selecto {u'_total': 2, u'values': [{u'_key': u'1035', u'name': u'Microsoft'}, {u'_key': u'universal-name=apple', u'name': u'Apple'}]} +``` + +```python +# Get statistics for a company page +application.get_statistics_company_page(1035) +{ + "followStatistics": { + "companySizes": { + "_total": 2, + "values": [ + { + "entryKey": "C", + "entryValue": "79027" + }, + { + "entryKey": "E", + "entryValue": "59788" + } + ] + }, + "count": 386420, + "countries": { + "_total": 1, + "values": [ + { + "entryKey": "us", + "entryValue": "386420" + } + ] + }, + "countsByMonth": { + "_total": 1, + "values": [ + { + "date": { + "month": 1, + "year": 2015 + }, + "newCount": 44868, + "totalCount": 1111157 + } + ] + }, + "employeeCount": 3992, + "functions": { + "_total": 2, + "values": [ + { + "entryKey": "25", + "entryValue": "100" + }, + { + "entryKey": "15", + "entryValue": "3892" + } + ] + }, + "nonEmployeeCount": 107253, + "regions": { + "_total": 1, + "values": [ + { + "entryKey": "us-84", + "entryValue": "107253" + } + ] + }, + "seniorities": { + "_total": 2, + "values": [ + { + "entryKey": "4", + "entryValue": "326866" + }, + { + "entryKey": "3", + "entryValue": "319703" + } + ] + } + }, + "statusUpdateStatistics": { + "viewsByMonth": { + "_total": 1, + "values": [ + { + "clicks": 81333, + "comments": 1203, + "date": { + "month": 1, + "year": 2015 + }, + "engagement": 0.003203537929382338, + "impressions": 32797489, + "likes": 20387, + "shares": 2145 + } + ] + } + } +} +``` +```python +# Get a specific company update +application.get_specific_company_update(1035,"UNIU-c1035-5720424522989961216-FOLLOW_CMPY") +{ + "isCommentable": true, + "isLikable": true, + "isLiked": false, + "likes": { + "_total": 1, + "values": [ + { + "person": { + "firstName": "Frodo", + "headline": "2nd Generation Adventurer", + "id": "12345678", + "lastName": "Baggins", + "pictureUrl": "https://media.licdn.com/mpr/mprx/…" + } + } + ] + }, + "numLikes": 1, + "timestamp": 1423270834567, + "updateComments": { + "_total": 1, + "values": [ + { + "comment": "Great comment!", + "id": 987654321, + "person": { + "apiStandardProfileRequest": { + "headers": { + "_total": 1, + "values": [ + { + "name": "x-li-auth-token", + "value": "name:Ff1A" + } + ] + }, + "url": "https://api.linkedin.com/v1/people/12345678" + }, + "firstName": "Samwise", + "headline": "Journeyman", + "id": "23456789", + "lastName": "Gamgee", + "pictureUrl": "https://media.licdn.com/mpr/mprx/…", + "siteStandardProfileRequest": { + "url": "https://www.linkedin.com/profile/view?id=…" + } + }, + "sequenceNumber": 0, + "timestamp": 1423281179569 + } + ] + }, + "updateContent": { + "company": { + "id": 1337, + "name": "LinkedIn" + }, + "companyStatusUpdate": { + "share": { + "comment": "This is a test comment.", + "content": { + "description": "Test description", + "eyebrowUrl": "http://linkd.in/…", + "shortenedUrl": "http://linkd.in/…", + "submittedImageUrl": "http://m.c.lnkd.licdn.com/…", + "submittedUrl": "http://linkd.in/…", + "thumbnailUrl": "https://media.licdn.com/…", + "title": "Best Advice: Take Jobs Others Don't Want" + }, + "id": "s132435465768", + "source": { + "serviceProvider": { + "name": "LINKEDIN" + }, + "serviceProviderShareId": "s97867564534231" + }, + "timestamp": 1423270834567, + "visibility": { + "code": "anyone" + } + } + } + }, + "updateKey": "UPDATE-c1337-998877665544332211", + "updateType": "CMPY" +} +``` +```python # Get the latest updates about Microsoft application.get_company_updates(1035, params={'count': 2}) {u'_count': 2, diff --git a/examples/authentication.py b/examples/authentication.py index cb1a22b..a43771d 100644 --- a/examples/authentication.py +++ b/examples/authentication.py @@ -8,5 +8,5 @@ RETURN_URL = 'http://localhost:8000' authentication = LinkedInAuthentication(API_KEY, API_SECRET, RETURN_URL, PERMISSIONS.enums.values()) - print authentication.authorization_url + print(authentication.authorization_url) application = LinkedInApplication(authentication) diff --git a/examples/http_api.py b/examples/http_api.py index 92c63a4..aa9c9b5 100644 --- a/examples/http_api.py +++ b/examples/http_api.py @@ -68,5 +68,5 @@ def do_GET(self): if __name__ == '__main__': httpd = ThreadingTCPServer(('localhost', PORT), CustomHandler) - print 'Server started on port:', PORT + print('Server started on port:', PORT) httpd.serve_forever() diff --git a/linkedin/linkedin.py b/linkedin/linkedin.py index 81fe33a..d54676f 100644 --- a/linkedin/linkedin.py +++ b/linkedin/linkedin.py @@ -2,15 +2,16 @@ import contextlib import hashlib import random -import urllib +import urllib.parse as urllib import requests from requests_oauthlib import OAuth1 from .exceptions import LinkedInError from .models import AccessToken, LinkedInInvitation, LinkedInMessage -from .utils import enum, to_utf8, raise_for_error, json, StringIO - +from .utils import enum, to_utf8, raise_for_error +import json +from io import StringIO __all__ = ['LinkedInAuthentication', 'LinkedInApplication', 'PERMISSIONS'] @@ -94,7 +95,8 @@ def authorization_url(self): 'redirect_uri': self.redirect_uri} # urlencode uses quote_plus when encoding the query string so, # we ought to be encoding the qs by on our own. - qsl = ['%s=%s' % (urllib.quote(k), urllib.quote(v)) for k, v in qd.items()] + qsl = ['%s=%s' % (urllib.quote(k), urllib.quote(v)) for k, v in + qd.items()] return '%s?%s' % (self.AUTHORIZATION_URL, '&'.join(qsl)) @property @@ -102,8 +104,8 @@ def last_error(self): return self._error def _make_new_state(self): - return hashlib.md5( - '%s%s' % (random.randrange(0, 2 ** 63), self.secret)).hexdigest() + h_str = '%s%s' % (random.randrange(0, 2 ** 63), self.secret) + return hashlib.md5(h_str.encode("utf-8")).hexdigest() def get_access_token(self, timeout=60): assert self.authorization_code, 'You must first get the authorization code' @@ -112,10 +114,12 @@ def get_access_token(self, timeout=60): 'redirect_uri': self.redirect_uri, 'client_id': self.key, 'client_secret': self.secret} - response = requests.post(self.ACCESS_TOKEN_URL, data=qd, timeout=timeout) + response = requests.post(self.ACCESS_TOKEN_URL, data=qd, + timeout=timeout) raise_for_error(response) response = response.json() - self.token = AccessToken(response['access_token'], response['expires_in']) + self.token = AccessToken(response['access_token'], + response['expires_in']) return self.token @@ -129,7 +133,7 @@ def parse(cls, selector): elif type(selector) in (list, tuple): result.write(','.join(map(cls.parse, selector))) else: - result.write(to_utf8(selector)) + result.write(selector) return result.getvalue() @@ -146,9 +150,11 @@ def __init__(self, authentication=None, token=None): def make_request(self, method, url, data=None, params=None, headers=None, timeout=60): if headers is None: - headers = {'x-li-format': 'json', 'Content-Type': 'application/json'} + headers = {'x-li-format': 'json', + 'Content-Type': 'application/json'} else: - headers.update({'x-li-format': 'json', 'Content-Type': 'application/json'}) + headers.update( + {'x-li-format': 'json', 'Content-Type': 'application/json'}) if params is None: params = {} @@ -157,11 +163,14 @@ def make_request(self, method, url, data=None, params=None, headers=None, if isinstance(self.authentication, LinkedInDeveloperAuthentication): # Let requests_oauthlib.OAuth1 do *all* of the work here - auth = OAuth1(self.authentication.consumer_key, self.authentication.consumer_secret, - self.authentication.user_token, self.authentication.user_secret) + auth = OAuth1(self.authentication.consumer_key, + self.authentication.consumer_secret, + self.authentication.user_token, + self.authentication.user_secret) kw.update({'auth': auth}) else: - params.update({'oauth2_access_token': self.authentication.token.access_token}) + params.update( + {'oauth2_access_token': self.authentication.token.access_token}) return requests.request(method.upper(), url, **kw) @@ -175,7 +184,8 @@ def get_profile(self, member_id=None, member_url=None, selectors=None, else: url = '%s/id=%s' % (ENDPOINTS.PEOPLE, str(member_id)) elif member_url: - url = '%s/url=%s' % (ENDPOINTS.PEOPLE, urllib.quote_plus(member_url)) + url = '%s/url=%s' % ( + ENDPOINTS.PEOPLE, urllib.quote_plus(member_url)) else: url = '%s/~' % ENDPOINTS.PEOPLE if selectors: @@ -198,10 +208,12 @@ def search_profile(self, selectors=None, params=None, headers=None): def get_picture_urls(self, member_id=None, member_url=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)) + urllib.quote_plus( + member_url)) else: url = '%s/~/picture-urls::(original)' % ENDPOINTS.PEOPLE @@ -228,10 +240,12 @@ def get_connections(self, member_id=None, member_url=None, selectors=None, def get_memberships(self, member_id=None, member_url=None, group_id=None, selectors=None, params=None, headers=None): if member_id: - url = '%s/id=%s/group-memberships' % (ENDPOINTS.PEOPLE, str(member_id)) + url = '%s/id=%s/group-memberships' % ( + ENDPOINTS.PEOPLE, str(member_id)) elif member_url: url = '%s/url=%s/group-memberships' % (ENDPOINTS.PEOPLE, - urllib.quote_plus(member_url)) + urllib.quote_plus( + member_url)) else: url = '%s/~/group-memberships' % ENDPOINTS.PEOPLE @@ -264,7 +278,8 @@ def get_posts(self, group_id, post_ids=None, selectors=None, params=None, raise_for_error(response) return response.json() - def get_post_comments(self, post_id, selectors=None, params=None, headers=None): + def get_post_comments(self, post_id, selectors=None, params=None, + headers=None): url = '%s/%s/comments' % (ENDPOINTS.POSTS, post_id) if selectors: url = '%s:(%s)' % (url, LinkedInSelector.parse(selectors)) @@ -276,7 +291,9 @@ def get_post_comments(self, post_id, selectors=None, params=None, headers=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 @@ -303,10 +320,11 @@ def submit_group_post(self, group_id, title, summary, submitted_url, return True def like_post(self, post_id, action): - url = '%s/%s/relation-to-viewer/is-liked' % (ENDPOINTS.POSTS, str(post_id)) + url = '%s/%s/relation-to-viewer/is-liked' % ( + ENDPOINTS.POSTS, str(post_id)) try: self.make_request('PUT', url, data=json.dumps(action)) - except (requests.ConnectionError, requests.HTTPError), error: + except (requests.ConnectionError, requests.HTTPError) as error: raise LinkedInError(error.message) else: return True @@ -318,19 +336,21 @@ def comment_post(self, post_id, comment): url = '%s/%s/comments' % (ENDPOINTS.POSTS, str(post_id)) try: self.make_request('POST', url, data=json.dumps(post)) - except (requests.ConnectionError, requests.HTTPError), error: + except (requests.ConnectionError, requests.HTTPError) as error: raise LinkedInError(error.message) else: return True - def get_company_by_email_domain(self, email_domain, params=None, headers=None): + def get_company_by_email_domain(self, email_domain, params=None, + headers=None): url = '%s?email-domain=%s' % (ENDPOINTS.COMPANIES, email_domain) response = self.make_request('GET', url, params=params, headers=headers) raise_for_error(response) return response.json() - def get_companies(self, company_ids=None, universal_names=None, selectors=None, + def get_companies(self, company_ids=None, universal_names=None, + selectors=None, params=None, headers=None): identifiers = [] url = ENDPOINTS.COMPANIES @@ -350,6 +370,24 @@ def get_companies(self, company_ids=None, universal_names=None, selectors=None, raise_for_error(response) return response.json() + def get_statistics_company_page(self, company_id, params=None, + headers=None): + url = "%s/%s/company-statistics" % ( + ENDPOINTS.COMPANIES, str(company_id) + ) + response = self.make_request('GET', url, params=params, headers=headers) + raise_for_error(response) + return response.json() + + def get_specific_company_update(self, company_id, update_id, params=None, + headers=None): + url = "%s/%s/updates/key=%s?format=json" % ( + ENDPOINTS.COMPANIES, str(company_id), str(update_id) + ) + response = self.make_request('GET', url, params=params, headers=headers) + raise_for_error(response) + return response.json() + def get_company_updates(self, company_id, params=None, headers=None): url = '%s/%s/updates' % (ENDPOINTS.COMPANIES, str(company_id)) response = self.make_request('GET', url, params=params, headers=headers) @@ -373,7 +411,8 @@ def follow_company(self, company_id): return True def unfollow_company(self, company_id): - url = '%s/~/following/companies/id=%s' % (ENDPOINTS.PEOPLE, str(company_id)) + url = '%s/~/following/companies/id=%s' % ( + ENDPOINTS.PEOPLE, str(company_id)) response = self.make_request('DELETE', url) raise_for_error(response) return True @@ -387,7 +426,8 @@ def search_company(self, selectors=None, params=None, headers=None): raise_for_error(response) return response.json() - def submit_company_share(self, company_id, comment=None, title=None, description=None, + def submit_company_share(self, company_id, comment=None, title=None, + description=None, submitted_url=None, submitted_image_url=None, visibility_code='anyone'): @@ -481,9 +521,10 @@ def get_network_updates(self, types, member_id=None, raise_for_error(response) return response.json() - def get_network_update(self, types, update_key, + def get_network_update(self, update_key, types=None, self_scope=True, params=None, headers=None): - url = '%s/~/network/updates/key=%s' % (ENDPOINTS.PEOPLE, str(update_key)) + url = '%s/~/network/updates/key=%s' % ( + ENDPOINTS.PEOPLE, str(update_key)) if not params: params = {} @@ -505,7 +546,8 @@ def get_network_status(self, params=None, headers=None): return response.json() def send_invitation(self, invitation): - assert type(invitation) == LinkedInInvitation, 'LinkedInInvitation required' + assert type( + invitation) == LinkedInInvitation, 'LinkedInInvitation required' url = '%s/~/mailbox' % ENDPOINTS.PEOPLE response = self.make_request('POST', url, data=json.dumps(invitation.json)) @@ -522,13 +564,15 @@ def send_message(self, message): def comment_on_update(self, update_key, comment): comment = {'comment': comment} - url = '%s/~/network/updates/key=%s/update-comments' % (ENDPOINTS.PEOPLE, update_key) + url = '%s/~/network/updates/key=%s/update-comments' % ( + ENDPOINTS.PEOPLE, update_key) response = self.make_request('POST', url, data=json.dumps(comment)) raise_for_error(response) return True def like_update(self, update_key, is_liked=True): - url = '%s/~/network/updates/key=%s/is-liked' % (ENDPOINTS.PEOPLE, update_key) + url = '%s/~/network/updates/key=%s/is-liked' % ( + ENDPOINTS.PEOPLE, update_key) response = self.make_request('PUT', url, data=json.dumps(is_liked)) raise_for_error(response) return True diff --git a/linkedin/server.py b/linkedin/server.py index 4032a2a..4a892bd 100644 --- a/linkedin/server.py +++ b/linkedin/server.py @@ -21,7 +21,7 @@ def quick_api(api_key, secret_key, port=8000): auth = LinkedInAuthentication(api_key, secret_key, 'http://localhost:8000/', PERMISSIONS.enums.values()) app = LinkedInApplication(authentication=auth) - print auth.authorization_url + print(auth.authorization_url) _wait_for_user_to_enter_browser(app, port) return app diff --git a/linkedin/utils.py b/linkedin/utils.py index 7eda996..9213e3f 100644 --- a/linkedin/utils.py +++ b/linkedin/utils.py @@ -1,25 +1,14 @@ # -*- coding: utf-8 -*- import requests from .exceptions import LinkedInError, get_exception_for_error_code - -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - -try: - import simplejson as json -except ImportError: - try: - from django.utils import simplejson as json - except ImportError: - import json +import sys def enum(enum_type='enum', base_classes=None, methods=None, **attrs): """ Generates a enumeration with the given attributes. """ + # Enumerations can not be initalized as a new instance def __init__(instance, *args, **kwargs): raise RuntimeError('%s types can not be initialized.' % enum_type) @@ -31,7 +20,7 @@ def __init__(instance, *args, **kwargs): methods = {} base_classes = base_classes + (object,) - for k, v in methods.iteritems(): + for k, v in methods.items(): methods[k] = classmethod(v) attrs['enums'] = attrs.copy() @@ -40,11 +29,23 @@ def __init__(instance, *args, **kwargs): return type(enum_type, base_classes, methods) -def to_utf8(st): - if isinstance(st, unicode): - return st.encode('utf-8') - else: - return bytes(st) +if sys.version_info < (3,): + import __builtin__ + + + def to_utf8(x): + return __builtin__.unicode(x) + + + def to_string(x): + return str(x) +else: + def to_utf8(x): + return x + + + def to_string(x): + return x def raise_for_error(response):