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.
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/PKG-INFO +2 -2
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/README.md +1 -1
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/pyproject.toml +1 -1
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/pyproject.toml.orig +1 -1
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/adapters/teamtailor.py +23 -10
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/registry.toml +2 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/LICENSE +0 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/__init__.py +0 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/adapters/__init__.py +0 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/adapters/ashby.py +0 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/adapters/greenhouse.py +0 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/adapters/wttj.py +0 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/cli.py +0 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/__init__.py +0 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/accounts.py +0 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/adapter.py +0 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/attempts.py +0 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/browser.py +0 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/commands.py +0 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/matching.py +0 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/paths.py +0 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/registry.py +0 -0
- {atsfill-0.0.3.dev14 → atsfill-0.0.3.dev16}/src/atsfill/core/results.py +0 -0
- {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.
|
|
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
|
-
     
|
|
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
|
-
     
|
|
33
33
|
<!-- /badges -->
|
|
34
34
|
|
|
35
35
|
| System | Account needed | `read` without a browser |
|
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
|
306
|
+
while self._uploads and time.monotonic() < deadline:
|
|
301
307
|
held = form.evaluate(_READ_BACK, QUESTION)
|
|
302
|
-
if all(held.get(i) ==
|
|
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
|
|
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
|