You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

95 lines
3.7 KiB
Python

"""NATIVE HEALTH adapter — SmartRecruiters career page, Show More AJAX, keyword filter."""
from __future__ import annotations
from loguru import logger
from gimme_job.adapters.base import ManifestDrivenAdapter
from gimme_job.adapters.registry import register
from gimme_job.models.dto import JobPostingCandidate, RawJobCard
_MAX_SHOW_MORE = 10 # max "Show more jobs" clicks to prevent infinite loops
_KEYWORDS = ["orthodontist", "orthodontic", "dentist"]
@register("nativehealth")
class NativeHealthAdapter(ManifestDrivenAdapter):
def collect_cards(self, page, manifest) -> list[RawJobCard]:
"""Click all 'Show more jobs' links, then extract and keyword-filter all jobs."""
try:
page.wait_for_selector(
"a[href*='smartrecruiters.com/NATIVEHEALTH/']",
timeout=15000,
state="attached",
)
except Exception:
logger.debug("[nativehealth] Initial jobs wait timed out")
# Click "Show more jobs" links until none remain (AJAX loads more into same page)
for i in range(_MAX_SHOW_MORE):
show_more = page.query_selector("a:text('Show more jobs')")
if not show_more:
break
prev_count = len(
page.query_selector_all("a[href*='smartrecruiters.com/NATIVEHEALTH/']")
)
show_more.click()
try:
page.wait_for_function(
f"document.querySelectorAll(\"a[href*='smartrecruiters.com/NATIVEHEALTH/']\").length > {prev_count}",
timeout=10000,
)
logger.debug(f"[nativehealth] Show more {i + 1}: loaded more jobs")
except Exception:
logger.debug(f"[nativehealth] Show more {i + 1}: no new jobs appeared")
break
# Extract all jobs via JS, getting location from h3 heading of each group
raw_data = page.evaluate("""() => {
const links = document.querySelectorAll("a[href*='smartrecruiters.com/NATIVEHEALTH/']");
return Array.from(links).map(a => {
// Walk up: a → li → ul (job list) → location group div → h3
const locationDiv = a.closest('li')?.parentElement?.parentElement;
const location = locationDiv?.querySelector('h3')?.textContent?.trim() || null;
return {
title: (a.querySelector('h4')?.textContent || '').trim(),
employment: (a.querySelector('p')?.textContent || '').trim(),
url: a.href,
location
};
}).filter(j => j.title);
}""")
cards = []
for item in raw_data:
if not self._matches_keywords(item["title"]):
continue
cards.append(RawJobCard(
title=item["title"],
company="NATIVE HEALTH",
location=item["location"] or None,
employment_type=item["employment"] or None,
url=item["url"] or None,
))
logger.info(
f"[nativehealth] Keyword filter: {len(cards)}/{len(raw_data)} cards matched"
)
return cards
def paginate(self, page, page_index: int, config) -> bool:
"""Single page after Show More clicks — no further pagination."""
return False
def normalize(self, raw: RawJobCard) -> JobPostingCandidate:
return super().normalize(raw)
@staticmethod
def _matches_keywords(title: str | None) -> bool:
if not title:
return False
title_lower = title.lower()
return any(kw in title_lower for kw in _KEYWORDS)