atsfill 0.0.5.dev144__tar.gz → 0.0.5.dev146__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.5.dev144 → atsfill-0.0.5.dev146}/PKG-INFO +1 -1
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/pyproject.toml +2 -1
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/pyproject.toml.orig +4 -1
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/core/accounts.py +16 -5
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/core/adapter.py +27 -1
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/core/commands.py +53 -30
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/core/results.py +1 -1
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/LICENSE +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/README.md +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/__init__.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/__init__.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/ashby.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/bamboohr.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/breezy.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/dayforce.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/eightfold.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/greenhouse.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/gupy.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/hireology.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/hiringthing.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/hrmdirect.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/isolvedhire.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/jazzhr.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/join.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/lever.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/manatal.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/oracle.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/paycor.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/paylocity.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/personio.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/phenom.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/pinpoint.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/recruitee.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/rippling.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/smartrecruiters.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/successfactors.js +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/successfactors.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/taleo.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/teamtailor.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/ukg.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/workable.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/workday.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/wttj.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/adapters/zohorecruit.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/cli.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/core/__init__.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/core/attempts.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/core/browser.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/core/conditions.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/core/html.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/core/matching.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/core/otp.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/core/passwords.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/core/paths.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/core/registry.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/core/snapshot.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/core/telemetry.py +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/src/atsfill/py.typed +0 -0
- {atsfill-0.0.5.dev144 → atsfill-0.0.5.dev146}/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.
|
|
3
|
+
Version: 0.0.5.dev146
|
|
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.
|
|
3
|
+
version = "0.0.5.dev146"
|
|
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"
|
|
@@ -51,6 +51,7 @@ 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"]
|
|
54
55
|
commit-check = ["commit-check==2.19.0"]
|
|
55
56
|
dev = [
|
|
56
57
|
"pytest>=9.1.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "atsfill"
|
|
3
|
-
version = "0.0.5.
|
|
3
|
+
version = "0.0.5.dev146"
|
|
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"
|
|
@@ -41,6 +41,9 @@ requires = ["uv_build>=0.13.0,<0.14.0"]
|
|
|
41
41
|
build-backend = "uv_build"
|
|
42
42
|
|
|
43
43
|
[dependency-groups]
|
|
44
|
+
docs = [
|
|
45
|
+
"zensical==0.0.73",
|
|
46
|
+
]
|
|
44
47
|
commit-check = [
|
|
45
48
|
"commit-check==2.19.0",
|
|
46
49
|
]
|
|
@@ -8,9 +8,11 @@ from pathlib import Path
|
|
|
8
8
|
from atsfill.core import browser
|
|
9
9
|
from atsfill.core.adapter import (
|
|
10
10
|
CreatesAccount,
|
|
11
|
+
CreatesAccountByOtp,
|
|
11
12
|
FinishesSignUp,
|
|
12
13
|
NeedsAccount,
|
|
13
14
|
SignsIn,
|
|
15
|
+
SignsInByOtp,
|
|
14
16
|
SignUpDetails,
|
|
15
17
|
Unsupported,
|
|
16
18
|
)
|
|
@@ -61,21 +63,30 @@ def remember(user_dir: Path, adapter: str, scope: str | None, address: str) -> N
|
|
|
61
63
|
|
|
62
64
|
def sign_in(
|
|
63
65
|
page: Page,
|
|
64
|
-
adapter: SignsIn,
|
|
66
|
+
adapter: SignsIn | SignsInByOtp,
|
|
65
67
|
scope: str | None,
|
|
66
68
|
address: str,
|
|
67
|
-
password: str,
|
|
69
|
+
password: str | None,
|
|
68
70
|
session: Path,
|
|
69
71
|
details: SignUpDetails | None = None,
|
|
70
72
|
create: bool = False,
|
|
71
73
|
) -> None:
|
|
72
74
|
"""Signs in from the system's login page and stores the session: with `create`, creates
|
|
73
75
|
the account from the user's sign-up `details`; without, `details` finish a sign-up the
|
|
74
|
-
account was left in, where the adapter knows that step.
|
|
76
|
+
account was left in, where the adapter knows that step. An account without a password
|
|
77
|
+
(`password` None) signs in by OTP, and creates the same way.
|
|
75
78
|
Raises what the adapter raises, without the password in it."""
|
|
76
79
|
page.goto(adapter.login_url(scope))
|
|
77
80
|
try:
|
|
78
|
-
if
|
|
81
|
+
if isinstance(adapter, SignsInByOtp):
|
|
82
|
+
if create:
|
|
83
|
+
assert isinstance(adapter, CreatesAccountByOtp) and details is not None
|
|
84
|
+
adapter.sign_up_by_otp(page, address, details)
|
|
85
|
+
else:
|
|
86
|
+
adapter.sign_in_by_otp(page, address)
|
|
87
|
+
elif password is None:
|
|
88
|
+
raise AssertionError(f"{adapter.name} signs in with a password")
|
|
89
|
+
elif create:
|
|
79
90
|
assert isinstance(adapter, CreatesAccount) and details is not None
|
|
80
91
|
adapter.sign_up(page, address, password, details)
|
|
81
92
|
elif details is not None and isinstance(adapter, FinishesSignUp):
|
|
@@ -83,7 +94,7 @@ def sign_in(
|
|
|
83
94
|
else:
|
|
84
95
|
adapter.sign_in(page, address, password)
|
|
85
96
|
except Exception as e:
|
|
86
|
-
if password in str(e):
|
|
97
|
+
if password and password in str(e):
|
|
87
98
|
raise redacted(e, password) from None
|
|
88
99
|
raise
|
|
89
100
|
if not adapter.signed_in(page.request, scope):
|
|
@@ -42,7 +42,7 @@ def out_of_scope(field_id: str, what: str) -> str:
|
|
|
42
42
|
|
|
43
43
|
class Refused(Exception):
|
|
44
44
|
"""The system turned down a sign-in or a sign-up for a reason it states:
|
|
45
|
-
`email_in_use`, `email_domain_rejected`, `password_rule`, `wrong_password`.
|
|
45
|
+
`email_in_use`, `email_domain_rejected`, `password_rule`, `wrong_password`, `no_account`.
|
|
46
46
|
|
|
47
47
|
The message is the result's reason, `code` or `code: detail`.
|
|
48
48
|
"""
|
|
@@ -125,6 +125,18 @@ class SignsIn(NeedsAccount, Protocol):
|
|
|
125
125
|
...
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
@runtime_checkable
|
|
129
|
+
class SignsInByOtp(NeedsAccount, Protocol):
|
|
130
|
+
"""An adapter atsfill can sign in through with an email and the OTP its system emails each
|
|
131
|
+
time, an account without a password (ADR 0015)."""
|
|
132
|
+
|
|
133
|
+
def sign_in_by_otp(self, page: Page, email: str) -> None:
|
|
134
|
+
"""Signs in on the page `login_url` loaded, typing the OTP with `otp.enter`. Raises as
|
|
135
|
+
`sign_in` does, and `Refused("no_account")` before the click that would turn an unknown
|
|
136
|
+
email's sign-in into a sign-up."""
|
|
137
|
+
...
|
|
138
|
+
|
|
139
|
+
|
|
128
140
|
ADDRESS = ("street", "city", "state", "zip", "country")
|
|
129
141
|
PHONE = re.compile(r"\+[1-9][0-9]{6,14}") # E.164
|
|
130
142
|
COUNTRY = re.compile(r"[A-Z]{2}") # ISO 3166 alpha-2
|
|
@@ -183,6 +195,20 @@ class CreatesAccount(SignsIn, Protocol):
|
|
|
183
195
|
...
|
|
184
196
|
|
|
185
197
|
|
|
198
|
+
@runtime_checkable
|
|
199
|
+
class CreatesAccountByOtp(SignsInByOtp, Protocol):
|
|
200
|
+
"""An adapter atsfill can create an account without a password through. Account creation is
|
|
201
|
+
a side effect it declares (ADR 0008)."""
|
|
202
|
+
|
|
203
|
+
asks: set[str] # the `SignUpDetails` fields its sign-up requires
|
|
204
|
+
|
|
205
|
+
def sign_up_by_otp(self, page: Page, email: str, details: SignUpDetails) -> None:
|
|
206
|
+
"""Creates the account from the page `login_url` loaded, or signs in where the email
|
|
207
|
+
has one, signed in once it returns. Raises as `sign_in_by_otp` does, `no_account` aside;
|
|
208
|
+
the details are given as `sign_up` gives them."""
|
|
209
|
+
...
|
|
210
|
+
|
|
211
|
+
|
|
186
212
|
@runtime_checkable
|
|
187
213
|
class FinishesSignUp(CreatesAccount, Protocol):
|
|
188
214
|
"""An adapter whose system can keep an account half made: signed up, but a step the
|
|
@@ -23,12 +23,14 @@ from atsfill.core.adapter import (
|
|
|
23
23
|
Adapter,
|
|
24
24
|
Blocked,
|
|
25
25
|
CreatesAccount,
|
|
26
|
+
CreatesAccountByOtp,
|
|
26
27
|
ListsShownFields,
|
|
27
28
|
NeedsAccount,
|
|
28
29
|
NotTaken,
|
|
29
30
|
ReadsByRequest,
|
|
30
31
|
Refused,
|
|
31
32
|
SignsIn,
|
|
33
|
+
SignsInByOtp,
|
|
32
34
|
SignUpDetails,
|
|
33
35
|
Unsupported,
|
|
34
36
|
)
|
|
@@ -59,14 +61,14 @@ def _detect(candidates: list[Adapter], url: str, page: Page | None = None) -> Ad
|
|
|
59
61
|
return next((a for a in candidates if a.detect(url, page)), None)
|
|
60
62
|
|
|
61
63
|
|
|
62
|
-
def login_command(adapter: str, user: str, url: str | None) -> str:
|
|
64
|
+
def login_command(adapter: str, user: str, url: str | None, *more: str) -> str:
|
|
63
65
|
"""The `login` command line for one account: its user, and a posting where it is scoped."""
|
|
64
66
|
command = ["atsfill", "login", adapter]
|
|
65
67
|
if user != paths.DEFAULT_USER:
|
|
66
68
|
command += ["--user", user]
|
|
67
69
|
if url:
|
|
68
70
|
command += ["--url", url]
|
|
69
|
-
return shlex.join(command)
|
|
71
|
+
return shlex.join([*command, *more])
|
|
70
72
|
|
|
71
73
|
|
|
72
74
|
def _login_required(url: str, adapter: NeedsAccount, user: str) -> Result:
|
|
@@ -74,27 +76,38 @@ def _login_required(url: str, adapter: NeedsAccount, user: str) -> Result:
|
|
|
74
76
|
return Result(Status.BLOCKED, url, adapter.name, reason=f"login_required: run `{command}`")
|
|
75
77
|
|
|
76
78
|
|
|
77
|
-
type Credentials = tuple[str, str] # email, password
|
|
79
|
+
type Credentials = tuple[str, str | None] # email, password: None for an account without one
|
|
78
80
|
# Refusals the page states before any account exists: the generated password goes.
|
|
79
81
|
NO_ACCOUNT = {"email_in_use", "email_domain_rejected", "password_rule", "sign_up_details"}
|
|
80
82
|
|
|
81
83
|
|
|
82
|
-
def
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
def _next_step(
|
|
85
|
+
reason: str | None, email: str, adapter: str, user: str, url: str | None
|
|
86
|
+
) -> str | None:
|
|
87
|
+
"""Names the command to run next: once the verification link sent to the address is
|
|
88
|
+
clicked, or `--create` for an email the system has no account for."""
|
|
89
|
+
code = (reason or "").split(":")[0]
|
|
90
|
+
if code == "email_link":
|
|
91
|
+
command = login_command(adapter, user, url)
|
|
85
92
|
return f"email_link: click the link sent to {email}, then run `{command}`"
|
|
93
|
+
if code == "no_account":
|
|
94
|
+
command = login_command(adapter, user, url, "--email", email, "--create")
|
|
95
|
+
return f"no_account: {email} has no {adapter} account; run `{command}`"
|
|
86
96
|
return reason
|
|
87
97
|
|
|
88
98
|
|
|
89
99
|
def _credentials(
|
|
90
100
|
adapter: NeedsAccount, user: str, mine: Path, scope: str | None, given: str | None = None
|
|
91
101
|
) -> Credentials | None:
|
|
92
|
-
"""The account's email and a password from its password source,
|
|
102
|
+
"""The account's email and a password from its password source, or no password for an
|
|
103
|
+
account without one when the user has an OTP command for it, where atsfill can sign in
|
|
93
104
|
through the adapter; None otherwise."""
|
|
94
|
-
if not isinstance(adapter, SignsIn):
|
|
105
|
+
if not isinstance(adapter, (SignsIn, SignsInByOtp)):
|
|
95
106
|
return None
|
|
96
107
|
if not (email := accounts.email(mine, adapter.name, scope)):
|
|
97
108
|
return None
|
|
109
|
+
if isinstance(adapter, SignsInByOtp):
|
|
110
|
+
return (email, None) if otp.command(user, adapter.name) else None
|
|
98
111
|
password = passwords.password(user, mine, adapter.name, scope, email, given)
|
|
99
112
|
return (email, password) if password else None
|
|
100
113
|
|
|
@@ -106,7 +119,8 @@ def _opened(
|
|
|
106
119
|
"""The loaded page and the adapter that claims it by its URL, else by the URL a redirect
|
|
107
120
|
lands on, else by its page; or the result that ends the command first. An adapter that
|
|
108
121
|
needs an account gets the user's session, and only that adapter's; signed out with a
|
|
109
|
-
password source,
|
|
122
|
+
password source, or an OTP command for an account without a password, atsfill signs in
|
|
123
|
+
again (ADR 0015). The adapter's name goes in `seen`
|
|
110
124
|
once known. The adapters are fresh: none keeps state from another
|
|
111
125
|
posting (a survey or the adapter gate runs many commands in one process)."""
|
|
112
126
|
candidates = adapters()
|
|
@@ -162,8 +176,8 @@ def _sign_in_again(
|
|
|
162
176
|
url: str,
|
|
163
177
|
credentials: Credentials | None,
|
|
164
178
|
) -> Result | None:
|
|
165
|
-
"""Signs in
|
|
166
|
-
|
|
179
|
+
"""Signs in as `_credentials` allows, stores the session and loads the posting again; the
|
|
180
|
+
result that ends the command instead, `login_required` where it does not."""
|
|
167
181
|
result = Result(Status.BLOCKED, url, adapter.name)
|
|
168
182
|
with _stops(result):
|
|
169
183
|
scope = adapter.account_scope(url)
|
|
@@ -172,14 +186,15 @@ def _sign_in_again(
|
|
|
172
186
|
except passwords.PasswordError as e:
|
|
173
187
|
result.reason = f"password_source: {e}"
|
|
174
188
|
return result
|
|
175
|
-
if credentials is None or not isinstance(adapter, SignsIn):
|
|
189
|
+
if credentials is None or not isinstance(adapter, (SignsIn, SignsInByOtp)):
|
|
176
190
|
return _login_required(url, adapter, user)
|
|
177
191
|
session = accounts.session_file(mine, adapter.name, scope)
|
|
178
192
|
accounts.sign_in(page, adapter, scope, *credentials, session)
|
|
179
193
|
page.goto(url)
|
|
180
194
|
return None
|
|
181
|
-
|
|
182
|
-
|
|
195
|
+
email = credentials[0] if credentials else ""
|
|
196
|
+
scoped = url if adapter.per_employer else None
|
|
197
|
+
result.reason = _next_step(result.reason, email, adapter.name, user, scoped)
|
|
183
198
|
return result
|
|
184
199
|
|
|
185
200
|
|
|
@@ -533,7 +548,9 @@ def login(
|
|
|
533
548
|
creates the account with a password atsfill generates and stores first (a side effect),
|
|
534
549
|
giving the sign-up the user's `details`; with a password already stored, the account may
|
|
535
550
|
exist, so it signs in with that password and keeps the account's email instead, the
|
|
536
|
-
`details` finishing a sign-up the account was left in.
|
|
551
|
+
`details` finishing a sign-up the account was left in. An account without a password signs
|
|
552
|
+
in through the user's OTP command in place of a password source, and is created the same
|
|
553
|
+
way."""
|
|
537
554
|
account, data = _account(adapter), paths.data_dir(data_dir)
|
|
538
555
|
scope = _scope(account, url)
|
|
539
556
|
mine = paths.user_dir(data, user)
|
|
@@ -543,8 +560,11 @@ def login(
|
|
|
543
560
|
raise ValueError("creating an account needs --email")
|
|
544
561
|
if details and not create:
|
|
545
562
|
raise ValueError("--details goes with --create")
|
|
563
|
+
by_otp = isinstance(account, SignsInByOtp)
|
|
564
|
+
if password and by_otp:
|
|
565
|
+
raise ValueError(f"{adapter} signs in with an emailed OTP, not a password")
|
|
546
566
|
try:
|
|
547
|
-
kept = create and passwords.stored(mine, adapter, scope) is not None
|
|
567
|
+
kept = create and not by_otp and passwords.stored(mine, adapter, scope) is not None
|
|
548
568
|
except passwords.PasswordError as e:
|
|
549
569
|
report.status, report.reason = Status.BLOCKED, f"password_source: {e}"
|
|
550
570
|
return report
|
|
@@ -578,11 +598,8 @@ def login(
|
|
|
578
598
|
)
|
|
579
599
|
else:
|
|
580
600
|
result = _headless(data, user, account, scope, credentials, session, details)
|
|
581
|
-
|
|
582
|
-
report.
|
|
583
|
-
result.status,
|
|
584
|
-
_email_link(result.reason, credentials[0], command),
|
|
585
|
-
)
|
|
601
|
+
report.status = result.status
|
|
602
|
+
report.reason = _next_step(result.reason, credentials[0], adapter, user, url)
|
|
586
603
|
if report.reason is None:
|
|
587
604
|
report.session = report.status = "signed_in"
|
|
588
605
|
return report
|
|
@@ -599,8 +616,9 @@ def _create(
|
|
|
599
616
|
) -> SessionReport:
|
|
600
617
|
"""Creates the account: refused before the browser opens when `details` lack one the
|
|
601
618
|
sign-up asks; the generated password is stored before the click, and deleted only on a
|
|
602
|
-
refusal the page states before any account exists.
|
|
603
|
-
|
|
619
|
+
refusal the page states before any account exists. An account without a password gets
|
|
620
|
+
none."""
|
|
621
|
+
if not isinstance(account, (CreatesAccount, CreatesAccountByOtp)):
|
|
604
622
|
report.status = Status.UNSUPPORTED
|
|
605
623
|
report.reason = f"no_sign_up: atsfill cannot create a {account.name} account"
|
|
606
624
|
return report
|
|
@@ -608,9 +626,10 @@ def _create(
|
|
|
608
626
|
report.status = Status.REFUSED
|
|
609
627
|
report.reason = f"sign_up_details: no {', '.join(missing)}"
|
|
610
628
|
return report
|
|
611
|
-
password = passwords.generate()
|
|
629
|
+
password = None if isinstance(account, CreatesAccountByOtp) else passwords.generate()
|
|
612
630
|
try:
|
|
613
|
-
|
|
631
|
+
if password:
|
|
632
|
+
passwords.store(mine, account.name, report.scope, password)
|
|
614
633
|
except passwords.PasswordError as e:
|
|
615
634
|
report.status, report.reason = Status.BLOCKED, f"password_source: {e}"
|
|
616
635
|
return report
|
|
@@ -618,10 +637,14 @@ def _create(
|
|
|
618
637
|
result = _headless(
|
|
619
638
|
data, report.user, account, report.scope, credentials, report.path, details, create=True
|
|
620
639
|
)
|
|
621
|
-
|
|
622
|
-
report.
|
|
623
|
-
report.password = "stored"
|
|
624
|
-
if
|
|
640
|
+
report.status = result.status
|
|
641
|
+
report.reason = _next_step(result.reason, email, account.name, report.user, url)
|
|
642
|
+
report.password = "stored" if password else "none"
|
|
643
|
+
if (
|
|
644
|
+
password
|
|
645
|
+
and result.status is Status.REFUSED
|
|
646
|
+
and (result.reason or "").split(":")[0] in NO_ACCOUNT
|
|
647
|
+
):
|
|
625
648
|
passwords.delete(mine, account.name, report.scope)
|
|
626
649
|
report.password = "deleted"
|
|
627
650
|
if report.reason is None:
|
|
@@ -641,7 +664,7 @@ def _headless(
|
|
|
641
664
|
) -> Result:
|
|
642
665
|
"""Signs in, or creates the account with `create`, through the adapter, an OTP it meets
|
|
643
666
|
from the user's `otp-commands` entry: `ready` once the session is stored."""
|
|
644
|
-
assert isinstance(account, SignsIn)
|
|
667
|
+
assert isinstance(account, (SignsIn, SignsInByOtp))
|
|
645
668
|
url = account.login_url(scope)
|
|
646
669
|
|
|
647
670
|
def run() -> Result:
|
|
@@ -121,7 +121,7 @@ class SessionReport:
|
|
|
121
121
|
path: Path
|
|
122
122
|
status: str | None = None # a login's outcome: signed_in | refused | blocked | unsupported
|
|
123
123
|
reason: str | None = None # why a login stored nothing, `code` or `code: detail`
|
|
124
|
-
password: str | None = None # after `--create`: stored | deleted, or
|
|
124
|
+
password: str | None = None # after `--create`: stored | deleted | kept, or none (OTP only)
|
|
125
125
|
|
|
126
126
|
|
|
127
127
|
@dataclass
|
|
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
|