"""Greenhouse job board — public endpoint used by every `boards.greenhouse.io/` page: `GET https://boards-api.greenhouse.io/v1/boards/{token}/jobs?content=false` → {"jobs":[{id, title, absolute_url, location:{name}, updated_at, first_published, departments?, offices?, requisition_id}]}. Verified 2026-09-12 against the `stripe` board (fixture trimmed to 20 jobs).""" from __future__ import annotations import re from collections.abc import Mapping from typing import Any from companyatlas.connectors._ats_base import AtsConnector from companyatlas.connectors._util import parse_date, text_of from companyatlas.sdk.connector import ConnectorMeta, register from companyatlas.sdk.models import ExtractedJob from companyatlas.taxonomy import FetchMode, Surface @register class GreenhouseConnector(AtsConnector): vendor = "greenhouse" token_re = re.compile(r"boards-api\.greenhouse\.io/v1/boards/([a-z0-9_-]+)", re.IGNORECASE) meta = ConnectorMeta(connector_id="greenhouse-v1", name="Greenhouse job board", version="1", category=Surface.JOBS_BOARD, fetch_mode=FetchMode.JSON, supports_discovery=False, supports_incremental=True, default_interval_s=6 * 3600, url_pattern=r"boards-api\.greenhouse\.io/v1/boards/", priority=50, pattern_required=True, accept="application/json", description="Public Greenhouse Job Board API (jobs list without descriptions)") def parse_jobs(self, data: Any, sensor: Mapping[str, Any]) -> list[ExtractedJob]: items = data.get("jobs") if isinstance(data, dict) else data out: list[ExtractedJob] = [] for j in items or []: if not isinstance(j, dict) or not j.get("title"): continue depts = j.get("departments") or [] offices = j.get("offices") or [] out.append(ExtractedJob( title=str(j["title"]), url=j.get("absolute_url"), external_id=str(j.get("id") or j.get("internal_job_id") or ""), department=text_of(depts[0]) if depts else None, location_text=text_of(j.get("location")), posted_at=parse_date(j.get("first_published") or j.get("updated_at")), raw={"requisition_id": j.get("requisition_id"), "updated_at": j.get("updated_at"), "offices": [text_of(o) for o in offices][:5], "language": j.get("language")}, )) return out