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.
Files changed (45) hide show
  1. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/PKG-INFO +2 -2
  2. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/README.md +1 -1
  3. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/pyproject.toml +1 -1
  4. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/pyproject.toml.orig +1 -1
  5. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/cli.py +46 -7
  6. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/commands.py +5 -3
  7. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/matching.py +3 -0
  8. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/results.py +1 -1
  9. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/LICENSE +0 -0
  10. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/__init__.py +0 -0
  11. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/__init__.py +0 -0
  12. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/ashby.py +0 -0
  13. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/bamboohr.py +0 -0
  14. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/breezy.py +0 -0
  15. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/dayforce.py +0 -0
  16. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/eightfold.py +0 -0
  17. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/greenhouse.py +0 -0
  18. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/hireology.py +0 -0
  19. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/hiringthing.py +0 -0
  20. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/jazzhr.py +0 -0
  21. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/lever.py +0 -0
  22. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/manatal.py +0 -0
  23. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/oracle.py +0 -0
  24. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/paycor.py +0 -0
  25. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/paylocity.py +0 -0
  26. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/personio.py +0 -0
  27. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/phenom.py +0 -0
  28. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/pinpoint.py +0 -0
  29. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/recruitee.py +0 -0
  30. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/rippling.py +0 -0
  31. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/smartrecruiters.py +0 -0
  32. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/teamtailor.py +0 -0
  33. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/workable.py +0 -0
  34. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/wttj.py +0 -0
  35. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/adapters/zohorecruit.py +0 -0
  36. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/__init__.py +0 -0
  37. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/accounts.py +0 -0
  38. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/adapter.py +0 -0
  39. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/attempts.py +0 -0
  40. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/browser.py +0 -0
  41. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/passwords.py +0 -0
  42. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/paths.py +0 -0
  43. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/core/registry.py +0 -0
  44. {atsfill-0.0.4.dev83 → atsfill-0.0.4.dev85}/src/atsfill/py.typed +0 -0
  45. {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.dev83
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 goes ahead with a `warning`.
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 goes ahead with a `warning`.
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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atsfill"
3
- version = "0.0.4.dev83"
3
+ version = "0.0.4.dev85"
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.dev83"
3
+ version = "0.0.4.dev85"
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"
@@ -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; exit 0 for ready and submitted,
48
- 1 for any other status, 2 for a usage error."""
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(value: Result | StatusReport | SessionReport) -> None:
179
- json.dump(dataclasses.asdict(value), sys.stdout, default=str, ensure_ascii=False, indent=2)
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 not send or result.status is not Status.READY:
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 verified, or the last
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 anyway
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