atsfill 0.0.3.dev16__tar.gz → 0.0.4__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 (79) hide show
  1. atsfill-0.0.4/PKG-INFO +206 -0
  2. atsfill-0.0.4/README.md +177 -0
  3. {atsfill-0.0.3.dev16 → atsfill-0.0.4}/pyproject.toml +11 -4
  4. {atsfill-0.0.3.dev16 → atsfill-0.0.4}/pyproject.toml.orig +12 -5
  5. atsfill-0.0.4/src/atsfill/adapters/__init__.py +84 -0
  6. atsfill-0.0.4/src/atsfill/adapters/adpmyjobs.py +760 -0
  7. {atsfill-0.0.3.dev16 → atsfill-0.0.4}/src/atsfill/adapters/ashby.py +32 -21
  8. atsfill-0.0.4/src/atsfill/adapters/bamboohr.py +481 -0
  9. atsfill-0.0.4/src/atsfill/adapters/breezy.py +403 -0
  10. atsfill-0.0.4/src/atsfill/adapters/dayforce.py +944 -0
  11. atsfill-0.0.4/src/atsfill/adapters/eightfold.py +537 -0
  12. {atsfill-0.0.3.dev16 → atsfill-0.0.4}/src/atsfill/adapters/greenhouse.py +144 -39
  13. atsfill-0.0.4/src/atsfill/adapters/gupy.py +294 -0
  14. atsfill-0.0.4/src/atsfill/adapters/hireology.py +351 -0
  15. atsfill-0.0.4/src/atsfill/adapters/hiringthing.py +326 -0
  16. atsfill-0.0.4/src/atsfill/adapters/hrmdirect.py +493 -0
  17. atsfill-0.0.4/src/atsfill/adapters/isolvedhire.py +613 -0
  18. atsfill-0.0.4/src/atsfill/adapters/jazzhr.py +276 -0
  19. atsfill-0.0.4/src/atsfill/adapters/jobaffinity.py +455 -0
  20. atsfill-0.0.4/src/atsfill/adapters/jobstreet.py +481 -0
  21. atsfill-0.0.4/src/atsfill/adapters/join.py +551 -0
  22. atsfill-0.0.4/src/atsfill/adapters/lever.py +252 -0
  23. atsfill-0.0.4/src/atsfill/adapters/manatal.py +354 -0
  24. atsfill-0.0.4/src/atsfill/adapters/oracle.py +662 -0
  25. atsfill-0.0.4/src/atsfill/adapters/paycor.py +370 -0
  26. atsfill-0.0.4/src/atsfill/adapters/paylocity.py +684 -0
  27. atsfill-0.0.4/src/atsfill/adapters/personio.py +238 -0
  28. atsfill-0.0.4/src/atsfill/adapters/phenom.py +1455 -0
  29. atsfill-0.0.4/src/atsfill/adapters/pinpoint.py +523 -0
  30. atsfill-0.0.4/src/atsfill/adapters/recruitee.py +302 -0
  31. atsfill-0.0.4/src/atsfill/adapters/recruiterflow.py +467 -0
  32. atsfill-0.0.4/src/atsfill/adapters/rippling.py +453 -0
  33. atsfill-0.0.4/src/atsfill/adapters/smartrecruiters.py +489 -0
  34. atsfill-0.0.4/src/atsfill/adapters/successfactors.js +75 -0
  35. atsfill-0.0.4/src/atsfill/adapters/successfactors.py +497 -0
  36. atsfill-0.0.4/src/atsfill/adapters/taleo.py +613 -0
  37. atsfill-0.0.4/src/atsfill/adapters/teamtailor.py +583 -0
  38. atsfill-0.0.4/src/atsfill/adapters/ukg.py +555 -0
  39. atsfill-0.0.4/src/atsfill/adapters/welcomekit.py +366 -0
  40. atsfill-0.0.4/src/atsfill/adapters/workable.py +419 -0
  41. atsfill-0.0.4/src/atsfill/adapters/workatastartup.py +830 -0
  42. atsfill-0.0.4/src/atsfill/adapters/workday.py +1017 -0
  43. {atsfill-0.0.3.dev16 → atsfill-0.0.4}/src/atsfill/adapters/wttj.py +139 -35
  44. atsfill-0.0.4/src/atsfill/adapters/zohorecruit.py +511 -0
  45. atsfill-0.0.4/src/atsfill/cli.py +270 -0
  46. {atsfill-0.0.3.dev16 → atsfill-0.0.4}/src/atsfill/core/__init__.py +1 -1
  47. atsfill-0.0.4/src/atsfill/core/accounts.py +140 -0
  48. atsfill-0.0.4/src/atsfill/core/adapter.py +298 -0
  49. {atsfill-0.0.3.dev16 → atsfill-0.0.4}/src/atsfill/core/attempts.py +8 -7
  50. atsfill-0.0.4/src/atsfill/core/browser.py +356 -0
  51. atsfill-0.0.4/src/atsfill/core/commands.py +876 -0
  52. atsfill-0.0.4/src/atsfill/core/conditions.py +133 -0
  53. atsfill-0.0.4/src/atsfill/core/html.py +71 -0
  54. atsfill-0.0.4/src/atsfill/core/matching.py +161 -0
  55. atsfill-0.0.4/src/atsfill/core/otp.py +109 -0
  56. atsfill-0.0.4/src/atsfill/core/passwords.py +127 -0
  57. atsfill-0.0.4/src/atsfill/core/paths.py +105 -0
  58. atsfill-0.0.4/src/atsfill/core/registry.py +203 -0
  59. atsfill-0.0.4/src/atsfill/core/results.py +150 -0
  60. atsfill-0.0.4/src/atsfill/core/snapshot.py +75 -0
  61. atsfill-0.0.4/src/atsfill/core/telemetry.py +236 -0
  62. atsfill-0.0.4/src/atsfill/registry.toml +430 -0
  63. atsfill-0.0.3.dev16/PKG-INFO +0 -164
  64. atsfill-0.0.3.dev16/README.md +0 -140
  65. atsfill-0.0.3.dev16/src/atsfill/adapters/__init__.py +0 -7
  66. atsfill-0.0.3.dev16/src/atsfill/adapters/teamtailor.py +0 -382
  67. atsfill-0.0.3.dev16/src/atsfill/cli.py +0 -127
  68. atsfill-0.0.3.dev16/src/atsfill/core/accounts.py +0 -42
  69. atsfill-0.0.3.dev16/src/atsfill/core/adapter.py +0 -58
  70. atsfill-0.0.3.dev16/src/atsfill/core/browser.py +0 -229
  71. atsfill-0.0.3.dev16/src/atsfill/core/commands.py +0 -278
  72. atsfill-0.0.3.dev16/src/atsfill/core/matching.py +0 -101
  73. atsfill-0.0.3.dev16/src/atsfill/core/paths.py +0 -40
  74. atsfill-0.0.3.dev16/src/atsfill/core/registry.py +0 -215
  75. atsfill-0.0.3.dev16/src/atsfill/core/results.py +0 -96
  76. atsfill-0.0.3.dev16/src/atsfill/registry.toml +0 -46
  77. {atsfill-0.0.3.dev16 → atsfill-0.0.4}/LICENSE +0 -0
  78. {atsfill-0.0.3.dev16 → atsfill-0.0.4}/src/atsfill/__init__.py +0 -0
  79. {atsfill-0.0.3.dev16 → atsfill-0.0.4}/src/atsfill/py.typed +0 -0
