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.
105 lines
4.0 KiB
Python
105 lines
4.0 KiB
Python
"""Gila River Health Care adapter — Infor CloudSuite, keyword search + click pagination."""
|
|
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
|
|
|
|
_SEARCH_KEYWORD = "dent" # broad match: captures Dentist, Dental, etc.
|
|
_KEYWORDS = ["orthodontist", "orthodontic", "dentist"]
|
|
|
|
|
|
@register("gilariver")
|
|
class GilaRiverAdapter(ManifestDrivenAdapter):
|
|
|
|
def apply_search(self, page, query) -> None:
|
|
"""Type keyword into search form and submit; wait for filtered results."""
|
|
try:
|
|
page.wait_for_selector(
|
|
".JobPostingSearchRel_col_KeywordSearch",
|
|
timeout=15000,
|
|
state="visible",
|
|
)
|
|
kw_input = page.query_selector(".JobPostingSearchRel_col_KeywordSearch")
|
|
if not kw_input:
|
|
logger.warning("[gilariver] Keyword input not found")
|
|
return
|
|
|
|
kw_input.click(click_count=3)
|
|
kw_input.type(_SEARCH_KEYWORD, delay=50)
|
|
kw_input.press("Enter")
|
|
page.wait_for_load_state("networkidle", timeout=15000)
|
|
logger.info(f"[gilariver] Searched for '{_SEARCH_KEYWORD}'")
|
|
except Exception as e:
|
|
logger.warning(f"[gilariver] apply_search failed: {e}")
|
|
|
|
def collect_cards(self, page, manifest) -> list[RawJobCard]:
|
|
"""Extract cards via JS and filter by keywords in-memory."""
|
|
raw_data = page.evaluate("""() => {
|
|
const cards = document.querySelectorAll('.lm-card-content');
|
|
return Array.from(cards).map(card => {
|
|
const titleA = card.querySelector('span.lm-card-field.text-strong a');
|
|
const subtitle = card.querySelector(
|
|
'span.lm-card-field:not(.text-strong):not(.text-emphasis)'
|
|
);
|
|
const posted = card.querySelector('span.lm-card-field.text-emphasis');
|
|
return {
|
|
title: (titleA?.textContent || '').trim(),
|
|
url: titleA?.href || '',
|
|
subtitle: (subtitle?.textContent || '').trim(),
|
|
posted: (posted?.textContent || '').trim()
|
|
};
|
|
}).filter(r => r.title);
|
|
}""")
|
|
|
|
cards = []
|
|
for item in raw_data:
|
|
title = item["title"]
|
|
if not self._matches_keywords(title):
|
|
continue
|
|
|
|
# Subtitle: "Location | Category | Status - Employment Type"
|
|
parts = [p.strip() for p in item["subtitle"].split("|")]
|
|
location = parts[0] if parts else None
|
|
|
|
cards.append(RawJobCard(
|
|
title=title,
|
|
company="Gila River Health Care",
|
|
location=location or None,
|
|
posted_text=item["posted"] or None,
|
|
url=item["url"] or None,
|
|
))
|
|
|
|
logger.info(
|
|
f"[gilariver] Keyword filter: {len(cards)}/{len(raw_data)} cards matched"
|
|
)
|
|
return cards
|
|
|
|
def paginate(self, page, page_index: int, config) -> bool:
|
|
"""Click Next Page button; return False when disabled or missing."""
|
|
try:
|
|
buttons = page.query_selector_all("button")
|
|
next_btn = next(
|
|
(b for b in buttons if b.inner_text().strip() == "Next Page"), None
|
|
)
|
|
if not next_btn or not next_btn.is_enabled():
|
|
return False
|
|
next_btn.click()
|
|
page.wait_for_load_state("networkidle", timeout=15000)
|
|
return True
|
|
except Exception as e:
|
|
logger.debug(f"[gilariver] paginate failed: {e}")
|
|
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)
|