atsfill 0.0.4.dev86__tar.gz → 0.0.4.dev88__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 (45) hide show
  1. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/PKG-INFO +1 -1
  2. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/pyproject.toml +1 -1
  3. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/pyproject.toml.orig +1 -1
  4. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/oracle.py +10 -10
  5. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/rippling.py +6 -2
  6. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/core/browser.py +9 -3
  7. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/LICENSE +0 -0
  8. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/README.md +0 -0
  9. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/__init__.py +0 -0
  10. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/__init__.py +0 -0
  11. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/ashby.py +0 -0
  12. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/bamboohr.py +0 -0
  13. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/breezy.py +0 -0
  14. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/dayforce.py +0 -0
  15. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/eightfold.py +0 -0
  16. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/greenhouse.py +0 -0
  17. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/hireology.py +0 -0
  18. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/hiringthing.py +0 -0
  19. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/jazzhr.py +0 -0
  20. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/lever.py +0 -0
  21. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/manatal.py +0 -0
  22. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/paycor.py +0 -0
  23. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/paylocity.py +0 -0
  24. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/personio.py +0 -0
  25. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/phenom.py +0 -0
  26. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/pinpoint.py +0 -0
  27. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/recruitee.py +0 -0
  28. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/smartrecruiters.py +0 -0
  29. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/teamtailor.py +0 -0
  30. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/workable.py +0 -0
  31. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/wttj.py +0 -0
  32. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/adapters/zohorecruit.py +0 -0
  33. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/cli.py +0 -0
  34. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/core/__init__.py +0 -0
  35. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/core/accounts.py +0 -0
  36. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/core/adapter.py +0 -0
  37. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/core/attempts.py +0 -0
  38. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/core/commands.py +0 -0
  39. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/core/matching.py +0 -0
  40. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/core/passwords.py +0 -0
  41. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/core/paths.py +0 -0
  42. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/core/registry.py +0 -0
  43. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/core/results.py +0 -0
  44. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/py.typed +0 -0
  45. {atsfill-0.0.4.dev86 → atsfill-0.0.4.dev88}/src/atsfill/registry.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atsfill
3
- Version: 0.0.4.dev86
3
+ Version: 0.0.4.dev88
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atsfill"
3
- version = "0.0.4.dev86"
3
+ version = "0.0.4.dev88"
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.4.dev86"
3
+ version = "0.0.4.dev88"
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"
@@ -255,7 +255,6 @@ class Oracle:
255
255
  self._picked: dict[str, str] = {} # a geography field's id: the item picked in it
256
256
  self._months: list[str] = []
257
257
  self._job = ""
258
- self._uploads: dict[str, str] = {}
259
258
 
260
259
  def detect(self, url: str, page: Page | None) -> bool:
261
260
  return JOB.search(urlsplit(url).path) is not None
@@ -311,14 +310,14 @@ class Oracle:
311
310
  if COUNTRY in on_page: # the address below it follows the country's format
312
311
  self._fill(page, COUNTRY, answers[COUNTRY])
313
312
  _settle(page)
314
- for field_id in [i for i in on_page if i != COUNTRY]:
313
+ # Files go last: a click while one attaches can be lost, and once attached its
314
+ # label takes the focus back from a field typed after it.
315
+ files = [i for i in on_page if self._kinds[i] == "file"]
316
+ for field_id in [i for i in on_page if i not in (COUNTRY, *files)] + files:
315
317
  self._fill(page, field_id, answers[field_id])
316
- self._uploads = {
317
- i: PurePath(a).name
318
- for i, a in answers.items()
319
- if self._fields[i].type is FieldType.FILE and isinstance(a, str)
320
- }
321
- self._wait_for_uploads(page)
318
+ self._wait_for_uploads(
319
+ page, {i: PurePath(a).name for i in files if isinstance(a := answers[i], str)}
320
+ )
322
321
 
323
322
  def read_back(self, page: Page) -> dict[str, Answer]:
324
323
  values: dict[str, Any] = page.evaluate(_READ_BACK, self._kinds)
@@ -490,8 +489,9 @@ class Oracle:
490
489
  def _file_input(self, page: Page, field_id: str) -> Locator:
491
490
  return page.locator(f"{_upload(field_id)} input[type=file]").first
492
491
 
493
- def _wait_for_uploads(self, page: Page) -> None:
494
- for field_id, name in self._uploads.items():
492
+ def _wait_for_uploads(self, page: Page, uploads: dict[str, str]) -> None:
493
+ """Waits for each file (by field id: its name) to show attached."""
494
+ for field_id, name in uploads.items():
495
495
  selector = f"{_upload(field_id)} :is({ATTACHED})"
496
496
  done = page.locator(selector, has_text=name)
497
497
  try:
@@ -218,8 +218,12 @@ class Rippling:
218
218
  timeout=PARSE_TIMEOUT_MS,
219
219
  ):
220
220
  _attach(page, "resume", answers["resume"])
221
- with suppress(Timeout): # its notice covers the fields below it while it shows
222
- page.get_by_text(PARSED_NOTICE).wait_for(state="hidden", timeout=NOTICE_TIMEOUT_MS)
221
+ # The page writes the parse and shows its notice a moment after the response; the
222
+ # notice then covers the fields below it while it shows.
223
+ notice = page.get_by_text(PARSED_NOTICE)
224
+ with suppress(Timeout):
225
+ notice.wait_for(timeout=NOTICE_TIMEOUT_MS)
226
+ notice.wait_for(state="hidden", timeout=NOTICE_TIMEOUT_MS)
223
227
  for field_id, field in self._fields.items():
224
228
  if field_id == "resume" or field_id not in answers:
225
229
  continue
@@ -63,6 +63,8 @@ INCLUDE = re.compile(r"^!#include (\S+)$", re.M)
63
63
  # A page holding a banner the lists hide: until they do, the blocker is still starting.
64
64
  PROBE = ("https://blocker.invalid/", '<div id="cookie-banner">probe</div>')
65
65
  BLOCKER_TIMEOUT_MS = 30_000 # its lists take seconds to compile on each launch, more under load
66
+ OVERLAY_GRACE_MS = 1_000 # how long a cover may stay on a click target before it counts
67
+ OVERLAY_POLL_MS = 100
66
68
 
67
69
 
68
70
  class OutOfTime(Exception):
@@ -338,7 +340,11 @@ def overlay_on(target: Locator) -> str | None:
338
340
 
339
341
 
340
342
  def click(target: Locator) -> None:
341
- """Clicks the target, raising `Overlaid` when an overlay is on top of it."""
342
- if overlay := overlay_on(target):
343
- raise Overlaid(overlay)
343
+ """Clicks the target, raising `Overlaid` when an overlay stays on top of it for the grace
344
+ period: a toast or a spinner that goes within it is waited out."""
345
+ deadline = time.monotonic() + OVERLAY_GRACE_MS / 1000
346
+ while overlay := overlay_on(target):
347
+ if time.monotonic() >= deadline:
348
+ raise Overlaid(overlay)
349
+ target.page.wait_for_timeout(OVERLAY_POLL_MS)
344
350
  target.click()
File without changes
File without changes