atsfill 0.0.5.dev189__tar.gz → 0.0.5.dev192__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 (67) hide show
  1. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/PKG-INFO +1 -1
  2. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/pyproject.toml +1 -1
  3. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/pyproject.toml.orig +1 -1
  4. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/lever.py +11 -3
  5. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/phenom.py +20 -10
  6. atsfill-0.0.5.dev192/src/atsfill/core/countries.py +260 -0
  7. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/core/matching.py +11 -3
  8. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/LICENSE +0 -0
  9. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/README.md +0 -0
  10. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/__init__.py +0 -0
  11. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/__init__.py +0 -0
  12. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/adp.py +0 -0
  13. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/adpmyjobs.py +0 -0
  14. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/ashby.py +0 -0
  15. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/bamboohr.py +0 -0
  16. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/breezy.py +0 -0
  17. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/dayforce.py +0 -0
  18. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/eightfold.py +0 -0
  19. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/greenhouse.py +0 -0
  20. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/gupy.py +0 -0
  21. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/hireology.py +0 -0
  22. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/hiringthing.py +0 -0
  23. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/hrmdirect.py +0 -0
  24. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/isolvedhire.py +0 -0
  25. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/jazzhr.py +0 -0
  26. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/jobaffinity.py +0 -0
  27. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/jobstreet.py +0 -0
  28. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/join.py +0 -0
  29. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/manatal.py +0 -0
  30. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/oracle.py +0 -0
  31. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/paycor.py +0 -0
  32. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/paylocity.py +0 -0
  33. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/personio.py +0 -0
  34. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/pinpoint.py +0 -0
  35. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/recruitee.py +0 -0
  36. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/recruiterflow.py +0 -0
  37. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/rippling.py +0 -0
  38. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/smartrecruiters.py +0 -0
  39. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/successfactors.js +0 -0
  40. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/successfactors.py +0 -0
  41. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/taleo.py +0 -0
  42. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/teamtailor.py +0 -0
  43. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/ukg.py +0 -0
  44. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/welcomekit.py +0 -0
  45. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/workable.py +0 -0
  46. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/workatastartup.py +0 -0
  47. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/workday.py +0 -0
  48. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/wttj.py +0 -0
  49. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/adapters/zohorecruit.py +0 -0
  50. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/cli.py +0 -0
  51. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/core/__init__.py +0 -0
  52. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/core/accounts.py +0 -0
  53. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/core/adapter.py +0 -0
  54. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/core/attempts.py +0 -0
  55. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/core/browser.py +0 -0
  56. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/core/commands.py +0 -0
  57. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/core/conditions.py +0 -0
  58. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/core/html.py +0 -0
  59. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/core/otp.py +0 -0
  60. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/core/passwords.py +0 -0
  61. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/core/paths.py +0 -0
  62. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/core/registry.py +0 -0
  63. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/core/results.py +0 -0
  64. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/core/snapshot.py +0 -0
  65. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/core/telemetry.py +0 -0
  66. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/py.typed +0 -0
  67. {atsfill-0.0.5.dev189 → atsfill-0.0.5.dev192}/src/atsfill/registry.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atsfill
3
- Version: 0.0.5.dev189
3
+ Version: 0.0.5.dev192
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.5.dev189"
3
+ version = "0.0.5.dev192"
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.5.dev189"
3
+ version = "0.0.5.dev192"
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"
@@ -88,7 +88,7 @@ _READ = """form => {
88
88
  }"""
89
89
 
90
90
  # What each field shows: a choice its checked option's text, a select its option's text, a file
91
- # its attached name.
91
+ # its attached name, the location its pick.
92
92
  _READ_BACK = """(form, kinds) => {
93
93
  const optionText = input => (input.closest("label")?.querySelector(
94
94
  ".application-answer-alternative")?.textContent || input.value).replace(/\\s+/g, " ").trim();
@@ -110,7 +110,10 @@ _READ_BACK = """(form, kinds) => {
110
110
  } else if (kind === "checkboxes") {
111
111
  out[name] = all.filter(e => e.type === "checkbox" && e.checked).map(optionText);
112
112
  } else if (kind === "consent") out[name] = el.checked ? "Yes" : "No";
