warmpath 0.3.4__tar.gz → 0.3.5__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.4 → warmpath-0.3.5}/PKG-INFO +1 -1
- {warmpath-0.3.4 → warmpath-0.3.5}/README.md +3 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/pyproject.toml +1 -1
- {warmpath-0.3.4 → warmpath-0.3.5}/tests/test_mutual_connections.py +97 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/uv.lock +1 -1
- {warmpath-0.3.4 → warmpath-0.3.5}/warmpath/cli.py +20 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/.gitignore +0 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/AGENTS.md +0 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/Taskfile.yaml +0 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/UNLICENSE +0 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/docs/skill.md +0 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/scripts/release.sh +0 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/tests/test_auth.py +0 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/tests/test_auth_http.py +0 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/tests/test_browser_http.py +0 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/tests/test_cli_commands.py +0 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/tests/test_companies.py +0 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/warmpath/__init__.py +0 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/warmpath/auth.py +0 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/warmpath/auth_http.py +0 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/warmpath/browser_http.py +0 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/warmpath/companies.py +0 -0
- {warmpath-0.3.4 → warmpath-0.3.5}/warmpath.jpg +0 -0
|
@@ -36,6 +36,9 @@ uvx warmpath human https://www.linkedin.com/in/mitchellh/ --refresh-cache
|
|
|
36
36
|
uvx warmpath company --help
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
`company` prints mutual contacts for second-degree candidates, looking them up
|
|
40
|
+
when LinkedIn's company search does not include their names.
|
|
41
|
+
|
|
39
42
|
`companies` walks the complete 1st-degree connections list, looks up current
|
|
40
43
|
employers, and deduplicates by company ID. It includes multiple current employers
|
|
41
44
|
and keeps unlinked employer names; `--urls` prints only known LinkedIn company
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
from unittest.mock import Mock
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from warmpath import cli
|
|
1
6
|
from warmpath.cli import (
|
|
2
7
|
candidate_matches_filter,
|
|
3
8
|
company_path_candidate,
|
|
@@ -106,6 +111,98 @@ def test_second_degree_candidate_renders_visible_mutual_connections() -> None:
|
|
|
106
111
|
assert "unknown introducer" not in rendered
|
|
107
112
|
|
|
108
113
|
|
|
114
|
+
@pytest.mark.parametrize("mutual_count", [None, 3])
|
|
115
|
+
@pytest.mark.parametrize("found_mutual", [True, False])
|
|
116
|
+
def test_company_looks_up_missing_mutual_contacts(
|
|
117
|
+
tmp_path, monkeypatch, mutual_count, found_mutual
|
|
118
|
+
) -> None:
|
|
119
|
+
monkeypatch.setattr(cli, "resolve_company", Mock(return_value=(
|
|
120
|
+
{"name": "Acme", "urn_id": "company-id"}, []
|
|
121
|
+
)))
|
|
122
|
+
monkeypatch.setattr(cli, "fetch_company_people", Mock(side_effect=lambda *args: (
|
|
123
|
+
[] if args[3] == 1 else [{
|
|
124
|
+
"name": "Employee",
|
|
125
|
+
"urn_id": "employee-id",
|
|
126
|
+
"distance": "DISTANCE_2",
|
|
127
|
+
"mutual_count": mutual_count,
|
|
128
|
+
"mutuals_truncated": True,
|
|
129
|
+
"_search_source": "search.current_company",
|
|
130
|
+
}]
|
|
131
|
+
)))
|
|
132
|
+
api = Mock()
|
|
133
|
+
api.search.return_value = [{
|
|
134
|
+
"entityUrn": "urn:li:fsd_profile:contact-id",
|
|
135
|
+
"title": {"text": "Ada Lovelace"},
|
|
136
|
+
"navigationUrl": "https://www.linkedin.com/in/ada-lovelace/",
|
|
137
|
+
"entityCustomTrackingInfo": {"memberDistance": "DISTANCE_1"},
|
|
138
|
+
}] if found_mutual else []
|
|
139
|
+
|
|
140
|
+
result = cli.find_company_path_candidates(
|
|
141
|
+
api, "Acme", 2, 5, None, 5, tmp_path, False
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
api.search.assert_called_once_with({
|
|
145
|
+
"filters": (
|
|
146
|
+
"List((key:resultType,value:List(PEOPLE)),"
|
|
147
|
+
"(key:connectionOf,value:List(employee-id)),"
|
|
148
|
+
"(key:network,value:List(F)))"
|
|
149
|
+
)
|
|
150
|
+
}, limit=mutual_count or cli.DEFAULT_MAX_MUTUAL_CONNECTIONS)
|
|
151
|
+
candidate = result["candidates"][0]
|
|
152
|
+
rendered = render_company_path_result(result)
|
|
153
|
+
assert "Employee" in rendered
|
|
154
|
+
assert candidate["evidence"]["source"] == "search.current_company"
|
|
155
|
+
if found_mutual:
|
|
156
|
+
expected = "Mutuals (3): Ada Lovelace, +2 more" if mutual_count else "Mutuals (1): Ada Lovelace"
|
|
157
|
+
assert expected in rendered
|
|
158
|
+
assert candidate["path_status"] == "partially_resolved"
|
|
159
|
+
assert candidate["path"][1]["url"] == "https://www.linkedin.com/in/ada-lovelace/"
|
|
160
|
+
else:
|
|
161
|
+
assert "Mutuals" not in rendered
|
|
162
|
+
assert candidate["path_status"] == "unresolved"
|
|
163
|
+
assert candidate["mutual_count"] == mutual_count
|
|
164
|
+
assert candidate["mutuals_truncated"] is True
|
|
165
|
+
|
|
166
|
+
# Repeating a company search reuses its mutual-contact lookup.
|
|
167
|
+
assert cli.find_company_path_candidates(
|
|
168
|
+
api, "Acme", 2, 5, None, 5, tmp_path, False
|
|
169
|
+
) == result
|
|
170
|
+
api.search.assert_called_once()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_company_skips_mutual_lookups_for_direct_known_and_unprinted_candidates(
|
|
174
|
+
tmp_path, monkeypatch
|
|
175
|
+
) -> None:
|
|
176
|
+
monkeypatch.setattr(cli, "resolve_company", Mock(return_value=(
|
|
177
|
+
{"name": "Acme", "urn_id": "company-id"}, []
|
|
178
|
+
)))
|
|
179
|
+
direct = {"name": "Direct", "urn_id": "direct-id", "jobtitle": "Engineer"}
|
|
180
|
+
second_degree = [
|
|
181
|
+
{"name": "Filtered out", "urn_id": "filtered-id", "jobtitle": "Recruiter"},
|
|
182
|
+
{
|
|
183
|
+
"name": "Known mutual",
|
|
184
|
+
"urn_id": "known-id",
|
|
185
|
+
"jobtitle": "Engineer",
|
|
186
|
+
"mutual_connections": [{"name": "Ada Lovelace"}],
|
|
187
|
+
},
|
|
188
|
+
{"name": "Over limit", "urn_id": "limited-id", "jobtitle": "Engineer"},
|
|
189
|
+
]
|
|
190
|
+
monkeypatch.setattr(cli, "fetch_company_people", Mock(
|
|
191
|
+
side_effect=lambda *args: [direct] if args[3] == 1 else second_degree
|
|
192
|
+
))
|
|
193
|
+
api = Mock()
|
|
194
|
+
|
|
195
|
+
result = cli.find_company_path_candidates(
|
|
196
|
+
api, "Acme", 2, 2, "Engineer", 5, tmp_path, False
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
assert [candidate["target"]["name"] for candidate in result["candidates"]] == [
|
|
200
|
+
"Direct", "Known mutual"
|
|
201
|
+
]
|
|
202
|
+
assert "Mutuals (1): Ada Lovelace" in render_company_path_result(result)
|
|
203
|
+
api.search.assert_not_called()
|
|
204
|
+
|
|
205
|
+
|
|
109
206
|
def test_second_degree_candidate_without_mutuals_keeps_unresolved_status() -> None:
|
|
110
207
|
row = {
|
|
111
208
|
"name": "Ruslan Gilemzianov",
|
|
@@ -682,6 +682,9 @@ def company_path_candidate(row: dict[str, Any], degree: int) -> dict[str, Any]:
|
|
|
682
682
|
"degree": degree,
|
|
683
683
|
"path_status": "unresolved",
|
|
684
684
|
"target": target,
|
|
685
|
+
"mutual_connections": mutual_connections,
|
|
686
|
+
"mutual_count": mutual_count,
|
|
687
|
+
"mutuals_truncated": mutuals_truncated,
|
|
685
688
|
"path": [
|
|
686
689
|
{"role": "me"},
|
|
687
690
|
{"role": "unknown_introducer"},
|
|
@@ -1190,6 +1193,23 @@ def find_company_path_candidates(
|
|
|
1190
1193
|
|
|
1191
1194
|
candidates.sort(key=candidate_score)
|
|
1192
1195
|
candidates = candidates[:limit]
|
|
1196
|
+
for index, candidate in enumerate(candidates):
|
|
1197
|
+
if candidate["degree"] != 2 or candidate_mutual_names(candidate):
|
|
1198
|
+
continue
|
|
1199
|
+
row = {
|
|
1200
|
+
**candidate["target"],
|
|
1201
|
+
"mutual_count": candidate.get("mutual_count"),
|
|
1202
|
+
"mutuals_truncated": candidate.get("mutuals_truncated", False),
|
|
1203
|
+
"_search_source": candidate["evidence"]["source"],
|
|
1204
|
+
}
|
|
1205
|
+
row = enrich_row_with_mutual_connections(
|
|
1206
|
+
api,
|
|
1207
|
+
row,
|
|
1208
|
+
cache_dir,
|
|
1209
|
+
refresh_cache,
|
|
1210
|
+
)
|
|
1211
|
+
candidates[index] = company_path_candidate(row, 2)
|
|
1212
|
+
|
|
1193
1213
|
direct_count = sum(1 for candidate in candidates if candidate["degree"] == 1)
|
|
1194
1214
|
second_count = sum(1 for candidate in candidates if candidate["degree"] == 2)
|
|
1195
1215
|
resolved_count = sum(
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|