"""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)