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.
94 lines
3.7 KiB
Python
94 lines
3.7 KiB
Python
"""SRPMIC (Salt River Pima-Maricopa Indian Community) adapter — GovernmentJobs.com."""
|
|
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
|
|
|
|
|
|
@register("srpmic")
|
|
class SRPMICAdapter(ManifestDrivenAdapter):
|
|
|
|
def collect_cards(self, page, manifest) -> list[RawJobCard]:
|
|
"""Extract job cards via JS from the current results page."""
|
|
raw_data = page.evaluate("""() => {
|
|
const links = document.querySelectorAll('a[href*="/careers/srpmic/jobs/"]');
|
|
return Array.from(links).map(a => {
|
|
const li = a.closest('li');
|
|
// Details list is the first <ul> directly inside the job card <li>
|
|
const detailsUl = li?.querySelector(':scope > ul');
|
|
const details = detailsUl
|
|
? Array.from(detailsUl.querySelectorAll('li')).map(l => l.innerText.trim())
|
|
: [];
|
|
// Posted date is in a direct child element starting with "Posted"
|
|
const postedText = Array.from(li?.children || [])
|
|
.map(c => c.innerText?.trim())
|
|
.find(t => t && t.startsWith('Posted')) || null;
|
|
return {
|
|
title: a.innerText.trim(),
|
|
url: a.href,
|
|
location: details[0] || null,
|
|
salary: details[1] || null,
|
|
posted: postedText
|
|
};
|
|
}).filter(j => j.title && j.url.includes('/careers/srpmic/jobs/'));
|
|
}""")
|
|
|
|
cards = []
|
|
for item in raw_data:
|
|
cards.append(RawJobCard(
|
|
title=item["title"],
|
|
company="Salt River Pima-Maricopa Indian Community",
|
|
location=item["location"],
|
|
salary_text=item["salary"],
|
|
posted_text=item["posted"],
|
|
url=item["url"],
|
|
))
|
|
|
|
logger.info(f"[srpmic] Extracted {len(cards)} cards")
|
|
return cards
|
|
|
|
def paginate(self, page, page_index: int, config) -> bool:
|
|
"""Navigate to next page using URL ?page=N parameter."""
|
|
# Check if Next link is present and enabled
|
|
has_next = page.evaluate("""() => {
|
|
const links = Array.from(document.querySelectorAll('a'));
|
|
const next = links.find(a =>
|
|
a.textContent.trim() === 'Next' ||
|
|
(a.getAttribute('aria-label') || '').toLowerCase().includes('next page')
|
|
);
|
|
return !!(next && !next.hasAttribute('disabled') &&
|
|
next.getAttribute('aria-disabled') !== 'true');
|
|
}""")
|
|
|
|
if not has_next:
|
|
return False
|
|
|
|
next_page_num = page_index + 2 # page_index 0 → go to page 2
|
|
current_url = page.url
|
|
|
|
if re.search(r'[?&]page=\d+', current_url):
|
|
next_url = re.sub(r'(page=)\d+', f'page={next_page_num}', current_url)
|
|
elif '?' in current_url:
|
|
next_url = current_url + f'&page={next_page_num}'
|
|
else:
|
|
next_url = current_url + f'?page={next_page_num}'
|
|
|
|
logger.debug(f"[srpmic] Navigating to page {next_page_num}: {next_url}")
|
|
page.goto(next_url, timeout=30000)
|
|
try:
|
|
page.wait_for_selector(
|
|
"a[href*='/careers/srpmic/jobs/']", timeout=15000, state="attached"
|
|
)
|
|
except Exception:
|
|
logger.debug("[srpmic] No jobs found on next page — stopping")
|
|
return False
|
|
return True
|
|
|
|
def normalize(self, raw: RawJobCard) -> JobPostingCandidate:
|
|
return super().normalize(raw)
|