closed-linkedin-api 2.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,382 @@
1
+ Metadata-Version: 2.4
2
+ Name: closed-linkedin-api
3
+ Version: 2.3.2
4
+ Summary: A Python API to interact with LinkedIn without using official API access.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Author: J.Antonio Barba
8
+ Author-email: falsigfx@gmail.com
9
+ Requires-Python: >=3.10,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Requires-Dist: beautifulsoup4 (>=4.13.3,<5.0.0)
18
+ Requires-Dist: certifi (>=2025.1.31,<2026.0.0)
19
+ Requires-Dist: charset_normalizer (>=3.4.1,<4.0.0)
20
+ Requires-Dist: idna (>=3.10,<4.0)
21
+ Requires-Dist: lxml (>=5.3.1,<6.0.0)
22
+ Requires-Dist: requests (>=2.32.3,<3.0.0)
23
+ Requires-Dist: urllib3 (>=2.3.0,<3.0.0)
24
+ Project-URL: Repository, https://github.com/omar6995/closed-linkedin-api
25
+ Description-Content-Type: text/markdown
26
+
27
+ <div align="center">
28
+
29
+ # LinkedIn API for Python
30
+
31
+ ![Build](https://img.shields.io/github/actions/workflow/status/EseToni/open-linkedin-api/ci.yml?label=Build&logo=github)
32
+ [![Documentation](https://img.shields.io/readthedocs/open-linkedin-api?label=Docs)](https://github.com/EseToni/open-linkedin-api/tree/main/documentation)
33
+ [![GitHub Release](https://img.shields.io/github/v/release/EseToni/open-linkedin-api?label=PyPI&logo=python)](https://pypi.org/project/open-linkedin-api/)
34
+ [![Discord](https://img.shields.io/badge/Discord-5865F2?logo=discord&logoColor=ffffff)](https://discord.gg/yourdiscordlink)
35
+
36
+
37
+ Search profiles, send messages, find jobs and more in Python. No official API access required.
38
+
39
+ <p align="center">
40
+ <a href="https://github.com/EseToni/open-linkedin-api/tree/main/documentation">Documentation</a>
41
+ ·
42
+ <a href="#quick-start">Quick Start</a>
43
+ ·
44
+ <a href="#how-it-works">How it works</a>
45
+ </p>
46
+
47
+ </div>
48
+
49
+ <br>
50
+
51
+ ## Features
52
+
53
+ - ✅ No official API access required. Just use a valid LinkedIn user account.
54
+ - ✅ Direct HTTP API interface. No Selenium, Pupeteer, or other browser-based scraping methods.
55
+ - ✅ Get and search people, companies, jobs, posts
56
+ - ✅ Send and retrieve messages
57
+ - ✅ Send and accept connection requests
58
+ - ✅ Get and react to posts
59
+
60
+ And more! [Read the docs](https://linkedin-api.readthedocs.io/en/latest/api.html) for all API methods.
61
+
62
+ > [!IMPORTANT]
63
+ > This library is not officially supported by LinkedIn. Using this library might violate LinkedIn's Terms of Service. Use it at your own risk.
64
+
65
+ ## Installation
66
+
67
+ > [!NOTE]
68
+ > Python >= 3.10 required
69
+
70
+ ###
71
+
72
+ ```bash
73
+ pip install open-linkedin-api
74
+ ```
75
+
76
+ Or, for bleeding edge:
77
+
78
+ ```bash
79
+ pip install git+https://github.com/EseToni/open-linkedin-api
80
+ ```
81
+
82
+ ### Quick Start
83
+
84
+ > [!TIP]
85
+ > See all API methods on the [docs](https://github.com/EseToni/open-linkedin-api/tree/main/documentation).
86
+
87
+ The following snippet demonstrates a few basic linkedin_api use cases:
88
+
89
+ ```python
90
+ from open_linkedin_api import Linkedin
91
+
92
+ # Authenticate using any Linkedin user account credentials
93
+ api = Linkedin('reedhoffman@linkedin.com', '*******')
94
+
95
+ # GET a profile
96
+ profile = api.get_profile('billy-g')
97
+
98
+ # GET a profiles contact info
99
+ contact_info = api.get_profile_contact_info('billy-g')
100
+
101
+ # GET 1st degree connections of a given profile
102
+ connections = api.get_profile_connections('1234asc12304')
103
+ ```
104
+
105
+ ## Commercial alternatives
106
+
107
+ > This is a sponsored section
108
+
109
+ <h3>
110
+ <a href="https://prospeo.io/api/linkedin-email-finder">
111
+ Prospeo
112
+ </a>
113
+ </h3>
114
+
115
+ Extract data and find verified emails in real-time with [Prospeo LinkedIn Email Finder API](https://prospeo.io/api/linkedin-email-finder).
116
+
117
+ <details>
118
+ <summary>Learn more</summary>
119
+ Submit a LinkedIn profile URL to our API and get:
120
+
121
+ - Profile data extracted in real-time
122
+ - Company data of the profile
123
+ - Verified work email of the profile
124
+ - Exclusive data points (gender, cleaned country code, time zone...)
125
+ - One do-it-all request
126
+ - Stable API, tested under high load
127
+
128
+ Try it with 75 profiles. [Get your FREE API key now](https://prospeo.io/api/linkedin-email-finder).
129
+
130
+ </details>
131
+
132
+ <h3>
133
+ <a href="https://nubela.co/proxycurl/?utm_campaign=influencer%20marketing&utm_source=github&utm_medium=social&utm_term=-&utm_content=tom%20quirk">
134
+ Proxycurl
135
+ </a>
136
+ </h3>
137
+
138
+ Scrape public LinkedIn profile data at scale with [Proxycurl APIs](https://nubela.co/proxycurl/?utm_campaign=influencer%20marketing&utm_source=github&utm_medium=social&utm_term=-&utm_content=tom%20quirk).
139
+
140
+ <details>
141
+ <summary>Learn more</summary>
142
+
143
+ - Scraping Public profiles are battle tested in court in HiQ VS LinkedIn case.
144
+ - GDPR, CCPA, SOC2 compliant
145
+ - High rate limit - 300 requests/minute
146
+ - Fast - APIs respond in ~2s
147
+ - Fresh data - 88% of data is scraped real-time, other 12% are not older than 29 days
148
+ - High accuracy
149
+ - Tons of data points returned per profile
150
+
151
+ Built for developers, by developers.
152
+
153
+ </details>
154
+
155
+ <h3>
156
+ <a href="https://www.unipile.com/communication-api/messaging-api/linkedin-api/?utm_campaign=git%20tom%20quirk">
157
+ Unipile
158
+ </a>
159
+ </h3>
160
+
161
+ Full [LinkedIn API](https://www.unipile.com/communication-api/messaging-api/linkedin-api/?utm_campaign=git%20tom%20quirk): Connect Classic/Sales Navigator/Recruiter, synchronize real-time messaging, enrich data and build outreach sequences…
162
+
163
+ <details>
164
+ <summary>Learn more</summary>
165
+
166
+ - Easily connect your users in the cloud with our white-label authentication (captcha solving, in-app validation, OTP, 2FA).
167
+ - Real-time webhook for each message received, read status, invitation accepted, and more.
168
+ - Data extraction: get profile, get company, get post, extract search results from Classic + Sales Navigator + Recruiter
169
+ - Outreach sequences: send invitations, InMail, messages, and comment on posts…
170
+
171
+ Test [all the features](https://www.unipile.com/communication-api/messaging-api/linkedin-api/?utm_campaign=git%20tom%20quirk) with our 7-day free trial.
172
+
173
+ </details>
174
+
175
+ <h3>
176
+ <a href="https://bit.ly/4fUyE9J">
177
+ ScrapIn
178
+ </a>
179
+ </h3>
180
+
181
+ Scrape Any Data from LinkedIn, without limit with [ScrapIn API](https://bit.ly/4fUyE9J).
182
+
183
+ <details>
184
+ <summary>Learn more</summary>
185
+
186
+ - Real time data (no-cache)
187
+ - Built for SaaS developers
188
+ - GDPR, CCPA, SOC2 compliant
189
+ - Interactive API documentation
190
+ - A highly stable API, backed by over 4 years of experience in data provisioning, with the added reliability of two additional data provider brands owned by the company behind ScrapIn.
191
+
192
+ Try it for free. [Get your API key now](https://bit.ly/4fUyE9J)
193
+
194
+ </details>
195
+
196
+ <h3>
197
+ <a href="https://bit.ly/3AFPGZd">
198
+ iScraper by ProAPIs, Inc.
199
+ </a>
200
+ </h3>
201
+
202
+ Access high-quality, real-time LinkedIn data at scale with [iScraper API](https://bit.ly/3AFPGZd), offering unlimited scalability and unmatched accuracy.
203
+
204
+ <details>
205
+
206
+ <summary>Learn more</summary>
207
+
208
+ - Real-time LinkedIn data scraping with unmatched accuracy
209
+ - Hosted datasets with powerful Lucene search access
210
+ - Designed for enterprise and corporate-level applications
211
+ - Handles millions of scrapes per day, ensuring unlimited scalability
212
+ - Trusted by top enterprises for mission-critical data needs
213
+ - Interactive API documentation built on OpenAPI 3 specs for seamless integration
214
+ - Backed by over 10 years of experience in real-time data provisioning
215
+ - Lowest price guarantee for high volume use
216
+
217
+ Get started [here](https://bit.ly/3AFPGZd).
218
+
219
+ </details>
220
+
221
+ > End sponsored section
222
+
223
+ ## Development
224
+
225
+ ### Dependencies
226
+
227
+ - [`poetry`](https://python-poetry.org/docs/)
228
+ - A valid Linkedin user account (don't use your personal account, if possible)
229
+
230
+ ### Development installation
231
+
232
+ 1. Create a `.env` config file (use `.env.example` as a reference)
233
+ 2. Install dependencies using `poetry`:
234
+
235
+ ```bash
236
+ poetry install
237
+ poetry self add poetry-plugin-dotenv
238
+ ```
239
+
240
+ ### Run tests
241
+
242
+ Run all tests:
243
+
244
+ ```bash
245
+ poetry run pytest
246
+ ```
247
+
248
+ Run unit tests:
249
+
250
+ ```bash
251
+ poetry run pytest tests/unit
252
+ ```
253
+
254
+ Run E2E tests:
255
+
256
+ ```bash
257
+ poetry run pytest tests/e2e
258
+ ```
259
+
260
+ ### Lint
261
+
262
+ ```bash
263
+ poetry run black --check .
264
+ ```
265
+
266
+ Or to fix:
267
+
268
+ ```bash
269
+ poetry run black .
270
+ ```
271
+
272
+ ### Troubleshooting
273
+
274
+ #### I keep getting a `CHALLENGE`
275
+
276
+ 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.
277
+
278
+ **Known reasons for Challenge** include:
279
+
280
+ - 2FA
281
+ - 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.
282
+
283
+ Please add more as you come across them.
284
+
285
+ #### Search problems
286
+
287
+ - 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.
288
+
289
+ ## How it works
290
+
291
+ This project attempts to provide a simple Python interface for the LinkedIn API.
292
+
293
+ > Do you mean the [legit LinkedIn API](https://developer.linkedin.com/)?
294
+
295
+ NO! To retrieve structured data, the [LinkedIn Website](https://linkedin.com) uses a service they call **Voyager**. Voyager endpoints give us access to pretty much everything we could want from LinkedIn: profiles, companies, connections, messages, etc. - anything that you can see on linkedin.com, we can get from Voyager.
296
+
297
+ This project aims to provide complete coverage for Voyager.
298
+
299
+ [How does it work?](#deep-dive)
300
+
301
+ ### Deep dive
302
+
303
+ Voyager endpoints look like this:
304
+
305
+ ```text
306
+ https://www.linkedin.com/voyager/api/identity/profileView/tom-quirk
307
+ ```
308
+
309
+ Or, more clearly
310
+
311
+ ```text
312
+ ___________________________________ _______________________________
313
+ | base path | resource |
314
+ https://www.linkedin.com/voyager/api /identity/profileView/tom-quirk
315
+ ```
316
+
317
+ They are authenticated with a simple cookie, which we send with every request, along with a bunch of headers.
318
+
319
+ To get a cookie, we POST a given username and password (of a valid LinkedIn user account) to `https://www.linkedin.com/uas/authenticate`.
320
+
321
+ ### Find new endpoints
322
+
323
+ We're looking at the LinkedIn website and we spot some data we want. What now?
324
+
325
+ The following describes the most reliable method to find relevant endpoints:
326
+
327
+ 1. `view source`
328
+ 1. `command-f`/search the page for some keyword in the data. This will exist inside of a `<code>` tag.
329
+ 1. Scroll down to the **next adjacent element** which will be another `<code>` tag, probably with an `id` that looks something like
330
+
331
+ ```html
332
+ <code style="display: none" id="datalet-bpr-guid-3900675">
333
+ {"request":"/voyager/api/identity/profiles/tom-quirk/profileView","status":200,"body":"bpr-guid-3900675"}
334
+ </code>
335
+ ```
336
+
337
+ The value of `request` is the url! 🤘
338
+
339
+ You can also use the `network` tab in you browsers developer tools, but you will encounter mixed results.
340
+
341
+ ### How Clients query Voyager
342
+
343
+ linkedin.com uses the [Rest-li Protocol](https://linkedin.github.io/rest.li/spec/protocol) for querying data. Rest-li is an internal query language/syntax where clients (like linkedin.com) specify what data they want. It's conceptually similar to the GraphQL.
344
+
345
+ Here's an example of making a request for an organisation's `name` and `groups` (the Linkedin groups it manages):
346
+
347
+ ```text
348
+ /voyager/api/organization/companies?decoration=(name,groups*~(entityUrn,largeLogo,groupName,memberCount,websiteUrl,url))&q=universalName&universalName=linkedin
349
+ ```
350
+
351
+ The "querying" happens in the `decoration` parameter, which looks like the following:
352
+
353
+ ```text
354
+ (
355
+ name,
356
+ groups*~(entityUrn,largeLogo,groupName,memberCount,websiteUrl,url)
357
+ )
358
+ ```
359
+
360
+ Here, we request an organisation name and a list of groups, where for each group we want `largeLogo`, `groupName`, and so on.
361
+
362
+ 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.
363
+
364
+ In contrast, the `/search/cluster` endpoint uses `q=guided`, and specifies its query with the `guided` parameter, whose value is something like
365
+
366
+ ```text
367
+ List(v->PEOPLE)
368
+ ```
369
+
370
+ 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).
371
+
372
+ ### Release a new version
373
+
374
+ 1. Bump `version` in `pyproject.toml`
375
+ 1. `poetry build`
376
+ 1. `poetry publish`
377
+ 1. Draft release notes in GitHub.
378
+
379
+ ## Disclaimer
380
+
381
+ This library is not endorsed or supported by LinkedIn. It is an unofficial library intended for educational purposes and personal use only. By using this library, you agree to not hold the author or contributors responsible for any consequences resulting from its usage.
382
+
@@ -0,0 +1,14 @@
1
+ open_linkedin_api/__init__.py,sha256=6uon3-l0pUnCsmMxBZ8Qm2qGmN_vIWjUPDjXkZQZNX4,710
2
+ open_linkedin_api/client.py,sha256=QBaGb9qP58ohnhkDr7o_1HaFPQ-iUrC6b95NLDEUpZU,7024
3
+ open_linkedin_api/cookie_auth.py,sha256=0FbUIEEFru05ygTyfdCmRpTlwqTJezXaFlH1ylbcl9c,8045
4
+ open_linkedin_api/cookie_repository.py,sha256=AGDKt-U1GrItyIbDerDJuCfWxaP5TBv5Qe8-KoY8iiY,2078
5
+ open_linkedin_api/graphql.py,sha256=YxB6FZVlyWoe0080V1aUJwCfT3b5_sH9uLGfUahfMTQ,8342
6
+ open_linkedin_api/linkedin.py,sha256=fdaDAhK4n5TrkYoOByrZu2kMJRvLUeSEsIePteEWL8k,49553
7
+ open_linkedin_api/parsers.py,sha256=Jkj09mLPSD8gEzVBRRtLT6wO784uBOYUufwR2zWpU0Q,26480
8
+ open_linkedin_api/settings.py,sha256=YvzKRqArH2tGW8Ip0Z85mWU4KHCGoxrGgy8oEyx7rls,245
9
+ open_linkedin_api/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ open_linkedin_api/utils/helpers.py,sha256=mdjZGCFZ4EcqSSHcNHcEIXoXbki5yA_eRY5CB4SMUog,7891
11
+ closed_linkedin_api-2.3.2.dist-info/METADATA,sha256=UtomZI712bSMO0sdT2gaGyInBGpaw26NYA8F6au0p8g,13088
12
+ closed_linkedin_api-2.3.2.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
13
+ closed_linkedin_api-2.3.2.dist-info/licenses/LICENSE,sha256=exG1ZBMsxjPvP-3dIBvORTsYoLTGQ7Owo46JnNvxDSI,1086
14
+ closed_linkedin_api-2.3.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.3.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Jose Antonio Barba Rodríguez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,31 @@
1
+ """
2
+ linkedin-api
3
+ """
4
+
5
+ from .linkedin import Linkedin
6
+ from .graphql import QueryRegistry, GraphQLQuery, QueryType, GraphQLClient
7
+ from .parsers import (
8
+ NormalizedJsonParser,
9
+ ProfileParser,
10
+ MessagingParser,
11
+ FeedParser,
12
+ )
13
+ from .client import Client, ChallengeException, UnauthorizedException
14
+ from .cookie_auth import CookieAuthenticator, CookieAuthenticationError
15
+
16
+ __all__ = [
17
+ "Linkedin",
18
+ "Client",
19
+ "ChallengeException",
20
+ "UnauthorizedException",
21
+ "QueryRegistry",
22
+ "GraphQLQuery",
23
+ "QueryType",
24
+ "GraphQLClient",
25
+ "NormalizedJsonParser",
26
+ "ProfileParser",
27
+ "MessagingParser",
28
+ "FeedParser",
29
+ "CookieAuthenticator",
30
+ "CookieAuthenticationError",
31
+ ]
@@ -0,0 +1,207 @@
1
+ import requests
2
+ import logging
3
+ from typing import Any, Dict, Optional
4
+ from open_linkedin_api.cookie_repository import CookieRepository
5
+ from open_linkedin_api.graphql import GraphQLClient, GraphQLQuery, QueryRegistry
6
+ from bs4 import BeautifulSoup, Tag
7
+ from requests.cookies import RequestsCookieJar
8
+ import json
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class ChallengeException(Exception):
14
+ pass
15
+
16
+
17
+ class UnauthorizedException(Exception):
18
+ pass
19
+
20
+
21
+ class Client(object):
22
+ """
23
+ Class to act as a client for the Linkedin API.
24
+ """
25
+
26
+ # Settings for general Linkedin API calls
27
+ LINKEDIN_BASE_URL = "https://www.linkedin.com"
28
+ API_BASE_URL = f"{LINKEDIN_BASE_URL}/voyager/api"
29
+ REQUEST_HEADERS = {
30
+ "user-agent": " ".join(
31
+ [
32
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5)",
33
+ "AppleWebKit/537.36 (KHTML, like Gecko)",
34
+ "Chrome/83.0.4103.116 Safari/537.36",
35
+ ]
36
+ ),
37
+ # "accept": "application/vnd.linkedin.normalized+json+2.1",
38
+ "accept-language": "en-AU,en-GB;q=0.9,en-US;q=0.8,en;q=0.7",
39
+ "x-li-lang": "en_US",
40
+ "x-restli-protocol-version": "2.0.0",
41
+ # "x-li-track": '{"clientVersion":"1.2.6216","osName":"web","timezoneOffset":10,"deviceFormFactor":"DESKTOP","mpName":"voyager-web"}',
42
+ }
43
+
44
+ # Settings for authenticating with Linkedin
45
+ AUTH_REQUEST_HEADERS = {
46
+ "X-Li-User-Agent": "LIAuthLibrary:0.0.3 com.linkedin.android:4.1.881 Asus_ASUS_Z01QD:android_9",
47
+ "User-Agent": "ANDROID OS",
48
+ "X-User-Language": "en",
49
+ "X-User-Locale": "en_US",
50
+ "Accept-Language": "en-us",
51
+ }
52
+
53
+ def __init__(
54
+ self, *, debug=False, refresh_cookies=False, proxies={}, cookies_dir: str = ""
55
+ ):
56
+ self.session = requests.session()
57
+ self.session.proxies.update(proxies)
58
+ self.session.headers.update(Client.REQUEST_HEADERS)
59
+ self.proxies = proxies
60
+ self.logger = logger
61
+ self.metadata = {}
62
+ self._use_cookie_cache = not refresh_cookies
63
+ self._cookie_repository = CookieRepository(cookies_dir=cookies_dir)
64
+
65
+ logging.basicConfig(level=logging.DEBUG if debug else logging.INFO)
66
+
67
+ def _request_session_cookies(self):
68
+ """
69
+ Return a new set of session cookies as given by Linkedin.
70
+ """
71
+ self.logger.debug("Requesting new cookies.")
72
+
73
+ res = requests.get(
74
+ f"{Client.LINKEDIN_BASE_URL}/uas/authenticate",
75
+ headers=Client.AUTH_REQUEST_HEADERS,
76
+ proxies=self.proxies,
77
+ )
78
+ return res.cookies
79
+
80
+ def _set_session_cookies(self, cookies: RequestsCookieJar):
81
+ """
82
+ Set cookies of the current session and save them to a file named as the username.
83
+ """
84
+ self.session.cookies = cookies
85
+ self.session.headers["csrf-token"] = self.session.cookies["JSESSIONID"].strip(
86
+ '"'
87
+ )
88
+
89
+ @property
90
+ def cookies(self):
91
+ return self.session.cookies
92
+
93
+ def authenticate(self, username: str, password: str):
94
+ if self._use_cookie_cache:
95
+ self.logger.debug("Attempting to use cached cookies")
96
+ cookies = self._cookie_repository.get(username)
97
+ if cookies:
98
+ self.logger.debug("Using cached cookies")
99
+ self._set_session_cookies(cookies)
100
+ self._fetch_metadata()
101
+ return
102
+
103
+ self._do_authentication_request(username, password)
104
+ self._fetch_metadata()
105
+
106
+ def _fetch_metadata(self):
107
+ """
108
+ Get metadata about the "instance" of the LinkedIn application for the signed in user.
109
+
110
+ Store this data in self.metadata
111
+ """
112
+ res = requests.get(
113
+ f"{Client.LINKEDIN_BASE_URL}",
114
+ cookies=self.session.cookies,
115
+ headers=Client.AUTH_REQUEST_HEADERS,
116
+ proxies=self.proxies,
117
+ )
118
+
119
+ soup = BeautifulSoup(res.text, "lxml")
120
+
121
+ clientApplicationInstanceRaw = soup.find(
122
+ "meta", attrs={"name": "applicationInstance"}
123
+ )
124
+ if clientApplicationInstanceRaw and isinstance(
125
+ clientApplicationInstanceRaw, Tag
126
+ ):
127
+ content = clientApplicationInstanceRaw.attrs.get("content")
128
+ if content and isinstance(content, str):
129
+ clientApplicationInstance = json.loads(content)
130
+ self.metadata["clientApplicationInstance"] = clientApplicationInstance
131
+
132
+ clientPageInstanceIdRaw = soup.find(
133
+ "meta", attrs={"name": "clientPageInstanceId"}
134
+ )
135
+ if clientPageInstanceIdRaw and isinstance(clientPageInstanceIdRaw, Tag):
136
+ clientPageInstanceId = clientPageInstanceIdRaw.attrs.get("content", {})
137
+ self.metadata["clientPageInstanceId"] = clientPageInstanceId
138
+
139
+ def _do_authentication_request(self, username: str, password: str):
140
+ """
141
+ Authenticate with Linkedin.
142
+
143
+ Return a session object that is authenticated.
144
+ """
145
+ self._set_session_cookies(self._request_session_cookies())
146
+
147
+ payload = {
148
+ "session_key": username,
149
+ "session_password": password,
150
+ "JSESSIONID": self.session.cookies["JSESSIONID"],
151
+ }
152
+
153
+ res = requests.post(
154
+ f"{Client.LINKEDIN_BASE_URL}/uas/authenticate",
155
+ data=payload,
156
+ cookies=self.session.cookies,
157
+ headers=Client.AUTH_REQUEST_HEADERS,
158
+ proxies=self.proxies,
159
+ )
160
+
161
+ data = res.json()
162
+
163
+ if data and data["login_result"] != "PASS":
164
+ raise ChallengeException(data["login_result"])
165
+
166
+ if res.status_code == 401:
167
+ raise UnauthorizedException()
168
+
169
+ if res.status_code != 200:
170
+ raise Exception()
171
+
172
+ self._set_session_cookies(res.cookies)
173
+ self._cookie_repository.save(res.cookies, username)
174
+
175
+ def get_graphql_client(self) -> GraphQLClient:
176
+ """
177
+ Get a GraphQL client configured with the current session.
178
+
179
+ :return: GraphQL client instance
180
+ :rtype: GraphQLClient
181
+ """
182
+ return GraphQLClient(self.session, Client.API_BASE_URL)
183
+
184
+ def graphql_request(
185
+ self,
186
+ query: GraphQLQuery,
187
+ variables: Optional[Dict[str, Any]] = None,
188
+ include_web_metadata: bool = False,
189
+ headers: Optional[Dict[str, str]] = None
190
+ ) -> Dict[str, Any]:
191
+ """
192
+ Execute a GraphQL request using the current session.
193
+
194
+ :param query: The GraphQL query to execute
195
+ :param variables: Variables to pass to the query
196
+ :param include_web_metadata: Whether to include web metadata
197
+ :param headers: Additional headers to include in the request
198
+ :return: JSON response from the API
199
+ """
200
+ client = self.get_graphql_client()
201
+ return client.execute(
202
+ query=query,
203
+ variables=variables,
204
+ include_web_metadata=include_web_metadata,
205
+ headers=headers
206
+ )
207
+