fix: restore polished account experience and align AI with Lark design
This commit is contained in:
@@ -7,7 +7,7 @@ contain credentials, cookies, or temporary authentication links.
|
||||
import asyncio
|
||||
import math
|
||||
import random
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from email.utils import parsedate_to_datetime
|
||||
from typing import Awaitable, Callable
|
||||
from urllib.parse import urljoin, urlparse
|
||||
@@ -41,6 +41,9 @@ class WqClient:
|
||||
self.auth_generation = 0
|
||||
self.credentials: tuple[str, str] | None = None
|
||||
self.verification_url: str | None = None
|
||||
self.permissions: list[str] | None = None
|
||||
self.session_expires_at: datetime | None = None
|
||||
self.session_duration: float | None = None
|
||||
self.sleep = sleep
|
||||
self.on_retry: Callable[[float], Awaitable[None]] | None = None
|
||||
|
||||
@@ -51,8 +54,47 @@ class WqClient:
|
||||
self.authenticated = False
|
||||
self.credentials = None
|
||||
self.verification_url = None
|
||||
self.permissions = None
|
||||
self.session_expires_at = None
|
||||
self.session_duration = None
|
||||
self.client.cookies.clear()
|
||||
|
||||
def capture_session(self, response: httpx.Response):
|
||||
"""Keep only permissions and expiry metadata; never retain the authentication body."""
|
||||
self.permissions, self.session_expires_at, self.session_duration = None, None, None
|
||||
try:
|
||||
data = response.json()
|
||||
except ValueError:
|
||||
return
|
||||
if not isinstance(data, dict):
|
||||
return
|
||||
permissions = data.get("permissions")
|
||||
if isinstance(permissions, list) and all(isinstance(p, str) for p in permissions):
|
||||
self.permissions = list(dict.fromkeys(permissions))
|
||||
token = data.get("token")
|
||||
expiry = token.get("expiry") if isinstance(token, dict) else None
|
||||
if (
|
||||
isinstance(expiry, (int, float))
|
||||
and not isinstance(expiry, bool)
|
||||
and math.isfinite(expiry)
|
||||
and 0 <= expiry <= 31536000
|
||||
):
|
||||
self.session_duration = expiry
|
||||
self.session_expires_at = datetime.now(timezone.utc) + timedelta(seconds=expiry)
|
||||
|
||||
def session_info(self):
|
||||
remaining = (
|
||||
max(0, int((self.session_expires_at - datetime.now(timezone.utc)).total_seconds()))
|
||||
if self.session_expires_at
|
||||
else None
|
||||
)
|
||||
return {
|
||||
"authenticated": self.authenticated and remaining != 0,
|
||||
"expires_at": self.session_expires_at,
|
||||
"remaining_seconds": remaining,
|
||||
"total_seconds": self.session_duration,
|
||||
}
|
||||
|
||||
def safe_verification_url(self, response: httpx.Response) -> str:
|
||||
url = urljoin(str(response.url), response.headers.get("Location", ""))
|
||||
base = urlparse(self.settings.wq_base_url)
|
||||
@@ -111,6 +153,7 @@ class WqClient:
|
||||
raise VerificationRequired(self.verification_url)
|
||||
self.credentials = (email, password)
|
||||
self.authenticated = False
|
||||
self.permissions, self.session_expires_at, self.session_duration = None, None, None
|
||||
if force:
|
||||
self.client.cookies.clear()
|
||||
self.verification_url = None
|
||||
@@ -126,6 +169,7 @@ class WqClient:
|
||||
if response.status_code not in (200, 201):
|
||||
raise WqError(f"平台认证返回异常状态 {response.status_code}", "authentication_failed")
|
||||
self.authenticated = True
|
||||
self.capture_session(response)
|
||||
self.auth_generation += 1
|
||||
self.verification_url = None
|
||||
|
||||
@@ -139,6 +183,7 @@ class WqClient:
|
||||
response = await self._request("POST", self.verification_url)
|
||||
if response.status_code in (200, 201):
|
||||
self.authenticated = True
|
||||
self.capture_session(response)
|
||||
self.auth_generation += 1
|
||||
self.verification_url = None
|
||||
elif response.status_code in (401, 403, 202):
|
||||
@@ -146,7 +191,7 @@ class WqClient:
|
||||
else:
|
||||
raise WqError("验证会话已失效,请重新连接", "authentication_failed")
|
||||
|
||||
async def get(self, path: str, params=None):
|
||||
async def get(self, path: str, params=None, headers=None):
|
||||
if not self.credentials:
|
||||
raise WqError("请先连接 WorldQuant", "disconnected")
|
||||
if not self.authenticated:
|
||||
@@ -154,7 +199,7 @@ class WqClient:
|
||||
refreshed = False
|
||||
for attempt in range(self.settings.retry_attempts):
|
||||
generation = self.auth_generation
|
||||
response = await self._request("GET", path, params=params)
|
||||
response = await self._request("GET", path, params=params, headers=headers)
|
||||
if response.status_code == 401 and not refreshed:
|
||||
await self.authenticate(*self.credentials, stale_generation=generation)
|
||||
refreshed = True
|
||||
@@ -189,6 +234,29 @@ class WqClient:
|
||||
async def profile(self):
|
||||
return await self.get("/users/self")
|
||||
|
||||
async def account_usage(self):
|
||||
"""Read independent account resources; unavailable sections do not hide the profile."""
|
||||
from .account_data import usage_snapshot
|
||||
|
||||
resources = {
|
||||
"simulations": "/users/self/activities/simulations",
|
||||
"submissions": "/users/self/activities/submissions",
|
||||
"alphas": "/users/self/alphas/summary",
|
||||
}
|
||||
data, errors = {}, {}
|
||||
for key, path in resources.items():
|
||||
try:
|
||||
data[key] = await self.get(
|
||||
path, headers={"Accept": "application/json;version=4.0"} if key == "alphas" else None
|
||||
)
|
||||
except VerificationRequired:
|
||||
raise
|
||||
except WqError as exc:
|
||||
if exc.code in ("authentication_failed", "disconnected"):
|
||||
raise
|
||||
errors[key] = str(exc)
|
||||
return usage_snapshot(data, errors)
|
||||
|
||||
async def alphas(self, submission, hidden, offset, before):
|
||||
# Cover every platform stage; submitted records are not assumed to be OS only.
|
||||
status_key = "status" if submission == "UNSUBMITTED" else "status!"
|
||||
|
||||
Reference in New Issue
Block a user