Skip to content

Platform Adapters

Each job data source is implemented as an adapter behind the JobSearchService ABC.

Interface Definition

from abc import ABC, abstractmethod

class JobSearchService(ABC):
    @abstractmethod
    def platform(self) -> Platform: ...

    @abstractmethod
    async def search(self, criteria: SearchCriteria) -> list[Job]: ...

    @abstractmethod
    async def fetch_details(self, job: Job) -> Job: ...

Adapter Architecture

graph LR
    subgraph kairos.core
        Trait[JobSearchService ABC]
    end

    subgraph kairos.platform
        Adzuna[AdzunaClient]
        Seek[SeekClient]
        LinkedIn[LinkedInClient]
        RL[RateLimiter]
        HC[PlatformHttpClient]
    end

    Adzuna --> Trait
    Seek --> Trait
    LinkedIn --> Trait
    Seek --> HC
    LinkedIn --> HC
    HC --> RL

Adzuna API (Primary)

Method: HTTP API calls to api.adzuna.com — structured JSON, no scraping.

flowchart LR
    Search[SearchCriteria] --> API["GET api.adzuna.com/v1/api/jobs/{country}/search/{page}"]
    API --> JSON[JSON Response]
    JSON --> Parse[Parse results array]
    Parse --> Map[Map to Job entity]
  • Free API key from developer.adzuna.com
  • Structured JSON: title, company, location, salary, description
  • Covers AU, US, UK, CA, NZ + 14 more countries
  • ~250 requests/day free tier
  • Pagination via page parameter, 20 results per page
  • Credentials injected from Settings (not read from config at runtime)

Seek.com.au (Fallback Scraper)

Method: httpx + beautifulsoup4 (HTTP + HTML parsing)

flowchart LR
    Search[Search URL] --> HTTP[httpx GET]
    HTTP --> HTML[HTML Response]
    HTML --> Parse["Parse SEEK_APOLLO_DATA JSON"]
    Parse --> Map[Map to Job entity]
  • Server-side rendered with Apollo GraphQL data embedded in <script> tags
  • Pagination: ~22 results per page
  • DOM fallback parsing via data-automation attributes
  • Rate limit: 2-5s randomized delay
  • No login required

LinkedIn (Fallback Scraper)

Method: httpx + beautifulsoup4 (public job listings)

  • Guest API endpoint for paginated HTML fragments (25 per page)
  • Detail pages: JSON-LD structured data on au.linkedin.com
  • Rate limit: 2-6s randomized delay
  • No login required
  • Fallback: if au.linkedin.com fails, tries www.linkedin.com

~~Indeed~~ (Deferred)

Indeed adapter is deferred due to Cloudflare protection requiring headless browser (Playwright) and high ban risk. Adzuna API covers the same job listings more reliably.

Shared Infrastructure

RateLimiter

Enforces randomized delays between requests: - Seek: 2-5s - LinkedIn: 2-6s

PlatformHttpClient

Wraps httpx.AsyncClient with: - Rate limiting - User-Agent rotation (pool of realistic browser UAs) - Retry with exponential backoff (3 attempts)

Adding a New Platform

  1. Create module under kairos/platform/<name>/
  2. Implement JobSearchService ABC
  3. Add variant to Platform enum in kairos.core
  4. Register in get_search_service() factory in kairos.platform.__init__

No existing code needs to change.