atsfill 0.0.8.dev228__tar.gz → 0.0.8.dev231__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 (69) hide show
  1. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/PKG-INFO +4 -4
  2. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/pyproject.toml +9 -9
  3. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/pyproject.toml.orig +9 -9
  4. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/workday.py +42 -19
  5. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/adapter.py +9 -0
  6. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/commands.py +16 -5
  7. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/results.py +1 -0
  8. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/telemetry.py +1 -0
  9. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/LICENSE +0 -0
  10. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/README.md +0 -0
  11. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/__init__.py +0 -0
  12. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/__init__.py +0 -0
  13. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/adp.py +0 -0
  14. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/adpmyjobs.py +0 -0
  15. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/ashby.py +0 -0
  16. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/bamboohr.py +0 -0
  17. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/breezy.py +0 -0
  18. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/dayforce.py +0 -0
  19. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/eightfold.py +0 -0
  20. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/greenhouse.py +0 -0
  21. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/gupy.py +0 -0
  22. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/hireology.py +0 -0
  23. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/hiringthing.py +0 -0
  24. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/hrmdirect.py +0 -0
  25. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/isolvedhire.py +0 -0
  26. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/jazzhr.py +0 -0
  27. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/jobaffinity.py +0 -0
  28. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/jobstreet.py +0 -0
  29. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/join.py +0 -0
  30. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/lever.py +0 -0
  31. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/manatal.py +0 -0
  32. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/oracle.py +0 -0
  33. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/paycor.py +0 -0
  34. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/paylocity.py +0 -0
  35. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/personio.py +0 -0
  36. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/phenom.py +0 -0
  37. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/pinpoint.py +0 -0
  38. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/recruitee.py +0 -0
  39. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/recruiterflow.py +0 -0
  40. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/rippling.py +0 -0
  41. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/smartrecruiters.py +0 -0
  42. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/successfactors.js +0 -0
  43. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/successfactors.py +0 -0
  44. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/taleo.py +0 -0
  45. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/teamtailor.py +0 -0
  46. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/ukg.py +0 -0
  47. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/welcomekit.py +0 -0
  48. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/wellfound.py +0 -0
  49. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/werecruit.py +0 -0
  50. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/workable.py +0 -0
  51. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/workatastartup.py +0 -0
  52. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/wttj.py +0 -0
  53. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/adapters/zohorecruit.py +0 -0
  54. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/cli.py +0 -0
  55. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/__init__.py +0 -0
  56. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/accounts.py +0 -0
  57. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/attempts.py +0 -0
  58. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/browser.py +0 -0
  59. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/conditions.py +0 -0
  60. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/countries.py +0 -0
  61. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/html.py +0 -0
  62. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/matching.py +0 -0
  63. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/otp.py +0 -0
  64. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/passwords.py +0 -0
  65. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/paths.py +0 -0
  66. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/registry.py +0 -0
  67. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/core/snapshot.py +0 -0
  68. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/py.typed +0 -0
  69. {atsfill-0.0.8.dev228 → atsfill-0.0.8.dev231}/src/atsfill/registry.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atsfill
3
- Version: 0.0.8.dev228
3
+ Version: 0.0.8.dev231
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
@@ -16,11 +16,11 @@ Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Programming Language :: Python :: 3.14
17
17
  Classifier: Topic :: Office/Business
18
18
  Classifier: Typing :: Typed
19
- Requires-Dist: camoufox==0.5.9b6
20
- Requires-Dist: filelock==4.1.1
19
+ Requires-Dist: camoufox==0.5.9
20
+ Requires-Dist: filelock==4.2.10
21
21
  Requires-Dist: keyring==25.7.0
22
22
  Requires-Dist: packaging==26.3
23
- Requires-Dist: posthog==7.78.0
23
+ Requires-Dist: posthog==7.87.1
24
24
  Requires-Python: >=3.14
25
25
  Project-URL: Homepage, https://github.com/shbernal/atsfill
