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.
59 lines
2.1 KiB
Python
59 lines
2.1 KiB
Python
"""USAJOBS adapter — waits for JS render, handles relative URLs, URL-based pagination."""
|
|
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("usajobs")
|
|
class UsaJobsAdapter(ManifestDrivenAdapter):
|
|
_BASE_URL = "https://www.usajobs.gov"
|
|
|
|
def _wait_for_results(self, page) -> None:
|
|
"""Wait for JS-rendered results or zero-results heading."""
|
|
try:
|
|
page.wait_for_selector(
|
|
"#search-results .border, h2.font-normal",
|
|
timeout=12000,
|
|
state="attached",
|
|
)
|
|
except Exception:
|
|
logger.debug("[usajobs] Results wait timed out — may be zero results")
|
|
|
|
def collect_cards(self, page, manifest):
|
|
self._wait_for_results(page)
|
|
return super().collect_cards(page, manifest)
|
|
|
|
def paginate(self, page, page_index: int, config) -> bool:
|
|
"""Navigate to next page by incrementing the &p= URL parameter."""
|
|
max_pages = config.pagination.max_pages
|
|
if page_index >= max_pages - 1:
|
|
return False
|
|
|
|
current_url = page.url
|
|
next_page = page_index + 2 # page_index is 0-based; URL param is 1-based
|
|
|
|
# Replace &p=N or add &p=N
|
|
if re.search(r"[?&]p=\d+", current_url):
|
|
next_url = re.sub(r"(p=)\d+", f"p={next_page}", current_url)
|
|
else:
|
|
sep = "&" if "?" in current_url else "?"
|
|
next_url = f"{current_url}{sep}p={next_page}"
|
|
|
|
logger.debug(f"[usajobs] Paginating to page {next_page}: {next_url}")
|
|
page.goto(next_url, timeout=30000)
|
|
self._wait_for_results(page)
|
|
return True
|
|
|
|
def normalize(self, raw: RawJobCard) -> JobPostingCandidate:
|
|
candidate = super().normalize(raw)
|
|
# USAJOBS job links are relative: /job/123456 → absolute URL
|
|
if candidate.job_url and candidate.job_url.startswith("/"):
|
|
candidate.job_url = self._BASE_URL + candidate.job_url
|
|
return candidate
|