atsfill 0.0.4.dev98__tar.gz → 0.0.4.dev99__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 (47) hide show
  1. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/PKG-INFO +1 -1
  2. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/pyproject.toml +1 -1
  3. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/pyproject.toml.orig +1 -1
  4. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/ashby.py +4 -4
  5. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/bamboohr.py +4 -4
  6. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/breezy.py +3 -3
  7. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/dayforce.py +2 -2
  8. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/eightfold.py +3 -3
  9. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/greenhouse.py +3 -3
  10. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/hireology.py +3 -3
  11. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/hiringthing.py +2 -2
  12. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/jazzhr.py +1 -1
  13. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/lever.py +1 -1
  14. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/manatal.py +2 -2
  15. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/oracle.py +1 -1
  16. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/paycor.py +2 -2
  17. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/paylocity.py +4 -4
  18. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/personio.py +3 -3
  19. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/phenom.py +3 -3
  20. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/pinpoint.py +4 -4
  21. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/recruitee.py +1 -1
  22. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/rippling.py +3 -3
  23. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/smartrecruiters.py +3 -3
  24. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/teamtailor.py +1 -1
  25. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/workable.py +3 -3
  26. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/workday.py +1 -1
  27. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/wttj.py +1 -1
  28. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/zohorecruit.py +3 -3
  29. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/core/adapter.py +8 -8
  30. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/core/commands.py +11 -10
  31. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/LICENSE +0 -0
  32. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/README.md +0 -0
  33. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/__init__.py +0 -0
  34. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/adapters/__init__.py +0 -0
  35. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/cli.py +0 -0
  36. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/core/__init__.py +0 -0
  37. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/core/accounts.py +0 -0
  38. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/core/attempts.py +0 -0
  39. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/core/browser.py +0 -0
  40. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/core/conditions.py +0 -0
  41. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/core/matching.py +0 -0
  42. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/core/passwords.py +0 -0
  43. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/core/paths.py +0 -0
  44. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/core/registry.py +0 -0
  45. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/core/results.py +0 -0
  46. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/py.typed +0 -0
  47. {atsfill-0.0.4.dev98 → atsfill-0.0.4.dev99}/src/atsfill/registry.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atsfill
3
- Version: 0.0.4.dev98
3
+ Version: 0.0.4.dev99
4
4
  Summary: Read, dry-run and submit job application forms on hiring systems: Ashby, Greenhouse, Teamtailor, Welcome to the Jungle.
5
5
  Keywords: job-applications,ats,ashby,greenhouse,teamtailor,ai-agents,cli
6
6
  Author: shbernal
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atsfill"
3
- version = "0.0.4.dev98"
3
+ version = "0.0.4.dev99"
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"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atsfill"
3
- version = "0.0.4.dev98"
3
+ version = "0.0.4.dev99"
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"
@@ -19,7 +19,7 @@ from atsfill.core.results import Answer, Field, FieldType, SideEffect
19
19
 
20
20
  HOST = "jobs.ashbyhq.com"
21
21
  GRAPHQL = f"https://{HOST}/api/non-user-graphql?op=ApiJobPosting"
22
- # The fields of the page's own ApiJobPosting query that `read_form` uses.
22
+ # The fields of the page's own ApiJobPosting query that `browser_read` uses.
23
23
  JOB_POSTING = """query ApiJobPosting($org: String!, $job: String!) {
24
24
  jobPosting(organizationHostedJobsPageName: $org, jobPostingId: $job) {
25
25
  applicationForm { ...Form }
@@ -78,7 +78,7 @@ class Ashby:
78
78
 
79
79
  def __init__(self) -> None:
80
80
  self._forms: list[dict[str, Any]] = [] # the application form, then each survey
81
- self._pending: set[Any] = set() # GraphQL requests in flight, since `read_form`
81
+ self._pending: set[Any] = set() # GraphQL requests in flight, since `browser_read`
82
82
  self._last_change = 0.0
83
83
  self._hosted: str | None = None # the jobs.ashbyhq.com posting a custom domain embeds
84
84
 
@@ -100,7 +100,7 @@ class Ashby:
100
100
  def posting(self, url: str) -> str:
101
101
  return _hosted(url) if urlsplit(url).hostname == HOST else self._hosted or url
102
102
 
103
- def read_api(self, url: str) -> list[Field] | None:
103
+ def request_read(self, url: str) -> list[Field] | None:
104
104
  """The fields from the GraphQL query the job page itself sends, for a hosted URL."""
105
105
  if urlsplit(url).hostname != HOST:
106
106
  return None
@@ -125,7 +125,7 @@ class Ashby:
125
125
  except OSError, ValueError, KeyError, TypeError, Unsupported:
126
126
  return None
127
127
 
128
- def read_form(self, page: Page) -> list[Field]:
128
+ def browser_read(self, page: Page) -> list[Field]:
129
129
  page.on("request", self._on_request)
130
130
  page.on("requestfinished", self._on_finished)
131
131
  page.on("requestfailed", self._on_finished)
@@ -1,8 +1,8 @@
1
1
  """BambooHR (`<employer>.bamboohr.com/careers/<job>`), as docs/adapters/bamboohr.md observes.
