atsfill 0.0.4.dev127__tar.gz → 0.0.4.dev129__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 (55) hide show
  1. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/PKG-INFO +1 -1
  2. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/pyproject.toml +1 -1
  3. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/pyproject.toml.orig +1 -1
  4. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/hrmdirect.py +9 -6
  5. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/successfactors.py +36 -2
  6. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/ukg.py +19 -13
  7. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/core/commands.py +1 -1
  8. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/registry.toml +5 -5
  9. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/LICENSE +0 -0
  10. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/README.md +0 -0
  11. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/__init__.py +0 -0
  12. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/__init__.py +0 -0
  13. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/ashby.py +0 -0
  14. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/bamboohr.py +0 -0
  15. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/breezy.py +0 -0
  16. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/dayforce.py +0 -0
  17. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/eightfold.py +0 -0
  18. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/greenhouse.py +0 -0
  19. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/gupy.py +0 -0
  20. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/hireology.py +0 -0
  21. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/hiringthing.py +0 -0
  22. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/jazzhr.py +0 -0
  23. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/join.py +0 -0
  24. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/lever.py +0 -0
  25. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/manatal.py +0 -0
  26. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/oracle.py +0 -0
  27. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/paycor.py +0 -0
  28. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/paylocity.py +0 -0
  29. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/personio.py +0 -0
  30. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/phenom.py +0 -0
  31. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/pinpoint.py +0 -0
  32. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/recruitee.py +0 -0
  33. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/rippling.py +0 -0
  34. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/smartrecruiters.py +0 -0
  35. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/successfactors.js +0 -0
  36. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/taleo.py +0 -0
  37. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/teamtailor.py +0 -0
  38. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/workable.py +0 -0
  39. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/workday.py +0 -0
  40. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/wttj.py +0 -0
  41. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/adapters/zohorecruit.py +0 -0
  42. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/cli.py +0 -0
  43. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/core/__init__.py +0 -0
  44. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/core/accounts.py +0 -0
  45. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/core/adapter.py +0 -0
  46. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/core/attempts.py +0 -0
  47. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/core/browser.py +0 -0
  48. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/core/conditions.py +0 -0
  49. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/core/html.py +0 -0
  50. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/core/matching.py +0 -0
  51. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/core/passwords.py +0 -0
  52. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/core/paths.py +0 -0
  53. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/core/registry.py +0 -0
  54. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/core/results.py +0 -0
  55. {atsfill-0.0.4.dev127 → atsfill-0.0.4.dev129}/src/atsfill/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atsfill
3
- Version: 0.0.4.dev127
3
+ Version: 0.0.4.dev129
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.4.dev127"
3
+ version = "0.0.4.dev129"
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.dev127"
3
+ version = "0.0.4.dev129"
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"
@@ -97,7 +97,7 @@ class HRMDirect:
97
97
  name = "hrmdirect"
98
98
  per_employer = True
99
99
  asks = {"name"}
