codex-antigravity-auth 1.3.0__py3-none-any.whl
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.
- codex_antigravity_auth/__init__.py +15 -0
- codex_antigravity_auth/accounts.py +435 -0
- codex_antigravity_auth/byok.py +632 -0
- codex_antigravity_auth/cli.py +2716 -0
- codex_antigravity_auth/constants.py +128 -0
- codex_antigravity_auth/fingerprint.py +57 -0
- codex_antigravity_auth/models.py +489 -0
- codex_antigravity_auth/oauth.py +142 -0
- codex_antigravity_auth/observability.py +167 -0
- codex_antigravity_auth/redaction.py +113 -0
- codex_antigravity_auth/schema.py +81 -0
- codex_antigravity_auth/server.py +1941 -0
- codex_antigravity_auth/service.py +214 -0
- codex_antigravity_auth/skills/anti/SKILL.md +118 -0
- codex_antigravity_auth/skills/anti/agents/openai.yaml +4 -0
- codex_antigravity_auth/skills/anti/scripts/anti.py +3045 -0
- codex_antigravity_auth/skills/anti/tests/test_anti.py +1387 -0
- codex_antigravity_auth/storage.py +283 -0
- codex_antigravity_auth/transform.py +957 -0
- codex_antigravity_auth-1.3.0.dist-info/METADATA +306 -0
- codex_antigravity_auth-1.3.0.dist-info/RECORD +25 -0
- codex_antigravity_auth-1.3.0.dist-info/WHEEL +5 -0
- codex_antigravity_auth-1.3.0.dist-info/entry_points.txt +2 -0
- codex_antigravity_auth-1.3.0.dist-info/licenses/LICENSE +21 -0
- codex_antigravity_auth-1.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Codex Antigravity Auth package."""
|
|
2
|
+
|
|
3
|
+
__all__ = ["app", "main"]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def __getattr__(name):
|
|
7
|
+
if name == "app":
|
|
8
|
+
from .server import app
|
|
9
|
+
|
|
10
|
+
return app
|
|
11
|
+
if name == "main":
|
|
12
|
+
from .cli import main
|
|
13
|
+
|
|
14
|
+
return main
|
|
15
|
+
raise AttributeError(name)
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import time
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Any
|
|
5
|
+
from .storage import load_accounts, get_accounts_json_path, update_accounts
|
|
6
|
+
from .oauth import refresh_access_token, token_expires_in_seconds
|
|
7
|
+
from .fingerprint import generate_fingerprint
|
|
8
|
+
from .redaction import redact_secret_text
|
|
9
|
+
|
|
10
|
+
class AccountManager:
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self._lock = threading.RLock()
|
|
13
|
+
self._failures = {} # email -> failure count
|
|
14
|
+
self._cooldowns = {} # email -> cooldown end timestamp
|
|
15
|
+
self._counters = {} # email -> family -> sanitized usage counters
|
|
16
|
+
|
|
17
|
+
def _sync_state_from_storage(self, data: dict[str, Any]) -> bool:
|
|
18
|
+
state_missing = "accountState" not in data
|
|
19
|
+
state = data.get("accountState", {})
|
|
20
|
+
previous_state = state if isinstance(state, dict) else {}
|
|
21
|
+
accounts = data.get("accounts", [])
|
|
22
|
+
account_emails = {
|
|
23
|
+
str(account.get("email"))
|
|
24
|
+
for account in accounts
|
|
25
|
+
if isinstance(account, dict) and account.get("email")
|
|
26
|
+
}
|
|
27
|
+
if isinstance(state, dict):
|
|
28
|
+
failures = state.get("failures", {})
|
|
29
|
+
cooldowns = state.get("cooldowns", {})
|
|
30
|
+
counters = state.get("counters", {})
|
|
31
|
+
if state_missing:
|
|
32
|
+
if self._failures:
|
|
33
|
+
failures = {
|
|
34
|
+
**(failures if isinstance(failures, dict) else {}),
|
|
35
|
+
**self._failures,
|
|
36
|
+
}
|
|
37
|
+
if self._cooldowns:
|
|
38
|
+
cooldowns = {
|
|
39
|
+
**(cooldowns if isinstance(cooldowns, dict) else {}),
|
|
40
|
+
**self._cooldowns,
|
|
41
|
+
}
|
|
42
|
+
if self._counters:
|
|
43
|
+
counters = {
|
|
44
|
+
**(counters if isinstance(counters, dict) else {}),
|
|
45
|
+
**self._counters,
|
|
46
|
+
}
|
|
47
|
+
active_cooldown_emails = set()
|
|
48
|
+
cleaned_cooldowns = {}
|
|
49
|
+
if isinstance(cooldowns, dict):
|
|
50
|
+
current_time = time.time()
|
|
51
|
+
for k, v in cooldowns.items():
|
|
52
|
+
email = str(k)
|
|
53
|
+
if email not in account_emails:
|
|
54
|
+
continue
|
|
55
|
+
if not isinstance(v, (int, float)) or isinstance(v, bool):
|
|
56
|
+
continue
|
|
57
|
+
cooldown_end = float(v)
|
|
58
|
+
if math.isfinite(cooldown_end) and cooldown_end > current_time:
|
|
59
|
+
cleaned_cooldowns[email] = cooldown_end
|
|
60
|
+
active_cooldown_emails.add(email)
|
|
61
|
+
self._cooldowns = cleaned_cooldowns
|
|
62
|
+
cleaned_failures = {}
|
|
63
|
+
if isinstance(failures, dict):
|
|
64
|
+
for k, v in failures.items():
|
|
65
|
+
email = str(k)
|
|
66
|
+
if email not in active_cooldown_emails:
|
|
67
|
+
continue
|
|
68
|
+
if not isinstance(v, (int, float)) or isinstance(v, bool):
|
|
69
|
+
continue
|
|
70
|
+
failure_number = float(v)
|
|
71
|
+
if not math.isfinite(failure_number):
|
|
72
|
+
continue
|
|
73
|
+
failure_count = int(failure_number)
|
|
74
|
+
if failure_count > 0:
|
|
75
|
+
cleaned_failures[email] = failure_count
|
|
76
|
+
self._failures = cleaned_failures
|
|
77
|
+
cleaned_counters: dict[str, dict[str, dict[str, Any]]] = {}
|
|
78
|
+
if isinstance(counters, dict):
|
|
79
|
+
for email_key, raw_families in counters.items():
|
|
80
|
+
email = str(email_key)
|
|
81
|
+
if email not in account_emails or not isinstance(raw_families, dict):
|
|
82
|
+
continue
|
|
83
|
+
family_counters: dict[str, dict[str, Any]] = {}
|
|
84
|
+
for family_key, raw_counter in raw_families.items():
|
|
85
|
+
family = str(family_key)
|
|
86
|
+
if family not in {"claude", "gemini"} or not isinstance(raw_counter, dict):
|
|
87
|
+
continue
|
|
88
|
+
family_counters[family] = self._sanitize_counter(raw_counter)
|
|
89
|
+
if family_counters:
|
|
90
|
+
cleaned_counters[email] = family_counters
|
|
91
|
+
self._counters = cleaned_counters
|
|
92
|
+
else:
|
|
93
|
+
self._failures = {}
|
|
94
|
+
self._cooldowns = {}
|
|
95
|
+
self._counters = {}
|
|
96
|
+
cleaned_state = {
|
|
97
|
+
"failures": self._failures,
|
|
98
|
+
"cooldowns": self._cooldowns,
|
|
99
|
+
"counters": self._counters,
|
|
100
|
+
}
|
|
101
|
+
return previous_state != cleaned_state
|
|
102
|
+
|
|
103
|
+
def _save_state_to_storage(self) -> None:
|
|
104
|
+
if not get_accounts_json_path().exists():
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
def mutate(data: dict[str, Any]) -> None:
|
|
108
|
+
data["accountState"] = {
|
|
109
|
+
"failures": self._failures,
|
|
110
|
+
"cooldowns": self._cooldowns,
|
|
111
|
+
"counters": self._counters,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
update_accounts(mutate)
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _sanitize_counter(raw_counter: dict[str, Any]) -> dict[str, Any]:
|
|
118
|
+
sanitized: dict[str, Any] = {}
|
|
119
|
+
integer_fields = (
|
|
120
|
+
"total_requests",
|
|
121
|
+
"successes",
|
|
122
|
+
"failures",
|
|
123
|
+
"rate_limits",
|
|
124
|
+
"input_tokens",
|
|
125
|
+
"output_tokens",
|
|
126
|
+
"total_tokens",
|
|
127
|
+
)
|
|
128
|
+
for field in integer_fields:
|
|
129
|
+
value = raw_counter.get(field, 0)
|
|
130
|
+
if isinstance(value, bool):
|
|
131
|
+
value = 0
|
|
132
|
+
try:
|
|
133
|
+
parsed = int(value)
|
|
134
|
+
except (TypeError, ValueError):
|
|
135
|
+
parsed = 0
|
|
136
|
+
sanitized[field] = max(0, parsed)
|
|
137
|
+
for field in ("last_success", "last_failure", "last_failure_class"):
|
|
138
|
+
value = raw_counter.get(field)
|
|
139
|
+
if isinstance(value, str) and not any(ord(ch) < 0x20 or ord(ch) == 0x7F for ch in value):
|
|
140
|
+
sanitized[field] = redact_secret_text(value)[:200]
|
|
141
|
+
return sanitized
|
|
142
|
+
|
|
143
|
+
@staticmethod
|
|
144
|
+
def _model_family(model: str) -> str:
|
|
145
|
+
return "claude" if "claude" in str(model).lower() else "gemini"
|
|
146
|
+
|
|
147
|
+
def _counter_for(self, email: str, family: str) -> dict[str, Any]:
|
|
148
|
+
account_counters = self._counters.setdefault(email, {})
|
|
149
|
+
counter = account_counters.setdefault(
|
|
150
|
+
family,
|
|
151
|
+
{
|
|
152
|
+
"total_requests": 0,
|
|
153
|
+
"successes": 0,
|
|
154
|
+
"failures": 0,
|
|
155
|
+
"rate_limits": 0,
|
|
156
|
+
"input_tokens": 0,
|
|
157
|
+
"output_tokens": 0,
|
|
158
|
+
"total_tokens": 0,
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
account_counters[family] = self._sanitize_counter(counter)
|
|
162
|
+
return account_counters[family]
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def _normalize_expires_at(value: Any) -> float:
|
|
166
|
+
try:
|
|
167
|
+
expires_at = float(value or 0)
|
|
168
|
+
except (TypeError, ValueError):
|
|
169
|
+
return 0
|
|
170
|
+
if not math.isfinite(expires_at):
|
|
171
|
+
return 0
|
|
172
|
+
# Epoch milliseconds are currently around 1.7e12; epoch seconds around 1.7e9.
|
|
173
|
+
if expires_at > 10_000_000_000:
|
|
174
|
+
expires_at = expires_at / 1000
|
|
175
|
+
return expires_at
|
|
176
|
+
|
|
177
|
+
def get_accounts(self) -> list[dict[str, Any]]:
|
|
178
|
+
with self._lock:
|
|
179
|
+
data = load_accounts()
|
|
180
|
+
return data.get("accounts", [])
|
|
181
|
+
|
|
182
|
+
def select_active_account(self, model: str) -> dict[str, Any] | None:
|
|
183
|
+
with self._lock:
|
|
184
|
+
selected: dict[str, Any] | None = None
|
|
185
|
+
|
|
186
|
+
def mutate(data: dict[str, Any]) -> bool:
|
|
187
|
+
nonlocal selected
|
|
188
|
+
dirty = self._sync_state_from_storage(data)
|
|
189
|
+
accounts = data.get("accounts", [])
|
|
190
|
+
if not accounts:
|
|
191
|
+
if dirty:
|
|
192
|
+
data["accountState"] = {
|
|
193
|
+
"failures": self._failures,
|
|
194
|
+
"cooldowns": self._cooldowns,
|
|
195
|
+
}
|
|
196
|
+
return dirty
|
|
197
|
+
|
|
198
|
+
family = "claude" if "claude" in model.lower() else "gemini"
|
|
199
|
+
family_map = data.setdefault("activeIndexByFamily", {"claude": 0, "gemini": 0})
|
|
200
|
+
|
|
201
|
+
# Simple rotation/selection strategy:
|
|
202
|
+
# Check accounts from the preferred active index for this family.
|
|
203
|
+
start_index = family_map.get(family, 0)
|
|
204
|
+
if not isinstance(start_index, int) or start_index < 0 or start_index >= len(accounts):
|
|
205
|
+
start_index = 0
|
|
206
|
+
|
|
207
|
+
current_time = time.time()
|
|
208
|
+
for i in range(len(accounts)):
|
|
209
|
+
idx = (start_index + i) % len(accounts)
|
|
210
|
+
acc = accounts[idx]
|
|
211
|
+
email = acc.get("email")
|
|
212
|
+
if not email:
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
cooldown_end = self._cooldowns.get(email, 0)
|
|
216
|
+
if cooldown_end > current_time:
|
|
217
|
+
continue
|
|
218
|
+
if cooldown_end:
|
|
219
|
+
self._cooldowns.pop(email, None)
|
|
220
|
+
self._failures.pop(email, None)
|
|
221
|
+
dirty = True
|
|
222
|
+
|
|
223
|
+
if not acc.get("fingerprint"):
|
|
224
|
+
acc["fingerprint"] = generate_fingerprint()
|
|
225
|
+
dirty = True
|
|
226
|
+
|
|
227
|
+
expires_at = self._normalize_expires_at(acc.get("expiresAt", 0))
|
|
228
|
+
if expires_at != acc.get("expiresAt", 0):
|
|
229
|
+
acc["expiresAt"] = expires_at
|
|
230
|
+
dirty = True
|
|
231
|
+
if not acc.get("accessToken") or expires_at < current_time + 300:
|
|
232
|
+
refresh_tok = acc.get("refreshToken")
|
|
233
|
+
if refresh_tok:
|
|
234
|
+
try:
|
|
235
|
+
refreshed = refresh_access_token(refresh_tok)
|
|
236
|
+
acc["accessToken"] = refreshed["access_token"]
|
|
237
|
+
acc["expiresAt"] = current_time + token_expires_in_seconds(refreshed)
|
|
238
|
+
if refreshed.get("refresh_token"):
|
|
239
|
+
acc["refreshToken"] = refreshed["refresh_token"]
|
|
240
|
+
dirty = True
|
|
241
|
+
except Exception as e:
|
|
242
|
+
self._record_failure(email, retry_after_seconds=None)
|
|
243
|
+
print(f"[*] Account {email} flagged as cooling down. Reason: {redact_secret_text(f'Token refresh failed: {e}')}")
|
|
244
|
+
dirty = True
|
|
245
|
+
continue
|
|
246
|
+
else:
|
|
247
|
+
self._record_failure(email, retry_after_seconds=None)
|
|
248
|
+
print(f"[*] Account {email} flagged as cooling down. Reason: Token expired and no refresh token is available")
|
|
249
|
+
dirty = True
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
if family_map.get(family) != idx:
|
|
253
|
+
dirty = True
|
|
254
|
+
family_map[family] = idx
|
|
255
|
+
if data.get("activeIndex") != idx:
|
|
256
|
+
dirty = True
|
|
257
|
+
data["activeIndex"] = idx
|
|
258
|
+
state_payload = {
|
|
259
|
+
"failures": self._failures,
|
|
260
|
+
"cooldowns": self._cooldowns,
|
|
261
|
+
"counters": self._counters,
|
|
262
|
+
}
|
|
263
|
+
if self._failures or self._cooldowns or self._counters or data.get("accountState"):
|
|
264
|
+
if data.get("accountState") != state_payload:
|
|
265
|
+
dirty = True
|
|
266
|
+
data["accountState"] = state_payload
|
|
267
|
+
selected = acc
|
|
268
|
+
return dirty
|
|
269
|
+
if dirty:
|
|
270
|
+
data["accountState"] = {
|
|
271
|
+
"failures": self._failures,
|
|
272
|
+
"cooldowns": self._cooldowns,
|
|
273
|
+
"counters": self._counters,
|
|
274
|
+
}
|
|
275
|
+
return dirty
|
|
276
|
+
|
|
277
|
+
update_accounts(mutate)
|
|
278
|
+
return selected
|
|
279
|
+
|
|
280
|
+
def _record_failure(self, email: str, retry_after_seconds: float | None = None) -> float:
|
|
281
|
+
previous_failures = self._failures.get(email, 0)
|
|
282
|
+
if not isinstance(previous_failures, int) or isinstance(previous_failures, bool) or previous_failures < 0:
|
|
283
|
+
previous_failures = 0
|
|
284
|
+
self._failures[email] = previous_failures + 1
|
|
285
|
+
backoff_factor = min(self._failures[email], 5)
|
|
286
|
+
cooldown_duration = 120 * (2 ** (backoff_factor - 1))
|
|
287
|
+
try:
|
|
288
|
+
retry_after = (
|
|
289
|
+
float(retry_after_seconds)
|
|
290
|
+
if retry_after_seconds is not None and not isinstance(retry_after_seconds, bool)
|
|
291
|
+
else 0.0
|
|
292
|
+
)
|
|
293
|
+
except (TypeError, ValueError):
|
|
294
|
+
retry_after = 0.0
|
|
295
|
+
if math.isfinite(retry_after) and retry_after > 0:
|
|
296
|
+
cooldown_duration = max(cooldown_duration, min(retry_after, 86_400.0))
|
|
297
|
+
self._cooldowns[email] = time.time() + cooldown_duration
|
|
298
|
+
return cooldown_duration
|
|
299
|
+
|
|
300
|
+
def mark_failure(
|
|
301
|
+
self,
|
|
302
|
+
email: str,
|
|
303
|
+
reason: str,
|
|
304
|
+
retry_after_seconds: float | None = None,
|
|
305
|
+
*,
|
|
306
|
+
model: str | None = None,
|
|
307
|
+
status_code: int | None = None,
|
|
308
|
+
) -> None:
|
|
309
|
+
with self._lock:
|
|
310
|
+
if not email:
|
|
311
|
+
return
|
|
312
|
+
cooldown_duration = self._record_failure(email, retry_after_seconds)
|
|
313
|
+
self._save_state_to_storage()
|
|
314
|
+
|
|
315
|
+
# Print warning
|
|
316
|
+
print(f"[*] Account {email} flagged as cooling down for {cooldown_duration}s. Reason: {redact_secret_text(reason)}")
|
|
317
|
+
|
|
318
|
+
def record_request(
|
|
319
|
+
self,
|
|
320
|
+
email: str,
|
|
321
|
+
model: str,
|
|
322
|
+
*,
|
|
323
|
+
status: str,
|
|
324
|
+
status_code: int | None = None,
|
|
325
|
+
error_class: str | None = None,
|
|
326
|
+
usage: dict[str, Any] | None = None,
|
|
327
|
+
) -> None:
|
|
328
|
+
with self._lock:
|
|
329
|
+
self._record_request_locked(
|
|
330
|
+
email,
|
|
331
|
+
model,
|
|
332
|
+
status=status,
|
|
333
|
+
status_code=status_code,
|
|
334
|
+
error_class=error_class,
|
|
335
|
+
usage=usage,
|
|
336
|
+
persist=True,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def _record_request_locked(
|
|
340
|
+
self,
|
|
341
|
+
email: str,
|
|
342
|
+
model: str,
|
|
343
|
+
*,
|
|
344
|
+
status: str,
|
|
345
|
+
status_code: int | None = None,
|
|
346
|
+
error_class: str | None = None,
|
|
347
|
+
usage: dict[str, Any] | None = None,
|
|
348
|
+
persist: bool,
|
|
349
|
+
) -> None:
|
|
350
|
+
if not email:
|
|
351
|
+
return
|
|
352
|
+
family = self._model_family(model)
|
|
353
|
+
counter = self._counter_for(email, family)
|
|
354
|
+
counter["total_requests"] = int(counter.get("total_requests", 0)) + 1
|
|
355
|
+
now_text = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
356
|
+
if status == "success":
|
|
357
|
+
counter["successes"] = int(counter.get("successes", 0)) + 1
|
|
358
|
+
counter["last_success"] = now_text
|
|
359
|
+
else:
|
|
360
|
+
counter["failures"] = int(counter.get("failures", 0)) + 1
|
|
361
|
+
if status_code == 429:
|
|
362
|
+
counter["rate_limits"] = int(counter.get("rate_limits", 0)) + 1
|
|
363
|
+
counter["last_failure"] = now_text
|
|
364
|
+
if error_class:
|
|
365
|
+
counter["last_failure_class"] = redact_secret_text(str(error_class))[:200]
|
|
366
|
+
if usage:
|
|
367
|
+
for usage_field, counter_field in (
|
|
368
|
+
("input_tokens", "input_tokens"),
|
|
369
|
+
("output_tokens", "output_tokens"),
|
|
370
|
+
("total_tokens", "total_tokens"),
|
|
371
|
+
):
|
|
372
|
+
value = usage.get(usage_field)
|
|
373
|
+
if isinstance(value, bool):
|
|
374
|
+
continue
|
|
375
|
+
try:
|
|
376
|
+
parsed = int(value)
|
|
377
|
+
except (TypeError, ValueError):
|
|
378
|
+
continue
|
|
379
|
+
if parsed > 0:
|
|
380
|
+
counter[counter_field] = int(counter.get(counter_field, 0)) + parsed
|
|
381
|
+
self._counters[email][family] = self._sanitize_counter(counter)
|
|
382
|
+
if persist:
|
|
383
|
+
self._save_state_to_storage()
|
|
384
|
+
|
|
385
|
+
def refresh_expiring_accounts(self, window_seconds: int = 300) -> dict[str, int]:
|
|
386
|
+
with self._lock:
|
|
387
|
+
summary = {"checked": 0, "refreshed": 0, "failed": 0}
|
|
388
|
+
if not get_accounts_json_path().exists():
|
|
389
|
+
return summary
|
|
390
|
+
current_time = time.time()
|
|
391
|
+
|
|
392
|
+
def mutate(data: dict[str, Any]) -> bool:
|
|
393
|
+
dirty = self._sync_state_from_storage(data)
|
|
394
|
+
accounts = data.get("accounts", [])
|
|
395
|
+
if not isinstance(accounts, list):
|
|
396
|
+
return dirty
|
|
397
|
+
for acc in accounts:
|
|
398
|
+
if not isinstance(acc, dict):
|
|
399
|
+
continue
|
|
400
|
+
email = acc.get("email")
|
|
401
|
+
refresh_tok = acc.get("refreshToken")
|
|
402
|
+
if not email or not refresh_tok:
|
|
403
|
+
continue
|
|
404
|
+
summary["checked"] += 1
|
|
405
|
+
expires_at = self._normalize_expires_at(acc.get("expiresAt", 0))
|
|
406
|
+
if expires_at > current_time + max(0, int(window_seconds)):
|
|
407
|
+
continue
|
|
408
|
+
try:
|
|
409
|
+
refreshed = refresh_access_token(refresh_tok)
|
|
410
|
+
acc["accessToken"] = refreshed["access_token"]
|
|
411
|
+
acc["expiresAt"] = current_time + token_expires_in_seconds(refreshed)
|
|
412
|
+
if refreshed.get("refresh_token"):
|
|
413
|
+
acc["refreshToken"] = refreshed["refresh_token"]
|
|
414
|
+
summary["refreshed"] += 1
|
|
415
|
+
dirty = True
|
|
416
|
+
except Exception:
|
|
417
|
+
self._record_failure(str(email), retry_after_seconds=None)
|
|
418
|
+
summary["failed"] += 1
|
|
419
|
+
dirty = True
|
|
420
|
+
if dirty:
|
|
421
|
+
data["accountState"] = {
|
|
422
|
+
"failures": self._failures,
|
|
423
|
+
"cooldowns": self._cooldowns,
|
|
424
|
+
"counters": self._counters,
|
|
425
|
+
}
|
|
426
|
+
return dirty
|
|
427
|
+
|
|
428
|
+
update_accounts(mutate)
|
|
429
|
+
return summary
|
|
430
|
+
|
|
431
|
+
def clear_failures(self, email: str) -> None:
|
|
432
|
+
with self._lock:
|
|
433
|
+
self._failures.pop(email, None)
|
|
434
|
+
self._cooldowns.pop(email, None)
|
|
435
|
+
self._save_state_to_storage()
|