返回 last30days-skill
jobs.py
根目录 / skills / last30days / scripts / lib / jobs.py
1 """Public jobs/careers retrieval for Hiring Signals.
2
3 Tiered strategy (each tier degrades gracefully; the artifact records which
4 tier produced results so synthesis knows the confidence level):
5
6 - Tier 1 - direct ATS API. The company's own board is authoritative and
7 structured. Discovery is careers-page-first: fetch the careers page and read
8 the provider + exact slug straight off the embed/link, then call that API.
9 We only emit ATS results when a call actually returns a non-empty board, so a
10 bad slug guess never produces fabricated coverage. Slug-probing is a fallback,
11 never the entry point.
12 - Tier 2 - careers page found, no supported ATS API. Parse schema.org
13 ``JobPosting`` JSON-LD (emitted by most careers pages for Google Jobs SEO,
14 regardless of ATS).
15 - Tier 3 - generic web search. Last resort, noisy, clearly low-confidence.
16 """
17
18 from __future__ import annotations
19
20 import json
21 import re
22 from html import unescape
23 from typing import Any, Callable
24 from urllib.parse import urlparse
25
26 from . import dates, grounding, http
27
28
29 ATS_PROVIDER_GREENHOUSE = "greenhouse"
30 ATS_PROVIDER_ASHBY = "ashby"
31 ATS_PROVIDER_LEVER = "lever"
32 ATS_PROVIDER_WORKABLE = "workable"
33 ATS_PROVIDER_SMARTRECRUITERS = "smartrecruiters"
34
35 # Detection patterns: map an ATS embed/link found on a careers page to
36 # (provider, slug). The published slug is authoritative - this is why discovery
37 # is careers-page-first rather than blind probing.
38 _ATS_LINK_PATTERNS: list[tuple[str, str]] = [
39 (ATS_PROVIDER_ASHBY, r"(?:jobs|api)\.ashbyhq\.com/(?:posting-api/job-board/)?([A-Za-z0-9_.-]+)"),
40 (ATS_PROVIDER_GREENHOUSE, r"boards(?:-api)?\.greenhouse\.io/(?:v1/boards/|embed/job_board\?for=)?([A-Za-z0-9_.-]+)"),
41 (ATS_PROVIDER_GREENHOUSE, r"job-boards\.greenhouse\.io/([A-Za-z0-9_.-]+)"),
42 (ATS_PROVIDER_GREENHOUSE, r"greenhouse\.io/embed/job_board\?for=([A-Za-z0-9_.-]+)"),
43 (ATS_PROVIDER_LEVER, r"(?:jobs\.lever\.co|api\.lever\.co/v0/postings)/([A-Za-z0-9_.-]+)"),
44 (ATS_PROVIDER_WORKABLE, r"apply\.workable\.com/(?:api/v[0-9]+/accounts/)?([A-Za-z0-9_.-]+)"),
45 (ATS_PROVIDER_WORKABLE, r"([A-Za-z0-9_-]+)\.workable\.com"),
46 (ATS_PROVIDER_SMARTRECRUITERS, r"(?:careers|jobs)\.smartrecruiters\.com/([A-Za-z0-9_.-]+)"),
47 (ATS_PROVIDER_SMARTRECRUITERS, r"api\.smartrecruiters\.com/v1/companies/([A-Za-z0-9_.-]+)"),
48 ]
49
50 # Tokens that show up in ATS URLs but are never real board slugs.
51 _SLUG_STOPWORDS = {"embed", "job_board", "v1", "v0", "api", "posting-api", "boards", "jobs", "job-boards", "www"}
52
53
54 def search_jobs(
55 company: str,
56 date_range: tuple[str, str],
57 config: dict[str, Any],
58 *,
59 depth: str = "default",
60 web_backend: str = "auto",
61 explicit: bool = False,
62 ) -> tuple[list[dict[str, Any]], dict[str, Any]]:
63 """Fetch public job postings for a company via the tiered strategy."""
64 company = company.strip()
65 if not company:
66 return [], {}
67
68 attempted: list[str] = []
69
70 # --- Discovery: fetch the careers page and read the ATS off it (Tier 1). ---
71 careers_html, careers_url = _resolve_careers_page(
72 company, date_range, config, backend=web_backend,
73 )
74 provider, slug = (None, None)
75 if careers_html:
76 provider, slug = detect_ats(careers_html)
77 if provider:
78 attempted.append(f"careers:{provider}:{slug}")
79
80 # Fallback discovery: cheap deterministic slug probe (only after careers-page
81 # discovery fails - never the entry point).
82 if not provider:
83 provider, slug, probe_attempts = _probe_ats(company)
84 attempted.extend(probe_attempts)
85
86 # --- Tier 1: call the resolved ATS API. Trust it only if it has jobs. ---
87 if provider and slug:
88 try:
89 items = _fetch_ats(provider, slug)
90 except http.HTTPError:
91 items = []
92 if items:
93 return items, _artifact("jobs", company, attempted, items, explicit,
94 tier="ats", provider=provider, slug=slug)
95
96 # --- Tier 2: parse JSON-LD JobPosting off the careers page. ---
97 if careers_html:
98 attempted.append("careers:jsonld")
99 jsonld_items = extract_jsonld_jobs(careers_html, careers_url or "")
100 if jsonld_items:
101 return jsonld_items, _artifact("jobs", company, attempted, jsonld_items,
102 explicit, tier="careers-jsonld")
103
104 # --- Tier 3: generic web search (noisy, low-confidence). ---
105 fallback_items, artifact = search_jobs_web(
106 company, date_range, config, backend=web_backend,
107 )
108 artifact = dict(artifact or {})
109 artifact.setdefault("attempted", attempted)
110 artifact.update({
111 "label": artifact.get("label", "jobs"),
112 "company": company,
113 "tier": "web",
114 "explicit": explicit,
115 "resultCount": len(fallback_items),
116 })
117 return fallback_items, artifact
118
119
120 # --------------------------------------------------------------------------- #
121 # Careers-page discovery
122 # --------------------------------------------------------------------------- #
123
124 def _resolve_careers_page(
125 company: str,
126 date_range: tuple[str, str],
127 config: dict[str, Any],
128 *,
129 backend: str = "auto",
130 ) -> tuple[str | None, str | None]:
131 """Find and fetch the company's careers page HTML.
132
133 Tries conventional URLs on a guessed domain first (free, deterministic);
134 falls back to a single web search for the careers page when a backend is
135 configured. Returns (html, url) or (None, None). Never raises.
136 """
137 slug = _company_slug(company)
138 candidates: list[str] = []
139 if slug:
140 for host in (f"{slug}.com", f"{slug}.ai", f"{slug}.io"):
141 candidates.extend([f"https://{host}/careers", f"https://{host}/jobs"])
142
143 for url in candidates:
144 with http.expected_misses(403, 404):
145 html = http.get_text(url, accept="text/html", retries=1)
146 if html and _looks_like_careers_html(html):
147 return html, url
148
149 if backend != "none":
150 careers_url = _search_for_careers_url(company, date_range, config, backend=backend)
151 if careers_url:
152 html = http.get_text(careers_url, accept="text/html", retries=1)
153 if html:
154 return html, careers_url
155
156 return None, None
157
158
159 def _search_for_careers_url(
160 company: str,
161 date_range: tuple[str, str],
162 config: dict[str, Any],
163 *,
164 backend: str = "auto",
165 ) -> str | None:
166 """Use the configured web backend to locate the careers page URL."""
167 try:
168 raw_items, _ = grounding.web_search(
169 f"{company} careers jobs", date_range, config, backend=backend,
170 )
171 except Exception:
172 return None
173 for raw in raw_items or []:
174 if not isinstance(raw, dict):
175 continue
176 url = str(raw.get("url") or "").strip()
177 if not url:
178 continue
179 lowered = url.lower()
180 if any(token in lowered for token in (
181 "career", "/jobs", "ashbyhq", "greenhouse", "lever.co", "workable", "smartrecruiters",
182 )):
183 return url
184 return None
185
186
187 def detect_ats(html: str) -> tuple[str | None, str | None]:
188 """Read the ATS provider + slug off a careers page's embed/links."""
189 if not html:
190 return None, None
191 for provider, pattern in _ATS_LINK_PATTERNS:
192 for match in re.finditer(pattern, html):
193 slug = match.group(1).strip().strip("/.")
194 if slug and slug.lower() not in _SLUG_STOPWORDS:
195 return provider, slug
196 return None, None
197
198
199 def _probe_ats(company: str) -> tuple[str | None, str | None, list[str]]:
200 """Fallback: probe candidate slugs against ATS APIs (deterministic).
201
202 Only reached when careers-page discovery fails. Returns the first provider
203 whose API returns a non-empty board.
204 """
205 attempts: list[str] = []
206 # Workable/SmartRecruiters are omitted here: their slugs rarely match the
207 # company name, so blind probing is unreliable - they're reached only via
208 # careers-page discovery, where the real slug is published.
209 for slug in _candidate_slugs(company):
210 for provider in (ATS_PROVIDER_GREENHOUSE, ATS_PROVIDER_ASHBY, ATS_PROVIDER_LEVER):
211 attempts.append(f"probe:{provider}:{slug}")
212 try:
213 with http.expected_misses(400, 401, 403, 404):
214 items = _fetch_ats(provider, slug)
215 except http.HTTPError as exc:
216 if exc.status_code in {400, 401, 403, 404}:
217 continue
218 raise
219 if items:
220 return provider, slug, attempts
221 return None, None, attempts
222
223
224 # --------------------------------------------------------------------------- #
225 # Tier 1: ATS API fetchers + parsers
226 # --------------------------------------------------------------------------- #
227
228 def _fetch_ats(provider: str, slug: str) -> list[dict[str, Any]]:
229 fetchers: dict[str, Callable[[str], list[dict[str, Any]]]] = {
230 ATS_PROVIDER_GREENHOUSE: search_greenhouse_board,
231 ATS_PROVIDER_ASHBY: search_ashby_board,
232 ATS_PROVIDER_LEVER: search_lever_board,
233 ATS_PROVIDER_WORKABLE: search_workable_board,
234 ATS_PROVIDER_SMARTRECRUITERS: search_smartrecruiters_board,
235 }
236 fetcher = fetchers.get(provider)
237 return fetcher(slug) if fetcher else []
238
239
240 def search_greenhouse_board(board_token: str) -> list[dict[str, Any]]:
241 """Return jobs from Greenhouse's public Job Board API."""
242 url = f"https://boards-api.greenhouse.io/v1/boards/{board_token}/jobs"
243 data = http.get(url, params={"content": "true"}, timeout=15, retries=2)
244 jobs = data.get("jobs") if isinstance(data, dict) else []
245 if not isinstance(jobs, list):
246 return []
247 return [_greenhouse_job_to_item(job, board_token) for job in jobs if isinstance(job, dict)]
248
249
250 def search_ashby_board(slug: str) -> list[dict[str, Any]]:
251 """Return jobs from Ashby's public posting API (needs a browser UA)."""
252 url = f"https://api.ashbyhq.com/posting-api/job-board/{slug}"
253 data = http.get(
254 url,
255 params={"includeCompensation": "true"},
256 headers={"User-Agent": http.BROWSER_USER_AGENT},
257 timeout=15,
258 retries=2,
259 )
260 return parse_ashby_response(data, slug)
261
262
263 def search_lever_board(slug: str) -> list[dict[str, Any]]:
264 """Return jobs from Lever's public postings API (returns a JSON list)."""
265 url = f"https://api.lever.co/v0/postings/{slug}"
266 data = http.get(url, params={"mode": "json"}, timeout=15, retries=2)
267 return parse_lever_response(data, slug)
268
269
270 def search_workable_board(slug: str) -> list[dict[str, Any]]:
271 """Return jobs from Workable's public widget API."""
272 url = f"https://apply.workable.com/api/v3/accounts/{slug}/jobs"
273 data = http.post(url, json_data={}, timeout=15, retries=2)
274 return parse_workable_response(data, slug)
275
276
277 def search_smartrecruiters_board(slug: str) -> list[dict[str, Any]]:
278 """Return jobs from SmartRecruiters' public postings API."""
279 url = f"https://api.smartrecruiters.com/v1/companies/{slug}/postings"
280 data = http.get(url, params={"limit": "100"}, timeout=15, retries=2)
281 return parse_smartrecruiters_response(data, slug)
282
283
284 def parse_greenhouse_response(payload: dict[str, Any], board_token: str = "") -> list[dict[str, Any]]:
285 """Parse a Greenhouse jobs payload for tests and callers with cached data."""
286 jobs = payload.get("jobs") if isinstance(payload, dict) else []
287 if not isinstance(jobs, list):
288 return []
289 return [_greenhouse_job_to_item(job, board_token) for job in jobs if isinstance(job, dict)]
290
291
292 def parse_ashby_response(payload: dict[str, Any], slug: str = "") -> list[dict[str, Any]]:
293 jobs = payload.get("jobs") if isinstance(payload, dict) else []
294 if not isinstance(jobs, list):
295 return []
296 items: list[dict[str, Any]] = []
297 for job in jobs:
298 if not isinstance(job, dict):
299 continue
300 department = str(job.get("departmentName") or job.get("teamName") or "").strip()
301 location = str(job.get("locationName") or job.get("location") or "").strip()
302 description = _clean_html(str(job.get("descriptionHtml") or job.get("descriptionPlain") or ""))
303 items.append(_ats_item(
304 provider=ATS_PROVIDER_ASHBY,
305 slug=slug,
306 ident=str(job.get("id") or job.get("jobId") or ""),
307 title=str(job.get("title") or "").strip(),
308 url=str(job.get("jobUrl") or job.get("applyUrl") or "").strip(),
309 description=description,
310 date=_date_part(job.get("publishedDate") or job.get("publishedAt")),
311 department=department,
312 location=location,
313 ))
314 return items
315
316
317 def parse_lever_response(payload: Any, slug: str = "") -> list[dict[str, Any]]:
318 postings = payload if isinstance(payload, list) else []
319 items: list[dict[str, Any]] = []
320 for job in postings:
321 if not isinstance(job, dict):
322 continue
323 categories = job.get("categories") if isinstance(job.get("categories"), dict) else {}
324 department = str(categories.get("department") or categories.get("team") or "").strip()
325 location = str(categories.get("location") or "").strip()
326 description = _clean_html(str(job.get("descriptionPlain") or job.get("description") or ""))
327 items.append(_ats_item(
328 provider=ATS_PROVIDER_LEVER,
329 slug=slug,
330 ident=str(job.get("id") or ""),
331 title=str(job.get("text") or "").strip(),
332 url=str(job.get("hostedUrl") or job.get("applyUrl") or "").strip(),
333 description=description,
334 date=_epoch_ms_to_date(job.get("createdAt")),
335 department=department,
336 location=location,
337 ))
338 return items
339
340
341 def parse_workable_response(payload: dict[str, Any], slug: str = "") -> list[dict[str, Any]]:
342 results = []
343 if isinstance(payload, dict):
344 results = payload.get("results") or payload.get("jobs") or []
345 if not isinstance(results, list):
346 return []
347 items: list[dict[str, Any]] = []
348 for job in results:
349 if not isinstance(job, dict):
350 continue
351 loc = job.get("location") if isinstance(job.get("location"), dict) else {}
352 location = _join_parts(str(loc.get("city") or ""), str(loc.get("country") or ""))
353 shortcode = str(job.get("shortcode") or "").strip()
354 url = str(job.get("url") or job.get("application_url") or "").strip()
355 if not url and shortcode and slug:
356 url = f"https://apply.workable.com/{slug}/j/{shortcode}/"
357 items.append(_ats_item(
358 provider=ATS_PROVIDER_WORKABLE,
359 slug=slug,
360 ident=shortcode or str(job.get("id") or ""),
361 title=str(job.get("title") or "").strip(),
362 url=url,
363 description=_clean_html(str(job.get("description") or "")),
364 date=_date_part(job.get("published_on") or job.get("created_at")),
365 department=str(job.get("department") or "").strip(),
366 location=location,
367 ))
368 return items
369
370
371 def parse_smartrecruiters_response(payload: dict[str, Any], slug: str = "") -> list[dict[str, Any]]:
372 content = payload.get("content") if isinstance(payload, dict) else []
373 if not isinstance(content, list):
374 return []
375 items: list[dict[str, Any]] = []
376 for job in content:
377 if not isinstance(job, dict):
378 continue
379 department = ""
380 if isinstance(job.get("department"), dict):
381 department = str(job["department"].get("label") or "").strip()
382 location = ""
383 if isinstance(job.get("location"), dict):
384 location = _join_parts(
385 str(job["location"].get("city") or ""),
386 str(job["location"].get("country") or ""),
387 )
388 ident = str(job.get("id") or job.get("uuid") or "").strip()
389 url = ""
390 if isinstance(job.get("ref"), str):
391 url = job["ref"]
392 if not url and slug and ident:
393 url = f"https://jobs.smartrecruiters.com/{slug}/{ident}"
394 items.append(_ats_item(
395 provider=ATS_PROVIDER_SMARTRECRUITERS,
396 slug=slug,
397 ident=ident,
398 title=str(job.get("name") or "").strip(),
399 url=url,
400 description="",
401 date=_date_part(job.get("releasedDate") or job.get("createdOn")),
402 department=department,
403 location=location,
404 ))
405 return items
406
407
408 # --------------------------------------------------------------------------- #
409 # Tier 2: JSON-LD JobPosting crawler
410 # --------------------------------------------------------------------------- #
411
412 _JSONLD_RE = re.compile(
413 r'<script[^>]+type=["\']application/ld\+json["\'][^>]*>(.*?)</script>',
414 re.IGNORECASE | re.DOTALL,
415 )
416
417
418 def extract_jsonld_jobs(html: str, base_url: str = "") -> list[dict[str, Any]]:
419 """Extract schema.org JobPosting objects embedded as JSON-LD in a page."""
420 if not html:
421 return []
422 domain = _domain(base_url)
423 items: list[dict[str, Any]] = []
424 seen: set[str] = set()
425 for block in _JSONLD_RE.findall(html):
426 for obj in _walk_jsonld(block):
427 if not isinstance(obj, dict):
428 continue
429 if not _is_job_posting(obj):
430 continue
431 title = str(obj.get("title") or obj.get("name") or "").strip()
432 if not title or title in seen:
433 continue
434 seen.add(title)
435 posting_url = str(obj.get("url") or "").strip()
436 items.append({
437 "id": f"JL{len(items) + 1}",
438 "title": title,
439 "url": posting_url,
440 "description": _clean_html(str(obj.get("description") or ""))[:2000],
441 "date": _date_part(obj.get("datePosted")),
442 "date_confidence": "high" if obj.get("datePosted") else "low",
443 "provider": "careers-jsonld",
444 "department": str(obj.get("occupationalCategory") or "").strip(),
445 "location": _jsonld_location(obj),
446 "source_url": base_url,
447 "source_domain": domain,
448 "relevance": 0.6,
449 "why_relevant": "JobPosting structured data on careers page",
450 })
451 return items
452
453
454 def _walk_jsonld(block: str) -> list[Any]:
455 try:
456 data = json.loads(block.strip())
457 except (json.JSONDecodeError, ValueError):
458 return []
459 out: list[Any] = []
460 stack = [data]
461 while stack:
462 node = stack.pop()
463 if isinstance(node, list):
464 stack.extend(node)
465 elif isinstance(node, dict):
466 out.append(node)
467 graph = node.get("@graph")
468 if isinstance(graph, list):
469 stack.extend(graph)
470 return out
471
472
473 def _is_job_posting(obj: dict[str, Any]) -> bool:
474 type_field = obj.get("@type")
475 if isinstance(type_field, str):
476 return type_field.lower() == "jobposting"
477 if isinstance(type_field, list):
478 return any(isinstance(t, str) and t.lower() == "jobposting" for t in type_field)
479 return False
480
481
482 def _jsonld_location(obj: dict[str, Any]) -> str:
483 loc = obj.get("jobLocation")
484 if isinstance(loc, list):
485 loc = loc[0] if loc else None
486 if isinstance(loc, dict):
487 address = loc.get("address")
488 if isinstance(address, dict):
489 return _join_parts(
490 str(address.get("addressLocality") or ""),
491 str(address.get("addressRegion") or ""),
492 str(address.get("addressCountry") or ""),
493 )
494 if obj.get("jobLocationType"):
495 return str(obj.get("jobLocationType")).strip()
496 return ""
497
498
499 # --------------------------------------------------------------------------- #
500 # Tier 3: generic web search fallback
501 # --------------------------------------------------------------------------- #
502
503 def search_jobs_web(
504 company: str,
505 date_range: tuple[str, str],
506 config: dict[str, Any],
507 *,
508 backend: str = "auto",
509 ) -> tuple[list[dict[str, Any]], dict[str, Any]]:
510 """Fallback to configured web search for public careers/jobs pages."""
511 if backend == "none":
512 return [], {}
513 query = f'{company} careers jobs hiring'
514 raw_items, artifact = grounding.web_search(query, date_range, config, backend=backend)
515 items: list[dict[str, Any]] = []
516 for index, raw in enumerate(raw_items):
517 if not isinstance(raw, dict):
518 continue
519 title = str(raw.get("title") or "").strip()
520 url = str(raw.get("url") or "").strip()
521 snippet = str(raw.get("snippet") or "").strip()
522 if not _looks_like_jobs_page(title, url, snippet):
523 continue
524 items.append({
525 "id": raw.get("id") or f"JW{index + 1}",
526 "title": title,
527 "url": url,
528 "description": snippet,
529 "date": raw.get("date"),
530 "date_confidence": raw.get("date_confidence") or "low",
531 "provider": "web",
532 "source_domain": raw.get("source_domain"),
533 "relevance": raw.get("relevance", 0.45),
534 "why_relevant": "Public careers/jobs web result",
535 })
536 artifact = dict(artifact or {})
537 artifact.update({"label": "jobs-web", "company": company, "resultCount": len(items)})
538 return items, artifact
539
540
541 # --------------------------------------------------------------------------- #
542 # Item builders + helpers
543 # --------------------------------------------------------------------------- #
544
545 def _ats_item(
546 *,
547 provider: str,
548 slug: str,
549 ident: str,
550 title: str,
551 url: str,
552 description: str,
553 date: str | None,
554 department: str,
555 location: str,
556 ) -> dict[str, Any]:
557 prefix = {
558 ATS_PROVIDER_ASHBY: "AB",
559 ATS_PROVIDER_LEVER: "LV",
560 ATS_PROVIDER_WORKABLE: "WK",
561 ATS_PROVIDER_SMARTRECRUITERS: "SR",
562 }.get(provider, "AT")
563 return {
564 "id": f"{prefix}{ident or slug}",
565 "title": title,
566 "url": url,
567 "description": description,
568 "date": date,
569 "date_confidence": "high" if date else "low",
570 "provider": provider,
571 "board_token": slug,
572 "department": department,
573 "departments": [department] if department else [],
574 "location": location,
575 "offices": [location] if location else [],
576 "relevance": 0.75,
577 "why_relevant": f"Public {provider} job posting",
578 }
579
580
581 def _greenhouse_job_to_item(job: dict[str, Any], board_token: str) -> dict[str, Any]:
582 departments = [
583 str(dept.get("name") or "").strip()
584 for dept in (job.get("departments") or [])
585 if isinstance(dept, dict) and str(dept.get("name") or "").strip()
586 ]
587 offices = [
588 str(office.get("name") or office.get("location") or "").strip()
589 for office in (job.get("offices") or [])
590 if isinstance(office, dict) and str(office.get("name") or office.get("location") or "").strip()
591 ]
592 location = ""
593 if isinstance(job.get("location"), dict):
594 location = str(job["location"].get("name") or "").strip()
595 description = _clean_html(str(job.get("content") or ""))
596 return {
597 "id": f"GH{job.get('id') or job.get('internal_job_id') or board_token}",
598 "title": str(job.get("title") or "").strip(),
599 "url": str(job.get("absolute_url") or "").strip(),
600 "description": description,
601 "date": _date_part(job.get("updated_at")),
602 "date_confidence": "high" if job.get("updated_at") else "low",
603 "provider": ATS_PROVIDER_GREENHOUSE,
604 "board_token": board_token,
605 "department": departments[0] if departments else "",
606 "departments": departments,
607 "location": location,
608 "offices": offices,
609 "relevance": 0.75,
610 "why_relevant": "Public Greenhouse job posting",
611 }
612
613
614 def _artifact(
615 label: str,
616 company: str,
617 attempted: list[str],
618 items: list[dict[str, Any]],
619 explicit: bool,
620 *,
621 tier: str,
622 provider: str = "",
623 slug: str = "",
624 ) -> dict[str, Any]:
625 return {
626 "label": label,
627 "company": company,
628 "attempted": attempted,
629 "resultCount": len(items),
630 "explicit": explicit,
631 "tier": tier,
632 "provider": provider,
633 "board_token": slug,
634 }
635
636
637 def _candidate_slugs(company: str) -> list[str]:
638 base = _company_slug(company)
639 if not base:
640 return []
641 candidates = [base]
642 compact = re.sub(r"(inc|labs|ai|hq|app|tech)$", "", base)
643 if compact and compact != base:
644 candidates.append(compact)
645 # hyphenated form (e.g. "listen labs" -> "listen-labs")
646 hyphen = re.sub(r"[^a-z0-9]+", "-", company.lower()).strip("-")
647 if hyphen and hyphen not in candidates:
648 candidates.append(hyphen)
649 deduped: list[str] = []
650 for token in candidates:
651 if token and token not in deduped:
652 deduped.append(token)
653 return deduped[:4]
654
655
656 def _join_parts(*parts: str) -> str:
657 """Join non-empty, stripped location parts as 'City, Country'."""
658 return ", ".join(part.strip() for part in parts if part and part.strip())
659
660
661 def _company_slug(company: str) -> str:
662 text = company.lower()
663 text = re.sub(r"\b(inc|inc\.|llc|ltd|corp|corporation|company|co\.)\b", "", text)
664 return re.sub(r"[^a-z0-9]+", "", text).strip()
665
666
667 def _domain(url: str) -> str:
668 try:
669 return urlparse(url).netloc.lower().lstrip("www.")
670 except ValueError:
671 return ""
672
673
674 def _date_part(value: Any) -> str | None:
675 text = str(value or "")
676 if not text:
677 return None
678 match = re.search(r"\d{4}-\d{2}-\d{2}", text)
679 return match.group(0) if match else None
680
681
682 def _epoch_ms_to_date(value: Any) -> str | None:
683 try:
684 ms = int(value)
685 except (TypeError, ValueError):
686 return None
687 if ms <= 0:
688 return None
689 return dates.timestamp_to_date(ms / 1000)
690
691
692 def _clean_html(value: str) -> str:
693 value = unescape(value)
694 value = re.sub(r"<br\s*/?>", "\n", value, flags=re.I)
695 value = re.sub(r"</p\s*>", "\n", value, flags=re.I)
696 value = re.sub(r"<[^>]+>", " ", value)
697 value = re.sub(r"\s+", " ", value)
698 return value.strip()
699
700
701 def _looks_like_careers_html(html: str) -> bool:
702 lowered = html.lower()
703 if any(token in lowered for token in ("ashbyhq.com", "greenhouse.io", "lever.co", "workable.com", "smartrecruiters.com")):
704 return True
705 return bool(re.search(r"\b(open roles|open positions|join (our|the) team|current openings|jobposting)\b", lowered))
706
707
708 def _looks_like_jobs_page(title: str, url: str, snippet: str) -> bool:
709 haystack = " ".join([title, url, snippet]).lower()
710 return bool(re.search(r"\b(careers?|jobs?|job openings?|hiring|greenhouse|lever|ashby|workable)\b", haystack))
711
711 lines PYTHON