atsfill 0.0.5.dev187__tar.gz → 0.0.5.dev189__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.dev187 → atsfill-0.0.5.dev189}/PKG-INFO +1 -1
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/pyproject.toml +1 -1
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/pyproject.toml.orig +1 -1
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/phenom.py +46 -5
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/core/paths.py +8 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/LICENSE +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/README.md +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/__init__.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/__init__.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/adp.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/adpmyjobs.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/ashby.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/bamboohr.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/breezy.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/dayforce.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/eightfold.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/greenhouse.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/gupy.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/hireology.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/hiringthing.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/hrmdirect.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/isolvedhire.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/jazzhr.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/jobaffinity.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/jobstreet.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/join.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/lever.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/manatal.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/oracle.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/paycor.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/paylocity.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/personio.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/pinpoint.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/recruitee.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/recruiterflow.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/rippling.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/smartrecruiters.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/successfactors.js +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/successfactors.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/taleo.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/teamtailor.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/ukg.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/welcomekit.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/workable.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/workatastartup.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/workday.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/wttj.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/adapters/zohorecruit.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/cli.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/core/__init__.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/core/accounts.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/core/adapter.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/core/attempts.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/core/browser.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/core/commands.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/core/conditions.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/core/html.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/core/matching.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/core/otp.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/core/passwords.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/core/registry.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/core/results.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/core/snapshot.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/core/telemetry.py +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/src/atsfill/py.typed +0 -0
- {atsfill-0.0.5.dev187 → atsfill-0.0.5.dev189}/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.dev189
|
|
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
|
|
@@ -8,6 +8,7 @@ fields, dependent lists): a request read or a browser read interprets them witho
|
|
|
8
8
|
step. Filling walks the wizard on a fresh session, each step's answers read back before Next.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import hashlib
|
|
11
12
|
import http.cookiejar
|
|
12
13
|
import json
|
|
13
14
|
import re
|
|
@@ -23,7 +24,7 @@ from pathlib import PurePath
|
|
|
23
24
|
from typing import Any
|
|
24
25
|
from urllib.parse import parse_qs, urlsplit
|
|
25
26
|
|
|
26
|
-
from atsfill.core import browser, matching
|
|
27
|
+
from atsfill.core import browser, matching, paths
|
|
27
28
|
from atsfill.core.adapter import Gap, NotTaken, Unsupported, known_gap
|
|
28
29
|
from atsfill.core.browser import HTTP_TIMEOUT_S, BrowserError, Locator, Overlaid, Page, Timeout
|
|
29
30
|
from atsfill.core.results import (
|
|
@@ -67,10 +68,12 @@ PICK_TRIES = 3
|
|
|
67
68
|
MAX_STEPS = 12
|
|
68
69
|
OPTIONS_TIMEOUT_S = 8
|
|
69
70
|
LOADERS_AT_ONCE = 8 # a loader request a country: 245 on one site
|
|
71
|
+
COUNTRIES_KEPT_S = 24 * 3600 # a site's country fields, kept between reads
|
|
70
72
|
YES_NO = ["Yes", "No"]
|
|
71
73
|
NO_YES = ["No", "Yes"]
|
|
72
74
|
MONTH = " (MM/YYYY)"
|
|
73
75
|
YEAR = " (year)"
|
|
76
|
+
NUMBER = " (a number)" # an input that takes only digits
|
|
74
77
|
PICKER = ".react-datepicker"
|
|
75
78
|
MONTHS: list[str] = [
|
|
76
79
|
"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"
|
|
@@ -363,7 +366,7 @@ class Phenom:
|
|
|
363
366
|
self._open(page, fresh=True)
|
|
364
367
|
self._accept_terms(page)
|
|
365
368
|
self._kept, self._shown = {}, {}
|
|
366
|
-
if RESUME_ID in answers and page
|
|
369
|
+
if RESUME_ID in answers and _shows_upload(page):
|
|
367
370
|
_upload(page, _one(answers[RESUME_ID]))
|
|
368
371
|
self._kept[RESUME_ID] = PurePath(_one(answers[RESUME_ID])).name
|
|
369
372
|
for number in range(1, MAX_STEPS + 1):
|
|
@@ -662,7 +665,8 @@ class _Config:
|
|
|
662
665
|
if html == "daterangepicker" or widget == "daterangepicker":
|
|
663
666
|
return self.date_range(p, u, path, nav, when)
|
|
664
667
|
if p.get("type") == "array" and get(p, "items.properties") and html not in WIDGETS:
|
|
665
|
-
|
|
668
|
+
shown = p.get("minItems"), p.get("defaultEntries"), u.get("minItemsRequired")
|
|
669
|
+
if not any(int(n or 0) for n in shown):
|
|
666
670
|
return [] # entries only once Add is clicked
|
|
667
671
|
return self.object(p["items"], u.get("items") or {}, f"{path}[0].", nav, False, when)
|
|
668
672
|
if p.get("type") == "object" or html in ("object", "phoneNumber_v2"):
|
|
@@ -679,6 +683,7 @@ class _Config:
|
|
|
679
683
|
# a signature's date: today's, the only one the page takes
|
|
680
684
|
"prefilled": date.today().isoformat() if p.get("defaultTodayDate") is True else None,
|
|
681
685
|
"max_length": p.get("maxLength") if isinstance(p.get("maxLength"), int) else None,
|
|
686
|
+
"number": kind == "text" and _number(p, html, widget),
|
|
682
687
|
}
|
|
683
688
|
self.labels[path] = raw["label"]
|
|
684
689
|
if kind in ("select", "typeahead", "radio", "checkboxes"):
|
|
@@ -1005,7 +1010,7 @@ class _Config:
|
|
|
1005
1010
|
values, texts = self.listed.get(path) or ([], [])
|
|
1006
1011
|
held, _ = self.prefilled(path)
|
|
1007
1012
|
codes = sorted(values, key=lambda c: c != held)
|
|
1008
|
-
parts = self.
|
|
1013
|
+
parts = self.country_parts(payload, codes)
|
|
1009
1014
|
by_id: dict[str, dict[Any, dict[str, Any]]] = {}
|
|
1010
1015
|
for code, part in zip(codes, parts, strict=True):
|
|
1011
1016
|
part = get(part, "applyGetReferences.data")
|
|
@@ -1017,6 +1022,24 @@ class _Config:
|
|
|
1017
1022
|
shown = dict(zip(values, texts, strict=False))
|
|
1018
1023
|
return [_by_country(path, rows, codes, shown) for rows in by_id.values()]
|
|
1019
1024
|
|
|
1025
|
+
def country_parts(self, payload: str, codes: list[Any]) -> list[Any]:
|
|
1026
|
+
"""Each country's loader answer, kept a day for the site: a read posts one a country,
|
|
1027
|
+
and two reads in a row get `/applySubmit` refused (403) on one site."""
|
|
1028
|
+
key = hashlib.sha1(f"{self.ph.get('locale')} {payload}".encode()).hexdigest()[:12]
|
|
1029
|
+
path = paths.cache_dir() / "phenom" / f"{self.ph.get('refNum')}-{key}.json"
|
|
1030
|
+
kept: dict[str, Any] = {}
|
|
1031
|
+
with suppress(OSError, ValueError):
|
|
1032
|
+
if time.time() - path.stat().st_mtime < COUNTRIES_KEPT_S:
|
|
1033
|
+
kept = json.loads(path.read_text())
|
|
1034
|
+
missing = [c for c in codes if str(c) not in kept]
|
|
1035
|
+
if missing:
|
|
1036
|
+
parts = self.calls(payload, [{"countryCode": c} for c in missing])
|
|
1037
|
+
kept |= {str(c): p for c, p in zip(missing, parts, strict=True) if p is not None}
|
|
1038
|
+
with suppress(OSError):
|
|
1039
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1040
|
+
path.write_text(json.dumps(kept))
|
|
1041
|
+
return [kept.get(str(c)) for c in codes]
|
|
1042
|
+
|
|
1020
1043
|
def date_range(
|
|
1021
1044
|
self, p: dict, u: dict, path: str, nav: Any, when: Condition | None
|
|
1022
1045
|
) -> list[dict[str, Any]]:
|
|
@@ -1064,7 +1087,10 @@ class _Config:
|
|
|
1064
1087
|
held = get(
|
|
1065
1088
|
{"phApp": self.ph}, get(self.config, f"uiSchema.{name}.preFillWindowObj") or ""
|
|
1066
1089
|
)
|
|
1067
|
-
held
|
|
1090
|
+
if held is None: # the job's data by its name (`jobCountry`), or the page's locale
|
|
1091
|
+
held = ({"locale": self.ph.get("locale")} | self.ddo).get(name)
|
|
1092
|
+
if "pattern" in test: # a job location naming a county
|
|
1093
|
+
return isinstance(held, str) and re.search(test["pattern"], held) is not None
|
|
1068
1094
|
return True if values is None else held in values
|
|
1069
1095
|
return Is(f"{path}{name}", values)
|
|
1070
1096
|
|
|
@@ -1229,6 +1255,11 @@ def _kind(p: dict, html: Any, widget: Any, path: str) -> str:
|
|
|
1229
1255
|
return "text"
|
|
1230
1256
|
|
|
1231
1257
|
|
|
1258
|
+
def _number(p: dict, html: Any, widget: Any) -> bool:
|
|
1259
|
+
"""Whether the input takes only digits (`input[type=number]`)."""
|
|
1260
|
+
return p.get("type") in ("integer", "number") or bool({html, widget} & {"number", "updown"})
|
|
1261
|
+
|
|
1262
|
+
|
|
1232
1263
|
def _picker(form: str) -> str:
|
|
1233
1264
|
"""The calendar a format picks from: `date` (a day), `month` (`MM/yyyy`), `year`."""
|
|
1234
1265
|
tokens = {t[0] for t in re.findall(r"y+|m+|d+", form.lower())}
|
|
@@ -1292,6 +1323,7 @@ def _field(raw: dict[str, Any]) -> Field:
|
|
|
1292
1323
|
if kind == "checkbox": # an optional box starts clear
|
|
1293
1324
|
options = YES_NO if raw["required"] else NO_YES
|
|
1294
1325
|
label = raw["label"] + {"month": MONTH, "year": YEAR}.get(kind, "")
|
|
1326
|
+
label += NUMBER if raw.get("number") else ""
|
|
1295
1327
|
sets = raw.get("option_sets")
|
|
1296
1328
|
field_type = (
|
|
1297
1329
|
FieldType.TYPEAHEAD if kind == "typeahead" and not options and not sets else TYPES[kind]
|
|
@@ -1449,6 +1481,15 @@ def _one(answer: Answer) -> str:
|
|
|
1449
1481
|
return answer if isinstance(answer, str) else answer[0]
|
|
1450
1482
|
|
|
1451
1483
|
|
|
1484
|
+
def _shows_upload(page: Page) -> bool:
|
|
1485
|
+
"""Whether the resume upload shows: on one site a moment after the form, past its terms."""
|
|
1486
|
+
try:
|
|
1487
|
+
page.locator(RESUME).first.wait_for(state="attached", timeout=FIELD_TIMEOUT_MS)
|
|
1488
|
+
except Timeout:
|
|
1489
|
+
return False
|
|
1490
|
+
return True
|
|
1491
|
+
|
|
1492
|
+
|
|
1452
1493
|
def _upload(page: Page, path: str) -> None:
|
|
1453
1494
|
"""Attaches the resume, taken once Phenom's `/fileUpload` answers or the page shows its
|
|
1454
1495
|
name, and waits for its parse, which fills and clears fields when it comes back: until
|
|
@@ -60,6 +60,14 @@ def _move_to_default(data: Path) -> None:
|
|
|
60
60
|
(data / name).rename(default / name)
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
def cache_dir() -> Path:
|
|
64
|
+
"""Public data atsfill can fetch again, kept to spare a hiring system the requests:
|
|
65
|
+
`$XDG_CACHE_HOME/atsfill` on Linux (`~/.cache/atsfill`), `~/.atsfill/cache` elsewhere."""
|
|
66
|
+
if xdg := os.environ.get("XDG_CACHE_HOME"):
|
|
67
|
+
return Path(xdg) / "atsfill"
|
|
68
|
+
return Path.home() / (".cache/atsfill" if LINUX else ".atsfill/cache")
|
|
69
|
+
|
|
70
|
+
|
|
63
71
|
def config_file() -> Path:
|
|
64
72
|
if xdg := os.environ.get("XDG_CONFIG_HOME"):
|
|
65
73
|
return Path(xdg) / "atsfill/config.toml"
|
|
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
|