atsfill 0.0.5.dev167__tar.gz → 0.0.5.dev169__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.
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/PKG-INFO +2 -2
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/pyproject.toml +1 -1
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/pyproject.toml.orig +1 -1
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/adpmyjobs.py +80 -23
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/registry.toml +2 -2
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/LICENSE +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/README.md +1 -1
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/__init__.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/__init__.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/ashby.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/bamboohr.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/breezy.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/dayforce.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/eightfold.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/greenhouse.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/gupy.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/hireology.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/hiringthing.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/hrmdirect.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/isolvedhire.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/jazzhr.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/jobaffinity.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/jobstreet.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/join.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/lever.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/manatal.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/oracle.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/paycor.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/paylocity.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/personio.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/phenom.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/pinpoint.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/recruitee.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/recruiterflow.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/rippling.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/smartrecruiters.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/successfactors.js +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/successfactors.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/taleo.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/teamtailor.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/ukg.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/welcomekit.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/workable.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/workatastartup.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/workday.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/wttj.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/adapters/zohorecruit.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/cli.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/core/__init__.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/core/accounts.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/core/adapter.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/core/attempts.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/core/browser.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/core/commands.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/core/conditions.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/core/html.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/core/matching.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/core/otp.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/core/passwords.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/core/paths.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/core/registry.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/core/results.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/core/snapshot.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/src/atsfill/core/telemetry.py +0 -0
- {atsfill-0.0.5.dev167 → atsfill-0.0.5.dev169}/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.
|
|
3
|
+
Version: 0.0.5.dev169
|
|
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
|
[](https://shbernal.github.io/atsfill/run-history/)
|
|
90
90
|
[](https://shbernal.github.io/atsfill/run-history/)
|
|
91
91
|
[](https://shbernal.github.io/atsfill/run-history/)
|
|
92
|
+
[](https://shbernal.github.io/atsfill/run-history/)
|
|
92
93
|
[](https://shbernal.github.io/atsfill/run-history/)
|
|
93
94
|
[](https://shbernal.github.io/atsfill/run-history/)
|
|
94
95
|
[](https://shbernal.github.io/atsfill/run-history/)
|
|
95
96
|
[](https://shbernal.github.io/atsfill/run-history/)
|
|
96
|
-
[](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.
|
|
@@ -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
|
-
|
|
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}§ionID={p['sectionID']}")
|
|
294
295
|
p = next(iter(listed.get("questionSections") or []), {})
|
|
295
|
-
|
|
296
|
-
|
|
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 "
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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"
|
|
@@ -205,7 +205,7 @@ scope = { is = "supported", date = 2026-10-20, commit = "d35da0b" }
|
|
|
205
205
|
[adp]
|
|
206
206
|
kind = "ats"
|
|
207
207
|
share = 0.0108
|
|
208
|
-
account = "to_read" # Apply opens a talent community step, name, email and a required mobile number, verified with an OTP
|
|
208
|
+
account = "to_read" # Apply opens a talent community step, name, email and a required mobile number, verified with an emailed OTP, then the application's wizard
|
|
209
209
|
hosting = ["hosted"]
|
|
210
210
|
hosts = ["workforcenow.adp.com"]
|
|
211
211
|
scope = { is = "in_scope", date = 2026-10-26, reason = "unsupported_flow: sign_up_phone: the talent community step before the form requires a mobile number, even when its OTP goes to the email (14 of 15 employers)" }
|
|
@@ -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 = "
|
|
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
|
[](https://shbernal.github.io/atsfill/run-history/)
|
|
61
61
|
[](https://shbernal.github.io/atsfill/run-history/)
|
|
62
62
|
[](https://shbernal.github.io/atsfill/run-history/)
|
|
63
|
+
[](https://shbernal.github.io/atsfill/run-history/)
|
|
63
64
|
[](https://shbernal.github.io/atsfill/run-history/)
|
|
64
65
|
[](https://shbernal.github.io/atsfill/run-history/)
|
|
65
66
|
[](https://shbernal.github.io/atsfill/run-history/)
|
|
66
67
|
[](https://shbernal.github.io/atsfill/run-history/)
|
|
67
|
-
[](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.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|