atsfill 0.0.3.dev14__tar.gz → 0.0.3.dev16__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 (24) hide show
  1. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/PKG-INFO +2 -2
  2. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/README.md +1 -1
  3. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/pyproject.toml +1 -1
  4. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/pyproject.toml.orig +1 -1
  5. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/adapters/teamtailor.py +23 -10
  6. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/registry.toml +2 -0
  7. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/LICENSE +0 -0
  8. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/__init__.py +0 -0
  9. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/adapters/__init__.py +0 -0
  10. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/adapters/ashby.py +0 -0
  11. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/adapters/greenhouse.py +0 -0
  12. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/adapters/wttj.py +0 -0
  13. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/cli.py +0 -0
  14. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/__init__.py +0 -0
  15. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/accounts.py +0 -0
  16. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/adapter.py +0 -0
  17. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/attempts.py +0 -0
  18. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/browser.py +0 -0
  19. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/commands.py +0 -0
  20. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/matching.py +0 -0
  21. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/paths.py +0 -0
  22. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/registry.py +0 -0
  23. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/results.py +0 -0
  24. {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atsfill
3
- Version: 0.0.3.dev14
3
+ Version: 0.0.3.dev16
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
@@ -53,7 +53,7 @@ It is built for agents with a shell: every command prints one JSON document and
53
53
  ## Supported hiring systems
54
54
 
55
55
  <!-- badges -->
56
- ![ashby: send verified 2026-10-19](https://img.shields.io/badge/ashby-send%20verified%202026--10--19-9ece6a) ![greenhouse: dry run verified 2026-10-19](https://img.shields.io/badge/greenhouse-dry%20run%20verified%202026--10--19-7aa2f7) ![teamtailor: dry run verified 2026-10-19](https://img.shields.io/badge/teamtailor-dry%20run%20verified%202026--10--19-7aa2f7) ![wttj: dry run verified 2026-10-19](https://img.shields.io/badge/wttj-dry%20run%20verified%202026--10--19-7aa2f7)
56
+ ![ashby: send verified 2026-10-19](https://img.shields.io/badge/ashby-send%20verified%202026--10--19-9ece6a) ![greenhouse: dry run verified 2026-10-19](https://img.shields.io/badge/greenhouse-dry%20run%20verified%202026--10--19-7aa2f7) ![teamtailor: send verified 2026-10-20](https://img.shields.io/badge/teamtailor-send%20verified%202026--10--20-9ece6a) ![wttj: send verified 2026-10-20](https://img.shields.io/badge/wttj-send%20verified%202026--10--20-9ece6a)
57
57
  <!-- /badges -->
58
58
 
59
59
  | System | Account needed | `read` without a browser |
@@ -29,7 +29,7 @@ It is built for agents with a shell: every command prints one JSON document and
29
29
  ## Supported hiring systems
30
30
 
31
31
  <!-- badges -->
32
- ![ashby: send verified 2026-10-19](https://img.shields.io/badge/ashby-send%20verified%202026--10--19-9ece6a) ![greenhouse: dry run verified 2026-10-19](https://img.shields.io/badge/greenhouse-dry%20run%20verified%202026--10--19-7aa2f7) ![teamtailor: dry run verified 2026-10-19](https://img.shields.io/badge/teamtailor-dry%20run%20verified%202026--10--19-7aa2f7) ![wttj: dry run verified 2026-10-19](https://img.shields.io/badge/wttj-dry%20run%20verified%202026--10--19-7aa2f7)
32
+ ![ashby: send verified 2026-10-19](https://img.shields.io/badge/ashby-send%20verified%202026--10--19-9ece6a) ![greenhouse: dry run verified 2026-10-19](https://img.shields.io/badge/greenhouse-dry%20run%20verified%202026--10--19-7aa2f7) ![teamtailor: send verified 2026-10-20](https://img.shields.io/badge/teamtailor-send%20verified%202026--10--20-9ece6a) ![wttj: send verified 2026-10-20](https://img.shields.io/badge/wttj-send%20verified%202026--10--20-9ece6a)
33
33
  <!-- /badges -->
34
34
 
35
35
  | System | Account needed | `read` without a browser |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atsfill"
3
- version = "0.0.3.dev14"
3
+ version = "0.0.3.dev16"
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.3.dev14"
3
+ version = "0.0.3.dev16"
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"
@@ -7,6 +7,8 @@ soon as it is picked. The read-back is the form's `FormData`.
7
7
 
8
8
  import re
9
9
  import time
10
+ import unicodedata
11
+ from pathlib import PurePath
10
12
  from typing import Any
11
13
  from urllib.parse import urlsplit
12
14
 
@@ -164,6 +166,7 @@ class Teamtailor:
164
166
  self._fields: dict[str, Field] = {}
165
167
  self._qualifying: set[str] = set()
166
168
  self._follow_ups: dict[str, bool] = {} # shown by an earlier answer: required once shown
169
+ self._uploads: dict[str, str] = {} # file field id: the answer's file name
167
170
 
168
171
  def detect(self, url: str, page: Page | None) -> bool:
169
172
  """A `*.teamtailor.com` job, or a job page with Teamtailor's "powered by" link or its
@@ -225,15 +228,23 @@ class Teamtailor:
225
228
  if required and field_id not in answers and self._shown(form, field_id):
226
229
  label = self._fields[field_id].label
227
230
  raise Unsupported(f"missing_answer: an earlier answer asks {label!r}")
228
- self._wait_for_uploads(form, answers)
231
+ self._uploads = {
232
+ i: PurePath(a).name
233
+ for i, a in answers.items()
234
+ if self._fields[i].type is FieldType.FILE and isinstance(a, str)
235
+ }
236
+ self._wait_for_uploads(form)
229
237
 
230
238
  def read_back(self, page: Page) -> dict[str, Answer]:
231
239
  values = page.locator(FORM).evaluate(_READ_BACK, QUESTION)
240
+ for field_id, name in self._uploads.items():
241
+ if values.get(field_id) == _stored_name(name):
242
+ values[field_id] = name
232
243
  return {k: v for k, v in values.items() if v not in (None, "", [])}
233
244
 
234
245
  def submit(self, page: Page) -> str | None:
235
246
  _click(page, page.locator(f"{FORM} [type=submit]"))
236
- page.wait_for_url(re.compile(r"/applications/\d+/thanks"), timeout=SUBMIT_TIMEOUT_MS)
247
+ page.wait_for_url(re.compile(r"/applications/[^/]+/thanks"), timeout=SUBMIT_TIMEOUT_MS)
237
248
  return page.url
238
249
 
239
250
  def _fill(self, form: Locator, field_id: str, answer: Answer) -> None:
@@ -290,20 +301,22 @@ class Teamtailor:
290
301
  )
291
302
  return form.locator(f"[name='{field_id}']").first.locator("xpath=..")
292
303
 
293
- def _wait_for_uploads(self, form: Locator, answers: dict[str, Answer]) -> None:
294
- files = {
295
- i: a.rsplit("/", 1)[-1]
296
- for i, a in answers.items()
297
- if self._fields[i].type is FieldType.FILE and isinstance(a, str)
298
- }
304
+ def _wait_for_uploads(self, form: Locator) -> None:
299
305
  deadline = time.monotonic() + UPLOAD_TIMEOUT_S
300
- while files and time.monotonic() < deadline:
306
+ while self._uploads and time.monotonic() < deadline:
301
307
  held = form.evaluate(_READ_BACK, QUESTION)
302
- if all(held.get(i) == name for i, name in files.items()):
308
+ if all(held.get(i) == _stored_name(n) for i, n in self._uploads.items()):
303
309
  return
304
310
  form.page.wait_for_timeout(500)
305
311
 
306
312
 
313
+ def _stored_name(name: str) -> str:
314
+ """The name Teamtailor stores a file under: ASCII, lowercase, each run of other
315
+ characters than letters, digits, `.` and `-` one `_`."""
316
+ ascii_name = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode()
317
+ return re.sub(r"[^a-z0-9.\-]+", "_", ascii_name.lower())
318
+
319
+
307
320
  def _field(raw: dict[str, Any]) -> Field | None:
308
321
  """The field a raw description is, None for a kind atsfill cannot fill."""
309
322
  kinds = QUESTION_TYPES if raw.get("question") else STANDARD_TYPES
@@ -27,6 +27,7 @@ kind = "ats"
27
27
  account = "none"
28
28
  hosting = ["hosted"]
29
29
  adapter = "teamtailor"
30
+ send_verified = { date = 2026-10-20, version = "0.0.3", by = "shbernal", commit = "4e09f53" }
30
31
  dry_run_verified = { date = 2026-10-19, version = "0.0.2", by = "gate", commit = "91a0bba" }
31
32
 
32
33
  [greenhouse]
@@ -41,4 +42,5 @@ kind = "board"
41
42
  account = "to_send"
42
43
  hosting = ["hosted"]
43
44
  adapter = "wttj"
45
+ send_verified = { date = 2026-10-20, version = "0.0.3", by = "shbernal", commit = "65458a5" }
44
46
  dry_run_verified = { date = 2026-10-19, version = "0.0.2", by = "gate", commit = "ad9f478" }
File without changes