warmpath 0.1.0__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.1.0/.gitignore +12 -0
- warmpath-0.1.0/PKG-INFO +6 -0
- warmpath-0.1.0/README.md +56 -0
- warmpath-0.1.0/Taskfile.yaml +20 -0
- warmpath-0.1.0/UNLICENSE +24 -0
- warmpath-0.1.0/docs/skill.md +219 -0
- warmpath-0.1.0/pyproject.toml +26 -0
- warmpath-0.1.0/tests/test_cli_commands.py +725 -0
- warmpath-0.1.0/tests/test_mutual_connections.py +159 -0
- warmpath-0.1.0/uv.lock +479 -0
- warmpath-0.1.0/warmpath/__init__.py +1 -0
- warmpath-0.1.0/warmpath/cli.py +1852 -0
- warmpath-0.1.0/warmpath.jpg +0 -0
warmpath-0.1.0/PKG-INFO
ADDED
warmpath-0.1.0/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Warmpath
|
|
4
|
+
|
|
5
|
+
Find LinkedIn mutuals and warm paths using your logged-in LinkedIn cookies.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
1. Install [Get cookies.txt LOCALLY](https://chromewebstore.google.com/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc?hl=en).
|
|
10
|
+
2. Log in to LinkedIn in Chrome.
|
|
11
|
+
3. Use the extension to export cookies for `linkedin.com` in Netscape `cookies.txt` format.
|
|
12
|
+
4. Create `~/.config/warmpath/linkedin.cookies` and paste the full export there:
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
~/.config/warmpath/linkedin.cookies
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Warmpath needs the `li_at` and `JSESSIONID` cookies. Keep this file private; it lives outside the repository.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Company
|
|
23
|
+
|
|
24
|
+
Who can introduce me into this company?
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
uvx warmpath company HashiCorp
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Skill
|
|
31
|
+
|
|
32
|
+
Which reachable people match this recruiting need?
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
uvx warmpath skill Flutter
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Human
|
|
39
|
+
|
|
40
|
+
Can I reach this exact person, and through whom?
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
uvx warmpath human https://www.linkedin.com/in/mitchellh/
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## More Examples
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
uvx warmpath company "HashiCorp" --max-degree 2 --limit 5
|
|
50
|
+
uvx warmpath company https://www.linkedin.com/company/hashicorp/ --cookie-file ~/.config/warmpath/linkedin.cookies
|
|
51
|
+
uvx warmpath skill Leadership --max-depth 2
|
|
52
|
+
uvx warmpath human https://www.linkedin.com/in/mitchellh/ --refresh-cache
|
|
53
|
+
uvx warmpath company --help
|
|
54
|
+
uvx warmpath skill --help
|
|
55
|
+
uvx warmpath human --help
|
|
56
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
version: 3
|
|
2
|
+
|
|
3
|
+
env:
|
|
4
|
+
UV_CACHE_DIR: .uv-cache
|
|
5
|
+
|
|
6
|
+
tasks:
|
|
7
|
+
typecheck:
|
|
8
|
+
run: once
|
|
9
|
+
desc: Type-check Python with Pyright.
|
|
10
|
+
cmd: uv run python -m pyright warmpath
|
|
11
|
+
|
|
12
|
+
test:
|
|
13
|
+
run: once
|
|
14
|
+
desc: Run Python unit and live integration tests.
|
|
15
|
+
cmd: uv run pytest
|
|
16
|
+
|
|
17
|
+
check:
|
|
18
|
+
deps:
|
|
19
|
+
- typecheck
|
|
20
|
+
- test
|
warmpath-0.1.0/UNLICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES, OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org/>
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Warmpath Skill Command Architecture
|
|
2
|
+
|
|
3
|
+
This document describes the current implementation of:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
uvx warmpath skill <Skill>
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Entry Point
|
|
10
|
+
|
|
11
|
+
The CLI entry point is `warmpath.cli:main`.
|
|
12
|
+
|
|
13
|
+
For the `skill` subcommand, the call path is:
|
|
14
|
+
|
|
15
|
+
1. `main(argv)`
|
|
16
|
+
2. `parse_skill_args(argv[1:])`
|
|
17
|
+
3. `run_skill_command(args)`
|
|
18
|
+
4. `find_skill_connections(...)`
|
|
19
|
+
5. `render_skill_connections_result(result)`
|
|
20
|
+
|
|
21
|
+
`run_skill_command` builds a LinkedIn API client from the cookie file, resolves the cache directory, calls the skill search pipeline, and prints the rendered result.
|
|
22
|
+
|
|
23
|
+
## Defaults
|
|
24
|
+
|
|
25
|
+
Current defaults:
|
|
26
|
+
|
|
27
|
+
- `--max-depth`: `2`
|
|
28
|
+
- `--limit`: `5`
|
|
29
|
+
- candidate search window: `25`
|
|
30
|
+
- cache directory: `~/.cache/warmpath`
|
|
31
|
+
|
|
32
|
+
`--limit` is the display limit. It caps how many profiles are printed.
|
|
33
|
+
|
|
34
|
+
The candidate search window controls how many LinkedIn search rows are fetched per network depth before filtering and ranking. It is currently `max(--limit, 25)`.
|
|
35
|
+
|
|
36
|
+
## Search Flow
|
|
37
|
+
|
|
38
|
+
`find_skill_connections` loops over network depths:
|
|
39
|
+
|
|
40
|
+
- degree `1` maps to LinkedIn network filter `F`
|
|
41
|
+
- degree `2` maps to LinkedIn network filter `S`
|
|
42
|
+
|
|
43
|
+
For each degree, `fetch_skill_connection_rows` calls:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
api.search(
|
|
47
|
+
{
|
|
48
|
+
"keywords": skill,
|
|
49
|
+
"filters": (
|
|
50
|
+
"List((key:resultType,value:List(PEOPLE)),"
|
|
51
|
+
f"(key:network,value:List({network_depth})))"
|
|
52
|
+
),
|
|
53
|
+
},
|
|
54
|
+
limit=search_limit,
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
So, with default `--max-depth 2`, the command performs two LinkedIn people-search requests when the search cache is cold:
|
|
59
|
+
|
|
60
|
+
1. first-degree people search
|
|
61
|
+
2. second-degree people search
|
|
62
|
+
|
|
63
|
+
## Row Normalization
|
|
64
|
+
|
|
65
|
+
Each raw LinkedIn search result is normalized by `connection_result_row`.
|
|
66
|
+
|
|
67
|
+
The normalized row keeps:
|
|
68
|
+
|
|
69
|
+
- name
|
|
70
|
+
- network distance
|
|
71
|
+
- job title
|
|
72
|
+
- location
|
|
73
|
+
- profile URN
|
|
74
|
+
- canonical profile URL
|
|
75
|
+
- mutual connection metadata, when present
|
|
76
|
+
|
|
77
|
+
Profile URLs are canonicalized to `https://www.linkedin.com/in/<public-id>/`.
|
|
78
|
+
|
|
79
|
+
## Skill Verification
|
|
80
|
+
|
|
81
|
+
Search results are not accepted blindly.
|
|
82
|
+
|
|
83
|
+
For each candidate row, `skill_match_quality` tries to verify the requested skill:
|
|
84
|
+
|
|
85
|
+
1. Fetch profile skills with `api.get_profile_skills(public_id=<public-id>)`.
|
|
86
|
+
2. If that returns a non-empty payload, inspect the payload for an exact normalized skill match.
|
|
87
|
+
3. If the public-id lookup returns an empty list, retry with `api.get_profile_skills(urn_id=<urn-id>)`, when a URN is available.
|
|
88
|
+
4. If profile-skill lookup does not confirm the skill, fall back to visible search-row text: name, job title, and location.
|
|
89
|
+
|
|
90
|
+
Match quality is recorded as:
|
|
91
|
+
|
|
92
|
+
- `pinned_profile`
|
|
93
|
+
- `profile_skill`
|
|
94
|
+
- `visible_text`
|
|
95
|
+
|
|
96
|
+
Rows with no match quality are discarded unless they are pinned.
|
|
97
|
+
|
|
98
|
+
## Pinned Skill Profiles
|
|
99
|
+
|
|
100
|
+
Some LinkedIn skill/profile combinations can be known-good even when profile-skill endpoints return empty data.
|
|
101
|
+
|
|
102
|
+
Current pinned profiles:
|
|
103
|
+
|
|
104
|
+
- `Leadership`: `https://www.linkedin.com/in/timur-pokayonkov/`
|
|
105
|
+
|
|
106
|
+
Pinned profiles:
|
|
107
|
+
|
|
108
|
+
- bypass profile-skill verification
|
|
109
|
+
- are still required to appear in the LinkedIn search result window
|
|
110
|
+
- are ranked before non-pinned rows at the same network degree
|
|
111
|
+
|
|
112
|
+
This handles the case where LinkedIn search returns Timur as a direct connection for `Leadership`, but the profile-skill endpoint returns no usable skill payload.
|
|
113
|
+
|
|
114
|
+
## Ranking
|
|
115
|
+
|
|
116
|
+
Candidates are deduped by:
|
|
117
|
+
|
|
118
|
+
1. profile URN
|
|
119
|
+
2. profile URL
|
|
120
|
+
3. fallback tuple of name, job title, and location
|
|
121
|
+
|
|
122
|
+
Then candidates are sorted by `skill_candidate_score`:
|
|
123
|
+
|
|
124
|
+
1. network degree: first-degree before second-degree
|
|
125
|
+
2. pinned profile rank
|
|
126
|
+
3. match quality: pinned profile, profile-skill match, visible-text match
|
|
127
|
+
4. original LinkedIn search rank
|
|
128
|
+
5. name
|
|
129
|
+
|
|
130
|
+
After sorting, the list is sliced to `--limit`.
|
|
131
|
+
|
|
132
|
+
## Rendering
|
|
133
|
+
|
|
134
|
+
`render_skill_connections_result` prints:
|
|
135
|
+
|
|
136
|
+
- searched skill
|
|
137
|
+
- max depth
|
|
138
|
+
- count of first-degree and second-degree candidates in the final sliced result
|
|
139
|
+
- grouped first-degree profiles
|
|
140
|
+
- grouped second-degree profiles
|
|
141
|
+
|
|
142
|
+
Each candidate prints:
|
|
143
|
+
|
|
144
|
+
- name
|
|
145
|
+
- role, when available
|
|
146
|
+
- location, when available
|
|
147
|
+
- mutual connection names for second-degree candidates, when available
|
|
148
|
+
- profile URL, when available
|
|
149
|
+
|
|
150
|
+
## Cache Model
|
|
151
|
+
|
|
152
|
+
`cached_json` stores JSON payloads under `~/.cache/warmpath`.
|
|
153
|
+
|
|
154
|
+
Cache filenames use:
|
|
155
|
+
|
|
156
|
+
- namespace, such as `skill-search-raw` or `profile-skills`
|
|
157
|
+
- SHA-256 digest of the structured cache key
|
|
158
|
+
|
|
159
|
+
For skill search, cache keys include:
|
|
160
|
+
|
|
161
|
+
- skill text
|
|
162
|
+
- network depth
|
|
163
|
+
- search limit
|
|
164
|
+
|
|
165
|
+
For profile skills, cache keys include either:
|
|
166
|
+
|
|
167
|
+
- public profile ID
|
|
168
|
+
- profile URN ID
|
|
169
|
+
|
|
170
|
+
For mutual searches, cache keys include:
|
|
171
|
+
|
|
172
|
+
- target profile URN ID
|
|
173
|
+
- mutual search limit
|
|
174
|
+
|
|
175
|
+
`--refresh-cache` bypasses existing cache files and writes fresh payloads.
|
|
176
|
+
|
|
177
|
+
## HTTP Request Fanout
|
|
178
|
+
|
|
179
|
+
The current implementation can make substantially more than 1-5 HTTP requests when uncached.
|
|
180
|
+
|
|
181
|
+
With defaults:
|
|
182
|
+
|
|
183
|
+
- `--max-depth 2`
|
|
184
|
+
- display `--limit 5`
|
|
185
|
+
- candidate search window `25`
|
|
186
|
+
|
|
187
|
+
Cold-cache request shape:
|
|
188
|
+
|
|
189
|
+
- 1 first-degree people search
|
|
190
|
+
- 1 second-degree people search
|
|
191
|
+
- up to 25 first-degree profile-skill public-id requests
|
|
192
|
+
- up to 25 first-degree profile-skill URN fallback requests
|
|
193
|
+
- up to 25 second-degree profile-skill public-id requests
|
|
194
|
+
- up to 25 second-degree profile-skill URN fallback requests
|
|
195
|
+
- up to 5 displayed second-degree mutual-search requests when search rows do not expose mutual names
|
|
196
|
+
|
|
197
|
+
Worst case with default display limit: `2 + (25 * 2 * 2) + 5 = 107` LinkedIn API calls.
|
|
198
|
+
|
|
199
|
+
Typical count is lower because:
|
|
200
|
+
|
|
201
|
+
- pinned profiles skip profile-skill verification
|
|
202
|
+
- rows without a public ID or URN have fewer lookup keys
|
|
203
|
+
- public-id skill payloads can stop the URN fallback
|
|
204
|
+
- only displayed second-degree candidates without visible mutual names are enriched with mutual searches
|
|
205
|
+
- warm cache avoids repeated HTTP calls
|
|
206
|
+
|
|
207
|
+
This is why caching still matters for the current implementation. Parallelizing requests would reduce latency, but it would not reduce request volume or LinkedIn rate-limit exposure.
|
|
208
|
+
|
|
209
|
+
## Known Tradeoffs
|
|
210
|
+
|
|
211
|
+
The current approach favors recall over minimal request count:
|
|
212
|
+
|
|
213
|
+
- A wider search window prevents a useful direct connection from being excluded before ranking.
|
|
214
|
+
- Profile-skill verification reduces false positives from keyword search.
|
|
215
|
+
- Visible-text fallback keeps usable results when LinkedIn skill payloads are empty.
|
|
216
|
+
- Second-degree mutual enrichment keeps the printed output actionable when LinkedIn search rows omit exact mutual profiles.
|
|
217
|
+
- Pinned profiles handle known LinkedIn data gaps.
|
|
218
|
+
|
|
219
|
+
The main cost is HTTP fanout. If request volume becomes the priority, the likely simplification is to remove per-profile skill verification and trust LinkedIn keyword search plus visible-text ranking.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "warmpath"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Local LinkedIn visible-connections scraper."
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"open-linkedin-api>=2.3.1",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
[dependency-groups]
|
|
11
|
+
dev = [
|
|
12
|
+
"pytest>=8.4.1",
|
|
13
|
+
"pytest-xdist>=3.8.0",
|
|
14
|
+
"pyright>=1.1.402",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[tool.pytest.ini_options]
|
|
18
|
+
addopts = "-n auto"
|
|
19
|
+
testpaths = ["tests"]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
warmpath = "warmpath.cli:main"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["hatchling"]
|
|
26
|
+
build-backend = "hatchling.build"
|