113
- else out[name] = el.value;
113
+ else if (kind === "location") {
114
+ const picked = form.querySelector("[name=selectedLocation]")?.value;
115
+ out[name] = picked ? JSON.parse(picked).name : null;
116
+ } else out[name] = el.value;
114
117
  }
115
118
  return out;
116
119
  }"""
@@ -159,7 +162,7 @@ class Lever:
159
162
  # the resume is parsed into the form, so it goes first and is waited for
160
163
  for name, answer in sorted(answers.items(), key=lambda a: a[0] != "resume"):
161
164
  values = [answer] if isinstance(answer, str) else answer
162
- controls = page.locator(f"{FORM} [name='{name}']:not([type=hidden])")
165
+ controls = page.locator(f"{FORM} [name={_quoted(name)}]:not([type=hidden])")
163
166
  match self._kinds[name]:
164
167
  case "file":
165
168
  _attach(controls.first, values[0], resume=name == "resume")
@@ -224,6 +227,11 @@ def _field(raw: dict[str, Any]) -> Field:
224
227
  )
225
228
 
226
229
 
230
+ def _quoted(name: str) -> str:
231
+ """A control name as a CSS string: an employer names its links, quotes and all."""
232
+ return '"' + name.replace("\\", "\\\\").replace('"', '\\"') + '"'
233
+
234
+
227
235
  def _option(controls: Locator, value: str) -> Locator | None:
228
236
  """The radio or checkbox whose text is `value`."""
229
237
  for control in controls.all():
@@ -25,7 +25,7 @@ from typing import Any
25
25
  from urllib.parse import parse_qs, urlsplit
26
26
 
27
27
  from atsfill.core import browser, matching, paths
28
- from atsfill.core.adapter import Gap, NotTaken, Unsupported, known_gap
28
+ from atsfill.core.adapter import Blocked, Gap, NotTaken, Unsupported, known_gap
29
29
  from atsfill.core.browser import HTTP_TIMEOUT_S, BrowserError, Locator, Overlaid, Page, Timeout
30
30
  from atsfill.core.results import (
31
31
  AllOf,
@@ -238,22 +238,24 @@ _FETCH = """async url => {
238
238
  return response.json();
239
239
  }"""
240
240
 
241
- # Posts each body to `/applySubmit`, `most` at a time; null for one that fails.
241
+ # Posts each body to `/applySubmit`, `most` at a time; null for one that fails. With the
242
+ # answers, whether one was refused (403), after which none is posted.
242
243
  _POSTS = """async ([bodies, token, most]) => {
243
244
  const out = bodies.map(() => null);
244
- let next = 0;
245
+ let next = 0, refused = false;
245
246
  const post = async () => {
246
- for (let i = next++; i < bodies.length; i = next++) {
247
+ for (let i = next++; i < bodies.length && !refused; i = next++) {
247
248
  try {
248
249
  const response = await fetch('/applySubmit', {method: 'POST',
249
250
  body: JSON.stringify(bodies[i]),
250
251
  headers: {'Content-Type': 'application/json', 'x-csrf-token': token}});
251
252
  if (response.ok) out[i] = await response.json();
253
+ refused ||= response.status === 403;
252
254
  } catch {}
253
255
  }
254
256
  };
255
257
  await Promise.all(Array.from({length: most}, post));
256
- return out;
258
+ return [out, refused];
257
259
  }"""
258
260
 
259
261
 
@@ -350,14 +352,22 @@ class Phenom:
350
352
  except ValueError as e:
351
353
  raise RuntimeError(str(e)) from None
352
354
  token = get(ph, "sessionParams.csrfToken") or ""
355
+
356
+ refused = False
357
+
358
+ def posts(bodies: list[dict[str, Any]]) -> list[Any]:
359
+ nonlocal refused
360
+ out, now = page.evaluate(_POSTS, [bodies, token, LOADERS_AT_ONCE])
361
+ refused |= now
362
+ return out
363
+
353
364
  try:
354
- fields, raws = _read(
355
- ph,
356
- lambda url: page.evaluate(_FETCH, url),
357
- lambda bodies: page.evaluate(_POSTS, [bodies, token, LOADERS_AT_ONCE]),
358
- )
365
+ fields, raws = _read(ph, lambda url: page.evaluate(_FETCH, url), posts)
359
366
  except (BrowserError, ValueError, KeyError) as e:
360
367
  raise RuntimeError(f"no configuration for the wizard ({e})") from None
368
+ finally: # raised once the read ends, so the answers that came are kept
369
+ if refused:
370
+ raise Blocked("rate_limited: the careers site answers /applySubmit 403")
361
371
  self._raws = {raw["id"]: raw for raw in raws}
362
372
  return fields
363
373
 
@@ -0,0 +1,260 @@
1
+ """Countries by their ISO 3166-1 alpha-3 code and the English names an answer gives them: some
2
+ typeaheads end a place with its country's code, `Madrid, Comunidad de Madrid, ESP`."""
3
+
4
+ _TABLE = """
5
+ ABW Aruba
6
+ AFG Afghanistan
7
+ AGO Angola
8
+ AIA Anguilla
9
+ ALA Åland Islands
10
+ ALB Albania
11
+ AND Andorra
12
+ ARE United Arab Emirates
13
+ ARG Argentina
14
+ ARM Armenia
15
+ ASM American Samoa
16
+ ATA Antarctica
17
+ ATF French Southern Territories
18
+ ATG Antigua and Barbuda
19
+ AUS Australia
20
+ AUT Austria
21
+ AZE Azerbaijan
22
+ BDI Burundi
23
+ BEL Belgium
24
+ BEN Benin
25
+ BES Bonaire
26
+ BFA Burkina Faso
27
+ BGD Bangladesh
28
+ BGR Bulgaria
29
+ BHR Bahrain
30
+ BHS Bahamas
31
+ BIH Bosnia and Herzegovina
32
+ BLM Saint Barthélemy
33
+ BLR Belarus
34
+ BLZ Belize
35
+ BMU Bermuda
36
+ BOL Bolivia
37
+ BRA Brazil
38
+ BRB Barbados
39
+ BRN Brunei Darussalam | Brunei
40
+ BTN Bhutan
41
+ BVT Bouvet Island
42
+ BWA Botswana
43
+ CAF Central African Republic
44
+ CAN Canada
45
+ CCK Cocos (Keeling) Islands
46
+ CHE Switzerland
47
+ CHL Chile
48
+ CHN China
49
+ CIV Côte d'Ivoire
50
+ CMR Cameroon
51
+ COD Democratic Republic of the Congo | DR Congo
52
+ COG Congo
53
+ COK Cook Islands
54
+ COL Colombia
55
+ COM Comoros
56
+ CPV Cabo Verde
57
+ CRI Costa Rica
58
+ CUB Cuba
59
+ CUW Curaçao
60
+ CXR Christmas Island
61
+ CYM Cayman Islands
62
+ CYP Cyprus
63
+ CZE Czechia | Czech Republic
64
+ DEU Germany
65
+ DJI Djibouti
66
+ DMA Dominica
67
+ DNK Denmark
68
+ DOM Dominican Republic
69
+ DZA Algeria
70
+ ECU Ecuador
71
+ EGY Egypt
72
+ ERI Eritrea
73
+ ESH Western Sahara
74
+ ESP Spain
75
+ EST Estonia
76
+ ETH Ethiopia
77
+ FIN Finland
78
+ FJI Fiji
79
+ FLK Falkland Islands (Malvinas) | Falkland Islands
80
+ FRA France
81
+ FRO Faroe Islands
82
+ FSM Micronesia
83
+ GAB Gabon
84
+ GBR United Kingdom | UK | Great Britain
85
+ GEO Georgia
86
+ GGY Guernsey
87
+ GHA Ghana
88
+ GIB Gibraltar
89
+ GIN Guinea
90
+ GLP Guadeloupe
91
+ GMB Gambia
92
+ GNB Guinea-Bissau
93
+ GNQ Equatorial Guinea
94
+ GRC Greece
95
+ GRD Grenada
96
+ GRL Greenland
97
+ GTM Guatemala
98
+ GUF French Guiana
99
+ GUM Guam
100
+ GUY Guyana
101
+ HKG Hong Kong
102
+ HMD Heard Island and McDonald Islands
103
+ HND Honduras
104
+ HRV Croatia
105
+ HTI Haiti
106
+ HUN Hungary
107
+ IDN Indonesia
108
+ IMN Isle of Man
109
+ IND India
110
+ IOT British Indian Ocean Territory
111
+ IRL Ireland
112
+ IRN Iran
113
+ IRQ Iraq
114
+ ISL Iceland
115
+ ISR Israel
116
+ ITA Italy
117
+ JAM Jamaica
118
+ JEY Jersey
119
+ JOR Jordan
120
+ JPN Japan
121
+ KAZ Kazakhstan
122
+ KEN Kenya
123
+ KGZ Kyrgyzstan
124
+ KHM Cambodia
125
+ KIR Kiribati
126
+ KNA Saint Kitts and Nevis
127
+ KOR South Korea | Korea
128
+ KWT Kuwait
129
+ LAO Lao People's Democratic Republic | Laos
130
+ LBN Lebanon
131
+ LBR Liberia
132
+ LBY Libya
133
+ LCA Saint Lucia
134
+ LIE Liechtenstein
135
+ LKA Sri Lanka
136
+ LSO Lesotho
137
+ LTU Lithuania
138
+ LUX Luxembourg
139
+ LVA Latvia
140
+ MAC Macao
141
+ MAF Saint Martin (French part)
142
+ MAR Morocco
143
+ MCO Monaco
144
+ MDA Moldova
145
+ MDG Madagascar
146
+ MDV Maldives
147
+ MEX Mexico
148
+ MHL Marshall Islands
149
+ MKD North Macedonia
150
+ MLI Mali
151
+ MLT Malta
152
+ MMR Myanmar
153
+ MNE Montenegro
154
+ MNG Mongolia
155
+ MNP Northern Mariana Islands
156
+ MOZ Mozambique
157
+ MRT Mauritania
158
+ MSR Montserrat
159
+ MTQ Martinique
160
+ MUS Mauritius
161
+ MWI Malawi
162
+ MYS Malaysia
163
+ MYT Mayotte
164
+ NAM Namibia
165
+ NCL New Caledonia
166
+ NER Niger
167
+ NFK Norfolk Island
168
+ NGA Nigeria
169
+ NIC Nicaragua
170
+ NIU Niue
171
+ NLD Netherlands
172
+ NOR Norway
173
+ NPL Nepal
174
+ NRU Nauru
175
+ NZL New Zealand
176
+ OMN Oman
177
+ PAK Pakistan
178
+ PAN Panama
179
+ PCN Pitcairn
180
+ PER Peru
181
+ PHL Philippines
182
+ PLW Palau
183
+ PNG Papua New Guinea
184
+ POL Poland
185
+ PRI Puerto Rico
186
+ PRK North Korea
187
+ PRT Portugal
188
+ PRY Paraguay
189
+ PSE Palestine
190
+ PYF French Polynesia
191
+ QAT Qatar
192
+ REU Réunion
193
+ ROU Romania
194
+ RUS Russian Federation | Russia
195
+ RWA Rwanda
196
+ SAU Saudi Arabia
197
+ SDN Sudan
198
+ SEN Senegal
199
+ SGP Singapore
200
+ SGS South Georgia and the South Sandwich Islands
201
+ SHN Saint Helena
202
+ SJM Svalbard and Jan Mayen
203
+ SLB Solomon Islands
204
+ SLE Sierra Leone
205
+ SLV El Salvador
206
+ SMR San Marino
207
+ SOM Somalia
208
+ SPM Saint Pierre and Miquelon
209
+ SRB Serbia
210
+ SSD South Sudan
211
+ STP Sao Tome and Principe
212
+ SUR Suriname
213
+ SVK Slovakia
214
+ SVN Slovenia
215
+ SWE Sweden
216
+ SWZ Eswatini
217
+ SXM Sint Maarten (Dutch part)
218
+ SYC Seychelles
219
+ SYR Syrian Arab Republic | Syria
220
+ TCA Turks and Caicos Islands
221
+ TCD Chad
222
+ TGO Togo
223
+ THA Thailand
224
+ TJK Tajikistan
225
+ TKL Tokelau
226
+ TKM Turkmenistan
227
+ TLS Timor-Leste
228
+ TON Tonga
229
+ TTO Trinidad and Tobago
230
+ TUN Tunisia
231
+ TUR Türkiye | Turkey
232
+ TUV Tuvalu
233
+ TWN Taiwan
234
+ TZA Tanzania
235
+ UGA Uganda
236
+ UKR Ukraine
237
+ UMI United States Minor Outlying Islands
238
+ URY Uruguay
239
+ USA United States | US
240
+ UZB Uzbekistan
241
+ VAT Holy See (Vatican City State) | Vatican City
242
+ VCT Saint Vincent and the Grenadines
243
+ VEN Venezuela
244
+ VGB British Virgin Islands
245
+ VIR U.S. Virgin Islands
246
+ VNM Viet Nam | Vietnam
247
+ VUT Vanuatu
248
+ WLF Wallis and Futuna
249
+ WSM Samoa
250
+ YEM Yemen
251
+ ZAF South Africa
252
+ ZMB Zambia
253
+ ZWE Zimbabwe
254
+ """
255
+
256
+ ALPHA_3 = {
257
+ name.strip().casefold(): row[:3]
258
+ for row in _TABLE.strip().splitlines()
259
+ for name in row[4:].split("|")
260
+ }
@@ -6,6 +6,7 @@ from pathlib import Path, PurePath
6
6
  from urllib.parse import urlsplit
7
7
 
8
8
  from atsfill.core.conditions import Form, describe
9
+ from atsfill.core.countries import ALPHA_3
9
10
  from atsfill.core.results import Answer, Check, Field, FieldType
10
11
 
11
12
 
@@ -25,14 +26,21 @@ def _in_options(field: Field, values: list[str], form: Form | None) -> bool:
25
26
  return options is None or all(v in options for v in values)
26
27
 
27
28
 
29
+ def _place(part: str) -> str:
30
+ """A place's part, a country's name as its alpha-3 code, which some suggestions show."""
31
+ part = part.strip()
32
+ return ALPHA_3.get(part, part).casefold()
33
+
34
+
28
35
  def _picked_from(answer: str, suggestion: str) -> bool:
29
36
  """The suggestion starts with the answer, or holds its comma-separated parts in order:
30
- "Paris" and "Paris, France" both pick "Paris, Île-de-France, France"."""
37
+ "Paris" and "Paris, France" both pick "Paris, Île-de-France, France" and
38
+ "Paris, Île-de-France, FRA"."""
31
39
  answer, suggestion = _norm(answer).casefold(), _norm(suggestion).casefold()
32
40
  if suggestion.startswith(answer):
33
41
  return True
34
- parts = iter(p.strip() for p in suggestion.split(","))
35
- return all(any(p == wanted.strip() for p in parts) for wanted in answer.split(","))
42
+ parts = iter(_place(p) for p in suggestion.split(","))
43
+ return all(any(p == _place(wanted) for p in parts) for wanted in answer.split(","))
36
44
 
37
45
 
38
46
  def _absolute_url(value: str) -> bool:
File without changes
File without changes