"""JobPosting JSON-LD / microdata on any careers page (schema.org). Shared helper `jobs_from_jsonld` is also used by the generic HTML connector; this connector is picked when discovery has seen JobPosting structured data on a careers URL that has no ATS board.""" from __future__ import annotations from collections.abc import Mapping from typing import Any from companyatlas.connectors._util import country_code, description_hash, finish_job, job_blocks, jobs_text, parse_date, text_of from companyatlas.fetch import FetchResult from companyatlas.sdk import normalize from companyatlas.sdk.connector import Connector, ConnectorMeta, register from companyatlas.sdk.models import ExtractedJob, Extraction from companyatlas.taxonomy import FetchMode, Surface def _place(loc: Any) -> tuple[str | None, str | None, str | None, str | None]: """(location_text, city, region, country) from a schema.org Place / PostalAddress / string.""" if loc is None: return None, None, None, None if isinstance(loc, list): loc = loc[0] if loc else None if loc is None: return None, None, None, None if isinstance(loc, str): return loc, None, None, None addr = loc.get("address") if isinstance(loc, dict) else None if isinstance(addr, str): return addr, None, None, None addr = addr if isinstance(addr, dict) else (loc if isinstance(loc, dict) and "addressLocality" in loc else {}) city = text_of(addr.get("addressLocality")) region = text_of(addr.get("addressRegion")) country_raw = addr.get("addressCountry") if isinstance(country_raw, dict): country_raw = country_raw.get("name") country = country_code(country_raw) if isinstance(country_raw, str) else None parts = [x for x in (city, region, (country or (country_raw if isinstance(country_raw, str) else None))) if x] return (", ".join(parts) or text_of(loc.get("name")) if isinstance(loc, dict) else None), city, region, country def jobs_from_jsonld(items: list[dict[str, Any]] | None, *, page_url: str) -> list[ExtractedJob]: out: list[ExtractedJob] = [] for jp in items or []: title = text_of(jp.get("title")) or text_of(jp.get("name")) if not title: continue loc_text, city, region, country = _place(jp.get("jobLocation")) ident = jp.get("identifier") ext = None if isinstance(ident, dict): ext = text_of(ident.get("value")) or text_of(ident.get("name")) elif isinstance(ident, str | int): ext = str(ident) remote = True if str(jp.get("jobLocationType") or "").upper() == "TELECOMMUTE" else None salary = jp.get("baseSalary") if isinstance(jp.get("baseSalary"), dict) else {} val = salary.get("value") if isinstance(salary.get("value"), dict) else {} emp = jp.get("employmentType") job = ExtractedJob(title=title, url=text_of(jp.get("url")) or page_url, external_id=ext, location_text=loc_text, city=city, region=region, country=country, remote=remote, employment_type=text_of(emp[0] if isinstance(emp, list) and emp else emp), posted_at=parse_date(jp.get("datePosted")), description_hash=description_hash(jp.get("description")), salary_min=val.get("minValue") or val.get("value"), salary_max=val.get("maxValue"), salary_currency=salary.get("currency"), salary_period=val.get("unitText"), department=text_of(jp.get("occupationalCategory")) if isinstance(jp.get("occupationalCategory"), str) else None, raw={"validThrough": jp.get("validThrough"), "hiringOrganization": text_of(jp.get("hiringOrganization"))}) out.append(finish_job(job)) return out @register class JsonLdJobsConnector(Connector): meta = ConnectorMeta(connector_id="jsonld-jobs-v1", name="JobPosting structured data", version="1", category=Surface.JOBS_BOARD, fetch_mode=FetchMode.HTTP, default_interval_s=12 * 3600, surfaces=(), priority=2, description="schema.org JobPosting JSON-LD / microdata (auto-picked only for non-ATS jobs_board URLs; careers HTML uses generic-html)") def extract(self, sensor: Mapping[str, Any], result: FetchResult) -> Extraction: page = normalize.parse(result.text, url=result.final_url, surface=str(sensor.get("surface") or "")) items = list(page.jsonld.get("job_postings") or []) + list(page.microdata.get("job_postings") or []) jobs = jobs_from_jsonld(items, page_url=result.final_url) header = f"{page.title or 'Careers'} — {len(jobs)} structured job postings" ex = page.to_extraction() ex.jobs = jobs if jobs: ex.blocks = ex.blocks + job_blocks(jobs, path="JobPosting") ex.text = ex.text + "\n" + jobs_text(jobs, header) ex.meta.update({"job_count": len(jobs), "structured": bool(jobs)}) return ex __all__ = ["JsonLdJobsConnector", "jobs_from_jsonld"]