atsfill 0.0.4.dev56__tar.gz → 0.0.4.dev57__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.dev56 → atsfill-0.0.4.dev57}/PKG-INFO +1 -1
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/pyproject.toml +1 -1
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/pyproject.toml.orig +1 -1
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/__init__.py +23 -20
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/greenhouse.py +0 -1
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/paycor.py +0 -1
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/cli.py +2 -2
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/core/commands.py +13 -9
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/LICENSE +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/README.md +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/__init__.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/ashby.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/bamboohr.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/breezy.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/dayforce.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/hireology.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/jazzhr.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/lever.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/manatal.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/oracle.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/paylocity.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/phenom.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/recruitee.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/smartrecruiters.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/teamtailor.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/workable.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/adapters/wttj.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/core/__init__.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/core/accounts.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/core/adapter.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/core/attempts.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/core/browser.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/core/matching.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/core/paths.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/core/registry.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/core/results.py +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/src/atsfill/py.typed +0 -0
- {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev57}/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.dev57
|
|
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
|
|
@@ -18,23 +18,26 @@ from atsfill.adapters.workable import Workable
|
|
|
18
18
|
from atsfill.adapters.wttj import WelcomeToTheJungle
|
|
19
19
|
from atsfill.core.adapter import Adapter
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
21
|
+
|
|
22
|
+
def adapters() -> list[Adapter]:
|
|
23
|
+
"""One fresh instance of each adapter: a command's adapters never see another posting."""
|
|
24
|
+
return [
|
|
25
|
+
Ashby(),
|
|
26
|
+
Teamtailor(),
|
|
27
|
+
Greenhouse(),
|
|
28
|
+
WelcomeToTheJungle(),
|
|
29
|
+
SmartRecruiters(),
|
|
30
|
+
Oracle(),
|
|
31
|
+
Manatal(),
|
|
32
|
+
Paylocity(),
|
|
33
|
+
Workable(),
|
|
34
|
+
Hireology(),
|
|
35
|
+
Paycor(),
|
|
36
|
+
JazzHR(),
|
|
37
|
+
Lever(),
|
|
38
|
+
Dayforce(),
|
|
39
|
+
Recruitee(),
|
|
40
|
+
Breezy(),
|
|
41
|
+
BambooHR(),
|
|
42
|
+
Phenom(), # last: it claims a URL by its path alone
|
|
43
|
+
]
|
|
@@ -133,7 +133,6 @@ class Greenhouse:
|
|
|
133
133
|
|
|
134
134
|
def detect(self, url: str, page: Page | None) -> bool:
|
|
135
135
|
"""A board URL, or an employer page whose `#grnhse_iframe` embeds a job application."""
|
|
136
|
-
self._embed = None # one process detects many postings: a survey, the adapter gate
|
|
137
136
|
if _board_job(url):
|
|
138
137
|
return True
|
|
139
138
|
if page is None:
|
|
@@ -154,7 +154,6 @@ class Paycor:
|
|
|
154
154
|
|
|
155
155
|
def detect(self, url: str, page: Page | None) -> bool:
|
|
156
156
|
"""A `recruitingbypaycor.com/career` job URL, or an employer page that embeds one."""
|
|
157
|
-
self._start = None # one process detects many postings: a survey, the adapter gate
|
|
158
157
|
parts = urlsplit(url)
|
|
159
158
|
if parts.hostname == HOST and (ids := _ids(url)):
|
|
160
159
|
self._start = self._apply(*ids)
|
|
@@ -5,7 +5,7 @@ import sys
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
from atsfill import core
|
|
8
|
-
from atsfill.adapters import
|
|
8
|
+
from atsfill.adapters import adapters
|
|
9
9
|
from atsfill.core import browser
|
|
10
10
|
from atsfill.core.adapter import NeedsAccount
|
|
11
11
|
from atsfill.core.results import Result, SessionReport, Status, StatusReport
|
|
@@ -64,7 +64,7 @@ def parser() -> argparse.ArgumentParser:
|
|
|
64
64
|
)
|
|
65
65
|
sub.add_parser("status", help="version, paths, browser, and each adapter's state")
|
|
66
66
|
sub.add_parser("install-browser", help="download the Camoufox build atsfill is pinned to")
|
|
67
|
-
accounts = [a.name for a in
|
|
67
|
+
accounts = [a.name for a in adapters() if isinstance(a, NeedsAccount)]
|
|
68
68
|
sub.add_parser(
|
|
69
69
|
"login", help="sign in, in a visible window, to a system that needs an account"
|
|
70
70
|
).add_argument("adapter", choices=accounts)
|
|
@@ -3,7 +3,7 @@ from contextlib import contextmanager
|
|
|
3
3
|
from importlib.metadata import version
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
from atsfill.adapters import
|
|
6
|
+
from atsfill.adapters import adapters
|
|
7
7
|
from atsfill.core import accounts, attempts, browser, matching, paths, registry
|
|
8
8
|
from atsfill.core.adapter import (
|
|
9
9
|
Adapter,
|
|
@@ -26,8 +26,8 @@ from atsfill.core.results import (
|
|
|
26
26
|
type Sessions = dict[str, Path]
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
def _detect(url: str, page: Page | None = None) -> Adapter | None:
|
|
30
|
-
return next((a for a in
|
|
29
|
+
def _detect(candidates: list[Adapter], url: str, page: Page | None = None) -> Adapter | None:
|
|
30
|
+
return next((a for a in candidates if a.detect(url, page)), None)
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def _login_required(url: str, adapter: Adapter) -> Result:
|
|
@@ -42,8 +42,10 @@ def _opened(
|
|
|
42
42
|
"""The loaded page and the adapter that claims it by its URL, else by the URL a redirect
|
|
43
43
|
lands on, else by its page; or the result that ends the command first. An adapter that
|
|
44
44
|
needs an account gets its session, and only that adapter's (ADR 0007). The adapter's name
|
|
45
|
-
goes in `detected` once known.
|
|
46
|
-
adapter
|
|
45
|
+
goes in `detected` once known. The adapters are fresh: none keeps state from another
|
|
46
|
+
posting (a survey or the adapter gate runs many commands in one process)."""
|
|
47
|
+
candidates = adapters()
|
|
48
|
+
adapter = _detect(candidates, url)
|
|
47
49
|
if adapter:
|
|
48
50
|
detected.append(adapter.name)
|
|
49
51
|
session = None
|
|
@@ -60,7 +62,9 @@ def _opened(
|
|
|
60
62
|
raise
|
|
61
63
|
# A page that never finishes loading (a redirect to an employer's career page)
|
|
62
64
|
# is left to the adapter that claimed its URL.
|
|
63
|
-
if adapter is None and (
|
|
65
|
+
if adapter is None and (
|
|
66
|
+
adapter := _detect(candidates, page.url) or _detect(candidates, url, page)
|
|
67
|
+
):
|
|
64
68
|
detected.append(adapter.name)
|
|
65
69
|
if adapter is None:
|
|
66
70
|
yield _no_adapter(url)
|
|
@@ -195,7 +199,7 @@ def _form_command(
|
|
|
195
199
|
|
|
196
200
|
def read(url: str, *, data_dir: Path | None = None, sessions: Sessions | None = None) -> Result:
|
|
197
201
|
"""Lists the form's fields, over HTTP when an adapter can (ADR 0011), else from the page."""
|
|
198
|
-
for adapter in
|
|
202
|
+
for adapter in adapters():
|
|
199
203
|
if not isinstance(adapter, ReadsOverHttp):
|
|
200
204
|
continue
|
|
201
205
|
if (fields := adapter.read_api(url)) is not None:
|
|
@@ -261,7 +265,7 @@ def status(*, data_dir: Path | None = None) -> StatusReport:
|
|
|
261
265
|
config=paths.config_file(),
|
|
262
266
|
browser=browser.version(),
|
|
263
267
|
browser_pinned=browser.BUILD,
|
|
264
|
-
adapters=[_adapter_status(a, data) for a in
|
|
268
|
+
adapters=[_adapter_status(a, data) for a in adapters()],
|
|
265
269
|
)
|
|
266
270
|
|
|
267
271
|
|
|
@@ -290,7 +294,7 @@ def _adapter_status(adapter: Adapter, data: Path) -> AdapterStatus:
|
|
|
290
294
|
|
|
291
295
|
|
|
292
296
|
def _account(name: str) -> NeedsAccount:
|
|
293
|
-
adapter = next((a for a in
|
|
297
|
+
adapter = next((a for a in adapters() if a.name == name), None)
|
|
294
298
|
if not isinstance(adapter, NeedsAccount):
|
|
295
299
|
raise ValueError(f"{name} is not an adapter that needs an account")
|
|
296
300
|
return adapter
|
|
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
|