2
2
 
3
3
  The job page renders its application form, behind Apply for This Job, from a public JSON
4
- answer that lists every field with its label, requirement and options. `read_api` fetches it
5
- over HTTP, `read_form` through the page's own browser context, and both list the fields from
4
+ answer that lists every field with its label, requirement and options. `request_read` fetches it
5
+ over HTTP, `browser_read` through the page's own browser context, and both list the fields from
6
6
  it. Dropdowns are Fabric menus over a hidden `select`; a file is uploaded once attached.
7
7
  """
8
8
 
@@ -159,7 +159,7 @@ class BambooHR:
159
159
  job = _job(url)
160
160
  return f"https://{job[0]}/careers/{job[1]}" if job else url
161
161
 
162
- def read_api(self, url: str) -> list[Field] | None:
162
+ def request_read(self, url: str) -> list[Field] | None:
163
163
  if (job := _job(url)) is None:
164
164
  return None
165
165
  try:
@@ -169,7 +169,7 @@ class BambooHR:
169
169
  except OSError, ValueError, KeyError, TypeError, Unsupported:
170
170
  return None
171
171
 
172
- def read_form(self, page: Page) -> list[Field]:
172
+ def browser_read(self, page: Page) -> list[Field]:
173
173
  host, job = _job(page.url) or ("", "")
174
174
  form = page.locator(FORM)
175
175
  try:
@@ -2,8 +2,8 @@
2
2
  docs/adapters/breezy.md observes.
3
3
 
4
4
  Two page engines, an AngularJS one and a React one, render the same markup: named controls
5
- under `h3` headings, the questionnaire's as `section_<section>_question_<n>`. The fields are
6
- read from the page. Files upload to Breezy when attached; the rest reaches it on submit.
5
+ under `h3` headings, the questionnaire's as `section_<section>_question_<n>`. Only a browser
6
+ read lists the fields. Files upload to Breezy when attached; the rest reaches it on submit.
7
7
  """
8
8
 
9
9
  import re
@@ -236,7 +236,7 @@ class Breezy:
236
236
  match = JOB.match(parts.path)
237
237
  return f"https://{parts.hostname}/p/{match[1]}" if match else url
238
238
 
239
- def read_form(self, page: Page) -> list[Field]:
239
+ def browser_read(self, page: Page) -> list[Field]:
240
240
  if not urlsplit(page.url).path.rstrip("/").endswith("/apply"):
241
241
  page.goto(f"{self.posting(page.url)}/apply")
242
242
  form = page.locator(FORM)
@@ -197,7 +197,7 @@ class Dayforce:
197
197
  found = _job(url)
198
198
  return f"https://{HOST}/{found[1]}/{found[2]}/jobs/{found[3]}" if found else url
199
199
 
200
- def read_form(self, page: Page) -> list[Field]:
200
+ def browser_read(self, page: Page) -> list[Field]:
201
201
  found = _job(page.url)
202
202
  if found is None:
203
203
  raise Unsupported("posting_gone: not a Dayforce job page")
@@ -481,7 +481,7 @@ class Dayforce:
481
481
  option = box_id.removeprefix(f"{base}answerFollowup").removeprefix("s_")
482
482
  followup = f"{field_id}.followup" + (f".{option}" if option else "")
483
483
  if box.is_visible() and followup not in self._fields:
484
- self._shown.append(box_id) # a comment box `read_form` did not list
484
+ self._shown.append(box_id) # a comment box `browser_read` did not list
485
485
 
