"""Consulting BFrench Jobs adapter — JazzHR single-page table, 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 _KEYWORDS = ["orthodontist", "orthodontic", "dentist"] @register("bfrench") class BFrenchAdapter(ManifestDrivenAdapter): def collect_cards(self, page, manifest) -> list[RawJobCard]: """Extract all job rows from the table and filter by keywords.""" raw_data = page.evaluate("""() => { const links = document.querySelectorAll('a[href*="/apply/jobs/details/"]'); return Array.from(links).map(a => { const row = a.closest('tr'); const cells = row ? Array.from(row.querySelectorAll('td')) : []; return { title: a.innerText.trim(), url: a.href, location: cells[1]?.innerText?.trim() || null }; }).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="Consulting BFrench", location=item["location"], url=item["url"], )) logger.info( f"[bfrench] Keyword filter: {len(cards)}/{len(raw_data)} cards matched" ) return cards def paginate(self, page, page_index: int, config) -> bool: 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)