openroles 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openroles-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +23 -0
- openroles-0.1.0/.github/ISSUE_TEMPLATE/config.yml +1 -0
- openroles-0.1.0/.github/ISSUE_TEMPLATE/new_ats.yml +22 -0
- openroles-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +9 -0
- openroles-0.1.0/.github/dependabot.yml +6 -0
- openroles-0.1.0/.github/workflows/ci.yml +34 -0
- openroles-0.1.0/.github/workflows/publish.yml +40 -0
- openroles-0.1.0/.gitignore +7 -0
- openroles-0.1.0/CHANGELOG.md +11 -0
- openroles-0.1.0/CODE_OF_CONDUCT.md +9 -0
- openroles-0.1.0/CONTRIBUTING.md +61 -0
- openroles-0.1.0/LICENSE +21 -0
- openroles-0.1.0/PKG-INFO +191 -0
- openroles-0.1.0/README.md +169 -0
- openroles-0.1.0/SECURITY.md +9 -0
- openroles-0.1.0/llms.txt +28 -0
- openroles-0.1.0/pyproject.toml +50 -0
- openroles-0.1.0/src/openroles/__init__.py +84 -0
- openroles-0.1.0/src/openroles/__main__.py +5 -0
- openroles-0.1.0/src/openroles/_http.py +130 -0
- openroles-0.1.0/src/openroles/cli.py +86 -0
- openroles-0.1.0/src/openroles/discover.py +101 -0
- openroles-0.1.0/src/openroles/mcp.py +145 -0
- openroles-0.1.0/src/openroles/models.py +80 -0
- openroles-0.1.0/src/openroles/providers/__init__.py +6 -0
- openroles-0.1.0/src/openroles/providers/ashby.py +117 -0
- openroles-0.1.0/src/openroles/providers/greenhouse.py +48 -0
- openroles-0.1.0/src/openroles/providers/lever.py +100 -0
- openroles-0.1.0/src/openroles/providers/workday.py +326 -0
- openroles-0.1.0/src/openroles/py.typed +0 -0
- openroles-0.1.0/src/openroles/robots.py +68 -0
- openroles-0.1.0/tests/__init__.py +0 -0
- openroles-0.1.0/tests/conftest.py +42 -0
- openroles-0.1.0/tests/fixtures/ashby.json +9 -0
- openroles-0.1.0/tests/fixtures/greenhouse.json +8 -0
- openroles-0.1.0/tests/fixtures/lever.json +8 -0
- openroles-0.1.0/tests/fixtures/workday_job.json +3 -0
- openroles-0.1.0/tests/fixtures/workday_list.json +7 -0
- openroles-0.1.0/tests/test_discover.py +40 -0
- openroles-0.1.0/tests/test_mcp.py +40 -0
- openroles-0.1.0/tests/test_providers.py +103 -0
- openroles-0.1.0/tests/test_robots.py +26 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Something reads a board wrongly, or fails
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: input
|
|
6
|
+
id: target
|
|
7
|
+
attributes:
|
|
8
|
+
label: Board
|
|
9
|
+
description: The ats:token or board URL (a public board, so it is fine to share)
|
|
10
|
+
placeholder: greenhouse:acme
|
|
11
|
+
validations:
|
|
12
|
+
required: true
|
|
13
|
+
- type: textarea
|
|
14
|
+
id: what
|
|
15
|
+
attributes:
|
|
16
|
+
label: What happened, and what you expected
|
|
17
|
+
validations:
|
|
18
|
+
required: true
|
|
19
|
+
- type: input
|
|
20
|
+
id: version
|
|
21
|
+
attributes:
|
|
22
|
+
label: openroles and Python versions
|
|
23
|
+
placeholder: openroles 0.1.0, Python 3.12
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blank_issues_enabled: true
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Support another ATS
|
|
2
|
+
description: Ask for (or offer to build) a reader for another applicant tracking system
|
|
3
|
+
labels: [new-ats]
|
|
4
|
+
body:
|
|
5
|
+
- type: input
|
|
6
|
+
id: ats
|
|
7
|
+
attributes:
|
|
8
|
+
label: Which system
|
|
9
|
+
placeholder: Workable
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: endpoint
|
|
14
|
+
attributes:
|
|
15
|
+
label: Its public board endpoint
|
|
16
|
+
description: The URL a careers page reads its open roles from, and whether it is documented or allowed by robots.txt
|
|
17
|
+
- type: checkboxes
|
|
18
|
+
id: help
|
|
19
|
+
attributes:
|
|
20
|
+
label: Helping
|
|
21
|
+
options:
|
|
22
|
+
- label: I'd like to build it (see CONTRIBUTING.md, "Adding an ATS")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
## What changed
|
|
2
|
+
|
|
3
|
+
## How it was checked
|
|
4
|
+
|
|
5
|
+
<!-- Tests added or updated, and how you checked it against a real board (without pasting that board's data). -->
|
|
6
|
+
|
|
7
|
+
- [ ] `pytest` and `ruff check .` pass
|
|
8
|
+
- [ ] No runtime dependency added; works on Python 3.9
|
|
9
|
+
- [ ] robots.txt and the request limits are respected
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ci-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
test:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
timeout-minutes: 10
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v7
|
|
25
|
+
- uses: actions/setup-python@v7
|
|
26
|
+
with:
|
|
27
|
+
python-version: ${{ matrix.python }}
|
|
28
|
+
- run: pip install -e . pytest ruff
|
|
29
|
+
- run: ruff check .
|
|
30
|
+
- run: pytest -q
|
|
31
|
+
- name: The CLI and the MCP server start
|
|
32
|
+
run: |
|
|
33
|
+
openroles --version
|
|
34
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | openroles mcp | grep -q list_open_roles
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Runs when a GitHub release is published. PyPI trusts this workflow directly
|
|
4
|
+
# (trusted publishing), so no token is stored anywhere.
|
|
5
|
+
on:
|
|
6
|
+
release:
|
|
7
|
+
types: [published]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
timeout-minutes: 10
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
|
+
- uses: actions/setup-python@v7
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
- run: pip install build
|
|
22
|
+
- run: python -m build
|
|
23
|
+
- uses: actions/upload-artifact@v7
|
|
24
|
+
with:
|
|
25
|
+
name: dist
|
|
26
|
+
path: dist/
|
|
27
|
+
|
|
28
|
+
publish:
|
|
29
|
+
needs: build
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
timeout-minutes: 10
|
|
32
|
+
environment: pypi
|
|
33
|
+
permissions:
|
|
34
|
+
id-token: write
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/download-artifact@v8
|
|
37
|
+
with:
|
|
38
|
+
name: dist
|
|
39
|
+
path: dist/
|
|
40
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-10-15)
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- Greenhouse, Lever (global and EU), Ashby and Workday boards, in one `Job` shape.
|
|
8
|
+
- `detect`: a board from a company name, a board URL or a careers page.
|
|
9
|
+
- CLI (`openroles`), Python API (`openroles.fetch`, `openroles.detect`) and an MCP server
|
|
10
|
+
(`openroles mcp`) with no dependencies.
|
|
11
|
+
- robots.txt honoured; two requests per host at a time; Workday stops on 429 or 503.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
openroles follows the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
|
|
4
|
+
In short: be welcoming, be kind, assume good faith, and keep criticism about the work.
|
|
5
|
+
Harassment of any kind is not tolerated.
|
|
6
|
+
|
|
7
|
+
To report a problem, contact the maintainer privately through
|
|
8
|
+
[their GitHub profile](https://github.com/aranculleton). Reports are kept confidential,
|
|
9
|
+
and the maintainer will act on them as the Covenant describes.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Contributing to openroles
|
|
2
|
+
|
|
3
|
+
Thanks for helping. openroles is small on purpose: four readers, one shape, no
|
|
4
|
+
dependencies. Contributions that keep it that way are very welcome.
|
|
5
|
+
|
|
6
|
+
## Set up
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
git clone https://github.com/aranculleton/openroles && cd openroles
|
|
10
|
+
python -m venv .venv && . .venv/bin/activate
|
|
11
|
+
pip install -e . pytest ruff
|
|
12
|
+
pytest && ruff check .
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The tests never touch the network. Every answer comes from `tests/fixtures/`, through
|
|
16
|
+
the `web` fixture in `tests/conftest.py`.
|
|
17
|
+
|
|
18
|
+
## What makes a good change
|
|
19
|
+
|
|
20
|
+
- **Standard library only.** No runtime dependencies, ever. It is the reason people can
|
|
21
|
+
drop this into anything.
|
|
22
|
+
- **Python 3.9 and up.** CI runs 3.9 to 3.13.
|
|
23
|
+
- **One shape.** Every reader returns `openroles.Job` with the same fields. A field only
|
|
24
|
+
one ATS has goes into an existing field or not at all; open an issue first if you think
|
|
25
|
+
the shape needs to change.
|
|
26
|
+
- **Polite, always.** robots.txt is read and obeyed, never worked around. Two requests per
|
|
27
|
+
host at a time. A board that answers 429 or 503 is left alone. These are not up for
|
|
28
|
+
trade-offs.
|
|
29
|
+
- **A test for every fix.** The smallest input that showed the bug, as an invented
|
|
30
|
+
fixture. Never paste a real company's board or a real person's details into a fixture.
|
|
31
|
+
|
|
32
|
+
## Adding an ATS
|
|
33
|
+
|
|
34
|
+
The most useful contribution. A new reader is welcome when the system publishes an
|
|
35
|
+
employer's open roles at an endpoint that is either documented as public or allowed by
|
|
36
|
+
its robots.txt. Wanted, if their boards permit it: Workable, Recruitee, Personio,
|
|
37
|
+
Teamtailor, BambooHR, JazzHR, Breezy.
|
|
38
|
+
|
|
39
|
+
1. Add `src/openroles/providers/<ats>.py` with `fetch(token) -> list[Job]` and
|
|
40
|
+
`exists(token) -> bool`, and check robots.txt first unless the endpoint is documented
|
|
41
|
+
as a public API.
|
|
42
|
+
2. Register it in `providers/__init__.py`, `models.ATS`, and `discover.py` (its board URL
|
|
43
|
+
pattern, so `detect` finds it).
|
|
44
|
+
3. Add an invented fixture and tests: the fields, an unknown board, robots.txt saying no.
|
|
45
|
+
4. Add a row to the README's "Supported systems" table and a line to `CHANGELOG.md`.
|
|
46
|
+
|
|
47
|
+
## Pull requests
|
|
48
|
+
|
|
49
|
+
Small and focused is easiest to review. CI must be green (ruff and the tests on every
|
|
50
|
+
supported Python). Say in the description what you changed and how you checked it
|
|
51
|
+
against a real board, without pasting that board's data.
|
|
52
|
+
|
|
53
|
+
## Releases
|
|
54
|
+
|
|
55
|
+
Maintainers bump `__version__` in `src/openroles/__init__.py`, add the `CHANGELOG.md`
|
|
56
|
+
entry, and publish a GitHub release. The release workflow builds the package and
|
|
57
|
+
publishes it to PyPI.
|
|
58
|
+
|
|
59
|
+
## Conduct
|
|
60
|
+
|
|
61
|
+
Everyone taking part is expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md).
|
openroles-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aran Culleton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
openroles-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: openroles
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Every open role, straight from the employer's own ATS: Greenhouse, Lever, Ashby and Workday, with no key and no dependencies. CLI, Python API and MCP server.
|
|
5
|
+
Project-URL: Homepage, https://github.com/aranculleton/openroles
|
|
6
|
+
Project-URL: Issues, https://github.com/aranculleton/openroles/issues
|
|
7
|
+
Project-URL: Changelog, https://github.com/aranculleton/openroles/blob/main/CHANGELOG.md
|
|
8
|
+
Author: Aran Culleton
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ashby,ats,careers,greenhouse,job-board,job-search,jobs,lever,mcp,workday
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
18
|
+
Classifier: Topic :: Office/Business
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# openroles
|
|
24
|
+
|
|
25
|
+
**Every open role, straight from the employer's own careers system.** Greenhouse, Lever,
|
|
26
|
+
Ashby and Workday, in one shape, from the same public JSON their own careers pages read.
|
|
27
|
+
No API key, no account, no scraping service, and nothing but the Python standard library.
|
|
28
|
+
|
|
29
|
+
[](https://github.com/aranculleton/openroles/actions/workflows/ci.yml)
|
|
30
|
+
[](https://pypi.org/project/openroles/)
|
|
31
|
+
[](https://pypi.org/project/openroles/)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
|
|
34
|
+
```console
|
|
35
|
+
$ pip install openroles
|
|
36
|
+
$ openroles figma --query data --limit 3
|
|
37
|
+
2026-09-23 Data Engineer Intern (2027) San Francisco, CA • New York, NY https://boards.greenhouse.io/figma/jobs/6178851004?gh_jid=6178851004
|
|
38
|
+
2026-09-21 Data Platform Engineer San Francisco, CA • New York, NY • Unite https://boards.greenhouse.io/figma/jobs/6201407004?gh_jid=6201407004
|
|
39
|
+
2026-09-21 PhD Intern, Data Science (2027) San Francisco, CA • New York, NY https://boards.greenhouse.io/figma/jobs/6200626004?gh_jid=6200626004
|
|
40
|
+
|
|
41
|
+
3 open role(s)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Give it a company name, a careers page, a board URL or `ats:token`. It finds the board,
|
|
45
|
+
reads every open role, and gives you the same fields whichever system the employer uses.
|
|
46
|
+
|
|
47
|
+
## Why
|
|
48
|
+
|
|
49
|
+
Job sites miss roles, show them late, and repost them stale. The employer's own board is
|
|
50
|
+
the source: it is complete, current, and carries the real apply link. Every major
|
|
51
|
+
applicant tracking system publishes that board as JSON for its own careers page.
|
|
52
|
+
`openroles` reads it the same way, politely, and puts the four biggest systems behind
|
|
53
|
+
one function.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install openroles # or: uvx openroles <company>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Python 3.9 or newer. No dependencies.
|
|
62
|
+
|
|
63
|
+
## Command line
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
openroles linear # every open role at Linear
|
|
67
|
+
openroles https://jobs.ashbyhq.com/linear # the same, from the board URL
|
|
68
|
+
openroles lever:zoox --query "data" --since 14 # title words, posted in the last 14 days
|
|
69
|
+
openroles figma --location "New York" # filter by place (--remote for remote only)
|
|
70
|
+
openroles https://adobe.wd5.myworkdayjobs.com/external_experienced \
|
|
71
|
+
--query "data scientist" --details # Workday, with each posting's description
|
|
72
|
+
openroles detect "Figma" # which ATS, and the board's token
|
|
73
|
+
openroles linear --json > roles.json # JSON (the default when piped), or --jsonl
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Python
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import openroles
|
|
80
|
+
|
|
81
|
+
openroles.detect("Figma")
|
|
82
|
+
# [Board(ats='greenhouse', token='figma'), Board(ats='ashby', token='figma')]
|
|
83
|
+
|
|
84
|
+
jobs = openroles.fetch("greenhouse:figma", query="data", location="New York")
|
|
85
|
+
for job in jobs:
|
|
86
|
+
print(job.posted, job.title, job.location, job.url)
|
|
87
|
+
|
|
88
|
+
job.to_dict() # plain JSON-ready dict
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`fetch()` raises `openroles.BoardError` when a board cannot be read (a wrong token, no
|
|
92
|
+
answer), and `openroles.RobotsDisallowed` when the host's robots.txt says no.
|
|
93
|
+
|
|
94
|
+
## For AI agents (MCP)
|
|
95
|
+
|
|
96
|
+
`openroles mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server over
|
|
97
|
+
stdio, so Claude, Cursor and other agents can look up any employer's open roles
|
|
98
|
+
themselves. Two tools: `find_job_board` and `list_open_roles` (descriptions left out
|
|
99
|
+
unless asked for, to keep answers short).
|
|
100
|
+
|
|
101
|
+
Claude Code:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
claude mcp add openroles -- uvx openroles mcp
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Claude Desktop, Cursor and other clients (`mcpServers` in their config):
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"mcpServers": {
|
|
112
|
+
"openroles": { "command": "uvx", "args": ["openroles", "mcp"] }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
There is also [`llms.txt`](llms.txt), a short brief for a model that has to use the
|
|
118
|
+
library or the CLI.
|
|
119
|
+
|
|
120
|
+
## What you get
|
|
121
|
+
|
|
122
|
+
Every role, from every system, has the same fields:
|
|
123
|
+
|
|
124
|
+
| Field | |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `id` | stable across runs: `gh-`, `lv-`, `ab-` or `wd-` plus the ATS's own id |
|
|
127
|
+
| `ats`, `board` | where it came from, e.g. `greenhouse`, `figma` |
|
|
128
|
+
| `title`, `url` | the role and its apply page |
|
|
129
|
+
| `location`, `locations` | every location the posting lists, joined and as a list |
|
|
130
|
+
| `remote` | `True`, `False`, or `None` when the board does not say |
|
|
131
|
+
| `posted` | `YYYY-MM-DD`, first published (not last edited) where the ATS says |
|
|
132
|
+
| `department`, `employment_type` | where the board provides them |
|
|
133
|
+
| `compensation` | the posted pay band, where the employer publishes one |
|
|
134
|
+
| `deadline` | where the board provides one |
|
|
135
|
+
| `description` | plain text, HTML removed |
|
|
136
|
+
|
|
137
|
+
## Supported systems
|
|
138
|
+
|
|
139
|
+
| ATS | Requests | Notes |
|
|
140
|
+
|---|---|---|
|
|
141
|
+
| Greenhouse | 1 per board | descriptions included |
|
|
142
|
+
| Lever | 1 per board | descriptions with their requirement lists; EU boards found automatically |
|
|
143
|
+
| Ashby | 1 per board | descriptions and posted pay included |
|
|
144
|
+
| Workday | 1 per 20 roles, plus 1 per description | `--query` searches the board; `--country` narrows it; `--details` fetches descriptions |
|
|
145
|
+
|
|
146
|
+
`detect` finds a board from a company name (Greenhouse, Lever and Ashby, by trying the
|
|
147
|
+
names an employer is likely to use), from any board URL, and from a careers page that
|
|
148
|
+
links to or embeds one. A Workday board cannot be guessed from a name: give its URL.
|
|
149
|
+
|
|
150
|
+
Not supported, because none publishes an open board this way: SuccessFactors, Oracle,
|
|
151
|
+
iCIMS and custom careers sites. SmartRecruiters publishes one, but its robots.txt closes it
|
|
152
|
+
to every crawler except LinkedIn's, so `openroles` does not read it.
|
|
153
|
+
|
|
154
|
+
## Polite by design
|
|
155
|
+
|
|
156
|
+
- **robots.txt is read first** for Ashby, Workday and any careers page, and a refusal is
|
|
157
|
+
honoured, never worked around. For Workday the careers site's own rules count too.
|
|
158
|
+
- **At most two requests in flight per host**, a timeout on every call, and an honest
|
|
159
|
+
`User-Agent` naming this project.
|
|
160
|
+
- **A Workday board that answers 429 or 503 is left alone** for the rest of the run.
|
|
161
|
+
|
|
162
|
+
## Is this allowed?
|
|
163
|
+
|
|
164
|
+
Greenhouse, Lever and Ashby document these endpoints as public job board APIs, for
|
|
165
|
+
exactly this purpose: showing an employer's open roles somewhere else. Workday's is the
|
|
166
|
+
endpoint its own careers pages call; it is not documented, it can change without notice,
|
|
167
|
+
and `openroles` only reads it where robots.txt allows. Use it the way the boards are
|
|
168
|
+
meant to be used: to find roles and apply to them, at a human pace. It is not a tool for
|
|
169
|
+
hammering a site or republishing someone's board wholesale.
|
|
170
|
+
|
|
171
|
+
## Where it came from
|
|
172
|
+
|
|
173
|
+
`openroles` is the employer-board reader from
|
|
174
|
+
[last-job-search](https://github.com/aranculleton/last-job-search), a job search that Claude
|
|
175
|
+
runs end to end, extracted so anyone can use it.
|
|
176
|
+
|
|
177
|
+
## Contributing
|
|
178
|
+
|
|
179
|
+
Contributions are welcome, and the most useful one is a reader for another ATS
|
|
180
|
+
(Workable, Recruitee, Personio, Teamtailor and more are wanted). [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
181
|
+
has the set-up, the few rules that keep the library small and polite, and a checklist
|
|
182
|
+
for adding a system. Bugs and requests go in [Issues](https://github.com/aranculleton/openroles/issues).
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
pip install -e . pytest ruff
|
|
186
|
+
pytest && ruff check . # offline: every answer comes from tests/fixtures/
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Licence
|
|
190
|
+
|
|
191
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# openroles
|
|
2
|
+
|
|
3
|
+
**Every open role, straight from the employer's own careers system.** Greenhouse, Lever,
|
|
4
|
+
Ashby and Workday, in one shape, from the same public JSON their own careers pages read.
|
|
5
|
+
No API key, no account, no scraping service, and nothing but the Python standard library.
|
|
6
|
+
|
|
7
|
+
[](https://github.com/aranculleton/openroles/actions/workflows/ci.yml)
|
|
8
|
+
[](https://pypi.org/project/openroles/)
|
|
9
|
+
[](https://pypi.org/project/openroles/)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
12
|
+
```console
|
|
13
|
+
$ pip install openroles
|
|
14
|
+
$ openroles figma --query data --limit 3
|
|
15
|
+
2026-09-23 Data Engineer Intern (2027) San Francisco, CA • New York, NY https://boards.greenhouse.io/figma/jobs/6178851004?gh_jid=6178851004
|
|
16
|
+
2026-09-21 Data Platform Engineer San Francisco, CA • New York, NY • Unite https://boards.greenhouse.io/figma/jobs/6201407004?gh_jid=6201407004
|
|
17
|
+
2026-09-21 PhD Intern, Data Science (2027) San Francisco, CA • New York, NY https://boards.greenhouse.io/figma/jobs/6200626004?gh_jid=6200626004
|
|
18
|
+
|
|
19
|
+
3 open role(s)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Give it a company name, a careers page, a board URL or `ats:token`. It finds the board,
|
|
23
|
+
reads every open role, and gives you the same fields whichever system the employer uses.
|
|
24
|
+
|
|
25
|
+
## Why
|
|
26
|
+
|
|
27
|
+
Job sites miss roles, show them late, and repost them stale. The employer's own board is
|
|
28
|
+
the source: it is complete, current, and carries the real apply link. Every major
|
|
29
|
+
applicant tracking system publishes that board as JSON for its own careers page.
|
|
30
|
+
`openroles` reads it the same way, politely, and puts the four biggest systems behind
|
|
31
|
+
one function.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install openroles # or: uvx openroles <company>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Python 3.9 or newer. No dependencies.
|
|
40
|
+
|
|
41
|
+
## Command line
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
openroles linear # every open role at Linear
|
|
45
|
+
openroles https://jobs.ashbyhq.com/linear # the same, from the board URL
|
|
46
|
+
openroles lever:zoox --query "data" --since 14 # title words, posted in the last 14 days
|
|
47
|
+
openroles figma --location "New York" # filter by place (--remote for remote only)
|
|
48
|
+
openroles https://adobe.wd5.myworkdayjobs.com/external_experienced \
|
|
49
|
+
--query "data scientist" --details # Workday, with each posting's description
|
|
50
|
+
openroles detect "Figma" # which ATS, and the board's token
|
|
51
|
+
openroles linear --json > roles.json # JSON (the default when piped), or --jsonl
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Python
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import openroles
|
|
58
|
+
|
|
59
|
+
openroles.detect("Figma")
|
|
60
|
+
# [Board(ats='greenhouse', token='figma'), Board(ats='ashby', token='figma')]
|
|
61
|
+
|
|
62
|
+
jobs = openroles.fetch("greenhouse:figma", query="data", location="New York")
|
|
63
|
+
for job in jobs:
|
|
64
|
+
print(job.posted, job.title, job.location, job.url)
|
|
65
|
+
|
|
66
|
+
job.to_dict() # plain JSON-ready dict
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`fetch()` raises `openroles.BoardError` when a board cannot be read (a wrong token, no
|
|
70
|
+
answer), and `openroles.RobotsDisallowed` when the host's robots.txt says no.
|
|
71
|
+
|
|
72
|
+
## For AI agents (MCP)
|
|
73
|
+
|
|
74
|
+
`openroles mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server over
|
|
75
|
+
stdio, so Claude, Cursor and other agents can look up any employer's open roles
|
|
76
|
+
themselves. Two tools: `find_job_board` and `list_open_roles` (descriptions left out
|
|
77
|
+
unless asked for, to keep answers short).
|
|
78
|
+
|
|
79
|
+
Claude Code:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
claude mcp add openroles -- uvx openroles mcp
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Claude Desktop, Cursor and other clients (`mcpServers` in their config):
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"openroles": { "command": "uvx", "args": ["openroles", "mcp"] }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
There is also [`llms.txt`](llms.txt), a short brief for a model that has to use the
|
|
96
|
+
library or the CLI.
|
|
97
|
+
|
|
98
|
+
## What you get
|
|
99
|
+
|
|
100
|
+
Every role, from every system, has the same fields:
|
|
101
|
+
|
|
102
|
+
| Field | |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `id` | stable across runs: `gh-`, `lv-`, `ab-` or `wd-` plus the ATS's own id |
|
|
105
|
+
| `ats`, `board` | where it came from, e.g. `greenhouse`, `figma` |
|
|
106
|
+
| `title`, `url` | the role and its apply page |
|
|
107
|
+
| `location`, `locations` | every location the posting lists, joined and as a list |
|
|
108
|
+
| `remote` | `True`, `False`, or `None` when the board does not say |
|
|
109
|
+
| `posted` | `YYYY-MM-DD`, first published (not last edited) where the ATS says |
|
|
110
|
+
| `department`, `employment_type` | where the board provides them |
|
|
111
|
+
| `compensation` | the posted pay band, where the employer publishes one |
|
|
112
|
+
| `deadline` | where the board provides one |
|
|
113
|
+
| `description` | plain text, HTML removed |
|
|
114
|
+
|
|
115
|
+
## Supported systems
|
|
116
|
+
|
|
117
|
+
| ATS | Requests | Notes |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| Greenhouse | 1 per board | descriptions included |
|
|
120
|
+
| Lever | 1 per board | descriptions with their requirement lists; EU boards found automatically |
|
|
121
|
+
| Ashby | 1 per board | descriptions and posted pay included |
|
|
122
|
+
| Workday | 1 per 20 roles, plus 1 per description | `--query` searches the board; `--country` narrows it; `--details` fetches descriptions |
|
|
123
|
+
|
|
124
|
+
`detect` finds a board from a company name (Greenhouse, Lever and Ashby, by trying the
|
|
125
|
+
names an employer is likely to use), from any board URL, and from a careers page that
|
|
126
|
+
links to or embeds one. A Workday board cannot be guessed from a name: give its URL.
|
|
127
|
+
|
|
128
|
+
Not supported, because none publishes an open board this way: SuccessFactors, Oracle,
|
|
129
|
+
iCIMS and custom careers sites. SmartRecruiters publishes one, but its robots.txt closes it
|
|
130
|
+
to every crawler except LinkedIn's, so `openroles` does not read it.
|
|
131
|
+
|
|
132
|
+
## Polite by design
|
|
133
|
+
|
|
134
|
+
- **robots.txt is read first** for Ashby, Workday and any careers page, and a refusal is
|
|
135
|
+
honoured, never worked around. For Workday the careers site's own rules count too.
|
|
136
|
+
- **At most two requests in flight per host**, a timeout on every call, and an honest
|
|
137
|
+
`User-Agent` naming this project.
|
|
138
|
+
- **A Workday board that answers 429 or 503 is left alone** for the rest of the run.
|
|
139
|
+
|
|
140
|
+
## Is this allowed?
|
|
141
|
+
|
|
142
|
+
Greenhouse, Lever and Ashby document these endpoints as public job board APIs, for
|
|
143
|
+
exactly this purpose: showing an employer's open roles somewhere else. Workday's is the
|
|
144
|
+
endpoint its own careers pages call; it is not documented, it can change without notice,
|
|
145
|
+
and `openroles` only reads it where robots.txt allows. Use it the way the boards are
|
|
146
|
+
meant to be used: to find roles and apply to them, at a human pace. It is not a tool for
|
|
147
|
+
hammering a site or republishing someone's board wholesale.
|
|
148
|
+
|
|
149
|
+
## Where it came from
|
|
150
|
+
|
|
151
|
+
`openroles` is the employer-board reader from
|
|
152
|
+
[last-job-search](https://github.com/aranculleton/last-job-search), a job search that Claude
|
|
153
|
+
runs end to end, extracted so anyone can use it.
|
|
154
|
+
|
|
155
|
+
## Contributing
|
|
156
|
+
|
|
157
|
+
Contributions are welcome, and the most useful one is a reader for another ATS
|
|
158
|
+
(Workable, Recruitee, Personio, Teamtailor and more are wanted). [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
159
|
+
has the set-up, the few rules that keep the library small and polite, and a checklist
|
|
160
|
+
for adding a system. Bugs and requests go in [Issues](https://github.com/aranculleton/openroles/issues).
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
pip install -e . pytest ruff
|
|
164
|
+
pytest && ruff check . # offline: every answer comes from tests/fixtures/
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Licence
|
|
168
|
+
|
|
169
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
Please report a vulnerability privately, through GitHub's **Report a vulnerability**
|
|
4
|
+
button on this repository's Security tab, rather than in a public issue.
|
|
5
|
+
|
|
6
|
+
openroles only reads public job boards and never sends credentials, so the issues that
|
|
7
|
+
matter most are ones that would make it fetch somewhere it should not (a crafted board
|
|
8
|
+
response steering a request to another host), ignore robots.txt, or crash a caller on
|
|
9
|
+
hostile input. A fix for a confirmed report is released as soon as it is ready.
|
openroles-0.1.0/llms.txt
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# openroles
|
|
2
|
+
|
|
3
|
+
> Read an employer's open roles straight from its applicant tracking system (Greenhouse, Lever, Ashby, Workday). No API key. Python standard library only. CLI, Python API and MCP server.
|
|
4
|
+
|
|
5
|
+
## Use it
|
|
6
|
+
|
|
7
|
+
- CLI: `openroles <company | careers URL | board URL | ats:token> [--query WORDS] [--location TEXT] [--remote] [--since DAYS] [--limit N] [--json]`
|
|
8
|
+
- Find the board: `openroles detect <company or URL>` prints `ats:token` lines.
|
|
9
|
+
- Python: `openroles.fetch(target, query=None, location=None, remote=None, since_days=None, limit=None)` returns a list of `Job`; `openroles.detect(target)` returns a list of `Board`.
|
|
10
|
+
- MCP: `openroles mcp` (stdio). Tools: `find_job_board(target)`, `list_open_roles(target, query?, location?, remote_only?, since_days?, limit?, include_descriptions?, country?)`.
|
|
11
|
+
|
|
12
|
+
## Targets
|
|
13
|
+
|
|
14
|
+
- `greenhouse:<token>`, `lever:<token>`, `ashby:<token>`, `workday:<tenant>.wd<N>/<Site>`
|
|
15
|
+
- Board URLs: boards.greenhouse.io/<token>, jobs.lever.co/<token>, jobs.ashbyhq.com/<token>, <tenant>.wd<N>.myworkdayjobs.com/<Site>
|
|
16
|
+
- A careers page URL: the board it links to or embeds is found.
|
|
17
|
+
- A company name: tried on Greenhouse, Lever and Ashby. Workday needs its URL.
|
|
18
|
+
|
|
19
|
+
## Job fields
|
|
20
|
+
|
|
21
|
+
id, ats, board, title, url, location, locations, remote (true/false/null), posted (YYYY-MM-DD), department, employment_type, compensation, deadline, description (plain text).
|
|
22
|
+
|
|
23
|
+
## Notes
|
|
24
|
+
|
|
25
|
+
- `query` words must all appear in the title. Workday also searches the board with them.
|
|
26
|
+
- Workday descriptions cost one request each: pass `details=True` (CLI `--details`) or a query.
|
|
27
|
+
- Errors: `BoardError` (wrong token, no answer), `RobotsDisallowed` (robots.txt says no).
|
|
28
|
+
- Polite: robots.txt honoured, two requests per host at a time, stops on 429/503.
|