warmpath 0.3.1__tar.gz → 0.3.3__tar.gz
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.
- {warmpath-0.3.1 → warmpath-0.3.3}/PKG-INFO +1 -1
- warmpath-0.3.3/README.md +55 -0
- {warmpath-0.3.1 → warmpath-0.3.3}/docs/skill.md +1 -1
- {warmpath-0.3.1 → warmpath-0.3.3}/pyproject.toml +1 -1
- {warmpath-0.3.1 → warmpath-0.3.3}/tests/test_auth.py +144 -19
- {warmpath-0.3.1 → warmpath-0.3.3}/uv.lock +1 -1
- {warmpath-0.3.1 → warmpath-0.3.3}/warmpath/auth.py +3 -21
- {warmpath-0.3.1 → warmpath-0.3.3}/warmpath/cli.py +43 -6
- warmpath-0.3.1/README.md +0 -80
- {warmpath-0.3.1 → warmpath-0.3.3}/.gitignore +0 -0
- {warmpath-0.3.1 → warmpath-0.3.3}/AGENTS.md +0 -0
- {warmpath-0.3.1 → warmpath-0.3.3}/Taskfile.yaml +0 -0
- {warmpath-0.3.1 → warmpath-0.3.3}/UNLICENSE +0 -0
- {warmpath-0.3.1 → warmpath-0.3.3}/scripts/release.sh +0 -0
- {warmpath-0.3.1 → warmpath-0.3.3}/tests/test_cli_commands.py +0 -0
- {warmpath-0.3.1 → warmpath-0.3.3}/tests/test_mutual_connections.py +0 -0
- {warmpath-0.3.1 → warmpath-0.3.3}/warmpath/__init__.py +0 -0
- {warmpath-0.3.1 → warmpath-0.3.3}/warmpath.jpg +0 -0
warmpath-0.3.3/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Warmpath
|
|
4
|
+
|
|
5
|
+
Find LinkedIn mutuals and warm paths using your logged-in browser session.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
Log in to LinkedIn in a regular browser window, then import that session. Replace `chrome` with your browser's name if needed.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
# Import and check your browser session
|
|
13
|
+
uvx warmpath auth import --browser chrome
|
|
14
|
+
uvx warmpath auth status
|
|
15
|
+
|
|
16
|
+
# Find people who can introduce you to a company
|
|
17
|
+
uvx warmpath company HashiCorp
|
|
18
|
+
uvx warmpath company https://www.linkedin.com/company/hashicorp/ --max-degree 2 --limit 5
|
|
19
|
+
|
|
20
|
+
# Find reachable people with a skill
|
|
21
|
+
uvx warmpath skill Flutter
|
|
22
|
+
uvx warmpath skill Leadership --max-depth 2
|
|
23
|
+
|
|
24
|
+
# Find mutual connections with a person
|
|
25
|
+
uvx warmpath human https://www.linkedin.com/in/mitchellh/
|
|
26
|
+
|
|
27
|
+
# Refresh cached results
|
|
28
|
+
uvx warmpath human https://www.linkedin.com/in/mitchellh/ --refresh-cache
|
|
29
|
+
|
|
30
|
+
# Show options for a command; also works with auth, skill, and human
|
|
31
|
+
uvx warmpath company --help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Browser sessions
|
|
35
|
+
|
|
36
|
+
Supported browsers are Chrome, Chromium, Firefox, Edge, Brave, Safari, Arc, Vivaldi, Opera, Opera GX (`opera-gx`), and LibreWolf; availability depends on your operating system. Your OS may ask for keychain/keyring access. If the cookie store is locked, close the browser and retry. Private-window sessions cannot be imported.
|
|
37
|
+
|
|
38
|
+
Warmpath imports only LinkedIn's `li_at` and `JSESSIONID` cookies and manages the saved session automatically. `company`, `skill`, and `human` use it on subsequent runs. Failed imports preserve the previous session.
|
|
39
|
+
|
|
40
|
+
`auth import` and `auth status` print only `Logged in as <name>` after confirming your session with LinkedIn. If `auth status` cannot confirm a login, it prints `Not logged in` and exits with status 1. Both commands require a connection to LinkedIn. If the session expires or LinkedIn stops accepting it, log in again and repeat `auth import`.
|
|
41
|
+
|
|
42
|
+
## Development
|
|
43
|
+
|
|
44
|
+
Browser cookie import uses [browser-cookie3](https://github.com/borisbabic/browser_cookie3).
|
|
45
|
+
|
|
46
|
+
[go-task](https://taskfile.dev/) runs the repository checks. Install it with Homebrew on macOS, or follow the [installation guide](https://taskfile.dev/docs/installation) for other platforms. Then verify the tool, install the Python development dependencies, and run the checks:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
# macOS
|
|
50
|
+
brew install go-task
|
|
51
|
+
|
|
52
|
+
task --version
|
|
53
|
+
uv sync
|
|
54
|
+
task check
|
|
55
|
+
```
|
|
@@ -18,7 +18,7 @@ For the `skill` subcommand, the call path is:
|
|
|
18
18
|
4. `find_skill_connections(...)`
|
|
19
19
|
5. `render_skill_connections_result(result)`
|
|
20
20
|
|
|
21
|
-
`run_skill_command` builds a LinkedIn API client from the session saved by `warmpath auth import --browser <browser>`, resolves the cache directory, calls the skill search pipeline, and prints the rendered result. Use `warmpath auth status` to check
|
|
21
|
+
`run_skill_command` builds a LinkedIn API client from the session saved by `warmpath auth import --browser <browser>`, resolves the cache directory, calls the skill search pipeline, and prints the rendered result. Use `warmpath auth status` to check whether you are logged in and as whom.
|
|
22
22
|
|
|
23
23
|
## Defaults
|
|
24
24
|
|
|
@@ -6,6 +6,7 @@ from unittest.mock import Mock
|
|
|
6
6
|
import pytest
|
|
7
7
|
from requests import Request
|
|
8
8
|
from requests.cookies import RequestsCookieJar
|
|
9
|
+
from requests.exceptions import ConnectionError, HTTPError, Timeout
|
|
9
10
|
|
|
10
11
|
from warmpath import auth, cli
|
|
11
12
|
|
|
@@ -17,6 +18,17 @@ def auth_path(tmp_path, monkeypatch):
|
|
|
17
18
|
return path
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
@pytest.fixture(autouse=True)
|
|
22
|
+
def profile_fetch(monkeypatch):
|
|
23
|
+
response = Mock()
|
|
24
|
+
response.json.return_value = {
|
|
25
|
+
"miniProfile": {"firstName": "Ada", "lastName": "Lovelace"},
|
|
26
|
+
}
|
|
27
|
+
fetch = Mock(return_value=response)
|
|
28
|
+
monkeypatch.setattr(cli.Linkedin, "_fetch", fetch)
|
|
29
|
+
return fetch
|
|
30
|
+
|
|
31
|
+
|
|
20
32
|
@pytest.fixture
|
|
21
33
|
def browser_cookies():
|
|
22
34
|
jar = RequestsCookieJar()
|
|
@@ -136,26 +148,124 @@ def test_imported_session_reaches_linkedin_with_csrf_and_cookie_attributes(reade
|
|
|
136
148
|
assert "Cookie" not in session.prepare_request(Request("GET", "http://www.linkedin.com/")).headers
|
|
137
149
|
|
|
138
150
|
|
|
139
|
-
def
|
|
151
|
+
def test_import_and_status_print_only_the_logged_in_user(reader, capsys, auth_path, profile_fetch):
|
|
140
152
|
cli.main(["auth", "import", "--browser", "Chrome"])
|
|
153
|
+
output = capsys.readouterr()
|
|
154
|
+
assert output.out == "Logged in as Ada Lovelace\n"
|
|
155
|
+
assert output.err == ""
|
|
156
|
+
profile_fetch.assert_called_once_with("/me", timeout=15, evade=cli.no_delay)
|
|
157
|
+
profile_fetch.reset_mock()
|
|
141
158
|
before = auth_path.read_bytes()
|
|
142
159
|
reader.reset_mock()
|
|
143
160
|
cli.main(["auth", "status"])
|
|
144
161
|
|
|
145
162
|
output = capsys.readouterr()
|
|
146
163
|
assert output.err == ""
|
|
147
|
-
assert output.out
|
|
148
|
-
assert
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
164
|
+
assert output.out == "Logged in as Ada Lovelace\n"
|
|
165
|
+
assert auth_path.read_bytes() == before
|
|
166
|
+
reader.assert_not_called()
|
|
167
|
+
profile_fetch.assert_called_once_with("/me", timeout=15, evade=cli.no_delay)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@pytest.mark.parametrize("command", [["status"], ["import", "--browser", "chrome"]])
|
|
171
|
+
@pytest.mark.parametrize("profile,expected", [
|
|
172
|
+
({"firstName": " Ada ", "lastName": " Lovelace "}, "Ada Lovelace"),
|
|
173
|
+
({"firstName": "Адель", "lastName": "Низамутдинов"}, "Адель Низамутдинов"),
|
|
174
|
+
({"firstName": "Ada"}, "Ada"),
|
|
175
|
+
({"lastName": "Lovelace"}, "Lovelace"),
|
|
176
|
+
])
|
|
177
|
+
def test_auth_prints_the_logged_in_users_name(command, profile, expected, reader, profile_fetch, capsys):
|
|
178
|
+
auth.import_browser("chrome")
|
|
179
|
+
profile_fetch.return_value.json.return_value = {"miniProfile": profile}
|
|
180
|
+
|
|
181
|
+
cli.main(["auth", *command])
|
|
182
|
+
|
|
183
|
+
output = capsys.readouterr()
|
|
184
|
+
assert output.out == f"Logged in as {expected}\n"
|
|
185
|
+
assert output.err == ""
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@pytest.mark.parametrize("payload", [
|
|
189
|
+
{}, None, [], {"miniProfile": None}, {"miniProfile": "private-linkedin-token"},
|
|
190
|
+
{"miniProfile": {"firstName": " ", "lastName": None}},
|
|
191
|
+
{"miniProfile": {"firstName": ["private-linkedin-token"], "lastName": 123}},
|
|
192
|
+
])
|
|
193
|
+
def test_status_rejects_a_profile_without_a_name(payload, reader, profile_fetch, capsys):
|
|
194
|
+
auth.import_browser("chrome")
|
|
195
|
+
profile_fetch.return_value.json.return_value = payload
|
|
196
|
+
|
|
197
|
+
with pytest.raises(SystemExit) as error:
|
|
198
|
+
cli.main(["auth", "status"])
|
|
199
|
+
|
|
200
|
+
assert error.value.code == 1
|
|
201
|
+
output = capsys.readouterr()
|
|
202
|
+
assert output.out == "Not logged in\n"
|
|
203
|
+
assert output.err == ""
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@pytest.mark.parametrize("failure", ["connection", "timeout", "http", "json"])
|
|
207
|
+
def test_status_handles_failed_user_lookup_without_exposing_secrets(failure, reader, profile_fetch, capsys, auth_path):
|
|
208
|
+
auth.import_browser("chrome")
|
|
209
|
+
before = auth_path.read_bytes()
|
|
210
|
+
reader.reset_mock()
|
|
211
|
+
if failure == "connection":
|
|
212
|
+
profile_fetch.side_effect = ConnectionError("private-linkedin-token")
|
|
213
|
+
elif failure == "timeout":
|
|
214
|
+
profile_fetch.side_effect = Timeout("private-linkedin-token")
|
|
215
|
+
elif failure == "http":
|
|
216
|
+
profile_fetch.return_value.raise_for_status.side_effect = HTTPError("private-linkedin-token")
|
|
217
|
+
else:
|
|
218
|
+
profile_fetch.return_value.json.side_effect = ValueError("private-linkedin-token")
|
|
219
|
+
|
|
220
|
+
with pytest.raises(SystemExit) as error:
|
|
221
|
+
cli.main(["auth", "status"])
|
|
222
|
+
|
|
223
|
+
assert error.value.code == 1
|
|
224
|
+
output = capsys.readouterr()
|
|
225
|
+
assert output.out == "Not logged in\n"
|
|
226
|
+
assert output.err == ""
|
|
155
227
|
assert auth_path.read_bytes() == before
|
|
156
228
|
reader.assert_not_called()
|
|
157
229
|
|
|
158
230
|
|
|
231
|
+
@pytest.mark.parametrize("status_code", [401, 403])
|
|
232
|
+
def test_status_reports_rejected_sessions(status_code, reader, profile_fetch, capsys):
|
|
233
|
+
auth.import_browser("chrome")
|
|
234
|
+
profile_fetch.return_value.status_code = status_code
|
|
235
|
+
|
|
236
|
+
with pytest.raises(SystemExit) as error:
|
|
237
|
+
cli.main(["auth", "status"])
|
|
238
|
+
|
|
239
|
+
assert error.value.code == 1
|
|
240
|
+
output = capsys.readouterr()
|
|
241
|
+
assert output.out == "Not logged in\n"
|
|
242
|
+
assert output.err == ""
|
|
243
|
+
profile_fetch.return_value.json.assert_not_called()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@pytest.mark.parametrize("failure", ["rejected", "connection", "missing_name"])
|
|
247
|
+
def test_failed_import_lookup_preserves_previous_session(failure, reader, browser_cookies, auth_path, profile_fetch, capsys):
|
|
248
|
+
auth.import_browser("chrome")
|
|
249
|
+
before = auth_path.read_bytes()
|
|
250
|
+
next(cookie for cookie in browser_cookies if cookie.name == "li_at").value = "new-private-token"
|
|
251
|
+
if failure == "rejected":
|
|
252
|
+
profile_fetch.return_value.status_code = 401
|
|
253
|
+
elif failure == "connection":
|
|
254
|
+
profile_fetch.side_effect = ConnectionError("new-private-token")
|
|
255
|
+
else:
|
|
256
|
+
profile_fetch.return_value.json.return_value = {}
|
|
257
|
+
|
|
258
|
+
with pytest.raises(SystemExit) as error:
|
|
259
|
+
cli.main(["auth", "import", "--browser", "chrome"])
|
|
260
|
+
|
|
261
|
+
assert error.value.code == 2
|
|
262
|
+
output = capsys.readouterr()
|
|
263
|
+
assert output.out == ""
|
|
264
|
+
assert output.err
|
|
265
|
+
assert "new-private-token" not in output.err
|
|
266
|
+
assert auth_path.read_bytes() == before
|
|
267
|
+
|
|
268
|
+
|
|
159
269
|
@pytest.mark.skipif(os.name != "posix", reason="Unix file permissions")
|
|
160
270
|
def test_auth_store_is_private_even_when_replacing_an_existing_file(reader, auth_path):
|
|
161
271
|
auth_path.parent.mkdir()
|
|
@@ -217,7 +327,7 @@ def test_failed_save_preserves_previous_session_and_cleans_up(reader, auth_path,
|
|
|
217
327
|
|
|
218
328
|
|
|
219
329
|
@pytest.mark.parametrize("command", [
|
|
220
|
-
["
|
|
330
|
+
["company", "Acme"], ["skill", "Python"],
|
|
221
331
|
["human", "https://www.linkedin.com/in/example/"],
|
|
222
332
|
])
|
|
223
333
|
def test_missing_auth_has_import_guidance_without_browser_or_api_access(command, reader, monkeypatch, capsys, auth_path):
|
|
@@ -234,19 +344,34 @@ def test_missing_auth_has_import_guidance_without_browser_or_api_access(command,
|
|
|
234
344
|
linkedin.assert_not_called()
|
|
235
345
|
|
|
236
346
|
|
|
347
|
+
def test_missing_auth_status_prints_not_logged_in(reader, monkeypatch, capsys, auth_path):
|
|
348
|
+
linkedin = Mock()
|
|
349
|
+
monkeypatch.setattr(cli, "Linkedin", linkedin)
|
|
350
|
+
|
|
351
|
+
with pytest.raises(SystemExit) as error:
|
|
352
|
+
cli.main(["auth", "status"])
|
|
353
|
+
|
|
354
|
+
assert error.value.code == 1
|
|
355
|
+
output = capsys.readouterr()
|
|
356
|
+
assert output.out == "Not logged in\n"
|
|
357
|
+
assert output.err == ""
|
|
358
|
+
assert not auth_path.exists()
|
|
359
|
+
reader.assert_not_called()
|
|
360
|
+
linkedin.assert_not_called()
|
|
361
|
+
|
|
362
|
+
|
|
237
363
|
@pytest.mark.parametrize("content", [b"", b"[]", b"{}", b"private-token", b"\xff"])
|
|
238
|
-
def
|
|
364
|
+
def test_corrupt_store_status_prints_not_logged_in(content, auth_path, capsys):
|
|
239
365
|
auth_path.parent.mkdir()
|
|
240
366
|
auth_path.write_bytes(content)
|
|
241
367
|
|
|
242
368
|
with pytest.raises(SystemExit) as error:
|
|
243
369
|
cli.main(["auth", "status"])
|
|
244
370
|
|
|
245
|
-
assert error.value.code ==
|
|
371
|
+
assert error.value.code == 1
|
|
246
372
|
output = capsys.readouterr()
|
|
247
|
-
assert
|
|
248
|
-
assert
|
|
249
|
-
assert "private-token" not in output.out + output.err
|
|
373
|
+
assert output.out == "Not logged in\n"
|
|
374
|
+
assert output.err == ""
|
|
250
375
|
assert auth_path.read_bytes() == content
|
|
251
376
|
|
|
252
377
|
|
|
@@ -266,7 +391,7 @@ def test_invalid_stored_cookie_is_rejected(field, value, reader, auth_path):
|
|
|
266
391
|
auth.load_cookies()
|
|
267
392
|
|
|
268
393
|
|
|
269
|
-
def test_expired_session_is_reported_and_rejected_without_reimport(reader, capsys):
|
|
394
|
+
def test_expired_session_is_reported_and_rejected_without_reimport(reader, capsys, profile_fetch):
|
|
270
395
|
session = auth.import_browser("chrome")
|
|
271
396
|
next(cookie for cookie in session.cookies if cookie.name == "li_at").expires = 1
|
|
272
397
|
auth.save_auth(session)
|
|
@@ -277,9 +402,8 @@ def test_expired_session_is_reported_and_rejected_without_reimport(reader, capsy
|
|
|
277
402
|
|
|
278
403
|
assert status_error.value.code == 1
|
|
279
404
|
output = capsys.readouterr()
|
|
280
|
-
assert
|
|
281
|
-
assert
|
|
282
|
-
assert "warmpath auth import --browser chrome" in output.out
|
|
405
|
+
assert output.out == "Not logged in\n"
|
|
406
|
+
assert output.err == ""
|
|
283
407
|
|
|
284
408
|
with pytest.raises(SystemExit) as api_error:
|
|
285
409
|
cli.build_api()
|
|
@@ -287,6 +411,7 @@ def test_expired_session_is_reported_and_rejected_without_reimport(reader, capsy
|
|
|
287
411
|
assert api_error.value.code == 2
|
|
288
412
|
assert "warmpath auth import --browser chrome" in capsys.readouterr().err
|
|
289
413
|
reader.assert_not_called()
|
|
414
|
+
profile_fetch.assert_not_called()
|
|
290
415
|
|
|
291
416
|
|
|
292
417
|
@pytest.mark.parametrize("arguments", [[], ["import"], ["import", "--browser", "unknown"], ["login"]])
|
|
@@ -125,7 +125,7 @@ def save_auth(session: AuthSession) -> None:
|
|
|
125
125
|
temporary.unlink(missing_ok=True)
|
|
126
126
|
|
|
127
127
|
|
|
128
|
-
def import_browser(browser: str) -> AuthSession:
|
|
128
|
+
def import_browser(browser: str, *, save: bool = True) -> AuthSession:
|
|
129
129
|
if browser not in BROWSERS:
|
|
130
130
|
raise AuthError(f"Unsupported browser. Choose from: {', '.join(BROWSERS)}.")
|
|
131
131
|
try:
|
|
@@ -147,7 +147,8 @@ def import_browser(browser: str) -> AuthSession:
|
|
|
147
147
|
f"{', '.join(missing)}. Sign in to LinkedIn in a regular browser window, "
|
|
148
148
|
f"then run warmpath auth import --browser {browser} again."
|
|
149
149
|
)
|
|
150
|
-
|
|
150
|
+
if save:
|
|
151
|
+
save_auth(session)
|
|
151
152
|
return session
|
|
152
153
|
|
|
153
154
|
|
|
@@ -213,22 +214,3 @@ def load_cookies() -> RequestsCookieJar:
|
|
|
213
214
|
f"Sign in to LinkedIn, then run warmpath auth import --browser {session.browser}."
|
|
214
215
|
)
|
|
215
216
|
return select_cookies(session.cookies)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def render_status(session: AuthSession) -> str:
|
|
219
|
-
missing = session.missing_cookies()
|
|
220
|
-
lines = [
|
|
221
|
-
f"Status: {'expired or incomplete' if missing else 'ready'} (local expiry check)",
|
|
222
|
-
f"Browser: {session.browser}",
|
|
223
|
-
f"Imported: {session.imported_at.isoformat()}",
|
|
224
|
-
f"Store: {auth_store_path()}",
|
|
225
|
-
]
|
|
226
|
-
for cookie in sorted(session.cookies, key=lambda cookie: cookie.name):
|
|
227
|
-
expiry = "session"
|
|
228
|
-
if cookie.expires is not None:
|
|
229
|
-
expiry = datetime.fromtimestamp(cookie.expires, timezone.utc).isoformat()
|
|
230
|
-
lines.append(f"Cookie: {cookie.name}; expires: {expiry}")
|
|
231
|
-
if missing:
|
|
232
|
-
lines.append(f"Missing or expired: {', '.join(missing)}")
|
|
233
|
-
lines.append(f"Run warmpath auth import --browser {session.browser} again.")
|
|
234
|
-
return "\n".join(lines)
|
|
@@ -9,6 +9,7 @@ from typing import Any, Callable, NoReturn
|
|
|
9
9
|
from urllib.parse import unquote, urlparse
|
|
10
10
|
|
|
11
11
|
from open_linkedin_api import Linkedin
|
|
12
|
+
from requests.exceptions import RequestException
|
|
12
13
|
|
|
13
14
|
from warmpath import auth
|
|
14
15
|
|
|
@@ -99,9 +100,9 @@ def use_fast_fetches(api: Any) -> None:
|
|
|
99
100
|
api._post = fast_post
|
|
100
101
|
|
|
101
102
|
|
|
102
|
-
def build_api() -> Any:
|
|
103
|
+
def build_api(session: auth.AuthSession | None = None) -> Any:
|
|
103
104
|
try:
|
|
104
|
-
cookies = auth.load_cookies()
|
|
105
|
+
cookies = auth.select_cookies(session.cookies) if session is not None else auth.load_cookies()
|
|
105
106
|
except auth.AuthError as exc:
|
|
106
107
|
fail(str(exc), 2)
|
|
107
108
|
api = Linkedin("", "", cookies=cookies)
|
|
@@ -109,6 +110,35 @@ def build_api() -> Any:
|
|
|
109
110
|
return api
|
|
110
111
|
|
|
111
112
|
|
|
113
|
+
def logged_in_user_name(api: Any) -> str:
|
|
114
|
+
try:
|
|
115
|
+
response = api._fetch("/me", timeout=15)
|
|
116
|
+
if response.status_code in (401, 403):
|
|
117
|
+
raise auth.AuthError(
|
|
118
|
+
"LinkedIn rejected the saved session. Sign in to LinkedIn, "
|
|
119
|
+
f"then reimport your session. {auth.IMPORT_HINT}"
|
|
120
|
+
)
|
|
121
|
+
response.raise_for_status()
|
|
122
|
+
data = response.json()
|
|
123
|
+
except (RequestException, ValueError) as exc:
|
|
124
|
+
raise auth.AuthError(
|
|
125
|
+
"Could not fetch the logged-in LinkedIn user. Check your connection "
|
|
126
|
+
f"and try again, or reimport your session. {auth.IMPORT_HINT}"
|
|
127
|
+
) from exc
|
|
128
|
+
|
|
129
|
+
profile = data.get("miniProfile") if isinstance(data, dict) else None
|
|
130
|
+
name = ""
|
|
131
|
+
if isinstance(profile, dict):
|
|
132
|
+
name = " ".join(
|
|
133
|
+
value.strip()
|
|
134
|
+
for key in ("firstName", "lastName")
|
|
135
|
+
if isinstance(value := profile.get(key), str) and value.strip()
|
|
136
|
+
)
|
|
137
|
+
if not name:
|
|
138
|
+
raise auth.AuthError("LinkedIn did not return a name for the logged-in user.")
|
|
139
|
+
return name
|
|
140
|
+
|
|
141
|
+
|
|
112
142
|
def profile_urn_id(api: Any, public_id: str) -> str:
|
|
113
143
|
urn_id = profile_urn_id_from_html(api, public_id)
|
|
114
144
|
if urn_id:
|
|
@@ -1743,21 +1773,28 @@ def parse_auth_args(argv: list[str]) -> argparse.Namespace:
|
|
|
1743
1773
|
choices=auth.BROWSERS,
|
|
1744
1774
|
help="Browser where you are logged in to LinkedIn.",
|
|
1745
1775
|
)
|
|
1746
|
-
commands.add_parser("status", help="
|
|
1776
|
+
commands.add_parser("status", help="Show whether you are logged in and as whom.")
|
|
1747
1777
|
return parser.parse_args(argv)
|
|
1748
1778
|
|
|
1749
1779
|
|
|
1750
1780
|
def run_auth_command(args: argparse.Namespace) -> None:
|
|
1751
1781
|
try:
|
|
1752
1782
|
session = (
|
|
1753
|
-
auth.import_browser(args.browser)
|
|
1783
|
+
auth.import_browser(args.browser, save=False)
|
|
1754
1784
|
if args.auth_command == "import"
|
|
1755
1785
|
else auth.load_auth()
|
|
1756
1786
|
)
|
|
1757
|
-
print(auth.render_status(session))
|
|
1758
1787
|
if session.missing_cookies():
|
|
1788
|
+
print("Not logged in")
|
|
1759
1789
|
raise SystemExit(1)
|
|
1790
|
+
name = logged_in_user_name(build_api(session))
|
|
1791
|
+
if args.auth_command == "import":
|
|
1792
|
+
auth.save_auth(session)
|
|
1793
|
+
print(f"Logged in as {name}")
|
|
1760
1794
|
except auth.AuthError as exc:
|
|
1795
|
+
if args.auth_command == "status":
|
|
1796
|
+
print("Not logged in")
|
|
1797
|
+
raise SystemExit(1)
|
|
1761
1798
|
fail(str(exc), 2)
|
|
1762
1799
|
|
|
1763
1800
|
|
|
@@ -1771,7 +1808,7 @@ def parse_main_args(argv: list[str]) -> argparse.Namespace:
|
|
|
1771
1808
|
Import your LinkedIn session from a browser.
|
|
1772
1809
|
|
|
1773
1810
|
auth status
|
|
1774
|
-
|
|
1811
|
+
Show whether you are logged in and as whom.
|
|
1775
1812
|
|
|
1776
1813
|
human PROFILE_URL
|
|
1777
1814
|
Print mutual LinkedIn connections for a profile URL.
|
warmpath-0.3.1/README.md
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-

|
|
2
|
-
|
|
3
|
-
# Warmpath
|
|
4
|
-
|
|
5
|
-
Find LinkedIn mutuals and warm paths using your logged-in browser session.
|
|
6
|
-
|
|
7
|
-
## Setup
|
|
8
|
-
|
|
9
|
-
Log in to LinkedIn in a regular browser window, then import that session:
|
|
10
|
-
|
|
11
|
-
```sh
|
|
12
|
-
uvx warmpath auth import --browser chrome
|
|
13
|
-
uvx warmpath auth status
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Import uses [browser-cookie3](https://github.com/borisbabic/browser_cookie3) to read your browser's local cookie store. Supported browsers are Chrome, Chromium, Firefox, Edge, Brave, Safari, Arc, Vivaldi, Opera, Opera GX (`opera-gx`), and LibreWolf; availability depends on your operating system. Your OS may ask for keychain/keyring access. If the cookie store is locked, close the browser and retry. Private-window sessions cannot be imported.
|
|
17
|
-
|
|
18
|
-
Warmpath imports only LinkedIn's `li_at` and `JSESSIONID` cookies and manages the saved session automatically. `company`, `skill`, and `human` use it on subsequent runs. Failed imports preserve the previous session.
|
|
19
|
-
|
|
20
|
-
`auth status` shows the source browser, import time, cookie expiry, and storage location without displaying cookie values. It checks the saved session locally; LinkedIn may revoke a session before its cookies expire. If the session expires or LinkedIn stops accepting it, log in again and repeat `auth import`.
|
|
21
|
-
|
|
22
|
-
## Development
|
|
23
|
-
|
|
24
|
-
[go-task](https://taskfile.dev/) is required to run the repository checks. Install it with Homebrew on macOS:
|
|
25
|
-
|
|
26
|
-
```sh
|
|
27
|
-
brew install go-task
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
For other platforms, follow the [go-task installation guide](https://taskfile.dev/docs/installation). Verify that the `task` command is available before running any checks:
|
|
31
|
-
|
|
32
|
-
```sh
|
|
33
|
-
task --version
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Then install the Python development dependencies and run the full check suite:
|
|
37
|
-
|
|
38
|
-
```sh
|
|
39
|
-
uv sync
|
|
40
|
-
task check
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Usage
|
|
44
|
-
|
|
45
|
-
### Company
|
|
46
|
-
|
|
47
|
-
Who can introduce me into this company?
|
|
48
|
-
|
|
49
|
-
```sh
|
|
50
|
-
uvx warmpath company HashiCorp
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Skill
|
|
54
|
-
|
|
55
|
-
Which reachable people match this recruiting need?
|
|
56
|
-
|
|
57
|
-
```sh
|
|
58
|
-
uvx warmpath skill Flutter
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### Human
|
|
62
|
-
|
|
63
|
-
Can I reach this exact person, and through whom?
|
|
64
|
-
|
|
65
|
-
```sh
|
|
66
|
-
uvx warmpath human https://www.linkedin.com/in/mitchellh/
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
## More Examples
|
|
70
|
-
|
|
71
|
-
```sh
|
|
72
|
-
uvx warmpath company "HashiCorp" --max-degree 2 --limit 5
|
|
73
|
-
uvx warmpath auth import --browser firefox
|
|
74
|
-
uvx warmpath company https://www.linkedin.com/company/hashicorp/
|
|
75
|
-
uvx warmpath skill Leadership --max-depth 2
|
|
76
|
-
uvx warmpath human https://www.linkedin.com/in/mitchellh/ --refresh-cache
|
|
77
|
-
uvx warmpath company --help
|
|
78
|
-
uvx warmpath skill --help
|
|
79
|
-
uvx warmpath human --help
|
|
80
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|