26
26
  Project-URL: Repository, https://github.com/shbernal/atsfill
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atsfill"
3
- version = "0.0.8.dev228"
3
+ version = "0.0.8.dev231"
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"
@@ -28,11 +28,11 @@ classifiers = [
28
28
  "Typing :: Typed",
29
29
  ]
30
30
  dependencies = [
31
- "camoufox==0.5.9b6",
32
- "filelock==4.1.1",
31
+ "camoufox==0.5.9",
32
+ "filelock==4.2.10",
33
33
  "keyring==25.7.0",
34
34
  "packaging==26.3",
35
- "posthog==7.78.0",
35
+ "posthog==7.87.1",
36
36
  ]
37
37
 
38
38
  [[project.authors]]
@@ -51,13 +51,13 @@ requires = ["uv_build>=0.13.0,<0.14.0"]
51
51
  build-backend = "uv_build"
52
52
 
53
53
  [dependency-groups]
54
- docs = ["zensical==0.0.73"]
55
- commit-check = ["commit-check==2.19.0"]
54
+ docs = ["zensical==0.1.1"]
55
+ commit-check = ["commit-check==2.20.5"]
56
56
  dev = [
57
- "pytest>=9.1.1",
57
+ "pytest>=9.2.0",
58
58
  "pytest-xdist>=3.8.0",
59
- "ruff>=0.17.1",
60
- "ty==0.0.89",
59
+ "ruff>=0.17.4",
60
+ "ty==0.0.101",
61
61
  ]
62
62
 
63
63
  [tool.ruff]
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atsfill"
3
- version = "0.0.8.dev228"
3
+ version = "0.0.8.dev231"
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"
@@ -21,11 +21,11 @@ classifiers = [
21
21
  "Typing :: Typed",
22
22
  ]
23
23
  dependencies = [
24
- "camoufox==0.5.9b6",
25
- "filelock==4.1.1",
24
+ "camoufox==0.5.9",
25
+ "filelock==4.2.10",
26
26
  "keyring==25.7.0",
27
27
  "packaging==26.3",
28
- "posthog==7.78.0",
28
+ "posthog==7.87.1",
29
29
  ]
30
30
 
31
31
  [project.urls]
@@ -42,16 +42,16 @@ build-backend = "uv_build"
42
42
 
43
43
  [dependency-groups]
44
44
  docs = [
45
- "zensical==0.0.73",
45
+ "zensical==0.1.1",
46
46
  ]
47
47
  commit-check = [
48
- "commit-check==2.19.0",
48
+ "commit-check==2.20.5",
49
49
  ]
50
50
  dev = [
51
- "pytest>=9.1.1",
51
+ "pytest>=9.2.0",
52
52
  "pytest-xdist>=3.8.0",
53
- "ruff>=0.17.1",
54
- "ty==0.0.89",
53
+ "ruff>=0.17.4",
54
+ "ty==0.0.101",
55
55
  ]
56
56
 
57
57
  [tool.ruff]
@@ -5,8 +5,8 @@ observes.
5
5
  Applying needs an account, one per tenant: the account scope. The form is a wizard of pages,
6
6
  each saved to a draft application as it is left; only the current page is in the DOM. The
7
7
  read walks the pages that save with what they hold, the posting's questionnaires and apply
