atsfill 0.0.4.dev62__tar.gz → 0.0.4.dev63__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.dev62 → atsfill-0.0.4.dev63}/PKG-INFO +1 -1
  2. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/pyproject.toml +1 -1
  3. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/pyproject.toml.orig +1 -1
  4. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/manatal.py +51 -11
  5. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/LICENSE +0 -0
  6. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/README.md +0 -0
  7. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/__init__.py +0 -0
  8. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/__init__.py +0 -0
  9. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/ashby.py +0 -0
  10. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/bamboohr.py +0 -0
  11. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/breezy.py +0 -0
  12. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/dayforce.py +0 -0
  13. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/greenhouse.py +0 -0
  14. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/hireology.py +0 -0
  15. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/jazzhr.py +0 -0
  16. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/lever.py +0 -0
  17. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/oracle.py +0 -0
  18. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/paycor.py +0 -0
  19. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/paylocity.py +0 -0
  20. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/phenom.py +0 -0
  21. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/recruitee.py +0 -0
  22. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/smartrecruiters.py +0 -0
  23. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/teamtailor.py +0 -0
  24. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/workable.py +0 -0
  25. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/adapters/wttj.py +0 -0
  26. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/cli.py +0 -0
  27. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/core/__init__.py +0 -0
  28. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/core/accounts.py +0 -0
  29. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/core/adapter.py +0 -0
  30. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/core/attempts.py +0 -0
  31. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/core/browser.py +0 -0
  32. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/core/commands.py +0 -0
  33. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/core/matching.py +0 -0
  34. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/core/paths.py +0 -0
  35. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/core/registry.py +0 -0
  36. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/core/results.py +0 -0
  37. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/src/atsfill/py.typed +0 -0
  38. {atsfill-0.0.4.dev62 → atsfill-0.0.4.dev63}/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.dev62
3
+ Version: 0.0.4.dev63
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.dev62"
3
+ version = "0.0.4.dev63"
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.dev62"
3
+ version = "0.0.4.dev63"
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,9 @@ 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
+ # A salary's amount comes with a currency and a frequency select, each with an id of its own.
38
+ SALARIES = {"expected_salary": "expected", "current_salary": "current"}
39
+ FREQUENCIES = ["Hourly", "Daily", "Weekly", "Monthly", "Yearly"]
37
40
  OTHER = "Other" # the radio or checkbox list option that opens a text box once picked
