"""Indian Health Service Dentistry adapter — single-page table, keyword post-filter.""" from __future__ import annotations import re 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 _BASE_URL = "https://www.ihs.gov" _KEYWORDS = ["orthodontist", "orthodontic", "dentist"] @register("ihs") class IHSAdapter(ManifestDrivenAdapter): def collect_cards(self, page, manifest): """Extract all rows via JS, combine city+state into location, filter by keywords.""" try: page.wait_for_selector("table.cf_grid tbody tr", timeout=15000, state="attached") except Exception: logger.debug("[ihs] Table wait timed out") raw_data = page.evaluate("""() => { const rows = document.querySelectorAll('table.cf_grid tbody tr'); return Array.from(rows).map(row => { const titleEl = row.querySelector('td:nth-child(2) a'); const city = (row.querySelector('td:nth-child(3)')?.textContent || '').trim(); const state = (row.querySelector('td:nth-child(4)')?.textContent || '').trim(); return { title: (titleEl?.textContent || '').trim(), url: titleEl?.getAttribute('href') || '', city: city, state: state }; }).filter(r => r.title); }""") cards = [] for item in raw_data: title = item["title"] if not self._matches_keywords(title): continue # Strip HPSA score prefix: "(N-HPSA Score) CityName" → "CityName" city = re.sub(r"^\([^)]+\)\s*", "", item["city"]) location = f"{city}, {item['state']}".strip(", ") url = item["url"] if url.startswith("/"): url = _BASE_URL + url cards.append(RawJobCard( title=title, company="Indian Health Service", location=location or None, url=url or None, )) logger.info( f"[ihs] Keyword filter: {len(cards)}/{len(raw_data)} cards matched" ) return cards def paginate(self, page, page_index: int, config) -> bool: """Single page — no 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)