486
486
  def _set_question(
487
487
  self, page: Page, target: Locator, field: Field, question: dict[str, Any], answer: Answer
@@ -3,7 +3,7 @@ the employer's own domain), as docs/adapters/eightfold.md observes.
3
3
 
4
4
  A posting's apply page (`/careers/apply?pid=<pid>`) renders its form from a questions API:
5
5
  sections of questions by id, type, requirement and options, and the questions whose answer
6
- shows each follow-up. `read_api` fetches it over HTTP, `read_form` through the page's browser
6
+ shows each follow-up. `request_read` fetches it over HTTP, `browser_read` through the page's browser
7
7
  context, and both list the fields from it. Each question renders as a control whose element id
8
8
  is its section's title and its id, with every other character an underscore.
9
9
  """
@@ -162,7 +162,7 @@ class Eightfold:
162
162
  parts, pid = urlsplit(url), _pid(url)
163
163
  return f"https://{parts.hostname}/careers/job/{pid}" if pid else url
164
164
 
165
- def read_api(self, url: str) -> list[Field] | None:
165
+ def request_read(self, url: str) -> list[Field] | None:
166
166
  if (pid := _pid(url)) is None:
167
167
  return None
168
168
  try:
@@ -185,7 +185,7 @@ class Eightfold:
185
185
  except OSError, ValueError, KeyError, TypeError, Unsupported, Blocked:
186
186
  return None
187
187
 
188
- def read_form(self, page: Page) -> list[Field]:
188
+ def browser_read(self, page: Page) -> list[Field]:
189
189
  pid = _pid(page.url)
190
190
  if pid is None:
191
191
  raise Unsupported(f"posting_gone: {page.url} is not a job page")
@@ -44,7 +44,7 @@ API_TYPES = {
44
44
  }
45
45
  YES_NO = ["Yes", "No"]
46
46
  # Sections the page adds or names its own way, or shows a question of only on another answer
47
- # (EEOC race after Hispanic/Latino): `read_api` declines a posting with any (ADR 0011).
47
+ # (EEOC race after Hispanic/Latino): `request_read` declines a posting with any (ADR 0011).
48
48
  UNMAPPED = ("education", "compliance", "demographic_questions")
49
49
 
50
50
  # The fields in page order. A select's options are read by opening it, from Python.
@@ -167,7 +167,7 @@ class Greenhouse:
167
167
  board, job = _board_job(url) or _board_job(self._embed or "") or ("", url)
168
168
  return f"https://job-boards.greenhouse.io/{board}/jobs/{job}"
169
169
 
170
- def read_api(self, url: str) -> list[Field] | None:
170
+ def request_read(self, url: str) -> list[Field] | None:
171
171
  """The fields from the Job Board API, unless the page holds some the API leaves out."""
172
172
  found = _board_job(url)
173
173
  if found is None:
@@ -177,7 +177,7 @@ class Greenhouse:
177
177
  except OSError, ValueError, KeyError, TypeError:
178
178
  return None
179
179
 
180
- def read_form(self, page: Page) -> list[Field]:
180
+ def browser_read(self, page: Page) -> list[Field]:
181
181
  if self._embed:
182
182
  page.goto(self._embed)
183
183
  form = page.locator(FORM)
@@ -2,7 +2,7 @@
2
2
  docs/adapters/hireology.md observes.
3
3
 
4
4
  The job page renders its application form from a public form API, which lists every field by
5
- id, type and requirement. `read_api` fetches it over HTTP, `read_form` through the page's own
5
+ id, type and requirement. `request_read` fetches it over HTTP, `browser_read` through the page's own
6
6
  browser context, and both list the fields from it. Each field renders as one control whose id is
7
7
  `<field id>-<n>`; nothing but the submit reaches Hireology.
8
8
  """
@@ -202,7 +202,7 @@ class Hireology:
202
202
  job = _job(url)
203
203
  return API.format(job=job) if job else url
204
204
 
205
- def read_api(self, url: str) -> list[Field] | None:
205
+ def request_read(self, url: str) -> list[Field] | None:
206
206
  if (job := _job(url)) is None:
207
207
  return None
208
208
  request = urllib.request.Request(API.format(job=job), headers={"User-Agent": "atsfill"})
@@ -212,7 +212,7 @@ class Hireology:
212
212
  except OSError, ValueError, KeyError, TypeError, Unsupported:
213
213
  return None
214
214
 
215
- def read_form(self, page: Page) -> list[Field]:
215
+ def browser_read(self, page: Page) -> list[Field]:
216
216
  job = _job(page.url)
217
217
  try:
218
218
  page.locator(SUBMIT).wait_for(state="attached", timeout=FORM_TIMEOUT_MS)
@@ -4,7 +4,7 @@ observes.
4
4
  The job page carries the posting's configuration in a React component's props, and its Apply
5
5
  button renders the form from them in the page: MUI inputs named by field id, radios and boxes,
6
6
  react-select comboboxes for the country and state, Dropzone areas that upload a file as soon as
7
- it is attached. `read_form` lists the rendered controls; there is no `read_api`, since which
7
+ it is attached. `browser_read` lists the rendered controls; there is no `request_read`, since which
8
8
  controls render is the page's logic over the props, not the props alone.
9
9
  """
10
10
 
@@ -157,7 +157,7 @@ class HiringThing:
157
157
  job = JOB.match(parts.path)
158
158
  return f"https://{parts.hostname}/job/{job[1]}" if job else url
159
159
 
160
- def read_form(self, page: Page) -> list[Field]:
160
+ def browser_read(self, page: Page) -> list[Field]:
161
161
  props = page.locator(PROPS).first
162
162
  try:
163
163
  props.wait_for(state="attached", timeout=PAGE_TIMEOUT_MS)
@@ -140,7 +140,7 @@ class JazzHR:
140
140
  job = _job(url)
141
141
  return f"https://{job[0]}/apply/{job[1]}" if job else url
142
142
 
143
- def read_form(self, page: Page) -> list[Field]:
143
+ def browser_read(self, page: Page) -> list[Field]:
144
144
  form = page.locator(FORM)
145
145
  try:
146
146
  form.wait_for(state="attached", timeout=FORM_TIMEOUT_MS)
@@ -133,7 +133,7 @@ class Lever:
133
133
  parts, job = urlsplit(url), _job(url)
134
134
  return f"https://{parts.hostname}/{job[0]}/{job[1]}" if job else url
135
135
 
136
- def read_form(self, page: Page) -> list[Field]:
136
+ def browser_read(self, page: Page) -> list[Field]:
137
137
  if not urlsplit(page.url).path.rstrip("/").endswith("/apply"):
138
138
  page.goto(f"{self.posting(page.url)}/apply")
139
139
  form = page.locator(FORM)
@@ -191,7 +191,7 @@ class Manatal:
191
191
  def posting(self, url: str) -> str:
192
192
  return _posting(url)
193
193
 
194
- def read_api(self, url: str) -> list[Field] | None:
194
+ def request_read(self, url: str) -> list[Field] | None:
195
195
  """The fields from the endpoint the apply page fetches, its job id from that page."""
196
196
  client = urlsplit(url).path.strip("/").split("/")[0]
197
197
  try:
@@ -210,7 +210,7 @@ class Manatal:
210
210
  except OSError, ValueError, KeyError, TypeError:
211
211
  return None
212
212
 
213
- def read_form(self, page: Page) -> list[Field]:
213
+ def browser_read(self, page: Page) -> list[Field]:
214
214
  with page.expect_response(
215
215
  lambda r: r.request.method == "GET" and "/application-form/" in r.url,
216
216
  timeout=LOAD_TIMEOUT_MS,
@@ -266,7 +266,7 @@ class Oracle:
266
266
  assert match, url
267
267
  return f"https://{parts.hostname}/hcmUI/CandidateExperience/job/{match[1]}"
268
268
 
269
- def read_form(self, page: Page) -> list[Field]:
269
+ def browser_read(self, page: Page) -> list[Field]:
270
270
  parts = urlsplit(page.url)
271
271
  match = JOB.search(parts.path)
272
272
  assert match, page.url # detected on a job URL
@@ -3,7 +3,7 @@
3
3
  The apply page is server-rendered. Before the form it may show pages of their own: the
4
4
  federal contractor's self-identification (VEVRAA) and the Yes/No qualifying questions. Each
5
5
  posts its answers to the next, which carries them as hidden inputs; the form posts everything
6
- to `ApplyJob.action` on Submit. `read_form` walks those pages to the form, `fill` walks them
6
+ to `ApplyJob.action` on Submit. `browser_read` walks those pages to the form, `fill` walks them
7
7
  again with the answers.
8
8
  """
9
9
 
@@ -171,7 +171,7 @@ class Paycor:
171
171
  job = (query.get("id") or query.get("gni") or [url])[0]
172
172
  return f"{CAREER}JobIntroduction.action?id={job}"
173
173
 
174
- def read_form(self, page: Page) -> list[Field]:
174
+ def browser_read(self, page: Page) -> list[Field]:
175
175
  self._before, self._codes = [], {}
176
176
  self._walk(page, None)
177
177
  controls = [(_field(c), c["kind"]) for c in page.evaluate(_CONTROLS)]
@@ -2,8 +2,8 @@
2
2
  docs/adapters/paylocity.md observes.
3
3
 
4
4
  The apply page carries the whole form's configuration in `window.pageData`: the sections and
5
- their fields, the screener questions and the option lists. `read_api` fetches it over HTTP,
6
- `read_form` takes it from the page, and both list the fields from it. The form is one page or
5
+ their fields, the screener questions and the option lists. `request_read` fetches it over HTTP,
6
+ `browser_read` takes it from the page, and both list the fields from it. The form is one page or
7
7
  a wizard of steps that render one at a time, so `fill` keeps each step's read-back before
8
8
  moving on; nothing reaches the employer before Submit.
9
9
  """
@@ -326,7 +326,7 @@ class Paylocity:
326
326
  job = _job(url)
327
327
  return f"https://{HOST}/Recruiting/Jobs/Details/{job}" if job else url
328
328
 
329
- def read_api(self, url: str) -> list[Field] | None:
329
+ def request_read(self, url: str) -> list[Field] | None:
330
330
  if (job := _job(url)) is None:
331
331
  return None
332
332
  request = urllib.request.Request(APPLY + job, headers={"User-Agent": "atsfill"})
@@ -337,7 +337,7 @@ class Paylocity:
337
337
  except OSError, ValueError, KeyError, TypeError, Unsupported:
338
338
  return None
339
339
 
340
- def read_form(self, page: Page) -> list[Field]:
340
+ def browser_read(self, page: Page) -> list[Field]:
341
341
  job = _job(page.url)
342
342
  if job and "/apply/" not in page.url.lower():
343
343
  page.goto(APPLY + job)
@@ -2,7 +2,7 @@
2
2
 
3
3
  The apply page is rendered on the server, its form whole in the HTML: every field is one native
4
4
  control named by its id, labelled by a `label[for]` or its group's label, a star marking it
5
- required. `read_api` fetches that HTML over HTTP, `read_form` takes the page's form, and both
5
+ required. `request_read` fetches that HTML over HTTP, `browser_read` takes the page's form, and both
6
6
  list the fields from it with one parser. A document is uploaded to Personio once attached.
7
7
  """
8
8
 
@@ -72,7 +72,7 @@ class Personio:
72
72
  job = _job(url)
73
73
  return f"https://{job[0]}.jobs.personio.com/job/{job[1]}" if job else url
74
74
 
75
- def read_api(self, url: str) -> list[Field] | None:
75
+ def request_read(self, url: str) -> list[Field] | None:
76
76
  if _job(url) is None:
77
77
  return None
78
78
  request = urllib.request.Request(_apply(url), headers={"User-Agent": "atsfill"})
@@ -84,7 +84,7 @@ class Personio:
84
84
  form = next((n for n in _parse(html).walk() if n.tag == "form"), None)
85
85
  return [f for f, _ in _form(form)] if form else None
86
86
 
87
- def read_form(self, page: Page) -> list[Field]:
87
+ def browser_read(self, page: Page) -> list[Field]:
88
88
  if _job(page.url) and not urlsplit(page.url).path.rstrip("/").endswith("/apply"):
89
89
  page.goto(_apply(page.url))
90
90
  try:
@@ -4,7 +4,7 @@ as docs/adapters/phenom.md observes.
4
4
  The job page's Apply link leads to Phenom's own wizard (`/apply?jobSeqNo=`) or to another
5
5
  system. The wizard renders one step at a time from the employer's configuration, a form schema
6
6
  on Phenom's CDN, and the parts its loaders serve (the job's questionnaires, the country's
7
- fields, dependent lists): reading interprets them, over HTTP or from the page, without passing a
7
+ fields, dependent lists): a request read or a browser read interprets them without passing a
8
8
  step. Filling walks the wizard on a fresh session, each step's answers read back before Next.
