atsfill 0.0.4.dev83__tar.gz → 0.0.4.dev85__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.dev83 → atsfill-0.0.4.dev85}/PKG-INFO +2 -2
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/README.md +1 -1
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/pyproject.toml +1 -1
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/pyproject.toml.orig +1 -1
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/cli.py +46 -7
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/commands.py +5 -3
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/matching.py +3 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/results.py +1 -1
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/LICENSE +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/__init__.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/__init__.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/ashby.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/bamboohr.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/breezy.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/dayforce.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/eightfold.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/greenhouse.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/hireology.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/hiringthing.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/jazzhr.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/lever.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/manatal.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/oracle.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/paycor.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/paylocity.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/personio.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/phenom.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/pinpoint.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/recruitee.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/rippling.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/smartrecruiters.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/teamtailor.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/workable.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/wttj.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/zohorecruit.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/__init__.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/accounts.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/adapter.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/attempts.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/browser.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/passwords.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/paths.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/registry.py +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/py.typed +0 -0
- {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/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.dev85
|
|
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
|
|
@@ -131,7 +131,7 @@ Every push to `main` that passes CI is also published as a dev release; `uvx --p
|
|
|
131
131
|
|
|
132
132
|
A dry run is not invisible: some systems save answers or the resume as they are typed.
|
|
133
133
|
`atsfill status` lists each system's side effects, whether it is allowed to submit, and how far atsfill has verified it: [the systems](docs/adapters/README.md).
|
|
134
|
-
A `send` through an adapter no send was verified on
|
|
134
|
+
A dry run or a `send` through an adapter no send was verified on carries a `warning`; the send goes ahead anyway.
|
|
135
135
|
|
|
136
136
|
One home can apply for more than one person: `--user <name>` gives each their own sessions, double-send guard and `[users.<name>]` section in the config file ([ADR 0015](docs/adr/0015-users-and-accounts.md)).
|
|
137
137
|
Given an account's email (`atsfill login <adapter> --email`) and a password source (a `[password-commands]` entry such as `rbw get example.com {email}`, or `--password -` on stdin), atsfill signs in by itself, and again whenever a form command finds the session signed out.
|
|
@@ -105,7 +105,7 @@ Every push to `main` that passes CI is also published as a dev release; `uvx --p
|
|
|
105
105
|
|
|
106
106
|
A dry run is not invisible: some systems save answers or the resume as they are typed.
|
|
107
107
|
`atsfill status` lists each system's side effects, whether it is allowed to submit, and how far atsfill has verified it: [the systems](docs/adapters/README.md).
|
|
108
|
-
A `send` through an adapter no send was verified on
|
|
108
|
+
A dry run or a `send` through an adapter no send was verified on carries a `warning`; the send goes ahead anyway.
|
|
109
109
|
|
|
110
110
|
One home can apply for more than one person: `--user <name>` gives each their own sessions, double-send guard and `[users.<name>]` section in the config file ([ADR 0015](docs/adr/0015-users-and-accounts.md)).
|
|
111
111
|
Given an account's email (`atsfill login <adapter> --email`) and a password source (a `[password-commands]` entry such as `rbw get example.com {email}`, or `--password -` on stdin), atsfill signs in by itself, and again whenever a form command finds the session signed out.
|
|
@@ -44,8 +44,10 @@ HELP = {
|
|
|
44
44
|
EPILOG = """\
|
|
45
45
|
A dry run is visible to the hiring system: some save each answer or the resume as it is
|
|
46
46
|
typed (`status` lists each adapter's side effects). A `send` that ends `submitted` is a real
|
|
47
|
-
application, final. Each command prints one JSON result
|
|
48
|
-
|
|
47
|
+
application, final. Each command prints one JSON result on one line, without the keys that
|
|
48
|
+
are null, false or empty, and after `fill` and `send` only the checks that failed; `-v`
|
|
49
|
+
prints all of it. Exit 0 for ready and submitted, 1 for any other status, 2 for a usage
|
|
50
|
+
error."""
|
|
49
51
|
|
|
50
52
|
|
|
51
53
|
def parser() -> argparse.ArgumentParser:
|
|
@@ -103,6 +105,10 @@ def parser() -> argparse.ArgumentParser:
|
|
|
103
105
|
action="store_true",
|
|
104
106
|
help="create the account with --email and a password atsfill generates and stores",
|
|
105
107
|
)
|
|
108
|
+
for name in ("read", "fill", "send", "status", "login", "logout"):
|
|
109
|
+
sub.choices[name].add_argument(
|
|
110
|
+
"-v", "--verbose", action="store_true", help="the whole result, every key, indented"
|
|
111
|
+
)
|
|
106
112
|
return p
|
|
107
113
|
|
|
108
114
|
|
|
@@ -146,7 +152,7 @@ def run(args: argparse.Namespace) -> int:
|
|
|
146
152
|
)
|
|
147
153
|
case "status":
|
|
148
154
|
report = core.status(data_dir=args.data_dir)
|
|
149
|
-
emit(report)
|
|
155
|
+
emit(report, args.verbose)
|
|
150
156
|
signed_out = any(s.session == "signed_out" for s in report.sessions)
|
|
151
157
|
return 1 if signed_out or report.browser != report.browser_pinned else 0
|
|
152
158
|
case "install-browser":
|
|
@@ -169,16 +175,49 @@ def run(args: argparse.Namespace) -> int:
|
|
|
169
175
|
except ValueError as e:
|
|
170
176
|
print(f"atsfill: {e}", file=sys.stderr)
|
|
171
177
|
return 2
|
|
172
|
-
emit(session)
|
|
178
|
+
emit(session, args.verbose)
|
|
173
179
|
return 0 if args.command == "logout" or session.session == "signed_in" else 1
|
|
174
|
-
emit(result)
|
|
180
|
+
emit(result, args.verbose, args.command)
|
|
175
181
|
return 0 if result.status in OK else 1
|
|
176
182
|
|
|
177
183
|
|
|
178
|
-
def emit(
|
|
179
|
-
|
|
184
|
+
def emit(
|
|
185
|
+
value: Result | StatusReport | SessionReport, verbose: bool, command: str | None = None
|
|
186
|
+
) -> None:
|
|
187
|
+
"""Prints the whole value with `verbose`; else what its consumer acts on, on one line."""
|
|
188
|
+
data = dataclasses.asdict(value)
|
|
189
|
+
if verbose:
|
|
190
|
+
json.dump(data, sys.stdout, default=str, ensure_ascii=False, indent=2)
|
|
191
|
+
else:
|
|
192
|
+
if isinstance(value, Result):
|
|
193
|
+
data = _slim(data, command)
|
|
194
|
+
json.dump(_sparse(data), sys.stdout, default=str, ensure_ascii=False, separators=(",", ":"))
|
|
180
195
|
sys.stdout.write("\n")
|
|
181
196
|
|
|
182
197
|
|
|
198
|
+
def _slim(result: dict, command: str | None) -> dict:
|
|
199
|
+
"""A form command's result without what the consumer already has: the URL it passed and,
|
|
200
|
+
after `read`, the fields, the side effects and each answer it gave."""
|
|
201
|
+
del result["url"]
|
|
202
|
+
if command != "read":
|
|
203
|
+
del result["fields"], result["side_effects"]
|
|
204
|
+
result["checks"] = [
|
|
205
|
+
{"field_id": c["field_id"], "read_back": c["read_back"]}
|
|
206
|
+
for c in result["checks"]
|
|
207
|
+
if not c["match"]
|
|
208
|
+
]
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _sparse(value: object) -> object:
|
|
213
|
+
"""Drops the keys whose value is null, false or empty, at every depth."""
|
|
214
|
+
if isinstance(value, dict):
|
|
215
|
+
kept = {k: _sparse(v) for k, v in value.items()}
|
|
216
|
+
return {k: v for k, v in kept.items() if v not in (None, False, [], {})}
|
|
217
|
+
if isinstance(value, list):
|
|
218
|
+
return [_sparse(v) for v in value]
|
|
219
|
+
return value
|
|
220
|
+
|
|
221
|
+
|
|
183
222
|
def main() -> None:
|
|
184
223
|
sys.exit(run(parser().parse_args()))
|
|
@@ -265,9 +265,11 @@ def _form_command(
|
|
|
265
265
|
reason = f"already_sent: last attempt ended {last}; pass --resend"
|
|
266
266
|
return Result(Status.REFUSED, url, adapter.name, reason=reason)
|
|
267
267
|
result = _fill(page, adapter, url, answers, user)
|
|
268
|
-
if
|
|
268
|
+
if result.status is not Status.READY:
|
|
269
269
|
return result
|
|
270
270
|
result.warning = _unverified(adapter.name)
|
|
271
|
+
if not send:
|
|
272
|
+
return result
|
|
271
273
|
attempts.record(mine, posting, attempts.PENDING, url=url)
|
|
272
274
|
try:
|
|
273
275
|
result.evidence = adapter.submit(page)
|
|
@@ -336,8 +338,8 @@ def send(
|
|
|
336
338
|
|
|
337
339
|
|
|
338
340
|
def _unverified(adapter: str) -> str | None:
|
|
339
|
-
"""The warning a send carries when no send through the adapter was
|
|
340
|
-
verified one fails now (ADR 0012)."""
|
|
341
|
+
"""The warning a ready dry run or a send carries when no send through the adapter was
|
|
342
|
+
verified, or the last verified one fails now (ADR 0012)."""
|
|
341
343
|
source = registry.for_adapter(adapter)
|
|
342
344
|
if source and source.send_broken:
|
|
343
345
|
return f"send_broken: {_broken(source)}"
|
|
@@ -73,6 +73,9 @@ def _answer_problem(field: Field, answer: object) -> str | None:
|
|
|
73
73
|
values = [answer]
|
|
74
74
|
if field.type in (FieldType.CHOICE, FieldType.MULTI_CHOICE) and not _in_options(field, values):
|
|
75
75
|
return f"{answer!r} is not one of {field.options}"
|
|
76
|
+
length = len(_norm(values[0]))
|
|
77
|
+
if field.type is FieldType.TEXT and field.max_length and length > field.max_length:
|
|
78
|
+
return f"{length} characters, over the limit of {field.max_length}"
|
|
76
79
|
if field.type is FieldType.DATE and not re.fullmatch(r"\d{4}-\d{2}-\d{2}", values[0]):
|
|
77
80
|
return "a date answer is YYYY-MM-DD"
|
|
78
81
|
if field.type is FieldType.FILE:
|
|
@@ -63,7 +63,7 @@ class Result:
|
|
|
63
63
|
checks: list[Check] = field(default_factory=list)
|
|
64
64
|
side_effects: list[SideEffect] = field(default_factory=list)
|
|
65
65
|
evidence: str | None = None
|
|
66
|
-
warning: str | None = None # `code: detail`, on a send that went ahead
|
|
66
|
+
warning: str | None = None # `code: detail`, on a ready dry run or a send that went ahead
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
@dataclass
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|