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.
Files changed (42) hide show
  1. openroles-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +23 -0
  2. openroles-0.1.0/.github/ISSUE_TEMPLATE/config.yml +1 -0
  3. openroles-0.1.0/.github/ISSUE_TEMPLATE/new_ats.yml +22 -0
  4. openroles-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +9 -0
  5. openroles-0.1.0/.github/dependabot.yml +6 -0
  6. openroles-0.1.0/.github/workflows/ci.yml +34 -0
  7. openroles-0.1.0/.github/workflows/publish.yml +40 -0
  8. openroles-0.1.0/.gitignore +7 -0
  9. openroles-0.1.0/CHANGELOG.md +11 -0
  10. openroles-0.1.0/CODE_OF_CONDUCT.md +9 -0
  11. openroles-0.1.0/CONTRIBUTING.md +61 -0
  12. openroles-0.1.0/LICENSE +21 -0
  13. openroles-0.1.0/PKG-INFO +191 -0
  14. openroles-0.1.0/README.md +169 -0
  15. openroles-0.1.0/SECURITY.md +9 -0
  16. openroles-0.1.0/llms.txt +28 -0
  17. openroles-0.1.0/pyproject.toml +50 -0
  18. openroles-0.1.0/src/openroles/__init__.py +84 -0
  19. openroles-0.1.0/src/openroles/__main__.py +5 -0
  20. openroles-0.1.0/src/openroles/_http.py +130 -0
  21. openroles-0.1.0/src/openroles/cli.py +86 -0
  22. openroles-0.1.0/src/openroles/discover.py +101 -0
  23. openroles-0.1.0/src/openroles/mcp.py +145 -0
  24. openroles-0.1.0/src/openroles/models.py +80 -0
  25. openroles-0.1.0/src/openroles/providers/__init__.py +6 -0
  26. openroles-0.1.0/src/openroles/providers/ashby.py +117 -0
  27. openroles-0.1.0/src/openroles/providers/greenhouse.py +48 -0
  28. openroles-0.1.0/src/openroles/providers/lever.py +100 -0
  29. openroles-0.1.0/src/openroles/providers/workday.py +326 -0
  30. openroles-0.1.0/src/openroles/py.typed +0 -0
  31. openroles-0.1.0/src/openroles/robots.py +68 -0
  32. openroles-0.1.0/tests/__init__.py +0 -0
  33. openroles-0.1.0/tests/conftest.py +42 -0
  34. openroles-0.1.0/tests/fixtures/ashby.json +9 -0
  35. openroles-0.1.0/tests/fixtures/greenhouse.json +8 -0
  36. openroles-0.1.0/tests/fixtures/lever.json +8 -0
  37. openroles-0.1.0/tests/fixtures/workday_job.json +3 -0
  38. openroles-0.1.0/tests/fixtures/workday_list.json +7 -0
  39. openroles-0.1.0/tests/test_discover.py +40 -0
  40. openroles-0.1.0/tests/test_mcp.py +40 -0
  41. openroles-0.1.0/tests/test_providers.py +103 -0
  42. 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,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: monthly
@@ -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,7 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ dist/
6
+ build/
7
+ .venv/
@@ -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).
@@ -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.
@@ -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
+ [![CI](https://github.com/aranculleton/openroles/actions/workflows/ci.yml/badge.svg)](https://github.com/aranculleton/openroles/actions/workflows/ci.yml)
30
+ [![PyPI](https://img.shields.io/pypi/v/openroles)](https://pypi.org/project/openroles/)
31
+ [![Python](https://img.shields.io/pypi/pyversions/openroles)](https://pypi.org/project/openroles/)
32
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](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
+ [![CI](https://github.com/aranculleton/openroles/actions/workflows/ci.yml/badge.svg)](https://github.com/aranculleton/openroles/actions/workflows/ci.yml)
8
+ [![PyPI](https://img.shields.io/pypi/v/openroles)](https://pypi.org/project/openroles/)
9
+ [![Python](https://img.shields.io/pypi/pyversions/openroles)](https://pypi.org/project/openroles/)
10
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](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.
@@ -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.