9
9
  """
10
10
 
@@ -260,7 +260,7 @@ class Phenom:
260
260
  seq = _seq(url)
261
261
  return f"https://{parts.hostname}/apply?jobSeqNo={seq}" if seq else url
262
262
 
263
- def read_api(self, url: str) -> list[Field] | None:
263
+ def request_read(self, url: str) -> list[Field] | None:
264
264
  """The fields from the job's apply page and the configuration it names, over HTTP;
265
265
  None for a job applied to on another system, or a page it does not understand."""
266
266
  parts = urlsplit(url)
@@ -297,7 +297,7 @@ class Phenom:
297
297
  except OSError, ValueError, KeyError, TypeError, RuntimeError, Unsupported:
298
298
  return None
299
299
 
300
- def read_form(self, page: Page) -> list[Field]:
300
+ def browser_read(self, page: Page) -> list[Field]:
301
301
  _check_open(page)
302
302
  self._apply = _apply_url(page)
303
303
  self._open(page)
@@ -2,8 +2,8 @@
2
2
  docs/adapters/pinpoint.md observes.
3
3
 
4
4
  The apply page is rendered on the server: the standard fields as native controls, every other
5
- field as a React component whose props sit in a JSON script next to it. `read_api` fetches that
6
- HTML over HTTP, `read_form` takes the page's form, and both list the fields from it with one
5
+ field as a React component whose props sit in a JSON script next to it. `request_read` fetches that
6
+ HTML over HTTP, `browser_read` takes the page's form, and both list the fields from it with one
7
7
  parser. A document is uploaded to Pinpoint once attached.
8
8
  """
