atsfill 0.0.4.dev133__tar.gz → 0.0.4.dev134__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.4.dev133 → atsfill-0.0.4.dev134}/PKG-INFO +2 -2
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/README.md +1 -1
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/pyproject.toml +1 -1
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/pyproject.toml.orig +1 -1
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/successfactors.js +3 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/successfactors.py +67 -14
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/registry.toml +1 -1
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/LICENSE +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/__init__.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/__init__.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/ashby.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/bamboohr.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/breezy.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/dayforce.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/eightfold.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/greenhouse.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/gupy.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/hireology.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/hiringthing.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/hrmdirect.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/isolvedhire.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/jazzhr.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/join.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/lever.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/manatal.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/oracle.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/paycor.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/paylocity.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/personio.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/phenom.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/pinpoint.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/recruitee.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/rippling.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/smartrecruiters.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/taleo.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/teamtailor.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/ukg.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/workable.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/workday.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/wttj.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/adapters/zohorecruit.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/cli.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/core/__init__.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/core/accounts.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/core/adapter.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/core/attempts.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/core/browser.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/core/commands.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/core/conditions.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/core/html.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/core/matching.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/core/passwords.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/core/paths.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/core/registry.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/src/atsfill/core/results.py +0 -0
- {atsfill-0.0.4.dev133 → atsfill-0.0.4.dev134}/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.
|
|
3
|
+
Version: 0.0.4.dev134
|
|
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
|
|
@@ -57,7 +57,7 @@ It is built for agents with a shell: every command prints one JSON document and
|
|
|
57
57
|
## Supported hiring systems
|
|
58
58
|
|
|
59
59
|
<!-- badges -->
|
|
60
|
-
                                                       
|
|
61
61
|
<!-- /badges -->
|
|
62
62
|
|
|
63
63
|
| System | Account needed | `read` without a browser |
|
|
@@ -29,7 +29,7 @@ It is built for agents with a shell: every command prints one JSON document and
|
|
|
29
29
|
## Supported hiring systems
|
|
30
30
|
|
|
31
31
|
<!-- badges -->
|
|
32
|
-
                                                       
