from __future__ import annotations from urllib.parse import urljoin, urlsplit from bs4 import BeautifulSoup from .base import ExtractedJob, ExtractionResult, FieldEvidence class CordaCampusAdapter: """Extract the public job cards intentionally published by Corda Campus.""" parser_key = "regional-corda-campus" parser_version = "1.0.0" source_hosts = ("cordacampus.com",) def _supports_url(self, url: str) -> bool: parts = urlsplit(url) host = (parts.hostname or "").lower() return host.endswith(self.source_hosts) and parts.path.rstrip("/") == "/jobs" def extract(self, content: str, *, url: str) -> ExtractionResult: if not self._supports_url(url): return ExtractionResult( [], self.parser_key, self.parser_version, 0.0, ["Onherkenbare regiobron"] ) source_host = (urlsplit(url).hostname or "").lower() soup = BeautifulSoup(content, "lxml") jobs: list[ExtractedJob] = [] seen_urls: set[str] = set() for card in soup.select('a.event-item[href*="/job/"]'): job_url = urljoin(url, str(card.get("href") or "")) job_host = (urlsplit(job_url).hostname or "").lower() if job_host != source_host or job_url in seen_urls: continue title_node = card.select_one(".job-title") employer_node = card.select_one(".company-title") title = title_node.get_text(" ", strip=True) if title_node else "" employer = employer_node.get_text(" ", strip=True) if employer_node else "" if not title: continue date_node = card.select_one(".bottom-info") date_posted = date_node.get_text(" ", strip=True) if date_node else "" external_id = urlsplit(job_url).path.rstrip("/").rsplit("/", 1)[-1] description = f"Vacature van {employer or 'een Corda-werkgever'} via Corda Campus." jobs.append( ExtractedJob( url=job_url, title=title, employer_name=employer, external_id=external_id, location_text="Hasselt, Limburg", region="Limburg", country="BE", description_text=description, date_posted=date_posted, raw={"regional_listing": "corda-campus"}, evidence=[ FieldEvidence("title", "corda-job-card", 0.95, title[:240]), FieldEvidence("employer_name", "corda-job-card", 0.92, employer[:240]), FieldEvidence( "location_text", "regional-source-scope", 0.72, "Corda Campus, Hasselt", ), ], ) ) seen_urls.add(job_url) warnings = [] if jobs else ["Geen actuele Corda-vacatures gevonden"] return ExtractionResult( jobs, self.parser_key, self.parser_version, 0.9 if jobs else 0.0, warnings, ) class LocalEmployerListingAdapter: """Extract reviewed public listing pages of employers in the Hasselt-Genk area.""" parser_key = "regional-local-employers" parser_version = "1.0.0" supported_routes = { "acagroup.be": {"/en/jobs"}, "www.xploregroup.be": {"/en/jobs"}, "www.uhasselt.be": {"/vacatures"}, "ses.pxl.be": {"/"}, "ziekenhuis-oost-limburg.cvw.io": {"/"}, } def _supports_url(self, url: str) -> bool: return self._route(url) is not None def _route(self, url: str) -> tuple[str, str] | None: parts = urlsplit(url) host = (parts.hostname or "").lower() path = parts.path.rstrip("/") or "/" if parts.scheme != "https" or path not in self.supported_routes.get(host, set()): return None return host, path @staticmethod def _job( *, url: str, title: str, employer: str, location: str, description: str, external_id: str, evidence_source: str, ) -> ExtractedJob: return ExtractedJob( url=url, title=title, employer_name=employer, external_id=external_id, location_text=location, region="Limburg", country="BE", description_text=description, raw={"regional_listing": evidence_source}, evidence=[ FieldEvidence("title", evidence_source, 0.94, title[:240]), FieldEvidence("employer_name", "reviewed-source", 0.96, employer), FieldEvidence("location_text", "regional-source-scope", 0.78, location), ], ) @staticmethod def _same_host_url(base_url: str, href: str) -> str: job_url = urljoin(base_url, href) if (urlsplit(job_url).hostname or "").lower() != ( urlsplit(base_url).hostname or "" ).lower(): return "" return job_url def _aca(self, soup: BeautifulSoup, url: str) -> list[ExtractedJob]: jobs = [] for link in soup.find_all("a", href=True): href = str(link.get("href") or "") if not href.startswith("/en/jobs/"): continue title_node = link.find("h3") title = title_node.get_text(" ", strip=True) if title_node else "" job_url = self._same_host_url(url, href) if not title or not job_url: continue description_node = link.find("p") description = ( description_node.get_text(" ", strip=True) if description_node else f"Vacature bij ACA Group: {title}." ) jobs.append( self._job( url=job_url, title=title, employer="ACA Group", location="Hasselt (hybride; kantoorselectie per vacature)", description=description, external_id=urlsplit(job_url).path.rstrip("/").rsplit("/", 1)[-1], evidence_source="aca-job-card", ) ) return jobs def _xplore(self, soup: BeautifulSoup, url: str) -> list[ExtractedJob]: jobs = [] for heading in soup.find_all("h3"): link = heading.find("a", href=True) if not link or not str(link.get("href") or "").startswith("/en/jobs/"): continue card = heading.parent location_node = next( ( node for node in card.find_all("p") if "Hasselt" in node.get_text(" ", strip=True) ), None, ) if not location_node: continue title = link.get_text(" ", strip=True) job_url = self._same_host_url(url, str(link.get("href") or "")) if not title or not job_url: continue location = location_node.get_text(" ", strip=True) jobs.append( self._job( url=job_url, title=title, employer="Xplore Group", location=location, description=( f"Vacature bij Xplore Group met Hasselt als mogelijke werklocatie: {title}." ), external_id=urlsplit(job_url).path.rstrip("/").rsplit("/", 1)[-1], evidence_source="xplore-job-card", ) ) return jobs def _uhasselt(self, soup: BeautifulSoup, url: str) -> list[ExtractedJob]: jobs = [] for card in soup.select("section.vacancy-item"): title_node = card.find("h3") link = card.find("a", href=True) title = title_node.get_text(" ", strip=True) if title_node else "" job_url = self._same_host_url(url, str(link.get("href") or "")) if link else "" if not title or not job_url or "/vacatures/detail/" not in urlsplit(job_url).path: continue external_id = urlsplit(job_url).path.split("/detail/", 1)[-1].split("-", 1)[0] jobs.append( self._job( url=job_url, title=title, employer="Universiteit Hasselt", location="Hasselt/Diepenbeek, Limburg", description=card.get_text(" ", strip=True), external_id=external_id, evidence_source="uhasselt-vacancy-card", ) ) return jobs def _pxl(self, soup: BeautifulSoup, url: str) -> list[ExtractedJob]: jobs = [] for card in soup.select(".vacature-card[id^='Vacature_']"): external_id = str(card.get("id") or "").removeprefix("Vacature_") title_node = card.select_one(".vacature-card-titel") title = title_node.get_text(" ", strip=True) if title_node else "" if not title or not external_id.isdigit(): continue cells = [ node.get_text(" ", strip=True) for node in card.select("td.vacature-card-td-text") ] campus = next((value for value in cells if value.startswith("Campus ")), "Hasselt") job_url = f"{url.rstrip('/')}?vacature_id={external_id}" jobs.append( self._job( url=job_url, title=title, employer="Hogeschool PXL", location=f"{campus}, Limburg", description=" ยท ".join(cells), external_id=external_id, evidence_source="pxl-vacancy-card", ) ) return jobs def _zol(self, soup: BeautifulSoup, url: str) -> list[ExtractedJob]: jobs = [] for link in soup.select("a[data-item='readmore'][data-jobid][href]"): external_id = str(link.get("data-jobid") or "") title_node = link.select_one(".job-title") title = title_node.get_text(" ", strip=True) if title_node else "" job_url = self._same_host_url(url, str(link.get("href") or "")) if not title or not external_id.isdigit() or not job_url: continue jobs.append( self._job( url=job_url, title=title, employer="Ziekenhuis Oost-Limburg", location="Genk/Lanaken/Maaseik, Limburg", description=f"Publieke vacature van Ziekenhuis Oost-Limburg: {title}.", external_id=external_id, evidence_source="zol-cvwarehouse-card", ) ) return jobs def extract(self, content: str, *, url: str) -> ExtractionResult: route = self._route(url) if route is None: return ExtractionResult( [], self.parser_key, self.parser_version, 0.0, ["Onherkenbare lokale werkgeversbron"], ) host, _ = route soup = BeautifulSoup(content, "lxml") extractors = { "acagroup.be": self._aca, "www.xploregroup.be": self._xplore, "www.uhasselt.be": self._uhasselt, "ses.pxl.be": self._pxl, "ziekenhuis-oost-limburg.cvw.io": self._zol, } jobs = extractors[host](soup, url) unique_jobs = list({job.url: job for job in jobs}.values()) warnings = [] if unique_jobs else ["Geen actuele lokale vacatures gevonden"] return ExtractionResult( unique_jobs, self.parser_key, self.parser_version, 0.9 if unique_jobs else 0.0, warnings, )