warmpath 0.3.2__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.2 → warmpath-0.3.3}/PKG-INFO +1 -1
- {warmpath-0.3.2 → warmpath-0.3.3}/README.md +1 -1
- {warmpath-0.3.2 → warmpath-0.3.3}/docs/skill.md +1 -1
- {warmpath-0.3.2 → warmpath-0.3.3}/pyproject.toml +1 -1
- {warmpath-0.3.2 → warmpath-0.3.3}/tests/test_auth.py +68 -36
- {warmpath-0.3.2 → warmpath-0.3.3}/uv.lock +1 -1
- {warmpath-0.3.2 → warmpath-0.3.3}/warmpath/auth.py +3 -21
- {warmpath-0.3.2 → warmpath-0.3.3}/warmpath/cli.py +13 -8
- {warmpath-0.3.2 → warmpath-0.3.3}/.gitignore +0 -0
- {warmpath-0.3.2 → warmpath-0.3.3}/AGENTS.md +0 -0
- {warmpath-0.3.2 → warmpath-0.3.3}/Taskfile.yaml +0 -0
- {warmpath-0.3.2 → warmpath-0.3.3}/UNLICENSE +0 -0
- {warmpath-0.3.2 → warmpath-0.3.3}/scripts/release.sh +0 -0
- {warmpath-0.3.2 → warmpath-0.3.3}/tests/test_cli_commands.py +0 -0
- {warmpath-0.3.2 → warmpath-0.3.3}/tests/test_mutual_connections.py +0 -0
- {warmpath-0.3.2 → warmpath-0.3.3}/warmpath/__init__.py +0 -0
- {warmpath-0.3.2 → warmpath-0.3.3}/warmpath.jpg +0 -0
|
@@ -37,7 +37,7 @@ Supported browsers are Chrome, Chromium, Firefox, Edge, Brave, Safari, Arc, Viva
|
|
|
37
37
|
|
|
38
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
39
|
|
|
40
|
-
`auth status`
|
|
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
41
|
|
|
42
42
|
## Development
|
|
43
43
|
|
|
@@ -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
|
|
|
@@ -148,42 +148,41 @@ def test_imported_session_reaches_linkedin_with_csrf_and_cookie_attributes(reade
|
|
|
148
148
|
assert "Cookie" not in session.prepare_request(Request("GET", "http://www.linkedin.com/")).headers
|
|
149
149
|
|
|
150
150
|
|
|
151
|
-
def
|
|
151
|
+
def test_import_and_status_print_only_the_logged_in_user(reader, capsys, auth_path, profile_fetch):
|
|
152
152
|
cli.main(["auth", "import", "--browser", "Chrome"])
|
|
153
|
-
|
|
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()
|
|
154
158
|
before = auth_path.read_bytes()
|
|
155
159
|
reader.reset_mock()
|
|
156
160
|
cli.main(["auth", "status"])
|
|
157
161
|
|
|
158
162
|
output = capsys.readouterr()
|
|
159
163
|
assert output.err == ""
|
|
160
|
-
assert output.out
|
|
161
|
-
assert output.out.count("User: Ada Lovelace") == 1
|
|
162
|
-
assert "Browser: chrome" in output.out
|
|
163
|
-
assert "Imported:" in output.out
|
|
164
|
-
assert str(auth_path) in output.out
|
|
165
|
-
assert "Cookie: JSESSIONID; expires: session" in output.out
|
|
166
|
-
assert "Cookie: li_at; expires: 2100-01-01T00:00:00+00:00" in output.out
|
|
167
|
-
assert "private-linkedin-token" not in output.out
|
|
168
|
-
assert "private-csrf-token" not in output.out
|
|
164
|
+
assert output.out == "Logged in as Ada Lovelace\n"
|
|
169
165
|
assert auth_path.read_bytes() == before
|
|
170
166
|
reader.assert_not_called()
|
|
171
167
|
profile_fetch.assert_called_once_with("/me", timeout=15, evade=cli.no_delay)
|
|
172
168
|
|
|
173
169
|
|
|
170
|
+
@pytest.mark.parametrize("command", [["status"], ["import", "--browser", "chrome"]])
|
|
174
171
|
@pytest.mark.parametrize("profile,expected", [
|
|
175
172
|
({"firstName": " Ada ", "lastName": " Lovelace "}, "Ada Lovelace"),
|
|
176
173
|
({"firstName": "Адель", "lastName": "Низамутдинов"}, "Адель Низамутдинов"),
|
|
177
174
|
({"firstName": "Ada"}, "Ada"),
|
|
178
175
|
({"lastName": "Lovelace"}, "Lovelace"),
|
|
179
176
|
])
|
|
180
|
-
def
|
|
177
|
+
def test_auth_prints_the_logged_in_users_name(command, profile, expected, reader, profile_fetch, capsys):
|
|
181
178
|
auth.import_browser("chrome")
|
|
182
179
|
profile_fetch.return_value.json.return_value = {"miniProfile": profile}
|
|
183
180
|
|
|
184
|
-
cli.main(["auth",
|
|
181
|
+
cli.main(["auth", *command])
|
|
185
182
|
|
|
186
|
-
|
|
183
|
+
output = capsys.readouterr()
|
|
184
|
+
assert output.out == f"Logged in as {expected}\n"
|
|
185
|
+
assert output.err == ""
|
|
187
186
|
|
|
188
187
|
|
|
189
188
|
@pytest.mark.parametrize("payload", [
|
|
@@ -198,11 +197,10 @@ def test_status_rejects_a_profile_without_a_name(payload, reader, profile_fetch,
|
|
|
198
197
|
with pytest.raises(SystemExit) as error:
|
|
199
198
|
cli.main(["auth", "status"])
|
|
200
199
|
|
|
201
|
-
assert error.value.code ==
|
|
200
|
+
assert error.value.code == 1
|
|
202
201
|
output = capsys.readouterr()
|
|
203
|
-
assert
|
|
204
|
-
assert "
|
|
205
|
-
assert "private-linkedin-token" not in output.out + output.err
|
|
202
|
+
assert output.out == "Not logged in\n"
|
|
203
|
+
assert output.err == ""
|
|
206
204
|
|
|
207
205
|
|
|
208
206
|
@pytest.mark.parametrize("failure", ["connection", "timeout", "http", "json"])
|
|
@@ -222,12 +220,10 @@ def test_status_handles_failed_user_lookup_without_exposing_secrets(failure, rea
|
|
|
222
220
|
with pytest.raises(SystemExit) as error:
|
|
223
221
|
cli.main(["auth", "status"])
|
|
224
222
|
|
|
225
|
-
assert error.value.code ==
|
|
223
|
+
assert error.value.code == 1
|
|
226
224
|
output = capsys.readouterr()
|
|
227
|
-
assert
|
|
228
|
-
assert
|
|
229
|
-
assert "User:" not in output.out
|
|
230
|
-
assert "private-linkedin-token" not in output.out + output.err
|
|
225
|
+
assert output.out == "Not logged in\n"
|
|
226
|
+
assert output.err == ""
|
|
231
227
|
assert auth_path.read_bytes() == before
|
|
232
228
|
reader.assert_not_called()
|
|
233
229
|
|
|
@@ -240,14 +236,36 @@ def test_status_reports_rejected_sessions(status_code, reader, profile_fetch, ca
|
|
|
240
236
|
with pytest.raises(SystemExit) as error:
|
|
241
237
|
cli.main(["auth", "status"])
|
|
242
238
|
|
|
243
|
-
assert error.value.code ==
|
|
239
|
+
assert error.value.code == 1
|
|
244
240
|
output = capsys.readouterr()
|
|
245
|
-
assert
|
|
246
|
-
assert
|
|
247
|
-
assert "User:" not in output.out
|
|
241
|
+
assert output.out == "Not logged in\n"
|
|
242
|
+
assert output.err == ""
|
|
248
243
|
profile_fetch.return_value.json.assert_not_called()
|
|
249
244
|
|
|
250
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
|
+
|
|
251
269
|
@pytest.mark.skipif(os.name != "posix", reason="Unix file permissions")
|
|
252
270
|
def test_auth_store_is_private_even_when_replacing_an_existing_file(reader, auth_path):
|
|
253
271
|
auth_path.parent.mkdir()
|
|
@@ -309,7 +327,7 @@ def test_failed_save_preserves_previous_session_and_cleans_up(reader, auth_path,
|
|
|
309
327
|
|
|
310
328
|
|
|
311
329
|
@pytest.mark.parametrize("command", [
|
|
312
|
-
["
|
|
330
|
+
["company", "Acme"], ["skill", "Python"],
|
|
313
331
|
["human", "https://www.linkedin.com/in/example/"],
|
|
314
332
|
])
|
|
315
333
|
def test_missing_auth_has_import_guidance_without_browser_or_api_access(command, reader, monkeypatch, capsys, auth_path):
|
|
@@ -326,19 +344,34 @@ def test_missing_auth_has_import_guidance_without_browser_or_api_access(command,
|
|
|
326
344
|
linkedin.assert_not_called()
|
|
327
345
|
|
|
328
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
|
+
|
|
329
363
|
@pytest.mark.parametrize("content", [b"", b"[]", b"{}", b"private-token", b"\xff"])
|
|
330
|
-
def
|
|
364
|
+
def test_corrupt_store_status_prints_not_logged_in(content, auth_path, capsys):
|
|
331
365
|
auth_path.parent.mkdir()
|
|
332
366
|
auth_path.write_bytes(content)
|
|
333
367
|
|
|
334
368
|
with pytest.raises(SystemExit) as error:
|
|
335
369
|
cli.main(["auth", "status"])
|
|
336
370
|
|
|
337
|
-
assert error.value.code ==
|
|
371
|
+
assert error.value.code == 1
|
|
338
372
|
output = capsys.readouterr()
|
|
339
|
-
assert
|
|
340
|
-
assert
|
|
341
|
-
assert "private-token" not in output.out + output.err
|
|
373
|
+
assert output.out == "Not logged in\n"
|
|
374
|
+
assert output.err == ""
|
|
342
375
|
assert auth_path.read_bytes() == content
|
|
343
376
|
|
|
344
377
|
|
|
@@ -369,9 +402,8 @@ def test_expired_session_is_reported_and_rejected_without_reimport(reader, capsy
|
|
|
369
402
|
|
|
370
403
|
assert status_error.value.code == 1
|
|
371
404
|
output = capsys.readouterr()
|
|
372
|
-
assert
|
|
373
|
-
assert
|
|
374
|
-
assert "warmpath auth import --browser chrome" in output.out
|
|
405
|
+
assert output.out == "Not logged in\n"
|
|
406
|
+
assert output.err == ""
|
|
375
407
|
|
|
376
408
|
with pytest.raises(SystemExit) as api_error:
|
|
377
409
|
cli.build_api()
|
|
@@ -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)
|
|
@@ -100,9 +100,9 @@ def use_fast_fetches(api: Any) -> None:
|
|
|
100
100
|
api._post = fast_post
|
|
101
101
|
|
|
102
102
|
|
|
103
|
-
def build_api() -> Any:
|
|
103
|
+
def build_api(session: auth.AuthSession | None = None) -> Any:
|
|
104
104
|
try:
|
|
105
|
-
cookies = auth.load_cookies()
|
|
105
|
+
cookies = auth.select_cookies(session.cookies) if session is not None else auth.load_cookies()
|
|
106
106
|
except auth.AuthError as exc:
|
|
107
107
|
fail(str(exc), 2)
|
|
108
108
|
api = Linkedin("", "", cookies=cookies)
|
|
@@ -1773,23 +1773,28 @@ def parse_auth_args(argv: list[str]) -> argparse.Namespace:
|
|
|
1773
1773
|
choices=auth.BROWSERS,
|
|
1774
1774
|
help="Browser where you are logged in to LinkedIn.",
|
|
1775
1775
|
)
|
|
1776
|
-
commands.add_parser("status", help="Show
|
|
1776
|
+
commands.add_parser("status", help="Show whether you are logged in and as whom.")
|
|
1777
1777
|
return parser.parse_args(argv)
|
|
1778
1778
|
|
|
1779
1779
|
|
|
1780
1780
|
def run_auth_command(args: argparse.Namespace) -> None:
|
|
1781
1781
|
try:
|
|
1782
1782
|
session = (
|
|
1783
|
-
auth.import_browser(args.browser)
|
|
1783
|
+
auth.import_browser(args.browser, save=False)
|
|
1784
1784
|
if args.auth_command == "import"
|
|
1785
1785
|
else auth.load_auth()
|
|
1786
1786
|
)
|
|
1787
|
-
print(auth.render_status(session))
|
|
1788
1787
|
if session.missing_cookies():
|
|
1788
|
+
print("Not logged in")
|
|
1789
1789
|
raise SystemExit(1)
|
|
1790
|
-
|
|
1791
|
-
|
|
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}")
|
|
1792
1794
|
except auth.AuthError as exc:
|
|
1795
|
+
if args.auth_command == "status":
|
|
1796
|
+
print("Not logged in")
|
|
1797
|
+
raise SystemExit(1)
|
|
1793
1798
|
fail(str(exc), 2)
|
|
1794
1799
|
|
|
1795
1800
|
|
|
@@ -1803,7 +1808,7 @@ def parse_main_args(argv: list[str]) -> argparse.Namespace:
|
|
|
1803
1808
|
Import your LinkedIn session from a browser.
|
|
1804
1809
|
|
|
1805
1810
|
auth status
|
|
1806
|
-
Show
|
|
1811
|
+
Show whether you are logged in and as whom.
|
|
1807
1812
|
|
|
1808
1813
|
human PROFILE_URL
|
|
1809
1814
|
Print mutual LinkedIn connections for a profile URL.
|
|
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
|