Metadata-Version: 2.5
Name: hiringindex
Version: 0.1.1
Summary: Python client for the HiringIndex API: live job postings read from employers' own ATS, plus market aggregates.
Project-URL: Repository, https://github.com/starnikov-oleg-org/hiringindex-python
Project-URL: Homepage, https://hiringindex.org
Project-URL: API, https://rapidapi.com/starnikovoleg/api/hiringindex
Author: Oleg Starnikov
License-Expression: MIT
License-File: LICENSE
Keywords: api client,ats,hiring,job postings,job search,jobs,labor market,rapidapi,salary data
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Office/Business
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# hiringindex

A Python client for the [HiringIndex API](https://rapidapi.com/starnikovoleg/api/hiringindex).
The API covers more than a million and a half live job postings, read directly
from the applicant tracking systems employers hire on: Workday, SmartRecruiters,
Greenhouse, Workable, Lever, Ashby, Recruitee, Teamtailor, Breezy and Personio.
It has three endpoints. One searches postings with a filter, one returns
aggregates over the same filter (salary percentiles, top employers, city,
country and seniority splits, posting age), and one fetches a single posting by
id. This package is a thin wrapper around those endpoints. It uses only the
standard library, sends your filters to the API unchanged, and returns the JSON
responses as plain dictionaries.

## Install

```bash
pip install hiringindex
```

Requires Python 3.9 or newer. There are no other dependencies.

You need a RapidAPI key with a subscription to
[HiringIndex](https://rapidapi.com/starnikovoleg/api/hiringindex). Pass it to
the client, or set it in the environment:

```bash
export RAPIDAPI_KEY=your-key
```

## Quickstart

```python
from hiringindex import HiringIndex

client = HiringIndex()  # reads RAPIDAPI_KEY; or HiringIndex(api_key="...")

# Remote data engineering roles posted in the last week
page = client.search(job_titles=["Data Engineer"], remote_flag=["true"], days_ago=7, limit=20)
print(page["total_count"], "postings")
for job in page["jobs"]:
    print(job["title"], job.get("company_name"), job.get("apply_url"))

# Berlin roles whose advertised yearly pay reaches EUR 60,000
page = client.search(
    cities=["Berlin"],
    salary={"min": 60000, "period": ["year"], "currency": ["EUR"], "match": "overlaps"},
    limit=20,
)

# One posting by id: the `_id` from a search result, used verbatim
job = client.job(page["jobs"][0]["_id"])

# What the Software Engineer market looks like in San Francisco
market = client.insights(job_titles=["Software Engineer"], cities=["San Francisco"])
print(market["headline"]["row_count"], "rows,", market["headline"]["company_count"], "employers")
for band in market["salary"]:  # one entry per currency and pay period
    print(band["currency"], band.get("period"), band["count"], band.get("min", {}).get("p50"))

# Data Engineers in Germany and the Netherlands, one block per country
by_country = client.insights(
    job_titles=["Data Engineer"],
    country_codes=["DE", "NL"],
    group_by="country_code",
    percentiles=True,
)
for group in by_country["groups"]:
    print(group["country_code"], group["row_count"])
```

### Filters

Filters are keyword arguments with the API's own names. The client does not
rename or check them.

| Key | Type | Matches |
|-----|------|---------|
| `job_titles` | list of str | the posting title |
| `keywords` | list of str | the title or the description; several keywords match as OR |
| `cities` | list of str | city names as employers write them |
| `country_codes` | list of str | ISO 3166-1 alpha-2 codes such as `DE` (a country name matches nothing) |
| `company_name` | str | the employer name, whole and case-insensitive |
| `handles` | list of str | ATS board handles, as the `handle` field reports them |
| `remote_flag` | list of str | `["true"]` for remote, `["false"]` for on-site |
| `employment_type` | list of str | as the vendor writes it |
| `seniority` | list of str | as the `seniority` field reports it |
| `source_platforms` | list of str | `workday`, `greenhouse`, `lever`, `ashby` and the other sources |
| `salary` | dict | `min`, `max`, `currency` (ISO 4217 list), `period` (list of `year`, `month`, `week`, `day`, `hour`), `match` (`contains` or `overlaps`) |
| `days_ago` | int | published no earlier than N days ago |
| `page`, `limit` | int | search only, 1 to 100 each |

`insights` takes the same filter without `page` and `limit`, plus `group_by`
(`city`, `city_only` or `country_code`), `city_aliases`, `min_rows` and
`percentiles`.

A key the API does not know is rejected with a `422` that lists the valid keys,
so a typo raises an error instead of returning the wrong slice. Salary amounts
are never converted between currencies, so send `currency` and `period` with a
`salary` filter.

Fields a source did not state are left out of a posting rather than set to
`None`, so read optional fields with `job.get(...)`. `remote_flag` on a posting
is a string such as `"true"`, not a boolean.

### Paging

`iter_jobs` walks the pages for you and yields postings one at a time. It stops
after the last page, or after page 100, the furthest the API pages (so at most
`100 * limit` rows per filter). Each page is one request, made only when the
loop reaches it.

```python
from itertools import islice

for job in client.iter_jobs(job_titles=["Data Engineer"], cities=["Berlin"], limit=100):
    print(job["_id"], job["title"])

first_50 = list(islice(client.iter_jobs(keywords=["Kubernetes"], limit=50), 50))
```

## Errors

Every error response raises `HiringIndexError`, with `status`, `code`,
`message`, `meta` and `request_id` attributes. Branch on `code`, not on
`message`, because the wording of messages may change.

```python
from hiringindex import HiringIndex, HiringIndexError, KeywordTooCommon

client = HiringIndex()

try:
    page = client.search(keywords=["excel"])
except KeywordTooCommon as err:
    # The term matches too many postings. Narrowing by city or country does not help:
    # search the role with job_titles, or use a rarer keyword.
    print(err.term, err.estimated_matches, err.limit)
except HiringIndexError as err:
    print(err.status, err.code, err.message, err.request_id)
```

| Status | `code` | When |
|--------|--------|------|
| 400 | `invalid_request` | the body is not a JSON object |
| 401, 403 | `None` | missing or invalid key, or no subscription (answered by RapidAPI) |
| 404 | `not_found` | `job()` with an id the API did not issue |
| 422 | `invalid_request` | an unknown filter key or a malformed value |
| 422 | `keyword_too_common` | raised as `KeywordTooCommon`, a subclass of `HiringIndexError` |
| 429 | `None` | plan quota or rate limit reached (answered by RapidAPI) |
| 503 | `busy` | capacity exhausted for the moment; wait `err.retry_after_seconds` |
| 503 | `timeout` | the filter did not finish inside the API's time limit; narrow it or retry |

If no HTTP response arrives at all, for example on a connection failure or a
timeout, the standard library's `OSError` subclasses (`urllib.error.URLError`,
`TimeoutError`) propagate unchanged. The default timeout is 40 seconds, because
`insights` over a wide slice can take up to about 30 seconds on a cold cache.

## Links

- API listing, plans and full reference: https://rapidapi.com/starnikovoleg/api/hiringindex
- Website: https://hiringindex.org

## License

MIT
