├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.rst ├── examples ├── __init__.py ├── authentication.py └── http_api.py ├── linkedin ├── __init__.py ├── exceptions.py ├── linkedin.py ├── models.py ├── server.py └── utils.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | .idea 37 | 38 | .settings/org.eclipse.core.resources.prefs 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | script: python setup.py test 5 | notifications: 6 | - email: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2013 Ozgur Vatansever (ozgurvt@gmail.com) 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without restriction, 6 | including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, 8 | and to permit persons to whom the Software is furnished to do so, 9 | subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 16 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python LinkedIn 2 | 3 | Python interface to the LinkedIn API 4 | 5 | [![LinkedIn](http://developer.linkedin.com/sites/default/files/LinkedIn_Logo60px.png)](http://developer.linkedin.com) 6 | 7 | This library provides a pure Python interface to the LinkedIn **Profile**, **Group**, **Company**, **Jobs**, **Search**, **Share**, **Network** and **Invitation** REST APIs. 8 | 9 | [LinkedIn](http://developer.linkedin.com) provides a service that lets people bring their LinkedIn profiles and networks with them to your site or application via their OAuth based API. This library provides a lightweight interface over a complicated LinkedIn OAuth based API to make it for python programmers easy to use. 10 | 11 | ## Installation 12 | 13 | [![Build Status](https://travis-ci.org/ozgur/python-linkedin.png?branch=master)](https://travis-ci.org/ozgur/python-linkedin) 14 | 15 | You can install **python-linkedin** library via pip: 16 | 17 | $ pip install python-linkedin 18 | 19 | ## Authentication 20 | 21 | 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. 22 | 23 | ### HTTP API example 24 | 25 | Set `LINKEDIN_API_KEY` and `LINKEDIN_API_SECRET`, configure your app to redirect to `http://localhost:8080/code`, then execute: 26 | 27 | 0. `http_api.py` 28 | 1. Visit `http://localhost:8080` in your browser, curl or similar 29 | 2. A tab in your browser will open up, give LinkedIn permission there 30 | 3. You'll then be presented with a list of available routes, hit any, e.g.: 31 | 4. `curl -XGET http://localhost:8080/get_profile` 32 | 33 | ### Developer Authentication 34 | 35 | To connect to LinkedIn as a developer or just to access your own data, you don't even have to implement an OAuth 2.0 flow that involves redirects. You can simply use the 4 credentials that are provided to you in your LinkedIn appliation as part of an OAuth 1.0a flow and immediately access your data. Here's how: 36 | 37 | ```python 38 | from linkedin import linkedin 39 | 40 | # Define CONSUMER_KEY, CONSUMER_SECRET, 41 | # USER_TOKEN, and USER_SECRET from the credentials 42 | # provided in your LinkedIn application 43 | 44 | # Instantiate the developer authentication class 45 | 46 | authentication = linkedin.LinkedInDeveloperAuthentication(CONSUMER_KEY, CONSUMER_SECRET, 47 | USER_TOKEN, USER_SECRET, 48 | RETURN_URL, linkedin.PERMISSIONS.enums.values()) 49 | 50 | # Pass it in to the app... 51 | 52 | application = linkedin.LinkedInApplication(authentication) 53 | 54 | # Use the app.... 55 | 56 | application.get_profile() 57 | ``` 58 | 59 | 60 | ### Production Authentication 61 | In order to use the LinkedIn OAuth 2.0, you have an **application key** and **application secret**. You can get more detail from [here](http://developers.linkedin.com/documents/authentication). 62 | 63 | For debugging purposes you can use the credentials below. It belongs to my test application. Nothing's harmful. 64 | 65 | ```python 66 | KEY = 'wFNJekVpDCJtRPFX812pQsJee-gt0zO4X5XmG6wcfSOSlLocxodAXNMbl0_hw3Vl' 67 | SECRET = 'daJDa6_8UcnGMw1yuq9TjoO_PMKukXMo8vEMo7Qv5J-G3SPgrAV0FqFCd0TNjQyG' 68 | ``` 69 | You can also get those keys from [here](http://developer.linkedin.com/rest). 70 | 71 | LinkedIn redirects the user back to your website's URL after granting access (giving proper permissions) to your application. We call that url **RETURN URL**. Assuming your return url is **http://localhost:8000**, you can write something like this: 72 | 73 | ```python 74 | from linkedin import linkedin 75 | 76 | API_KEY = 'wFNJekVpDCJtRPFX812pQsJee-gt0zO4X5XmG6wcfSOSlLocxodAXNMbl0_hw3Vl' 77 | API_SECRET = 'daJDa6_8UcnGMw1yuq9TjoO_PMKukXMo8vEMo7Qv5J-G3SPgrAV0FqFCd0TNjQyG' 78 | RETURN_URL = 'http://localhost:8000' 79 | 80 | authentication = linkedin.LinkedInAuthentication(API_KEY, API_SECRET, RETURN_URL, linkedin.PERMISSIONS.enums.values()) 81 | # Optionally one can send custom "state" value that will be returned from OAuth server 82 | # It can be used to track your user state or something else (it's up to you) 83 | # Be aware that this value is sent to OAuth server AS IS - make sure to encode or hash it 84 | #authorization.state = 'your_encoded_message' 85 | print authentication.authorization_url # open this url on your browser 86 | application = linkedin.LinkedInApplication(authentication) 87 | ``` 88 | When you grant access to the application, you will be redirected to the return url with the following query strings appended to your **RETURN_URL**: 89 | 90 | ```python 91 | "http://localhost:8000/?code=AQTXrv3Pe1iWS0EQvLg0NJA8ju_XuiadXACqHennhWih7iRyDSzAm5jaf3R7I8&state=ea34a04b91c72863c82878d2b8f1836c" 92 | ``` 93 | 94 | This means that the value of the **authorization_code** is **AQTXrv3Pe1iWS0EQvLg0NJA8ju_XuiadXACqHennhWih7iRyDSzAm5jaf3R7I8**. After setting it by hand, we can call the **.get_access_token()** to get the actual token. 95 | 96 | ```python 97 | authentication.authorization_code = 'AQTXrv3Pe1iWS0EQvLg0NJA8ju_XuiadXACqHennhWih7iRyDSzAm5jaf3R7I8' 98 | authentication.get_access_token() 99 | ``` 100 | 101 | After you get the access token, you are now permitted to make API calls on behalf of the user who granted access to you app. In addition to that, in order to prevent from going through the OAuth flow for every consecutive request, 102 | one can directly assign the access token obtained before to the application instance. 103 | 104 | ```python 105 | application = linkedin.LinkedInApplication(token='AQTFtPILQkJzXHrHtyQ0rjLe3W0I') 106 | ``` 107 | 108 | ## Quick Usage From Python Interpreter 109 | 110 | For testing the library using an interpreter, you can benefit from the test server. 111 | 112 | ```python 113 | from linkedin import server 114 | application = server.quick_api(KEY, SECRET) 115 | ``` 116 | This will print the authorization url to the screen. Go into that URL using a browser to grant access to the application. After you do so, the method will return with an API object you can now use. 117 | 118 | ## Profile API 119 | The Profile API returns a member's LinkedIn profile. You can use this call to return one of two versions of a user's profile which are **public profile** and **standard profile**. For more information, check out the [documentation](http://developers.linkedin.com/documents/profile-api). 120 | 121 | ```python 122 | application.get_profile() 123 | {u'firstName': u'ozgur', 124 | u'headline': u'This is my headline', 125 | u'lastName': u'vatansever', 126 | u'siteStandardProfileRequest': {u'url': u'http://www.linkedin.com/profile/view?id=46113651&authType=name&authToken=Egbj&trk=api*a101945*s101945*'}} 127 | ``` 128 | 129 | There are many **field selectors** that enable the client fetch more information from the API. All of them used by each API are listed [here](http://developers.linkedin.com/documents/field-selectors). 130 | 131 | ```python 132 | application.get_profile(selectors=['id', 'first-name', 'last-name', 'location', 'distance', 'num-connections', 'skills', 'educations']) 133 | {u'distance': 0, 134 | u'educations': {u'_total': 1, 135 | u'values': [{u'activities': u'This is my activity and society field', 136 | u'degree': u'graduate', 137 | u'endDate': {u'year': 2009}, 138 | u'fieldOfStudy': u'computer science', 139 | u'id': 42611838, 140 | u'notes': u'This is my additional notes field', 141 | u'schoolName': u'\u0130stanbul Bilgi \xdcniversitesi', 142 | u'startDate': {u'year': 2004}}]}, 143 | u'firstName': u'ozgur', 144 | u'id': u'COjFALsKDP', 145 | u'lastName': u'vatansever', 146 | u'location': {u'country': {u'code': u'tr'}, u'name': u'Istanbul, Turkey'}, 147 | u'numConnections': 13} 148 | ``` 149 | 150 | ## Connections API 151 | The Connections API returns a list of **1st degree** connections for a user who has granted access to their account. For more information, you check out its [documentation](http://developers.linkedin.com/documents/connections-api). 152 | 153 | To fetch your connections, you simply call **.get_connections()** method with proper GET querystring: 154 | 155 | ```python 156 | application.get_connections() 157 | {u'_total': 13, 158 | u'values': [{u'apiStandardProfileRequest': {u'headers': {u'_total': 1, 159 | u'values': [{u'name': u'x-li-auth-token', u'value': u'name:16V1033'}]}, 160 | u'url': u'http://api.linkedin.com/v1/people/lddvGtD5xk'}, 161 | u'firstName': u'John', 162 | u'headline': u'Ruby', 163 | u'id': u'2323SDFSsfd34', 164 | u'industry': u'Computer Software', 165 | u'lastName': u'DOE', 166 | u'location': {u'country': {u'code': u'tr'}, u'name': u'Istanbul, Turkey'}, 167 | u'siteStandardProfileRequest': {u'url': u'http://www.linkedin.com/profile/view?id=049430532&authType=name&authToken=16V8&trk=api*a101945*s101945*'}}, 168 | .... 169 | 170 | application.get_connections(selectors=['headline', 'first-name', 'last-name'], params={'start':10, 'count':5}) 171 | ``` 172 | 173 | ## Search API 174 | There are 3 types of Search APIs. One is the **People Search** API, second one is the **Company Search** API and the last one is **Jobs Search** API. 175 | 176 | The People Search API returns information about people. It lets you implement most of what shows up when you do a search for "People" in the top right box on LinkedIn.com. 177 | You can get more information from [here](http://developers.linkedin.com/documents/people-search-api). 178 | 179 | ```python 180 | application.search_profile(selectors=[{'people': ['first-name', 'last-name']}], params={'keywords': 'apple microsoft'}) 181 | # Search URL is https://api.linkedin.com/v1/people-search:(people:(first-name,last-name))?keywords=apple%20microsoft 182 | 183 | {u'people': {u'_count': 10, 184 | u'_start': 0, 185 | u'_total': 2, 186 | u'values': [ 187 | {u'firstName': u'John', u'lastName': 'Doe'}, 188 | {u'firstName': u'Jane', u'lastName': u'Doe'} 189 | ]}} 190 | ``` 191 | 192 | The Company Search API enables search across company pages. You can get more information from [here](http://developers.linkedin.com/documents/company-search). 193 | 194 | ```python 195 | application.search_company(selectors=[{'companies': ['name', 'universal-name', 'website-url']}], params={'keywords': 'apple microsoft'}) 196 | # Search URL is https://api.linkedin.com/v1/company-search:(companies:(name,universal-name,website-url))?keywords=apple%20microsoft 197 | 198 | {u'companies': {u'_count': 10, 199 | u'_start': 0, 200 | u'_total': 1064, 201 | u'values': [{u'name': u'Netflix', 202 | u'universalName': u'netflix', 203 | u'websiteUrl': u'http://netflix.com'}, 204 | {u'name': u'Alliance Data', 205 | u'universalName': u'alliance-data', 206 | u'websiteUrl': u'www.alliancedata.com'}, 207 | {u'name': u'GHA Technologies', 208 | u'universalName': u'gha-technologies', 209 | u'websiteUrl': u'www.gha-associates.com'}, 210 | {u'name': u'Intelligent Decisions', 211 | u'universalName': u'intelligent-decisions', 212 | u'websiteUrl': u'http://www.intelligent.net'}, 213 | {u'name': u'Mindfire Solutions', 214 | u'universalName': u'mindfire-solutions', 215 | u'websiteUrl': u'www.mindfiresolutions.com'}, 216 | {u'name': u'Babel Media', 217 | u'universalName': u'babel-media', 218 | u'websiteUrl': u'http://www.babelmedia.com/'}, 219 | {u'name': u'Milestone Technologies', 220 | u'universalName': u'milestone-technologies', 221 | u'websiteUrl': u'www.milestonepowered.com'}, 222 | {u'name': u'Denali Advanced Integration', 223 | u'universalName': u'denali-advanced-integration', 224 | u'websiteUrl': u'www.denaliai.com'}, 225 | {u'name': u'MicroAge', 226 | u'universalName': u'microage', 227 | u'websiteUrl': u'www.microage.com'}, 228 | {u'name': u'TRUSTe', 229 | u'universalName': u'truste', 230 | u'websiteUrl': u'http://www.truste.com/'}]}} 231 | ``` 232 | 233 | The Job Search API enables search across LinkedIn's job postings. You can get more information from [here](http://developers.linkedin.com/documents/job-search-api). 234 | 235 | ```python 236 | application.search_job(selectors=[{'jobs': ['id', 'customer-job-code', 'posting-date']}], params={'title': 'python', 'count': 2}) 237 | {u'jobs': {u'_count': 2, 238 | u'_start': 0, 239 | u'_total': 206747, 240 | u'values': [{u'customerJobCode': u'0006YT23WQ', 241 | u'id': 5174636, 242 | u'postingDate': {u'day': 21, u'month': 3, u'year': 2013}}, 243 | {u'customerJobCode': u'00023CCVC2', 244 | u'id': 5174634, 245 | u'postingDate': {u'day': 21, u'month': 3, u'year': 2013}}]}} 246 | ``` 247 | 248 | ## Group API 249 | The Groups API provides rich access to read and interact with LinkedIn’s groups functionality. You can get more information from [here](http://developers.linkedin.com/documents/groups-api). By the help of the interface, you can fetch group details, get your group memberships as well as your posts for a specific group which you are a member of. 250 | 251 | ```python 252 | application.get_group(41001) 253 | {u'id': u'41001', u'name': u'Object Oriented Programming'} 254 | 255 | application.get_memberships(params={'count': 20}) 256 | {u'_total': 1, 257 | u'values': [{u'_key': u'25827', 258 | u'group': {u'id': u'25827', u'name': u'Python Community'}, 259 | u'membershipState': {u'code': u'member'}}]} 260 | 261 | application.get_posts(41001) 262 | 263 | application.get_post_comments( 264 | %POST_ID%, 265 | selectors=[ 266 | {"creator": ["first-name", "last-name"]}, 267 | "creation-timestamp", 268 | "text" 269 | ], 270 | params={"start": 0, "count": 20} 271 | ) 272 | ``` 273 | 274 | You can also submit a new post into a specific group. 275 | 276 | ```python 277 | title = 'Scala for the Impatient' 278 | summary = 'A new book has been published' 279 | submitted_url = 'http://horstmann.com/scala/' 280 | submitted_image_url = 'http://horstmann.com/scala/images/cover.png' 281 | description = 'It is a great book for the keen beginners. Check it out!' 282 | 283 | application.submit_group_post(41001, title, summary, submitted_url, submitted_image_url, description) 284 | ``` 285 | 286 | ## Company API 287 | The Company API: 288 | * Retrieves and displays one or more company profiles based on the company ID or universal name. 289 | * Returns basic company profile data, such as name, website, and industry. 290 | * Returns handles to additional company content, such as RSS stream and Twitter feed. 291 | 292 | You can query a company with either its **ID** or **Universal Name**. For more information, you can check out the documentation [here](http://developers.linkedin.com/documents/company-lookup-api-and-fields). 293 | 294 | ```python 295 | application.get_companies(company_ids=[1035], universal_names=['apple'], selectors=['name'], params={'is-company-admin': 'true'}) 296 | # 1035 is Microsoft 297 | # The URL is as follows: https://api.linkedin.com/v1/companies::(1035,universal-name=apple)?is-company-admin=true 298 | 299 | {u'_total': 2, 300 | u'values': [{u'_key': u'1035', u'name': u'Microsoft'}, 301 | {u'_key': u'universal-name=apple', u'name': u'Apple'}]} 302 | 303 | # Get the latest updates about Microsoft 304 | application.get_company_updates(1035, params={'count': 2}) 305 | {u'_count': 2, 306 | u'_start': 0, 307 | u'_total': 58, 308 | u'values': [{u'isCommentable': True, 309 | u'isLikable': True, 310 | u'isLiked': False, 311 | u'numLikes': 0, 312 | u'timestamp': 1363855486620, 313 | u'updateComments': {u'_total': 0}, 314 | u'updateContent': {u'company': {u'id': 1035, u'name': u'Microsoft'}, 315 | u'companyJobUpdate': {u'action': {u'code': u'created'}, 316 | u'job': {u'company': {u'id': 1035, u'name': u'Microsoft'}, 317 | u'description': u'Job Category: SalesLocation: Sacramento, CA, USJob ID: 812346-106756Division: Retail StoresStore...', 318 | u'id': 5173319, 319 | u'locationDescription': u'Sacramento, CA, US', 320 | u'position': {u'title': u'Store Manager, Specialty Store'}, 321 | u'siteJobRequest': {u'url': u'http://www.linkedin.com/jobs?viewJob=&jobId=5173319'}}}}, 322 | u'updateKey': u'UNIU-c1035-5720424522989961216-FOLLOW_CMPY', 323 | u'updateType': u'CMPY'}, 324 | {u'isCommentable': True, 325 | u'isLikable': True, 326 | u'isLiked': False, 327 | u'numLikes': 0, 328 | u'timestamp': 1363855486617, 329 | u'updateComments': {u'_total': 0}, 330 | u'updateContent': {u'company': {u'id': 1035, u'name': u'Microsoft'}, 331 | u'companyJobUpdate': {u'action': {u'code': u'created'}, 332 | u'job': {u'company': {u'id': 1035, u'name': u'Microsoft'}, 333 | u'description': u'Job Category: Software Engineering: TestLocation: Redmond, WA, USJob ID: 794953-81760Division:...', 334 | u'id': 5173313, 335 | u'locationDescription': u'Redmond, WA, US', 336 | u'position': {u'title': u'Software Development Engineer in Test, Senior-IEB-MSCIS (794953)'}, 337 | u'siteJobRequest': {u'url': u'http://www.linkedin.com/jobs?viewJob=&jobId=5173313'}}}}, 338 | u'updateKey': u'UNIU-c1035-5720424522977378304-FOLLOW_CMPY', 339 | u'updateType': u'CMPY'}]} 340 | ``` 341 | 342 | You can follow or unfollow a specific company as well. 343 | 344 | ```python 345 | application.follow_company(1035) 346 | True 347 | 348 | application.unfollow_company(1035) 349 | True 350 | ``` 351 | 352 | ## Job API 353 | The Jobs APIs provide access to view jobs and job data. You can get more information from its [documentation](http://developers.linkedin.com/documents/job-lookup-api-and-fields). 354 | 355 | ```python 356 | application.get_job(job_id=5174636) 357 | {u'active': True, 358 | u'company': {u'id': 2329, u'name': u'Schneider Electric'}, 359 | u'descriptionSnippet': u"The Industrial Accounts Sales Manager is a quota carrying senior sales position principally responsible for generating new sales and growing company's share of wallet within the industrial business, contracting business and consulting engineering business. The primary objective is to build and establish strong and lasting relationships with technical teams and at executive level within specific in", 360 | u'id': 5174636, 361 | u'position': {u'title': u'Industrial Accounts Sales Manager'}, 362 | u'postingTimestamp': 1363860033000} 363 | ``` 364 | 365 | You can also fetch you job bookmarks. 366 | 367 | ```python 368 | application.get_job_bookmarks() 369 | {u'_total': 0} 370 | ``` 371 | 372 | ## Share API 373 | Network updates serve as one of the core experiences on LinkedIn, giving users the ability to share rich content to their professional network. You can get more information from [here](http://developers.linkedin.com/documents/share-api). 374 | 375 | ``` 376 | application.submit_share('Posting from the API using JSON', 'A title for your share', None, 'http://www.linkedin.com', 'http://d.pr/3OWS') 377 | {'updateKey': u'UNIU-8219502-5705061301949063168-SHARE' 378 | 'updateURL': 'http://www.linkedin.com/updates?discuss=&scope=8219502&stype=M&topic=5705061301949063168&type=U&a=aovi'} 379 | ``` 380 | 381 | ## Network API 382 | The Get Network Updates API returns the users network updates, which is the LinkedIn term for the user's feed. This call returns most of what shows up in the middle column of the LinkedIn.com home page, either for the member or the member's connections. You can get more information from [here](http://developers.linkedin.com/documents/get-network-updates-and-statistics-api). 383 | 384 | There are many network update types. You can look at them by importing **NETWORK_UPDATES** enumeration. 385 | 386 | ```python 387 | from linkedin.linkedin import NETWORK_UPDATES 388 | print NETWORK_UPDATES.enums 389 | {'APPLICATION': 'APPS', 390 | 'CHANGED_PROFILE': 'PRFU', 391 | 'COMPANY': 'CMPY', 392 | 'CONNECTION': 'CONN', 393 | 'EXTENDED_PROFILE': 'PRFX', 394 | 'GROUP': 'JGRP', 395 | 'JOB': 'JOBS', 396 | 'PICTURE': 'PICT', 397 | 'SHARED': 'SHAR', 398 | 'VIRAL': 'VIRL'} 399 | 400 | update_types = (NETWORK_UPDATES.CONNECTION, NETWORK_UPDATES.PICTURE) 401 | application.get_network_updates(update_types) 402 | 403 | {u'_total': 1, 404 | u'values': [{u'isCommentable': True, 405 | u'isLikable': True, 406 | u'isLiked': False, 407 | u'numLikes': 0, 408 | u'timestamp': 1363470126509, 409 | u'updateComments': {u'_total': 0}, 410 | u'updateContent': {u'person': {u'apiStandardProfileRequest': {u'headers': {u'_total': 1, 411 | u'values': [{u'name': u'x-li-auth-token', u'value': u'name:Egbj'}]}, 412 | u'url': u'http://api.linkedin.com/v1/people/COjFALsKDP'}, 413 | u'firstName': u'ozgur', 414 | u'headline': u'This is my headline', 415 | u'id': u'COjFALsKDP', 416 | u'lastName': u'vatansever', 417 | u'siteStandardProfileRequest': {u'url': u'http://www.linkedin.com/profile/view?id=46113651&authType=name&authToken=Egbj&trk=api*a101945*s101945*'}}}, 418 | u'updateKey': u'UNIU-46113651-5718808205493026816-SHARE', 419 | u'updateType': u'SHAR'}]} 420 | ``` 421 | 422 | ## Invitation API 423 | The Invitation API allows your users to invite people they find in your application to their LinkedIn network. You can get more information from [here](http://developers.linkedin.com/documents/invitation-api). 424 | 425 | ```python 426 | from linkedin.models import LinkedInRecipient, LinkedInInvitation 427 | recipient = LinkedInRecipient(None, 'john.doe@python.org', 'John', 'Doe') 428 | print recipient.json 429 | {'person': {'_path': '/people/email=john.doe@python.org', 430 | 'first-name': 'John', 431 | 'last-name': 'Doe'}} 432 | 433 | invitation = LinkedInInvitation('Hello John', "What's up? Can I add you as a friend?", (recipient,), 'friend') 434 | print invitation.json 435 | {'body': "What's up? Can I add you as a friend?", 436 | 'item-content': {'invitation-request': {'connect-type': 'friend'}}, 437 | 'recipients': {'values': [{'person': {'_path': '/people/email=john.doe@python.org', 438 | 'first-name': 'John', 439 | 'last-name': 'Doe'}}]}, 440 | 'subject': 'Hello John'} 441 | 442 | application.send_invitation(invitation) 443 | True 444 | ``` 445 | 446 | ## Throttle Limits 447 | 448 | LinkedIn API keys are throttled by default. You should take a look at the [Throttle Limits Documentation](http://developer.linkedin.com/documents/throttle-limits) to get more information about it. 449 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Python LinkedIn 2 | ================= 3 | 4 | Python interface to the LinkedIn API 5 | 6 | This library provides a pure Python interface to the LinkedIn **Profile**, **Group**, **Company**, **Jobs**, **Search**, **Share**, **Network** and **Invitation** REST APIs. 7 | 8 | `LinkedIn `_ provides a service that lets people bring their LinkedIn profiles and networks with them to your site or application via their OAuth based API. This library provides a lightweight interface over a complicated LinkedIn OAuth based API to make it for python programmers easy to use. 9 | 10 | Installation 11 | -------------------- 12 | 13 | You can install **python-linkedin** library via pip: 14 | 15 | .. code-block:: bash 16 | 17 | $ pip install python-linkedin 18 | 19 | Authentication 20 | ----------------------- 21 | 22 | LinkedIn REST API uses **Oauth 2.0** protocol for authentication. In order to use the LinkedIn API, you have an **application key** and **application secret**. You can get more detail from `here `_. 23 | 24 | For debugging purposes you can use the credentials below. It belongs to my test application. Nothing's harmful. 25 | 26 | .. code-block:: python 27 | 28 | KEY = 'wFNJekVpDCJtRPFX812pQsJee-gt0zO4X5XmG6wcfSOSlLocxodAXNMbl0_hw3Vl' 29 | SECRET = 'daJDa6_8UcnGMw1yuq9TjoO_PMKukXMo8vEMo7Qv5J-G3SPgrAV0FqFCd0TNjQyG' 30 | 31 | 32 | LinkedIn redirects the user back to your website's URL after granting access (giving proper permissions) to your application. We call that url **RETURN URL**. Assuming your return url is **http://localhost:8000**, you can write something like this: 33 | 34 | .. code-block:: python 35 | 36 | from linkedin import linkedin 37 | 38 | API_KEY = "wFNJekVpDCJtRPFX812pQsJee-gt0zO4X5XmG6wcfSOSlLocxodAXNMbl0_hw3Vl" 39 | API_SECRET = "daJDa6_8UcnGMw1yuq9TjoO_PMKukXMo8vEMo7Qv5J-G3SPgrAV0FqFCd0TNjQyG" 40 | RETURN_URL = "http://localhost:8000" 41 | # Optionally one can send custom "state" value that will be returned from OAuth server 42 | # It can be used to track your user state or something else (it's up to you) 43 | # Be aware that this value is sent to OAuth server AS IS - make sure to encode or hash it 44 | #authorization.state = 'your_encoded_message' 45 | authentication = linkedin.LinkedInAuthentication(API_KEY, API_SECRET, RETURN_URL, linkedin.PERMISSIONS.enums.values()) 46 | print authentication.authorization_url 47 | application = linkedin.LinkedInApplication(authentication) 48 | 49 | When you grant access to the application, you will be redirected to the return url with the following query strings appended to your **RETURN_URL**: 50 | 51 | .. code-block:: python 52 | 53 | "http://localhost:8000/?code=AQTXrv3Pe1iWS0EQvLg0NJA8ju_XuiadXACqHennhWih7iRyDSzAm5jaf3R7I8&state=ea34a04b91c72863c82878d2b8f1836c" 54 | 55 | 56 | This means that the value of the **authorization_code** is **AQTXrv3Pe1iWS0EQvLg0NJA8ju_XuiadXACqHennhWih7iRyDSzAm5jaf3R7I8**. After setting it by hand, we can call the **.get_access_token()** to get the actual token. 57 | 58 | .. code-block:: python 59 | 60 | authentication.authorization_code = "AQTXrv3Pe1iWS0EQvLg0NJA8ju_XuiadXACqHennhWih7iRyDSzAm5jaf3R7I8" 61 | authentication.get_access_token() 62 | 63 | 64 | After you get the access token, you are now permitted to make API calls on behalf of the user who granted access to you app. In addition to that, in order to prevent from going through the OAuth flow for every consecutive request, 65 | one can directly assign the access token obtained before to the application instance. 66 | 67 | 68 | .. code-block:: python 69 | 70 | application = linkedin.LinkedInApplication(token='AQTFtPILQkJzXHrHtyQ0rjLe3W0I') 71 | 72 | 73 | Quick Usage From Python Interpreter 74 | --------------------------------------------------------- 75 | 76 | For testing the library using an interpreter, use the quick helper. 77 | 78 | .. code-block:: python 79 | 80 | from linkedin import server 81 | application = server.quick_api(KEY, SECRET) 82 | 83 | This will print the authorization url to the screen. Go into this URL using a browser, after you login, the method will return with an API object you can now use. 84 | 85 | .. code-block:: python 86 | 87 | application.get_profile() 88 | 89 | 90 | More 91 | ----------------- 92 | For more information, visit the `homepage `_ of the project. 93 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ozgur/python-linkedin/9832fd995d6f74f14700bdbeeea3ab39151e737f/examples/__init__.py -------------------------------------------------------------------------------- /examples/authentication.py: -------------------------------------------------------------------------------- 1 | from linkedin.linkedin import (LinkedInAuthentication, LinkedInApplication, 2 | PERMISSIONS) 3 | 4 | 5 | if __name__ == '__main__': 6 | API_KEY = 'wFNJekVpDCJtRPFX812pQsJee-gt0zO4X5XmG6wcfSOSlLocxodAXNMbl0_hw3Vl' 7 | API_SECRET = 'daJDa6_8UcnGMw1yuq9TjoO_PMKukXMo8vEMo7Qv5J-G3SPgrAV0FqFCd0TNjQyG' 8 | RETURN_URL = 'http://localhost:8000' 9 | authentication = LinkedInAuthentication(API_KEY, API_SECRET, RETURN_URL, 10 | PERMISSIONS.enums.values()) 11 | print authentication.authorization_url 12 | application = LinkedInApplication(authentication) 13 | -------------------------------------------------------------------------------- /examples/http_api.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Samuel Marks ' 2 | __version__ = '0.1.0' 3 | 4 | try: 5 | from urllib.parse import urlparse 6 | except ImportError: 7 | from urlparse import urlparse 8 | 9 | from socketserver import ThreadingTCPServer 10 | from http.server import SimpleHTTPRequestHandler 11 | 12 | from webbrowser import open_new_tab 13 | from json import dumps 14 | from os import environ 15 | 16 | 17 | from linkedin.linkedin import LinkedInAuthentication, LinkedInApplication, PERMISSIONS 18 | 19 | PORT = 8080 20 | 21 | 22 | class LinkedInWrapper(object): 23 | """ Simple namespacing """ 24 | API_KEY = environ.get('LINKEDIN_API_KEY') 25 | API_SECRET = environ.get('LINKEDIN_API_SECRET') 26 | RETURN_URL = 'http://localhost:{0}/code'.format(globals()['PORT']) 27 | authentication = LinkedInAuthentication(API_KEY, API_SECRET, RETURN_URL, PERMISSIONS.enums.values()) 28 | application = LinkedInApplication(authentication) 29 | 30 | 31 | liw = LinkedInWrapper() 32 | run_already = False 33 | params_to_d = lambda params: { 34 | l[0]: l[1] for l in map(lambda j: j.split('='), urlparse(params).query.split('&')) 35 | } 36 | 37 | 38 | class CustomHandler(SimpleHTTPRequestHandler): 39 | def json_headers(self, status_code=200): 40 | self.send_response(status_code) 41 | self.send_header('Content-type', 'application/json') 42 | self.end_headers() 43 | 44 | def do_GET(self): 45 | parsedurl = urlparse(self.path) 46 | authed = liw.authentication.token is not None 47 | 48 | if parsedurl.path == '/code': 49 | self.json_headers() 50 | 51 | liw.authentication.authorization_code = params_to_d(self.path).get('code') 52 | self.wfile.write(dumps({'access_token': liw.authentication.get_access_token(), 53 | 'routes': list(filter(lambda d: not d.startswith('_'), dir(liw.application)))}).encode('utf8')) 54 | elif parsedurl.path == '/routes': 55 | self.json_headers() 56 | 57 | self.wfile.write(dumps({'routes': list(filter(lambda d: not d.startswith('_'), dir(liw.application)))}).encode('utf8')) 58 | elif not authed: 59 | self.json_headers() 60 | 61 | if not globals()['run_already']: 62 | open_new_tab(liw.authentication.authorization_url) 63 | globals()['run_already'] = True 64 | self.wfile.write(dumps({'path': self.path, 'authed': type(liw.authentication.token) is None}).encode('utf8')) 65 | elif authed and len(parsedurl.path) and parsedurl.path[1:] in dir(liw.application): 66 | self.json_headers() 67 | self.wfile.write(dumps(getattr(liw.application, parsedurl.path[1:])()).encode('utf8')) 68 | else: 69 | self.json_headers(501) 70 | self.wfile.write(dumps({'error': 'NotImplemented'}).encode('utf8')) 71 | 72 | 73 | if __name__ == '__main__': 74 | httpd = ThreadingTCPServer(('localhost', PORT), CustomHandler) 75 | 76 | print('Server started on port:{}'.format(PORT)) 77 | httpd.serve_forever() 78 | -------------------------------------------------------------------------------- /linkedin/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '4.2' 2 | VERSION = tuple(map(int, __version__.split('.'))) 3 | -------------------------------------------------------------------------------- /linkedin/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | class LinkedInError(Exception): 4 | pass 5 | 6 | 7 | class LinkedInBadRequestError(LinkedInError): 8 | pass 9 | 10 | 11 | class LinkedInUnauthorizedError(LinkedInError): 12 | pass 13 | 14 | 15 | class LinkedInPaymentRequiredError(LinkedInError): 16 | pass 17 | 18 | 19 | class LinkedInNotFoundError(LinkedInError): 20 | pass 21 | 22 | 23 | class LinkedInConflictError(LinkedInError): 24 | pass 25 | 26 | 27 | class LinkedInForbiddenError(LinkedInError): 28 | pass 29 | 30 | 31 | class LinkedInInternalServiceError(LinkedInError): 32 | pass 33 | 34 | 35 | ERROR_CODE_EXCEPTION_MAPPING = { 36 | 400: LinkedInBadRequestError, 37 | 401: LinkedInUnauthorizedError, 38 | 402: LinkedInPaymentRequiredError, 39 | 403: LinkedInForbiddenError, 40 | 404: LinkedInNotFoundError, 41 | 409: LinkedInForbiddenError, 42 | 500: LinkedInInternalServiceError} 43 | 44 | 45 | def get_exception_for_error_code(error_code): 46 | return ERROR_CODE_EXCEPTION_MAPPING.get(error_code, LinkedInError) 47 | -------------------------------------------------------------------------------- /linkedin/linkedin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | import contextlib 4 | import hashlib 5 | import random 6 | 7 | try: 8 | from urllib.parse import quote, quote_plus 9 | except ImportError: 10 | from urllib import quote, quote_plus 11 | 12 | import requests 13 | from requests_oauthlib import OAuth1 14 | 15 | from .exceptions import LinkedInError 16 | from .models import AccessToken, LinkedInInvitation, LinkedInMessage 17 | from .utils import enum, to_utf8, raise_for_error, json, StringIO 18 | 19 | 20 | __all__ = ['LinkedInAuthentication', 'LinkedInApplication', 'PERMISSIONS'] 21 | 22 | PERMISSIONS = enum('Permission', 23 | COMPANY_ADMIN='rw_company_admin', 24 | BASIC_PROFILE='r_basicprofile', 25 | FULL_PROFILE='r_fullprofile', 26 | EMAIL_ADDRESS='r_emailaddress', 27 | NETWORK='r_network', 28 | CONTACT_INFO='r_contactinfo', 29 | NETWORK_UPDATES='rw_nus', 30 | GROUPS='rw_groups', 31 | MESSAGES='w_messages') 32 | 33 | ENDPOINTS = enum('LinkedInURL', 34 | PEOPLE='https://api.linkedin.com/v1/people', 35 | PEOPLE_SEARCH='https://api.linkedin.com/v1/people-search', 36 | GROUPS='https://api.linkedin.com/v1/groups', 37 | POSTS='https://api.linkedin.com/v1/posts', 38 | COMPANIES='https://api.linkedin.com/v1/companies', 39 | COMPANY_SEARCH='https://api.linkedin.com/v1/company-search', 40 | JOBS='https://api.linkedin.com/v1/jobs', 41 | JOB_SEARCH='https://api.linkedin.com/v1/job-search') 42 | 43 | NETWORK_UPDATES = enum('NetworkUpdate', 44 | APPLICATION='APPS', 45 | COMPANY='CMPY', 46 | CONNECTION='CONN', 47 | JOB='JOBS', 48 | GROUP='JGRP', 49 | PICTURE='PICT', 50 | EXTENDED_PROFILE='PRFX', 51 | CHANGED_PROFILE='PRFU', 52 | SHARED='SHAR', 53 | VIRAL='VIRL') 54 | 55 | 56 | class LinkedInDeveloperAuthentication(object): 57 | """ 58 | Uses all four credentials provided by LinkedIn as part of an OAuth 1.0a 59 | flow that provides instant API access with no redirects/approvals required. 60 | Useful for situations in which users would like to access their own data or 61 | during the development process. 62 | """ 63 | 64 | def __init__(self, consumer_key, consumer_secret, user_token, user_secret, 65 | redirect_uri, permissions=[]): 66 | self.consumer_key = consumer_key 67 | self.consumer_secret = consumer_secret 68 | self.user_token = user_token 69 | self.user_secret = user_secret 70 | self.redirect_uri = redirect_uri 71 | self.permissions = permissions 72 | 73 | 74 | class LinkedInAuthentication(object): 75 | """ 76 | Implements a standard OAuth 2.0 flow that involves redirection for users to 77 | authorize the application to access account data. 78 | """ 79 | AUTHORIZATION_URL = 'https://www.linkedin.com/uas/oauth2/authorization' 80 | ACCESS_TOKEN_URL = 'https://www.linkedin.com/uas/oauth2/accessToken' 81 | 82 | def __init__(self, key, secret, redirect_uri, permissions=None): 83 | self.key = key 84 | self.secret = secret 85 | self.redirect_uri = redirect_uri 86 | self.permissions = permissions or [] 87 | self.state = None 88 | self.authorization_code = None 89 | self.token = None 90 | self._error = None 91 | 92 | @property 93 | def authorization_url(self): 94 | qd = {'response_type': 'code', 95 | 'client_id': self.key, 96 | 'scope': (' '.join(self.permissions)).strip(), 97 | 'state': self.state or self._make_new_state(), 98 | 'redirect_uri': self.redirect_uri} 99 | # urlencode uses quote_plus when encoding the query string so, 100 | # we ought to be encoding the qs by on our own. 101 | qsl = ['%s=%s' % (quote(k), quote(v)) for k, v in qd.items()] 102 | return '%s?%s' % (self.AUTHORIZATION_URL, '&'.join(qsl)) 103 | 104 | @property 105 | def last_error(self): 106 | return self._error 107 | 108 | def _make_new_state(self): 109 | return hashlib.md5( 110 | '{}{}'.format(random.randrange(0, 2 ** 63), self.secret).encode("utf8") 111 | ).hexdigest() 112 | 113 | def get_access_token(self, timeout=60): 114 | assert self.authorization_code, 'You must first get the authorization code' 115 | qd = {'grant_type': 'authorization_code', 116 | 'code': self.authorization_code, 117 | 'redirect_uri': self.redirect_uri, 118 | 'client_id': self.key, 119 | 'client_secret': self.secret} 120 | response = requests.post(self.ACCESS_TOKEN_URL, data=qd, timeout=timeout) 121 | raise_for_error(response) 122 | response = response.json() 123 | self.token = AccessToken(response['access_token'], response['expires_in']) 124 | return self.token 125 | 126 | 127 | class LinkedInSelector(object): 128 | @classmethod 129 | def parse(cls, selector): 130 | with contextlib.closing(StringIO()) as result: 131 | if type(selector) == dict: 132 | for k, v in selector.items(): 133 | result.write('%s:(%s)' % (to_utf8(k), cls.parse(v))) 134 | elif type(selector) in (list, tuple): 135 | result.write(','.join(map(cls.parse, selector))) 136 | else: 137 | result.write(to_utf8(selector)) 138 | return result.getvalue() 139 | 140 | 141 | class LinkedInApplication(object): 142 | BASE_URL = 'https://api.linkedin.com' 143 | 144 | def __init__(self, authentication=None, token=None): 145 | assert authentication or token, 'Either authentication instance or access token is required' 146 | self.authentication = authentication 147 | if not self.authentication: 148 | self.authentication = LinkedInAuthentication('', '', '') 149 | self.authentication.token = AccessToken(token, None) 150 | 151 | def make_request(self, method, url, data=None, params=None, headers=None, 152 | timeout=60): 153 | if headers is None: 154 | headers = {'x-li-format': 'json', 'Content-Type': 'application/json'} 155 | else: 156 | headers.update({'x-li-format': 'json', 'Content-Type': 'application/json'}) 157 | 158 | if params is None: 159 | params = {} 160 | kw = dict(data=data, params=params, 161 | headers=headers, timeout=timeout) 162 | 163 | if isinstance(self.authentication, LinkedInDeveloperAuthentication): 164 | # Let requests_oauthlib.OAuth1 do *all* of the work here 165 | auth = OAuth1(self.authentication.consumer_key, self.authentication.consumer_secret, 166 | self.authentication.user_token, self.authentication.user_secret) 167 | kw.update({'auth': auth}) 168 | else: 169 | params.update({'oauth2_access_token': self.authentication.token.access_token}) 170 | 171 | return requests.request(method.upper(), url, **kw) 172 | 173 | def get_profile(self, member_id=None, member_url=None, selectors=None, 174 | params=None, headers=None): 175 | if member_id: 176 | if type(member_id) is list: 177 | # Batch request, ids as CSV. 178 | url = '%s::(%s)' % (ENDPOINTS.PEOPLE, 179 | ','.join(member_id)) 180 | else: 181 | url = '%s/id=%s' % (ENDPOINTS.PEOPLE, str(member_id)) 182 | elif member_url: 183 | url = '%s/url=%s' % (ENDPOINTS.PEOPLE, quote_plus(member_url)) 184 | else: 185 | url = '%s/~' % ENDPOINTS.PEOPLE 186 | if selectors: 187 | url = '%s:(%s)' % (url, LinkedInSelector.parse(selectors)) 188 | 189 | response = self.make_request('GET', url, params=params, headers=headers) 190 | raise_for_error(response) 191 | return response.json() 192 | 193 | def search_profile(self, selectors=None, params=None, headers=None): 194 | if selectors: 195 | url = '%s:(%s)' % (ENDPOINTS.PEOPLE_SEARCH, 196 | LinkedInSelector.parse(selectors)) 197 | else: 198 | url = ENDPOINTS.PEOPLE_SEARCH 199 | response = self.make_request('GET', url, params=params, headers=headers) 200 | raise_for_error(response) 201 | return response.json() 202 | 203 | def get_picture_urls(self, member_id=None, member_url=None, 204 | params=None, headers=None): 205 | if member_id: 206 | url = '%s/id=%s/picture-urls::(original)' % (ENDPOINTS.PEOPLE, str(member_id)) 207 | elif member_url: 208 | url = '%s/url=%s/picture-urls::(original)' % (ENDPOINTS.PEOPLE, 209 | quote_plus(member_url)) 210 | else: 211 | url = '%s/~/picture-urls::(original)' % ENDPOINTS.PEOPLE 212 | 213 | response = self.make_request('GET', url, params=params, headers=headers) 214 | raise_for_error(response) 215 | return response.json() 216 | 217 | def get_connections(self, member_id=None, member_url=None, selectors=None, 218 | params=None, headers=None): 219 | if member_id: 220 | url = '%s/id=%s/connections' % (ENDPOINTS.PEOPLE, str(member_id)) 221 | elif member_url: 222 | url = '%s/url=%s/connections' % (ENDPOINTS.PEOPLE, 223 | quote_plus(member_url)) 224 | else: 225 | url = '%s/~/connections' % ENDPOINTS.PEOPLE 226 | if selectors: 227 | url = '%s:(%s)' % (url, LinkedInSelector.parse(selectors)) 228 | 229 | response = self.make_request('GET', url, params=params, headers=headers) 230 | raise_for_error(response) 231 | return response.json() 232 | 233 | def get_memberships(self, member_id=None, member_url=None, group_id=None, 234 | selectors=None, params=None, headers=None): 235 | if member_id: 236 | url = '%s/id=%s/group-memberships' % (ENDPOINTS.PEOPLE, str(member_id)) 237 | elif member_url: 238 | url = '%s/url=%s/group-memberships' % (ENDPOINTS.PEOPLE, 239 | quote_plus(member_url)) 240 | else: 241 | url = '%s/~/group-memberships' % ENDPOINTS.PEOPLE 242 | 243 | if group_id: 244 | url = '%s/%s' % (url, str(group_id)) 245 | 246 | if selectors: 247 | url = '%s:(%s)' % (url, LinkedInSelector.parse(selectors)) 248 | 249 | response = self.make_request('GET', url, params=params, headers=headers) 250 | raise_for_error(response) 251 | return response.json() 252 | 253 | def get_group(self, group_id, selectors=None, params=None, headers=None): 254 | url = '%s/%s' % (ENDPOINTS.GROUPS, str(group_id)) 255 | 256 | response = self.make_request('GET', url, params=params, headers=headers) 257 | raise_for_error(response) 258 | return response.json() 259 | 260 | def get_posts(self, group_id, post_ids=None, selectors=None, params=None, 261 | headers=None): 262 | url = '%s/%s/posts' % (ENDPOINTS.GROUPS, str(group_id)) 263 | if post_ids: 264 | url = '%s::(%s)' % (url, ','.join(map(str, post_ids))) 265 | if selectors: 266 | url = '%s:(%s)' % (url, LinkedInSelector.parse(selectors)) 267 | 268 | response = self.make_request('GET', url, params=params, headers=headers) 269 | raise_for_error(response) 270 | return response.json() 271 | 272 | def get_post_comments(self, post_id, selectors=None, params=None, headers=None): 273 | url = '%s/%s/comments' % (ENDPOINTS.POSTS, post_id) 274 | if selectors: 275 | url = '%s:(%s)' % (url, LinkedInSelector.parse(selectors)) 276 | 277 | response = self.make_request('GET', url, params=params, headers=headers) 278 | raise_for_error(response) 279 | return response.json() 280 | 281 | def join_group(self, group_id): 282 | url = '%s/~/group-memberships/%s' % (ENDPOINTS.PEOPLE, str(group_id)) 283 | response = self.make_request('PUT', url, 284 | data=json.dumps({'membershipState': {'code': 'member'}})) 285 | raise_for_error(response) 286 | return True 287 | 288 | def leave_group(self, group_id): 289 | url = '%s/~/group-memberships/%s' % (ENDPOINTS.PEOPLE, str(group_id)) 290 | response = self.make_request('DELETE', url) 291 | raise_for_error(response) 292 | return True 293 | 294 | def submit_group_post(self, group_id, title, summary, submitted_url, 295 | submitted_image_url, content_title, description): 296 | post = { 297 | 'title': title, 'summary': summary, 298 | 'content': { 299 | 'submitted-url': submitted_url, 300 | 'title': content_title, 301 | 'description': description 302 | } 303 | } 304 | if submitted_image_url: 305 | post['content']['submitted-image-url'] = submitted_image_url 306 | 307 | url = '%s/%s/posts' % (ENDPOINTS.GROUPS, str(group_id)) 308 | response = self.make_request('POST', url, data=json.dumps(post)) 309 | raise_for_error(response) 310 | return True 311 | 312 | def like_post(self, post_id, action): 313 | url = '%s/%s/relation-to-viewer/is-liked' % (ENDPOINTS.POSTS, str(post_id)) 314 | try: 315 | self.make_request('PUT', url, data=json.dumps(action)) 316 | except (requests.ConnectionError, requests.HTTPError) as error: 317 | raise LinkedInError(error.message) 318 | else: 319 | return True 320 | 321 | def comment_post(self, post_id, comment): 322 | post = { 323 | 'text': comment 324 | } 325 | url = '%s/%s/comments' % (ENDPOINTS.POSTS, str(post_id)) 326 | try: 327 | self.make_request('POST', url, data=json.dumps(post)) 328 | except (requests.ConnectionError, requests.HTTPError) as error: 329 | raise LinkedInError(error.message) 330 | else: 331 | return True 332 | 333 | def get_company_by_email_domain(self, email_domain, params=None, headers=None): 334 | url = '%s?email-domain=%s' % (ENDPOINTS.COMPANIES, email_domain) 335 | 336 | response = self.make_request('GET', url, params=params, headers=headers) 337 | raise_for_error(response) 338 | return response.json() 339 | 340 | def get_companies(self, company_ids=None, universal_names=None, selectors=None, 341 | params=None, headers=None): 342 | identifiers = [] 343 | url = ENDPOINTS.COMPANIES 344 | if company_ids: 345 | identifiers += map(str, company_ids) 346 | 347 | if universal_names: 348 | identifiers += ['universal-name=%s' % un for un in universal_names] 349 | 350 | if identifiers: 351 | url = '%s::(%s)' % (url, ','.join(identifiers)) 352 | 353 | if selectors: 354 | url = '%s:(%s)' % (url, LinkedInSelector.parse(selectors)) 355 | 356 | response = self.make_request('GET', url, params=params, headers=headers) 357 | raise_for_error(response) 358 | return response.json() 359 | 360 | def get_company_updates(self, company_id, params=None, headers=None): 361 | url = '%s/%s/updates' % (ENDPOINTS.COMPANIES, str(company_id)) 362 | response = self.make_request('GET', url, params=params, headers=headers) 363 | raise_for_error(response) 364 | return response.json() 365 | 366 | def get_company_products(self, company_id, selectors=None, params=None, 367 | headers=None): 368 | url = '%s/%s/products' % (ENDPOINTS.COMPANIES, str(company_id)) 369 | if selectors: 370 | url = '%s:(%s)' % (url, LinkedInSelector.parse(selectors)) 371 | response = self.make_request('GET', url, params=params, headers=headers) 372 | raise_for_error(response) 373 | return response.json() 374 | 375 | def follow_company(self, company_id): 376 | url = '%s/~/following/companies' % ENDPOINTS.PEOPLE 377 | post = {'id': company_id} 378 | response = self.make_request('POST', url, data=json.dumps(post)) 379 | raise_for_error(response) 380 | return True 381 | 382 | def unfollow_company(self, company_id): 383 | url = '%s/~/following/companies/id=%s' % (ENDPOINTS.PEOPLE, str(company_id)) 384 | response = self.make_request('DELETE', url) 385 | raise_for_error(response) 386 | return True 387 | 388 | def search_company(self, selectors=None, params=None, headers=None): 389 | url = ENDPOINTS.COMPANY_SEARCH 390 | if selectors: 391 | url = '%s:(%s)' % (url, LinkedInSelector.parse(selectors)) 392 | 393 | response = self.make_request('GET', url, params=params, headers=headers) 394 | raise_for_error(response) 395 | return response.json() 396 | 397 | def submit_company_share(self, company_id, comment=None, title=None, description=None, 398 | submitted_url=None, submitted_image_url=None, 399 | visibility_code='anyone'): 400 | 401 | post = { 402 | 'visibility': { 403 | 'code': visibility_code, 404 | }, 405 | } 406 | if comment is not None: 407 | post['comment'] = comment 408 | if title is not None and submitted_url is not None: 409 | post['content'] = { 410 | 'title': title, 411 | 'submitted-url': submitted_url, 412 | 'description': description, 413 | } 414 | if submitted_image_url: 415 | post['content']['submitted-image-url'] = submitted_image_url 416 | 417 | url = '%s/%s/shares' % (ENDPOINTS.COMPANIES, company_id) 418 | 419 | response = self.make_request('POST', url, data=json.dumps(post)) 420 | raise_for_error(response) 421 | return response.json() 422 | 423 | def get_job(self, job_id, selectors=None, params=None, headers=None): 424 | url = '%s/%s' % (ENDPOINTS.JOBS, str(job_id)) 425 | url = '%s:(%s)' % (url, LinkedInSelector.parse(selectors)) 426 | response = self.make_request('GET', url, params=params, headers=headers) 427 | raise_for_error(response) 428 | return response.json() 429 | 430 | def get_job_bookmarks(self, selectors=None, params=None, headers=None): 431 | url = '%s/~/job-bookmarks' % ENDPOINTS.PEOPLE 432 | if selectors: 433 | url = '%s:(%s)' % (url, LinkedInSelector.parse(selectors)) 434 | 435 | response = self.make_request('GET', url, params=params, headers=headers) 436 | raise_for_error(response) 437 | return response.json() 438 | 439 | def search_job(self, selectors=None, params=None, headers=None): 440 | url = ENDPOINTS.JOB_SEARCH 441 | if selectors: 442 | url = '%s:(%s)' % (url, LinkedInSelector.parse(selectors)) 443 | 444 | response = self.make_request('GET', url, params=params, headers=headers) 445 | raise_for_error(response) 446 | return response.json() 447 | 448 | def submit_share(self, comment=None, title=None, description=None, 449 | submitted_url=None, submitted_image_url=None, 450 | visibility_code='anyone'): 451 | post = { 452 | 'visibility': { 453 | 'code': visibility_code, 454 | }, 455 | } 456 | if comment is not None: 457 | post['comment'] = comment 458 | if title is not None and submitted_url is not None: 459 | post['content'] = { 460 | 'title': title, 461 | 'submitted-url': submitted_url, 462 | 'description': description, 463 | } 464 | if submitted_image_url: 465 | post['content']['submitted-image-url'] = submitted_image_url 466 | 467 | url = '%s/~/shares' % ENDPOINTS.PEOPLE 468 | response = self.make_request('POST', url, data=json.dumps(post)) 469 | raise_for_error(response) 470 | return response.json() 471 | 472 | def get_network_updates(self, types, member_id=None, 473 | self_scope=True, params=None, headers=None): 474 | if member_id: 475 | url = '%s/id=%s/network/updates' % (ENDPOINTS.PEOPLE, 476 | str(member_id)) 477 | else: 478 | url = '%s/~/network/updates' % ENDPOINTS.PEOPLE 479 | 480 | if not params: 481 | params = {} 482 | 483 | if types: 484 | params.update({'type': types}) 485 | 486 | if self_scope is True: 487 | params.update({'scope': 'self'}) 488 | 489 | response = self.make_request('GET', url, params=params, headers=headers) 490 | raise_for_error(response) 491 | return response.json() 492 | 493 | def get_network_update(self, types, update_key, 494 | self_scope=True, params=None, headers=None): 495 | url = '%s/~/network/updates/key=%s' % (ENDPOINTS.PEOPLE, str(update_key)) 496 | 497 | if not params: 498 | params = {} 499 | 500 | if types: 501 | params.update({'type': types}) 502 | 503 | if self_scope is True: 504 | params.update({'scope': 'self'}) 505 | 506 | response = self.make_request('GET', url, params=params, headers=headers) 507 | raise_for_error(response) 508 | return response.json() 509 | 510 | def get_network_status(self, params=None, headers=None): 511 | url = '%s/~/network/network-stats' % ENDPOINTS.PEOPLE 512 | response = self.make_request('GET', url, params=params, headers=headers) 513 | raise_for_error(response) 514 | return response.json() 515 | 516 | def send_invitation(self, invitation): 517 | assert type(invitation) == LinkedInInvitation, 'LinkedInInvitation required' 518 | url = '%s/~/mailbox' % ENDPOINTS.PEOPLE 519 | response = self.make_request('POST', url, 520 | data=json.dumps(invitation.json)) 521 | raise_for_error(response) 522 | return True 523 | 524 | def send_message(self, message): 525 | assert type(message) == LinkedInMessage, 'LinkedInInvitation required' 526 | url = '%s/~/mailbox' % ENDPOINTS.PEOPLE 527 | response = self.make_request('POST', url, 528 | data=json.dumps(message.json)) 529 | raise_for_error(response) 530 | return True 531 | 532 | def comment_on_update(self, update_key, comment): 533 | comment = {'comment': comment} 534 | url = '%s/~/network/updates/key=%s/update-comments' % (ENDPOINTS.PEOPLE, update_key) 535 | response = self.make_request('POST', url, data=json.dumps(comment)) 536 | raise_for_error(response) 537 | return True 538 | 539 | def like_update(self, update_key, is_liked=True): 540 | url = '%s/~/network/updates/key=%s/is-liked' % (ENDPOINTS.PEOPLE, update_key) 541 | response = self.make_request('PUT', url, data=json.dumps(is_liked)) 542 | raise_for_error(response) 543 | return True 544 | -------------------------------------------------------------------------------- /linkedin/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import collections 3 | 4 | AccessToken = collections.namedtuple('AccessToken', ['access_token', 'expires_in']) 5 | 6 | 7 | class LinkedInRecipient(object): 8 | def __init__(self, member_id, email, first_name, last_name): 9 | assert member_id or email, 'Either member ID or email must be given' 10 | if member_id: 11 | self.member_id = str(member_id) 12 | else: 13 | self.member_id = None 14 | self.email = email 15 | self.first_name = first_name 16 | self.last_name = last_name 17 | 18 | @property 19 | def json(self): 20 | result = {'person': None} 21 | if self.member_id: 22 | result['person'] = {'_path': '/people/id=%s' % self.member_id} 23 | else: 24 | result['person'] = {'_path': '/people/email=%s' % self.email} 25 | 26 | if self.first_name: 27 | result['person']['first-name'] = self.first_name 28 | 29 | if self.last_name: 30 | result['person']['last-name'] = self.last_name 31 | 32 | return result 33 | 34 | 35 | class LinkedInInvitation(object): 36 | def __init__(self, subject, body, recipients, connect_type, auth_name=None, 37 | auth_value=None): 38 | self.subject = subject 39 | self.body = body 40 | self.recipients = recipients 41 | self.connect_type = connect_type 42 | self.auth_name = auth_name 43 | self.auth_value = auth_value 44 | 45 | @property 46 | def json(self): 47 | result = { 48 | 'recipients': { 49 | 'values': [] 50 | }, 51 | 'subject': self.subject, 52 | 'body': self.body, 53 | 'item-content': { 54 | 'invitation-request': { 55 | 'connect-type': self.connect_type 56 | } 57 | } 58 | } 59 | for recipient in self.recipients: 60 | result['recipients']['values'].append(recipient.json) 61 | 62 | if self.auth_name and self.auth_value: 63 | auth = {'name': self.auth_name, 'value': self.auth_value} 64 | result['item-content']['invitation-request']['authorization'] = auth 65 | 66 | return result 67 | 68 | 69 | class LinkedInMessage(object): 70 | def __init__(self, subject, body, recipients, auth_name=None, 71 | auth_value=None): 72 | self.subject = subject 73 | self.body = body 74 | self.recipients = recipients 75 | self.auth_name = auth_name 76 | self.auth_value = auth_value 77 | 78 | @property 79 | def json(self): 80 | result = { 81 | 'recipients': { 82 | 'values': [] 83 | }, 84 | 'subject': self.subject, 85 | 'body': self.body, 86 | } 87 | for recipient in self.recipients: 88 | result['recipients']['values'].append(recipient.json) 89 | 90 | if self.auth_name and self.auth_value: 91 | auth = {'name': self.auth_name, 'value': self.auth_value} 92 | result['item-content']['invitation-request']['authorization'] = auth 93 | 94 | return result 95 | -------------------------------------------------------------------------------- /linkedin/server.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import BaseHTTPServer 3 | import urlparse 4 | 5 | from .linkedin import LinkedInApplication, LinkedInAuthentication, PERMISSIONS 6 | 7 | 8 | def quick_api(api_key, secret_key, port=8000): 9 | """ 10 | This method helps you get access to linkedin api quickly when using it 11 | from the interpreter. 12 | Notice that this method creates http server and wait for a request, so it 13 | shouldn't be used in real production code - it's just an helper for debugging 14 | 15 | The usage is basically: 16 | api = quick_api(KEY, SECRET) 17 | After you do that, it will print a URL to the screen which you must go in 18 | and allow the access, after you do that, the method will return with the api 19 | object. 20 | """ 21 | auth = LinkedInAuthentication(api_key, secret_key, 'http://localhost:8000/', 22 | PERMISSIONS.enums.values()) 23 | app = LinkedInApplication(authentication=auth) 24 | print auth.authorization_url 25 | _wait_for_user_to_enter_browser(app, port) 26 | return app 27 | 28 | 29 | def _wait_for_user_to_enter_browser(app, port): 30 | class MyHandler(BaseHTTPServer.BaseHTTPRequestHandler): 31 | def do_GET(self): 32 | p = self.path.split('?') 33 | if len(p) > 1: 34 | params = urlparse.parse_qs(p[1], True, True) 35 | app.authentication.authorization_code = params['code'][0] 36 | app.authentication.get_access_token() 37 | 38 | server_address = ('', port) 39 | httpd = BaseHTTPServer.HTTPServer(server_address, MyHandler) 40 | httpd.handle_request() 41 | -------------------------------------------------------------------------------- /linkedin/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import requests 3 | from .exceptions import LinkedInError, get_exception_for_error_code 4 | import sys 5 | from io import StringIO 6 | 7 | try: 8 | import simplejson as json 9 | except ImportError: 10 | try: 11 | from django.utils import simplejson as json 12 | except ImportError: 13 | import json 14 | 15 | 16 | if sys.version_info < (3,): 17 | import __builtin__ 18 | 19 | def to_utf8(x): 20 | return __builtin__.unicode(x) 21 | 22 | def to_string(x): 23 | return str(x) 24 | else: 25 | def to_utf8(x): 26 | return x 27 | 28 | def to_string(x): 29 | return x 30 | 31 | 32 | def enum(enum_type='enum', base_classes=None, methods=None, **attrs): 33 | """ 34 | Generates a enumeration with the given attributes. 35 | """ 36 | # Enumerations can not be initalized as a new instance 37 | def __init__(instance, *args, **kwargs): 38 | raise RuntimeError('%s types can not be initialized.' % enum_type) 39 | 40 | if base_classes is None: 41 | base_classes = () 42 | 43 | if methods is None: 44 | methods = {} 45 | 46 | base_classes = base_classes + (object,) 47 | for k, v in methods.items(): 48 | methods[k] = classmethod(v) 49 | 50 | attrs['enums'] = attrs.copy() 51 | methods.update(attrs) 52 | methods['__init__'] = __init__ 53 | return type(to_string(enum_type), base_classes, methods) 54 | 55 | def raise_for_error(response): 56 | try: 57 | response.raise_for_status() 58 | except (requests.HTTPError, requests.ConnectionError) as error: 59 | try: 60 | if len(response.content) == 0: 61 | # There is nothing we can do here since LinkedIn has neither sent 62 | # us a 2xx response nor a response content. 63 | return 64 | response = response.json() 65 | if ('error' in response) or ('errorCode' in response): 66 | message = '%s: %s' % (response.get('error', str(error)), 67 | response.get('message', 'Unknown Error')) 68 | error_code = response.get('status') 69 | ex = get_exception_for_error_code(error_code) 70 | raise ex(message) 71 | else: 72 | raise LinkedInError(error.message) 73 | except (ValueError, TypeError): 74 | raise LinkedInError(error.message) 75 | 76 | 77 | HTTP_METHODS = enum('HTTPMethod', GET='GET', POST='POST', 78 | PUT='PUT', DELETE='DELETE', PATCH='PATCH') 79 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | future==0.14.3 2 | requests 3 | requests_oauthlib -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | 4 | try: 5 | from setuptools import setup 6 | except ImportError: 7 | from distutils.core import setup 8 | 9 | from linkedin import __version__ 10 | 11 | 12 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: 13 | long_description = readme.read() 14 | 15 | setup(name='python-linkedin', 16 | version=__version__, 17 | description='Python Interface to the LinkedIn API', 18 | long_description=long_description, 19 | classifiers=[ 20 | 'Development Status :: 5 - Production/Stable', 21 | 'Environment :: Console', 22 | 'Intended Audience :: Developers', 23 | 'License :: OSI Approved :: MIT License', 24 | 'Operating System :: OS Independent', 25 | 'Programming Language :: Python :: 2.7', 26 | 'Programming Language :: Python :: 3', 27 | 'Programming Language :: Python :: 3.4', 28 | 'Natural Language :: English', 29 | ], 30 | keywords='linkedin python', 31 | author='Ozgur Vatansever', 32 | author_email='ozgurvt@gmail.com', 33 | maintainer='Ozgur Vatansever', 34 | maintainer_email='ozgurvt@gmail.com', 35 | url='http://ozgur.github.com/python-linkedin/', 36 | license='MIT', 37 | packages=['linkedin'], 38 | install_requires=['requests>=1.1.0', 'requests-oauthlib>=0.3'], 39 | zip_safe=False 40 | ) 41 | --------------------------------------------------------------------------------