atsfill 0.0.8.dev228__tar.gz → 0.0.8.dev229__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.8.dev228 → atsfill-0.0.8.dev229}/PKG-INFO +1 -1
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/pyproject.toml +1 -1
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/pyproject.toml.orig +1 -1
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/workday.py +42 -19
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/adapter.py +9 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/commands.py +14 -3
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/results.py +1 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/telemetry.py +1 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/LICENSE +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/README.md +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/__init__.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/__init__.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/adp.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/adpmyjobs.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/ashby.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/bamboohr.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/breezy.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/dayforce.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/eightfold.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/greenhouse.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/gupy.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/hireology.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/hiringthing.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/hrmdirect.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/isolvedhire.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/jazzhr.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/jobaffinity.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/jobstreet.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/join.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/lever.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/manatal.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/oracle.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/paycor.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/paylocity.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/personio.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/phenom.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/pinpoint.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/recruitee.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/recruiterflow.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/rippling.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/smartrecruiters.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/successfactors.js +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/successfactors.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/taleo.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/teamtailor.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/ukg.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/welcomekit.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/wellfound.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/werecruit.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/workable.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/workatastartup.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/wttj.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/adapters/zohorecruit.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/cli.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/__init__.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/accounts.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/attempts.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/browser.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/conditions.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/countries.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/html.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/matching.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/otp.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/passwords.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/paths.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/registry.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/core/snapshot.py +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/py.typed +0 -0
- {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev229}/src/atsfill/registry.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atsfill
|
|
3
|
-
Version: 0.0.8.
|
|
3
|
+
Version: 0.0.8.dev229
|
|
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
|
|
@@ -5,8 +5,8 @@ observes.
|
|
|
5
5
|
Applying needs an account, one per tenant: the account scope. The form is a wizard of pages,
|
|
6
6
|
each saved to a draft application as it is left; only the current page is in the DOM. The
|
|
7
7
|
read walks the pages that save with what they hold, the posting's questionnaires and apply
|
|
8
|
-
flow standing for the pages it does not reach;
|
|
9
|
-
before it is saved.
|
|
8
|
+
flow standing for the pages it does not reach; a page behind a consent's tick only a fill
|
|
9
|
+
reaches, on the user's answer; the read-back is each page's values, read before it is saved.
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import re
|
|
@@ -193,7 +193,7 @@ def _profile_hosts(request: Request, scope: str | None) -> list[str]:
|
|
|
193
193
|
|
|
194
194
|
class Workday:
|
|
195
195
|
name = "workday"
|
|
196
|
-
|
|
196
|
+
behind_consent = "the pages after a consent's tick, which the draft does not keep"
|
|
197
197
|
per_employer = True
|
|
198
198
|
asks: set[str] = set()
|
|
199
199
|
known_gaps = {
|
|
@@ -216,6 +216,8 @@ class Workday:
|
|
|
216
216
|
self._listed: set[str] = set() # the ids the read listed, unfillable ones too
|
|
217
217
|
self._walked: set[str] = set() # the pages the read reached
|
|
218
218
|
self._walled = False # no read gets past the page this one stopped on
|
|
219
|
+
self._behind = False # the fill stopped on a page behind that one, for `revealed`
|
|
220
|
+
self._phone_codes = ""
|
|
219
221
|
self._values: dict[str, Answer] = {}
|
|
220
222
|
self._apply = ""
|
|
221
223
|
|
|
@@ -292,15 +294,16 @@ class Workday:
|
|
|
292
294
|
page.goto(self._apply)
|
|
293
295
|
_wait_for_page(page)
|
|
294
296
|
seen: dict[str, list[Field]] = {}
|
|
295
|
-
self._walked, self._walled = set(), False
|
|
297
|
+
self._walked, self._walled, self._behind = set(), False, False
|
|
298
|
+
self._phone_codes = f"{calypso}/jobapplication/{found.tenant}{PHONE_CODES}"
|
|
296
299
|
for _ in range(MAX_PAGES):
|
|
297
300
|
step = _step(page)
|
|
298
301
|
if step == REVIEW: # repeats the answers, an upload without its input
|
|
299
302
|
break
|
|
300
303
|
self._walked.add(step)
|
|
301
304
|
if not _questions_page(step):
|
|
302
|
-
|
|
303
|
-
seen[step] = _read(page,
|
|
305
|
+
listed = {f.id for f in from_api.get(step, [])}
|
|
306
|
+
seen[step] = _read(page, self._phone_codes, listed)
|
|
304
307
|
try:
|
|
305
308
|
_save_and_continue(page)
|
|
306
309
|
except NotTaken:
|
|
@@ -316,10 +319,13 @@ class Workday:
|
|
|
316
319
|
return fields
|
|
317
320
|
|
|
318
321
|
def fill(self, page: Page, answers: dict[str, Answer]) -> None:
|
|
319
|
-
"""Fills each page of the wizard from the first and saves it, up to Review
|
|
320
|
-
page.
|
|
321
|
-
|
|
322
|
-
|
|
322
|
+
"""Fills each page of the wizard from the first and saves it, up to Review, or up to a
|
|
323
|
+
page behind the consent: the next call goes on from there with what `revealed` listed."""
|
|
324
|
+
if not self._behind:
|
|
325
|
+
page.goto(self._apply)
|
|
326
|
+
_wait_for_page(page)
|
|
327
|
+
self._values = {}
|
|
328
|
+
self._behind = False
|
|
323
329
|
pending: set[PageRequest] = set()
|
|
324
330
|
page.on("request", lambda r: pending.add(r) if DEFINITION in r.url else None)
|
|
325
331
|
for done in ("requestfinished", "requestfailed"):
|
|
@@ -343,6 +349,8 @@ class Workday:
|
|
|
343
349
|
_until_rendered(page, pending)
|
|
344
350
|
page.wait_for_timeout(SETTLE_MS)
|
|
345
351
|
self._unread(page)
|
|
352
|
+
if self._behind:
|
|
353
|
+
return
|
|
346
354
|
self._values |= {r["id"]: r["value"] for r in _filled(page, answers)}
|
|
347
355
|
if self._values.get(PHONE_NUMBER) and (code := _phone_code(page)):
|
|
348
356
|
self._values[PHONE_NUMBER] = f"+{code} {self._values[PHONE_NUMBER]}"
|
|
@@ -351,21 +359,31 @@ class Workday:
|
|
|
351
359
|
|
|
352
360
|
def _unread(self, page: Page) -> None:
|
|
353
361
|
"""Stops on fields the read did not list, optional ones too. A page the read did not
|
|
354
|
-
reach is read once the pages before it are saved
|
|
355
|
-
it read, an answer opened the field (a country's address, a referral's
|
|
356
|
-
stops unsaved, so no read would list it."""
|
|
362
|
+
reach is read once the pages before it are saved; where no read can get there, `revealed`
|
|
363
|
+
lists it. On one it read, an answer opened the field (a country's address, a referral's
|
|
364
|
+
name): the page stops unsaved, so no read would list it."""
|
|
357
365
|
unread = _not_listed(_page_fields(page), self._listed)
|
|
358
366
|
if not unread:
|
|
359
367
|
return
|
|
360
368
|
step, labels = _step(page), ", ".join(r["label"] for r in unread)
|
|
361
369
|
if step not in self._walked and self._walled:
|
|
362
|
-
|
|
363
|
-
|
|
370
|
+
self._behind = True
|
|
371
|
+
elif step not in self._walked:
|
|
364
372
|
raise NotTaken(f"{step} shows fields the read did not: {labels}; read again")
|
|
365
|
-
|
|
373
|
+
elif step == MY_INFORMATION and any(r["id"].startswith(ADDRESS) for r in unread):
|
|
366
374
|
detail = f"the country answered shows fields the read did not: {labels}"
|
|
367
375
|
raise Unsupported(known_gap(self, "other_country", detail))
|
|
368
|
-
|
|
376
|
+
else:
|
|
377
|
+
raise Unsupported(f"unhandled_rendering: an answer showed {labels}, not listed")
|
|
378
|
+
|
|
379
|
+
def revealed(self, page: Page) -> list[Field]:
|
|
380
|
+
"""The fields of the page behind the consent the fill stopped on, none once it ended."""
|
|
381
|
+
if not self._behind:
|
|
382
|
+
return []
|
|
383
|
+
fields = _read(page, self._phone_codes, self._listed)
|
|
384
|
+
self._fields |= {f.id: f for f in fields if not f.unfillable}
|
|
385
|
+
self._listed |= {f.id for f in fields}
|
|
386
|
+
return fields
|
|
369
387
|
|
|
370
388
|
def read_back(self, page: Page) -> dict[str, Answer]:
|
|
371
389
|
"""Each page's values, read before it was saved; one file as its name."""
|
|
@@ -607,6 +625,7 @@ def _field(raw: dict[str, Any]) -> Field | None:
|
|
|
607
625
|
return Field(
|
|
608
626
|
raw["id"], raw["label"], kind, raw["required"], options,
|
|
609
627
|
prefilled=None if site else value, default=value if site else None,
|
|
628
|
+
notice=raw["notice"],
|
|
610
629
|
) # fmt: skip
|
|
611
630
|
|
|
612
631
|
|
|
@@ -978,7 +997,8 @@ def _errors(page: Page) -> str:
|
|
|
978
997
|
return re.sub(r"\s+", " ", " ".join(page.locator(ERRORS).all_inner_texts())).strip()
|
|
979
998
|
|
|
980
999
|
|
|
981
|
-
# Each field the page shows: its id, label, whether required (`*`), rendering and
|
|
1000
|
+
# Each field the page shows: its id, label, whether required (`*`), rendering, value, and the
|
|
1001
|
+
# text a consent's box stands under.
|
|
982
1002
|
_FIELDS = """() => {
|
|
983
1003
|
const text = e => (e?.innerText ?? "").replace(/\\s+/g, " ").trim();
|
|
984
1004
|
const labelOf = (f, e) => text(f.querySelector(`label[for="${CSS.escape(e.id)}"]`)
|
|
@@ -1029,8 +1049,11 @@ _FIELDS = """() => {
|
|
|
1029
1049
|
kind = "text";
|
|
1030
1050
|
value = typed.value;
|
|
1031
1051
|
}
|
|
1052
|
+
const notice = kind === "box" && f.closest("[role=group]")
|
|
1053
|
+
?.querySelector("[data-automation-id=instructionalText]");
|
|
1032
1054
|
return {id: f.dataset.fkitId, label: label.replace(/\\*$/, "").trim(),
|
|
1033
|
-
required: label.endsWith("*"), kind, value, options
|
|
1055
|
+
required: label.endsWith("*"), kind, value, options,
|
|
1056
|
+
notice: text(notice || null) || null};
|
|
1034
1057
|
});
|
|
1035
1058
|
}"""
|
|
1036
1059
|
|
|
@@ -317,6 +317,15 @@ class RevealsFields(Adapter, Protocol):
|
|
|
317
317
|
...
|
|
318
318
|
|
|
319
319
|
|
|
320
|
+
@runtime_checkable
|
|
321
|
+
class RevealsBehindConsent(RevealsFields, Protocol):
|
|
322
|
+
"""A `RevealsFields` whose fields stand on pages behind a consent's tick, which no draft
|
|
323
|
+
keeps: a read gives no answer, so it stops at the tick, and only a fill gets past it. A
|
|
324
|
+
refusal for them is `behind_consent`, counted apart from other answers (ADR 0005)."""
|
|
325
|
+
|
|
326
|
+
behind_consent: str # where they stand
|
|
327
|
+
|
|
328
|
+
|
|
320
329
|
@runtime_checkable
|
|
321
330
|
class ListsShownFields(Adapter, Protocol):
|
|
322
331
|
"""An adapter whose form can show fields after an answer: once filled, it says which the
|
|
@@ -34,6 +34,7 @@ from atsfill.core.adapter import (
|
|
|
34
34
|
ProfileIncomplete,
|
|
35
35
|
ReadsByRequest,
|
|
36
36
|
Refused,
|
|
37
|
+
RevealsBehindConsent,
|
|
37
38
|
RevealsFields,
|
|
38
39
|
SendsToOpen,
|
|
39
40
|
SignedOut,
|
|
@@ -354,9 +355,14 @@ def _shown_as_listed(
|
|
|
354
355
|
result.reason = f"mismatch: {', '.join(wrong)} shows though its condition is unmet"
|
|
355
356
|
|
|
356
357
|
|
|
357
|
-
def _refuse(
|
|
358
|
+
def _refuse(
|
|
359
|
+
result: Result,
|
|
360
|
+
problems: list[matching.Problem],
|
|
361
|
+
*more: str,
|
|
362
|
+
code: str = "invalid_answers",
|
|
363
|
+
) -> None:
|
|
358
364
|
result.status = Status.REFUSED
|
|
359
|
-
result.reason = "
|
|
365
|
+
result.reason = f"{code}: " + "; ".join([*problems, *more])
|
|
360
366
|
result.refused = [f"{p.field_id}: {p.rule}" for p in problems]
|
|
361
367
|
|
|
362
368
|
|
|
@@ -386,9 +392,14 @@ def _revealed(
|
|
|
386
392
|
result,
|
|
387
393
|
problems,
|
|
388
394
|
*[f"{f.id} takes {f.options}" for f in new if f.options and f.id not in mine],
|
|
395
|
+
code=(
|
|
396
|
+
"behind_consent"
|
|
397
|
+
if isinstance(adapter, RevealsBehindConsent)
|
|
398
|
+
else "invalid_answers"
|
|
399
|
+
),
|
|
389
400
|
)
|
|
390
401
|
return mine
|
|
391
|
-
left = [f for f in new if f.id not in mine]
|
|
402
|
+
left = [f for f in new if f.id not in mine and f.prefilled is None]
|
|
392
403
|
result.warnings += [f"an answer showed {f.id} ({f.label}), left empty" for f in left]
|
|
393
404
|
adapter.fill(page, matching.to_fill(new, {f.id: mine[f.id] for f in new if f.id in mine}))
|
|
394
405
|
_no_field(result, answers)
|
|
@@ -76,6 +76,7 @@ class Field:
|
|
|
76
76
|
option_sets: list[OptionSet] | None = None # a dependent list's; `options` where none holds
|
|
77
77
|
min_length: int | None = None # a text the system refuses shorter
|
|
78
78
|
knockouts: list[str] | None = None # the options whose choice ends the application (ADR 0005)
|
|
79
|
+
notice: str | None = None # the text a consent stands under: what a Yes accepts (ADR 0005)
|
|
79
80
|
|
|
80
81
|
|
|
81
82
|
@dataclass(frozen=True)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|