9
9
 
@@ -125,7 +125,7 @@ class Pinpoint:
125
125
  job = _job(url)
126
126
  return f"https://{urlsplit(url).hostname}/en/postings/{job[1]}" if job else url
127
127
 
128
- def read_api(self, url: str) -> list[Field] | None:
128
+ def request_read(self, url: str) -> list[Field] | None:
129
129
  if _job(url) is None:
130
130
  return None
131
131
  request = urllib.request.Request(_apply(url), headers={"User-Agent": "atsfill"})
@@ -139,7 +139,7 @@ class Pinpoint:
139
139
  )
140
140
  return [f for f, _ in _form(form, self)] if form else None
141
141
 
142
- def read_form(self, page: Page) -> list[Field]:
142
+ def browser_read(self, page: Page) -> list[Field]:
143
143
  if _job(page.url) and not urlsplit(page.url).path.rstrip("/").endswith("/new"):
144
144
  page.goto(_apply(page.url))
145
145
  try:
@@ -143,7 +143,7 @@ class Recruitee:
143
143
  match = JOB.match(parts.path)
144
144
  return f"https://{parts.hostname}/o/{match[1]}" if match else url
145
145
 
146
- def read_form(self, page: Page) -> list[Field]:
146
+ def browser_read(self, page: Page) -> list[Field]:
147
147
  if not urlsplit(page.url).path.rstrip("/").endswith("/c/new"):