8
- flow standing for the pages it does not reach; the read-back is each page's values, read
9
- before it is saved.
8
+ flow standing for the pages it does not reach; a page behind a consent's tick only a fill
9
+ reaches, on the user's answer; the read-back is each page's values, read before it is saved.
10
10
  """
11
11
 
12
12
  import re
@@ -193,7 +193,7 @@ def _profile_hosts(request: Request, scope: str | None) -> list[str]:
193
193
 
194
194
  class Workday:
195
195
  name = "workday"
196
- lists_every_field = "the fill stops on a field the read did not list, page by page"
196
+ behind_consent = "the pages after a consent's tick, which the draft does not keep"
197
197
  per_employer = True
198
198
  asks: set[str] = set()
199
199
  known_gaps = {
@@ -216,6 +216,8 @@ class Workday:
216
216
  self._listed: set[str] = set() # the ids the read listed, unfillable ones too
217
217
  self._walked: set[str] = set() # the pages the read reached
218
218
  self._walled = False # no read gets past the page this one stopped on
219
+ self._behind = False # the fill stopped on a page behind that one, for `revealed`
220
+ self._phone_codes = ""
219
221
  self._values: dict[str, Answer] = {}
220
222
  self._apply = ""
221
223
 
@@ -292,15 +294,16 @@ class Workday:
292
294
  page.goto(self._apply)
293
295
  _wait_for_page(page)
294
296
  seen: dict[str, list[Field]] = {}
295
- self._walked, self._walled = set(), False
297
+ self._walked, self._walled, self._behind = set(), False, False
298
+ self._phone_codes = f"{calypso}/jobapplication/{found.tenant}{PHONE_CODES}"
296
299
  for _ in range(MAX_PAGES):
297
300
  step = _step(page)
298
301
  if step == REVIEW: # repeats the answers, an upload without its input
299
302
  break
300
303
  self._walked.add(step)
301
304
  if not _questions_page(step):
302
- phone_codes = f"{calypso}/jobapplication/{found.tenant}{PHONE_CODES}"
303
- seen[step] = _read(page, phone_codes, {f.id for f in from_api.get(step, [])})
305
+ listed = {f.id for f in from_api.get(step, [])}
306
+ seen[step] = _read(page, self._phone_codes, listed)
304
307
  try:
305
308
  _save_and_continue(page)
306
309
  except NotTaken:
@@ -316,10 +319,13 @@ class Workday:
316
319
  return fields
317
320
 
318
321
  def fill(self, page: Page, answers: dict[str, Answer]) -> None:
319
- """Fills each page of the wizard from the first and saves it, up to Review."""
320
- page.goto(self._apply)
321
- _wait_for_page(page)
322
- self._values = {}
322
+ """Fills each page of the wizard from the first and saves it, up to Review, or up to a
323
+ page behind the consent: the next call goes on from there with what `revealed` listed."""
324
+ if not self._behind:
325
+ page.goto(self._apply)
326
+ _wait_for_page(page)
327
+ self._values = {}
328
+ self._behind = False
323
329
  pending: set[PageRequest] = set()
324
330
  page.on("request", lambda r: pending.add(r) if DEFINITION in r.url else None)
325
331
  for done in ("requestfinished", "requestfailed"):
@@ -343,6 +349,8 @@ class Workday:
343
349
  _until_rendered(page, pending)
344
350
  page.wait_for_timeout(SETTLE_MS)
345
351
  self._unread(page)
352
+ if self._behind:
353
+ return
346
354
  self._values |= {r["id"]: r["value"] for r in _filled(page, answers)}
347
355
  if self._values.get(PHONE_NUMBER) and (code := _phone_code(page)):
348
356
  self._values[PHONE_NUMBER] = f"+{code} {self._values[PHONE_NUMBER]}"
@@ -351,21 +359,31 @@ class Workday:
351
359
 
352
360
  def _unread(self, page: Page) -> None:
353
361
  """Stops on fields the read did not list, optional ones too. A page the read did not
354
- reach is read once the pages before it are saved, unless no read can get there. On one
355
- it read, an answer opened the field (a country's address, a referral's name): the page
356
- stops unsaved, so no read would list it."""
362
+ reach is read once the pages before it are saved; where no read can get there, `revealed`
363
+ lists it. On one it read, an answer opened the field (a country's address, a referral's
364
+ name): the page stops unsaved, so no read would list it."""
357
365
  unread = _not_listed(_page_fields(page), self._listed)
358
366
  if not unread:
359
367
  return
360
368
  step, labels = _step(page), ", ".join(r["label"] for r in unread)
