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
pageparameter, 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-automationattributes - 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.comfails, trieswww.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¶
- Create module under
kairos/platform/<name>/ - Implement
JobSearchServiceABC - Add variant to
Platformenum inkairos.core - Register in
get_search_service()factory inkairos.platform.__init__
No existing code needs to change.