148
148
  page.goto(f"{self.posting(page.url)}/c/new")
149
149
  form = page.locator(FORM)
@@ -3,7 +3,7 @@ observes.
3
3
 
4
4
  The job's apply page (`…/apply`) carries its form in the page data: the standard questions by
5
5
  id and type, the employer's question groups, and whether the EEO questionnaire is asked.
6
- `read_api` fetches the page over HTTP, `read_form` reads the data of the page it opened, and
6
+ `request_read` fetches the page over HTTP, `browser_read` reads the data of the page it opened, and
7
7
  both list the fields from it. Each field renders in an element whose `data-testid` is the
8
8
  field's id; the résumé is uploaded and parsed as soon as it is attached.
9
9
  """
@@ -175,7 +175,7 @@ class Rippling:
175
175
  job = _job(url)
176
176
  return POSTING.format(board=job[0], job=job[1]) if job else url
177
177
 
178
- def read_api(self, url: str) -> list[Field] | None:
178
+ def request_read(self, url: str) -> list[Field] | None:
179
179
  if (job := _job(url)) is None:
180
180
  return None
181
181
  request = urllib.request.Request(
@@ -189,7 +189,7 @@ class Rippling:
189
189
  except OSError, ValueError, KeyError, TypeError, Unsupported:
190
190
  return None
191
191
 
192
- def read_form(self, page: Page) -> list[Field]:
192
+ def browser_read(self, page: Page) -> list[Field]:
193
193
  job = _job(page.url)
194
194
  assert job is not None
195
195
  apply = APPLY.format(board=job[0], job=job[1])
@@ -2,7 +2,7 @@
2
2
  docs/adapters/smartrecruiters.md observes.
3
3
 
4
4
  The form is two pages: the profile, then the screening questions and consents. The second
5
- page's questions come from the page's own `__OC_CONTEXT__`, so `read_form` lists them without
5
+ page's questions come from the page's own `__OC_CONTEXT__`, so `browser_read` lists them without
6
6
  leaving the first page. Nothing but the resume reaches SmartRecruiters before submit, so the
7
7
  read-back is the page: the profile as it stood when `fill` left it, then the second page.
8
8
  """
@@ -136,7 +136,7 @@ class SmartRecruiters:
136
136
  def posting(self, url: str) -> str:
137
137
  return _key(url) or url
138
138
 
139
- def read_form(self, page: Page) -> list[Field]:
139
+ def browser_read(self, page: Page) -> list[Field]:
140
140
  if not ONECLICK.match(urlsplit(page.url).path):
141
141
  link = page.locator(APPLY_LINK).first
142
142
  try:
@@ -252,7 +252,7 @@ class SmartRecruiters:
252
252
  return fields
253
253
 
254
254
  def _question_answers(self, question_id: str) -> list[tuple[str, dict[str, Any]]]:
255
- """(field id, the question's field) for each field `read_form` listed."""
255
+ """(field id, the question's field) for each field `browser_read` listed."""
256
256
  entries = self._questions[question_id]["fields"]
257
257
  ids = [f"{question_id}.{e['id']}" if len(entries) > 1 else question_id for e in entries]