100
- known_gaps = {"screening_first": Gap.FLOW, "sign_up_phone": Gap.FLOW}
100
+ known_gaps = {"screening_first": Gap.FLOW}
101
101
  side_effects = [
102
102
  SideEffect(
103
103
  "sign_up", "employer", "the account is the employer's candidate, emailed a link"
@@ -184,17 +184,18 @@ class HRMDirect:
184
184
 
185
185
  def sign_up(self, page: Page, email: str, password: str, details: SignUpDetails) -> None:
186
186
  """Fills the apply page's first step, which creates the account signed in and sets no
187
- password: the email that follows holds a link to set one, `blocked: email_link`."""
187
+ password: the email that follows holds a link to set one, `blocked: email_link`. The
188
+ mobile phone is given only where the org requires it; a missing one is refused before
189
+ anything is sent."""
188
190
  _to_apply(page)
189
191
  if not page.locator(IDENTITY).is_visible():
190
192
  raise Unsupported(
191
193
  known_gap(self, "screening_first", "the form's first page comes before sign-up")
192
194
  )
193
195
  phone = page.locator(f"{IDENTITY} .form-group:has(input.mobile-number)")
194
- if phone.count() and not phone.locator(".common-option-label").count():
195
- raise Unsupported(
196
- known_gap(self, "sign_up_phone", "the sign-up step requires a mobile phone")
197
- )
196
+ phone_asked = phone.count() and not phone.locator(".common-option-label").count()
197
+ if phone_asked and not details.phone:
198
+ raise Refused("sign_up_details: no phone")
198
199
  if page.locator(RECAPTCHA).count():
199
200
  raise Blocked("captcha: a reCAPTCHA box keeps the sign-up's Continue disabled")
200
201
  first, _, last = (details.name or "").strip().partition(" ")
@@ -203,6 +204,8 @@ class HRMDirect:
203
204
  form.locator("input.last-name").fill(last or first)
204
205
  form.locator("input.email").fill(email)
205
206
  form.locator("input.email-confirm").fill(email)
207
+ if phone_asked:
208
+ form.locator("input.mobile-number").fill(details.phone or "")
206
209
  for group in form.locator(".radio-group:has(input[required])").all():
207
210
  group.locator("label:has(input[value=false])").click() # declines, the least given
208
211
  form.locator("button.submit-button").click()
@@ -34,6 +34,8 @@ SIGNED_IN = re.compile(r"\bsignOut|\blogout\b|_logout", re.IGNORECASE)
34
34
  FIELDS = (Path(__file__).with_suffix(".js")).read_text()
35
35
  # What the candidate pages show once loaded
36
36
  SIGN_IN = "#username"
37
+ COUNTRY = "fbclc_country" # its options' values are ISO 3166 alpha-2 codes
38
+ PHONE, CALLING_CODE = "#fbclc_phoneNumber", "#fbclc_ituCode" # `CA`: `Canada (+1)`
37
39
  SIGN_UP = "#fbclc_userName"
38
40
  QUICK_APPLY = "#fbqa_apply"
39
41
  FORM = ".RCMFormField"
@@ -86,6 +88,22 @@ def _company(request: Request) -> list[tuple[str, str]]:
86
88
  )
87
89
 
88
90
 
91
+ def _calling_code(select: Locator, details: SignUpDetails) -> tuple[str, str]:
92
+ """The calling code's option for the E.164 phone, the one of `country` among the countries
93
+ sharing it, and the national number that follows the code."""
94
+ options = select.evaluate("s => [...s.options].map(o => [o.value, o.textContent])")
95
+ phone = (details.phone or "").removeprefix("+")
96
+ codes = [
97
+ (len(m[1]), value == details.country, value)
98
+ for value, text in options
99
+ if (m := re.search(r"\(\+(\d+)\)", text)) and phone.startswith(m[1])
100
+ ]
101
+ if not codes:
102
+ raise Unsupported(f"unhandled_rendering: the sign-up offers no calling code for {phone}")
103
+ length, _, value = max(codes, key=lambda c: c[:2]) # the first of the best
104
+ return value, phone[length:]
105
+
106
+
89
107
  class SuccessFactors:
90
108
  name = "successfactors"
91
109
  per_employer = True
@@ -146,6 +164,10 @@ class SuccessFactors:
146
164
  raise Refused(f"wrong_password: {message}") from None
147
165
 
148
166
  def sign_up(self, page: Page, email: str, password: str, details: SignUpDetails) -> None:
167
+ """Creates the account from the career site's sign-up: the name, the country of
168
+ residence from `country` (the list's first without it), and the phone where the site
169
+ requires one, its calling code picked from the number; a missing phone is refused
170
+ before anything is sent."""
149
171
  self._candidate(page)
150
172
  if page.locator(SIGN_IN).count():
151
173
  page.locator(REGISTER).first.click()
@@ -160,6 +182,9 @@ class SuccessFactors:
160
182
  ) from None
161
183
  register.first.click()
162
184
  page.locator(SIGN_UP).wait_for(timeout=LANDING_TIMEOUT_MS)
185
+ phone_asked = page.locator(f"{PHONE}[aria-required=true]:visible").count() > 0
186
+ if phone_asked and not details.phone:
187
+ raise Refused("sign_up_details: no phone")
163
188
  first, _, last = (details.name or "").strip().partition(" ")
164
189
  for selector, value in [
165
190
  (SIGN_UP, email),
@@ -170,10 +195,17 @@ class SuccessFactors:
170
195
  ("#fbclc_lName", last or first),
171
196
  ]:
172
197
  page.fill(selector, value)
173
- for select in page.locator("select:visible").all(): # the country, a phone code
198
+ if phone_asked and details.phone:
199
+ code, national = _calling_code(page.locator(CALLING_CODE), details)
200
+ page.locator(CALLING_CODE).select_option(code)
201
+ page.fill(PHONE, national)
202
+ for select in page.locator("select:visible").all(): # the country, any other
174
203
  if not select.input_value():
175
204
  values = select.evaluate("s => [...s.options].map(o => o.value).filter(v => v)")
176
- select.select_option(values[0])
205
+ wanted = details.country if select.get_attribute("id") == COUNTRY else None
206
+ if wanted and wanted not in values:
207
+ raise Unsupported(f"unhandled_rendering: the sign-up's countries lack {wanted}")
208
+ select.select_option(wanted or values[0])
177
209
  if page.locator(PRIVACY).count():
178
210
  page.locator(PRIVACY).click()
179
211
  dialog = page.locator("[role=dialog]:visible").last
@@ -201,6 +233,8 @@ class SuccessFactors:
201
233
  if page.locator(f"{SIGN_IN}:visible").count(): # created, signed out
202
234
  self.sign_in(page, email, password)
203
235
  return
236
+ if self.signed_in(page.request, None): # created, back on the career site
237
+ return
204
238
  raise Unsupported(f"unhandled_rendering: the sign-up led to {urlsplit(page.url).path}")
205
239
 
206
240
  def browser_read(self, page: Page) -> list[Field]:
@@ -36,7 +36,6 @@ YES_NO = ["Yes", "No"]
36
36
  DATE_SHOWN = re.compile(r"(\d\d)/(\d\d)/(\d{4})")
37
37
  COUNTRY, STATE = "Country", "State"
38
38
  DAYS, DOCUMENTS = "availability_days", "documents"
39
- PHONE_GAP = "sign_up_phone"
40
39
 
41
40
  # Auth0's error codes on the sign-in and sign-up pages, by what atsfill raises
42
41
  ERRORS = [
@@ -218,8 +217,8 @@ def board(url: str) -> Board | None:
218
217
  class UKG:
219
218
  name = "ukg"
220
219
  per_employer = True
221
- asks = {"name"}
222
- known_gaps = {PHONE_GAP: Gap.FLOW}
220
+ asks = {"name", "phone"} # the phone only some tenants require, asked before the sign-up
221
+ known_gaps: dict[str, Gap] = {}
223
222
  side_effects = [
224
223
  SideEffect("sign_up", "employer", "the account is created in the employer's tenant"),
225
224
  SideEffect("upload", "hiring_system", "a document is stored in the account's documents"),
@@ -284,7 +283,7 @@ class UKG:
284
283
  _left_sign_in(page)
285
284
  if "/AuthCode/Register" not in page.url:
286
285
  return
287
- _register(page, details.name or "")
286
+ _register(page, details)
288
287
 
289
288
  def browser_read(self, page: Page) -> list[Field]:
290
289
  found = board(page.url)
@@ -397,26 +396,33 @@ def _left_sign_in(page: Page) -> None:
397
396
  raise Unsupported("unhandled_rendering: the sign-in page answered nothing")
398
397
 
399
398
 
400
- def _register(page: Page, name: str) -> None:
401
- """The tenant's own step after the sign-up: the name, and the privacy policy's box on some
402
- tenants; the box for text messages is left. Its inputs take typed keys only: a filled value
399
+ def _register(page: Page, details: SignUpDetails) -> None:
400
+ """The tenant's own step after the sign-up: the name, the phone where the tenant requires
401
+ it, and the privacy policy's box on some tenants; the optional consents (text messages, AI
402
+ scoring, ticked by default) are declined. Its inputs take typed keys only: a filled value
403
403
  leaves Create account disabled."""
404
404
  page.locator("input[name=firstName]").wait_for(timeout=FORM_TIMEOUT_MS)
405
405
  page.wait_for_load_state("networkidle")
406
+ first, _, last = (details.name or "").strip().partition(" ")
407
+ values = [("firstName", first), ("lastName", last or first)]
406
408
  if page.locator("ukg-input[name=phoneNumber][required=true]").count():
407
- raise Unsupported(f"{Gap.FLOW}: {PHONE_GAP}: the tenant's sign-up asks a phone number")
408
- first, _, last = name.strip().partition(" ")
409
- for field, value in (("firstName", first), ("lastName", last or first)):
409
+ values.append(("phoneNumber", details.phone or ""))
410
+ for field, value in values:
410
411
  page.locator(f"input[name={field}]").click()
411
412
  page.locator(f"input[name={field}]").press_sequentially(value)
412
- privacy = page.locator("ukg-checkbox[data-automation*=privacy]:visible")
413
- for box in privacy.all():
414
- if box.get_attribute("aria-checked") != "true":
413
+ for box in page.locator("ukg-checkbox[data-automation*=-checkbox]:visible").all():
414
+ wanted = "privacy" in (box.get_attribute("data-automation") or "")
415
+ if _ticked(box) != wanted:
415
416
  box.click()
416
417
  page.get_by_role("button", name="Create account").click()
417
418
  page.wait_for_url(lambda u: "/AuthCode/Register" not in u, timeout=FORM_TIMEOUT_MS)
418
419
 
419
420
 
421
+ def _ticked(box: Locator) -> bool:
422
+ """A `ukg-checkbox`'s state: `aria-checked` once clicked, `checked` as the page set it."""
423
+ return "true" in (box.get_attribute("aria-checked"), box.get_attribute("checked"))
424
+
425
+
420
426
  def _field(raw: dict[str, Any], states: dict[str, list[str]] | None) -> Field:
421
427
  kind, field_id = raw["kind"], raw["id"]
422
428
  label = raw["label"] or field_id
@@ -63,7 +63,7 @@ def _login_required(url: str, adapter: NeedsAccount, user: str) -> Result:
63
63
 
64
64
  type Credentials = tuple[str, str] # email, password
65
65
  # Refusals the page states before any account exists: the generated password goes.
66
- NO_ACCOUNT = {"email_in_use", "email_domain_rejected", "password_rule"}
66
+ NO_ACCOUNT = {"email_in_use", "email_domain_rejected", "password_rule", "sign_up_details"}
67
67
 
68
68
 
69
69
  def _email_link(reason: str | None, email: str, command: str) -> str | None:
@@ -96,7 +96,7 @@ share = 0.0350
96
96
  account = "to_read"
97
97
  hosting = ["hosted"]
98
98
  adapter = "successfactors"
99
- unsupported = { date = 2026-10-26, version = "0.0.3", commit = "0d8baea", by = "agent", reason = "quick_apply_sign_up: a Quick Apply page creates the account and sends the application in one submit, its sign-in offering no sign-up; sign_up_fields: a sign-up asking a phone number" }
99
+ unsupported = { date = 2026-10-26, version = "0.0.3", commit = "0d8baea", by = "agent", reason = "quick_apply_sign_up: a Quick Apply page creates the account and sends the application in one submit, its sign-in offering no sign-up; sign_up_fields: a sign-up asking who may see the profile" }
100
100
  mapped = { date = 2026-10-20, version = "0.0.2", by = "agent", commit = "7934b25" }
101
101
 
102
102
  [ukg]
@@ -106,7 +106,7 @@ account = "to_read"
106
106
  hosting = ["hosted"]
107
107
  adapter = "ukg"
108
108
  mapped = { date = 2026-10-20, version = "0.0.2", by = "agent", commit = "7934b25" }
109
- unsupported = { date = 2026-10-26, version = "0.0.3", commit = "a60a17d", by = "agent", reason = "unsupported_flow: sign_up_phone: the tenant's details step after the sign-up requires a phone number, which login does not take (10 of 17 tenants)" }
109
+ unsupported = { date = 2026-10-26, version = "0.0.3", commit = "a60a17d", by = "agent", reason = "unhandled on the forms the sign-up now reaches, 3 of 8 tenants ready: no gender decline box to tick (3), a State whose condition names no Country (1), a numeric answer read back differently (1)" }
110
110
 
111
111
  [paycom]
112
112
  kind = "ats"
@@ -260,10 +260,10 @@ dry_run_verified = { date = 2026-10-22, version = "0.0.3", by = "adapter_gate",
260
260
  [ukgready]
261
261
  kind = "ats"
262
262
  share = 0.0085
263
- account = "to_read" # the first step's Continue creates a password-less account and the job application; a sign-up with a password asks a zip code
263
+ account = "to_read" # the sign-in's Create a new account sets a password, asking the name, country and zip code
264
264
  hosting = ["hosted"]
265
265
  mapped = { date = 2026-10-20, version = "0.0.2", by = "agent", commit = "7934b25" }
266
- unsupported = { date = 2026-10-26, version = "0.0.3", commit = "95f2522", by = "agent", reason = "unsupported_flow: sign_up_zip: the sign-up that sets a password requires a zip code, which login does not take; the first step's account has none, so it cannot be signed in to again (14 of 14 tenants)" }
266
+ unsupported = { date = 2026-10-26, version = "0.0.3", commit = "b0cb322", by = "agent", reason = "unsupported_flow: profile_wizard: signed in, the form is the applicant portal's profile wizard, items of a dozen types edited in cards, entry lists, a security question and an electronic signature; not built within the round (1 tenant walked)" }
267
267
 
268
268
  [adpmyjobs]
269
269
  kind = "ats"
@@ -306,7 +306,7 @@ share = 0.0059
306
306
  account = "to_read"
307
307
  hosting = ["hosted"]
308
308
  mapped = { date = 2026-10-20, version = "0.0.2", by = "agent", commit = "7934b25" }
309
- unsupported = { date = 2026-10-26, version = "0.0.3", commit = "211fe03", by = "agent", reason = "unsupported_flow: screening_first: the form's first page comes before the sign-up step (4 of 14 orgs); sign_up_phone: the sign-up step requires a mobile phone, which login does not take (1 of 17); a reCAPTCHA box on the sign-up step (1 of 17); 3 orgs signed in, under the 5 dry runs the gate needs" }
309
+ unsupported = { date = 2026-10-26, version = "0.0.3", commit = "211fe03", by = "agent", reason = "unsupported_flow: screening_first: the form's first page comes before the sign-up step (4 of 14 orgs); a reCAPTCHA box on the sign-up step (1 of 17); 3 orgs signed in, under the 5 dry runs the gate needs" }
310
310
 
311
311
  [applicantpro]
312
312
  kind = "ats"
File without changes
File without changes