atsfill 0.0.7.dev206__tar.gz → 0.0.7.dev209__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.7.dev206 → atsfill-0.0.7.dev209}/PKG-INFO +1 -1
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/pyproject.toml +1 -1
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/pyproject.toml.orig +1 -1
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/join.py +19 -3
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/phenom.py +19 -1
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/LICENSE +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/README.md +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/__init__.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/__init__.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/adp.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/adpmyjobs.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/ashby.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/bamboohr.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/breezy.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/dayforce.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/eightfold.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/greenhouse.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/gupy.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/hireology.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/hiringthing.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/hrmdirect.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/isolvedhire.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/jazzhr.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/jobaffinity.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/jobstreet.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/lever.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/manatal.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/oracle.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/paycor.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/paylocity.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/personio.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/pinpoint.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/recruitee.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/recruiterflow.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/rippling.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/smartrecruiters.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/successfactors.js +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/successfactors.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/taleo.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/teamtailor.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/ukg.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/welcomekit.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/wellfound.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/werecruit.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/workable.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/workatastartup.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/workday.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/wttj.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/adapters/zohorecruit.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/cli.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/__init__.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/accounts.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/adapter.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/attempts.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/browser.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/commands.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/conditions.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/countries.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/html.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/matching.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/otp.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/passwords.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/paths.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/registry.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/results.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/snapshot.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/core/telemetry.py +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/py.typed +0 -0
- {atsfill-0.0.7.dev206 → atsfill-0.0.7.dev209}/src/atsfill/registry.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atsfill
|
|
3
|
-
Version: 0.0.7.
|
|
3
|
+
Version: 0.0.7.dev209
|
|
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
|
|
@@ -12,6 +12,7 @@ import json
|
|
|
12
12
|
import re
|
|
13
13
|
import time
|
|
14
14
|
from collections.abc import Callable
|
|
15
|
+
from contextlib import suppress
|
|
15
16
|
from dataclasses import replace
|
|
16
17
|
from typing import Any
|
|
17
18
|
from urllib.parse import urlsplit
|
|
@@ -50,6 +51,7 @@ OPENING_TIMEOUT_S = 20
|
|
|
50
51
|
CONTINUE_TIMEOUT_S = 15
|
|
51
52
|
ADVANCE_TIMEOUT_MS = 3_000 # how long a clicked card takes to move the wizard on by itself
|
|
52
53
|
INPUT_TIMEOUT_MS = 3_000 # how long a step takes to show its own input
|
|
54
|
+
ANSWER_SHOWN = 200 # of an answer that is not the expected one, in the error
|
|
53
55
|
CLICKS = 3 # a click on a step still rendering is lost, on a card or on Continue
|
|
54
56
|
LEAVE_TIMEOUT_MS = 5_000 # how long Continue takes to leave a step, per click
|
|
55
57
|
POLL_MS = 250
|
|
@@ -80,6 +82,7 @@ LACKS = {
|
|
|
80
82
|
"candidate.phoneNumber": "a phone",
|
|
81
83
|
}
|
|
82
84
|
PDF = [".pdf", "application/pdf"]
|
|
85
|
+
REMOVE_FILE = "button:has(svg[name=TrashIcon])" # a stored file's Remove file, in any language
|
|
83
86
|
|
|
84
87
|
# The profile form's controls, by its field ids.
|
|
85
88
|
TEXTS = {
|
|
@@ -136,7 +139,11 @@ class Join:
|
|
|
136
139
|
side_effects = [
|
|
137
140
|
SideEffect("sign_up", "hiring_system", "login --create creates the candidate account"),
|
|
138
141
|
SideEffect("profile", "hiring_system", "profile --answers saves the account profile"),
|
|
139
|
-
SideEffect(
|
|
142
|
+
SideEffect(
|
|
143
|
+
"upload",
|
|
144
|
+
"hiring_system",
|
|
145
|
+
"a file is stored as soon as it is chosen, replacing the one the account keeps",
|
|
146
|
+
),
|
|
140
147
|
SideEffect("submit", "employer", "the application reaches the employer"),
|
|
141
148
|
]
|
|
142
149
|
|
|
@@ -215,7 +222,10 @@ class Join:
|
|
|
215
222
|
"query": QUESTIONS,
|
|
216
223
|
}
|
|
217
224
|
answered = page.evaluate(_FETCH, [f"{GRAPHQL}?operationName=JobScreeningQuestions", body])
|
|
218
|
-
|
|
225
|
+
listed = ((answered or {}).get("data") or {}).get("jobScreeningQuestions")
|
|
226
|
+
if not listed:
|
|
227
|
+
raise RuntimeError(f"JobScreeningQuestions answered {str(answered)[:ANSWER_SHOWN]}")
|
|
228
|
+
questions = listed["questions"]
|
|
219
229
|
self._questions = sorted(questions, key=lambda q: q.get("position") or 0)
|
|
220
230
|
browser.click(page.locator("[data-testid=ApplyButton]").locator("visible=true").first)
|
|
221
231
|
page.wait_for_url("**/apply/**", timeout=STEP_TIMEOUT_MS)
|
|
@@ -383,7 +393,13 @@ class Join:
|
|
|
383
393
|
def _file(self, page: Page, field_id: str, answer: Answer | None) -> None:
|
|
384
394
|
if not isinstance(answer, str):
|
|
385
395
|
return
|
|
386
|
-
page.locator("input[type=file]").
|
|
396
|
+
upload, stored = page.locator("input[type=file]"), page.locator(REMOVE_FILE)
|
|
397
|
+
with suppress(Timeout): # the step before's Remove file stays a moment
|
|
398
|
+
upload.first.wait_for(state="attached", timeout=INPUT_TIMEOUT_MS)
|
|
399
|
+
upload.or_(stored).first.wait_for(state="attached", timeout=STEP_TIMEOUT_MS)
|
|
400
|
+
if not upload.count(): # a file the account keeps: the answer replaces it
|
|
401
|
+
stored.click(timeout=STEP_TIMEOUT_MS)
|
|
402
|
+
upload.first.set_input_files(answer)
|
|
387
403
|
# once stored, the step shows the file in place of its input
|
|
388
404
|
page.wait_for_function(
|
|
389
405
|
"() => !document.querySelector('input[type=file]')", timeout=UPLOAD_TIMEOUT_MS
|
|
@@ -71,6 +71,7 @@ MAX_STEPS = 12
|
|
|
71
71
|
OPTIONS_TIMEOUT_S = 8
|
|
72
72
|
LOADERS_AT_ONCE = 8 # a loader request a country: 245 on one site
|
|
73
73
|
COUNTRIES_KEPT_S = 24 * 3600 # a site's country fields, kept between reads
|
|
74
|
+
RATE_LIMITED = "rate_limited: the careers site answers /applySubmit 403"
|
|
74
75
|
YES_NO = ["Yes", "No"]
|
|
75
76
|
NO_YES = ["No", "Yes"]
|
|
76
77
|
MONTH = " (MM/YYYY)"
|
|
@@ -373,11 +374,28 @@ class Phenom:
|
|
|
373
374
|
raise RuntimeError(f"no configuration for the wizard ({e})") from None
|
|
374
375
|
finally: # raised once the read ends, so the answers that came are kept
|
|
375
376
|
if refused:
|
|
376
|
-
raise Blocked(
|
|
377
|
+
raise Blocked(RATE_LIMITED)
|
|
377
378
|
self._raws = {raw["id"]: raw for raw in raws}
|
|
378
379
|
return fields
|
|
379
380
|
|
|
380
381
|
def fill(self, page: Page, answers: dict[str, Answer]) -> None:
|
|
382
|
+
"""Fills the steps. The firewall can start refusing mid-fill, which the page does not
|
|
383
|
+
show: the step just never passes."""
|
|
384
|
+
refused = False
|
|
385
|
+
|
|
386
|
+
def on_response(response: Any) -> None:
|
|
387
|
+
nonlocal refused
|
|
388
|
+
refused |= response.status == 403 and "/applySubmit" in response.url
|
|
389
|
+
|
|
390
|
+
page.on("response", on_response)
|
|
391
|
+
try:
|
|
392
|
+
self._fill_steps(page, answers)
|
|
393
|
+
finally:
|
|
394
|
+
page.remove_listener("response", on_response)
|
|
395
|
+
if refused:
|
|
396
|
+
raise Blocked(RATE_LIMITED)
|
|
397
|
+
|
|
398
|
+
def _fill_steps(self, page: Page, answers: dict[str, Answer]) -> None:
|
|
381
399
|
page.context.clear_cookies()
|
|
382
400
|
self._open(page, fresh=True)
|
|
383
401
|
self._accept_terms(page)
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|