atsfill 0.0.4.dev56__tar.gz → 0.0.4.dev58__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.dev56 → atsfill-0.0.4.dev58}/PKG-INFO +1 -1
  2. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/pyproject.toml +1 -1
  3. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/pyproject.toml.orig +1 -1
  4. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/__init__.py +23 -20
  5. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/ashby.py +2 -0
  6. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/greenhouse.py +0 -1
  7. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/paycor.py +0 -1
  8. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/cli.py +2 -2
  9. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/core/commands.py +13 -9
  10. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/LICENSE +0 -0
  11. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/README.md +0 -0
  12. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/__init__.py +0 -0
  13. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/bamboohr.py +0 -0
  14. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/breezy.py +0 -0
  15. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/dayforce.py +0 -0
  16. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/hireology.py +0 -0
  17. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/jazzhr.py +0 -0
  18. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/lever.py +0 -0
  19. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/manatal.py +0 -0
  20. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/oracle.py +0 -0
  21. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/paylocity.py +0 -0
  22. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/phenom.py +0 -0
  23. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/recruitee.py +0 -0
  24. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/smartrecruiters.py +0 -0
  25. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/teamtailor.py +0 -0
  26. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/workable.py +0 -0
  27. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/adapters/wttj.py +0 -0
  28. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/core/__init__.py +0 -0
  29. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/core/accounts.py +0 -0
  30. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/core/adapter.py +0 -0
  31. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/core/attempts.py +0 -0
  32. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/core/browser.py +0 -0
  33. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/core/matching.py +0 -0
  34. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/core/paths.py +0 -0
  35. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/core/registry.py +0 -0
  36. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/core/results.py +0 -0
  37. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/src/atsfill/py.typed +0 -0
  38. {atsfill-0.0.4.dev56 → atsfill-0.0.4.dev58}/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.dev56
3
+ Version: 0.0.4.dev58
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.dev56"
3
+ version = "0.0.4.dev58"
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.dev56"
3
+ version = "0.0.4.dev58"
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"
@@ -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
- ADAPTERS: list[Adapter] = [
22
- Ashby(),
23
- Teamtailor(),
24
- Greenhouse(),
25
- WelcomeToTheJungle(),
26
- SmartRecruiters(),
27
- Oracle(),
28
- Manatal(),
29
- Paylocity(),
30
- Workable(),
31
- Hireology(),
32
- Paycor(),
33
- JazzHR(),
34
- Lever(),
35
- Dayforce(),
36
- Recruitee(),
37
- Breezy(),
38
- BambooHR(),
39
- Phenom(), # last: it claims a URL by its path alone
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
+ ]
@@ -149,6 +149,8 @@ class Ashby:
149
149
  case FieldType.TEXT | FieldType.DATE | FieldType.PHONE, str():
150
150
  box = entry.locator("input, textarea").first
151
151
  box.fill(answer)
152
+ if fields[field_id].type is FieldType.DATE:
153
+ box.press("Escape") # its picker stays open over the next field
152
154
  self._saved(page, box.blur)
153
155
  case FieldType.CHOICE, str() if entry.locator("input[role=combobox]").count():
154
156
  entry.locator("input[role=combobox]").fill(answer)
@@ -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 ADAPTERS
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 ADAPTERS if isinstance(a, NeedsAccount)]
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 ADAPTERS
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 ADAPTERS if a.detect(url, page)), None)
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 = _detect(url)
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 (adapter := _detect(page.url) or _detect(url, page)):
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 ADAPTERS:
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 ADAPTERS],
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 ADAPTERS if a.name == name), None)
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