From 2e5e82f3f116bdcabfacfb2ae30408116fcc975d Mon Sep 17 00:00:00 2001 From: Tom Quirk Date: Tue, 26 Mar 2019 18:38:50 +1000 Subject: [PATCH 1/5] init pypi deploy travis --- .travis.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2779fc1d..a7f04f86 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,4 +10,11 @@ install: - pipenv install --pre script: - - pipenv run python -m pytest tests -s \ No newline at end of file + - pipenv run python -m pytest tests -s + +deploy: + provider: pypi + user: "Your username" + password: "Your password" + on: + branch: master From 21dd39a015637e5dde0c6d6a95b0eade00de5946 Mon Sep 17 00:00:00 2001 From: Tom Quirk Date: Tue, 26 Mar 2019 18:46:41 +1000 Subject: [PATCH 2/5] mayz --- tests/test_linkedin_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_linkedin_api.py b/tests/test_linkedin_api.py index b83c2dba..d7a61e8f 100644 --- a/tests/test_linkedin_api.py +++ b/tests/test_linkedin_api.py @@ -9,6 +9,7 @@ TEST_PROFILE_ID = os.getenv("TEST_PROFILE_ID") TEST_CONVERSATION_ID = os.getenv("TEST_CONVERSATION_ID") +print(TEST_LINKEDIN_USERNAME, TEST_LINKEDIN_PASSWORD) if not ( TEST_LINKEDIN_USERNAME and TEST_LINKEDIN_PASSWORD From 8c81747b999e6a7da05fa00b91cdabaa90909501 Mon Sep 17 00:00:00 2001 From: Tom Quirk Date: Tue, 26 Mar 2019 19:05:27 +1000 Subject: [PATCH 3/5] test test.pypi deploy --- .travis.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index a7f04f86..2952863f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,12 +9,13 @@ install: - pipenv --python 3 - pipenv install --pre -script: - - pipenv run python -m pytest tests -s +# reminder: we can't run tests because Linkedin always throws challenges (likely because of IP) deploy: provider: pypi - user: "Your username" - password: "Your password" + user: + secure: "OIlyR8F/jKJbU5CQmUmbek/ZomLvQCdxeiITv14XEG1/gbDlS3DcesCZhPDhvXmuOtke+Q0Thd98PpOrs7VNGT5YVWSTlZGKxmUnOJoMzkFltSWO4GFnwfXKWT5Euu1pbkTNgQxUmPgucGZdHWo96pr+Bjp5QV45GuiDIfFKT/jkjgr5mriVvpbvKvaXqw6TgeQulLlKE86O4rRQAHz5txspJtl3CF4nQao79ZKUyyZwnpBbjvsKehuq2jXGuDB8UTCSJkH0wtzRFqXQnJdv5Ghm84YPlaqk50zbgMg0yz4Ylehi1CSud4zcegc94FSAbv4yB18T4BvrZcdIckufbw24QgqFSU3atSzTZ7zCQ4JFBa+SQn1ppy+s8c8XXHshF+sxfMJb8/wKzXurvhDkiiBgiin+0Qgju1z/1QxZ4J+898/5Eq1qxlAoeNbkOz4RvF+5mja2w8wySDQASGznlksOP7ARGc/aQpz6S+Cv3rTmhcO+JaDxz/Qomq82LoIY2h0WHLWv9iUWQrNuMZPkEcQptQ5XZQsuJDkRQThF+kGriiV0hZqfoeqHZC9coCCUYGszEI733+M5wK9GRHOZwEKwYAbiM4MU6ItKtZs+/oynlQWQAFe55hdQZUgDvhB9lR3FxJih9z7KXVIot27B1h2ls5WYZ1hXfNO3/Vv3h3Y=" + password: + secure: "FgZ9Z6ufPC++HAhVLffqhw+5PLpRvN4OXNRs3ghNn8IjXouVKBf6enOb0TjYp2N2oDH/++xscmswhQuYA0TiuaX7Wbd7oHZtt/xCDYNiy5A9nNwmmiHzMlwqwkc7URMj+AF6ksuro/rCgfC/SBrKGQQDW905I2b96ly3fblZMBh3iIRFBtxhbpVw7UFOxxPtiUiHJWj/YmwwNJ5fYTgDBnjpy7xuKPa8Ms6HYlU2hypp65Jq0EhfPKdDYQyh5VWnnqCAnNn1nfjHzpl/rckcS1j4mx3VRsIbuSRt/73Gid4Be1/wgXHnuJAuvj4Ew08JryNk0nVW5je93LuGidtAcUtt5nzCIfRwdDFpSez395qP6nUCwFOeNEnH9P2YD9diHJbOr5oSc3sBQLTBX6aauXw54h0CMHHYZKOhrvC9cn223RPnl0Pxsk/6PAcb0yb1v7Yv4COmg4B1dS9bfZM6u+4VByrQQeEqL854bIznDFWzOCJ1FPUhwN7sY48cfUxsgNeJyerESNcW2F/UpHJP+42PhCSlGlRD0eiIoOr51IawSXGb588C+DCQqzEh7nhMkdUFPtFdh/QD7ogw7S/y7k3TCVH1EHkY1QoZ3DFIRowOcpltyOIZSD3DyzdTOX+luaMn4w7+fyWd+zSQiiPSAeHsw2Us0AKTrIMphtAcLaQ=" on: - branch: master + branch: travis \ No newline at end of file From d8c3777e3f485153d3cee7a0464268a0b71292bb Mon Sep 17 00:00:00 2001 From: Tom Quirk Date: Tue, 26 Mar 2019 19:08:21 +1000 Subject: [PATCH 4/5] yas --- .travis.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2952863f..19fbf378 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,12 +2,15 @@ dist: xenial language: python python: 3.7 -before_install: - - pip install pipenv +# before_install: +# - pip install pipenv -install: - - pipenv --python 3 - - pipenv install --pre +# install: +# - pipenv --python 3 +# - pipenv install --pre + +script: + echo "pass" # reminder: we can't run tests because Linkedin always throws challenges (likely because of IP) From 04865b42feaa3ca7eb7dd3f9d8fbbaac95ca6faa Mon Sep 17 00:00:00 2001 From: Tom Quirk Date: Tue, 26 Mar 2019 20:23:50 +1000 Subject: [PATCH 5/5] clean up some shtuff --- .travis.yml | 24 ------------------ DOCS.md | 8 +++--- README.md | 54 ++++++++++++++++++++++++---------------- linkedin_api/__init__.py | 2 +- setup.py | 26 ++++++++++++++++--- 5 files changed, 60 insertions(+), 54 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 19fbf378..00000000 --- a/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -dist: xenial -language: python -python: 3.7 - -# before_install: -# - pip install pipenv - -# install: -# - pipenv --python 3 -# - pipenv install --pre - -script: - echo "pass" - -# reminder: we can't run tests because Linkedin always throws challenges (likely because of IP) - -deploy: - provider: pypi - user: - secure: "OIlyR8F/jKJbU5CQmUmbek/ZomLvQCdxeiITv14XEG1/gbDlS3DcesCZhPDhvXmuOtke+Q0Thd98PpOrs7VNGT5YVWSTlZGKxmUnOJoMzkFltSWO4GFnwfXKWT5Euu1pbkTNgQxUmPgucGZdHWo96pr+Bjp5QV45GuiDIfFKT/jkjgr5mriVvpbvKvaXqw6TgeQulLlKE86O4rRQAHz5txspJtl3CF4nQao79ZKUyyZwnpBbjvsKehuq2jXGuDB8UTCSJkH0wtzRFqXQnJdv5Ghm84YPlaqk50zbgMg0yz4Ylehi1CSud4zcegc94FSAbv4yB18T4BvrZcdIckufbw24QgqFSU3atSzTZ7zCQ4JFBa+SQn1ppy+s8c8XXHshF+sxfMJb8/wKzXurvhDkiiBgiin+0Qgju1z/1QxZ4J+898/5Eq1qxlAoeNbkOz4RvF+5mja2w8wySDQASGznlksOP7ARGc/aQpz6S+Cv3rTmhcO+JaDxz/Qomq82LoIY2h0WHLWv9iUWQrNuMZPkEcQptQ5XZQsuJDkRQThF+kGriiV0hZqfoeqHZC9coCCUYGszEI733+M5wK9GRHOZwEKwYAbiM4MU6ItKtZs+/oynlQWQAFe55hdQZUgDvhB9lR3FxJih9z7KXVIot27B1h2ls5WYZ1hXfNO3/Vv3h3Y=" - password: - secure: "FgZ9Z6ufPC++HAhVLffqhw+5PLpRvN4OXNRs3ghNn8IjXouVKBf6enOb0TjYp2N2oDH/++xscmswhQuYA0TiuaX7Wbd7oHZtt/xCDYNiy5A9nNwmmiHzMlwqwkc7URMj+AF6ksuro/rCgfC/SBrKGQQDW905I2b96ly3fblZMBh3iIRFBtxhbpVw7UFOxxPtiUiHJWj/YmwwNJ5fYTgDBnjpy7xuKPa8Ms6HYlU2hypp65Jq0EhfPKdDYQyh5VWnnqCAnNn1nfjHzpl/rckcS1j4mx3VRsIbuSRt/73Gid4Be1/wgXHnuJAuvj4Ew08JryNk0nVW5je93LuGidtAcUtt5nzCIfRwdDFpSez395qP6nUCwFOeNEnH9P2YD9diHJbOr5oSc3sBQLTBX6aauXw54h0CMHHYZKOhrvC9cn223RPnl0Pxsk/6PAcb0yb1v7Yv4COmg4B1dS9bfZM6u+4VByrQQeEqL854bIznDFWzOCJ1FPUhwN7sY48cfUxsgNeJyerESNcW2F/UpHJP+42PhCSlGlRD0eiIoOr51IawSXGb588C+DCQqzEh7nhMkdUFPtFdh/QD7ogw7S/y7k3TCVH1EHkY1QoZ3DFIRowOcpltyOIZSD3DyzdTOX+luaMn4w7+fyWd+zSQiiPSAeHsw2Us0AKTrIMphtAcLaQ=" - on: - branch: travis \ No newline at end of file diff --git a/DOCS.md b/DOCS.md index 0a32fa6b..306f80f1 100644 --- a/DOCS.md +++ b/DOCS.md @@ -337,7 +337,7 @@ if err: ### linkedin.mark_conversation_as_seen(conversation_urn_id) -Mark a given conversation as seen. +Mark a given conversation as seen. **Arguments** @@ -410,8 +410,8 @@ results = linkedin.search_people( keywords='software,lol', connection_of='AC000120303', network_depth='F', - regions=[4909], - industries=[29, 1] + regions=['au:4909'], + industries=['29', '1'] ) ``` @@ -450,4 +450,4 @@ invite_to_ignore = linkedin.get_invitations()[1] linkedin.reply_invitation(invitation_entity_urn=invite_to_accept['entityUrn'], invitation_shared_secret=invite_to_accept['sharedSecret']) linkedin.reply_invitation(invitation_entity_urn=invite_to_ignore['entityUrn'], invitation_shared_secret=invite_to_ignore['sharedSecret'], action="ignore") -``` \ No newline at end of file +``` diff --git a/README.md b/README.md index 062cbc6a..0e933556 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,15 @@ ๐Ÿ‘จโ€๐Ÿ’ผ Python Wrapper for the Linkedin API -![v1.0.1](https://img.shields.io/badge/PyPI-v1.0.1-blue.svg) +[![PyPI version](https://badge.fury.io/py/linkedin-api.svg)](https://badge.fury.io/py/linkedin-api) > No "official" API access required - just use a valid Linkedin account! -Programmatically send messages, perform searches, get profile data and more, all with a standard Linkedin account! +Programmatically send messages, perform searches, get profile data and more, all with a regular Linkedin user account! ##### USE AT YOUR OWN RISK ๐Ÿ˜‰ -This project should only be used as a learning project. Using it would violate Linkedin's User Agreement. I am not responsible for your account being blocked (which they will definitely do - see User Agreement section 8.2). Hint: **don't use a Linkedin account that you care about**) -##### Development Notice -This project is NOT STABLE by any stretch of the imagination. The API is subject to change at a moment's notice. @tomquirk will be amending the PyPI releases to reflect this fact soon. +This project should only be used as a learning project. Using it would violate Linkedin's User Agreement. I am not responsible for your account being blocked (which they will definitely do - see User Agreement section 8.2). Hint: **don't use a Linkedin account that you care about**) ## Overview @@ -27,9 +25,11 @@ So specifically, this project aims to provide complete coverage for Voyager. [How do we do it?](#in-depth-overview) ### Want to contribute? -[How do I find endpoints?](to-find-endpoints) + +[Learn how to find endpoints](#to-find-endpoints) ## Installation + ``` $ pip install linkedin-api ``` @@ -53,25 +53,26 @@ connections = api.get_profile_connections('1234asc12304', max_connections=200) ``` ## Documentation + For a complete reference documentation, see the [DOCS.md](https://github.com/tomquirk/linkedin-api/blob/master/DOCS.md) ## Development Setup ### Dependencies -* Python 3.7 -* A valid Linkedin user account (don't use your personal account, if possible) -* Pipenv (optional) +- Python 3.7 +- A valid Linkedin user account (don't use your personal account, if possible) +- Pipenv (optional) ### Installation 1. Create a `.env` config file. An example is provided in `.env.example` - you include at least all of the settings set there. 2. Using pipenv... - ``` - $ pipenv install - $ pipenv shell - ``` + ``` + $ pipenv install + $ pipenv shell + ``` ### Running tests @@ -86,22 +87,28 @@ $ python -m pytest tests Linkedin will throw you a curve ball in the form of a Challenge URL. We currently don't handle this, and so you're kinda screwed. We think it could be only IP-based (i.e. logging in from different location). Your best chance at resolution is to log out and log back in on your browser. ##### Known reasons for Challenge: + - 2FA - Rate-limit - "It looks like youโ€™re visiting a very high number of pages on LinkedIn.". Note - n=1 experiment where this page was hit after ~900 contiguous requests in a single session (within the hour) (these included random delays between each request), as well as a bunch of testing, so who knows the actual limit. Please add more as you come across them. #### Search woes + - Mileage may vary when searching general keywords like "software" using the standard `search` method. They've recently added some smarts around search whereby they group results by people, company, jobs etc. if the query is general enough. Try to use an entity-specific search method (i.e. search_people) where possible. + + ## In-depth overview Voyager endpoints look like this: + ``` https://www.linkedin.com/voyager/api/identity/profileView/tom-quirk ``` Or, more clearly + ``` ___________________________________ _______________________________ | base path | resource | @@ -112,19 +119,22 @@ They are authenticated with a simple cookie, which we send with every request, a To get a cookie, we POST a given username and password (of a valid Linkedin user account) to `https://www.linkedin.com/uas/authenticate`. + + ### To find endpoints... We're looking at the Linkedin website and we spot some data we want. What now? -The most reliable method to find the relevant endpoint is to: +The most reliable method to find the relevant endpoint is to: + 1. `view source` 2. `command-f`/search the page for some keyword in the data. This will exist inside of a `` tag. 3. Scroll down to the **next adjacent element** which will be another `` tag, probably with an `id` that looks something like - ```html - - ``` + ```html + + ``` 4. The value of `request` is the url! :woot: You can also use the `network` tab in you browsers developer tools, but you will encounter mixed results. @@ -140,20 +150,22 @@ Here's an example of making a request for an organisation's `name` and `groups` ``` The "querying" happens in the `decoration` parameter, which looks like + ``` ( name, groups*~(entityUrn,largeLogo,groupName,memberCount,websiteUrl,url) ) ``` + So here, we request an organisation name, and a list of groups, where for each group we want `largeLogo`, `groupName`, etc. -Different endpoints use different parameters (and perhaps even different syntaxes) to specify these queries. Notice that the above query had a parameter `q` whose value was `universalName`; the query was then specified with the `decoration` parameter. +Different endpoints use different parameters (and perhaps even different syntaxes) to specify these queries. Notice that the above query had a parameter `q` whose value was `universalName`; the query was then specified with the `decoration` parameter. In contrast, the `/search/cluster` endpoint uses `q=guided`, and specifies its query with the `guided` parameter, whose value is something like + ``` List(v->PEOPLE) ``` It could be possible to document (and implement a nice interface for) this query language - as we add more endpoints to this project, I'm sure it will become more clear if such a thing would be possible (and if it's worth it). - diff --git a/linkedin_api/__init__.py b/linkedin_api/__init__.py index 2572ba8a..72726452 100644 --- a/linkedin_api/__init__.py +++ b/linkedin_api/__init__.py @@ -4,7 +4,7 @@ from .linkedin import Linkedin __title__ = "linkedin_api" -__version__ = "1.0.1" +__version__ = "1.1.0" __description__ = "Python Wrapper for the Linkedin API" __license__ = "MIT" diff --git a/setup.py b/setup.py index b540ece0..5f09edba 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,33 @@ import setuptools +import ast +import re +from pathlib import Path + +CURRENT_DIR = Path(__file__).parent + + +def get_long_description() -> str: + readme_md = CURRENT_DIR / "README.md" + with open(readme_md, encoding="utf8") as ld_file: + return ld_file.read() + + +def get_version() -> str: + black_py = CURRENT_DIR / "linkedin_api/__init__.py" + _version_re = re.compile(r"__version__\s+=\s+(?P.*)") + with open(black_py, "r", encoding="utf8") as f: + match = _version_re.search(f.read()) + version = match.group("version") if match is not None else '"unknown"' + return str(ast.literal_eval(version)) -with open("README.md", "r") as fh: - long_description = fh.read() setuptools.setup( name="linkedin_api", - version="1.0.1", + version=get_version(), author="Tom Quirk", author_email="tomquirkacc@gmail.com", description="Python wrapper for the Linkedin API", - long_description=long_description, + long_description=get_long_description(), long_description_content_type="text/markdown", url="https://github.com/tomquirk/linkedin-api", license="MIT",