atsfill 0.0.4.dev58__tar.gz → 0.0.4.dev61__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 (38) hide show
  1. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/PKG-INFO +1 -1
  2. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/pyproject.toml +1 -1
  3. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/pyproject.toml.orig +1 -1
  4. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/manatal.py +16 -5
  5. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/oracle.py +12 -1
  6. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/paylocity.py +10 -0
  7. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/smartrecruiters.py +14 -0
  8. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/LICENSE +0 -0
  9. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/README.md +0 -0
  10. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/__init__.py +0 -0
  11. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/__init__.py +0 -0
  12. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/ashby.py +0 -0
  13. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/bamboohr.py +0 -0
  14. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/breezy.py +0 -0
  15. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/dayforce.py +0 -0
  16. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/greenhouse.py +0 -0
  17. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/hireology.py +0 -0
  18. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/jazzhr.py +0 -0
  19. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/lever.py +0 -0
  20. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/paycor.py +0 -0
  21. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/phenom.py +0 -0
  22. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/recruitee.py +0 -0
  23. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/teamtailor.py +0 -0
  24. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/workable.py +0 -0
  25. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/adapters/wttj.py +0 -0
  26. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/cli.py +0 -0
  27. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/core/__init__.py +0 -0
  28. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/core/accounts.py +0 -0
  29. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/core/adapter.py +0 -0
  30. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/core/attempts.py +0 -0
  31. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/core/browser.py +0 -0
  32. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/core/commands.py +0 -0
  33. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/core/matching.py +0 -0
  34. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/core/paths.py +0 -0
  35. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/core/registry.py +0 -0
  36. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/core/results.py +0 -0
  37. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/src/atsfill/py.typed +0 -0
  38. {atsfill-0.0.4.dev58 → atsfill-0.0.4.dev61}/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.dev58
3
+ Version: 0.0.4.dev61
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.dev58"
3
+ version = "0.0.4.dev61"
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.dev58"
3
+ version = "0.0.4.dev61"
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"
@@ -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
- self._entries_field(e) if _kind(e) == "entries" else _field(e, options(e))
276
- for e in entries
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 continue;
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
@@ -129,6 +129,7 @@ CONTACT = ("workHistory.mayWeContactSupervisor.0", "Yes")
129
129
  GRADUATED = ("educationHistory.didYouGraduate.0", "Yes")
130
130
  WORK: dict[str, list[Spec]] = {
131
131
  "companyName": [Spec("workHistory.companyName.0", "Company Name", "text")],
132
+ "companyWebsite": [Spec("workHistory.companyUrl.0", "Company Website", "text")],
132
133
  "position": [Spec("workHistory.position.0", "Position", "text")],
133
134
  "companyPhone": [Spec("workHistory.companyPhone.0", "Company Phone Number", "phone")],
134
135
  "responsibilities": [Spec("workHistory.responsibilities.0", "Responsibilities", "text")],
@@ -191,6 +192,15 @@ EDUCATION: dict[str, list[Spec]] = {
191
192
  GRADUATED,
192
193
  )
193
194
  ], # fmt: skip
195
+ "graduationDate": [
196
+ Spec(
197
+ "txt-educationHistory-graduationDate-0",
198
+ "Graduation Date (MM/YYYY)",
199
+ "month",
200
+ False,
201
+ when=GRADUATED,
202
+ )
203
+ ], # fmt: skip
194
204
  "schoolType": [
195
205
  Spec("educationHistory.type.0", "School Type", "dropdown", options="schoolTypeOptions")
196
206
  ],
@@ -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