atsfill 0.0.5.dev167__tar.gz → 0.0.5.dev168__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 (65) hide show
  1. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/PKG-INFO +2 -2
  2. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/pyproject.toml +1 -1
  3. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/pyproject.toml.orig +1 -1
  4. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/adpmyjobs.py +80 -23
  5. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/registry.toml +1 -1
  6. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/LICENSE +0 -0
  7. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/README.md +1 -1
  8. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/__init__.py +0 -0
  9. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/__init__.py +0 -0
  10. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/ashby.py +0 -0
  11. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/bamboohr.py +0 -0
  12. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/breezy.py +0 -0
  13. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/dayforce.py +0 -0
  14. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/eightfold.py +0 -0
  15. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/greenhouse.py +0 -0
  16. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/gupy.py +0 -0
  17. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/hireology.py +0 -0
  18. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/hiringthing.py +0 -0
  19. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/hrmdirect.py +0 -0
  20. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/isolvedhire.py +0 -0
  21. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/jazzhr.py +0 -0
  22. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/jobaffinity.py +0 -0
  23. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/jobstreet.py +0 -0
  24. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/join.py +0 -0
  25. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/lever.py +0 -0
  26. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/manatal.py +0 -0
  27. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/oracle.py +0 -0
  28. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/paycor.py +0 -0
  29. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/paylocity.py +0 -0
  30. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/personio.py +0 -0
  31. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/phenom.py +0 -0
  32. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/pinpoint.py +0 -0
  33. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/recruitee.py +0 -0
  34. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/recruiterflow.py +0 -0
  35. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/rippling.py +0 -0
  36. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/smartrecruiters.py +0 -0
  37. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/successfactors.js +0 -0
  38. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/successfactors.py +0 -0
  39. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/taleo.py +0 -0
  40. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/teamtailor.py +0 -0
  41. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/ukg.py +0 -0
  42. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/welcomekit.py +0 -0
  43. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/workable.py +0 -0
  44. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/workatastartup.py +0 -0
  45. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/workday.py +0 -0
  46. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/wttj.py +0 -0
  47. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/adapters/zohorecruit.py +0 -0
  48. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/cli.py +0 -0
  49. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/core/__init__.py +0 -0
  50. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/core/accounts.py +0 -0
  51. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/core/adapter.py +0 -0
  52. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/core/attempts.py +0 -0
  53. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/core/browser.py +0 -0
  54. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/core/commands.py +0 -0
  55. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/core/conditions.py +0 -0
  56. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/core/html.py +0 -0
  57. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/core/matching.py +0 -0
  58. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/core/otp.py +0 -0
  59. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/core/passwords.py +0 -0
  60. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/core/paths.py +0 -0
  61. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/core/registry.py +0 -0
  62. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/core/results.py +0 -0
  63. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/core/snapshot.py +0 -0
  64. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/core/telemetry.py +0 -0
  65. {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev168}/src/atsfill/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atsfill