38
41
  SHORT = {"full_name", "email", "phone_number", "city", "address", "latest_degree",
39
42
  "latest_university", "current_company", "current_department", "current_position",
@@ -43,8 +46,8 @@ LOAD_TIMEOUT_MS = 30_000
43
46
  SUBMIT_TIMEOUT_MS = 120_000
44
47
 
45
48
  # The answer each input holds, by field id, read as the page reads them on Apply.
46
- _READ = """fields => Object.fromEntries(fields.map(([id, kind]) => {
47
- const el = document.getElementById('field' + id);
49
+ _READ = """fields => Object.fromEntries(fields.map(([id, kind, dom]) => {
50
+ const el = document.getElementById(dom);
48
51
  const named = [...document.querySelectorAll(`input[name="${id}"]`)];
49
52
  const texts = s => [...s.selectedOptions].filter(o => o.value).map(o => o.text.trim());
50
53
  switch (kind) {
@@ -107,6 +110,24 @@ def _kind(entry: dict[str, Any]) -> str:
107
110
  return "text"
108
111
 
109
112
 
113
+ def _salary(entry: dict[str, Any]) -> list[tuple[str, str]]:
114
+ """A salary's currency and frequency selects: the field id each is listed as, and its own id."""
115
+ prefix = SALARIES.get(entry["client_field_slug"])
116
+ if prefix is None or entry["field_type"] != "candidate_field":
117
+ return []
118
+ return [(f"{entry['id']}.{part}", f"{prefix}_{part}") for part in ("currency", "frequency")]
119
+
120
+
121
+ def _inputs(entries: list[dict[str, Any]]) -> list[tuple[str, str, str]]:
122
+ """Each input the page reads on Apply: its field id, its kind and its element id."""
123
+ inputs = []
124
+ for entry in entries:
125
+ if (kind := _kind(entry)) != "entries":
126
+ inputs.append((str(entry["id"]), kind, f"field{entry['id']}"))
127
+ inputs += [(fid, "select", dom) for fid, dom in _salary(entry)]
128
+ return inputs
129
+
130
+
110
131
  def _field(entry: dict[str, Any], options: list[str] | None) -> Field:
111
132
  """The field an entry of the endpoint lists, a sub-form of entries aside."""
112
133
  kind, slug = _kind(entry), entry["client_field_slug"]
@@ -182,7 +203,10 @@ class Manatal:
182
203
  return None
183
204
  entries = _get(f"jobs/{job.group(1)}/application-form/")
184
205
  terms = bool(TERMS_REQUIRED.search(html))
185
- return self._fields(entries, terms, lambda e: _options(e, client))
206
+ salary = any(_salary(e) for e in entries)
207
+ currencies = [c["name"] for c in _get("currencies/")] if salary else []
208
+ salaries = lambda dom: currencies if dom.endswith("currency") else FREQUENCIES # noqa: E731
209
+ return self._fields(entries, terms, lambda e: _options(e, client), salaries)
186
210
  except OSError, ValueError, KeyError, TypeError:
187
211
  return None
188
212
 
@@ -199,19 +223,31 @@ class Manatal:
199
223
  raise Unsupported(f"posting_gone: the form answered {response.status}")
200
224
  self._entries = response.json()
201
225
  page.wait_for_selector("#submit-id-submit", timeout=LOAD_TIMEOUT_MS)
202
- # The lists of a multiple select come in their own requests, once the form is drawn.
226
+ # The lists of a multiple select and the currencies come in their own requests, once the
227
+ # form is drawn.
203
228
  page.wait_for_function(
204
229
  "() => [...document.querySelectorAll('select.multiselect')]"
205
- ".every(s => s.options.length && s.classList.contains('select2-hidden-accessible'))",
230
+ ".every(s => s.options.length && s.classList.contains('select2-hidden-accessible'))"
231
+ " && [...document.querySelectorAll('#expected_currency, #current_currency')]"
232
+ ".every(s => s.options.length)",
206
233
  timeout=LOAD_TIMEOUT_MS,
207
234
  )
208
235
  terms = page.locator(f'input[name="{TERMS}"]').count() > 0
209
- return self._fields(self._entries, terms, lambda e: self._shown(page, e))
236
+ return self._fields(
237
+ self._entries,
238
+ terms,
239
+ lambda e: self._shown(page, e),
240
+ lambda dom: page.locator(f"#{dom} option").evaluate_all(
241
+ "os => os.map(o => o.text.trim())"
242
+ ),
243
+ )
210
244
 
211
245
  def fill(self, page: Page, answers: dict[str, Answer]) -> None:
212
- kinds = {str(e["id"]): _kind(e) for e in self._entries} | {TERMS: "terms"}
246
+ inputs = _inputs(self._entries)
247
+ kinds = {fid: kind for fid, kind, _ in inputs} | {TERMS: "terms"}
248
+ doms = {fid: dom for fid, _, dom in inputs}
213
249
  for field_id, answer in answers.items():
214
- box = page.locator(f"#field{field_id}")
250
+ box = page.locator(f"#{doms.get(field_id, f'field{field_id}')}")
215
251
  named = page.locator(f'input[name="{field_id}"]')
216
252
  match kinds.get(field_id), answer:
217
253
  case "text", str():
@@ -247,10 +283,10 @@ class Manatal:
247
283
  )
248
284
 
249
285
  def read_back(self, page: Page) -> dict[str, Answer]:
250
- kinds = [[str(e["id"]), _kind(e)] for e in self._entries if _kind(e) != "entries"]
286
+ inputs = [list(i) for i in _inputs(self._entries)]
251
287
  if page.locator(f'input[name="{TERMS}"]').count():
252
- kinds.append([TERMS, "terms"])
253
- held = page.evaluate(_READ, kinds)
288
+ inputs.append([TERMS, "terms", ""])
289
+ held = page.evaluate(_READ, inputs)
254
290
  return {k: v for k, v in held.items() if v not in (None, [])}
255
291
 
256
292
  def submit(self, page: Page) -> str | None:
@@ -271,6 +307,7 @@ class Manatal:
271
307
  entries: list[dict[str, Any]],
272
308
  terms: bool,
273
309
  options: Callable[[dict[str, Any]], list[str] | None],
310
+ salaries: Callable[[str], list[str]],
274
311
  ) -> list[Field]:
275
312
  fields = []
276
313
  for entry in entries:
@@ -278,6 +315,9 @@ class Manatal:
278
315
  fields.append(self._entries_field(entry))
279
316
  continue
280
317
  fields.append(field := _field(entry, options(entry)))
318
+ for fid, dom in _salary(entry):
319
+ label = f"{entry['label']}: {fid.rsplit('.', 1)[1]}"
320
+ fields.append(Field(fid, label, FieldType.CHOICE, field.required, salaries(dom)))
281
321
  if _kind(entry) in ("radio", "checkbox") and OTHER in (field.options or []):
282
322
  fields.append(self._other_field(field))
283
323
  if terms:
File without changes
File without changes