|
|
33
33
|
<!-- /badges -->
|
|
34
34
|
|
|
35
35
|
| System | Account needed | `read` without a browser |
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
d.kind = 'picklist';
|
|
27
27
|
d.id = picklist.id;
|
|
28
28
|
d.owns = picklist.getAttribute('aria-owns');
|
|
29
|
+
d.placeholder = picklist.placeholder; // its empty item's text, in the page's language
|
|
30
|
+
const opener = box.querySelector('button.rcmpaginatedselectbutton');
|
|
31
|
+
d.disabled = !!opener && opener.disabled; // a cascading list before its parent is chosen
|
|
29
32
|
d.value = picklist.value === picklist.placeholder ? '' : picklist.value;
|
|
30
33
|
d.required = d.required || picklist.getAttribute('aria-required') === 'true';
|
|
31
34
|
} else if (radios.length) {
|
|
@@ -11,7 +11,7 @@ import re
|
|
|
11
11
|
import time
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import Any
|
|
14
|
-
from urllib.parse import urlsplit
|
|
14
|
+
from urllib.parse import parse_qs, urlsplit
|
|
15
15
|
|
|
16
16
|
from atsfill.core.adapter import (
|
|
17
17
|
Blocked,
|
|
@@ -22,7 +22,7 @@ from atsfill.core.adapter import (
|
|
|
22
22
|
Unsupported,
|
|
23
23
|
known_gap,
|
|
24
24
|
)
|
|
25
|
-
from atsfill.core.browser import Locator, Page, Request, Timeout
|
|
25
|
+
from atsfill.core.browser import Locator, Page, PageRequest, Request, Timeout
|
|
26
26
|
from atsfill.core.results import Answer, Field, FieldType, SideEffect
|
|
27
27
|
|
|
28
28
|
JOB = re.compile(r"^(?:/[^/]+)?/job/[^/]+/(?P<id>\d{5,})/?$")
|
|
@@ -30,13 +30,19 @@ DC_HOST = re.compile(r"^career\d*\.(?:sapsf|successfactors)\.(?:eu|com|cn)$")
|
|
|
30
30
|
APPLY = "a[href*='/talentcommunity/apply/']"
|
|
31
31
|
COMPANY_COOKIE = "careerSiteCompanyId"
|
|
32
32
|
PROFILE = "/career?company={company}&career_ns=job_listing_summary&navBarLevel=MY_PROFILE"
|
|
33
|
-
|
|
33
|
+
JOB_REQ = "career_job_req_id" # the job the apply link posts, the candidate pages' own id
|
|
34
|
+
APPLICATION = "/career?company={company}&career_ns=job_application&career_job_req_id={req}"
|
|
35
|
+
GUIDED = "body.cxApplyEnabled" # a site whose application is a step-by-step one, not sections
|
|
36
|
+
HOME = "body.careerUserLoggedIn" # any candidate page signed in, the candidate home's too
|
|
37
|
+
# The body's class for an account; a signed-out page also holds a log-out link and its label
|
|
38
|
+
SIGNED_IN = re.compile(r"<body[^>]*\bclass=[\"'][^\"']*\bcareerUserLoggedIn\b")
|
|
34
39
|
FIELDS = (Path(__file__).with_suffix(".js")).read_text()
|
|
35
40
|
# What the candidate pages show once loaded
|
|
36
41
|
SIGN_IN = "#username"
|
|
37
42
|
COUNTRY = "fbclc_country" # its options' values are ISO 3166 alpha-2 codes
|
|
38
43
|
PHONE, CALLING_CODE = "#fbclc_phoneNumber", "#fbclc_ituCode" # `CA`: `Canada (+1)`
|
|
39
44
|
SIGN_UP = "#fbclc_userName"
|
|
45
|
+
PASSCODE = "#passcode" # an OTP some sign-ups ask before the account exists
|
|
40
46
|
QUICK_APPLY = "#fbqa_apply"
|
|
41
47
|
FORM = ".RCMFormField"
|
|
42
48
|
LANDED = f"{SIGN_IN}, {SIGN_UP}, {FORM}"
|
|
@@ -47,6 +53,8 @@ SIGN_IN_MODAL = "a[onclick*=openSignInModal]" # a Quick Apply page's
|
|
|
47
53
|
REFUSAL = re.compile(r"required|exist|already|invalid|bereits|erforderlich|ungültig", re.I)
|
|
48
54
|
ACTIVATE = re.compile(r"activat|verify your|verification e-?mail|bestätig", re.I)
|
|
49
55
|
PRIVACY = "#dataPrivacyId"
|
|
56
|
+
VISIBILITY = "input[name=fbclc_searPref]" # who may see the profile
|
|
57
|
+
APPLIED_ONLY = "2" # its option for only the recruiters of the jobs applied to
|
|
50
58
|
EXPAND = "[id$=_expandAllSections]"
|
|
51
59
|
SUBMIT = "[id$=_submitBtn]"
|
|
52
60
|
OVERLAY = "#sfOverlayMgr" # a confirmation dialog
|
|
@@ -88,6 +96,15 @@ def _company(request: Request) -> list[tuple[str, str]]:
|
|
|
88
96
|
)
|
|
89
97
|
|
|
90
98
|
|
|
99
|
+
def _posts_job(request: PageRequest) -> bool:
|
|
100
|
+
"""Whether the apply link's request posts a job to the candidate pages."""
|
|
101
|
+
return (
|
|
102
|
+
request.method == "POST"
|
|
103
|
+
and bool(DC_HOST.match(urlsplit(request.url).hostname or ""))
|
|
104
|
+
and JOB_REQ in (request.post_data or "")
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
91
108
|
def _calling_code(select: Locator, details: SignUpDetails) -> tuple[str, str]:
|
|
92
109
|
"""The calling code's option for the E.164 phone, the one of `country` among the countries
|
|
93
110
|
sharing it, and the national number that follows the code."""
|
|
@@ -108,7 +125,10 @@ class SuccessFactors:
|
|
|
108
125
|
name = "successfactors"
|
|
109
126
|
per_employer = True
|
|
110
127
|
asks = {"name"}
|
|
111
|
-
known_gaps = {
|
|
128
|
+
known_gaps = {
|
|
129
|
+
"quick_apply_sign_up": Gap.FLOW,
|
|
130
|
+
"guided_application": Gap.FLOW,
|
|
131
|
+
}
|
|
112
132
|
side_effects = [
|
|
113
133
|
SideEffect("sign_up", "employer", "the account is created in the employer's tenant"),
|
|
114
134
|
SideEffect("fill", "hiring_system", "an attachment is uploaded to the account's profile"),
|
|
@@ -140,7 +160,7 @@ class SuccessFactors:
|
|
|
140
160
|
return self._postings.get(scope or "", f"https://{scope}/")
|
|
141
161
|
|
|
142
162
|
def signed_in(self, request: Request, scope: str | None) -> bool:
|
|
143
|
-
"""The profile page of the session's company
|
|
163
|
+
"""The profile page of the session's company marks its body for an account."""
|
|
144
164
|
for host, company in _company(request):
|
|
145
165
|
response = request.get(f"https://{host}{PROFILE.format(company=company)}")
|
|
146
166
|
if response.ok and SIGNED_IN.search(response.text()):
|
|
@@ -165,8 +185,9 @@ class SuccessFactors:
|
|
|
165
185
|
|
|
166
186
|
def sign_up(self, page: Page, email: str, password: str, details: SignUpDetails) -> None:
|
|
167
187
|
"""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),
|
|
169
|
-
requires one, its calling code picked from the number
|
|
188
|
+
residence from `country` (the list's first without it), the phone where the site
|
|
189
|
+
requires one, its calling code picked from the number, and the profile visible only to
|
|
190
|
+
the recruiters of the jobs applied to where the site asks; a missing phone is refused
|
|
170
191
|
before anything is sent."""
|
|
171
192
|
self._candidate(page)
|
|
172
193
|
if page.locator(SIGN_IN).count():
|
|
@@ -206,6 +227,11 @@ class SuccessFactors:
|
|
|
206
227
|
if wanted and wanted not in values:
|
|
207
228
|
raise Unsupported(f"unhandled_rendering: the sign-up's countries lack {wanted}")
|
|
208
229
|
select.select_option(wanted or values[0])
|
|
230
|
+
if page.locator(f"{VISIBILITY}:visible").count():
|
|
231
|
+
least = page.locator(f"{VISIBILITY}[value='{APPLIED_ONLY}']")
|
|
232
|
+
if not least.count():
|
|
233
|
+
raise Unsupported("unhandled_rendering: the profile's visibility has no jobs only")
|
|
234
|
+
least.check()
|
|
209
235
|
if page.locator(PRIVACY).count():
|
|
210
236
|
page.locator(PRIVACY).click()
|
|
211
237
|
dialog = page.locator("[role=dialog]:visible").last
|
|
@@ -216,6 +242,8 @@ class SuccessFactors:
|
|
|
216
242
|
page.wait_for_timeout(POLL_MS)
|
|
217
243
|
if page.locator(FORM).count():
|
|
218
244
|
return
|
|
245
|
+
if page.locator(f"{PASSCODE}:visible").count():
|
|
246
|
+
raise Blocked("email_otp: the sign-up sent a passcode to the email")
|
|
219
247
|
if not page.locator(f"{SIGN_UP}:visible").count():
|
|
220
248
|
break
|
|
221
249
|
if self._message(page, REFUSAL):
|
|
@@ -226,7 +254,7 @@ class SuccessFactors:
|
|
|
226
254
|
if re.search(r"password|kennwort", message, re.IGNORECASE):
|
|
227
255
|
raise Refused(f"password_rule: {message}")
|
|
228
256
|
if message:
|
|
229
|
-
raise Unsupported(
|
|
257
|
+
raise Unsupported(f"unhandled_rendering: the sign-up refused: {message}")
|
|
230
258
|
text = page.locator("body").inner_text()
|
|
231
259
|
if ACTIVATE.search(text):
|
|
232
260
|
raise Blocked("email_link: the sign-up sent an activation email")
|
|
@@ -249,8 +277,8 @@ class SuccessFactors:
|
|
|
249
277
|
page.wait_for_timeout(SETTLE_MS)
|
|
250
278
|
fields = []
|
|
251
279
|
for d in page.evaluate(FIELDS):
|
|
252
|
-
if d["kind"] == "picklist":
|
|
253
|
-
d["options"] = self._options(page, d)
|
|
280
|
+
if d["kind"] == "picklist": # a disabled one waits for its parent: no items yet
|
|
281
|
+
d["options"] = [] if d["disabled"] else self._options(page, d)
|
|
254
282
|
self._fields[d["key"]] = d
|
|
255
283
|
fields.append(self._field(d))
|
|
256
284
|
return fields
|
|
@@ -288,7 +316,7 @@ class SuccessFactors:
|
|
|
288
316
|
items = self._open(page, d)
|
|
289
317
|
options = [t for t in items.evaluate_all("as => as.map(a => a.title.trim())") if t]
|
|
290
318
|
self._close(page, d)
|
|
291
|
-
return [o for o in options if o
|
|
319
|
+
return [o for o in options if o not in (NO_SELECTION, d["placeholder"])]
|
|
292
320
|
|
|
293
321
|
def _open(self, page: Page, d: dict[str, Any]) -> Locator:
|
|
294
322
|
"""Opens a picklist; a click can go unheard, or close a list another left open."""
|
|
@@ -343,6 +371,8 @@ class SuccessFactors:
|
|
|
343
371
|
self._upload(page, d, value)
|
|
344
372
|
|
|
345
373
|
def _pick(self, page: Page, d: dict[str, Any], value: str) -> None:
|
|
374
|
+
if d["disabled"]:
|
|
375
|
+
raise NotTaken(f"{d['key']} stays disabled: its parent's answer offers no items")
|
|
346
376
|
items = self._open(page, d)
|
|
347
377
|
titles = [t.strip() for t in items.evaluate_all("as => as.map(a => a.title)")]
|
|
348
378
|
wanted = next((i for i, t in enumerate(titles) if t == value), None)
|
|
@@ -364,8 +394,8 @@ class SuccessFactors:
|
|
|
364
394
|
confirm.first.wait_for(timeout=LIST_TIMEOUT_MS)
|
|
365
395
|
confirm.first.click()
|
|
366
396
|
deadline = time.monotonic() + UPLOAD_TIMEOUT_S
|
|
367
|
-
while self._current(page, d["key"])["multiple"]:
|
|
368
|
-
if time.monotonic() > deadline:
|
|
397
|
+
while (now := self._current(page, d["key"]))["multiple"] or now["id"] is None:
|
|
398
|
+
if time.monotonic() > deadline: # deleting, it shows neither button
|
|
369
399
|
raise NotTaken(f"{d['key']} kept its file")
|
|
370
400
|
page.wait_for_timeout(POLL_MS)
|
|
371
401
|
source = page.locator("[role=dialog]:visible input[type=file]")
|
|
@@ -407,16 +437,39 @@ class SuccessFactors:
|
|
|
407
437
|
return page.url
|
|
408
438
|
|
|
409
439
|
def _candidate(self, page: Page) -> None:
|
|
410
|
-
"""From the posting, follows its apply link to the candidate pages.
|
|
440
|
+
"""From the posting, follows its apply link to the candidate pages. The link posts the
|
|
441
|
+
job there; signed in, some sites drop it and land on the candidate home, so the
|
|
442
|
+
application is opened from the job the link posted."""
|
|
411
443
|
host = urlsplit(page.url).hostname or ""
|
|
444
|
+
posted: list[PageRequest] = []
|
|
412
445
|
if not DC_HOST.match(host):
|
|
413
446
|
apply = page.locator(APPLY)
|
|
414
447
|
if not apply.count():
|
|
415
448
|
raise Unsupported("posting_gone: the posting has no apply link")
|
|
449
|
+
|
|
450
|
+
def keep(request: PageRequest) -> None:
|
|
451
|
+
if _posts_job(request):
|
|
452
|
+
posted.append(request)
|
|
453
|
+
|
|
454
|
+
page.on("request", keep)
|
|
416
455
|
apply.first.evaluate("a => a.click()") # some sites open a dialog on a real click
|
|
417
456
|
try:
|
|
457
|
+
page.locator(f"{LANDED}, {HOME}").first.wait_for(
|
|
458
|
+
state="attached", timeout=LANDING_TIMEOUT_MS
|
|
459
|
+
)
|
|
460
|
+
page.wait_for_load_state(timeout=LANDING_TIMEOUT_MS)
|
|
461
|
+
if posted and JOB_REQ not in page.url and not page.locator(LANDED).count():
|
|
462
|
+
post = posted[0]
|
|
463
|
+
parts = urlsplit(post.url)
|
|
464
|
+
job = parse_qs(parts.query) | parse_qs(post.post_data or "")
|
|
465
|
+
application = APPLICATION.format(company=job["company"][0], req=job[JOB_REQ][0])
|
|
466
|
+
page.goto(f"https://{parts.hostname}{application}")
|
|
418
467
|
page.locator(LANDED).first.wait_for(state="attached", timeout=LANDING_TIMEOUT_MS)
|
|
419
468
|
except Timeout:
|
|
469
|
+
if page.locator(GUIDED).count():
|
|
470
|
+
raise Unsupported(
|
|
471
|
+
known_gap(self, "guided_application", "the application is a step-by-step one")
|
|
472
|
+
) from None
|
|
420
473
|
raise Unsupported(
|
|
421
474
|
f"unhandled_rendering: the apply link led to {urlsplit(page.url).path}"
|
|
422
475
|
) from None
|
|
@@ -96,8 +96,8 @@ 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 who may see the profile" }
|
|
100
99
|
mapped = { date = 2026-10-20, version = "0.0.2", by = "agent", commit = "7934b25" }
|
|
100
|
+
dry_run_verified = { date = 2026-10-26, version = "0.0.3", by = "adapter_gate", commit = "79c8327" }
|
|
101
101
|
|
|
102
102
|
[ukg]
|
|
103
103
|
kind = "ats"
|
|
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
|