atsfill-0.0.4/PKG-INFO ADDED
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: atsfill
3
+ Version: 0.0.4
4
+ Summary: Read, dry-run and submit job application forms on hiring systems: Ashby, Greenhouse, Teamtailor, Welcome to the Jungle.
5
+ Keywords: job-applications,ats,ashby,greenhouse,teamtailor,ai-agents,cli
6
+ Author: shbernal
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: End Users/Desktop
12
+ Classifier: Operating System :: MacOS
13
+ Classifier: Operating System :: Microsoft :: Windows
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Office/Business
18
+ Classifier: Typing :: Typed
19
+ Requires-Dist: camoufox==0.5.8
20
+ Requires-Dist: filelock==4.1.1
21
+ Requires-Dist: keyring==25.7.0
22
+ Requires-Dist: packaging==26.3
23
+ Requires-Dist: posthog==7.78.0
24
+ Requires-Python: >=3.14
25
+ Project-URL: Homepage, https://github.com/shbernal/atsfill
26
+ Project-URL: Repository, https://github.com/shbernal/atsfill
27
+ Project-URL: Issues, https://github.com/shbernal/atsfill/issues
28
+ Description-Content-Type: text/markdown
29
+
30
+ <div align="center">
31
+
32
+ # atsfill
33
+
34
+ Let your agent apply for you. One CLI for every hiring system.
35
+
36
+ [![PyPI][pypi-badge]][pypi]
37
+ [![Python][python-badge]][pypi]
38
+ [![CI][ci-badge]][ci]
39
+ [![License][license-badge]][license]
40
+
41
+ ---
42
+
43
+ [Install](#install) • [Quickstart](#quickstart) • [How it works](#how-it-works) • [Docs](docs/adr/README.md)
44
+
45
+ ---
46
+
47
+ </div>
48
+
49
+ atsfill reads the application form behind a job posting, fills it with your answers, and checks what the hiring system will receive before anything is sent.
50
+ It is built for agents with a shell: every command prints one JSON document and exits with a code a script can branch on.
51
+
52
+ - `read` lists a form's fields with their ids, types, options and whether they are required.
53
+ - `fill` is a dry run: it fills the form, reads it back field by field, and never submits.
54
+ - `send` fills again and submits only when every answer reads back as given.
55
+ - Submitting is off until you enable it per hiring system, in a config file the agent does not write.
56
+ - A posting already sent is refused unless you pass `--resend`.
57
+
58
+ ## Supported hiring systems
59
+
60
+ <!-- run-history -->
61
+ [![oracle run history](https://shbernal.github.io/atsfill/run-history/oracle.svg)](https://shbernal.github.io/atsfill/run-history/)
62
+ [![smartrecruiters run history](https://shbernal.github.io/atsfill/run-history/smartrecruiters.svg)](https://shbernal.github.io/atsfill/run-history/)
63
+ [![greenhouse run history](https://shbernal.github.io/atsfill/run-history/greenhouse.svg)](https://shbernal.github.io/atsfill/run-history/)
64
+ [![manatal run history](https://shbernal.github.io/atsfill/run-history/manatal.svg)](https://shbernal.github.io/atsfill/run-history/)
65
+ [![paylocity run history](https://shbernal.github.io/atsfill/run-history/paylocity.svg)](https://shbernal.github.io/atsfill/run-history/)
66
+ [![dayforce run history](https://shbernal.github.io/atsfill/run-history/dayforce.svg)](https://shbernal.github.io/atsfill/run-history/)
67
+ [![workable run history](https://shbernal.github.io/atsfill/run-history/workable.svg)](https://shbernal.github.io/atsfill/run-history/)
68
+ [![ashby run history](https://shbernal.github.io/atsfill/run-history/ashby.svg)](https://shbernal.github.io/atsfill/run-history/)
69
+ [![bamboohr run history](https://shbernal.github.io/atsfill/run-history/bamboohr.svg)](https://shbernal.github.io/atsfill/run-history/)
70
+ [![teamtailor run history](https://shbernal.github.io/atsfill/run-history/teamtailor.svg)](https://shbernal.github.io/atsfill/run-history/)
71
+ [![jazzhr run history](https://shbernal.github.io/atsfill/run-history/jazzhr.svg)](https://shbernal.github.io/atsfill/run-history/)
72
+ [![lever run history](https://shbernal.github.io/atsfill/run-history/lever.svg)](https://shbernal.github.io/atsfill/run-history/)
73
+ [![hireology run history](https://shbernal.github.io/atsfill/run-history/hireology.svg)](https://shbernal.github.io/atsfill/run-history/)
74
+ [![paycor run history](https://shbernal.github.io/atsfill/run-history/paycor.svg)](https://shbernal.github.io/atsfill/run-history/)
75
+ [![zohorecruit run history](https://shbernal.github.io/atsfill/run-history/zohorecruit.svg)](https://shbernal.github.io/atsfill/run-history/)
76
+ [![phenom run history](https://shbernal.github.io/atsfill/run-history/phenom.svg)](https://shbernal.github.io/atsfill/run-history/)
77
+ [![breezy run history](https://shbernal.github.io/atsfill/run-history/breezy.svg)](https://shbernal.github.io/atsfill/run-history/)
78
+ [![recruitee run history](https://shbernal.github.io/atsfill/run-history/recruitee.svg)](https://shbernal.github.io/atsfill/run-history/)
79
+ [![rippling run history](https://shbernal.github.io/atsfill/run-history/rippling.svg)](https://shbernal.github.io/atsfill/run-history/)
80
+ [![personio run history](https://shbernal.github.io/atsfill/run-history/personio.svg)](https://shbernal.github.io/atsfill/run-history/)
81
+ [![eightfold run history](https://shbernal.github.io/atsfill/run-history/eightfold.svg)](https://shbernal.github.io/atsfill/run-history/)
82
+ [![pinpoint run history](https://shbernal.github.io/atsfill/run-history/pinpoint.svg)](https://shbernal.github.io/atsfill/run-history/)
83
+ [![hiringthing run history](https://shbernal.github.io/atsfill/run-history/hiringthing.svg)](https://shbernal.github.io/atsfill/run-history/)
84
+ [![recruiterflow run history](https://shbernal.github.io/atsfill/run-history/recruiterflow.svg)](https://shbernal.github.io/atsfill/run-history/)
85
+ [![jobaffinity run history](https://shbernal.github.io/atsfill/run-history/jobaffinity.svg)](https://shbernal.github.io/atsfill/run-history/)
86
+ [![welcomekit run history](https://shbernal.github.io/atsfill/run-history/welcomekit.svg)](https://shbernal.github.io/atsfill/run-history/)
87
+ [![join run history](https://shbernal.github.io/atsfill/run-history/join.svg)](https://shbernal.github.io/atsfill/run-history/)
88
+ [![workatastartup run history](https://shbernal.github.io/atsfill/run-history/workatastartup.svg)](https://shbernal.github.io/atsfill/run-history/)
89
+ [![wttj run history](https://shbernal.github.io/atsfill/run-history/wttj.svg)](https://shbernal.github.io/atsfill/run-history/)
90
+ [![workday run history](https://shbernal.github.io/atsfill/run-history/workday.svg)](https://shbernal.github.io/atsfill/run-history/)
91
+ [![successfactors run history](https://shbernal.github.io/atsfill/run-history/successfactors.svg)](https://shbernal.github.io/atsfill/run-history/)
92
+ [![isolvedhire run history](https://shbernal.github.io/atsfill/run-history/isolvedhire.svg)](https://shbernal.github.io/atsfill/run-history/)
93
+ [![jobstreet run history](https://shbernal.github.io/atsfill/run-history/jobstreet.svg)](https://shbernal.github.io/atsfill/run-history/)
94
+ [![gupy run history](https://shbernal.github.io/atsfill/run-history/gupy.svg)](https://shbernal.github.io/atsfill/run-history/)
95
+ [![ukg run history](https://shbernal.github.io/atsfill/run-history/ukg.svg)](https://shbernal.github.io/atsfill/run-history/)
96
+ [![adpmyjobs run history](https://shbernal.github.io/atsfill/run-history/adpmyjobs.svg)](https://shbernal.github.io/atsfill/run-history/)
97
+ <!-- /run-history -->
98
+
99
+ Each line is a hiring system atsfill has an adapter for, and its [run history](https://shbernal.github.io/atsfill/run-history/): the last 30 days of runs, green when a form filled or an application went through, red when it failed.
100
+ [The systems](docs/adapters/README.md) lists which ones need an account, to read the form or only to send it.
101
+ Any other system returns `unsupported`, so the consumer can fall back to driving a browser itself.
102
+
103
+ ## Why atsfill?
104
+
105
+ An agent applying through a generic browser carries each system's quirks as prose, rereads it every run, and checks the form before submitting only because an instruction says so.
106
+ atsfill moves the quirks into one adapter per system and makes the check code: a `send` whose read-back differs from the answers is refused.
107
+
108
+ | | Generic browser agent | atsfill |
109
+ | --- | --- | --- |
110
+ | System quirks | prose in the prompt | an adapter per system |
111
+ | Check before submit | an instruction | the read-back must match |
112
+ | Permission to submit | whatever the agent decides | a config file, per system |
113
+ | Double send | possible | refused without `--resend` |
114
+ | Output | page snapshots | one JSON result |
115
+
116
+ ## Install
117
+
118
+ atsfill runs on Linux, macOS and Windows with Python 3.14, which `uvx` fetches if needed.
119
+
120
+ ```sh
121
+ uvx atsfill install-browser
122
+ ```
123
+
124
+ This downloads the Camoufox browser build atsfill is pinned to: 1.3 GB, about 2.5 GB once unpacked.
125
+
126
+ Every push to `main` that passes CI is also published as a dev release; `uvx --prerelease allow atsfill` runs the newest.
127
+
128
+ ## Quickstart
129
+
130
+ 1. List the form's fields:
131
+
132
+ ```sh
133
+ uvx atsfill read https://job-boards.greenhouse.io/acme/jobs/1234
134
+ ```
135
+
136
+ 2. Write your answers to `answers.json`, keyed by field id. Files take an absolute path:
137
+
138
+ ```json
139
+ {
140
+ "first_name": "Ada",
141
+ "last_name": "Lovelace",
142
+ "email": "ada@example.com",
143
+ "resume": "/home/ada/resume.pdf"
144
+ }
145
+ ```
146
+
147
+ 3. Dry-run the fill. It ends `ready` when every answer reads back as given:
148
+
149
+ ```sh
150
+ uvx atsfill fill https://job-boards.greenhouse.io/acme/jobs/1234 --answers answers.json
151
+ ```
152
+
153
+ 4. Allow the system to submit in `~/.config/atsfill/config.toml`:
154
+
155
+ ```toml
156
+ submit-allowed = ["greenhouse"]
157
+ ```
158
+
159
+ 5. Send. A `submitted` result carries the evidence of the send:
160
+
161
+ ```sh
162
+ uvx atsfill send https://job-boards.greenhouse.io/acme/jobs/1234 --answers answers.json
163
+ ```
164
+
165
+ A dry run is not invisible: some systems save answers or the resume as they are typed.
166
+ `atsfill status` lists each system's side effects, whether it is allowed to submit, and its scope: [the systems](docs/adapters/README.md).
167
+
168
+ One home can send for more than one user: `--user <name>` gives each their own sessions, double-send guard and `[users.<name>]` section in the config file ([ADR 0015](docs/adr/0015-users-and-accounts.md)).
169
+ Given an account's email (`atsfill login <adapter> --email`) and a password source (a `[password-commands]` entry such as `rbw get example.com {email}`, or `--password -` on stdin), atsfill signs in by itself, and again whenever a form command finds the session signed out.
170
+ When a system emails an OTP, on a sign-in, a sign-up or a form, atsfill gets it from the user's `[otp-commands]` entry, a command that reads the mailbox (`{email}` and `{since}` are filled in), and types it in; without one the command ends `blocked: no_otp_command`.
171
+ Where a system's sign-up allows it, `atsfill login <adapter> --create --email <address>` (with `--details <file.json>` where the sign-up asks the user's name, phone or address) creates the account with a password atsfill generates and keeps in the system's secret store, never printed; creating an account is a side effect, never part of `fill` or `send`.
172
+ Where a system applies only from a complete account profile, `atsfill profile <adapter>` reads its profile form and `atsfill profile <adapter> --answers <file.json>` saves it, step by step, skipping the steps that already match; a form command that meets an incomplete profile ends `blocked: profile_incomplete`, naming that command. Saving a profile is a side effect, never part of `fill` or `send` ([ADR 0017](docs/adr/0017-account-profiles.md)).
173
+
174
+ ## How it works
175
+
176
+ ```mermaid
177
+ flowchart LR
178
+ CLI[atsfill CLI] --> Core
179
+ Core -->|detect, read, fill, read back, submit| Adapter[adapter for the system]
180
+ Adapter --> Browser[headless Camoufox]
181
+ Core --> SubmitGate{read-back matches,<br>submit enabled,<br>not already sent}
182
+ SubmitGate -->|yes| Submit[submit]
183
+ SubmitGate -->|no| Refused[refused]
184
+ ```
185
+
186
+ Each command launches its own headless Camoufox, a Firefox build patched to hide automation, with a fresh context and one saved device identity.
187
+ The core owns every rule about when a send is allowed; an adapter only knows its system's page.
188
+
189
+ Every result has a `status` (`ready`, `refused`, `submitted`, `blocked`, `unknown`, `unsupported`) and, when it is not clean, a `reason` code.
190
+ The exit code is 0 for `ready` and `submitted`, 1 otherwise, 2 for a usage error.
191
+
192
+ Each `read`, `fill` and `send` sends one anonymous event (the system, the command, the status, a hash of the posting, never answers or URLs); `atsfill report` sends a failed run's URL and page, scrubbed of your answers, when you ask: [what is sent and how to turn it off](docs/telemetry.md).
193
+
194
+ ## More
195
+
196
+ - [Architecture decisions](docs/adr/README.md): the contract, the submit gate, the browser, sessions.
197
+ - [Hiring systems](docs/adapters/README.md): every system mapped so far, in backlog order, and each one's form as observed.
198
+ - `atsfill --help` for every command and option.
199
+
200
+ [pypi]: https://pypi.org/project/atsfill/
201
+ [pypi-badge]: https://img.shields.io/pypi/v/atsfill?style=for-the-badge&color=7aa2f7&logo=pypi&logoColor=white
202
+ [python-badge]: https://img.shields.io/pypi/pyversions/atsfill?style=for-the-badge&color=9ece6a&logo=python&logoColor=white
203
+ [ci]: https://github.com/shbernal/atsfill/actions/workflows/ci.yml
204
+ [ci-badge]: https://img.shields.io/github/actions/workflow/status/shbernal/atsfill/ci.yml?branch=main&style=for-the-badge&color=bb9af7&logo=githubactions&logoColor=white
205
+ [license]: LICENSE
206
+ [license-badge]: https://img.shields.io/github/license/shbernal/atsfill?style=for-the-badge&color=e0af68
@@ -0,0 +1,177 @@
1
+ <div align="center">
2
+
3
+ # atsfill
4
+
5
+ Let your agent apply for you. One CLI for every hiring system.
6
+
7
+ [![PyPI][pypi-badge]][pypi]
8
+ [![Python][python-badge]][pypi]
9
+ [![CI][ci-badge]][ci]
10
+ [![License][license-badge]][license]
11
+
12
+ ---
13
+
14
+ [Install](#install) • [Quickstart](#quickstart) • [How it works](#how-it-works) • [Docs](docs/adr/README.md)
15
+
16
+ ---
17
+
18
+ </div>
19
+
20
+ atsfill reads the application form behind a job posting, fills it with your answers, and checks what the hiring system will receive before anything is sent.
21
+ It is built for agents with a shell: every command prints one JSON document and exits with a code a script can branch on.
22
+
23
+ - `read` lists a form's fields with their ids, types, options and whether they are required.
24
+ - `fill` is a dry run: it fills the form, reads it back field by field, and never submits.
25
+ - `send` fills again and submits only when every answer reads back as given.
26
+ - Submitting is off until you enable it per hiring system, in a config file the agent does not write.
27
+ - A posting already sent is refused unless you pass `--resend`.
28
+
29
+ ## Supported hiring systems
30
+
31
+ <!-- run-history -->
32
+ [![oracle run history](https://shbernal.github.io/atsfill/run-history/oracle.svg)](https://shbernal.github.io/atsfill/run-history/)
33
+ [![smartrecruiters run history](https://shbernal.github.io/atsfill/run-history/smartrecruiters.svg)](https://shbernal.github.io/atsfill/run-history/)
34
+ [![greenhouse run history](https://shbernal.github.io/atsfill/run-history/greenhouse.svg)](https://shbernal.github.io/atsfill/run-history/)
35
+ [![manatal run history](https://shbernal.github.io/atsfill/run-history/manatal.svg)](https://shbernal.github.io/atsfill/run-history/)
36
+ [![paylocity run history](https://shbernal.github.io/atsfill/run-history/paylocity.svg)](https://shbernal.github.io/atsfill/run-history/)
37
+ [![dayforce run history](https://shbernal.github.io/atsfill/run-history/dayforce.svg)](https://shbernal.github.io/atsfill/run-history/)
38
+ [![workable run history](https://shbernal.github.io/atsfill/run-history/workable.svg)](https://shbernal.github.io/atsfill/run-history/)
39
+ [![ashby run history](https://shbernal.github.io/atsfill/run-history/ashby.svg)](https://shbernal.github.io/atsfill/run-history/)
40
+ [![bamboohr run history](https://shbernal.github.io/atsfill/run-history/bamboohr.svg)](https://shbernal.github.io/atsfill/run-history/)
41
+ [![teamtailor run history](https://shbernal.github.io/atsfill/run-history/teamtailor.svg)](https://shbernal.github.io/atsfill/run-history/)
42
+ [![jazzhr run history](https://shbernal.github.io/atsfill/run-history/jazzhr.svg)](https://shbernal.github.io/atsfill/run-history/)
43
+ [![lever run history](https://shbernal.github.io/atsfill/run-history/lever.svg)](https://shbernal.github.io/atsfill/run-history/)
44
+ [![hireology run history](https://shbernal.github.io/atsfill/run-history/hireology.svg)](https://shbernal.github.io/atsfill/run-history/)
45
+ [![paycor run history](https://shbernal.github.io/atsfill/run-history/paycor.svg)](https://shbernal.github.io/atsfill/run-history/)
46
+ [![zohorecruit run history](https://shbernal.github.io/atsfill/run-history/zohorecruit.svg)](https://shbernal.github.io/atsfill/run-history/)
47
+ [![phenom run history](https://shbernal.github.io/atsfill/run-history/phenom.svg)](https://shbernal.github.io/atsfill/run-history/)
48
+ [![breezy run history](https://shbernal.github.io/atsfill/run-history/breezy.svg)](https://shbernal.github.io/atsfill/run-history/)
49
+ [![recruitee run history](https://shbernal.github.io/atsfill/run-history/recruitee.svg)](https://shbernal.github.io/atsfill/run-history/)
50
+ [![rippling run history](https://shbernal.github.io/atsfill/run-history/rippling.svg)](https://shbernal.github.io/atsfill/run-history/)
51
+ [![personio run history](https://shbernal.github.io/atsfill/run-history/personio.svg)](https://shbernal.github.io/atsfill/run-history/)
52
+ [![eightfold run history](https://shbernal.github.io/atsfill/run-history/eightfold.svg)](https://shbernal.github.io/atsfill/run-history/)
53
+ [![pinpoint run history](https://shbernal.github.io/atsfill/run-history/pinpoint.svg)](https://shbernal.github.io/atsfill/run-history/)
54
+ [![hiringthing run history](https://shbernal.github.io/atsfill/run-history/hiringthing.svg)](https://shbernal.github.io/atsfill/run-history/)
55
+ [![recruiterflow run history](https://shbernal.github.io/atsfill/run-history/recruiterflow.svg)](https://shbernal.github.io/atsfill/run-history/)
56
+ [![jobaffinity run history](https://shbernal.github.io/atsfill/run-history/jobaffinity.svg)](https://shbernal.github.io/atsfill/run-history/)
57
+ [![welcomekit run history](https://shbernal.github.io/atsfill/run-history/welcomekit.svg)](https://shbernal.github.io/atsfill/run-history/)
58
+ [![join run history](https://shbernal.github.io/atsfill/run-history/join.svg)](https://shbernal.github.io/atsfill/run-history/)
59
+ [![workatastartup run history](https://shbernal.github.io/atsfill/run-history/workatastartup.svg)](https://shbernal.github.io/atsfill/run-history/)
60
+ [![wttj run history](https://shbernal.github.io/atsfill/run-history/wttj.svg)](https://shbernal.github.io/atsfill/run-history/)
61
+ [![workday run history](https://shbernal.github.io/atsfill/run-history/workday.svg)](https://shbernal.github.io/atsfill/run-history/)
62
+ [![successfactors run history](https://shbernal.github.io/atsfill/run-history/successfactors.svg)](https://shbernal.github.io/atsfill/run-history/)
63
+ [![isolvedhire run history](https://shbernal.github.io/atsfill/run-history/isolvedhire.svg)](https://shbernal.github.io/atsfill/run-history/)
64
+ [![jobstreet run history](https://shbernal.github.io/atsfill/run-history/jobstreet.svg)](https://shbernal.github.io/atsfill/run-history/)
65
+ [![gupy run history](https://shbernal.github.io/atsfill/run-history/gupy.svg)](https://shbernal.github.io/atsfill/run-history/)
66
+ [![ukg run history](https://shbernal.github.io/atsfill/run-history/ukg.svg)](https://shbernal.github.io/atsfill/run-history/)
67
+ [![adpmyjobs run history](https://shbernal.github.io/atsfill/run-history/adpmyjobs.svg)](https://shbernal.github.io/atsfill/run-history/)
68
+ <!-- /run-history -->
69
+
70
+ Each line is a hiring system atsfill has an adapter for, and its [run history](https://shbernal.github.io/atsfill/run-history/): the last 30 days of runs, green when a form filled or an application went through, red when it failed.
71
+ [The systems](docs/adapters/README.md) lists which ones need an account, to read the form or only to send it.
72
+ Any other system returns `unsupported`, so the consumer can fall back to driving a browser itself.
73
+
74
+ ## Why atsfill?
75
+
76
+ An agent applying through a generic browser carries each system's quirks as prose, rereads it every run, and checks the form before submitting only because an instruction says so.
77
+ atsfill moves the quirks into one adapter per system and makes the check code: a `send` whose read-back differs from the answers is refused.
78
+
79
+ | | Generic browser agent | atsfill |
80
+ | --- | --- | --- |
81
+ | System quirks | prose in the prompt | an adapter per system |
82
+ | Check before submit | an instruction | the read-back must match |
83
+ | Permission to submit | whatever the agent decides | a config file, per system |
84
+ | Double send | possible | refused without `--resend` |
85
+ | Output | page snapshots | one JSON result |
86
+
87
+ ## Install
88
+
89
+ atsfill runs on Linux, macOS and Windows with Python 3.14, which `uvx` fetches if needed.
90
+
91
+ ```sh
92
+ uvx atsfill install-browser
93
+ ```
94
+
95
+ This downloads the Camoufox browser build atsfill is pinned to: 1.3 GB, about 2.5 GB once unpacked.
96
+
97
+ Every push to `main` that passes CI is also published as a dev release; `uvx --prerelease allow atsfill` runs the newest.
98
+
99
+ ## Quickstart
100
+
101
+ 1. List the form's fields:
102
+
103
+ ```sh
104
+ uvx atsfill read https://job-boards.greenhouse.io/acme/jobs/1234
105
+ ```
106
+
107
+ 2. Write your answers to `answers.json`, keyed by field id. Files take an absolute path:
108
+
109
+ ```json
110
+ {
111
+ "first_name": "Ada",
112
+ "last_name": "Lovelace",
113
+ "email": "ada@example.com",
114
+ "resume": "/home/ada/resume.pdf"
115
+ }
116
+ ```
117
+
118
+ 3. Dry-run the fill. It ends `ready` when every answer reads back as given:
119
+
120
+ ```sh
121
+ uvx atsfill fill https://job-boards.greenhouse.io/acme/jobs/1234 --answers answers.json
122
+ ```
123
+
124
+ 4. Allow the system to submit in `~/.config/atsfill/config.toml`:
125
+
126
+ ```toml
127
+ submit-allowed = ["greenhouse"]
128
+ ```
129
+
130
+ 5. Send. A `submitted` result carries the evidence of the send:
131
+
132
+ ```sh
133
+ uvx atsfill send https://job-boards.greenhouse.io/acme/jobs/1234 --answers answers.json
134
+ ```
135
+
136
+ A dry run is not invisible: some systems save answers or the resume as they are typed.
137
+ `atsfill status` lists each system's side effects, whether it is allowed to submit, and its scope: [the systems](docs/adapters/README.md).
138
+
139
+ One home can send for more than one user: `--user <name>` gives each their own sessions, double-send guard and `[users.<name>]` section in the config file ([ADR 0015](docs/adr/0015-users-and-accounts.md)).
140
+ Given an account's email (`atsfill login <adapter> --email`) and a password source (a `[password-commands]` entry such as `rbw get example.com {email}`, or `--password -` on stdin), atsfill signs in by itself, and again whenever a form command finds the session signed out.
141
+ When a system emails an OTP, on a sign-in, a sign-up or a form, atsfill gets it from the user's `[otp-commands]` entry, a command that reads the mailbox (`{email}` and `{since}` are filled in), and types it in; without one the command ends `blocked: no_otp_command`.
142
+ Where a system's sign-up allows it, `atsfill login <adapter> --create --email <address>` (with `--details <file.json>` where the sign-up asks the user's name, phone or address) creates the account with a password atsfill generates and keeps in the system's secret store, never printed; creating an account is a side effect, never part of `fill` or `send`.
143
+ Where a system applies only from a complete account profile, `atsfill profile <adapter>` reads its profile form and `atsfill profile <adapter> --answers <file.json>` saves it, step by step, skipping the steps that already match; a form command that meets an incomplete profile ends `blocked: profile_incomplete`, naming that command. Saving a profile is a side effect, never part of `fill` or `send` ([ADR 0017](docs/adr/0017-account-profiles.md)).
144
+
145
+ ## How it works
146
+
147
+ ```mermaid
148
+ flowchart LR
149
+ CLI[atsfill CLI] --> Core
150
+ Core -->|detect, read, fill, read back, submit| Adapter[adapter for the system]
151
+ Adapter --> Browser[headless Camoufox]
152
+ Core --> SubmitGate{read-back matches,<br>submit enabled,<br>not already sent}
153
+ SubmitGate -->|yes| Submit[submit]
154
+ SubmitGate -->|no| Refused[refused]
155
+ ```
156
+
157
+ Each command launches its own headless Camoufox, a Firefox build patched to hide automation, with a fresh context and one saved device identity.
158
+ The core owns every rule about when a send is allowed; an adapter only knows its system's page.
159
+
160
+ Every result has a `status` (`ready`, `refused`, `submitted`, `blocked`, `unknown`, `unsupported`) and, when it is not clean, a `reason` code.
161
+ The exit code is 0 for `ready` and `submitted`, 1 otherwise, 2 for a usage error.
162
+
163
+ Each `read`, `fill` and `send` sends one anonymous event (the system, the command, the status, a hash of the posting, never answers or URLs); `atsfill report` sends a failed run's URL and page, scrubbed of your answers, when you ask: [what is sent and how to turn it off](docs/telemetry.md).
164
+
165
+ ## More
166
+
167
+ - [Architecture decisions](docs/adr/README.md): the contract, the submit gate, the browser, sessions.
168
+ - [Hiring systems](docs/adapters/README.md): every system mapped so far, in backlog order, and each one's form as observed.
169
+ - `atsfill --help` for every command and option.
170
+
171
+ [pypi]: https://pypi.org/project/atsfill/
172
+ [pypi-badge]: https://img.shields.io/pypi/v/atsfill?style=for-the-badge&color=7aa2f7&logo=pypi&logoColor=white
173
+ [python-badge]: https://img.shields.io/pypi/pyversions/atsfill?style=for-the-badge&color=9ece6a&logo=python&logoColor=white
174
+ [ci]: https://github.com/shbernal/atsfill/actions/workflows/ci.yml
175
+ [ci-badge]: https://img.shields.io/github/actions/workflow/status/shbernal/atsfill/ci.yml?branch=main&style=for-the-badge&color=bb9af7&logo=githubactions&logoColor=white
176
+ [license]: LICENSE
177
+ [license-badge]: https://img.shields.io/github/license/shbernal/atsfill?style=for-the-badge&color=e0af68
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atsfill"
3
- version = "0.0.3.dev16"
3
+ version = "0.0.4"
4
4
  description = "Read, dry-run and submit job application forms on hiring systems: Ashby, Greenhouse, Teamtailor, Welcome to the Jungle."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -20,13 +20,20 @@ classifiers = [
20
20
  "Environment :: Console",
21
21
  "Intended Audience :: End Users/Desktop",
22
22
  "Operating System :: MacOS",
23
+ "Operating System :: Microsoft :: Windows",
23
24
  "Operating System :: POSIX :: Linux",
24
25
  "Programming Language :: Python :: 3",
25
26
  "Programming Language :: Python :: 3.14",
26
27
  "Topic :: Office/Business",
27
28
  "Typing :: Typed",
28
29
  ]
29
- dependencies = ["camoufox==0.5.8"]
30
+ dependencies = [
31
+ "camoufox==0.5.8",
32
+ "filelock==4.1.1",
33
+ "keyring==25.7.0",
34
+ "packaging==26.3",
35
+ "posthog==7.78.0",
36
+ ]
30
37
 
31
38
  [[project.authors]]
32
39
  name = "shbernal"
@@ -44,6 +51,8 @@ requires = ["uv_build>=0.13.0,<0.14.0"]
44
51
  build-backend = "uv_build"
45
52
 
46
53
  [dependency-groups]
54
+ docs = ["zensical==0.0.73"]
55
+ commit-check = ["commit-check==2.19.0"]
47
56
  dev = [
48
57
  "pytest>=9.1.1",
49
58
  "pytest-xdist>=3.8.0",
@@ -67,8 +76,6 @@ select = [
67
76
 
68
77
  [tool.pytest.ini_options]
69
78
  testpaths = ["tests"]
70
- markers = ["live: drives a real hiring system page with data from .env and .tmp/; opt-in with -m live"]
71
- addopts = "-m 'not live' -n 8"
72
79
 
73
80
  [[tool.uv.index]]
74
81
  name = "testpypi"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atsfill"
3
- version = "0.0.3.dev16"
3
+ version = "0.0.4"
4
4
  description = "Read, dry-run and submit job application forms on hiring systems: Ashby, Greenhouse, Teamtailor, Welcome to the Jungle."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -13,6 +13,7 @@ classifiers = [
13
13
  "Environment :: Console",
14
14
  "Intended Audience :: End Users/Desktop",
15
15
  "Operating System :: MacOS",
16
+ "Operating System :: Microsoft :: Windows",
16
17
  "Operating System :: POSIX :: Linux",
17
18
  "Programming Language :: Python :: 3",
18
19
  "Programming Language :: Python :: 3.14",
@@ -21,6 +22,10 @@ classifiers = [
21
22
  ]
22
23
  dependencies = [
23
24
  "camoufox==0.5.8",
25
+ "filelock==4.1.1",
26
+ "keyring==25.7.0",
27
+ "packaging==26.3",
28
+ "posthog==7.78.0",
24
29
  ]
25
30
 
26
31
  [project.urls]
@@ -36,6 +41,12 @@ requires = ["uv_build>=0.13.0,<0.14.0"]
36
41
  build-backend = "uv_build"
37
42
 
38
43
  [dependency-groups]
44
+ docs = [
45
+ "zensical==0.0.73",
46
+ ]
47
+ commit-check = [
48
+ "commit-check==2.19.0",
49
+ ]
39
50
  dev = [
40
51
  "pytest>=9.1.1",
41
52
  "pytest-xdist>=3.8.0",
@@ -52,10 +63,6 @@ select = ["E", "F", "I", "UP", "B", "SIM"]
52
63
 
53
64
  [tool.pytest.ini_options]
54
65
  testpaths = ["tests"]
55
- markers = [
56
- "live: drives a real hiring system page with data from .env and .tmp/; opt-in with -m live",
57
- ]
58
- addopts = "-m 'not live' -n 8"
59
66
 
60
67
  [[tool.uv.index]]
61
68
  name = "testpypi"
@@ -0,0 +1,84 @@
1
+ from atsfill.adapters.adpmyjobs import ADPMyJobs
2
+ from atsfill.adapters.ashby import Ashby
3
+ from atsfill.adapters.bamboohr import BambooHR
4
+ from atsfill.adapters.breezy import Breezy
5
+ from atsfill.adapters.dayforce import Dayforce
6
+ from atsfill.adapters.eightfold import Eightfold
7
+ from atsfill.adapters.greenhouse import Greenhouse
8
+ from atsfill.adapters.gupy import Gupy
9
+ from atsfill.adapters.hireology import Hireology
10
+ from atsfill.adapters.hiringthing import HiringThing
11
+ from atsfill.adapters.isolvedhire import IsolvedHire
12
+ from atsfill.adapters.jazzhr import JazzHR
13
+ from atsfill.adapters.jobaffinity import JobAffinity
14
+ from atsfill.adapters.jobstreet import Jobstreet
15
+ from atsfill.adapters.join import Join
16
+ from atsfill.adapters.lever import Lever
17
+ from atsfill.adapters.manatal import Manatal
18
+ from atsfill.adapters.oracle import Oracle
19
+ from atsfill.adapters.paycor import Paycor
20
+ from atsfill.adapters.paylocity import Paylocity
21
+ from atsfill.adapters.personio import Personio
22
+ from atsfill.adapters.phenom import Phenom
23
+ from atsfill.adapters.pinpoint import Pinpoint
24
+ from atsfill.adapters.recruitee import Recruitee
25
+ from atsfill.adapters.recruiterflow import Recruiterflow
26
+ from atsfill.adapters.rippling import Rippling
27
+ from atsfill.adapters.smartrecruiters import SmartRecruiters
28
+ from atsfill.adapters.successfactors import SuccessFactors
29
+ from atsfill.adapters.teamtailor import Teamtailor
30
+ from atsfill.adapters.ukg import UKG
31
+ from atsfill.adapters.welcomekit import WelcomeKit
32
+ from atsfill.adapters.workable import Workable
33
+ from atsfill.adapters.workatastartup import WorkAtAStartup
34
+ from atsfill.adapters.workday import Workday
35
+ from atsfill.adapters.wttj import WelcomeToTheJungle
36
+ from atsfill.adapters.zohorecruit import ZohoRecruit
37
+ from atsfill.core.adapter import Adapter, Filtered
38
+
39
+
40
+ def adapters() -> list[Adapter]:
41
+ """One fresh instance of each adapter: a command's adapters never see another posting."""
42
+ return [
43
+ Ashby(),
44
+ Teamtailor(),
45
+ Greenhouse(),
46
+ WelcomeToTheJungle(),
47
+ WorkAtAStartup(),
48
+ Join(),
49
+ JobAffinity(),
50
+ WelcomeKit(),
51
+ SmartRecruiters(),
52
+ Oracle(),
53
+ Manatal(),
54
+ Paylocity(),
55
+ Workable(),
56
+ Hireology(),
57
+ Paycor(),
58
+ JazzHR(),
59
+ Lever(),
60
+ Dayforce(),
61
+ Recruitee(),
62
+ Recruiterflow(),
63
+ Breezy(),
64
+ BambooHR(),
65
+ ZohoRecruit(),
66
+ Personio(),
67
+ Rippling(),
68
+ Pinpoint(),
69
+ HiringThing(),
70
+ Eightfold(),
71
+ Workday(),
72
+ Gupy(),
73
+ UKG(),
74
+ IsolvedHire(),
75
+ Jobstreet(),
76
+ ADPMyJobs(),
77
+ SuccessFactors(), # it claims a career site's job path on any host
78
+ Phenom(), # last: it claims a URL by its path alone
79
+ ]
80
+
81
+
82
+ def filters() -> list[str]:
83
+ """The cosmetic filters of each adapter whose system's banner the blocker's lists miss."""
84
+ return [line for a in adapters() if isinstance(a, Filtered) for line in a.filters]