"""Personio job XML feed — public feed behind `.jobs.personio.de`: `GET https://{token}.jobs.personio.de/xml` → …. Job URL `https://{token}.jobs.personio.de/job/{id}`. Verified 2026-09-12 against `personio` (fixture).""" from __future__ import annotations import re import xml.etree.ElementTree as ET from collections.abc import Mapping from typing import Any from companyatlas.connectors._ats_base import AtsConnector from companyatlas.connectors._util import parse_date from companyatlas.fetch import FetchResult from companyatlas.sdk.connector import ConnectorMeta, register from companyatlas.sdk.models import ExtractedJob from companyatlas.taxonomy import FetchMode, Surface @register class PersonioConnector(AtsConnector): vendor = "personio" token_re = re.compile(r"https?://([a-z0-9-]+)\.jobs\.personio\.(?:de|com)/xml", re.IGNORECASE) meta = ConnectorMeta(connector_id="personio-v1", name="Personio XML feed", version="1", category=Surface.JOBS_BOARD, fetch_mode=FetchMode.FEED, default_interval_s=12 * 3600, url_pattern=r"\.jobs\.personio\.(de|com)/xml", pattern_required=True, priority=50, accept="application/xml,text/xml", description="Public Personio job XML feed") def load(self, result: FetchResult) -> Any: if len(result.content) > 8 * 1024 * 1024 or b" list[ExtractedJob]: token = self.token(sensor) or "" host = re.sub(r"/xml.*$", "", str(sensor.get("url") or "")) or f"https://{token}.jobs.personio.de" out: list[ExtractedJob] = [] for pos in data.iter("position"): g = {c.tag: (c.text or "").strip() for c in pos if c.tag != "additionalOffices" and c.tag != "jobDescriptions"} name = g.get("name") if not name: continue offices = [g.get("office")] + [o.text.strip() for o in pos.iter("office") if o.text and o.text.strip() != g.get("office")] offices = [o for o in offices if o] jid = g.get("id") out.append(ExtractedJob(title=name, url=f"{host}/job/{jid}" if jid else None, external_id=jid or None, department=g.get("department") or None, location_text=offices[0] if offices else None, employment_type=g.get("schedule") or g.get("employmentType") or None, seniority=g.get("seniority") or None, posted_at=parse_date(g.get("createdAt")), raw={"recruitingCategory": g.get("recruitingCategory"), "occupation": g.get("occupation"), "subcompany": g.get("subcompany"), "yearsOfExperience": g.get("yearsOfExperience"), "offices": offices[:5]})) return out