atsfill 0.0.6.dev202__tar.gz → 0.0.6.dev204__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.6.dev202 → atsfill-0.0.6.dev204}/PKG-INFO +1 -1
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/pyproject.toml +1 -1
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/pyproject.toml.orig +1 -1
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/phenom.py +47 -16
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/commands.py +1 -1
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/matching.py +12 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/LICENSE +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/README.md +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/__init__.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/__init__.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/adp.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/adpmyjobs.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/ashby.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/bamboohr.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/breezy.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/dayforce.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/eightfold.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/greenhouse.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/gupy.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/hireology.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/hiringthing.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/hrmdirect.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/isolvedhire.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/jazzhr.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/jobaffinity.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/jobstreet.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/join.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/lever.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/manatal.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/oracle.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/paycor.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/paylocity.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/personio.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/pinpoint.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/recruitee.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/recruiterflow.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/rippling.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/smartrecruiters.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/successfactors.js +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/successfactors.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/taleo.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/teamtailor.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/ukg.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/welcomekit.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/wellfound.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/werecruit.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/workable.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/workatastartup.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/workday.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/wttj.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/adapters/zohorecruit.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/cli.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/__init__.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/accounts.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/adapter.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/attempts.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/browser.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/conditions.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/countries.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/html.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/otp.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/passwords.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/paths.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/registry.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/results.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/snapshot.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/core/telemetry.py +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/py.typed +0 -0
- {atsfill-0.0.6.dev202 → atsfill-0.0.6.dev204}/src/atsfill/registry.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: atsfill
|
|
3
|
-
Version: 0.0.6.
|
|
3
|
+
Version: 0.0.6.dev204
|
|
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
|
|
@@ -43,6 +43,8 @@ JOB = re.compile(r"^(/[a-z]{2,8}/[a-z]{2})/(job|\w*apply)(?:/|$)")
|
|
|
43
43
|
APPLY = re.compile(r"^/[a-z]{2,8}/[a-z]{2}/\w*apply$") # `apply`, `hvhapply` for hourly roles
|
|
44
44
|
SEQ = re.compile(r"^/[a-z]{2,8}/[a-z]{2}/job/([A-Za-z0-9]+)")
|
|
45
45
|
CDN = "https://cdn-prod-static.phenompeople.com/CareerConnectResources"
|
|
46
|
+
RENDERER = "MULTI_PAGE_renderer" # the script of the page that has one, in its loader's data
|
|
47
|
+
MULTI = "multipage" # what `_phapp_of` adds to `phApp`
|
|
46
48
|
FORM = "form.rjsf"
|
|
47
49
|
NEXT = "button#next"
|
|
48
50
|
TERMS = "terms" # a notice some sites' wizard opens on, its one button accepting it
|
|
@@ -110,6 +112,7 @@ TEMPLATE = re.compile(r"\{\{\s*(.*?)\s*\}\}")
|
|
|
110
112
|
TAG = re.compile(r"<[^>]+>")
|
|
111
113
|
PAGE_VALUE = re.compile(r"%%(phApp\.[\w.]+)%%")
|
|
112
114
|
ANSWER = re.compile(r"%%formData\.(\w+)%%")
|
|
115
|
+
PLACEHOLDER = re.compile(r"(please )?(choose|select)( (one|an?)\b.*)?$", re.IGNORECASE)
|
|
113
116
|
SEARCHING = re.compile(r"Searching", re.IGNORECASE) # a typeahead's menu before its results
|
|
114
117
|
ACCEPTED = re.compile(r"\b(DOCX?|PDF|TXT|HTML?|RTF)\b", re.IGNORECASE)
|
|
115
118
|
|
|
@@ -216,12 +219,14 @@ _APPLY_LINK = """seq => {
|
|
|
216
219
|
return own ? own.href : null;
|
|
217
220
|
}"""
|
|
218
221
|
|
|
219
|
-
# The errors a step shows, each with its field's control id.
|
|
222
|
+
# The errors a step shows, each with its field's control id and whether the page hides it.
|
|
220
223
|
_ERRORS = """form => [...form.querySelectorAll('.error-detail li, .text-danger, .error-message')]
|
|
221
224
|
.filter(e => e.innerText.trim())
|
|
222
225
|
.map(e => {
|
|
223
226
|
const control = e.closest('.form-group')?.querySelector('input, select, textarea');
|
|
224
|
-
|
|
227
|
+
const hidden = !!control && !/^(checkbox|radio)$/.test(control.type)
|
|
228
|
+
&& !control.getClientRects().length;
|
|
229
|
+
return [control?.id || control?.name || '?', e.innerText.trim(), hidden];
|
|
225
230
|
}).slice(0, 4)"""
|
|
226
231
|
|
|
227
232
|
|
|
@@ -264,6 +269,7 @@ class Phenom:
|
|
|
264
269
|
known_gaps = {
|
|
265
270
|
"first_step": Gap.FLOW,
|
|
266
271
|
"long_wizard": Gap.FLOW,
|
|
272
|
+
"hidden_required": Gap.FLOW,
|
|
267
273
|
"calendar_without_month": Gap.RENDERING,
|
|
268
274
|
}
|
|
269
275
|
side_effects = [
|
|
@@ -493,6 +499,7 @@ def _phapp_of(html: str) -> dict[str, Any]:
|
|
|
493
499
|
ph = _literal(html, "var phApp = phApp || ")
|
|
494
500
|
ph["ddo"] = _literal(html, "phApp.ddo = ")
|
|
495
501
|
ph["sessionParams"] = _literal(html, "phApp.sessionParams = ")
|
|
502
|
+
ph[MULTI] = RENDERER in html
|
|
496
503
|
return ph
|
|
497
504
|
|
|
498
505
|
|
|
@@ -515,13 +522,11 @@ def _read(
|
|
|
515
522
|
ph: dict[str, Any], fetch: Callable[[str], Any], post: Post
|
|
516
523
|
) -> tuple[list[Field], list[dict[str, Any]]]:
|
|
517
524
|
"""The fields of every step, from the configuration the page names, and their raw form."""
|
|
518
|
-
|
|
519
|
-
base = f"{CDN}/{ph['refNum']}/{ats}/external/careersite/singlepage"
|
|
520
|
-
config = fetch(f"{base}/schema/form_schema.json")
|
|
525
|
+
config, bundles = _configuration(ph, fetch)
|
|
521
526
|
texts: dict[str, str] = {}
|
|
522
527
|
for bundle in ("default", ph.get("locale") or "en_us"):
|
|
523
528
|
with suppress(Exception): # a site has one bundle or both
|
|
524
|
-
texts |= fetch(f"{
|
|
529
|
+
texts |= fetch(f"{bundles}/Messages_{bundle}.json")
|
|
525
530
|
reader = _Config(ph, config, texts, post)
|
|
526
531
|
raws = [raw for step in reader.steps() for raw in step]
|
|
527
532
|
if not raws:
|
|
@@ -534,6 +539,27 @@ def _read(
|
|
|
534
539
|
return ([resume] if resume else []) + [_field(raw) for raw in raws], raws
|
|
535
540
|
|
|
536
541
|
|
|
542
|
+
def _configuration(ph: dict[str, Any], fetch: Callable[[str], Any]) -> tuple[dict[str, Any], str]:
|
|
543
|
+
"""The configuration the page's renderer loads, and where its bundles are: a site on the
|
|
544
|
+
multi-page renderer keeps a stale `singlepage` one, and its form under the page's own name
|
|
545
|
+
where its `multipage` path has none."""
|
|
546
|
+
site = f"{CDN}/{ph['refNum']}"
|
|
547
|
+
ats = (get(ph, "ddo.response.response.applyATS") or "").lower()
|
|
548
|
+
base = f"{site}/{ats}/external/careersite/{'multipage' if ph.get(MULTI) else 'singlepage'}"
|
|
549
|
+
try:
|
|
550
|
+
return fetch(f"{base}/schema/form_schema.json"), f"{base}/bundles"
|
|
551
|
+
except Exception:
|
|
552
|
+
if not ph.get(MULTI):
|
|
553
|
+
raise
|
|
554
|
+
names = [
|
|
555
|
+
get(node, "data.page.schemaname")
|
|
556
|
+
for node in fetch(f"{site}/schema/apply/MULTI_PAGE_form_schema.json")
|
|
557
|
+
if node.get("type") == "page"
|
|
558
|
+
]
|
|
559
|
+
form = next((name for name in names if name != TERMS), TERMS)
|
|
560
|
+
return fetch(f"{site}/schema/apply/multipage/{form}.json"), f"{site}/bundles/apply"
|
|
561
|
+
|
|
562
|
+
|
|
537
563
|
class _Config:
|
|
538
564
|
"""The wizard's steps, each its fields as raw dicts, from the employer's configuration:
|
|
539
565
|
a react-jsonschema-form schema and uiSchema with Phenom's widgets, rules on the job's data,
|
|
@@ -703,7 +729,7 @@ class _Config:
|
|
|
703
729
|
"datasourcepath": get(u, "ui:options.datasourcepath"),
|
|
704
730
|
"displayTextField": get(u, "ui:options.dataFieldMapKey") or "label",
|
|
705
731
|
}
|
|
706
|
-
raw["options"] =
|
|
732
|
+
raw["options"] = _choices(t for _, t in self.loaded_list(listing, api)) or None
|
|
707
733
|
if kind == "select" and re.fullmatch(r"\w+", path): # a default, the job's country
|
|
708
734
|
raw["default"] = self.prefilled(path)[1] or None
|
|
709
735
|
if kind in ("date", "month", "year"):
|
|
@@ -730,7 +756,7 @@ class _Config:
|
|
|
730
756
|
scope = path.rpartition(".")[0]
|
|
731
757
|
at = f"{scope}.{raw['parent']}" if scope else raw["parent"]
|
|
732
758
|
raw["option_sets"] = [
|
|
733
|
-
OptionSet(Is(at, [self.text(parent.get("title"))]),
|
|
759
|
+
OptionSet(Is(at, [self.text(parent.get("title"))]), _choices(
|
|
734
760
|
self.text(child.get("key"))
|
|
735
761
|
for child in parent.get("child") or []
|
|
736
762
|
if child.get("show") is not False
|
|
@@ -738,7 +764,7 @@ class _Config:
|
|
|
738
764
|
for parent in u.get("data") or []
|
|
739
765
|
] # fmt: skip
|
|
740
766
|
return None
|
|
741
|
-
return
|
|
767
|
+
return _choices(
|
|
742
768
|
self.text(child.get("key"))
|
|
743
769
|
for parent in u.get("data") or []
|
|
744
770
|
for child in parent.get("child") or []
|
|
@@ -750,7 +776,7 @@ class _Config:
|
|
|
750
776
|
if isinstance(v, dict) and v.get("htmlType") == "dependent_utm"),
|
|
751
777
|
None,
|
|
752
778
|
) # fmt: skip
|
|
753
|
-
return
|
|
779
|
+
return _choices(self.text(parent.get("title")) for parent in tree or [])
|
|
754
780
|
if kind == "radio" and not p.get("enum"):
|
|
755
781
|
return YES_NO
|
|
756
782
|
api = u.get("apiConfig") or get(u, "ui:options.apiConfig")
|
|
@@ -777,7 +803,7 @@ class _Config:
|
|
|
777
803
|
self.listed.setdefault(path.rsplit(".", 1)[-1], listed) # as a workflow names it
|
|
778
804
|
if raw.get("option_sets"): # a dependent list's, under its parents' answers before any
|
|
779
805
|
return None
|
|
780
|
-
return
|
|
806
|
+
return _choices(t for _, t in pairs) or None
|
|
781
807
|
|
|
782
808
|
def loaded_list(self, u: dict, api: dict) -> list[tuple[Any, str]]:
|
|
783
809
|
"""A list its own loader serves, by the uiSchema's names for the response's parts."""
|
|
@@ -851,7 +877,7 @@ class _Config:
|
|
|
851
877
|
if isinstance(d, dict)
|
|
852
878
|
]
|
|
853
879
|
if pairs:
|
|
854
|
-
options =
|
|
880
|
+
options = _choices(t for _, t in pairs if t)
|
|
855
881
|
raw["option_sets"] = [OptionSet(when[0] if len(when) == 1 else AllOf(when), options)]
|
|
856
882
|
raw["parent"] = at
|
|
857
883
|
return pairs
|
|
@@ -887,7 +913,7 @@ class _Config:
|
|
|
887
913
|
shown = dict(zip(map(str, values), texts, strict=False))
|
|
888
914
|
if isinstance(data, dict) and shown:
|
|
889
915
|
raw["option_sets"] = [
|
|
890
|
-
OptionSet(Is(at, [shown[key]]),
|
|
916
|
+
OptionSet(Is(at, [shown[key]]), _choices(t for _, t in rows if t))
|
|
891
917
|
for key, rows in pairs.items()
|
|
892
918
|
if key in shown
|
|
893
919
|
]
|
|
@@ -1291,8 +1317,10 @@ def _json_text(value: Any) -> str:
|
|
|
1291
1317
|
return json.dumps("" if value is None else str(value))[1:-1]
|
|
1292
1318
|
|
|
1293
1319
|
|
|
1294
|
-
def
|
|
1295
|
-
|
|
1320
|
+
def _choices(items: Iterable[str]) -> list[str]:
|
|
1321
|
+
"""A list's options, once each, without an entry that only asks to pick one: an employer's
|
|
1322
|
+
bundle can label a real value so, which no one means to answer."""
|
|
1323
|
+
return list(dict.fromkeys(i for i in items if i and not PLACEHOLDER.match(i)))
|
|
1296
1324
|
|
|
1297
1325
|
|
|
1298
1326
|
def _seq(url: str) -> str | None:
|
|
@@ -1388,7 +1416,10 @@ def _next(page: Page, number: int) -> None:
|
|
|
1388
1416
|
raise NotTaken(f"step {number}: {detail}")
|
|
1389
1417
|
raise RuntimeError(f"step {number} did not pass: {code}: {detail}")
|
|
1390
1418
|
if errors := page.locator(FORM).evaluate(_ERRORS) if page.locator(FORM).count() else []:
|
|
1391
|
-
|
|
1419
|
+
if hidden := next((dom for dom, _, hides in errors if hides), None):
|
|
1420
|
+
detail = f"step {number} requires {hidden}, which the page hides"
|
|
1421
|
+
raise Unsupported(known_gap(Phenom(), "hidden_required", detail))
|
|
1422
|
+
shown = "; ".join(f"{dom}: {message}" for dom, message, _ in errors)
|
|
1392
1423
|
raise RuntimeError(f"step {number} did not pass: {shown}")
|
|
1393
1424
|
raise RuntimeError(f"step {number} did not pass in time")
|
|
1394
1425
|
finally:
|
|
@@ -426,7 +426,7 @@ def _fill(
|
|
|
426
426
|
result.status, result.reason = Status.REFUSED, "invalid_answers: " + "; ".join(problems)
|
|
427
427
|
return result
|
|
428
428
|
result.warnings = matching.knockouts(result.fields, answers)
|
|
429
|
-
adapter.fill(page, answers)
|
|
429
|
+
adapter.fill(page, matching.to_fill(result.fields, answers))
|
|
430
430
|
_shown_as_listed(page, adapter, result, answers)
|
|
431
431
|
if result.status is not Status.READY:
|
|
432
432
|
return result
|
|
@@ -51,6 +51,9 @@ def _absolute_url(value: str) -> bool:
|
|
|
51
51
|
def matches(
|
|
52
52
|
field: Field, answer: Answer, read_back: Answer | None, form: Form | None = None
|
|
53
53
|
) -> bool:
|
|
54
|
+
if field.type is FieldType.MULTI_CHOICE and answer == []:
|
|
55
|
+
# a cleared list: a read-back may leave out a field that holds nothing
|
|
56
|
+
return read_back in (None, "", [])
|
|
54
57
|
if read_back is None:
|
|
55
58
|
return False
|
|
56
59
|
match field.type:
|
|
@@ -98,6 +101,8 @@ def _answer_problem(field: Field, answer: object, form: Form) -> str | None:
|
|
|
98
101
|
if field.type is FieldType.MULTI_CHOICE:
|
|
99
102
|
if not (isinstance(answer, list) and all(isinstance(v, str) for v in answer)):
|
|
100
103
|
return "a multi_choice answer is a list of strings"
|
|
104
|
+
if not answer:
|
|
105
|
+
return "required, so an empty list does not answer it" if form.asked(field) else None
|
|
101
106
|
values = answer
|
|
102
107
|
elif not isinstance(answer, str):
|
|
103
108
|
return f"a {field.type} answer is a string"
|
|
@@ -169,6 +174,13 @@ def problems(fields: list[Field], answers: dict[str, Answer]) -> list[str]:
|
|
|
169
174
|
return out
|
|
170
175
|
|
|
171
176
|
|
|
177
|
+
def to_fill(fields: list[Field], answers: dict[str, Answer]) -> dict[str, Answer]:
|
|
178
|
+
"""The answers an adapter fills, after `problems` passed them: an empty list clears what a
|
|
179
|
+
field holds, so one that holds nothing has nothing to fill."""
|
|
180
|
+
by_id = {f.id: f for f in fields}
|
|
181
|
+
return {i: a for i, a in answers.items() if a != [] or by_id[i].prefilled or by_id[i].default}
|
|
182
|
+
|
|
183
|
+
|
|
172
184
|
def knockouts(fields: list[Field], answers: dict[str, Answer]) -> list[str]:
|
|
173
185
|
"""The answers that end the application: never refused, as one can be the truthful one."""
|
|
174
186
|
by_id = {f.id: f for f in fields}
|
|
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
|
|
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
|