Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions api/app/job_searcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,16 @@ async def _search_level1_playwright(keywords: list[str]) -> list[dict]:
# moved from Lever (October 2026)
("scaleai", "Scale AI"),
("togetherai", "Together AI"),
# moved from Workable (October 2026)
("riverlane", "Riverlane"),
# graduate employers' careers sites (October 2026)
("mavensecuritiesholdingltd", "Maven Securities"),
("neptunenorth", "Neptune North"),
("netcraft", "Netcraft"),
("radixuniversity", "Radix Trading"),
("mthree", "mthree"),
("mako", "Mako Trading"),
("gsacapital", "GSA Capital"),
]

_ASHBY_COMPANIES: list[tuple[str, str]] = [
Expand Down Expand Up @@ -725,21 +735,29 @@ async def _search_level1_playwright(keywords: list[str]) -> list[dict]:
("improbable", "Improbable"),
("temporal", "Temporal"),
("runpod", "RunPod"),
# moved from Lever (October 2026)
("mistral.ai", "Mistral AI"),
# graduate employers' careers sites (October 2026)
("checkout.com", "Checkout.com"),
]

_LEVER_COMPANIES: list[tuple[str, str]] = [
("palantir", "Palantir"),
("mistral", "Mistral AI"),
# added from grad-programmes.json
("ion", "ION"),
# added from summer-internships.json
("spotify", "Spotify"),
("gearset", "Gearset"),
]

# Lever's EU instance (jobs.eu.lever.co): these boards are only on its own API.
_LEVER_EU_COMPANIES: list[tuple[str, str]] = [
# graduate employers' careers sites (October 2026)
("coretechsecurity", "CoreTech Security"),
]

_WORKABLE_COMPANIES: list[tuple[str, str]] = [
("starling-bank", "Starling Bank"),
("riverlane", "Riverlane"),
("capula-investment-management-ltd", "Capula Investment Management"),
("longshot-systems-ltd", "Longshot Systems"),
("insight-investment", "Insight Investment"),
Expand All @@ -750,6 +768,8 @@ async def _search_level1_playwright(keywords: list[str]) -> list[dict]:
("caxton", "Caxton Associates"),
# added from slug validation
("two-sigma", "Two Sigma"),
# graduate employers' careers sites (October 2026)
("grayce", "Grayce"),
]

_SMARTRECRUITERS_COMPANIES: list[tuple[str, str]] = [
Expand All @@ -758,6 +778,9 @@ async def _search_level1_playwright(keywords: list[str]) -> list[dict]:
("TTP1", "TTP"),
("Vitol", "Vitol"),
("LegalAndGeneral", "Legal & General"),
# graduate employers' careers sites (October 2026)
("AECOM2", "AECOM"),
("DataIntellect", "Data Intellect"),
]

async def _fetch_greenhouse(slug: str, company: str, keywords: list[str]) -> list[dict]:
Expand Down Expand Up @@ -826,9 +849,10 @@ async def _fetch_ashby(slug: str, company: str, keywords: list[str]) -> list[dic
return []


async def _fetch_lever(slug: str, company: str, keywords: list[str]) -> list[dict]:
async def _fetch_lever(slug: str, company: str, keywords: list[str], eu: bool = False) -> list[dict]:
"""A Lever board; `eu` for one on Lever's EU instance (jobs.eu.lever.co), whose postings only its own API has."""
try:
url = f"https://api.lever.co/v0/postings/{slug}?mode=json"
url = f"https://api.{'eu.' if eu else ''}lever.co/v0/postings/{slug}?mode=json"
async with http_client.async_client(timeout=8.0) as client:
resp = await client.get(url)
resp.raise_for_status()
Expand Down Expand Up @@ -947,7 +971,8 @@ async def _fetch_smartrecruiters(slug: str, company: str, keywords: list[str]) -
def listed_boards() -> list[tuple[str, str, str]]:
"""(ats, slug, company) of every board listed in this module."""
lists = {"greenhouse": _GREENHOUSE_COMPANIES, "ashby": _ASHBY_COMPANIES, "lever": _LEVER_COMPANIES,
"workable": _WORKABLE_COMPANIES, "smartrecruiters": _SMARTRECRUITERS_COMPANIES}
"lever_eu": _LEVER_EU_COMPANIES, "workable": _WORKABLE_COMPANIES,
"smartrecruiters": _SMARTRECRUITERS_COMPANIES}
return [(ats, slug, company) for ats, listed in lists.items() for slug, company in listed]


