Files
stock/backend/app/data/sync_utils.py
2026-09-07 18:07:31 +08:00

88 lines
2.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""懒加载管道共用件tushare pro 单例、限频重试、取值清洗、stock_sync_state 读写。
finance / dividend 等按需单查管道共用;语义与 company.py 内的私有版本一致
(那处历史代码未迁移,新管道一律从这里取)。
"""
from __future__ import annotations
import time
from datetime import datetime, timedelta, timezone
from sqlalchemy import select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.ext.asyncio import AsyncSession
from ..config import settings
from ..models import StockSyncState
# 频率超限特征(等待 62s 重试一次;与 company.py / data.etf_sync._call_retry 同款语义)
_RATE_MARKS = ("频率超限", "每分钟")
_pro = None # 惰性单例get_pro 每次都 ts.set_token 写文件,没必要重复)
def get_pro_lazy():
if not settings.tushare_token:
raise RuntimeError("未配置 TUSHARE_TOKEN无法拉取 tushare 数据backend/.env")
global _pro
if _pro is None:
from .tushare_provider import get_pro
_pro = get_pro()
return _pro
def call_retry(fn, *args, **kwargs):
"""同步调用 tushare 接口;「每分钟」级频率超限等 62s 重试一次。"""
try:
return fn(*args, **kwargs)
except Exception as e: # noqa: BLE001
msg = str(e)
if any(m in msg for m in _RATE_MARKS) and "小时" not in msg:
time.sleep(62)
return fn(*args, **kwargs)
raise
def utcnow() -> datetime:
return datetime.now(timezone.utc).replace(tzinfo=None)
def fresh(updated_at: datetime | None, days: int) -> bool:
return updated_at is not None and updated_at >= utcnow() - timedelta(days=days)
def s_clean(v) -> str | None:
"""pandas NaN / 空串 / None -> None其余 strip。"""
if v is None or (isinstance(v, float) and v != v):
return None
s = str(v).strip()
return s or None
def f_clean(v) -> float | None:
try:
f = float(v)
except (TypeError, ValueError):
return None
return None if f != f else f # NaN -> None
async def read_sync_state(session: AsyncSession, ts_code: str, kind: str) -> StockSyncState | None:
return (await session.execute(
select(StockSyncState).where(
StockSyncState.ts_code == ts_code, StockSyncState.kind == kind
))).scalar_one_or_none()
async def upsert_sync_state(session: AsyncSession, ts_code: str, kind: str, *, has_data: bool) -> None:
"""写入同步状态(不 commit由调用方统一提交"""
stmt = pg_insert(StockSyncState).values(
ts_code=ts_code, kind=kind, last_synced_at=utcnow(), has_data=has_data,
)
stmt = stmt.on_conflict_do_update(
index_elements=["ts_code", "kind"],
set_={"last_synced_at": stmt.excluded.last_synced_at, "has_data": stmt.excluded.has_data},
)
await session.execute(stmt)