3
- Version: 0.0.5.dev167
3
+ Version: 0.0.5.dev168
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
@@ -89,11 +89,11 @@ It is built for agents with a shell: every command prints one JSON document and
89
89
  [![wttj run history](https://shbernal.github.io/atsfill/run-history/wttj.svg)](https://shbernal.github.io/atsfill/run-history/)
90
90
  [![workday run history](https://shbernal.github.io/atsfill/run-history/workday.svg)](https://shbernal.github.io/atsfill/run-history/)
91
91
  [![successfactors run history](https://shbernal.github.io/atsfill/run-history/successfactors.svg)](https://shbernal.github.io/atsfill/run-history/)
92
+ [![adpmyjobs run history](https://shbernal.github.io/atsfill/run-history/adpmyjobs.svg)](https://shbernal.github.io/atsfill/run-history/)
92
93
  [![isolvedhire run history](https://shbernal.github.io/atsfill/run-history/isolvedhire.svg)](https://shbernal.github.io/atsfill/run-history/)
93
94
  [![jobstreet run history](https://shbernal.github.io/atsfill/run-history/jobstreet.svg)](https://shbernal.github.io/atsfill/run-history/)
94
95
  [![gupy run history](https://shbernal.github.io/atsfill/run-history/gupy.svg)](https://shbernal.github.io/atsfill/run-history/)
95
96
  [![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
97
  <!-- /run-history -->
98
98
 
99
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, one mark per posting and version, newest first.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atsfill"
3
- version = "0.0.5.dev167"
3
+ version = "0.0.5.dev168"
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.5.dev167"
3
+ version = "0.0.5.dev168"
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"
@@ -12,6 +12,7 @@ import re
12
12
  import time
13
13
  from contextlib import suppress
14
14
  from dataclasses import dataclass, field, replace
15
+ from datetime import UTC, datetime
15
16
  from html import unescape
16
17
  from typing import Any
17
18
  from urllib.parse import parse_qs, quote, urlsplit
@@ -35,12 +36,12 @@ POSTING = re.compile(r"^/(?P<site>[^/]+)/cx/(?:job/(?P<job>\d+)|job-details)")
35
36
  API = "https://my.adp.com/myadp_prefix/mycareer/api"
36
37
  QUESTIONNAIRES = f"{API}/rm/staffing/v3/work-fulfillment/recruiting-questionnaires"
37
38
  APPLICATIONS = f"https://{HOST}/cx/staffing/v2/job-applications"
39
+ APPLICANT = f"https://{HOST}/cx/staffing/v2/job-applicant" # the candidate profile
38
40
  SIGN_IN_TIMEOUT_MS = 60_000 # the sign-in page takes ~20 s to render its form
39
41
  SIGN_IN_STEP_TIMEOUT_MS = 30_000
40
42
  SIGNED_IN_TIMEOUT_MS = 60_000
41
43
  POSTING_TIMEOUT_MS = 45_000
42
44
  SECTION_TIMEOUT_MS = 45_000
43
- LOAD_TRIES = 3
44
45
  SUBMIT_TIMEOUT_MS = 90_000
45
46
  SETTLE_MS = 1_500 # an answer shows its follow-ups, a Next saves and routes
46
47
  OPTION_TIMEOUT_MS = 10_000
@@ -48,6 +49,7 @@ UPLOAD_TIMEOUT_MS = 60_000
48
49
  KEY_DELAY_MS = 40
49
50
  SKIPPED = {"INSTRUCTIONS", "HEADER"}
50
51
  YES_NO = ["Yes", "No"]
52
+ DATE = "%m/%d/%Y" # a date picker's typed form
51
53
 
52
54
  _FETCH = """async ([url, role, site]) => {
53
55
  const headers = {myjobstoken: sessionStorage.getItem('myJobsToken') ?? ''};
@@ -71,6 +73,8 @@ class Section:
71
73
  fields: list[Field] = field(default_factory=list)
72
74
  pages: list[set[str]] = field(default_factory=list) # the question ids on each page
73
75
  review: bool = False # the pages end on a review of the answers, before that last Next
76
+ # A page of entries by its number: what its Add button names, and how many it needs
77
+ entries: dict[int, tuple[str, int]] = field(default_factory=dict)
74
78
 
75
79
 
76
80
  class ADPMyJobs:
@@ -124,6 +128,9 @@ class ADPMyJobs:
124
128
  if found is None:
125
129
  raise RuntimeError(f"{page.url} is not an ADP MyJobs posting")
126
130
  site, req = found
131
+ # Left before it settles, the first page's loader wedges the tab: no workflow shows in it
132
+ with suppress(Timeout):
133
+ page.wait_for_load_state("networkidle", timeout=POSTING_TIMEOUT_MS)
127
134
  settings = _career_site(page, site)
128
135
  self._site_id = str(settings["externalId"])
129
136
  if not (settings.get("enabledFeatures") or {}).get("candidateApplicationWorkflow"):
@@ -202,6 +209,7 @@ class ADPMyJobs:
202
209
  for number, ids in enumerate(section.pages or [set()]):
203
210
  _on_section(page, section)
204
211
  _to_page(page, section, ids)
212
+ _add_entries(page, section, number)
205
213
  if unlisted := _unlisted(page, section):
206
214
  raise NotTaken(f"{section.title} shows {unlisted}, which the read did not list")
207
215
  self._answer(page, section, answers)
@@ -234,6 +242,12 @@ class ADPMyJobs:
234
242
  role = url.startswith(API)
235
243
  return page.evaluate(_FETCH, [url, role, self._site_id if "/api/rm/" in url else ""])
236
244
 
245
+ def _profile_name(self, page: Page) -> str:
246
+ """The candidate profile's name, first and last, as ADP has it on file."""
247
+ found = self._get(page, APPLICANT).get("jobApplicants") or [{}]
248
+ name = (found[0].get("person") or {}).get("personName") or {}
249
+ return " ".join(n for n in (name.get("givenName"), name.get("familyName1")) if n)
250
+
237
251
  def _assignment(self, page: Page) -> str:
238
252
  assignment = parse_qs(urlsplit(page.url).query).get("assignmentId")
239
253
  if not assignment:
@@ -244,20 +258,7 @@ class ADPMyJobs:
244
258
  page.wait_for_url(
245
259
  lambda u: "workflowComponent=" in u, timeout=SECTION_TIMEOUT_MS, wait_until="commit"
246
260
  )
247
- workflow = page.url
248
- for attempt in range(LOAD_TRIES): # it can spin: loaded again from the posting, it shows
249
- try:
250
- page.get_by_role("button", name="Next").wait_for(timeout=SECTION_TIMEOUT_MS)
251
- return
252
- except Timeout:
253
- if attempt == LOAD_TRIES - 1:
254
- raise
255
- site = urlsplit(workflow).path.split("/")[1]
256
- req = parse_qs(urlsplit(workflow).query)["reqId"][0]
257
- page.goto(f"https://{HOST}/{site}/cx/job-details?reqId={req}")
258
- with suppress(Timeout):
259
- page.wait_for_load_state("networkidle", timeout=POSTING_TIMEOUT_MS)
260
- page.goto(workflow)
261
+ page.get_by_role("button", name="Next").wait_for(timeout=SECTION_TIMEOUT_MS)
261
262
 
262
263
  def _workflow(self, page: Page) -> list[Section]:
263
264
  assignment = self._assignment(page)
@@ -292,8 +293,13 @@ class ADPMyJobs:
292
293
  if "questions" not in p:
293
294
  listed = self._get(page, f"{url}&sectionID={p['sectionID']}")
294
295
  p = next(iter(listed.get("questionSections") or []), {})
295
- questions += p.get("questions") or []
296
- section.pages.append({q["questionID"] for q in p.get("questions") or []})
296
+ asked = p.get("questions") or []
297
+ if "sectionRepeatMin" in (labels := _labels(p)): # each entry's questions, `<id>_<n>`
298
+ needed = int(labels["sectionRepeatMin"])
299
+ section.entries[len(section.pages)] = (labels["sectionRepeatLabel"], needed)
300
+ asked = [q for q in asked if _entry(q) <= needed]
301
+ questions += asked
302
+ section.pages.append({q["questionID"] for q in asked})
297
303
  # A form of pages that is not open yet lists none: the fill stops on its questions,
298
304
  # which a read once it is open lists.
299
305
  fields = [f for q in questions if (f := self._question(page, section, q, questions))]
@@ -316,7 +322,7 @@ class ADPMyJobs:
316
322
  default = raw.get("answerDefaultValue")
317
323
  options: list[str] | None = None
318
324
  match kind:
319
- case "TEXT" | "EMAIL":
325
+ case "TEXT" | "EMAIL" | "LONGTEXT":
320
326
  type_ = FieldType.TEXT
321
327
  case "PHONENUMBER":
322
328
  type_ = FieldType.PHONE
@@ -325,8 +331,13 @@ class ADPMyJobs:
325
331
  self._codes[field_id] = {"true": "Yes", "false": "No"}
326
332
  case "UPLOAD":
327
333
  type_ = FieldType.FILE
328
- case "ESIGNATURE":
334
+ case "DATE": # a saved one in epoch milliseconds, midnight in the US
335
+ type_ = FieldType.DATE
336
+ if default:
337
+ default = datetime.fromtimestamp(int(default) / 1000, UTC).date().isoformat()
338
+ case "ESIGNATURE": # refused unless it is the profile's name
329
339
  type_, label = FieldType.TEXT, "Electronic signature: your full legal name"
340
+ default = self._profile_name(page)
330
341
  case (
331
342
  "SELECTION"
332
343
  | "COUNTRIES"
@@ -403,8 +414,13 @@ class ADPMyJobs:
403
414
  case "Single choice":
404
415
  self._codes[ids[qid]] = labels
405
416
  f = Field(ids[qid], label, FieldType.CHOICE, True, list(labels.values()))
417
+ case "Multiple choice":
418
+ self._codes[ids[qid]] = labels
419
+ f = Field(ids[qid], label, FieldType.MULTI_CHOICE, True, list(labels.values()))
406
420
  case "Free form":
407
421
  f = Field(ids[qid], label, FieldType.TEXT, True)
422
+ case "Calendar":
423
+ f = Field(ids[qid], label, FieldType.DATE, True)
408
424
  case other:
409
425
  reason = f"unhandled_rendering: {label!r} is a {other} question"
410
426
  f = Field(ids[qid], label, FieldType.TEXT, True, unfillable=reason)
@@ -423,7 +439,7 @@ class ADPMyJobs:
423
439
  continue # not shown: its condition is unmet
424
440
  if _value(box, f, self._codes.get(f.id, {})) == answers[f.id]:
425
441
  continue # a list's default, the profile's value
426
- _set(page, box, f, str(answers[f.id]), self._codes.get(f.id, {}))
442
+ _set(page, box, f, answers[f.id], self._codes.get(f.id, {}))
427
443
  page.wait_for_timeout(SETTLE_MS if f.type is FieldType.CHOICE else 0)
428
444
 
429
445
  def _read(self, page: Page, section: Section) -> dict[str, Answer]:
@@ -610,6 +626,25 @@ def _to_page(page: Page, section: Section, ids: set[str]) -> None:
610
626
  raise RuntimeError(f"{section.title} shows another page than the one to fill")
611
627
 
612
628
 
629
+ def _entry(question: dict[str, Any]) -> int:
630
+ """Which entry of a page of entries a question belongs to; 0 for the page's own."""
631
+ labels = {x["code"]: x["value"] for x in question.get("questionLabels") or []}
632
+ return int(labels.get("repeatIndex", 0))
633
+
634
+
635
+ def _add_entries(page: Page, section: Section, number: int) -> None:
636
+ """A page of entries lists none until its Add button adds one, folded under its title
637
+ (`Employer 1`): adds the entries it needs, and unfolds each."""
638
+ label, needed = section.entries.get(number, ("", 0))
639
+ for n in range(1, needed + 1):
640
+ title = page.get_by_text(f"{label} {n}", exact=True)
641
+ if not title.count():
642
+ page.get_by_role("button", name=f"Add {label}").click()
643
+ title.wait_for(timeout=SECTION_TIMEOUT_MS)
644
+ if not page.locator(f'adp-form-group[data-name$="_{n}"]').first.is_visible():
645
+ title.click()
646
+
647
+
613
648
  def _groups(page: Page) -> list[str]:
614
649
  return page.locator("adp-form-group").evaluate_all("es => es.map(e => e.dataset.name)")
615
650
 
@@ -644,12 +679,27 @@ def _box(page: Page, section: Section, f: Field) -> Locator:
644
679
  question = f.id.removeprefix(f"{section.component}.")
645
680
  if section.component.startswith("prescreen"):
646
681
  labelled = f'[aria-labelledby="{question}-error"]:not(sdf-radio-button)'
647
- return page.locator(f"#{question}, {labelled}").first
682
+ boxes = page.locator(f"#{question}, {labelled}")
683
+ return boxes if f.type is FieldType.MULTI_CHOICE else boxes.first # a box per option
648
684
  return page.locator(f'adp-form-group[data-name="{question}"]').first
649
685
 
650
686
 
651
- def _set(page: Page, box: Locator, f: Field, answer: str, codes: dict[str, str]) -> None:
687
+ def _set(page: Page, box: Locator, f: Field, answer: Answer, codes: dict[str, str]) -> None:
652
688
  by_label = {v: k for k, v in codes.items()}
689
+ if isinstance(answer, list): # a prescreen's options, a checkbox each
690
+ chosen = {by_label.get(a, a) for a in answer}
691
+ for option in box.all():
692
+ if (option.get_attribute("aria-checked") == "true") != (
693
+ option.get_attribute("value") in chosen
694
+ ):
695
+ option.click()
696
+ return
697
+ if f.type is FieldType.DATE:
698
+ target = box.locator("input").first
699
+ target.fill("")
700
+ target.press_sequentially(datetime.fromisoformat(answer).strftime(DATE), delay=KEY_DELAY_MS)
701
+ target.press("Tab")
702
+ return
653
703
  if f.type is FieldType.FILE:
654
704
  _upload(page, box, f, answer)
655
705
  return
@@ -729,6 +779,13 @@ def _upload(page: Page, box: Locator, f: Field, answer: str) -> None:
729
779
 
730
780
 
731
781
  def _value(box: Locator, f: Field, codes: dict[str, str]) -> Answer | None:
782
+ if f.type is FieldType.MULTI_CHOICE:
783
+ checked = [o.get_attribute("value") or "" for o in box.all()
784
+ if o.get_attribute("aria-checked") == "true"] # fmt: skip
785
+ return [codes.get(c, c) for c in checked] or None
786
+ if f.type is FieldType.DATE:
787
+ typed = box.locator("input").first.input_value()
788
+ return datetime.strptime(typed, DATE).date().isoformat() if typed else None
732
789
  if f.type is FieldType.FILE: # the upload's text, its buttons icons alone: the file's name
733
790
  lines = box.locator("dynamic-form-sdf-upload").inner_text().splitlines()
734
791
  return next((t.strip() for t in lines if t.strip()), None)
@@ -756,5 +813,5 @@ def _value(box: Locator, f: Field, codes: dict[str, str]) -> Answer | None:
756
813
 
757
814
  def _errors(page: Page) -> str:
758
815
  """Each question's error, from the text kept for screen readers."""
759
- said = page.locator("rm-sr-field-error, sdf-alert-inline").all_text_contents()
816
+ said = page.locator("rm-sr-field-error, sdf-alert-inline, sdf-alert").all_text_contents()
760
817
  return "; ".join(dict.fromkeys(t.strip() for t in said if t.strip())) or "no error shown"
@@ -254,7 +254,7 @@ account = "to_read" # a candidate profile per career site without a password, s
254
254
  hosting = ["hosted"]
255
255
  hosts = ["myjobs.adp.com"]
256
256
  adapter = "adpmyjobs"
257
- scope = { is = "in_scope", date = 2026-10-29, reason = "adapter before its gate: Apply sends the application, the form follows it; 2 of 2 workflows walked ready" }
257
+ scope = { is = "supported", date = 2026-10-29, commit = "a14e403" }
258
258
 
259
259
  [phenom]
260
260
  kind = "ats"
File without changes
@@ -60,11 +60,11 @@ It is built for agents with a shell: every command prints one JSON document and
60
60
  [![wttj run history](https://shbernal.github.io/atsfill/run-history/wttj.svg)](https://shbernal.github.io/atsfill/run-history/)
61
61
  [![workday run history](https://shbernal.github.io/atsfill/run-history/workday.svg)](https://shbernal.github.io/atsfill/run-history/)
62
62
  [![successfactors run history](https://shbernal.github.io/atsfill/run-history/successfactors.svg)](https://shbernal.github.io/atsfill/run-history/)
63
+ [![adpmyjobs run history](https://shbernal.github.io/atsfill/run-history/adpmyjobs.svg)](https://shbernal.github.io/atsfill/run-history/)
63
64
  [![isolvedhire run history](https://shbernal.github.io/atsfill/run-history/isolvedhire.svg)](https://shbernal.github.io/atsfill/run-history/)
64
65
  [![jobstreet run history](https://shbernal.github.io/atsfill/run-history/jobstreet.svg)](https://shbernal.github.io/atsfill/run-history/)
65
66
  [![gupy run history](https://shbernal.github.io/atsfill/run-history/gupy.svg)](https://shbernal.github.io/atsfill/run-history/)
66
67
  [![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
68
  <!-- /run-history -->
69
69
 
70
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, one mark per posting and version, newest first.