提交
This commit is contained in:
87
backend/app/data/sync_utils.py
Normal file
87
backend/app/data/sync_utils.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""懒加载管道共用件: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)
|
||||
Reference in New Issue
Block a user