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.
80 lines
2.7 KiB
Python
80 lines
2.7 KiB
Python
"""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)
|