361
369
  if step not in self._walked and self._walled:
362
- raise Unsupported(f"unhandled_rendering: {step} shows {labels}, which no read reaches")
363
- if step not in self._walked:
370
+ self._behind = True
371
+ elif step not in self._walked:
364
372
  raise NotTaken(f"{step} shows fields the read did not: {labels}; read again")
365
- if step == MY_INFORMATION and any(r["id"].startswith(ADDRESS) for r in unread):
373
+ elif step == MY_INFORMATION and any(r["id"].startswith(ADDRESS) for r in unread):
366
374
  detail = f"the country answered shows fields the read did not: {labels}"
367
375
  raise Unsupported(known_gap(self, "other_country", detail))
368
- raise Unsupported(f"unhandled_rendering: an answer showed {labels}, not listed")
376
+ else:
377
+ raise Unsupported(f"unhandled_rendering: an answer showed {labels}, not listed")
378
+
379
+ def revealed(self, page: Page) -> list[Field]:
380
+ """The fields of the page behind the consent the fill stopped on, none once it ended."""
381
+ if not self._behind:
382
+ return []
383
+ fields = _read(page, self._phone_codes, self._listed)
384
+ self._fields |= {f.id: f for f in fields if not f.unfillable}
385
+ self._listed |= {f.id for f in fields}
386
+ return fields
369
387
 
370
388
  def read_back(self, page: Page) -> dict[str, Answer]:
371
389
  """Each page's values, read before it was saved; one file as its name."""
@@ -607,6 +625,7 @@ def _field(raw: dict[str, Any]) -> Field | None:
607
625
  return Field(
608
626
  raw["id"], raw["label"], kind, raw["required"], options,
609
627
  prefilled=None if site else value, default=value if site else None,
628
+ notice=raw["notice"],
610
629
  ) # fmt: skip
611
630
 
612
631
 
@@ -978,7 +997,8 @@ def _errors(page: Page) -> str:
978
997
  return re.sub(r"\s+", " ", " ".join(page.locator(ERRORS).all_inner_texts())).strip()
979
998
 
980
999
 
981
- # Each field the page shows: its id, label, whether required (`*`), rendering and value.
1000
+ # Each field the page shows: its id, label, whether required (`*`), rendering, value, and the
1001
+ # text a consent's box stands under.
982
1002
  _FIELDS = """() => {
983
1003
  const text = e => (e?.innerText ?? "").replace(/\\s+/g, " ").trim();
984
1004
  const labelOf = (f, e) => text(f.querySelector(`label[for="${CSS.escape(e.id)}"]`)
@@ -1029,8 +1049,11 @@ _FIELDS = """() => {
1029
1049
  kind = "text";
1030
1050
  value = typed.value;
1031
1051
  }
1052
+ const notice = kind === "box" && f.closest("[role=group]")
1053
+ ?.querySelector("[data-automation-id=instructionalText]");
1032
1054
  return {id: f.dataset.fkitId, label: label.replace(/\\*$/, "").trim(),
1033
- required: label.endsWith("*"), kind, value, options};
1055
+ required: label.endsWith("*"), kind, value, options,
1056
+ notice: text(notice || null) || null};
1034
1057
  });
1035
1058
  }"""
1036
1059
 
@@ -317,6 +317,15 @@ class RevealsFields(Adapter, Protocol):
317
317
  ...
318
318
 
319
319
 
320
+ @runtime_checkable
321
+ class RevealsBehindConsent(RevealsFields, Protocol):
322
+ """A `RevealsFields` whose fields stand on pages behind a consent's tick, which no draft
323
+ keeps: a read gives no answer, so it stops at the tick, and only a fill gets past it. A
324
+ refusal for them is `behind_consent`, counted apart from other answers (ADR 0005)."""
325
+
326
+ behind_consent: str # where they stand
327
+
328
+
320
329
  @runtime_checkable
321
330
  class ListsShownFields(Adapter, Protocol):
322
331
  """An adapter whose form can show fields after an answer: once filled, it says which the
