Files
AlphaPulse/backend/src/alphapulse/calendar/jin10_browser.py
T
eezd e840557d22 Add authenticated global economic calendar with Jin10 fallback
Collect macroeconomic releases, events and holidays through the official browser SDK with resumable history from 2025. Preserve evidence, revisions and observation times, and merge verified series with official fields taking priority.

Add category filters, field provenance, coverage and connection status. Verify calendar persistence and deployment recovery in isolated acceptance checks.
2026-10-31 12:57:14 +08:00

79 lines
4.1 KiB
Python

"""Use Jin10's own browser SDK rather than reproducing its encrypted transport."""
import asyncio
from datetime import date
from playwright.async_api import Error as BrowserError, async_playwright
from alphapulse.calendar.jin10 import KINDS, day_url, normalize
from alphapulse.calendar.jin10_auth import Jin10Error, uc_request
METHODS = {"data": "getCalendarData", "event": "getCalendarEvent", "holiday": "getCalendarHoliday"}
VALIDATION_DAY = date(2025, 1, 8)
def validate_access(responses: dict) -> None:
# This archived day has verified economic releases and events. An authenticated
# account alone does not prove that the SDK can read the economic calendar.
normalized = {kind: normalize(responses[kind], kind, VALIDATION_DAY) for kind in KINDS}
if not normalized["data"] or not normalized["event"]:
raise Jin10Error("金十历史日历读取权限未通过验证,请重新连接后检查账号权限")
class Jin10Browser:
def __init__(self) -> None:
self.playwright = self.browser = self.context = self.page = None
self.credential = None
self.lock = asyncio.Lock()
async def close(self) -> None:
if self.browser:
await self.browser.close()
if self.playwright:
await self.playwright.stop()
self.playwright = self.browser = self.context = self.page = None
self.credential = None
async def connect(self, credential: str) -> None:
await self.close()
info = await uc_request("/userinfo", token=credential)
if not isinstance(info.get("data"), dict) or not info["data"]:
raise Jin10Error("金十登录态已失效,请重新扫码")
try:
self.playwright = await async_playwright().start()
self.browser = await self.playwright.chromium.launch(headless=True, args=["--no-sandbox", "--disable-dev-shm-usage"])
self.context = await self.browser.new_context()
await self.context.add_cookies([{"name": "x-token", "value": credential, "domain": ".jin10.com",
"path": "/", "secure": True, "sameSite": "Lax"}])
self.page = await self.context.new_page()
# Keep the host page outside the collection window so its initial requests do not compete.
await self.page.goto(day_url(date(2024, 1, 1)), wait_until="domcontentloaded", timeout=30000)
await self.page.wait_for_function(
"window.Jin10FlashInstance && window.Jin10FlashInstance.socket && "
"window.Jin10FlashInstance.socket.readyState === 1", timeout=30000)
await self.page.wait_for_timeout(2000)
self.credential = credential
validate_access(await self.fetch_day(VALIDATION_DAY))
except BrowserError:
await self.close()
raise Jin10Error("金十浏览器连接失败,请检查网络与浏览器运行环境") from None
async def fetch_day(self, day: date) -> dict[str, dict]:
async with self.lock:
if self.page is None:
raise Jin10Error("金十浏览器尚未连接")
try:
result = {}
for kind in KINDS:
result[kind] = await self.page.evaluate(
"async ({method, day}) => {"
"if (!window.Jin10FlashInstance || window.Jin10FlashInstance.socket.readyState !== 1) throw Error('disconnected');"
"return await window.Jin10FlashInstance[method](day, {timeout: 15000});}",
{"method": METHODS[kind], "day": day.isoformat()})
info = await uc_request("/userinfo", token=self.credential)
if not isinstance(info.get("data"), dict) or not info["data"]:
raise Jin10Error("金十登录态已失效,未将空列表记为完整日历")
return result
except BrowserError:
# Browser exceptions may include URLs or script contents; never forward them to logs.
raise Jin10Error("金十日历请求失败或超时,未将本次响应记为成功") from None