atsfill 0.0.5.dev184__tar.gz → 0.0.5.dev186__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.dev184 → atsfill-0.0.5.dev186}/PKG-INFO +1 -1
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/pyproject.toml +1 -1
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/pyproject.toml.orig +1 -1
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/adpmyjobs.py +5 -5
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/workatastartup.py +20 -9
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/core/commands.py +1 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/core/matching.py +12 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/core/results.py +2 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/LICENSE +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/README.md +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/__init__.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/__init__.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/ashby.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/bamboohr.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/breezy.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/dayforce.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/eightfold.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/greenhouse.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/gupy.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/hireology.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/hiringthing.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/hrmdirect.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/isolvedhire.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/jazzhr.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/jobaffinity.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/jobstreet.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/join.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/lever.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/manatal.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/oracle.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/paycor.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/paylocity.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/personio.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/phenom.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/pinpoint.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/recruitee.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/recruiterflow.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/rippling.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/smartrecruiters.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/successfactors.js +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/successfactors.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/taleo.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/teamtailor.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/ukg.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/welcomekit.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/workable.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/workday.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/wttj.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/adapters/zohorecruit.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/cli.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/core/__init__.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/core/accounts.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/core/adapter.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/core/attempts.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/core/browser.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/core/conditions.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/core/html.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/core/otp.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/core/passwords.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/core/paths.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/core/registry.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/core/snapshot.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/core/telemetry.py +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/py.typed +0 -0
- {atsfill-0.0.5.dev184 → atsfill-0.0.5.dev186}/src/atsfill/registry.toml +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.dev186
|
|
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
|
|
@@ -408,15 +408,15 @@ class ADPMyJobs:
|
|
|
408
408
|
every = len(chosen) == len(labels)
|
|
409
409
|
incoming.setdefault(target, []).append(Is(ids[qid], None if every else chosen))
|
|
410
410
|
label = _text(raw.get("questionDescription") or qid)
|
|
411
|
+
knockouts = [labels[a["answerId"]] for a in answers if a.get("knockoutRule")] or None
|
|
411
412
|
match raw["questionType"]:
|
|
412
413
|
case "Instructions":
|
|
413
414
|
continue
|
|
414
|
-
case "Single choice":
|
|
415
|
+
case "Single choice" | "Multiple choice" as kind:
|
|
415
416
|
self._codes[ids[qid]] = labels
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
f = Field(ids[qid], label, FieldType.MULTI_CHOICE, True, list(labels.values()))
|
|
417
|
+
type_ = FieldType.CHOICE if kind == "Single choice" else FieldType.MULTI_CHOICE
|
|
418
|
+
options = list(labels.values())
|
|
419
|
+
f = Field(ids[qid], label, type_, True, options, knockouts=knockouts)
|
|
420
420
|
case "Free form":
|
|
421
421
|
f = Field(ids[qid], label, FieldType.TEXT, True)
|
|
422
422
|
case "Calendar":
|
|
@@ -390,6 +390,8 @@ class Control:
|
|
|
390
390
|
return form.locator("input[type=tel]")
|
|
391
391
|
case "country" | "combo":
|
|
392
392
|
return form.locator(f'input[aria-label="{self.where}"]')
|
|
393
|
+
case "companies":
|
|
394
|
+
return form.locator('input[aria-label="Company name"]')
|
|
393
395
|
case "multi" | "skills":
|
|
394
396
|
return form.get_by_label(self.where, exact=False)
|
|
395
397
|
case "month": # the selects' names carry the entry's id: <id>_start_date_month
|
|
@@ -429,15 +431,7 @@ STEPS: dict[str, list[Control]] = {
|
|
|
429
431
|
Control("phone_number", "Phone Number", "phone", FieldType.PHONE, False),
|
|
430
432
|
Control("share", "What is your job search status?", "radio", C, True),
|
|
431
433
|
Control("yc_affiliation", "Have you worked at a YC company?", "radio", C, True),
|
|
432
|
-
Control(
|
|
433
|
-
"hidden_companies",
|
|
434
|
-
"YC companies to be hidden from",
|
|
435
|
-
"combo",
|
|
436
|
-
T,
|
|
437
|
-
False,
|
|
438
|
-
"Company name",
|
|
439
|
-
unfillable="unhandled_rendering: a list of YC companies to be hidden from",
|
|
440
|
-
),
|
|
434
|
+
Control("hidden_companies", "YC companies to be hidden from", "companies", M, False),
|
|
441
435
|
],
|
|
442
436
|
"location": [
|
|
443
437
|
Control("city_current", "What city do you live in?", "place", TA, True),
|
|
@@ -625,6 +619,8 @@ def _value(form: Locator, c: Control) -> Answer | None:
|
|
|
625
619
|
return target.evaluate(_SINGLE_VALUE) or None
|
|
626
620
|
case "multi":
|
|
627
621
|
return target.evaluate(_CHIPS)
|
|
622
|
+
case "companies":
|
|
623
|
+
return target.evaluate(_COMPANIES)
|
|
628
624
|
case "skills":
|
|
629
625
|
return form.evaluate(_SKILLS)
|
|
630
626
|
return target.input_value()
|
|
@@ -640,6 +636,8 @@ _CHIPS = """e => {
|
|
|
640
636
|
const box = e.closest("[id$=__select]") ?? e.parentElement.parentElement.parentElement;
|
|
641
637
|
return [...box.querySelectorAll("[aria-label^='Remove ']")].map(c => c.ariaLabel.slice(7));
|
|
642
638
|
}"""
|
|
639
|
+
_COMPANIES = """e => [...e.parentElement.previousElementSibling.querySelectorAll(
|
|
640
|
+
"span[role=button] + span")].map(s => s.textContent.trim())"""
|
|
643
641
|
_SKILLS = """form => {
|
|
644
642
|
const groups = {};
|
|
645
643
|
for (const r of form.querySelectorAll("input[type=radio][name^=top_skills_]")) {
|
|
@@ -684,6 +682,8 @@ def _profile_fill(page: Page, form: Locator, c: Control, answer: Answer) -> None
|
|
|
684
682
|
form.locator(f'[aria-label="Remove {chip}"]').click()
|
|
685
683
|
for item in [a for a in answer if a not in have]:
|
|
686
684
|
_pick(page, c, target, item, strict=True, exact=True)
|
|
685
|
+
case "companies", list():
|
|
686
|
+
_companies(page, target, c, answer)
|
|
687
687
|
case "skills", list():
|
|
688
688
|
_skills(page, form, target, c, answer)
|
|
689
689
|
case kind, _:
|
|
@@ -715,6 +715,17 @@ def _pick(
|
|
|
715
715
|
box.press("Escape")
|
|
716
716
|
|
|
717
717
|
|
|
718
|
+
def _companies(page: Page, box: Locator, c: Control, answer: list[str]) -> None:
|
|
719
|
+
"""Removes the companies the answer leaves out, then searches each other one and picks the
|
|
720
|
+
result named exactly as it: a nearest name would hide the profile from another company."""
|
|
721
|
+
chips = box.locator("xpath=../preceding-sibling::div[1]//div[span[@role='button']]")
|
|
722
|
+
while extra := [i for i, h in enumerate(box.evaluate(_COMPANIES)) if h not in answer]:
|
|
723
|
+
chips.nth(extra[0]).locator("span[role=button]").click()
|
|
724
|
+
have = box.evaluate(_COMPANIES)
|
|
725
|
+
for company in [a for a in answer if a not in have]:
|
|
726
|
+
_pick(page, c, box, company, strict=True, exact=True)
|
|
727
|
+
|
|
728
|
+
|
|
718
729
|
def _skills(page: Page, form: Locator, box: Locator, c: Control, answer: list[str]) -> None:
|
|
719
730
|
"""Removes the skills the answer leaves out, adds the others, then picks each level."""
|
|
720
731
|
wanted: dict[str, str] = {}
|
|
@@ -424,6 +424,7 @@ def _fill(
|
|
|
424
424
|
if problems := matching.problems(result.fields, answers):
|
|
425
425
|
result.status, result.reason = Status.REFUSED, "invalid_answers: " + "; ".join(problems)
|
|
426
426
|
return result
|
|
427
|
+
result.warnings = matching.knockouts(result.fields, answers)
|
|
427
428
|
adapter.fill(page, answers)
|
|
428
429
|
_shown_as_listed(page, adapter, result, answers)
|
|
429
430
|
if result.status is not Status.READY:
|
|
@@ -159,3 +159,15 @@ def problems(fields: list[Field], answers: dict[str, Answer]) -> list[str]:
|
|
|
159
159
|
and (problem := _answer_problem(by_id[i], a, form))
|
|
160
160
|
]
|
|
161
161
|
return out
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def knockouts(fields: list[Field], answers: dict[str, Answer]) -> list[str]:
|
|
165
|
+
"""The answers that end the application: never refused, as one can be the truthful one."""
|
|
166
|
+
by_id = {f.id: f for f in fields}
|
|
167
|
+
return [
|
|
168
|
+
f"{i}: {', '.join(map(repr, chosen))} ends the application ({f.label})"
|
|
169
|
+
for i, a in answers.items()
|
|
170
|
+
if (f := by_id.get(i))
|
|
171
|
+
and f.knockouts
|
|
172
|
+
and (chosen := [v for v in ([a] if isinstance(a, str) else a) if v in f.knockouts])
|
|
173
|
+
]
|
|
@@ -75,6 +75,7 @@ class Field:
|
|
|
75
75
|
one_of: str | None = None # an alternative: one of the fields with this key answers (ADR 0005)
|
|
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
|
+
knockouts: list[str] | None = None # the options whose choice ends the application (ADR 0005)
|
|
78
79
|
|
|
79
80
|
|
|
80
81
|
@dataclass(frozen=True)
|
|
@@ -106,6 +107,7 @@ class Result:
|
|
|
106
107
|
unloaded: list[str] = field(default_factory=list) # steps a profile read could not load yet
|
|
107
108
|
notice: str | None = None # telemetry's, the first time it is on (ADR 0016)
|
|
108
109
|
report: str | None = None # how to send a failed run's report, when it was not sent
|
|
110
|
+
warnings: list[str] = field(default_factory=list) # the knockout answers a fill gave
|
|
109
111
|
|
|
110
112
|
|
|
111
113
|
@dataclass
|
|
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
|