atsfill 0.0.4.dev58__tar.gz → 0.0.4.dev60__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.4.dev58 → atsfill-0.0.4.dev60}/PKG-INFO +1 -1
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/pyproject.toml +1 -1
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/pyproject.toml.orig +1 -1
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/manatal.py +16 -5
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/oracle.py +12 -1
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/smartrecruiters.py +14 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/LICENSE +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/README.md +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/__init__.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/__init__.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/ashby.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/bamboohr.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/breezy.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/dayforce.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/greenhouse.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/hireology.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/jazzhr.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/lever.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/paycor.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/paylocity.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/phenom.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/recruitee.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/teamtailor.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/workable.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/adapters/wttj.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/cli.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/core/__init__.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/core/accounts.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/core/adapter.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/core/attempts.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/core/browser.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/core/commands.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/core/matching.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/core/paths.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/core/registry.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/core/results.py +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/src/atsfill/py.typed +0 -0
- {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev60}/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.
|
|
3
|
+
Version: 0.0.4.dev60
|
|
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
|
|
@@ -34,6 +34,7 @@ LISTS: dict[str, tuple[str, Callable[[dict[str, Any]], str]]] = {
|
|
|
34
34
|
"industries": ("clients/{client}/industries/", lambda n: n["name"]),
|
|
35
35
|
}
|
|
36
36
|
ENTRIES = ("educations", "experiences") # a sub-form of entries
|
|
37
|
+
OTHER = "Other" # the radio or checkbox list option that opens a text box once picked
|
|
37
38
|
SHORT = {"full_name", "email", "phone_number", "city", "address", "latest_degree",
|
|
38
39
|
"latest_university", "current_company", "current_department", "current_position",
|
|
39
40
|
"current_benefits", "expected_benefits", "skype", "contact"} # fmt: skip
|
|
@@ -153,7 +154,7 @@ def _options(entry: dict[str, Any], client: str) -> list[str] | None:
|
|
|
153
154
|
|
|
154
155
|
class Manatal:
|
|
155
156
|
name = "manatal"
|
|
156
|
-
known_gaps = {"entries": Gap.RENDERING}
|
|
157
|
+
known_gaps = {"entries": Gap.RENDERING, "other": Gap.RENDERING}
|
|
157
158
|
side_effects = [
|
|
158
159
|
SideEffect("submit", "hiring_system", "the resume and attachments are uploaded"),
|
|
159
160
|
SideEffect("submit", "employer", "the application reaches the employer"),
|
|
@@ -271,10 +272,14 @@ class Manatal:
|
|
|
271
272
|
terms: bool,
|
|
272
273
|
options: Callable[[dict[str, Any]], list[str] | None],
|
|
273
274
|
) -> list[Field]:
|
|
274
|
-
fields = [
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
275
|
+
fields = []
|
|
276
|
+
for entry in entries:
|
|
277
|
+
if _kind(entry) == "entries":
|
|
278
|
+
fields.append(self._entries_field(entry))
|
|
279
|
+
continue
|
|
280
|
+
fields.append(field := _field(entry, options(entry)))
|
|
281
|
+
if _kind(entry) in ("radio", "checkbox") and OTHER in (field.options or []):
|
|
282
|
+
fields.append(self._other_field(field))
|
|
278
283
|
if terms:
|
|
279
284
|
fields.append(Field(TERMS, TERMS_LABEL, FieldType.CHOICE, True, YES_NO))
|
|
280
285
|
return fields
|
|
@@ -287,6 +292,12 @@ class Manatal:
|
|
|
287
292
|
fid, entry["label"], FieldType.TEXT, bool(entry["is_required"]), unfillable=gap
|
|
288
293
|
)
|
|
289
294
|
|
|
295
|
+
def _other_field(self, field: Field) -> Field:
|
|
296
|
+
"""The text box an answer of Other opens, asked only then."""
|
|
297
|
+
fid = f"{field.id}.other"
|
|
298
|
+
gap = known_gap(self, "other", f"{fid} is the text box an answer of {OTHER} opens")
|
|
299
|
+
return Field(fid, f"{field.label}: {OTHER}", FieldType.TEXT, False, unfillable=gap)
|
|
300
|
+
|
|
290
301
|
@staticmethod
|
|
291
302
|
def _shown(page: Page, entry: dict[str, Any]) -> list[str] | None:
|
|
292
303
|
"""The options the page shows for a choice, without the empty placeholder."""
|
|
@@ -65,6 +65,7 @@ _READ = """() => {
|
|
|
65
65
|
const text = e => (e?.textContent || "").replace(/\\s+/g, " ").trim().replace(/\\s*\\*$/, "");
|
|
66
66
|
const stable = s => s?.replace(/^labelText-/, "").replace(/-\\d+(-label)?$/, "");
|
|
67
67
|
const shown = e => !!e.getClientRects().length;
|
|
68
|
+
const CONTROL = "input:not([type=hidden]), textarea, select, button, [role], [tabindex]";
|
|
68
69
|
const fields = [];
|
|
69
70
|
const add = f => { if (f.id && !fields.some(g => g.id === f.id)) fields.push(f); };
|
|
70
71
|
for (const block of document.querySelectorAll("apply-flow-block")) {
|
|
@@ -139,7 +140,12 @@ _READ = """() => {
|
|
|
139
140
|
field.kind = input.type === "email" ? "email" : "text";
|
|
140
141
|
field.value = input.value;
|
|
141
142
|
field.maxLength = input.maxLength > 0 ? input.maxLength : null;
|
|
142
|
-
} else
|
|
143
|
+
} else if (input?.disabled || !row.querySelector(CONTROL)) {
|
|
144
|
+
continue; // text, or a value the page sets
|
|
145
|
+
} else {
|
|
146
|
+
field.kind = "unhandled";
|
|
147
|
+
field.id ||= `${blockId}:${field.label}`;
|
|
148
|
+
}
|
|
143
149
|
add(field);
|
|
144
150
|
}
|
|
145
151
|
}
|
|
@@ -373,6 +379,11 @@ class Oracle:
|
|
|
373
379
|
if raw["kind"] == "tiles":
|
|
374
380
|
gap = known_gap(self, "entries", f"{raw['label']!r} needs entries added")
|
|
375
381
|
return Field(raw["id"], raw["label"], FieldType.TEXT, raw["required"], unfillable=gap)
|
|
382
|
+
if raw["kind"] == "unhandled":
|
|
383
|
+
unhandled = f"unhandled_rendering: {raw['label']!r} has no control atsfill knows"
|
|
384
|
+
return Field(
|
|
385
|
+
raw["id"], raw["label"], FieldType.TEXT, raw["required"], unfillable=unhandled
|
|
386
|
+
)
|
|
376
387
|
if raw["kind"] == "email":
|
|
377
388
|
self._kinds[EMAIL] = "email"
|
|
378
389
|
return None
|
|
@@ -61,6 +61,8 @@ PROFILE_TYPES = {
|
|
|
61
61
|
"phone": FieldType.PHONE,
|
|
62
62
|
"resume": FieldType.FILE,
|
|
63
63
|
}
|
|
64
|
+
# The profile's optional photo, an image upload beside the name.
|
|
65
|
+
PHOTO = "oc-avatar input[type=file]"
|
|
64
66
|
# Sections of the profile that take entries through their own sub-form.
|
|
65
67
|
ENTRIES = ("oc-experience", "oc-education")
|
|
66
68
|
# Screening question types: one field per entry of the question's `fields`.
|
|
@@ -111,6 +113,7 @@ class SmartRecruiters:
|
|
|
111
113
|
"entries": Gap.RENDERING,
|
|
112
114
|
"attachments": Gap.RENDERING,
|
|
113
115
|
"languages": Gap.RENDERING,
|
|
116
|
+
"photo": Gap.RENDERING,
|
|
114
117
|
}
|
|
115
118
|
side_effects = [
|
|
116
119
|
SideEffect(
|
|
@@ -150,6 +153,8 @@ class SmartRecruiters:
|
|
|
150
153
|
self._questions = {q["id"]: q for q in screening.get("questions") or []}
|
|
151
154
|
self._conditions = screening.get("conditionalConfigurations") or []
|
|
152
155
|
fields = [f for f in (_profile_field(page, *row) for row in PROFILE) if f]
|
|
156
|
+
if photo := self._photo(page):
|
|
157
|
+
fields.append(photo)
|
|
153
158
|
fields += [f for f in (self._entries(page, section) for section in ENTRIES) if f]
|
|
154
159
|
for question in self._questions.values():
|
|
155
160
|
fields += self._question_fields(question)
|
|
@@ -248,6 +253,15 @@ class SmartRecruiters:
|
|
|
248
253
|
if i in self._fields and not self._fields[i].unfillable
|
|
249
254
|
]
|
|
250
255
|
|
|
256
|
+
def _photo(self, page: Page) -> Field | None:
|
|
257
|
+
"""The profile photo, shown: an image upload atsfill does not fill."""
|
|
258
|
+
upload = page.locator(PHOTO).first
|
|
259
|
+
if not upload.count():
|
|
260
|
+
return None
|
|
261
|
+
label = upload.get_attribute("aria-label") or "Photo"
|
|
262
|
+
gap = known_gap(self, "photo", "photo is a profile photo")
|
|
263
|
+
return Field("photo", label, FieldType.FILE, False, unfillable=gap)
|
|
264
|
+
|
|
251
265
|
def _entries(self, page: Page, section: str) -> Field | None:
|
|
252
266
|
"""Experience or education, shown: entries added through a sub-form."""
|
|
253
267
|
host = page.locator(section).first
|
|
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
|