@@ -34,6 +34,7 @@ from atsfill.core.adapter import (
34
34
  ProfileIncomplete,
35
35
  ReadsByRequest,
36
36
  Refused,
37
+ RevealsBehindConsent,
37
38
  RevealsFields,
38
39
  SendsToOpen,
39
40
  SignedOut,
@@ -354,9 +355,14 @@ def _shown_as_listed(
354
355
  result.reason = f"mismatch: {', '.join(wrong)} shows though its condition is unmet"
355
356
 
356
357
 
357
- def _refuse(result: Result, problems: list[matching.Problem], *more: str) -> None:
358
+ def _refuse(
359
+ result: Result,
360
+ problems: list[matching.Problem],
361
+ *more: str,
362
+ code: str = "invalid_answers",
363
+ ) -> None:
358
364
  result.status = Status.REFUSED
359
- result.reason = "invalid_answers: " + "; ".join([*problems, *more])
365
+ result.reason = f"{code}: " + "; ".join([*problems, *more])
360
366
  result.refused = [f"{p.field_id}: {p.rule}" for p in problems]
361
367
 
362
368
 
@@ -386,9 +392,14 @@ def _revealed(
386
392
  result,
387
393
  problems,
388
394
  *[f"{f.id} takes {f.options}" for f in new if f.options and f.id not in mine],
395
+ code=(
396
+ "behind_consent"
397
+ if isinstance(adapter, RevealsBehindConsent)
398
+ else "invalid_answers"
399
+ ),
389
400
  )
390
401
  return mine
391
- left = [f for f in new if f.id not in mine]
402
+ left = [f for f in new if f.id not in mine and f.prefilled is None]
392
403
  result.warnings += [f"an answer showed {f.id} ({f.label}), left empty" for f in left]
393
404
  adapter.fill(page, matching.to_fill(new, {f.id: mine[f.id] for f in new if f.id in mine}))
394
405
  _no_field(result, answers)
@@ -794,7 +805,7 @@ def login(
794
805
  `email` is remembered for the account; `password` is used for this sign-in only. `create`
795
806
  creates the account with a password atsfill generates and stores first (a side effect),
796
807
  giving the sign-up the user's `details`; with a password already stored, the account may
797
- exist, so it signs in with that password and keeps the account's email instead, the
808
+ exist, so it signs in with that password and keeps the account's email if it has one, the
798
809
  `details` finishing a sign-up the account was left in. An account without a password signs
799
810
  in through the user's OTP command in place of a password source, and is created the same
800
811
  way."""
@@ -816,7 +827,7 @@ def login(
816
827
  report.status, report.reason = Status.BLOCKED, f"password_source: {e}"
817
828
  return report
818
829
  kept = stored is not None
819
- if email and not kept:
830
+ if email and not (kept and accounts.email(mine, adapter, scope)):
820
831
  accounts.remember(mine, adapter, scope, email)
821
832
  if create and not kept:
822
833
  assert email
@@ -76,6 +76,7 @@ class Field:
76
76
  option_sets: list[OptionSet] | None = None # a dependent list's; `options` where none holds
77
77
  min_length: int | None = None # a text the system refuses shorter
78
78
  knockouts: list[str] | None = None # the options whose choice ends the application (ADR 0005)
79
+ notice: str | None = None # the text a consent stands under: what a Yes accepts (ADR 0005)
79
80
 
80
81
 
81
82
  @dataclass(frozen=True)
@@ -46,6 +46,7 @@ REPORT = (
46
46
  # `unknown_field` is not one: ids come from `read`, so it can be a field read missed.
47
47
  EXCLUDED = (
48
48
  "invalid_answers",
49
+ "behind_consent",
49
50
  "answer_not_taken",
50
51
  "not_qualified",
51
52
  "already_sent",
File without changes
File without changes