Expand Down Expand Up @@ -982,8 +1007,11 @@ async def _search_level2_ats_apis(queries: list[str], boards: dict[str, list[tup
if not keywords:
return []

async def fetch_lever_eu(slug: str, company: str, keywords: list[str]) -> list[dict]:
return await _fetch_lever(slug, company, keywords, eu=True)

fetchers = {"greenhouse": _fetch_greenhouse, "ashby": _fetch_ashby, "lever": _fetch_lever,
"workable": _fetch_workable, "smartrecruiters": _fetch_smartrecruiters}
"lever_eu": fetch_lever_eu, "workable": _fetch_workable, "smartrecruiters": _fetch_smartrecruiters}
every = _all_boards() if boards is None else boards
tasks = [fetchers[ats](slug, name, keywords) for ats, listed in every.items() if ats in fetchers
for slug, name in listed]
Expand All @@ -1009,6 +1037,7 @@ async def _search_level2_ats_apis(queries: list[str], boards: dict[str, list[tup
"greenhouse": "https://boards-api.greenhouse.io/v1/boards/{slug}/jobs",
"ashby": "https://api.ashbyhq.com/posting-api/job-board/{slug}",
"lever": "https://api.lever.co/v0/postings/{slug}?mode=json",
"lever_eu": "https://api.eu.lever.co/v0/postings/{slug}?mode=json",
"workable": "https://apply.workable.com/api/v1/widget/accounts/{slug}",
"smartrecruiters": "https://api.smartrecruiters.com/v1/companies/{slug}/postings?limit=100",
}
Expand All @@ -1026,7 +1055,7 @@ def _board_slugs(company: str) -> list[str]:


def _board_jobs(ats: str, data) -> list[dict]:
if ats == "lever":
if ats in ("lever", "lever_eu"):
return data if isinstance(data, list) else []
if ats == "smartrecruiters":
return data.get("content") or []
Expand Down
27 changes: 27 additions & 0 deletions api/tests/test_job_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,33 @@ async def get(self, url, params=None):
return MagicMock(async_client=FakeClient)


def test_a_board_on_levers_eu_instance_is_fetched_from_its_own_api(monkeypatch):
# CoreTech Security's board is on jobs.eu.lever.co; api.lever.co doesn't have its postings.
asked: list[str] = []

class Client:
def __init__(self, *a, **k): pass
async def __aenter__(self): return self
async def __aexit__(self, *a): pass
async def get(self, url, params=None):
asked.append(url)
return httpx.Response(200, json=[{"id": "1", "text": "Graduate Software Engineer",
"categories": {"location": "London"},
"hostedUrl": "https://jobs.eu.lever.co/x/1"}],
request=httpx.Request("GET", url))
monkeypatch.setattr(job_searcher, "http_client", MagicMock(async_client=Client))
jobs = asyncio.run(job_searcher._search_level2_ats_apis(["software"], boards={"lever_eu": [("x", "X")]}))
assert asked == ["https://api.eu.lever.co/v0/postings/x?mode=json"]
assert [(j["company"], j["url"]) for j in jobs] == [("X", "https://jobs.eu.lever.co/x/1")]
web = _Web({"https://api.eu.lever.co/v0/postings/x?mode=json": (200, [{}, {}, {}])})
assert asyncio.run(job_searcher.check_board(web, "lever_eu", "x")) == 3


def test_no_board_is_listed_twice():
listed = [(ats, slug.lower()) for ats, slug, _ in job_searcher.listed_boards()]
assert len(listed) == len(set(listed))


def test_ashby_boards_with_a_string_location_are_read(monkeypatch):
# The posting API gives `location` as a string; reading it as a dict raised, and every Ashby board
# came back empty.
Expand Down
Loading