258
258
  return [
@@ -198,7 +198,7 @@ class Teamtailor:
198
198
  assert match, url
199
199
  return f"https://{parts.hostname}/jobs/{match[1]}"
200
200
 
201
- def read_form(self, page: Page) -> list[Field]:
201
+ def browser_read(self, page: Page) -> list[Field]:
202
202
  """Opens the form's own page, never the overlay nor "Apply with LinkedIn"."""
203
203
  parts = urlsplit(page.url)
204
204
  job = JOB.search(parts.path)
@@ -2,7 +2,7 @@
2
2
  docs/adapters/workable.md observes.
3
3
 
4
4
  Every field sits under an element whose `data-ui` is its id in Workable's form API, which
5
- `read_api` reads. Nothing but an attached file reaches Workable before submit, so the
5
+ `request_read` reads. Nothing but an attached file reaches Workable before submit, so the
6
6
  read-back is the page.
7
7
  """
8
8
 
@@ -159,7 +159,7 @@ class Workable:
159
159
  def posting(self, url: str) -> str:
160
160
  return f"https://apply.workable.com/j/{_code(url) or url}"
161
161
 
162
- def read_api(self, url: str) -> list[Field] | None:
162
+ def request_read(self, url: str) -> list[Field] | None:
163
163
  """The fields from the form API the page itself loads, unless it has a type it does not
164
164
  know."""
165
165
  code = _code(url)
@@ -174,7 +174,7 @@ class Workable:
174
174
  except OSError, ValueError, KeyError, TypeError:
175
175
  return None
176
176
 
177
- def read_form(self, page: Page) -> list[Field]:
177
+ def browser_read(self, page: Page) -> list[Field]:
178
178
  if "/apply" not in urlsplit(page.url).path:
179
179
  page.goto(f"{self.posting(page.url)}/apply/")
180
180
  form = page.locator(FORM)
@@ -253,7 +253,7 @@ class Workday:
253
253
  consent.check()
254
254
  _submit(page, "createAccountSubmitButton", email, password)
255
255
 
256
- def read_form(self, page: Page) -> list[Field]:
256
+ def browser_read(self, page: Page) -> list[Field]:
257
257
  """Walks the wizard as far as its pages save with what they hold, reading each page
258
258
  it reaches: from the page, else My Experience from the apply flow's sections and the
259
259
  questions from the questionnaires, which also stand for those pages when they are not
@@ -106,7 +106,7 @@ class WelcomeToTheJungle:
106
106
  _account_form(page, "sign-up-form", values, "/registrations")
107
107
  raise Blocked("email_link: the sign-up sent a confirmation link")
108
108
 
109
- def read_form(self, page: Page) -> list[Field]:
109
+ def browser_read(self, page: Page) -> list[Field]:
110
110
  """Opens the apply modal; a job that applies on another site has no form here."""
111
111
  if not JOB.match(urlsplit(page.url).path):
112
112
  raise RuntimeError(f"{page.url} is not a Welcome to the Jungle job")
@@ -3,7 +3,7 @@ docs/adapters/zohorecruit.md observes.
3
3
 
4
4
  The job page names its application form, which "I'm interested" renders from a public form
5
5
  API: sections of fields by id, type and requirement, and the pre-screening questions a form
6
- may ask with them. `read_api` fetches it over HTTP, `read_form` through the page's browser
6
+ may ask with them. `request_read` fetches it over HTTP, `browser_read` through the page's browser
7
7
  context, and both list the fields from it. Each field renders as a Lyte component whose id
8
8
  derives from the field's; nothing but the submit reaches Zoho.
9
9
  """
@@ -143,7 +143,7 @@ class ZohoRecruit:
143
143
  parts, job = urlsplit(url), _job(url)
144
144
  return POSTING.format(host=parts.hostname, job=job[2]) if job else url
145
145
 
146
- def read_api(self, url: str) -> list[Field] | None:
146
+ def request_read(self, url: str) -> list[Field] | None:
147
147
  if _job(url) is None:
148
148
  return None
149
149
  try:
@@ -155,7 +155,7 @@ class ZohoRecruit:
155
155
  except OSError, ValueError, KeyError, TypeError, Unsupported:
156
156
  return None
157
157
 
158
- def read_form(self, page: Page) -> list[Field]:
158
+ def browser_read(self, page: Page) -> list[Field]:
159
159
  job = _job(page.url)
160
160
  if job is None:
161
161
  raise Unsupported(f"posting_gone: {page.url} is not a job page")
@@ -17,7 +17,7 @@ class Unsupported(Exception):
17
17
  """What stops the form before any fill: a flow the adapter does not handle, or a fact about
18
18
  the posting (`posting_gone`, `external_step`). The message is `code: detail`.
19
19
 
20
- A field atsfill cannot fill is not raised: `read_form` returns it with `unfillable` set.
20
+ A field atsfill cannot fill is not raised: `browser_read` returns it with `unfillable` set.
21
21
  """
22
22
 
23
23
 
@@ -68,7 +68,7 @@ class Adapter(Protocol):
68
68
  """The posting's key for the double-send guard: every URL of one posting gives the same."""
69
69
  ...
70
70
 
71
- def read_form(self, page: Page) -> list[Field]:
71
+ def browser_read(self, page: Page) -> list[Field]:
72
72
  """Every field, one atsfill cannot fill included with its reason in `unfillable`."""
73
73
  ...
74
74
 
@@ -137,19 +137,19 @@ class CreatesAccount(SignsIn, Protocol):
137
137
 
138
138
 
139
139
  @runtime_checkable
140
- class ReadsOverHttp(Adapter, Protocol):
141
- """An adapter that can list a form's fields over HTTP, without a browser (ADR 0011)."""
140
+ class ReadsByRequest(Adapter, Protocol):
141
+ """An adapter with a request read: a form's fields listed without a browser (ADR 0011)."""
142
142
 
143
- def read_api(self, url: str) -> list[Field] | None:
144
- """The same fields as `read_form`; None when it cannot for this URL."""
143
+ def request_read(self, url: str) -> list[Field] | None:
144
+ """The same fields as `browser_read`; None when it cannot for this URL."""
145
145
  ...
146
146
 
147
147
 
148
148
  @runtime_checkable
149
149
  class ListsShownFields(Adapter, Protocol):
150
150
  """An adapter whose form can show fields after an answer: once filled, it says which the
151
- page shows, so that one `read_form` did not list stops the fill (ADR 0005)."""
151
+ page shows, so that one `browser_read` did not list stops the fill (ADR 0005)."""
152
152
 
153
153
  def shown_fields(self, page: Page) -> list[str]:
154
- """The ids of the fields the filled page shows, each as `read_form` would name it."""
154
+ """The ids of the fields the filled page shows, each as `browser_read` would name it."""
155
155
  ...
@@ -22,7 +22,7 @@ from atsfill.core.adapter import (
22
22
  ListsShownFields,
23
23
  NeedsAccount,
24
24
  NotTaken,
25
- ReadsOverHttp,
25
+ ReadsByRequest,
26
26
  Refused,
27
27
  SignsIn,
28
28
  Unsupported,
@@ -196,7 +196,7 @@ def _unfillable(result: Result, answers: dict[str, Answer] | None = None) -> boo
196
196
 
197
197
 
198
198
  def _listed(result: Result, fields: list[Field]) -> None:
199
- """Sets the fields `read_form` or `read_api` listed; their conditions must name listed
199
+ """Sets the fields `browser_read` or `request_read` listed; their conditions must name listed
200
200
  fields, never a loop."""
201
201
  if faults := conditions.faults(fields):
202
202
  raise ValueError("; ".join(faults))
@@ -206,7 +206,7 @@ def _listed(result: Result, fields: list[Field]) -> None:
206
206
  def _shown_as_listed(
207
207
  page: Page, adapter: Adapter, result: Result, answers: dict[str, Answer]
208
208
  ) -> None:
209
- """Stops a fill that showed a field `read_form` did not list, or a follow-up whose
209
+ """Stops a fill that showed a field `browser_read` did not list, or a follow-up whose
210
210
  condition the answers leave unmet: that condition was read wrong (ADR 0005)."""
211
211
  if not isinstance(adapter, ListsShownFields):
212
212
  return
@@ -259,7 +259,7 @@ def _fill(page: Page, adapter: Adapter, url: str, answers: dict[str, Answer], us
259
259
  """Fills and reads back; `ready` when every answer reads back as given."""
260
260
  result = _result(adapter, url, user)
261
261
  with _stops(result):
262
- _listed(result, adapter.read_form(page))
262
+ _listed(result, adapter.browser_read(page))
263
263
  if _unfillable(result, answers):
264
264
  return result
265
265
  if problems := matching.problems(result.fields, answers):
@@ -275,7 +275,7 @@ def _fill(page: Page, adapter: Adapter, url: str, answers: dict[str, Answer], us
275
275
  return result
276
276
 
277
277
 
278
- def _read_page(
278
+ def _browser_read(
279
279
  data: Path, user: str, url: str, sessions: Sessions | None, detected: list[str]
280
280
  ) -> Result:
281
281
  with _opened(data, user, url, sessions, detected) as opened:
@@ -284,7 +284,7 @@ def _read_page(
284
284
  page, adapter = opened
285
285
  result = _result(adapter, url, user)
286
286
  with _stops(result):
287
- _listed(result, adapter.read_form(page))
287
+ _listed(result, adapter.browser_read(page))
288
288
  _unfillable(result)
289
289
  return result
290
290
 
@@ -347,11 +347,12 @@ def read(
347
347
  data_dir: Path | None = None,
348
348
  sessions: Sessions | None = None,
349
349
  ) -> Result:
350
- """Lists the form's fields, over HTTP when an adapter can (ADR 0011), else from the page."""
350
+ """Lists the form's fields by a request read when an adapter has one (ADR 0011), else by a
351
+ browser read."""
351
352
  for adapter in adapters():
352
- if not isinstance(adapter, ReadsOverHttp):
353
+ if not isinstance(adapter, ReadsByRequest):
353
354
  continue
354
- if (fields := adapter.read_api(url)) is not None:
355
+ if (fields := adapter.request_read(url)) is not None:
355
356
  result = _result(adapter, url, user)
356
357
  try:
357
358
  _listed(result, fields)
@@ -360,7 +361,7 @@ def read(
360
361
  _unfillable(result)
361
362
  return result
362
363
  data = paths.data_dir(data_dir)
363
- return _in_budget(url, lambda seen: _read_page(data, user, url, sessions, seen))
364
+ return _in_budget(url, lambda seen: _browser_read(data, user, url, sessions, seen))
364
365
 
365
366
 
366
367
  def fill(
File without changes
File without changes